/*==========================================================================*\ | $Id: AssignmentOffering.java,v 1.13 2012/05/09 16:20:01 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 com.webobjects.eocontrol.*; import com.webobjects.foundation.*; import er.extensions.eof.ERXKey; import er.extensions.eof.ERXQ; import er.extensions.eof.qualifiers.ERXInQualifier; import er.extensions.foundation.ERXArrayUtilities; import er.extensions.foundation.ERXValueUtilities; import java.io.File; import java.util.Map; import java.util.HashMap; import org.apache.log4j.Logger; import org.webcat.core.*; import org.webcat.grader.graphs.*; import org.webcat.woextensions.MigratingEditingContext; // ------------------------------------------------------------------------- /** * Represents the binding between an assignment and a course offering * (i.e., giving a specific assignment in a given section of a course). * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.13 $, $Date: 2012/05/09 16:20:01 $ */ public class AssignmentOffering extends _AssignmentOffering { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * Creates a new AssignmentOffering object. */ public AssignmentOffering() { super(); } // ---------------------------------------------------------- /** * A static factory method for creating a new * AssignmentOffering object given required * attributes and relationships. * @param editingContext The context in which the new object will be * inserted * @param forAssignment The assignment to be offered * @param forCourseOffering The course offering for this assignment offering * @return The newly created object */ public static AssignmentOffering create( EOEditingContext editingContext, Assignment forAssignment, CourseOffering forCourseOffering) { AssignmentOffering result = create(editingContext, false, false, false); result.setAssignmentRelationship(forAssignment); result.setCourseOfferingRelationship(forCourseOffering); return result; } //~ Constants (for key names) ............................................. // Derived Attributes --- public static final String AVAILABLE_FROM_KEY = "availableFrom"; public static final String LATE_DEADLINE_KEY = "lateDeadline"; public static final String ASSIGNMENT_NAME_KEY = ASSIGNMENT_KEY + "." + Assignment.NAME_KEY; public static final String COURSE_OFFERING_STUDENTS_KEY = COURSE_OFFERING_KEY + "." + CourseOffering.STUDENTS_KEY; public static final String COURSE_OFFERING_INSTRUCTORS_KEY = COURSE_OFFERING_KEY + "." + CourseOffering.INSTRUCTORS_KEY; public static final String COURSE_OFFERING_GRADERS_KEY = COURSE_OFFERING_KEY + "." + CourseOffering.GRADERS_KEY; public static final String COURSE_OFFERING_SEMESTER_KEY = COURSE_OFFERING_KEY + "." + CourseOffering.SEMESTER_KEY; public static final String ID_FORM_KEY = "aoid"; public static final ERXKey<String> titleString = new ERXKey<String>("titleString"); public static final ERXKey<NSTimestamp> availableFrom = new ERXKey<NSTimestamp>(AVAILABLE_FROM_KEY); public static final ERXKey<NSTimestamp> lateDeadline = new ERXKey<NSTimestamp>(LATE_DEADLINE_KEY); //~ Methods ............................................................... // ---------------------------------------------------------- /** * Get a short (no longer than 60 characters) description of this * assignment offering. * @return the description */ public String userPresentableDescription() { String result = ""; if (courseOffering() != null) { result += courseOffering().compactName() + ": "; } if (assignment() != null) { result += assignment().name(); } if (result.equals("")) { result = super.userPresentableDescription(); } return result; } // ---------------------------------------------------------- public String permalink() { if ( cachedPermalink == null ) { cachedPermalink = Application.configurationProperties() .getProperty( "base.url" ) + "?page=UploadSubmission&" + ID_FORM_KEY + "=" + id(); } return cachedPermalink; } // ---------------------------------------------------------- /** * Determine the latest time when assignments are accepted. * @return the final deadline as a timestamp */ public NSTimestamp lateDeadline() { NSTimestamp myDueDate = dueDate(); if ( myDueDate != null ) { Assignment myAssignment = assignment(); if ( myAssignment != null ) { SubmissionProfile submissionProfile = myAssignment.submissionProfile(); if ( submissionProfile != null ) { myDueDate = new NSTimestamp( submissionProfile.deadTimeDelta(), myDueDate ); } } } log.debug( "lateDeadline() = " + myDueDate ); return myDueDate; } // ---------------------------------------------------------- /** * Determine the latest time when assignments are accepted. * @param user The user to check for * @return The final deadline as a timestamp */ public boolean userCanSubmit( User user ) { boolean result = false; if ( courseOffering().instructors().containsObject( user ) || courseOffering().graders().containsObject( user ) ) { result = true; } else if ( publish() ) { NSTimestamp now = new NSTimestamp(); result = availableFrom().before( now ) && lateDeadline().after( now ); } return result; } // ---------------------------------------------------------- /** * Determine the time when this assignment begins accepting * submissions. * @return the start time for the acceptance window. */ public NSTimestamp availableFrom() { NSTimestamp myDueDate = dueDate(); NSTimestamp openingDate = null; if ( myDueDate != null ) { Assignment myAssignment = assignment(); if ( myAssignment != null ) { SubmissionProfile submissionProfile = myAssignment.submissionProfile(); if ( submissionProfile != null && submissionProfile.availableTimeDeltaRaw() != null ) { openingDate = new NSTimestamp( - submissionProfile.availableTimeDelta(), myDueDate ); } } } log.debug( "availableFrom() = " + openingDate ); return ( openingDate == null ) ? new NSTimestamp( 0L ) : openingDate; } // ---------------------------------------------------------- public String titleString() { CourseOffering course = courseOffering(); return (course == null) ? titleString(null) : titleString(course.semester()); } // ---------------------------------------------------------- public String titleString(Semester semester) { CourseOffering course = courseOffering(); Assignment myAssignment = assignment(); String result = ""; if ( course != null ) { result += course.compactName(); if (course.semester() != semester) { result += "[" + course.semester() + "]"; } result += " "; } if ( myAssignment != null ) { result += myAssignment.titleString(); } return result; } // ---------------------------------------------------------- /** * Retrieve this object's <code>graphSummary</code> value. * @return the value of the attribute */ public AssignmentSummary graphSummary() { NSData dbValue = (NSData)storedValueForKey( "graphSummary" ); SubmissionProfile profile = ( assignment() == null ) ? null : assignment().submissionProfile(); double maxScore = ( profile == null ) ? 100.0 : profile.availablePoints() - profile.taPoints(); AssignmentSummary summary = super.graphSummary(); if ( profile != null && ( dbValue == null || Math.abs( summary.maxScore() - maxScore ) > 0.01 ) ) { // have to initialize the summary by fetching all the most // recent assignments summary.setMaxScore( maxScore ); NSArray<SubmissionResult> subs = SubmissionResult.mostRecentResultsForAssignment( editingContext(), this ); for (SubmissionResult sr : subs) { if (!courseOffering().isStaff(sr.submission().user())) { summary.addSubmission( sr.automatedScore() ); } } } return summary; } // ---------------------------------------------------------- /** * Return a list of all the submissions for this assignment that are * still in the grading queue, both suspended and unsuspended. * @return an NSArray of EnqueuedJob objects representing enqueued * submissions */ public NSArray<EnqueuedJob> allSubmissionsInQueue() { return EnqueuedJob.objectsMatchingQualifier(editingContext(), EnqueuedJob.assignmentOffering.is(this)); } // ---------------------------------------------------------- /** * Return a list of all the submissions for this assignment that are * still in the grading queue and that are not suspended. * @return an NSArray of EnqueuedJob objects representing active * submissions */ public NSArray<EnqueuedJob> activeSubmissionsInQueue() { return EnqueuedJob.objectsMatchingQualifier(editingContext(), EnqueuedJob.assignmentOffering.is(this).and( EnqueuedJob.paused.isFalse())); } // ---------------------------------------------------------- /** * Return a list of all the submissions for this assignment that are * still in the grading queue but that are marked as suspended, either * because of errors or because the instructor has halted grading for * this assignment offering. * @return an NSArray of EnqueuedJob objects representing suspended * submissions */ public NSArray<EnqueuedJob> suspendedSubmissionsInQueue() { return EnqueuedJob.objectsMatchingQualifier(editingContext(), EnqueuedJob.assignmentOffering.is(this).and( EnqueuedJob.paused.isTrue())); } // ---------------------------------------------------------- /** * Return the highest-numbered submission for this assignment offering * made by the given user. * @param user the user to look up * @return the most recent Submission object for the given user, or * null if there is none */ public Submission mostRecentSubFor( User user ) { Submission mostRecent = null; NSArray<Submission> subs = Submission.objectsMatchingQualifier( editingContext(), Submission.assignmentOffering.is(this).and(Submission.user.is(user)) ); if ( subs != null && subs.count() > 0 ) { mostRecent = subs.objectAtIndex( 0 ); for ( int j = 1; j < subs.count(); j++ ) { Submission s = subs.objectAtIndex( j ); if ( s.submitNumber() > mostRecent.submitNumber() ) { mostRecent = s; } } } return mostRecent; } // ---------------------------------------------------------- /** * Return a list consisting of the most recent submission for all * students who have submitted to this assignment offering. * @return an NSArray of Submission objects */ public NSArray<Submission> mostRecentSubsForAll() { NSMutableArray<Submission> recentSubs = new NSMutableArray<Submission>(); NSMutableArray<User> students = courseOffering().students().mutableClone(); NSArray<User> staff = courseOffering().instructors(); students.removeObjectsInArray( staff ); students.addObjectsFromArray( staff ); staff = courseOffering().graders(); students.removeObjectsInArray( staff ); students.addObjectsFromArray( staff ); for (User user : students) { Submission s = mostRecentSubFor(user); if ( s != null ) { recentSubs.addObject( s ); } } return recentSubs; } // ---------------------------------------------------------- /** * Return the most recent processed submission result for this assignment * offering made by the given user. * @param user the user to look up * @return the most recent SubmissionResult object for the given user, or * null if there is none */ public SubmissionResult mostRecentSubmissionResultFor(User user) { SubmissionResult newest = null; NSArray<SubmissionResult> subs = SubmissionResult.resultsForAssignmentAndUser( editingContext(), this, user); if (subs.count() > 0) { newest = subs.objectAtIndex(subs.count() - 1); } return newest; } // ---------------------------------------------------------- /** * Delete all the result information for this submission, including * all partner links, and requeue it for grading. This method uses the * submission's current editing context to make changes, but does * <b>not</b> commit those changes to the database (the caller must * use <code>saveChanges()</code>). * @param ec the editing context to use for updating the grading queue */ public void regradeMostRecentSubsForAll(EOEditingContext ec) { for (Submission sub : mostRecentSubsForAll()) { // A fake partnered submission will have a non-null // primarySubmission attribute. We only want to regrade the actual // submissions, so only enqueue the ones with null // primarySubmission. if (sub.primarySubmission() == null) { sub.requeueForGrading(ec); } } ec.saveChanges(); Grader.getInstance().graderQueue().enqueue(null); } // ---------------------------------------------------------- /** * Retrieve the name of the subdirectory where all submissions for this * assignment offering are stored. This subdirectory is relative to * the base submission directory for some authentication domain, such * as the value returned by * {@link AuthenticationDomain#submissionBaseDirBuffer()}. * @param dir the string buffer to add the requested subdirectory to * (a / is added to this buffer, followed by the subdirectory name * generated here) */ public void addSubdirTo( StringBuffer dir ) { courseOffering().addSubdirTo( dir ); dir.append( '/' ); dir.append( assignment().subdirName() ); } // ---------------------------------------------------------- /** * Retrieve this object's <code>moodleId</code> value. * @return the value of the attribute */ public Long moodleId() { Long result = super.moodleId(); if ( result == null && assignment() != null ) { result = assignment().moodleId(); } return result; } // ---------------------------------------------------------- /** * Check whether this assignment is past the due date. * * @return true if any submissions to this assignment will be counted * as late */ public boolean isLate() { return ( dueDate() == null ) ? false : dueDate().before( new NSTimestamp() ); } // ---------------------------------------------------------- /** * {@inheritDoc} */ @Override public boolean accessibleByUser(User user) { return courseOffering() != null && courseOffering().accessibleByUser(user); } // ---------------------------------------------------------- @Override public void awakeFromFetch(EOEditingContext ec) { super.awakeFromFetch(ec); // Only try to migrate if the EC isn't a migrating context. If it is, // we're already trying to migrate and this "awake" is coming from the // child migration context. if (!(ec instanceof org.webcat.woextensions.MigratingEditingContext)) { migrateAttributeValuesIfNeeded(); } } // ---------------------------------------------------------- /** * Called by {@link #awake} to migrate attribute values if needed when the * object is retrieved. */ public void migrateAttributeValuesIfNeeded() { log.debug("migrateAttributeValuesIfNeeded()"); if ( lastModified() == null ) { if (isNewObject()) { setLastModified(new NSTimestamp()); } else { MigratingEditingContext mec = MigratingEditingContext.newEditingContext(); try { mec.lock(); AssignmentOffering migratingObject = localInstance(mec); migratingObject.setLastModified(new NSTimestamp()); mec.saveChanges(); } finally { mec.unlock(); mec.dispose(); } } } } // ---------------------------------------------------------- @Override public void mightDelete() { log.debug("mightDelete()"); if (isNewObject()) return; if (hasStudentSubmissions()) { log.debug("mightDelete(): offering has non-staff submissions"); throw new ValidationException("You may not delete an assignment " + "offering that has already received student submissions."); } if (assignment() != null && !assignment() .conflictingSubdirNameExists(assignment().subdirName())) { StringBuffer buf = new StringBuffer("/"); addSubdirTo(buf); subdirToDelete = buf.toString(); } super.mightDelete(); } // ---------------------------------------------------------- @Override public boolean canDelete() { boolean result = (courseOffering() == null || editingContext() == null || !hasStudentSubmissions()); log.debug("canDelete() = " + result); return result; } // ---------------------------------------------------------- @Override public void didDelete( EOEditingContext context ) { log.debug("didDelete()"); super.didDelete( context ); // should check to see if this is a child ec EOObjectStore parent = context.parentObjectStore(); if (parent == null || !(parent instanceof EOEditingContext)) { log.debug("didDelete() on " + this); if (subdirToDelete != null) { log.debug("deleting subdir suffix " + subdirToDelete); for (AuthenticationDomain domain : AuthenticationDomain.authDomains()) { StringBuffer dir = domain.submissionBaseDirBuffer(); dir.append(subdirToDelete); File assignmentDir = new File(dir.toString()); if (assignmentDir.exists()) { log.debug("deleting " + assignmentDir); org.webcat.core.FileUtilities.deleteDirectory( assignmentDir); } } } else { log.debug("not deleting any subdirs"); } } } // ---------------------------------------------------------- /** * Check to see if any students have submitted to this assignment * offering. This check explicitly excludes any users who have * instructor-level or TA-level access to the associated course * offering. * @return true if any student submissions exist */ public boolean hasStudentSubmissions() { if (isNewObject()) return false; NSMutableArray<EOQualifier> qualifiers = new NSMutableArray<EOQualifier>(); // Must be a submission to this assignment qualifiers.add(new EOKeyValueQualifier( Submission.ASSIGNMENT_OFFERING_KEY, EOQualifier.QualifierOperatorEqual, this)); if (this.courseOffering() != null) { NSArray<User> people = this.courseOffering().instructors(); // Not an instructor if (people.count() > 0) { for (int i = 0; i < people.count(); i++) { // Add to query qualifiers.add(new EONotQualifier( new EOKeyValueQualifier( Submission.USER_KEY, EOQualifier.QualifierOperatorEqual, people.objectAtIndex(i) ))); } } people = this.courseOffering().graders(); // Not a TA if (this.courseOffering().graders().count() > 0) { for (int i = 0; i < people.count(); i++) { // Add to query qualifiers.add(new EONotQualifier( new EOKeyValueQualifier( Submission.USER_KEY, EOQualifier.QualifierOperatorEqual, people.objectAtIndex(i) ))); } } } EOFetchSpecification spec = new EOFetchSpecification( Submission.ENTITY_NAME, new EOAndQualifier(qualifiers), null); // Only need to return 1, since we're just trying to find out if // there are any at all spec.setFetchLimit(1); NSArray<Submission> result = Submission.objectsWithFetchSpecification(editingContext(), spec); if (log.isDebugEnabled()) { log.debug("hasStudentSubmissions(): fetch = " + result); } return result.count() > 0; } //~ Public Static Methods ................................................. // ---------------------------------------------------------- /** * Retrieve a set of assignment offerings for a given course with names * similar to some string. Here, "similar" means that the name of * the assignment associated with an offering is similar to the target * name, as defined by {@link Assignment#namesAreSimilar(String,String)}. * * @param context The editing context to use * @param targetName The name that results should be similar to * @param forCourseOffering The course offering to search for * @param limit the maximum number of assignment offerings to return * (or zero, if all should be returned) * @return an NSArray of the entities retrieved, sorted in descending order * by due date (latest due date first) */ public static NSMutableArray<AssignmentOffering> offeringsWithSimilarNames( EOEditingContext context, String targetName, org.webcat.core.CourseOffering forCourseOffering, int limit ) { NSMutableArray<AssignmentOffering> others = new NSMutableArray<AssignmentOffering>(); NSArray<AssignmentOffering> sameSection = AssignmentOffering .offeringsForCourseOffering(context, forCourseOffering); for (int i = 0; i < sameSection.count() && ( limit < 1 || others.count() < limit ); i++) { AssignmentOffering ao = sameSection.objectAtIndex(i); String name = ao.assignment().name(); if (!targetName.equals( name ) && Assignment.namesAreSimilar(name, targetName)) { log.debug("matching assignment offering (target = '" + targetName + "'): " + ao.titleString() + ", " + ao.dueDate()); others.addObject(ao); } } return others; } // ---------------------------------------------------------- /** * Retrieve a set of assignment offerings for a given course with names * similar to some string. Here, "similar" means that the name of * the assignment associated with an offering is similar to the target * name, as defined by {@link Assignment#namesAreSimilar(String,String)}. * * @param context The editing context to use * @param targetName The name that results should be similar to * @param course The course to search for * @param limit the maximum number of assignment offerings to return * (or zero, if all should be returned) * @return an NSArray of the entities retrieved, sorted in descending order * by due date (latest due date first) */ public static NSMutableArray<AssignmentOffering> offeringsWithSimilarNames( EOEditingContext context, String targetName, org.webcat.core.Course course, int limit ) { NSMutableArray<AssignmentOffering> others = new NSMutableArray<AssignmentOffering>(); NSArray<AssignmentOffering> sameSection = AssignmentOffering .offeringsForCourse(context, course); for (int i = 0; i < sameSection.count() && ( limit < 1 || others.count() < limit ); i++) { AssignmentOffering ao = sameSection.objectAtIndex(i); String name = ao.assignment().name(); if (!targetName.equals( name ) && Assignment.namesAreSimilar(name, targetName)) { log.debug("matching assignment offering (target = '" + targetName + "'): " + ao.titleString() + ", " + ao.dueDate()); others.addObject(ao); } } return others; } // ---------------------------------------------------------- private static EOQualifier and(EOQualifier left, EOQualifier right) { if (left == null) { return right; } else if (right == null) { return left; } else { return ERXQ.and(left, right); } } // ---------------------------------------------------------- /** * Retrieves a sorted set of assignment offerings that are available for * submission via an external submitter engine. The set of assignments * retrieved is determined by specific keys in the formValues dictionary * that is passed in. If the dictionary is empty, then all published * assignment offerings that are both currently accepting submissions * will be returned. Specific keys in the formValues dictionary can be * used to narrow the search: * * institution the institution property name: only assignments * associated with courses in departments from this * institution will be retrieved. * course the course number: only assignments associated with * this course number will be retrieved. * crn crn number/id: only assignmnets associated with this * course offering (course request number) will be * retrieved. * staff a boolean value that, if true, includes non-published * assignments as well as assignments that are past their * due dates. * * @param context the editing context to use for fetching. * @param formValues a dictionary of values encoding choices about which * assignment offerings to retrieve, and in what order. * @param currentTime the time to use when deciding whether or not due * dates have past. * @param groupByCRN request that each separate offering of a course will * have all of its assignment offerings listed separately * and grouped together. Otherwise (the default), only * the assignment offering with the latest due date will * be shown when one assignment is shared among many * course offerings. * @param showAll request that all assignments (including those that * are already closed or that have not yet become * available) be included. If true, this also limits * the request to include only assignments available * in the current semester. * @param preserveDateDifferences if true, offerings of the same assignment * with different due dates should be preserved (only * relevant if groupByCRN is false). * @return an array of the given assignment offerings */ public static NSArray<AssignmentOffering> objectsForSubmitterEngine( EOEditingContext context, NSDictionary<String, ?> formValues, NSTimestamp currentTime, boolean groupByCRN, boolean showAll, boolean preserveDateDifferences ) { // Set up the qualifier EOQualifier qualifier = null; Object valueObj = formValueForKey( formValues, "institution" ); if ( valueObj != null ) { qualifier = and(qualifier, courseOffering.dot(CourseOffering.course) .dot(Course.department).dot(Department.institution) .dot(AuthenticationDomain.propertyName).is( "authenticator." + valueObj)); } valueObj = formValueForKey( formValues, "crn" ); if ( valueObj != null ) { qualifier = and(qualifier, courseOffering.dot(CourseOffering.crn).is(valueObj.toString())); } valueObj = formValueForKey( formValues, "course" ); if ( valueObj != null ) { qualifier = and(qualifier, courseOffering.dot(CourseOffering.course).dot(Course.number) .is(ERXValueUtilities.intValue(valueObj))); } boolean forStaff = ERXValueUtilities.booleanValue( formValueForKey(formValues, "staff")); showAll = ERXValueUtilities.booleanValueWithDefault( formValueForKey(formValues, "showAll"), showAll || forStaff); if (!forStaff) { qualifier = and(qualifier, publish.isTrue()); } if (qualifier == null) { qualifier = assignment.isNotNull(); } if (log.isDebugEnabled()) { log.debug("objectsForSubmitterEngine(): qualifier = " + qualifier); } NSArray<EOSortOrdering> orderings = groupByCRN ? SUBMISSION_CRN_ORDERINGS : SUBMISSION_ORDERINGS; NSArray<AssignmentOffering> results = objectsMatchingQualifier(context, qualifier, orderings); valueObj = formValueForKey(formValues, "courses"); if (valueObj != null) { // filter down to the given list of courses log.debug("before courses filter: " + results); NSArray<String> courses = new NSArray<String>( valueObj.toString().split("\\s*,\\s*")); log.debug("courses filter = " + courses); results = EOQualifier.filteredArrayWithQualifier(results, new InQualifier(courseOffering.dot(CourseOffering.course) .dot(Course.number).dot("toString").key(), courses)); log.debug("after courses filter: " + results); } valueObj = formValueForKey(formValues, "crns"); if (valueObj != null) { // filter down to the given list of crns log.debug("before crns filter: " + results); results = EOQualifier.filteredArrayWithQualifier(results, new InQualifier( courseOffering.dot(CourseOffering.crn) .dot("toString").key(), new NSArray<String>(valueObj.toString().split("\\s*,\\s*")) )); log.debug("after crns filter: " + results); } qualifier = null; if (showAll) { qualifier = lateDeadline.greaterThan(Semester .forDate(context, currentTime).semesterStartDate()); } else { qualifier = availableFrom.lessThan(currentTime).and( lateDeadline.greaterThan(currentTime)); } { @SuppressWarnings("unchecked") NSArray<AssignmentOffering> newResults = ERXArrayUtilities.filteredArrayWithQualifierEvaluation( results, qualifier); results = newResults; } if (!groupByCRN) { NSMutableArray<AssignmentOffering> filteredResults = results.mutableClone(); Map<Course, Map<Assignment, NSMutableArray<AssignmentOffering>>> courseAssignmentMap = new HashMap<Course, Map<Assignment, NSMutableArray<AssignmentOffering>>>(); for (int i = 0; i < filteredResults.count(); i++) { AssignmentOffering ao = filteredResults.objectAtIndex(i); if (!addAssignmentIfNecessary( ao, courseAssignmentMap, preserveDateDifferences)) { filteredResults.removeObjectAtIndex(i); i--; } } results = filteredResults; } if (log.isDebugEnabled()) { log.debug("results = " + results); for (AssignmentOffering ao : results) { log.debug("offering = " + ao + ", semester = " + ao.courseOffering().semester()); } } return results; } // ---------------------------------------------------------- @Override public void willInsert() { setLastModified(new NSTimestamp()); super.willInsert(); } // ---------------------------------------------------------- @Override public void willUpdate() { setLastModified(new NSTimestamp()); super.willInsert(); } //~ Private Methods ....................................................... // ---------------------------------------------------------- /** * Extract a form value from a dictionary. The WORequest.formValues() * method returns a dictionary mapping each form key to an NSArray of * values. This method looks up the given key, and extracts the first * value from the array. * @param dict the dictionary of form values, in the format returned by * WORequest.formValues(). * @param key The key to look up. * @return the value for the given form key, or null if there is none */ static private Object formValueForKey( NSDictionary<String, ?> dict, String key) { Object values = dict.valueForKey( key ); if ( values != null && values instanceof NSArray ) { NSArray<?> array = (NSArray<?>)values; if ( array.count() > 0 ) { return array.objectAtIndex( 0 ); } else { return null; } } return values; } // ---------------------------------------------------------- /** * This helper method for {@link #objectsForSubmitterEngine()} simply * records an assignment in a two-level map organized first by course * and then by assignment name. * @param ao the assignment offering to add to the map * @param map the map to add to * @return true, if the assignment was added, which will only happen if * there are no other assignment offerings for the same assignment that * are already entered for some offering of the same course. * Alternatively, returns false if there is already an assignment/course * combination registered in the map that matches. */ static private boolean addAssignmentIfNecessary( AssignmentOffering ao, Map<Course, Map<Assignment, NSMutableArray<AssignmentOffering>>> map, boolean preserveDateDifferences) { Map<Assignment, NSMutableArray<AssignmentOffering>> courseMap = map.get(ao.courseOffering().course()); if (courseMap == null) { courseMap = new HashMap<Assignment, NSMutableArray<AssignmentOffering>>(); map.put(ao.courseOffering().course(), courseMap); } if (courseMap.get(ao.assignment()) == null) { courseMap.put( ao.assignment(), new NSMutableArray<AssignmentOffering>(ao)); return true; } else if (preserveDateDifferences) { NSMutableArray<AssignmentOffering> currentOfferings = courseMap.get(ao.assignment()); long due = ao.dueDate().getTime(); for (AssignmentOffering other : currentOfferings) { // Is there an existing offering within 5 minutes of this one if (Math.abs(due - other.dueDate().getTime()) < 5 * 60 * 1000) { return false; } } currentOfferings.add(ao); return true; } else { return false; } } // ---------------------------------------------------------- private static class InQualifier extends ERXInQualifier { public InQualifier( String key, NSArray<?> values ) { super( key, values, 1 ); } /** Tests if the given object's key is in the supplied values */ public boolean evaluateWithObject(Object object) { Object value = NSKeyValueCodingAdditions.Utility .valueForKeyPath(object, key()); return value != null && values().containsObject(value); } } //~ Instance/static variables ............................................. private String cachedPermalink; private String subdirToDelete; private static final NSArray<EOSortOrdering> SUBMISSION_ORDERINGS = courseOffering.dot(CourseOffering.course) .dot(Course.department).dot(Department.institution) .dot(AuthenticationDomain.displayableName).ascInsensitive() .then( courseOffering.dot(CourseOffering.course) .dot(Course.department).dot(Department.abbreviation) .ascInsensitive()) .then( courseOffering.dot(CourseOffering.course).dot(Course.number) .asc()) .then(dueDate.asc()) .then(assignment.dot(Assignment.name).ascInsensitive()); private static final NSArray<EOSortOrdering> SUBMISSION_CRN_ORDERINGS = courseOffering.dot(CourseOffering.course) .dot(Course.department).dot(Department.institution) .dot(AuthenticationDomain.displayableName).ascInsensitive() .then( courseOffering.dot(CourseOffering.course) .dot(Course.department).dot(Department.abbreviation) .ascInsensitive()) .then( courseOffering.dot(CourseOffering.course).dot(Course.number) .asc()) .then(courseOffering.dot(CourseOffering.crn).asc()) .then(dueDate.asc()) .then(assignment.dot(Assignment.name).ascInsensitive()); static Logger log = Logger.getLogger( AssignmentOffering.class ); }