/*==========================================================================*\ | $Id: StudentsForAssignmentPage.java,v 1.22 2012/01/20 21:23:59 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2012 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 java.util.HashMap; import java.util.Map; import com.webobjects.appserver.*; import com.webobjects.foundation.*; import er.extensions.appserver.ERXDisplayGroup; import er.extensions.foundation.ERXArrayUtilities; import org.apache.log4j.Logger; import org.webcat.core.*; import org.webcat.ui.WCTable; import org.webcat.ui.generators.JavascriptFunction; import org.webcat.ui.generators.JavascriptGenerator; import org.webcat.ui.util.ComponentIDGenerator; // ------------------------------------------------------------------------- /** * Show an overview of class grades for an assignment, and allow the user * to download them in spreadsheet form or edit them one at a time. * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.22 $, $Date: 2012/01/20 21:23:59 $ */ public class StudentsForAssignmentPage extends GraderAssignmentsComponent { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Default constructor. * @param context The page's context */ public StudentsForAssignmentPage(WOContext context) { super(context); staffSubmissionGroup = new ERXDisplayGroup<Submission>(); staffSubmissionGroup.setNumberOfObjectsPerBatch(100); staffSubmissionGroup.setSortOrderings( Submission.user.dot(User.name_LF).ascInsensitives().then( Submission.user.dot(User.userName).ascInsensitive()) ); offerings = new ERXDisplayGroup<AssignmentOffering>(); offerings.setSortOrderings( AssignmentOffering.titleString.ascInsensitives()); studentNewerSubmissions = new ERXDisplayGroup<Submission>(); studentNewerSubmissions.setDetailKey("allSubmissions"); } //~ KVC Attributes (must be public) ....................................... /** Submission in the worepetition */ public UserSubmissionPair aUserSubmission; public Submission aSubmission; public Submission partnerSubmission; public UserSubmissionPair selectedUserSubmissionForPickerDialog; public NSArray<UserSubmissionPair> allUserSubmissionsForNavigationForPickerDialog; /** index in the student worepetition */ public int index; public ERXDisplayGroup<Submission> staffSubmissionGroup; /** index in the staff worepetition */ public int staffIndex; public ERXDisplayGroup<AssignmentOffering> offerings; public AssignmentOffering assignmentOffering; public Submission aNewerSubmission; /** Value of the corresponding checkbox on the page. */ public boolean omitStaff = true; public boolean useBlackboardFormat = true; public ComponentIDGenerator idFor = new ComponentIDGenerator(this); //~ Methods ............................................................... // ---------------------------------------------------------- protected void beforeAppendToResponse( WOResponse response, WOContext context) { log.debug("appendToResponse()"); subStats = new HashMap<AssignmentOffering, Submission.CumulativeStats>(); offerings.setObjectArray(assignmentOfferings(courseOfferings())); if (log.isDebugEnabled()) { log.debug("assignment offerings:"); for (AssignmentOffering ao : offerings.allObjects()) { log.debug("\t" + ao); } } NSMutableArray<Submission> staffSubs = new NSMutableArray<Submission>(); NSArray<User> admins = User.administrators(localContext()); for (AssignmentOffering ao : offerings.displayedObjects()) { // Stuff the index variable into the public key so the group/stats // methods will work for us assignmentOffering = ao; NSArray<UserSubmissionPair> subs = Submission.submissionsForGrading( localContext(), ao, true, // omitPartners omitStaff, studentStats()); userGroup().setObjectArray(subs); @SuppressWarnings("unchecked") NSArray<User> staff = ERXArrayUtilities .arrayByAddingObjectsFromArrayWithoutDuplicates( ao.courseOffering().staff(), admins); staffSubs.addAll(extractSubmissions( Submission.submissionsForGrading( localContext(), ao, true, // omitPartners staff, null))); } staffSubmissionGroup.setObjectArray(staffSubs); selectedUserSubmissionForPickerDialog = null; allUserSubmissionsForNavigationForPickerDialog = null; super.beforeAppendToResponse(response, context); } // ---------------------------------------------------------- private NSArray<Submission> extractSubmissions( NSArray<UserSubmissionPair> userSubs) { NSMutableArray<Submission> submissions = new NSMutableArray<Submission>(); for (UserSubmissionPair pair : userSubs) { if (pair.userHasSubmission()) { submissions.addObject(pair.submission()); } } return submissions; } // ---------------------------------------------------------- public void setAUserSubmission(UserSubmissionPair pair) { aUserSubmission = pair; aSubmission = (pair != null ? pair.submission() : null); } // ---------------------------------------------------------- public WOActionResults pickOtherSubmission() { selectedUserSubmissionForPickerDialog = aUserSubmission; allUserSubmissionsForNavigationForPickerDialog = userGroup().displayedObjects(); JavascriptGenerator js = new JavascriptGenerator(); js.dijit("pickSubmissionDialog").call("show"); return js; } // ---------------------------------------------------------- public WCComponent self() { return this; } // ---------------------------------------------------------- public String tableId() { return idFor.get("submissionsTable_" + assignmentOffering.id()); } // ---------------------------------------------------------- public WOActionResults regradeSubmissions() { return new ConfirmingAction(this, false) { @Override protected String confirmationTitle() { return "Regrade Everyone's Submission?"; } @Override protected String confirmationMessage() { return "<p>This action will <b>regrade the most recent " + "submission for every student</b> who has submitted to " + "this assignment.</p><p>This will also <b>delete all " + "prior results</b> for the submissions to be regraded " + "and <b>delete all TA comments and scoring</b> that " + "have been recorded for the submissions to be regraded." + "</p><p>Each student\'s most recent submission will be " + "re-queued for grading, and each student will receive " + "an e-mail message when their new results are " + "available.</p><p class=\"center\">Regrade everyone's " + "most recent submission?</p>"; } @Override protected WOActionResults actionWasConfirmed() { for (AssignmentOffering offering : assignmentOfferings(courseOfferings())) { offering.regradeMostRecentSubsForAll(localContext()); } applyLocalChanges(); return null; } }; } // ---------------------------------------------------------- public WOComponent editSubmissionScore() { WCComponent destination = null; if (!hasMessages()) { if (aSubmission == null) { log.error("editSubmissionScore(): null submission!"); } else if (aSubmission.result() == null) { log.error("editSubmissionScore(): null submission result!"); log.error("student = " + aSubmission.user().userName()); } prefs().setSubmissionRelationship(aSubmission); destination = (WCComponent) super.next(); if (destination instanceof GradeStudentSubmissionPage) { GradeStudentSubmissionPage page = (GradeStudentSubmissionPage) destination; if (aUserSubmission != null) { page.availableSubmissions = userGroup().displayedObjects().immutableClone(); page.thisSubmissionIndex = page.availableSubmissions.indexOf(aUserSubmission); } } destination.nextPage = this; } return destination; } // ---------------------------------------------------------- public WOComponent editNewerSubmissionScore() { WCComponent destination = null; if (!hasMessages()) { if (aNewerSubmission == null) { log.error("editNewerSubmissionScore(): null submission!"); } else if (aNewerSubmission.result() == null) { log.error("editNewerSubmissionScore(): null submission result!"); log.error("student = " + aNewerSubmission.user().userName()); } prefs().setSubmissionRelationship(aNewerSubmission); // destination = pageWithName(GradeStudentSubmissionPage.class); destination = (WCComponent) super.next(); if (destination instanceof GradeStudentSubmissionPage) { GradeStudentSubmissionPage page = (GradeStudentSubmissionPage)destination; if (aUserSubmission != null) { page.availableSubmissions = userGroup().displayedObjects().immutableClone(); page.thisSubmissionIndex = page.availableSubmissions.indexOf(aUserSubmission); } } destination.nextPage = this; } return destination; } // ---------------------------------------------------------- public String markCompleteStatusIndicatorId() { return idFor.get("markCompleteStatusIndicator_" + assignmentOffering.id()); } // ---------------------------------------------------------- /** * Marks all the submissions shown that have been partially graded as * being completed, sending e-mail notifications as necessary. * @return null to force this page to reload */ public int markSubmissionsAsComplete() { int numberNotified = 0; assignmentOffering = offeringForAction; for (UserSubmissionPair pair : userGroup().allObjects()) { if (pair.userHasSubmission()) { Submission sub = pair.submission(); if (sub.result().status() == Status.UNFINISHED || (sub.result().status() != Status.CHECK && !sub.assignmentOffering().assignment() .usesTAScore())) { sub.result().setStatus(Status.CHECK); if (applyLocalChanges()) { numberNotified++; sub.emailNotificationToStudent( "has been updated by the course staff"); } } } } return numberNotified; } // ---------------------------------------------------------- /** * Marks all the submissions shown that have been partially graded as * being completed, sending e-mail notifications as necessary. * @return null to force this page to reload */ public WOActionResults markAsComplete() { offeringForAction = assignmentOffering; return new ConfirmingAction(this, true) { @Override protected String confirmationTitle() { return "Confirm Grading Is Complete?"; } @Override protected String confirmationMessage() { return "<p>You are about to mark all <b>partially graded</b> " + "submissions as now complete so that students can see " + "their feedback from you. Submissions that have " + "no remarks or manual scoring information will not be " + "affected. All students who are affected will receive " + "an e-mail notification.</p><p class=\"center\">" + "Mark partially graded submissions as complete?</p>"; } @Override protected void beforeActionWasConfirmed(JavascriptGenerator js) { InlineStatusIndicator.updateWithSpinner(js, markCompleteStatusIndicatorId(), "Notifying students that their scores are ready..."); } @Override protected WOActionResults actionWasConfirmed() { final int numberNotified = markSubmissionsAsComplete(); JavascriptGenerator js = new JavascriptGenerator(); WCTable.refresh(js, tableId(), new JavascriptFunction() { @Override public void generate(JavascriptGenerator g) { String students; if (numberNotified == 1) { students = "1 student was"; } else { students = "" + numberNotified + " students were"; } InlineStatusIndicator.updateWithState(g, markCompleteStatusIndicatorId(), InlineStatusIndicator.SUCCESS, students + " notified."); } }); return js; } }; } // ---------------------------------------------------------- public ERXDisplayGroup<Submission> studentNewerSubmissions() { if (studentNewerSubmissions.masterObject() != aSubmission) { studentNewerSubmissions.setMasterObject(aSubmission); studentNewerSubmissions.setObjectArray( aSubmission.allSubmissions()); studentNewerSubmissions.queryMin().takeValueForKey( aSubmission.submitNumber() + 1, Submission.SUBMIT_NUMBER_KEY); studentNewerSubmissions.setQualifier( studentNewerSubmissions.qualifierFromQueryValues()); } return studentNewerSubmissions; } // ---------------------------------------------------------- public boolean hasTAScore() { return aSubmission.result().taScoreRaw() != null; } // ---------------------------------------------------------- public boolean isMostRecentSubmission() { return aSubmission == aSubmission.latestSubmission(); } // ---------------------------------------------------------- public int mostRecentSubmissionNo() { return aSubmission.latestSubmission().submitNumber(); } // ---------------------------------------------------------- public String submitTimeSpanClass() { if (aSubmission.isLate()) { return "warn"; } else { return null; } } // ---------------------------------------------------------- public String newerSubmitTimeSpanClass() { if (aNewerSubmission.isLate()) { return "warn sm"; } else { return "sm"; } } // ---------------------------------------------------------- public void flushNavigatorDerivedData() { assignmentOffering = null; super.flushNavigatorDerivedData(); } // ---------------------------------------------------------- public WOComponent repartner() { for (UserSubmissionPair pair : userGroup().allObjects()) { Submission sub = pair.submission(); if (sub != null && sub.result() != null) { for (Submission psub : sub.result().submissions()) { if (psub != sub && psub.assignmentOffering().assignment() != sub.assignmentOffering().assignment()) { log.warn("found partner submission " + psub.user() + " #" + psub.submitNumber() + "\non incorrect assignment offering " + psub.assignmentOffering()); NSArray<AssignmentOffering> partnerOfferings = AssignmentOffering.objectsMatchingQualifier( localContext(), AssignmentOffering.courseOffering .dot(CourseOffering.course).eq( sub.assignmentOffering() .courseOffering().course()) .and(AssignmentOffering.courseOffering .dot(CourseOffering.students).eq( psub.user())) .and(AssignmentOffering.assignment .eq(sub.assignmentOffering().assignment()))); if (partnerOfferings.count() == 0) { log.error("Cannot locate correct assignment " + "offering for partner" + psub.user() + " #" + psub.submitNumber() + "\non incorrect assignment offering " + psub.assignmentOffering()); } else { if (partnerOfferings.count() > 1) { log.warn("Multiple possible offerings for " + "partner " + psub.user() + " #" + psub.submitNumber() + "\non incorrect assignment offering " + psub.assignmentOffering()); for (AssignmentOffering ao : partnerOfferings) { log.warn("\t" + ao); } } psub.setAssignmentOfferingRelationship( partnerOfferings.get(0)); } } } } } applyLocalChanges(); return null; } // ---------------------------------------------------------- public Submission.CumulativeStats studentStats() { Submission.CumulativeStats stats = subStats.get(assignmentOffering); if (stats == null) { stats = new Submission.CumulativeStats(); subStats.put(assignmentOffering, stats); } return stats; } // ---------------------------------------------------------- public ERXDisplayGroup<UserSubmissionPair> userGroup() { ERXDisplayGroup<UserSubmissionPair> group = userGroups.get(assignmentOffering); if (group == null) { group = new ERXDisplayGroup<UserSubmissionPair>(); group.setNumberOfObjectsPerBatch(100); group.setSortOrderings( UserSubmissionPair.user.dot(User.name_LF).ascInsensitives().then( UserSubmissionPair.user.dot(User.userName).ascInsensitive()) ); userGroups.put(assignmentOffering, group); } return group; } // ---------------------------------------------------------- public String newerSubmissionStatus() { String result = "feedback entered on earlier submission"; if (aNewerSubmission.result() == null) { result = "suspended"; EnqueuedJob job = aNewerSubmission.enqueuedJob(); if (job == null) { result = "cancelled"; } else if (!job.paused()) { result = "queued for grading"; } } // check date of submission against date of feedback else if (aSubmission.result() != null && aSubmission.result().lastUpdated() != null && aNewerSubmission.submitTime().after( aSubmission.result().lastUpdated())) { result = "newer than feedback"; } if (log.isDebugEnabled()) { log.debug("newerSubmissionStatus() for " + aNewerSubmission + " = " + result); if (aSubmission.result() != null && aSubmission.result().lastUpdated() != null) { log.debug(" selected submission last updated: " + aSubmission.result().lastUpdated()); } log.debug(" newer submission on: " + aNewerSubmission.submitTime()); } return result; } //~ Instance/static variables ............................................. private Map<AssignmentOffering, ERXDisplayGroup<UserSubmissionPair>> userGroups = new HashMap<AssignmentOffering, ERXDisplayGroup<UserSubmissionPair>>(); private Map<AssignmentOffering, Submission.CumulativeStats> subStats; private AssignmentOffering offeringForAction; private ERXDisplayGroup<Submission> studentNewerSubmissions; static Logger log = Logger.getLogger(StudentsForAssignmentPage.class); }