/*==========================================================================*\
| $Id: SubmissionResult.java,v 1.14 2012/06/06 18:43:56 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 java.io.File;
import org.apache.log4j.Logger;
import org.webcat.core.*;
import org.webcat.woextensions.MigratingEditingContext;
// -------------------------------------------------------------------------
/**
* Represents the results for a student submission.
*
* @author Stephen Edwards
* @author Last changed by $Author: stedwar2 $
* @version $Revision: 1.14 $, $Date: 2012/06/06 18:43:56 $
*/
public class SubmissionResult
extends _SubmissionResult
{
//~ Constructors ..........................................................
// ----------------------------------------------------------
/**
* Creates a new SubmissionResult object.
*/
public SubmissionResult()
{
super();
}
//~ KVC Attributes (must be public) .......................................
public static final String SUBMISSION_ASSIGNMENT_OFFERING_KEY =
SUBMISSIONS_KEY + "." + Submission.ASSIGNMENT_OFFERING_KEY;
public static final String SUBMISSION_USER_KEY =
SUBMISSIONS_KEY + "." + Submission.USER_KEY;
public static final String SUBMISSION_SUBMIT_TIME_KEY =
SUBMISSIONS_KEY + "." + Submission.SUBMIT_TIME_KEY;
public static final byte FORMAT_HTML = 0;
public static final byte FORMAT_TEXT = 1;
public static final NSArray<Byte> formats = new NSArray<Byte>(
new Byte[] { new Byte( FORMAT_HTML ), new Byte( FORMAT_TEXT ) });
public static final NSArray<String> formatStrings = new NSArray<String>(
new String[] { "HTML", "Plain Text" });
//~ Methods ...............................................................
// ----------------------------------------------------------
/**
* Retrieve the primary submission associated with this result.
* The primary submission is the one associated with the student
* who actually made the submission, as opposed to one of the
* partners associated with this submission.
* @return The submission from the primary submitter.
*/
public Submission submission()
{
Submission result = null;
NSArray<Submission> mySubmissions = submissions();
if (mySubmissions != null && mySubmissions.count() > 0)
{
result = mySubmissions.objectAtIndex(0);
Submission primary = result.primarySubmission();
if (primary != null)
{
result = primary;
}
}
return result;
}
// ----------------------------------------------------------
/**
* Retrueve the submission associated with this result for the
* given user.
* @param partner The user whose submission should be retrieved--either
* the primary submitter or one of the partners.
* @return The submission associated with the given user (partner) and
* this result, or the primary submission if this user does
* not have any submission for this result.
*/
public Submission submissionFor(User partner)
{
for (Submission sub : submissions())
{
if (sub.user() == partner)
{
return sub;
}
}
return submission();
}
// ----------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public boolean accessibleByUser(User user)
{
for (Submission sub : submissions())
{
if (sub.accessibleByUser(user))
{
return true;
}
}
return false;
}
// ----------------------------------------------------------
/**
* Computes the early bonus for this submission. The bonus
* is a positive amount added to the raw score.
*
* @return the bonus amount, or 0.0 if none
*/
public double earlyBonus()
{
double earlyBonus = 0.0;
Submission submission = submission();
long submitTime = submission.submitTime().getTime();
long dueTime = submission.assignmentOffering().dueDate().getTime();
SubmissionProfile profile =
submission.assignmentOffering().assignment().submissionProfile();
if ( profile.awardEarlyBonus()
&& dueTime > submitTime
&& profile.earlyBonusUnitTimeRaw() != null)
{
// Early bonus
//
long earlyBonusUnitTime = profile.earlyBonusUnitTime();
long earlyTime = dueTime - submitTime;
float earlyUnits = earlyTime / earlyBonusUnitTime;
earlyBonus = earlyUnits * profile.earlyBonusUnitPts();
if ( profile.earlyBonusMaxPtsRaw() != null
&& earlyBonus > profile.earlyBonusMaxPts() )
{
earlyBonus = profile.earlyBonusMaxPts();
}
}
return earlyBonus;
}
// ----------------------------------------------------------
/**
* Computes the late penalty for this submission. The penalty
* is a positive amount subtracted from the raw score.
*
* @return the penalty amount, or 0.0 if none
*/
public double latePenalty()
{
double latePenalty = 0.0;
Submission submission = submission();
long submitTime = submission.submitTime().getTime();
long dueTime = submission.assignmentOffering().dueDate().getTime();
SubmissionProfile profile =
submission.assignmentOffering().assignment().submissionProfile();
if ( profile.deductLatePenalty()
&& dueTime < submitTime
&& profile.latePenaltyUnitTimeRaw() != null)
{
// Late penalty
//
long latePenaltyUnitTime = profile.latePenaltyUnitTime();
long lateTime = submitTime - dueTime;
long lateUnits = (long)java.lang.Math.ceil(
( (double)lateTime ) / (double)latePenaltyUnitTime );
latePenalty = lateUnits * profile.latePenaltyUnitPts();
if ( profile.latePenaltyMaxPtsRaw() != null
&& latePenalty > profile.latePenaltyMaxPts() )
{
latePenalty = profile.latePenaltyMaxPts();
}
}
return latePenalty;
}
// ----------------------------------------------------------
/**
* Computes the combined bonus and/or late penalty for this
* submission, if any.
*
* @return the bonus/penalty adjustment amount
*/
public double scoreAdjustment()
{
return earlyBonus() - latePenalty();
}
// ----------------------------------------------------------
/**
* Computes the raw score for this submission, viewable by students.
* The raw score is the correctnessScore() plus the toolScore() plus
* (if grading results are viewable by students) the taScore().
*
* @return the raw score
*/
public double rawScoreForStudent()
{
double result = correctnessScore() + toolScore();
if (status() == Status.CHECK)
{
result += taScore();
}
return ( result >= 0.0 ) ? result : 0.0;
}
// ----------------------------------------------------------
/**
* Computes the raw score for this submission, viewable by staff.
* The raw score is the correctnessScore() plus the toolScore() plus
* the taScore().
*
* @return the raw score
*/
public double rawScore()
{
double result = correctnessScore() + toolScore() + taScore();
return ( result >= 0.0 ) ? result : 0.0;
}
// ----------------------------------------------------------
/**
* Computes the raw score for this submission, as viewable by the
* given user (either course staff or a student).
*
* @param user the user
* @return the final score
*/
public double rawScoreVisibleTo(User user)
{
if (user.hasAdminPrivileges() ||
submission().assignmentOffering().courseOffering().isStaff(user))
{
return rawScore();
}
else
{
return rawScoreForStudent();
}
}
// ----------------------------------------------------------
/**
* Computes the final score for this submission, viewable by students.
* The final score is the rawScore() plus the earlyBonus() minus the
* latePenalty().
*
* @return the final score
*/
public double finalScoreForStudent()
{
double result = rawScoreForStudent() + earlyBonus() - latePenalty();
return ( result >= 0.0 ) ? result : 0.0;
}
// ----------------------------------------------------------
/**
* Computes the final score for this submission, viewable by staff.
* The final score is the rawScoreForStaff() plus the earlyBonus()
* minus the latePenalty().
*
* @return the final score
*/
public double finalScore()
{
double result = rawScore() + earlyBonus() - latePenalty();
return ( result >= 0.0 ) ? result : 0.0;
}
// ----------------------------------------------------------
/**
* Computes the final score for this submission, viewable by the
* given user (either course staff or a student).
*
* @param user the user
* @return the final score
*/
public double finalScoreVisibleTo(User user)
{
if (user.hasAdminPrivileges()
|| submission().assignmentOffering().courseOffering().isStaff(user))
{
return finalScore();
}
else
{
return finalScoreForStudent();
}
}
// ----------------------------------------------------------
/**
* Check whether manual grading has been completed on this
* submission.
*
* @return true if TA markup by hand has been completed
* @deprecated Resurrected for old reports, but should not be used by
* any new code.
*/
@Deprecated
public boolean taGradingFinished()
{
return taScoreRaw() != null;
}
// ----------------------------------------------------------
/**
* Retrieve the "inline report" file as a File object.
* @return the file for this submission
*/
public File resultFile()
{
return new File( submission().resultDirName(), resultFileName() );
}
// TODO: should this operation (and its relatives) be in the Submission
// class instead of in this class?
// ----------------------------------------------------------
/**
* Retrieve the base file name for the "inline report".
* @return the base file name
*/
public static String resultFileName()
{
return "GraderReport.html";
}
// ----------------------------------------------------------
/**
* Retrieve the staff-directed "inline report" file as a File object.
* @return the file for this submission
*/
public File staffResultFile()
{
return new File( submission().resultDirName(), staffResultFileName() );
}
// ----------------------------------------------------------
/**
* Retrieve the base file name for the staff-directed "inline report".
* @return the base file name
*/
public static String staffResultFileName()
{
return "StaffGraderReport.html";
}
// ----------------------------------------------------------
/**
* Retrieve the properties file as a File object.
* @return the file for this submission
*/
public File propertiesFile()
{
if ( propertiesFile == null )
{
propertiesFile =
new File( submission().resultDirName(), propertiesFileName() );
}
return propertiesFile;
}
// ----------------------------------------------------------
/**
* Retrieve the base file name for the result properties file.
* @return the base file name
*/
public static String propertiesFileName()
{
return "grading.properties";
}
// ----------------------------------------------------------
/**
* Retrieve the properties object for this submission result.
* @return the properties object attached to the properties file
*/
public WCProperties properties()
{
if ( properties == null )
{
properties = new WCProperties(
submission().resultDirName() + "/" + propertiesFileName(),
null );
}
return properties;
}
// ----------------------------------------------------------
/**
* Retrieve the "inline summary" file as a File object.
* @return the file for this submission
*/
public File summaryFile()
{
return new File( submission().resultDirName(), summaryFileName() );
}
// ----------------------------------------------------------
/**
* Retrieve the base file name for the "inline summary".
* @return the base file name
*/
public String summaryFileName()
{
return "GraderSummary.html";
}
// ----------------------------------------------------------
public String scoreModifiers()
{
String result = null;
double rawScore = finalScore();
double earlyBonus = earlyBonus();
if ( earlyBonus > 0.0 )
{
rawScore -= earlyBonus;
result = " + " + earlyBonus + " early bonus";
}
double latePenalty = latePenalty();
if ( latePenalty < 0.0 )
{
rawScore -= latePenalty;
result = (result == null ? "" : result)
+ " - " + (-latePenalty) + " late penalty";
}
if ( result != null )
{
result = "" + rawScore + result + " = ";
}
return result;
}
// ----------------------------------------------------------
/**
* Get the corresponding icon URL for this file's grading status.
*
* @return The image URL as a string
*/
public String statusURL()
{
return Status.statusURL( status() );
}
// ----------------------------------------------------------
/**
* Retrieve the score for this result that is used in graphs. The score
* for graphing includes the raw correctness score plus the raw static
* analysis score, without any late penalty or TA manual grading
* included.
* @return the score
*/
public double automatedScore()
{
double result = correctnessScore() + toolScore();
if ( log.isDebugEnabled() )
{
log.debug( "automatedScore() = " + result );
}
return ( result >= 0.0 ) ? result : 0.0;
}
// ----------------------------------------------------------
protected boolean shouldMigrateIsMostRecent()
{
return (isMostRecentRaw() == null);
}
// ----------------------------------------------------------
protected void migrateIsMostRecent(MigratingEditingContext mec)
{
setAsMostRecentIfNecessary(mec);
}
// ----------------------------------------------------------
private void setAsMostRecentIfNecessary(EOEditingContext mec)
{
if ( log.isDebugEnabled() )
{
NSArray<SubmissionResult> subs = resultsForAssignmentAndUser(
mec, submission().assignmentOffering(), submission().user() );
for ( int i = 0; i < subs.count(); i++ )
{
SubmissionResult sr = subs.objectAtIndex( i );
log.debug( "sub " + i + ": " + sr.submission().submitNumber()
+ " " + sr.submission().submitTime() );
}
}
SubmissionResult newest = null;
NSArray<SubmissionResult> subs = mostRecentResultsForAssignmentAndUser(
mec, submission().assignmentOffering(), submission().user() );
if ( subs.count() > 0 )
{
newest = subs.objectAtIndex(0);
if ( !newest.isMostRecent() )
{
log.warn(
"most recent submission is unmarked: "
+ newest.submission().user().userName()
+ " #" + newest.submission().submitNumber() );
}
}
for ( int i = 1; i < subs.count(); i++ )
{
SubmissionResult thisSub = subs.objectAtIndex( i );
log.warn(
"multiple submissions marked most recent: "
+ thisSub.submission().user().userName()
+ " #" + thisSub.submission().submitNumber() );
thisSub.setIsMostRecent( false );
}
if ( newest != null )
{
if ( newest.submission().submitNumber() <
submission().submitNumber() )
{
newest.setIsMostRecent( false );
setIsMostRecent( true );
}
}
else // ( newest == null )
{
setIsMostRecent( true );
}
}
// ----------------------------------------------------------
/**
* Change the value of this object's <code>isMostRecent</code>
* property.
*
* @param value The new value for this property
*/
public void setIsMostRecent(boolean value)
{
boolean wasMostRecent = isMostRecent();
if (log.isDebugEnabled())
{
log.debug("setIsMostRecent(" + value + ") called");
log.debug(" submission = " + submission());
log.debug(" wasMostRecent = " + wasMostRecent);
}
User user = submission().user();
if (!submission().assignmentOffering().courseOffering().isStaff(user))
{
if (wasMostRecent && !value)
{
submission().assignmentOffering().graphSummary()
.removeSubmission(automatedScore());
}
else if (!wasMostRecent && value)
{
submission().assignmentOffering().graphSummary()
.addSubmission(automatedScore());
}
}
super.setIsMostRecent(value);
}
// ----------------------------------------------------------
/**
* Determine whether or not this submission is more recent than the
* one currently marked as most recent, and then set the flag and
* update any derived information if it is. Any changes are committed
* using this object's editing context.
*/
public void setAsMostRecentIfNecessary()
{
if ( submission() == null )
{
return;
}
EOEditingContext ec = editingContext();
setAsMostRecentIfNecessary(ec);
ec.saveChanges();
}
// ----------------------------------------------------------
public boolean hasCoverageData()
{
boolean result = false;
for (SubmissionFileStats sfs : submissionFileStats())
{
if (sfs.elementsRaw() != null)
{
result = true;
break;
}
}
return result;
}
// ----------------------------------------------------------
public void addCommentByLineFor(User commenter, String priorComments)
{
String newComments = comments();
if (newComments != null
&& (newComments.trim().equals("<br />") || newComments.equals("")))
{
setComments(null);
newComments = null;
}
if (status() == Status.TO_DO && newComments != null)
{
setStatus(Status.UNFINISHED);
}
if (newComments != null
&& newComments.indexOf("<") < 0
&& newComments.indexOf(">") < 0)
{
setCommentFormat(SubmissionResult.FORMAT_TEXT);
}
if (newComments != null && !newComments.equals(priorComments))
{
// update author info:
String byLine = "-- last updated by " + commenter.name();
if (commentFormat() == SubmissionResult.FORMAT_HTML)
{
byLine = "<p><span style=\"font-size:smaller\"><i>"
+ byLine + "</i></span></p>";
}
if (log.isDebugEnabled())
{
log.debug("new comments ='" + newComments + "'");
log.debug("byline ='" + byLine + "'");
}
if (!newComments.trim().endsWith(byLine))
{
log.debug("byLine not found");
if (commentFormat() == SubmissionResult.FORMAT_TEXT)
{
byLine = "\n" + byLine + "\n";
}
if (!(newComments.endsWith( "\n")
|| newComments.endsWith("\r")))
{
byLine = "\n" + byLine;
}
setComments(newComments + byLine);
}
}
}
// ----------------------------------------------------------
@Override
public void mightDelete()
{
log.debug("mightDelete()");
Submission sub = submission();
if (sub != null)
{
subdirToDelete = sub.resultDirName();
}
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);
}
}
}
}
// ----------------------------------------------------------
/**
* Change the value of this object's <code>lastUpdated</code>
* property.
*
* @param value The new value for this property
*/
@Override
public void setLastUpdated(NSTimestamp value)
{
super.setLastUpdated(value);
for (Submission sub : submissions())
{
sub.setIsSubmissionForGradingIfNecessary();
}
}
//~ Instance/static variables .............................................
private File propertiesFile;
private WCProperties properties;
private String subdirToDelete;
static Logger log = Logger.getLogger( SubmissionResult.class );
}