/*==========================================================================*\ | $Id: EditAssignmentPage.java,v 1.13 2012/05/16 14:10:42 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2011 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.grader; import com.webobjects.appserver.*; import com.webobjects.foundation.*; import er.extensions.appserver.ERXDisplayGroup; import java.net.URL; import java.net.MalformedURLException; import java.util.Map; import java.util.TimeZone; import java.util.TreeMap; import org.apache.log4j.Logger; import org.webcat.core.*; import org.webcat.ui.generators.JavascriptGenerator; import org.webcat.woextensions.ECAction; import static org.webcat.woextensions.ECAction.run; // ------------------------------------------------------------------------- /** * This class presents an assignment's properties so they can be edited. * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.13 $, $Date: 2012/05/16 14:10:42 $ */ public class EditAssignmentPage extends GraderAssignmentsComponent { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * This is the default constructor * * @param context The page's context */ public EditAssignmentPage( WOContext context ) { super( context ); } //~ KVC Attributes (must be public) ....................................... public ERXDisplayGroup<Step> scriptDisplayGroup; public int index; public Step thisStep; public ERXDisplayGroup<SubmissionProfile> submissionProfileDisplayGroup; public SubmissionProfile submissionProfile; public AssignmentOffering upcomingOffering; public AssignmentOffering thisOffering; public int thisOfferingIndex; public Assignment assignment; public AssignmentOffering selectedOffering; public ERXDisplayGroup<AssignmentOffering> offeringGroup; public NSArray<GradingPlugin> gradingPluginsToAdd; public GradingPlugin gradingPluginToAdd; //~ Methods ............................................................... // ---------------------------------------------------------- protected void beforeAppendToResponse( WOResponse response, WOContext context) { timeStart = System.currentTimeMillis(); log.debug("starting appendToResponse()"); currentTime = new NSTimestamp(); // Get all the available grading plugins. gradingPluginsToAdd = GradingPlugin.pluginsAvailableToUser( localContext(), user()); offeringGroup.setObjectArray(assignmentOfferings(courseOfferings())); if (selectedOffering == null) { if (offeringGroup.displayedObjects().count() > 0) { selectedOffering = offeringGroup.displayedObjects() .objectAtIndex(0); } else { selectedOffering = prefs().assignmentOffering(); } } if (assignment == null) { assignment = prefs().assignment(); if (assignment == null && selectedOffering != null) { assignment = selectedOffering.assignment(); } } scriptDisplayGroup.setMasterObject(assignment); // TODO: Fix NPEs on this page when no selectedOffering if (selectedOffering != null) { submissionProfileDisplayGroup.setObjectArray( SubmissionProfile.profilesForCourseIncludingMine( localContext(), user(), selectedOffering.courseOffering().course(), assignment.submissionProfile() ) ); } log.debug("starting super.appendToResponse()"); areDueDatesLocked = areDueDatesSame(); super.beforeAppendToResponse(response, context); } // ---------------------------------------------------------- protected void afterAppendToResponse(WOResponse response, WOContext context) { super.afterAppendToResponse(response, context); log.debug( "finishing super.appendToResponse()" ); log.debug( "finishing appendToResponse()" ); long timeTaken = System.currentTimeMillis() - timeStart; log.debug("Time in appendToResponse(): " + timeTaken + " ms"); } // ---------------------------------------------------------- public boolean validateURL( String assignUrl ) { boolean result = ( assignUrl == null ); if ( assignUrl != null ) { try { // Try to create an instance of URL, which will // throw an exception if the name isn't valid result = ( new URL( assignUrl ) != null ); } catch ( MalformedURLException e ) { error( "The specified URL is not valid." ); log.error( "Error in validateURL()", e ); } } log.debug( "url validation = " + result ); return result; } // ---------------------------------------------------------- public boolean allowsAllOfferingsForCourse() { return true; } // ---------------------------------------------------------- public boolean requiresAssignmentOffering() { // Want to show all offerings for this assignment. return false; } // ---------------------------------------------------------- public void flushNavigatorDerivedData() { selectedOffering = null; assignment = null; } // ---------------------------------------------------------- /* Checks for errors, then records the current selections. * * @returns false if there is an error message to display. */ protected boolean saveAndCanProceed() { return saveAndCanProceed( true ); } // ---------------------------------------------------------- /* Checks for errors, then records the current selections. * * @param requireProfile if true, a missing submission profile will * trigger a false result * @returns false if there is an error message to display. */ protected boolean saveAndCanProceed(boolean requireProfile) { if (requireProfile && assignment.submissionProfile() == null) { error("please select submission rules for this assignment."); } return validateURL(assignment.url()) && !hasMessages(); } // ---------------------------------------------------------- public WOComponent next() { if (saveAndCanProceed()) { return super.next(); } else { return null; } } // ---------------------------------------------------------- public boolean applyEnabled() { return hasSubmissionProfile(); } // ---------------------------------------------------------- public boolean finishEnabled() { return applyEnabled(); } // ---------------------------------------------------------- public boolean applyLocalChanges() { return saveAndCanProceed() && super.applyLocalChanges(); } // ---------------------------------------------------------- public boolean hasSubmissionProfile() { return null != assignment.submissionProfile(); } // ---------------------------------------------------------- public WOComponent editSubmissionProfile() { WCComponent result = null; if ( saveAndCanProceed() ) { // result = (WCComponent)pageWithName( // SelectSubmissionProfile.class.getName()); result = pageWithName(EditSubmissionProfilePage.class); result.nextPage = this; } return result; } // ---------------------------------------------------------- public WOComponent newSubmissionProfile() { WCComponent result = null; clearAllMessages(); if (saveAndCanProceed(false)) { SubmissionProfile newProfile = new SubmissionProfile(); localContext().insertObject(newProfile); assignment.setSubmissionProfileRelationship(newProfile); newProfile.setAuthor(user()); result = pageWithName(EditSubmissionProfilePage.class); result.nextPage = this; } return result; } // ---------------------------------------------------------- public boolean canDeleteOffering(AssignmentOffering offering) { return !offering.isNewObject() && !offering.hasStudentSubmissions(); } // ---------------------------------------------------------- public boolean canDeleteThisOffering() { return canDeleteOffering(thisOffering); } // ---------------------------------------------------------- public boolean canDeleteAnyOffering() { for (AssignmentOffering offering : offeringGroup.displayedObjects()) { if (canDeleteOffering(offering)) { return true; } } return false; } // ---------------------------------------------------------- public boolean shouldShowDueDatePicker() { return !areDueDatesLocked || (offeringGroup.displayedObjects().count() > 0 && thisOffering.equals( offeringGroup.displayedObjects().objectAtIndex(0))); } // ---------------------------------------------------------- public boolean areDueDatesLocked() { return areDueDatesLocked; } // ---------------------------------------------------------- public boolean areDueDatesSame() { NSTimestamp exemplar = null; for (AssignmentOffering offering : offeringGroup.displayedObjects()) { if (exemplar == null) { exemplar = offering.dueDate(); } else { if (offering.dueDate() == null || !exemplar.equals(offering.dueDate())) { return false; } } } return true; } // ---------------------------------------------------------- public WOActionResults lockDueDates() { if (saveAndCanProceed()) { areDueDatesLocked = true; NSTimestamp exemplar = null; for (AssignmentOffering offering : offeringGroup.displayedObjects()) { if (exemplar == null) { exemplar = offering.dueDate(); } else { offering.setDueDate(exemplar); } } applyLocalChanges(); } return new JavascriptGenerator() .refresh("allOfferings", "error-panel"); } // ---------------------------------------------------------- public WOActionResults unlockDueDates() { if (saveAndCanProceed()) { areDueDatesLocked = false; applyLocalChanges(); } return new JavascriptGenerator() .refresh("allOfferings", "error-panel"); } // ---------------------------------------------------------- public NSTimestamp dueDate() { return thisOffering.dueDate(); } // ---------------------------------------------------------- public void setDueDate(NSTimestamp value) { if (areDueDatesLocked) { for (AssignmentOffering offering : offeringGroup.displayedObjects()) { offering.setDueDate(value); } } else { thisOffering.setDueDate(value); } } // ---------------------------------------------------------- public boolean isPublished() { boolean published = true; for (AssignmentOffering offering : offeringGroup.displayedObjects()) { if (!offering.publish()) { published = false; break; } } return published; } // ---------------------------------------------------------- public WOActionResults togglePublished() { return updatePublishedAction(!isPublished()); } // ---------------------------------------------------------- private WOActionResults updatePublishedAction(boolean value) { if (saveAndCanProceed()) { for (AssignmentOffering offering : offeringGroup.displayedObjects()) { offering.setPublish(value); } applyLocalChanges(); } return new JavascriptGenerator() .refresh("allOfferingsActions", "error-panel"); } // ---------------------------------------------------------- public String iconForPublishedListItem() { return "icons/" + (isPublished() ? "eye.png" : "eye-half.png"); } // ---------------------------------------------------------- public boolean hasSuspendedSubs() { suspendedSubmissionCount = 0; for (AssignmentOffering offering : offeringGroup.displayedObjects()) { suspendedSubmissionCount += offering.suspendedSubmissionsInQueue().count(); } return suspendedSubmissionCount > 0; } // ---------------------------------------------------------- public int numSuspendedSubs() { return suspendedSubmissionCount; } // ---------------------------------------------------------- public String descriptionOfEnqueuedSubmissions() { int active = thisOffering.activeSubmissionsInQueue().count(); int suspended = thisOffering.suspendedSubmissionsInQueue().count(); StringBuffer buffer = new StringBuffer(); if (active == 0 && suspended == 0) { return "No submissions in queue"; } else { if (active > 0 && suspended > 0) { buffer.append(Integer.toString(active)); buffer.append(" <span class=\"check\">active</span>, "); buffer.append(Integer.toString(suspended)); buffer.append(" <span class=\"warn\">suspended</span>"); } else if (active > 0) { buffer.append(Integer.toString(active)); buffer.append(" <span class=\"check\">active</span>"); } else { buffer.append(Integer.toString(suspended)); buffer.append(" <span class=\"warn\">suspended</span>"); } } buffer.append(" submission"); if (active + suspended > 1) { buffer.append('s'); } buffer.append(" in queue"); return buffer.toString(); } // ---------------------------------------------------------- public WOActionResults releaseSuspendedSubs() { log.info("releasing all paused assignments: " + assignment.titleString()); run(new ECAction() { public void action() { for (AssignmentOffering offering : offeringGroup.displayedObjects()) { AssignmentOffering localAO = offering.localInstance(ec); NSArray<EnqueuedJob> jobList = localAO.suspendedSubmissionsInQueue(); for (EnqueuedJob job : jobList) { job.setPaused(false); job.setQueueTime(new NSTimestamp()); } log.info("released " + jobList.count() + " jobs"); } ec.saveChanges(); }}); // trigger the grading queue to read the released jobs Grader.getInstance().graderQueue().enqueue(null); return new JavascriptGenerator().refresh( "allOfferings", "allOfferingsActions", "error-panel"); } // ---------------------------------------------------------- public WOActionResults cancelSuspendedSubs() { run(new ECAction() { public void action() { for (AssignmentOffering offering : offeringGroup.displayedObjects()) { AssignmentOffering localAO = offering.localInstance(ec); log.info( "cancelling all paused assignments: " + coreSelections().courseOffering().course().deptNumber() + " " + localAO.assignment().name()); NSArray<EnqueuedJob> jobList = localAO.suspendedSubmissionsInQueue(); for (EnqueuedJob job : jobList) { ec.deleteObject(job); } log.info("cancelled " + jobList.count() + " jobs"); } ec.saveChanges(); }}); return new JavascriptGenerator().refresh( "allOfferings", "allOfferingsActions", "error-panel"); } // ---------------------------------------------------------- public WOActionResults toggleSuspended() { return updateSuspendedAction(!isSuspended()); } // ---------------------------------------------------------- private WOActionResults updateSuspendedAction(boolean value) { if (saveAndCanProceed()) { for (AssignmentOffering offering : offeringGroup.displayedObjects()) { offering.setGradingSuspended(value); } if (applyLocalChanges() && !value) { releaseSuspendedSubs(); } } return new JavascriptGenerator().refresh( "allOfferings", "allOfferingsActions", "error-panel"); } // ---------------------------------------------------------- public boolean isSuspended() { boolean suspended = false; for (AssignmentOffering offering : offeringGroup.displayedObjects()) { if (offering.gradingSuspended()) { suspended = true; break; } } return suspended; } // ---------------------------------------------------------- public String iconForSuspendedListItem() { return "icons/" + (isSuspended() ? "robot-off.png" : "robot.png"); } // ---------------------------------------------------------- public JavascriptGenerator addStep() { if (gradingPluginToAdd != null) { if (saveAndCanProceed()) { assignment.addNewStep(gradingPluginToAdd); applyLocalChanges(); scriptDisplayGroup.fetch(); } } return new JavascriptGenerator() .refresh("gradingSteps", "error-panel") .unblock("gradingStepsTable"); } // ---------------------------------------------------------- public WOActionResults removeStepActionOK() { int pos = stepForAction.order(); for (int i = pos; i < scriptDisplayGroup.displayedObjects().count(); i++) { scriptDisplayGroup.displayedObjects().objectAtIndex(i).setOrder(i); } if (stepForAction.config() != null && stepForAction.config().steps().count() == 1) { StepConfig thisConfig = stepForAction.config(); stepForAction.setConfigRelationship(null); localContext().deleteObject(thisConfig); } localContext().deleteObject(stepForAction); localContext().saveChanges(); stepForAction = null; scriptDisplayGroup.fetch(); return null; //FIXME new JavascriptGenerator().refresh("gradingSteps"); } // ---------------------------------------------------------- public WOActionResults removeStep() { if (!saveAndCanProceed()) { return displayMessages(); } stepForAction = thisStep; return new ConfirmingAction(this, false) { @Override protected String confirmationTitle() { return "Remove This Grading Step?"; } @Override protected String confirmationMessage() { return "<p>Are you sure that you want to remove the grading" + " step <b>" + stepForAction.gradingPlugin().name() + "</b>? All of the configuration settings for this step" + "will be lost. This cannot be undone.</p>"; } @Override protected WOActionResults actionWasConfirmed() { return removeStepActionOK(); } }; } // ---------------------------------------------------------- public boolean stepAllowsTimeout() { return thisStep.gradingPlugin().timeoutMultiplier() != 0; } // ---------------------------------------------------------- public WOComponent editStep() { WCComponent result = null; if ( saveAndCanProceed() ) { log.debug( "step = " + thisStep ); prefs().setAssignmentOfferingRelationship(selectedOffering); prefs().setStepRelationship( thisStep ); result = pageWithName(EditStepPage.class); result.nextPage = this; } return result; } // ---------------------------------------------------------- public void gradingStepsWereDropped( String sourceId, int[] dragIndices, String targetId, int[] dropIndices, boolean isCopy) { NSMutableArray<Step> steps = scriptDisplayGroup.displayedObjects().mutableClone(); NSMutableArray<Step> stepsRemoved = new NSMutableArray<Step>(); TreeMap<Integer, Step> finalStepPositions = new TreeMap<Integer, Step>(); for (int i = 0; i < dragIndices.length; i++) { Step step = steps.objectAtIndex(dragIndices[i]); finalStepPositions.put(dropIndices[i], step); stepsRemoved.addObject(step); } steps.removeObjectsInArray(stepsRemoved); for (Map.Entry<Integer, Step> movement : finalStepPositions.entrySet()) { int dropIndex = movement.getKey(); Step step = movement.getValue(); steps.insertObjectAtIndex(step, dropIndex); } int order = 1; for (Step step : steps) { step.setOrder(order++); } scriptDisplayGroup.fetch(); } // ---------------------------------------------------------- public Integer stepTimeout() { log.debug( "step = " + thisStep ); log.debug( "num steps = " + scriptDisplayGroup.displayedObjects().count() ); return thisStep.timeoutRaw(); } // ---------------------------------------------------------- public void setStepTimeout( Integer value ) { if ( value != null && !Step.timeoutIsWithinLimits( value ) ) { // set error message if timeout is out of range error( "The maximum timeout allowed is " + Step.maxTimeout() + ". Contact the administrator for higher limits." ); } // This will automatically restrict to the max value anyway thisStep.setTimeoutRaw( value ); } // ---------------------------------------------------------- public JavascriptGenerator clearGraph() { JavascriptGenerator js = new JavascriptGenerator(); for (AssignmentOffering offering : offeringGroup.displayedObjects()) { offering.clearGraphSummary(); js.refresh("scoreHistogram" + offering.id()); } applyLocalChanges(); return js; } // ---------------------------------------------------------- public void takeValuesFromRequest(WORequest arg0, WOContext arg1) { log.debug("takeValuesFromRequest()"); long timeStartedHere = System.currentTimeMillis(); super.takeValuesFromRequest(arg0, arg1); if (assignment != null) { log.debug("looking for similar submission profile by name"); String name = assignment.name(); if ( assignment.submissionProfile() == null && name != null ) { NSArray<AssignmentOffering> similar = AssignmentOffering .offeringsWithSimilarNames( localContext(), name, selectedOffering.courseOffering(), 1); if (similar.count() > 0) { AssignmentOffering ao = similar.objectAtIndex(0); assignment.setSubmissionProfile( ao.assignment().submissionProfile()); } } } long timeTaken = System.currentTimeMillis() - timeStartedHere; log.debug("Time in takeValuesFromRequest(): " + timeTaken + " ms"); } // ---------------------------------------------------------- private WOComponent flush(WOComponent page) { flushNavigatorDerivedData(); return page; } // ---------------------------------------------------------- private WOComponent deleteOfferingActionOk() { prefs().setAssignmentOfferingRelationship(null); for (AssignmentOffering ao : offeringGroup.displayedObjects()) { if (ao != offeringForAction) { prefs().setAssignmentOfferingRelationship(ao); break; } } if (!applyLocalChanges()) return flush(null); localContext().deleteObject(offeringForAction); if (!applyLocalChanges()) return flush(null); if (assignment.offerings().count() == 0) { prefs().setAssignmentOfferingRelationship(null); prefs().setAssignmentRelationship(null); localContext().deleteObject(assignment); applyLocalChanges(); } return flush(null); } // ---------------------------------------------------------- public WOActionResults deleteOffering() { if (!applyLocalChanges()) { return displayMessages(); } offeringForAction = thisOffering; return new ConfirmingAction(this, false) { @Override protected String confirmationTitle() { return "Delete This Assignment Offering?"; } @Override protected String confirmationMessage() { String message = "<p>This action will <b>delete the assignment offering \"" + offeringForAction + "\"</b>, " + "together with any staff submissions that have been " + "made to it.</p>"; if (offeringForAction.assignment().offerings().count() == 1) { message += "<p>Since this is the only offering of the selected " + "assignment, this action will also <b>delete the " + "assignment altogether</b>. This action cannot be " + "undone.</p>"; } return message + "<p class=\"center\">Delete this " + "assignment offering?</p>"; } @Override protected WOActionResults actionWasConfirmed() { return deleteOfferingActionOk(); } }; } // ---------------------------------------------------------- /** * Check whether the selected assignment is past the due date. * * @return true if any submissions to this assignment will be counted * as late */ public boolean upcomingOfferingIsLate() { NSTimestamp dueDate = upcomingOffering.dueDate(); if (dueDate != null) { return dueDate.before(currentTime); } else { // FIXME is this the best answer? return true; } } // ---------------------------------------------------------- public NSArray<AssignmentOffering> upcomingOfferings() { if (upcomingOfferings == null) { upcomingOfferings = AssignmentOffering.allOfferingsOrderedByDueDate( localContext()).mutableClone(); upcomingOfferings.removeObject(selectedOffering); } return upcomingOfferings; } // ---------------------------------------------------------- public TimeZone timeZone() { return TimeZone.getTimeZone(user().timeZoneName()); } // ---------------------------------------------------------- public Boolean surveysSupported() { if (surveysSupported == null) { surveysSupported = Boolean.valueOf( wcApplication().subsystemManager().subsystem("Opinions") != null); } return surveysSupported.booleanValue(); } //~ Instance/static variables ............................................. private int suspendedSubmissionCount = 0; private NSMutableArray<AssignmentOffering> upcomingOfferings; private NSTimestamp currentTime; private AssignmentOffering offeringForAction; private Step stepForAction; private boolean areDueDatesLocked; private long timeStart; private static Boolean surveysSupported; static Logger log = Logger.getLogger( EditAssignmentPage.class ); }