/*==========================================================================*\
| $Id: Submission.java,v 1.26 2012/06/06 19:15:32 aallowat 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.eoaccess.*;
import com.webobjects.eocontrol.*;
import com.webobjects.foundation.*;
import er.extensions.eof.ERXConstant;
import er.extensions.eof.ERXEOAccessUtilities;
import er.extensions.eof.ERXEOGlobalIDUtilities;
import er.extensions.eof.ERXQ;
import er.extensions.foundation.ERXFileUtilities;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import org.apache.log4j.Logger;
import org.webcat.core.*;
import org.webcat.core.messaging.UnexpectedExceptionMessage;
import org.webcat.grader.messaging.GradingResultsAvailableMessage;
import org.webcat.woextensions.ECAction;
import org.webcat.woextensions.WCFetchSpecification;
import static org.webcat.woextensions.ECAction.run;
import org.webcat.woextensions.MigratingEditingContext;
// -------------------------------------------------------------------------
/**
* Represents a single student assignment submission.
*
* @author Stephen Edwards
* @author Last changed by $Author: aallowat $
* @version $Revision: 1.26 $, $Date: 2012/06/06 19:15:32 $
*/
public class Submission
extends _Submission
{
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Creates a new Submission object.
*/
public Submission()
{
super();
}
//~ Constants (for key names) .............................................
public static final String ID_FORM_KEY = "sid";
//~ Methods ...............................................................
// ----------------------------------------------------------
/**
* Get a short (no longer than 60 characters) description of this
* submission, which currently returns {@link #fileName()} or
* {@link #dirName()}.
* @return the description
*/
@Override
public String userPresentableDescription()
{
if ( fileName() != null )
{
return file().getPath();
}
else
{
return dirName();
}
}
// ----------------------------------------------------------
/**
* Retrieve the name of the directory where this submission is stored.
* @return the directory name
*/
public String dirName()
{
if ( partnerLink() && result() != null )
{
return result().submission().dirName();
}
else
{
StringBuffer dir =
( user() == null )
? new StringBuffer("<null>")
: user().authenticationDomain().submissionBaseDirBuffer();
if ( assignmentOffering() != null )
{
assignmentOffering().addSubdirTo( dir );
}
else
{
dir.append( "/ASSIGNMENT" );
}
dir.append( '/' );
dir.append( ( user() == null )
? new StringBuffer("<null>")
: user().userName() );
dir.append( '/' );
dir.append( submitNumber() );
return dir.toString();
}
}
// ----------------------------------------------------------
/**
* Retrieve the submission file as a File object.
* @return the file for this submission
*/
public File file()
{
return new File( dirName(), fileName() );
}
// ----------------------------------------------------------
/**
* Retrieve this object's <code>id</code> value.
* @return the value of the attribute
*/
public Number id()
{
try
{
return (Number)EOUtilities.primaryKeyForObject(
editingContext() , this ).objectForKey( "id" );
}
catch (Exception e)
{
String subInfo = null;
try
{
subInfo = toString();
}
catch (Exception ee)
{
subInfo = ee.toString();
}
new UnexpectedExceptionMessage(e, null, null,
"An exception was generated trying to retrieve the "
+ "id for a submission.\n\nSubmission = " + subInfo)
.send();
return ERXConstant.ZeroInteger;
}
}
// ----------------------------------------------------------
/**
* Retrieve the submission file as a File object.
* @return the file for this submission
*/
public String resultDirName()
{
return dirName() + "/Results";
}
// ----------------------------------------------------------
/**
* Retrieve the submission file as a File object.
* @return the file for this submission
*/
public File resultDir()
{
return new File(resultDirName());
}
// ----------------------------------------------------------
/**
* Retrieve the path to the public resources directory in the result
* directory. Public resources are those that are generated by plug-ins
* during grading and are viewable by the user who submitted, any partners,
* course staff, and administrators.
*
* @return the path to the public resources directory
*/
public File publicResourcesDir()
{
return new File(resultDir(), "public");
}
// ----------------------------------------------------------
/**
* Retrieve the path to the public resource with the specified relative
* path in the public resources directory. If the path is invalid (for
* example, it tries to navigate to a parent directory), then null is
* returned.
*
* @param path the relative path to the resource
* @return the File object that represents the file, or null if the path
* was invalid
*/
public File fileForPublicResourceAtPath(String path)
{
File file = new File(publicResourcesDir(), path);
try
{
if (file.getCanonicalPath().startsWith(
publicResourcesDir().getCanonicalPath()))
{
return file;
}
}
catch (IOException e)
{
log.error("An error occurred while retrieving the canonical path "
+ "of the file " + file.toString());
}
return null;
}
// ----------------------------------------------------------
/**
* Converts a time to its human-readable format. Most useful
* when the time is "small," like a difference between two
* other time stamps.
*
* @param time The time to convert
* @return A human-readable version of the time
*/
public static String getStringTimeRepresentation( long time )
{
long days;
long hours;
long minutes;
// long seconds;
// long milliseconds;
StringBuffer buffer = new StringBuffer();
days = time / 86400000;
time %= 86400000;
hours = time / 3600000;
time %= 3600000;
minutes = time / 60000;
time %= 60000;
// seconds = time / 1000;
// milliseconds = time % 1000;
if (days > 0)
{
buffer.append(days);
buffer.append(" day");
if (days > 1)
{
buffer.append('s');
}
}
if (hours > 0)
{
if (buffer.length() > 0)
{
buffer.append(", ");
}
buffer.append(hours);
buffer.append(" hr");
if (hours > 1)
{
buffer.append('s');
}
}
if (minutes > 0)
{
if (buffer.length() > 0)
{
buffer.append( ", " );
}
buffer.append(minutes);
buffer.append(" min");
if (minutes > 1)
{
buffer.append('s');
}
}
if (days == 0 && hours == 0 && minutes == 0)
{
buffer.append("less than 1 min");
}
return buffer.toString();
}
// ----------------------------------------------------------
/**
* Checks whether the uploaded file is an archive.
* @return true if the uploaded file is a zip or jar file
*/
// public boolean fileIsArchive()
// {
// String fileName = fileName().toLowerCase();
// return ( fileName != null
// && ( fileName.endsWith( ".zip" )
// || fileName.endsWith( ".jar" ) ) );
// }
// ----------------------------------------------------------
public EnqueuedJob enqueuedJob()
{
NSArray<EnqueuedJob> jobs = enqueuedJobs();
if ( jobs != null && jobs.count() > 0 )
{
if ( jobs.count() > 1 )
{
log.error( "too many jobs for submission " + this );
}
return jobs.objectAtIndex( 0 );
}
else
{
return null;
}
}
// ----------------------------------------------------------
@Override
public void setPrimarySubmission(Submission value)
{
super.setPrimarySubmission(value);
setPartnerLink(value != null);
}
// ----------------------------------------------------------
public void partnerWith(NSArray<User> users)
{
// Collect all of the students enrolled in any offering of the course
// to which the submission is being made.
NSMutableSet<User> studentsEnrolled = new NSMutableSet<User>();
EOEditingContext ec = editingContext();
if (ec == null && assignmentOffering() != null)
{
ec = assignmentOffering().editingContext();
}
// Find all offerings of the current course in this semester
NSArray<CourseOffering> offerings =
CourseOffering.offeringsForSemesterAndCourse(ec,
assignmentOffering().courseOffering().course(),
assignmentOffering().courseOffering().semester());
for (CourseOffering offering : offerings)
{
studentsEnrolled.addObjectsFromArray(offering.students());
}
for (User partner : users)
{
// Only partner a user on a submission if they are enrolled in the
// same course as the user making the primary submission (but not
// necessarily in the same offering -- this is a little more
// flexible).
if (studentsEnrolled.containsObject(partner))
{
partnerWith(partner);
}
}
}
// ----------------------------------------------------------
public void partnerWith(User partner)
{
// Make sure that a user isn't trying to partner with himself.
if (partner.equals(user()))
{
return;
}
if (primarySubmission() != null)
{
primarySubmission().partnerWith(partner);
return;
}
EOEditingContext ec = editingContext();
if (ec == null)
{
ec = partner.editingContext();
}
int partnerSubmitNumber = 1;
// Find partner's home courseOffering and its assignment offering
AssignmentOffering partnerOffering = assignmentOffering();
NSArray<AssignmentOffering> partnerOfferings = AssignmentOffering
.objectsMatchingQualifier(ec,
AssignmentOffering.courseOffering.dot(CourseOffering.course)
.eq(assignmentOffering().courseOffering().course())
.and(AssignmentOffering.courseOffering
.dot(CourseOffering.students).eq(partner))
.and(AssignmentOffering.assignment
.eq(assignmentOffering().assignment())));
if (partnerOfferings.count() > 0)
{
partnerOffering = partnerOfferings.get(0);
}
EOQualifier qualifier =
Submission.assignmentOffering.eq(partnerOffering)
.and(Submission.user.eq(partner));
Submission highestSubmission = Submission.firstObjectMatchingQualifier(
ec,
qualifier,
Submission.submitNumber.descs());
if (highestSubmission != null)
{
partnerSubmitNumber = highestSubmission.submitNumber() + 1;
}
Submission newSubmission = new Submission();
ec.insertObject( newSubmission );
newSubmission.setFileName(fileName());
newSubmission.setPartnerLink(true);
newSubmission.setSubmitNumber(partnerSubmitNumber);
newSubmission.setSubmitTime(submitTime());
newSubmission.setAssignmentOfferingRelationship(partnerOffering);
newSubmission.setResultRelationship(result());
newSubmission.setUserRelationship(partner);
newSubmission.setPrimarySubmissionRelationship(this);
newSubmission.setIsSubmissionForGrading(isSubmissionForGrading());
addToPartneredSubmissionsRelationship(newSubmission);
}
// ----------------------------------------------------------
public void unpartnerFrom(NSArray<User> users)
{
for (User partner : users)
{
unpartnerFrom(partner);
}
}
// ----------------------------------------------------------
public void unpartnerFrom(User partner)
{
EOQualifier qualifier =
Submission.result.is(result()).and(Submission.user.eq(partner));
EOEditingContext ec = editingContext();
if (ec == null)
{
ec = partner.editingContext();
}
Submission partneredSubmission =
Submission.firstObjectMatchingQualifier(ec, qualifier, null);
if (partneredSubmission != null)
{
partneredSubmission.setResultRelationship(null);
ec.deleteObject(partneredSubmission);
}
}
// ----------------------------------------------------------
/**
* Gets an array containing all the users associated with this submission;
* that is, the user who submitted it as well as any partners.
*
* @return the array of all users associated with the submission
*/
public NSArray<User> allUsers()
{
NSMutableArray<User> users = new NSMutableArray<User>();
users.addObject(user());
for (Submission partnerSub : partneredSubmissions())
{
users.addObject(partnerSub.user());
}
return users;
}
// ----------------------------------------------------------
/**
* Gets a string containing the names of the user who made this submission
* and all of his or her partners, in the form "User 1, User 2, ..., and
* User N".
*
* @return a string containing the names of all of the partners
*/
public String namesOfAllUsers()
{
NSMutableArray<String> names = new NSMutableArray<String>();
names.addObject(user().name());
for (Submission partnerSub : partneredSubmissions())
{
names.addObject(partnerSub.user().name());
}
StringBuffer buffer = new StringBuffer();
buffer.append(names.objectAtIndex(0));
if (names.count() > 1)
{
for (int i = 1; i < names.count() - 1; i++)
{
buffer.append(", ");
buffer.append(names.objectAtIndex(i));
}
if (names.count() > 2)
{
buffer.append(',');
}
buffer.append(" and ");
buffer.append(names.lastObject());
}
return buffer.toString();
}
// ----------------------------------------------------------
/**
* Gets a string containing the names of the user who made this submission
* and all of his or her partners, in the form "User 1, User 2, ..., and
* User N".
*
* @return a string containing the names of all of the partners
*/
public String namesOfAllUsers_LF()
{
NSMutableArray<String> names = new NSMutableArray<String>();
names.addObject(user().name_LF());
for (Submission partnerSub : partneredSubmissions())
{
names.addObject(partnerSub.user().name_LF());
}
StringBuffer buffer = new StringBuffer();
buffer.append(names.objectAtIndex(0));
if (names.count() > 1)
{
for (int i = 1; i < names.count() - 1; i++)
{
buffer.append("; ");
buffer.append(names.objectAtIndex(i));
}
if (names.count() > 2)
{
buffer.append(';');
}
buffer.append(" and ");
buffer.append(names.lastObject());
}
return buffer.toString();
}
// ----------------------------------------------------------
/**
* Delete all the result information for this submission, including
* all partner links. 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>).
*/
private void deleteResultsForAllPartners()
{
SubmissionResult myResult = result();
if ( myResult != null )
{
log.debug( "removing SubmissionResult " + myResult );
myResult.setIsMostRecent(false);
setResultRelationship(null);
clearIsSubmissionForGrading();
// Have to copy out the list of submissions, since inside
// the loop, we'll be removing them one by one from the
// relationship.
@SuppressWarnings("unchecked")
NSArray<Submission> partnerSubs =
(NSArray<Submission>)myResult.submissions().clone();
for (Submission s : partnerSubs)
{
s.setResultRelationship(null);
s.clearIsSubmissionForGrading();
}
editingContext().deleteObject(myResult);
}
}
// ----------------------------------------------------------
/**
* 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 in which to make the changes (can be
* different than the editing context that owns this object)
*/
public void requeueForGrading( EOEditingContext ec )
{
if ( enqueuedJob() == null )
{
Submission me = this;
if ( ec != editingContext() )
{
me = localInstance( ec );
}
me.deleteResultsForAllPartners();
log.debug( "creating new job for Submission " + this );
EnqueuedJob job = new EnqueuedJob();
job.setQueueTime( new NSTimestamp() );
job.setRegrading( true );
ec.insertObject( job );
job.setSubmissionRelationship( me );
}
}
// ----------------------------------------------------------
public String permalink()
{
if ( cachedPermalink == null )
{
cachedPermalink = Application.configurationProperties()
.getProperty( "base.url" )
+ "?page=MostRecent&"
+ ID_FORM_KEY + "=" + id();
}
return cachedPermalink;
}
// ----------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public boolean accessibleByUser(User aUser)
{
return aUser == user()
|| (assignmentOffering() != null
&& assignmentOffering().accessibleByUser(aUser));
}
// ----------------------------------------------------------
public void emailNotificationToStudent( String message )
{
WCProperties properties =
new WCProperties( Application.configurationProperties() );
user().addPropertiesTo( properties );
if ( properties.getProperty( "login.url" ) == null )
{
String dest = Application.application().servletConnectURL();
properties.setProperty( "login.url", dest );
}
properties.setProperty( "submission.number",
Integer.toString( submitNumber() ) );
properties.setProperty( "message", message );
AssignmentOffering assignment = assignmentOffering();
if ( assignment != null )
{
properties.setProperty( "assignment.title",
assignment.titleString() );
}
properties.setProperty( "submission.result.link", permalink() );
try
{
new GradingResultsAvailableMessage(user(), properties)
.send();
}
catch (Exception e)
{
log.error("Unable to notify student of grading results", e);
}
/* org.webcat.core.Application.sendSimpleEmail(
user().email(),
properties.stringForKeyWithDefault(
"submission.email.title",
"[Grader] results available: #${submission.number}, "
+ "${assignment.title}" ),
properties.stringForKeyWithDefault(
"submission.email.body",
"The feedback report for ${assignment.title}\n"
+ "submission number ${submission.number} ${message}.\n\n"
+ "Log in to Web-CAT to view the report:\n\n"
+ "${submission.result.link}\n" )
);*/
}
// ----------------------------------------------------------
/**
* Check to see whether this submission is a better choice as the
* submission for grading currently set for this student/assignment
* offering combination, and if so, set it as the submission for grading.
*/
public void setIsSubmissionForGradingIfNecessary()
{
if (isSubmissionForGrading())
{
return;
}
Submission existingSubmissionForGrading = gradedSubmission();
if (existingSubmissionForGrading == null)
{
setIsSubmissionForGrading(true);
}
else
{
if (isBetterGradingChoiceThan(existingSubmissionForGrading))
{
setIsSubmissionForGrading(true);
existingSubmissionForGrading.setIsSubmissionForGrading(false);
}
else
{
setIsSubmissionForGrading(false);
existingSubmissionForGrading.setIsSubmissionForGrading(true);
}
}
}
// ----------------------------------------------------------
private void clearIsSubmissionForGrading()
{
if (isSubmissionForGrading())
{
setIsSubmissionForGrading(false);
System.out.println("removing submissionForGrading from " + this);
Submission existingSubmissionForGrading = gradedSubmission();
if (existingSubmissionForGrading != null)
{
existingSubmissionForGrading.setIsSubmissionForGrading(true);
System.out.println("setting submissionForGrading on "
+ existingSubmissionForGrading);
}
}
}
// ----------------------------------------------------------
/**
* Determine whether this submission, or another it is compared against,
* is the preferable one to use as the submission for grading. A
* submission is preferable for grading if it has any newer feedback,
* or if neither submission has any feedback, if it was made more
* recently.
* @param other The other submission to compare against.
* @return True if this submission should be used as the submission
* for grading instead of the other one.
*/
public boolean isBetterGradingChoiceThan(Submission other)
{
if (result() == null)
{
if (other.result() == null)
{
// Neither has a result!
return submitTime().after(other.submitTime());
}
// Otherwise, go with the other one
return false;
}
else if (other.result() == null)
{
return true;
}
else if (result().lastUpdated() == null)
{
// We have no feedback, so check other
if (other.result().lastUpdated() != null)
{
// Other has feedback, so go with it
return false;
}
else if (result().status() != Status.TO_DO)
{
if (other.result().status() != Status.TO_DO)
{
// Both have status change but no feedback timestamp,
// so go with newest submission
return submitTime().after(other.submitTime());
}
else
{
// We have a status change but other does not
return true;
}
}
else
{
// Neither has feedback, so go with newest submission
return submitTime().after(other.submitTime());
}
}
else if (other.result().lastUpdated() == null)
{
// We have feedback, but other doesn't
return true;
}
else
{
// Both have feedback, so go with most recent feedback
return result().lastUpdated().after(other.result().lastUpdated());
}
}
// ----------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
protected boolean shouldMigrateIsSubmissionForGrading()
{
return (isSubmissionForGradingRaw() == null);
}
// ----------------------------------------------------------
private void refreshIsSubmissionForGrading(NSArray<Submission> submissions)
{
Submission gradedSubmission = null;
// Iterate over the whole submission set and find the
// submission for grading (which is either the last submission
// is none are graded, or the latest of those that are graded).
for (Submission sub : submissions)
{
if (gradedSubmission == null
|| sub.isBetterGradingChoiceThan(gradedSubmission))
{
gradedSubmission = sub;
}
}
// Now that the entire submission chain is fetched, update the
// isSubmissionForGrading property among all of them.
for (Submission sub : submissions)
{
sub.setIsSubmissionForGrading(sub == gradedSubmission);
}
}
// ----------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
protected void migrateIsSubmissionForGrading(MigratingEditingContext mec)
{
if (user() == null || assignmentOffering() == null)
{
setIsSubmissionForGrading(false);
return;
}
WCFetchSpecification<Submission> candidates =
new WCFetchSpecification<Submission>(ENTITY_NAME,
Submission.assignmentOffering.eq(assignmentOffering()).and(
Submission.user.eq(user())),
submitNumber.descs());
candidates.setRefreshesRefetchedObjects(false);
refreshIsSubmissionForGrading(
objectsWithFetchSpecification(mec, candidates));
}
// ----------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
protected boolean shouldMigratePartnerLink()
{
// TODO: Fix the performance problems with auto-migration
// this method is temporarily disabled until the auto-migration
// performance problems can be worked out.
// return result() != null
// && ((partnerLink() && primarySubmission() == null)
// || (!partnerLink()
// && result().submissions().count() > 1
// && partneredSubmissions().count() == 0));
return false;
}
// ----------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
protected void migratePartnerLink(MigratingEditingContext mec)
{
// TODO: Fix the performance problems with auto-migration
// this method is temporarily disabled until the auto-migration
// performance problems can be worked out. To re-enable,
// erase migratePartnerLink() below and use its contents here.
}
// ----------------------------------------------------------
public void migratePartnerLink()
{
// guard from shouldMigratePartnerLink()
if (!(result() != null
&& ((partnerLink() && primarySubmission() == null)
|| (!partnerLink()
&& result().submissions().count() > 1
&& partneredSubmissions().count() == 0))))
{
return;
}
// Implementation from migratePartnerLink(MigratingEditingContext)
NSArray<Submission> mySubmissions = result().submissions();
if (mySubmissions.count() <= 1)
{
return;
}
Submission primary = mySubmissions.get(0);
// Likely an old submission that has not been migrated
// to use the newer relationships, so search
// First, find the primary submission
for (Submission thisSubmission : mySubmissions)
{
// Don't check the relationship, since we're presuming
// this batch of submissions only has the bits set,
// but not the primarySubmission relationship filled.
if (!thisSubmission.partnerLink())
{
primary = thisSubmission;
break;
}
}
if (primary.partnerLink())
{
// Yikes! We searched, and didn't find *any* submissions
// for this result without the partnerLink bit set! So
// promote this submission to be the primary submission
log.error("Cannot locate any primary submission for "
+ "result " + this);
for (Submission thisSubmission : mySubmissions)
{
log.error(" partner sub = "
+ thisSubmission.user()
+ " # "
+ thisSubmission.submitNumber()
+ " "
+ thisSubmission.hashCode()
+ ", "
+ thisSubmission.partnerLink()
+ ", pri = "
+ (thisSubmission.primarySubmission() == null
? null
: thisSubmission.primarySubmission().hashCode())
+ ", "
+ thisSubmission);
}
primary.setPartnerLink(false);
}
// Now, set up all the relationships for partners
for (Submission thisSubmission : mySubmissions)
{
if (thisSubmission.partnerLink())
{
thisSubmission.setPrimarySubmissionRelationship(primary);
}
}
// Now it is migrated!
}
// ----------------------------------------------------------
/**
* Gets the array of all submissions in the submission chain that contains
* this submission (that is, all submissions for this submission's user and
* assignment offering). The returned array is sorted by submission time in
* ascending order.
*
* @return an NSArray containing the Submission objects in this submission
* chain
*/
public NSArray<Submission> allSubmissions()
{
int newAOSubCount = assignmentOffering().submissions().count();
if (newAOSubCount != aoSubmissionsCountCache)
{
allSubmissionsCache = null;
aoSubmissionsCountCache = newAOSubCount;
}
if (allSubmissionsCache == null)
{
if (user() != null && assignmentOffering() != null)
{
allSubmissionsCache = submissionsForAssignmentOfferingAndUser(
editingContext(), assignmentOffering(), user());
}
}
return (allSubmissionsCache != null) ?
allSubmissionsCache : NO_SUBMISSIONS;
}
// ----------------------------------------------------------
/**
* Flush any cached data stored by the object in memory.
*/
@Override
public void flushCaches()
{
// Clear the in-memory cache of the all-submissions chain so that it
// will be fetched again.
aoSubmissionsCountCache = 0;
allSubmissionsCache = null;
}
// ----------------------------------------------------------
/**
* Gets the submission in this submission chain that represents the
* "submission for grading". If no manual grading has yet occurred, then
* this is equivalent to {@link #latestSubmission()}. Otherwise, if a TA
* has manually graded one or more submissions, then this method returns
* the latest of those.
*
* @return the submission for grading in this submission chain
*/
public Submission gradedSubmission()
{
NSArray<Submission> candidates = submissionsForGrading(
editingContext(), assignmentOffering(), user());
if (candidates.size() > 0)
{
return candidates.get(0);
}
else
{
return null;
}
}
// ----------------------------------------------------------
/**
* Gets the earliest (in other words, first) submission made in this
* submission chain.
*
* @return the earliest submission in the submission chain
*/
public Submission earliestSubmission()
{
if (user() == null || assignmentOffering() == null) return null;
NSArray<Submission> subs = allSubmissions();
if (subs != null && subs.count() >= 1)
{
return subs.objectAtIndex(0);
}
else
{
return null;
}
}
// ----------------------------------------------------------
/**
* Gets the latest (in other words, last) submission made in this
* submission chain. Typically clients should prefer to use the
* {@link #gradedSubmission()} method over this one, depending on their
* policy regarding the grading of submissions that are not the most recent
* one; this method exists for symmetry and to allow clients to distinguish
* between the graded submission and the last one, if necessary.
*
* @return the latest submission in the submission chain
*/
public Submission latestSubmission()
{
if (user() == null || assignmentOffering() == null) return null;
NSArray<Submission> subs = allSubmissions();
if (subs != null && subs.count() >= 1)
{
return subs.objectAtIndex(subs.count() - 1);
}
else
{
return null;
}
}
// ----------------------------------------------------------
/**
* Gets the index of the submission with the specified submission number in
* the allSubmissions array. This function isolates the logic required to
* handle the rare but possible case where there are gaps in the submission
* numbers of a student's submissions.
*
* @param number the submission number to search for
*
* @return the index of that submission in the allSubmissions array
*/
private int indexOfSubmissionWithSubmitNumber(int number)
{
NSArray<Submission> subs = allSubmissions();
if (subs.isEmpty())
{
return -1;
}
int index = number - 1;
if (index < 0)
{
index = 0;
}
else if (index > subs.count() - 1)
{
index = subs.count() - 1;
}
while (0 <= index && index < subs.count())
{
Submission sub = subs.objectAtIndex(index);
if (sub.submitNumber() == number)
{
return index;
}
else if (sub.submitNumber() < number)
{
index++;
}
else if (sub.submitNumber() > number)
{
index--;
}
}
return -1;
}
// ----------------------------------------------------------
/**
* Gets from the submission chain the submission with the specified
* submission number.
*
* @param number the number of the submission to retrieve from the chain
*
* @return the specified submission, or null if there was not one with
* that number in the chain (or there was some other error)
*/
public Submission submissionWithSubmitNumber(int number)
{
if (user() == null || assignmentOffering() == null)
{
return null;
}
int index = indexOfSubmissionWithSubmitNumber(number);
if (index == -1)
{
return null;
}
else
{
return allSubmissions().objectAtIndex(index);
}
}
// ----------------------------------------------------------
/**
* Gets the previous submission to this one in the submission chain.
*
* @return the previous submission, or null if it is the first one (or
* there was an error)
*/
public Submission previousSubmission()
{
if (submitNumberRaw() == null)
{
return null;
}
else
{
NSArray<Submission> subs = allSubmissions();
int index = indexOfSubmissionWithSubmitNumber(submitNumber());
if (index <= 0)
{
return null;
}
else
{
return subs.objectAtIndex(index - 1);
}
}
}
// ----------------------------------------------------------
/**
* Gets the next submission following this one in the submission chain.
*
* @return the next submission, or null if it is the first one (or
* there was an error)
*/
public Submission nextSubmission()
{
if (submitNumberRaw() == null)
{
return null;
}
else
{
NSArray<Submission> subs = allSubmissions();
int index = indexOfSubmissionWithSubmitNumber(submitNumber());
if (index == -1 || index == subs.count() - 1)
{
return null;
}
else
{
return subs.objectAtIndex(index + 1);
}
}
}
// ----------------------------------------------------------
/**
* Gets the earliest submission in the submission chain for this user and
* assignment offering that has valid non-zero coverage data.
*
* @return the earliest submission with valid non-zero coverage data, or
* null if there is no submission satisfying this
*/
public Submission earliestSubmissionWithCoverage()
{
NSArray<Submission> subs = allSubmissions();
for(Submission submission : subs)
{
if (submission.hasCoverage())
{
return submission;
}
}
return null;
}
// ----------------------------------------------------------
/**
* Gets a value indicating whether the submission has valid non-zero
* coverage data. This implies that the submission compiled without error
* and executed at least partially (but only for plug-ins that collect code
* coverage statistics).
*
* @return true if the submission has valid non-zero coverage data,
* otherwise false.
*/
public boolean hasCoverage()
{
if (result() == null)
{
return false;
}
NSArray<SubmissionFileStats> files = result().submissionFileStats();
for(SubmissionFileStats file : files)
{
if (file.elements() > 0)
{
return true;
}
}
return false;
}
// ----------------------------------------------------------
/**
* Gets the earliest submission in the submission chain for this user and
* assignment offering that has valid lines-of-code data.
*
* @return the earliest submission with valid lines-of-code data, or
* null if there is no submission satisfying this
*/
public Submission earliestSubmissionWithLOC()
{
NSArray<Submission> subs = allSubmissions();
for(Submission submission : subs)
{
if (submission.hasLOC())
{
return submission;
}
}
return null;
}
// ----------------------------------------------------------
/**
* Gets a value indicating whether the submission has valid lines-of-code
* data.
*
* @return true if the submission has valid non-zero lines-of-code data,
* otherwise false.
*/
public boolean hasLOC()
{
if (result() == null)
{
return false;
}
NSArray<SubmissionFileStats> files = result().submissionFileStats();
for(SubmissionFileStats file : files)
{
if (file.loc() > 0)
{
return true;
}
}
return false;
}
// ----------------------------------------------------------
/**
* Gets the earliest submission in the submission chain for this user and
* assignment offering that has a non-zero correctness score.
*
* @return the earliest submission with a non-zero correctness score, or
* null if there is no submission satisfying this
*/
public Submission earliestSubmissionWithCorrectnessScore()
{
NSArray<Submission> subs = allSubmissions();
for(Submission submission : subs)
{
if (submission.hasCorrectnessScore())
{
return submission;
}
}
return null;
}
// ----------------------------------------------------------
/**
* Gets a value indicating whether the submission has a non-zero
* correctness score. This implies that the code ran, but the converse is
* not true of course -- not all code that runs attains a non-zero
* correctness score.
*
* @return true if the submission has a non-zero correctness score.
*/
public boolean hasCorrectnessScore()
{
if (result() == null)
{
return false;
}
return result().correctnessScore() > 0;
}
// ----------------------------------------------------------
/**
* Gets the earliest submission in the submission chain for this user and
* assignment offering that has a non-zero correctness score.
*
* @return the earliest submission with a non-zero correctness score, or
* null if there is no submission satisfying this
*/
public Submission earliestSubmissionWithAnyData()
{
NSArray<Submission> subs = allSubmissions();
for(Submission submission : subs)
{
if (submission.hasAnyData())
{
return submission;
}
}
return null;
}
// ----------------------------------------------------------
/**
* Gets a value indicating whether the submission has any data of interest
* as generated by the grading plug-ins and grading process. For now, this
* includes the following items: coverage data, lines-of-code, correctness
* score.
*
* @return true if the submission has any valid grading data, otherwise
* false.
*/
public boolean hasAnyData()
{
// The order of these is important; they should be listed from most
// efficient to least efficient in order to short-circuit the test as
// quickly as possible. Correctness-score only requires checking the
// field on the results object; coverage and LOC require fetching all
// of the SubmissionFileStats and iterating over them.
if (hasCorrectnessScore() || hasCoverage() || hasLOC())
{
return true;
}
else
{
return false;
}
}
// ----------------------------------------------------------
/**
* Gets the next earliest submission to this one in the submission chain
* that has any data (LOC, coverage, or correctness score).
*
* @return the previous submission with any data, or null if there were no
* submissions before this one that had any data
*/
public Submission previousSubmissionWithAnyData()
{
Submission prevSub = previousSubmission();
while (prevSub != null && !prevSub.hasAnyData())
{
prevSub = prevSub.previousSubmission();
}
return prevSub;
}
// ----------------------------------------------------------
/**
* Gets the first submission following this one in the submission chain
* that has any data (LOC, coverage, or correctness score).
*
* @return the next submission with any data, or null if there were no
* submissions after this one that had any data
*/
public Submission nextSubmissionWithAnyData()
{
Submission nextSub = nextSubmission();
while (nextSub != null && !nextSub.hasAnyData())
{
nextSub = nextSub.nextSubmission();
}
return nextSub;
}
// ----------------------------------------------------------
/**
* Gets a qualifier that can be used to fetch only submissions that are in
* the same submission chain as this submission; that is, submissions by
* the same user to the same assignment offering.
*
* @return the qualifier
*/
public EOQualifier qualifierForSubmissionChain()
{
return ERXQ.and(
ERXQ.equals("user", user()),
ERXQ.equals("assignmentOffering", assignmentOffering()));
}
// ----------------------------------------------------------
/**
* Gets the earliest submission that has the highest correctness score
* among all the submissions in this submission chain.
*
* @return the earliest submission with the highest correctness score
*/
public Submission earliestSubmissionWithMaximumCorrectnessScore()
{
if (user() == null || assignmentOffering() == null)
{
return null;
}
NSArray<Submission> subs = allSubmissions();
Submission maxSubmission = null;
double maxCorrectnessScore = Double.MIN_VALUE;
for (Submission sub : subs)
{
SubmissionResult thisResult = sub.result();
if (thisResult != null)
{
double score = thisResult.correctnessScore();
if (score > maxCorrectnessScore)
{
maxSubmission = sub;
maxCorrectnessScore = score;
}
}
}
return maxSubmission;
}
// ----------------------------------------------------------
/**
* Gets the earliest submission that has the highest tool score among all
* the submissions in this submission chain.
*
* @return the earliest submission with the highest tool score
*/
public Submission earliestSubmissionWithMaximumToolScore()
{
if (user() == null || assignmentOffering() == null)
{
return null;
}
NSArray<Submission> subs = allSubmissions();
Submission maxSubmission = null;
double maxToolScore = Double.MIN_VALUE;
for (Submission sub : subs)
{
SubmissionResult thisResult = sub.result();
if (thisResult != null)
{
double score = thisResult.toolScore();
if (score > maxToolScore)
{
maxSubmission = sub;
maxToolScore = score;
}
}
}
return maxSubmission;
}
// ----------------------------------------------------------
/**
* Gets the earliest submission that has the highest automated score
* (correctness + tool) among all the submissions in this submission chain.
*
* @return the earliest submission with the highest automated score
*/
public Submission earliestSubmissionWithMaximumAutomatedScore()
{
if (user() == null || assignmentOffering() == null)
{
return null;
}
NSArray<Submission> subs = allSubmissions();
Submission maxSubmission = null;
double maxAutomatedScore = Double.MIN_VALUE;
for (Submission sub : subs)
{
SubmissionResult thisResult = sub.result();
if (thisResult != null)
{
double score = thisResult.automatedScore();
if (score > maxAutomatedScore)
{
maxSubmission = sub;
maxAutomatedScore = score;
}
}
}
return maxSubmission;
}
// ----------------------------------------------------------
/**
* Gets a value indicating whether or not the submission was late.
*
* @return true if the submission was late, otherwise false
*/
public boolean isLate()
{
return submitTime().after(assignmentOffering().dueDate());
}
// ----------------------------------------------------------
/**
* Get the "status" for this submission: suspended, cancelled, or queued.
* @return The status, or null if the submission has been completely
* processed and has a result available.
*/
public String status()
{
String status = null;
if (result() == null)
{
status = "suspended";
EnqueuedJob job = enqueuedJob();
if (job == null)
{
status = "cancelled";
}
else if (!job.paused())
{
status = "queued for grading";
}
}
return status;
}
// ----------------------------------------------------------
/**
* Computes the difference between the submission time and the
* due date/time, and renders it in a human-readable string.
*
* @return the string representation of how early or late
*/
public String earlyLateStatus()
{
String description = null;
long time = submitTime().getTime();
long dueTime = assignmentOffering().dueDate().getTime();
if (dueTime >= time)
{
// Early submission
description =
Submission.getStringTimeRepresentation(dueTime - time)
+ " early";
}
else
{
// Late submission
description =
Submission.getStringTimeRepresentation(time - dueTime)
+ " late";
}
return description;
}
// ----------------------------------------------------------
/**
* Gets the path to the grading.properties file associated with this
* submission, or where it would be if it did exist.
*
* @return the path to the grading.properties file for this submission
*/
public File gradingPropertiesFile()
{
return new File(resultDirName(), SubmissionResult.propertiesFileName());
}
// ----------------------------------------------------------
/**
* Creates the initial grading.properties file for this submission and
* returns a WCProperties object representing its contents.
*
* If the file already exists, this method does not overwrite it, and it
* returns the properties that are already in the file.
*
* @return a WCProperties object representing the content of the initial
* grading.properties file
* @throws IOException if an I/O error occurs
*/
public WCProperties createInitialGradingPropertiesFile() throws IOException
{
WCProperties properties = new WCProperties();
File propertiesFile = gradingPropertiesFile();
if (propertiesFile.exists())
{
properties.load(propertiesFile.getAbsolutePath());
return properties;
}
// Create the results directory if it does not already exist.
propertiesFile.getParentFile().mkdirs();
// Set the initial properties that only depend on the submission or
// the application configuration.
properties.addPropertiesFromDictionaryIfNotDefined(Application
.wcApplication().subsystemManager().pluginProperties());
properties.setProperty("frameworksBaseURL",
Application.application().frameworksBaseURL());
properties.setProperty("userName", user().userName());
properties.setProperty("resultDir", resultDirName());
properties.setProperty("scriptData", GradingPlugin.scriptDataRoot());
String crn = assignmentOffering().courseOffering().crn();
properties.setProperty("course",
assignmentOffering().courseOffering().course().deptNumber());
properties.setProperty("CRN", (crn == null) ? "null" : crn);
properties.setProperty("assignment",
assignmentOffering().assignment().name());
properties.setProperty("dueDateTimestamp",
Long.toString(assignmentOffering().dueDate().getTime()));
properties.setProperty("submissionTimestamp",
Long.toString(submitTime().getTime()));
properties.setProperty("submissionNo",
Integer.toString(submitNumber()));
properties.setProperty("numReports", "0");
Number toolPts = assignmentOffering().assignment()
.submissionProfile().toolPointsRaw();
properties.setProperty("max.score.tools",
(toolPts == null) ? "0" : toolPts.toString());
double maxCorrectnessScore = assignmentOffering().assignment()
.submissionProfile().availablePoints();
if (toolPts != null)
{
maxCorrectnessScore -= toolPts.doubleValue();
}
Number TAPts = assignmentOffering().assignment()
.submissionProfile().taPointsRaw();
if (TAPts != null)
{
maxCorrectnessScore -= TAPts.doubleValue();
}
properties.setProperty("max.score.correctness",
Double.toString(maxCorrectnessScore));
// Write the properties file to disk.
BufferedOutputStream out = new BufferedOutputStream(
new FileOutputStream(propertiesFile));
properties.store(
out, "Web-CAT grader script configuration properties");
out.close();
return properties;
}
// ----------------------------------------------------------
public String contentsOfResultFile(String relativePath) throws IOException
{
// Massage the path a little so we can prevent access outside the
// Results folder.
relativePath = relativePath.replace('\\', '/');
while (relativePath.startsWith("./"))
{
relativePath = relativePath.substring(2);
}
if (relativePath.startsWith("../") || relativePath.startsWith("/"))
{
throw new IllegalArgumentException(
"Path must not include parent directory or root directory"
+ "components");
}
File file = new File(resultDirName(), relativePath);
if (file.isDirectory())
{
throw new IllegalArgumentException(
"Path must be a file, not a directory");
}
return ERXFileUtilities.stringFromFile(file);
}
// ----------------------------------------------------------
@Override
public void mightDelete()
{
log.debug("mightDelete()");
subdirToDelete = dirName();
super.mightDelete();
}
// ----------------------------------------------------------
@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))
{
if (subdirToDelete != null)
{
File dir = new File(subdirToDelete);
if (dir.exists())
{
org.webcat.core.FileUtilities.deleteDirectory(dir);
}
}
}
}
// ----------------------------------------------------------
/**
* Find all submissions that are used for scoring for a given
* assignment by the specified users.
*
* @param ec The editing context
* @param anAssignmentOffering The offering to search for.
* @param omitPartners If false, include submissions from all
* partners working together. If true,
* include only the primary submitter's
* submission.
* @param users The list of users to find submissions for.
* @param accumulator If non-null, use this object to accumulate
* descriptive summary statistics about the
* submissions.
* @return An array of the user/submission pairs.
*/
public static NSArray<UserSubmissionPair> submissionsForGrading(
EOEditingContext ec,
AssignmentOffering anAssignmentOffering,
boolean omitPartners,
NSArray<User> users,
CumulativeStats accumulator)
{
NSMutableDictionary<User, Submission> submissions =
new NSMutableDictionary<User, Submission>();
NSMutableArray<User> realUsers = users.mutableClone();
boolean brokenPartners = submissionsForGradingWithMigration(
ec, anAssignmentOffering, omitPartners, realUsers, submissions,
accumulator);
if (brokenPartners)
{
// On the first fetch, some partner subs were found that
// were hooked to the wrong assignment, and they were updated.
// So re-execute the action to pull in all the results after
// this fix.
realUsers = users.mutableClone();
brokenPartners = submissionsForGradingWithMigration(
ec, anAssignmentOffering, omitPartners, realUsers,
submissions, accumulator);
if (brokenPartners)
{
log.error("submissionsForGrading() still found broken "
+ "partner submissions after two rounds.");
}
}
return generateUserSubmissionPairs(realUsers, submissions);
}
// ----------------------------------------------------------
private static NSArray<UserSubmissionPair> generateUserSubmissionPairs(
NSArray<User> users, NSDictionary<User, Submission> submissions)
{
NSMutableArray<UserSubmissionPair> pairs =
new NSMutableArray<UserSubmissionPair>(users.size());
for (User aUser : users)
{
Submission submission = submissions.objectForKey(aUser);
pairs.addObject(new UserSubmissionPair(aUser, submission));
}
return pairs;
}
// ----------------------------------------------------------
/**
* Find all submissions that are used for scoring for a given
* assignment by the specified users.
*
* @param anAssignmentOffering The offering to search for.
* @param omitPartners If false, include submissions from all
* partners working together. If true,
* include only the primary submitter's
* submission.
* @param users The list of users to find submissions for.
* @param submissions The submissions for grading, keyed by user.
* @param accumulator If non-null, use this object to accumulate
* descriptive summary statistics about the
* submissions.
* @return True if any broken partner submissions were found and we need
* to refetch.
*/
private static boolean submissionsForGradingWithMigration(
EOEditingContext ec,
final AssignmentOffering anAssignmentOffering,
boolean omitPartners,
NSMutableArray<User> users,
NSMutableDictionary<User, Submission> submissions,
CumulativeStats accumulator)
{
final NSMutableArray<Submission> brokenPartners =
new NSMutableArray<Submission>();
for (User student : users.immutableClone())
{
log.debug("Scanning submissions for " + student);
NSArray<Submission> candidates =
Submission.objectsMatchingQualifier(
ec,
Submission.assignmentOffering.eq(anAssignmentOffering).and(
// Submission.result.isNotNull()).and(
Submission.user.eq(student)));
if (log.isDebugEnabled())
{
log.debug("candidates using old fetch = ");
for (Submission s : candidates)
{
System.out.println("\t" + s);
}
NSArray<Submission> newCandidates =
Submission.objectsMatchingQualifier(
ec,
Submission.assignmentOffering.eq(anAssignmentOffering)
.and(Submission.user.eq(student)),
Submission.isSubmissionForGrading
.descs().then(
Submission.result.dot(SubmissionResult.lastUpdated)
.desc()).then(
Submission.result.dot(SubmissionResult.status)
.desc()).then(
Submission.submitNumber.desc()));
System.out.println();
log.debug("candidates using new fetch = ");
for (Submission s : newCandidates)
{
System.out.println("\t" + s);
System.out.println("\t\tnumber = " + s.submitNumber());
System.out.println("\t\tlast updated = "
+ s.result().lastUpdated());
System.out.println("\t\tstatus = " + s.result().status());
System.out.println("\t\tfor grading = "
+ s.isSubmissionForGrading());
}
}
Submission forGrading = null;
for (Submission sub : candidates)
{
if (sub.result() == null)
{
continue;
}
// Check to see if any partners are accidentally broken,
// and point to the wrong assignment due to an earlier bug
// in partnerWith()
for (Submission psub : sub.result().submissions())
{
if (psub != sub
&& psub.assignmentOffering() != null
&& sub.assignmentOffering() != null
&& psub.assignmentOffering().assignment() !=
sub.assignmentOffering().assignment())
{
brokenPartners.add(psub);
}
}
sub.migratePartnerLink();
if (forGrading == null
|| sub.isBetterGradingChoiceThan(forGrading))
{
forGrading = sub;
}
}
if (forGrading != null)
{
if (omitPartners && forGrading.partnerLink())
{
users.removeObject(student);
}
else
{
submissions.setObjectForKey(forGrading, student);
if (accumulator != null)
{
accumulator.accumulate(forGrading);
}
}
}
// If any partner submissions that were incorrectly hooked to
// the wrong assignment were found, patch them now.
if (brokenPartners.count() > 0)
{
run(new ECAction() { public void action() {
try
{
AssignmentOffering offering =
anAssignmentOffering.localInstance(ec);
for (Submission sub : brokenPartners)
{
Submission psub = sub.localInstance(ec);
log.warn("found partner submission "
+ psub.user() + " #" + psub.submitNumber()
+ "\non incorrect assignment offering "
+ psub.assignmentOffering());
NSArray<AssignmentOffering> partnerOfferings =
AssignmentOffering.objectsMatchingQualifier(
ec,
AssignmentOffering.courseOffering
.dot(CourseOffering.course).eq(
offering.courseOffering().course())
.and(AssignmentOffering.courseOffering
.dot(CourseOffering.students).eq(
psub.user()))
.and(AssignmentOffering.assignment
.eq(offering.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));
}
}
ec.saveChanges();
}
catch (Exception e)
{
log.error(
"Cannot update broken partner submissions", e);
}
}});
}
}
//return new Submissions(subs, brokenPartners);
return brokenPartners.count() > 0;
}
// ----------------------------------------------------------
/**
* Find all submissions that are used for scoring for a given
* assignment.
*
* @param ec The editing context
* @param anAssignmentOffering The offering to search for.
* @param omitPartners If false, include submissions from all
* partners working together. If true,
* include only the primary submitter's
* submission.
* @param omitStaff If true, leave out course staff.
* @param accumulator If non-null, use this object to accumulate
* descriptive summary statistics about the
* submissions.
* @return An array of the submissions found.
*/
public static NSArray<UserSubmissionPair> submissionsForGrading(
EOEditingContext ec,
AssignmentOffering anAssignmentOffering,
boolean omitPartners,
boolean omitStaff,
CumulativeStats accumulator)
{
CourseOffering courseOffering = anAssignmentOffering.courseOffering();
NSArray<User> users = omitStaff
? courseOffering.studentsWithoutStaff()
: courseOffering.studentsAndStaff();
return submissionsForGrading(
ec, anAssignmentOffering, omitPartners, users, accumulator);
}
// ----------------------------------------------------------
/**
* Retrieve objects according to the <code>submissionsForGrading</code>
* fetch specification.
*
* @param context The editing context to use
* @param assignmentOfferingBinding fetch spec parameter
* @param userBinding fetch spec parameter
* @return an NSArray of the entities retrieved
*/
public static NSArray<Submission> submissionsForGrading(
EOEditingContext context,
org.webcat.grader.AssignmentOffering assignmentOfferingBinding,
org.webcat.core.User userBinding
)
{
NSArray<Submission> subs = _Submission.submissionsForGrading(
context, assignmentOfferingBinding, userBinding);
if (subs.size() == 0 || !subs.get(0).isSubmissionForGrading())
{
NSArray<UserSubmissionPair> subPairs = submissionsForGrading(
context,
assignmentOfferingBinding,
false, // omitPartners
new NSArray<User>(userBinding),
null);
if (subPairs.size() > 0
&& subPairs.objectAtIndex(0).submission() != null)
{
Submission graded = subPairs.objectAtIndex(0).submission();
// EOEditingContext local = Application.newPeerEditingContext();
// try
// {
// local.lock();
// Submission localSub = graded.localInstance(local);
// localSub.setIsSubmissionForGrading(true);
// local.saveChanges();
// }
// finally
// {
// try
// {
// local.unlock();
// }
// finally
// {
// Application.releasePeerEditingContext(local);
// }
// }
subs = new NSArray<Submission>(graded);
}
}
return subs;
}
// ----------------------------------------------------------
/**
* Fetches the most recent submission by each user for a particular
* assignment.
*
* @param ec the editing context
* @param offering the assignment offering whose submissions should be
* fetched
*
* @return a dictionary with users as keys and the most recent submission
* by that user as the value; only users who have made a submission
* will appear in this map
*/
public static NSDictionary<User, Submission> latestSubmissionsForAssignment(
EOEditingContext ec,
AssignmentOffering offering)
{
EOModelGroup modelGroup = ERXEOAccessUtilities.modelGroup(ec);
EOEntity entity = modelGroup.entityNamed(ENTITY_NAME);
String modelName = entity.model().name();
String sql = "select OID as id, max(CSUBMITNUMBER) as csubmitnumber"
+ " from TSUBMISSION where CASSIGNMENTID = "
+ offering.id() + " group by CUSERID";
NSArray<NSDictionary> rawRows = EOUtilities.rawRowsForSQL(
ec, modelName, sql, null);
NSMutableArray<EOGlobalID> gids = new NSMutableArray<EOGlobalID>();
for (NSDictionary row : rawRows)
{
// This feels kind of like a hack; for globalIDForRow to work
// correctly, the object id must have the key "id".
((NSMutableDictionary) row).setObjectForKey(
row.objectForKey("OID"), "id");
EOGlobalID gid = entity.globalIDForRow(row);
if (gid != null)
{
gids.add(gid);
}
}
NSArray<Submission> submissions =
ERXEOGlobalIDUtilities.fetchObjectsWithGlobalIDs(ec, gids);
NSMutableDictionary<User, Submission> subMap =
new NSMutableDictionary<User, Submission>();
for (Submission sub : submissions)
{
subMap.setObjectForKey(sub, sub.user());
}
return subMap;
}
// ----------------------------------------------------------
/**
* A class used to accumulate basic descriptive statistics about
* multiple submission results.
*/
public static class CumulativeStats
{
//~ Fields ............................................................
private double min;
private double max;
private double total;
private ArrayList<Double> allScores;
private Double cachedMedian;
//~ Constructors ......................................................
// ----------------------------------------------------------
/**
* Create a new, empty object.
*/
public CumulativeStats()
{
min = 0.0;
max = 0.0;
total = 0.0;
allScores = new ArrayList<Double>();
}
//~ Methods ...........................................................
// ----------------------------------------------------------
/**
* Accumulate data about the given submission result.
* @param subResult The submission result to add
*/
public void accumulate(SubmissionResult subResult)
{
double score = subResult.finalScore();
if (allScores.size() == 0)
{
min = score;
max = score;
}
else
{
if (score < min)
{
min = score;
}
if (score > max)
{
max = score;
}
}
total += score;
cachedMedian = null;
allScores.add(score);
}
// ----------------------------------------------------------
/**
* Accumulate data about the given submission, if it has a result.
* @param submission The submission to add
*/
public void accumulate(Submission submission)
{
if (submission.result() != null)
{
accumulate(submission.result());
}
}
// ----------------------------------------------------------
/**
* Retrieve the minimum final score of all submission results
* accumulated so far.
* @return The minimum score
*/
public double min()
{
return min;
}
// ----------------------------------------------------------
/**
* Retrieve the maximum final score of all submission results
* accumulated so far.
* @return The maximum score
*/
public double max()
{
return max;
}
// ----------------------------------------------------------
/**
* Retrieve the mean (average) final score over all submission
* results accumulated so far.
* @return The mean score
*/
public double mean()
{
return (allScores.size() > 1)
? (total / allScores.size())
: total;
}
// ----------------------------------------------------------
/**
* Retrieve the median final score over all submission results
* accumulated so far.
* @return the median score
*/
public double median()
{
if (cachedMedian == null)
{
Collections.sort(allScores);
int count = allScores.size();
if (count == 0)
{
return 0;
}
else if (count % 2 == 0)
{
return (allScores.get((count / 2) - 1)
+ allScores.get(count / 2)) / 2;
}
else
{
return allScores.get(count / 2);
}
}
return (cachedMedian != null ? cachedMedian : 0.0);
}
// ----------------------------------------------------------
public java.util.List<Double> allScores()
{
return allScores;
}
// ----------------------------------------------------------
public String toString()
{
return "stats: "
+ allScores.size()
+ " subs: hi = "
+ max()
+ ", low = "
+ min()
+ ", avg = "
+ mean();
}
}
//~ Instance/static variables .............................................
private int aoSubmissionsCountCache;
private NSArray<Submission> allSubmissionsCache;
private String cachedPermalink;
private String subdirToDelete;
private static final NSArray<Submission> NO_SUBMISSIONS =
new NSArray<Submission>();
static Logger log = Logger.getLogger(Submission.class);
}