/*==========================================================================*\ | $Id: FinalReportPage.java,v 1.6 2011/12/06 18:38:25 stedwar2 Exp $ |*-------------------------------------------------------------------------*| | Copyright (C) 2006-2010 Virginia Tech | | This file is part of Web-CAT. | | Web-CAT is free software; you can redistribute it and/or modify | it under the terms of the GNU Affero General Public License as published | by the Free Software Foundation; either version 3 of the License, or | (at your option) any later version. | | Web-CAT is distributed in the hope that it will be useful, | but WITHOUT ANY WARRANTY; without even the implied warranty of | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | GNU General Public License for more details. | | You should have received a copy of the GNU Affero General Public License | along with Web-CAT; if not, see <http://www.gnu.org/licenses/>. \*==========================================================================*/ package org.webcat.grader; import com.webobjects.appserver.*; import com.webobjects.eocontrol.*; import com.webobjects.foundation.*; import er.extensions.eof.ERXConstant; import java.io.*; import org.apache.log4j.Logger; import org.webcat.core.*; import org.webcat.woextensions.WCResourceManager; // ------------------------------------------------------------------------- /** * This class presents the final grading report on a student * submission. If the submission has not yet completed grading, * then the page presents a message indicating that grading is * in-process and automatically reloads after 10 seconds. If some * error happened during grading, then the student is informed. * Otherwise, the final grading report is presented. * * @author Stephen Edwards * @author Last changed by $Author: stedwar2 $ * @version $Revision: 1.6 $, $Date: 2011/12/06 18:38:25 $ */ public class FinalReportPage extends GraderSubmissionComponent { //~ Constructors .......................................................... // ---------------------------------------------------------- /** * This is the default constructor * * @param context The page's context */ public FinalReportPage(WOContext context) { super(context); } //~ KVC Attributes (must be public) ....................................... public Submission submission; public SubmissionResult result; public WODisplayGroup statsDisplayGroup; // For iterating over display group public SubmissionFileStats stats; public int index; /** The job's isFinished attribute is tested once and stored here. This prevents race conditions arising from testing that field multiple times in the body of the page, or between generating a value for areInlineReports and later testing the job's isFinished attribute separately. */ public boolean reportIsReady; /** The associated refresh interval for this page */ public int refreshTimeout = 15; /** The report object */ public ResultFile report; /** Array of all the downloadable report files */ public NSMutableArray<ResultFile> reportArray; public boolean showReturnToGrading = false; //~ Methods ............................................................... // ---------------------------------------------------------- /** * Adds to the response of the page * * @param response The response being built * @param context The context of the request */ protected void beforeAppendToResponse( WOResponse response, WOContext context) { log.debug("beginning appendToResponse()"); jobData = null; if (submission == null && prefs() != null) { submission = prefs().submission(); if (submission != null && (submission.user() != user() || submission.assignmentOffering().assignment() != prefs().assignment())) { submission = null; } if (submission == null && prefs().assignment() != null) { submission = Submission.latestSubmissionForAssignmentAndUser( localContext(), prefs().assignment(), user()); if (submission != null) { prefs().setSubmissionRelationship(submission); } } } if (submission != null) { result = submission.result(); reportIsReady = (result != null); if (reportIsReady) { statsDisplayGroup.setObjectArray(result.submissionFileStats()); reportArray = result.resultFiles().mutableClone(); } else { reportArray = new NSMutableArray<ResultFile>(); } // Add user submission file to the downloadable results ResultFile userSubmission = new ResultFile(); userSubmission.setFileName( "../" + submission.fileName()); userSubmission.setMimeType("application/octet-stream"); userSubmission.setLabel("Your original submission"); reportArray.addObject(userSubmission); } showCoverageData = null; super.beforeAppendToResponse(response, context); } // ---------------------------------------------------------- /** * Returns the file delivery page with the non inline file. * @return the file delivery page */ public WOComponent fileDeliveryAction() { DeliverFile download = (DeliverFile)pageWithName(DeliverFile.class.getName()); download.setFileName( new File(submission.resultDirName(), report.fileName())); download.setContentType(report.mimeType()); download.setStartDownload(true); return download; } // ---------------------------------------------------------- /** * Returns null to force a reload of the current page. * @return always null, to refresh the current page */ public WOComponent refreshAction() { return null; } // ---------------------------------------------------------- /** * Jump to the start page for selecting past results. * @return the new page */ public WOComponent pastResults() { return pageWithName( wcSession().tabs.selectById("PastResults").pageName()); } // ---------------------------------------------------------- /** * Returns null to force a reload of the current page. * @return always null, to refresh the current page */ public boolean gradingPaused() { EnqueuedJob job = submission.enqueuedJob(); return (job != null && job.paused()); } // ---------------------------------------------------------- public boolean hasTAComments() { return result.status() == Status.CHECK && result.comments() != null && !result.comments().equals(""); } // ---------------------------------------------------------- public boolean hasNonZeroScore() { return result != null && result.finalScore() > 0.0; } // ---------------------------------------------------------- public WOComponent fileStatsDetails() { log.debug("fileStatsDetails()"); prefs().setSubmissionFileStatsRelationship(stats); WCComponent statsPage = (WCComponent)pageWithName( SubmissionFileDetailsPage.class.getName()); statsPage.nextPage = this; return statsPage; } // ---------------------------------------------------------- public WOComponent fullPrintableReport() { FullPrintableReport fullReport = pageWithName( FullPrintableReport.class); fullReport.result = result; fullReport.nextPage = this; return fullReport; } // ---------------------------------------------------------- public static String meter(double fraction) { if (blankGifUrl == null) { blankGifUrl = WCResourceManager.resourceURLFor( "images/blank.gif", "Core", null, null); } StringBuffer buffer = new StringBuffer(250); int covered = (int)(200.0 * fraction + 0.5); int uncovered = 200 - covered; buffer.append("<table class=\"percentbar\"><tr><td "); if (covered < 1) { // Completely uncovered buffer.append("class=\"minus\"><img src=\""); buffer.append(blankGifUrl); buffer.append( "\" width=\"200\" height=\"12\" alt=\"nothing covered\">"); } else if (uncovered > 0) { // Partially covered buffer.append("class=\"plus\"><img src=\""); buffer.append(blankGifUrl); buffer.append("\" width=\""); buffer.append(covered); buffer.append("\" height=\"12\" alt=\""); buffer.append((int)(100.0 * fraction + 0.5)); buffer.append(" covered\"></td><td class=\"minus\"><img src=\""); buffer.append(blankGifUrl); buffer.append("\" width=\""); buffer.append(uncovered); buffer.append("\" height=\"12\" alt=\""); buffer.append(100 - (int)(100.0 * fraction + 0.5)); buffer.append(" uncovered\">"); } else { // Completely covered buffer.append("class=\"plus\"><img src=\""); buffer.append(blankGifUrl); buffer.append( "\" width=\"200\" height=\"12\" alt=\"fully covered\">"); } buffer.append("</td></tr></table>"); return buffer.toString(); } // ---------------------------------------------------------- public String coverageMeter() { return meter(((double)stats.elementsCovered()) / ((double)stats.elements())); } // ---------------------------------------------------------- public int queuedJobCount() { ensureJobDataIsInitialized(); return jobData.queueSize; } // ---------------------------------------------------------- public int queuePosition() { ensureJobDataIsInitialized(); return jobData.queuePosition + 1; } // ---------------------------------------------------------- /** * Returns the estimated time needed to complete processing this job. * @return the most recent job wait */ public NSTimestamp estimatedWait() { ensureJobDataIsInitialized(); return new NSTimestamp(jobData.estimatedWait); } // ---------------------------------------------------------- /** * Returns the time taken to process the most recent job. * @return the most recent job wait */ public NSTimestamp mostRecentJobWait() { ensureJobDataIsInitialized(); return new NSTimestamp(jobData.mostRecentWait); } // ---------------------------------------------------------- /** * Returns the date format string for the corresponding time value * @param timeDelta the time to format * @return the time format to use */ public static String formatForSmallTime(long timeDelta) { String format = "%j days, %H:%M:%S"; final int minute = 60 * 1000; final int hour = 60 * minute; final int day = 24 * hour; if (timeDelta < minute) { format = "%S seconds"; } else if (timeDelta < hour) { format = "%M:%S minutes"; } else if (timeDelta < day) { format = "%H:%M:%S hours"; } return format; } // ---------------------------------------------------------- /** * Returns the date format string for the corresponding time value * @return the time format for the estimated job wait */ public String estimatedWaitFormat() { ensureJobDataIsInitialized(); return formatForSmallTime(jobData.estimatedWait); } // ---------------------------------------------------------- /** * Returns the date format string for the corresponding time value * @return the time format for the most recent job wait */ public String mostRecentJobWaitFormat() { ensureJobDataIsInitialized(); return formatForSmallTime(jobData.mostRecentWait); } // ---------------------------------------------------------- public Boolean showCoverageData() { if (showCoverageData == null) { showCoverageData = Boolean.valueOf(result.hasCoverageData()); } return showCoverageData; } // ---------------------------------------------------------- /** * Determine if this assignment is just for collecting submissions, * without any automated processing steps. * * @return true if the submission is just being collected */ public boolean justCollecting() { NSArray<Step> steps = result.submissionFor(user()) .assignmentOffering().assignment().steps(); return !result.summaryFile().exists() && !result.resultFile().exists() && (steps == null || steps.count() == 0); } // ---------------------------------------------------------- /** * Determine if user can submit to this assignment. * * @return true if the user can make another submission */ public boolean canSubmitAgain() { boolean answer = false; if (result != null && !showReturnToGrading) { // answer = result.submission().assignmentOffering().userCanSubmit( // wcSession().localUser() ); // This is all debugging code to figure out why we occasionally // get NPEs on the original line commented out above. Submission sub = result.submissionFor(user()); if (sub == null) { log.error("null submission for result found!"); try { log.error("result = " + result); } catch (Exception e) { log.error("unable to print result details:", e); } } else { AssignmentOffering ao = sub.assignmentOffering(); if (ao == null) { log.error("null assignment offering for submission found!"); try { log.error("result = " + result); log.error("submission = " + sub); } catch (Exception e) { log.error( "unable to print submission/result details:", e); } } else { answer = ao.userCanSubmit(user()); } } } return answer; } // ---------------------------------------------------------- /** * An action to go to the submission page for this assignment. * * @return the submission page for this assignment */ public WOComponent submitAgain() { return pageWithName( wcSession().tabs.selectById("UploadSubmission").pageName()); } // ---------------------------------------------------------- public WOComponent defaultAction() { return null; } // ---------------------------------------------------------- public Boolean showAutoGradedComments() { if (showAutoGradedComments == null) { if (submission.assignmentOffering().assignment() .submissionProfile().toolPoints() > 0.0) { showAutoGradedComments = Boolean.TRUE; } else { showAutoGradedComments = Boolean.FALSE; for (int i = 0; i < result.submissionFileStats().count(); i++) { SubmissionFileStats thisStats = result.submissionFileStats().objectAtIndex(i); if (thisStats.remarks() > 0) { showAutoGradedComments = Boolean.TRUE; break; } } } } return showAutoGradedComments; } // ---------------------------------------------------------- public String reportFileName() { String name = report.fileName(); if (name != null) { int pos = name.lastIndexOf('/'); if (pos >= 0) { name = name.substring(pos + 1); } } return name; } // ---------------------------------------------------------- static private class JobData { public NSArray<EnqueuedJob> jobs; public int queueSize; public int queuePosition; long mostRecentWait; long estimatedWait; } // ---------------------------------------------------------- private void ensureJobDataIsInitialized() { if (jobData == null) { jobData = new JobData(); NSMutableArray<EOQualifier> qualifiers = new NSMutableArray<EOQualifier>(); qualifiers.addObject(new EOKeyValueQualifier( EnqueuedJob.DISCARDED_KEY, EOQualifier.QualifierOperatorEqual, ERXConstant.integerForInt(0))); qualifiers.addObject(new EOKeyValueQualifier( EnqueuedJob.PAUSED_KEY, EOQualifier.QualifierOperatorEqual, ERXConstant.integerForInt(0))); qualifiers.addObject(new EOKeyValueQualifier( EnqueuedJob.REGRADING_KEY, EOQualifier.QualifierOperatorEqual, ERXConstant.integerForInt(0))); EOFetchSpecification fetchSpec = new EOFetchSpecification( EnqueuedJob.ENTITY_NAME, new EOAndQualifier(qualifiers), new NSArray<EOSortOrdering>(new EOSortOrdering[]{ new EOSortOrdering( EnqueuedJob.SUBMIT_TIME_KEY, EOSortOrdering.CompareAscending) })); jobData.jobs = EnqueuedJob.objectsWithFetchSpecification( localContext(), fetchSpec); jobData.queueSize = jobData.jobs.count(); if (oldQueuePos < 0 || oldQueuePos >= jobData.queueSize) { oldQueuePos = jobData.queueSize - 1; } jobData.queuePosition = jobData.queueSize; for (int i = oldQueuePos; i >= 0; i--) { if (jobData.jobs.objectAtIndex(i) == submission.enqueuedJob()) { jobData.queuePosition = i; break; } } oldQueuePos = jobData.queuePosition; if (jobData.queuePosition == jobData.queueSize) { log.error("cannot find job in queue for:" + submission); } Grader grader = Grader.getInstance(); jobData.mostRecentWait = grader.mostRecentJobWait(); jobData.estimatedWait = grader.estimatedJobTime() * (jobData.queuePosition + 1); } } // ---------------------------------------------------------- @Override public void flushNavigatorDerivedData() { submission = null; super.flushNavigatorDerivedData(); } //~ Instance/static variables ............................................. private JobData jobData; private int oldQueuePos = -1; private Boolean showCoverageData; private Boolean showAutoGradedComments; private static String blankGifUrl; static Logger log = Logger.getLogger(FinalReportPage.class); }