org.etudes.mneme.impl.SubmissionImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.etudes.mneme.impl.SubmissionImpl.java

Source

/**********************************************************************************
 * $URL$
 * $Id$
 ***********************************************************************************
 *
 * Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Etudes, Inc.
 * 
 * Portions completed before September 1, 2008
 * Copyright (c) 2007, 2008 The Regents of the University of Michigan & Foothill College, ETUDES Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 **********************************************************************************/

package org.etudes.mneme.impl;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.etudes.mneme.api.Answer;
import org.etudes.mneme.api.Assessment;
import org.etudes.mneme.api.AssessmentAccess;
import org.etudes.mneme.api.AssessmentService;
import org.etudes.mneme.api.AssessmentSubmissionStatus;
import org.etudes.mneme.api.AssessmentType;
import org.etudes.mneme.api.AttachmentService;
import org.etudes.mneme.api.Changeable;
import org.etudes.mneme.api.Expiration;
import org.etudes.mneme.api.GradingSubmissionStatus;
import org.etudes.mneme.api.MnemeService;
import org.etudes.mneme.api.Part;
import org.etudes.mneme.api.Question;
import org.etudes.mneme.api.ReviewTiming;
import org.etudes.mneme.api.SecurityService;
import org.etudes.mneme.api.Submission;
import org.etudes.mneme.api.SubmissionCompletionStatus;
import org.etudes.mneme.api.SubmissionEvaluation;
import org.etudes.mneme.api.SubmissionService;
import org.etudes.util.api.AccessAdvisor;
import org.sakaiproject.component.cover.ComponentManager;
import org.sakaiproject.entity.api.Reference;
import org.sakaiproject.entity.cover.EntityManager;
import org.sakaiproject.tool.api.SessionManager;

/**
 * SubmissionImpl implements Submission.
 */
public class SubmissionImpl implements Submission {
    /** Our logger. */
    private static Log M_log = LogFactory.getLog(SubmissionImpl.class);

    /** Dependency (optional, self-injected): AccessAdvisor. */
    protected transient AccessAdvisor accessAdvisor = null;

    protected List<Answer> answers = new ArrayList<Answer>();

    protected SubmissionAssessmentImpl assessment = null;

    protected transient AssessmentService assessmentService = null;

    protected transient AttachmentService attachmentService = null;

    protected transient String bestSubmissionId = null;

    protected SubmissionCompletionStatus completionStatus = SubmissionCompletionStatus.unknown;

    protected SubmissionEvaluationImpl evaluation = null;

    protected String id = null;

    protected Boolean isComplete = Boolean.FALSE;

    protected Boolean released = Boolean.FALSE;

    /** Track changes. */
    protected transient Changeable releasedChanged = new ChangeableImpl();

    protected Date reviewedDate = null;

    protected transient SecurityService securityService = null;

    protected transient SessionManager sessionManager = null;

    protected transient Integer siblingCount = 0;

    protected transient boolean staleEdit = false;

    protected Date startDate = null;

    protected transient SubmissionServiceImpl submissionService = null;

    protected Date submittedDate = null;

    protected Boolean testDrive = Boolean.FALSE;

    /** A value sent to setTotalScore before it is applied. */
    protected transient Float totalScoreToBe = null;

    protected transient boolean totalScoreToBeSet = false;

    protected transient Boolean ungradedSiblings = null;

    protected String userId = null;

    /**
     * Construct.
     */
    public SubmissionImpl() {
    }

    /**
     * Construct as a deep copy of another
     */
    protected SubmissionImpl(SubmissionImpl other) {
        set(other);
    }

    /**
     * {@inheritDoc}
     */
    public Boolean completeIfOver() {
        if (getIsOver(null, 0)) {
            Date over = getWhenOver();
            submissionService.autoCompleteSubmission(over, this);
            return Boolean.TRUE;
        }

        return Boolean.FALSE;
    }

    /**
     * {@inheritDoc}
     */
    public void consolidateTotalScore() {
        // check if there was a total score set
        if (!this.totalScoreToBeSet)
            return;
        this.totalScoreToBeSet = false;

        // for phantoms
        if (this.getIsPhantom()) {
            // if we have a value to set, phantoms get their evaluation set
            if (this.totalScoreToBe != null) {
                this.evaluation.setScore(this.totalScoreToBe);
            }

            return;
        }

        // adjust either the submission evaluation, or the single answer's evaluation if
        // there is a single answer only, and the submission's evaluation has not yet been set
        if (!getEvaluationUsed()) {
            // we need to have an answer
            if (this.answers.size() > 0) {
                Answer answer = answers.get(0);

                // if null, clear the evaluation score
                if (this.totalScoreToBe == null) {
                    answer.getEvaluation().setScore(null);
                }

                else {
                    // the final score "to be" will contain the auto-score for the answer (if there is one) - remove it
                    float evalScore = this.totalScoreToBe.floatValue();
                    Float autoScore = answer.getAutoScore();
                    if (autoScore != null) {
                        evalScore -= autoScore.floatValue();
                    }

                    // round away bogus decimals
                    evalScore = Math.round(evalScore * 100.0f) / 100.0f;

                    // Note: setting the final to be 0 will not cause a null answer score to become 0 from null
                    // (the grade_asssessment UI shows 0 for final score when there is no auto score and no evaluations set)
                    if ((evalScore != 0f) || (answer.getEvaluation().getScore() != null)) {
                        answer.getEvaluation().setScore(evalScore);
                    }
                }
            }
        }

        else {
            // take a null to mean clear the evaluation adjustment
            if (this.totalScoreToBe == null) {
                this.evaluation.setScore(null);
            }

            // compute the new adjustment to achieve this final score
            else {
                // the current answer total score
                float curAnswerScore = 0;
                for (Answer answer : answers) {
                    Float answerScore = answer.getTotalScore();
                    if (answerScore != null) {
                        curAnswerScore += answerScore.floatValue();
                    }
                }

                // the current total score, including the answer total and any current evaluation
                float curTotalScore = curAnswerScore;
                if (this.evaluation.getScore() != null) {
                    curTotalScore += this.evaluation.getScore().floatValue();
                }

                float total = this.totalScoreToBe.floatValue();
                this.totalScoreToBe = null;

                // if the current total is the total we want, we are done
                if (curTotalScore == total)
                    return;

                // adjust to remove the current answer score
                total -= curAnswerScore;

                // set this as the new total score
                this.evaluation.setScore(total);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean equals(Object obj) {
        // two SubmissionImpls are equals if they have the same id
        if (this == obj)
            return true;
        if ((obj == null) || (obj.getClass() != this.getClass()))
            return false;
        if ((this.id == null) || (((SubmissionImpl) obj).id == null))
            return false;
        return this.id.equals(((SubmissionImpl) obj).id);
    }

    /**
     * {@inheritDoc}
     */
    public Answer getAnswer(Question question) {
        if (question == null)
            return null;

        Answer rv = findAnswer(question.getId());
        return rv;
    }

    /**
     * {@inheritDoc}
     */
    public Answer getAnswer(String answerId) {
        for (Answer answer : this.answers) {
            if (answer.getId().equals(answerId)) {
                return answer;
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public List<Answer> getAnswers() {
        return this.answers;
    }

    /**
     * {@inheritDoc}
     */
    public Float getAnswersAutoScore() {
        // count the answer auto scores
        float total = 0;
        for (Answer answer : this.answers) {
            Float auto = answer.getAutoScore();
            if (auto != null) {
                total += auto.floatValue();
            }
        }

        // round away bogus decimals
        total = Math.round(total * 100.0f) / 100.0f;

        return Float.valueOf(total);
    }

    /**
     * {@inheritDoc}
     */
    public Assessment getAssessment() {
        return this.assessment;
    }

    /**
     * {@inheritDoc}
     */
    public AssessmentSubmissionStatus getAssessmentSubmissionStatus() {
        Date now = new Date();
        Assessment assessment = getAssessment();

        // if not open yet...
        if ((!assessment.getDates().getHideUntilOpen()) && (assessment.getDates().getOpenDate() != null)
                && now.before(assessment.getDates().getOpenDate()) && (!assessment.getFrozen())) {
            return AssessmentSubmissionStatus.future;
        }

        // if in future and hidden
        if ((assessment.getDates().getHideUntilOpen()) && (assessment.getDates().getOpenDate() != null)
                && now.before(assessment.getDates().getOpenDate()) && (!assessment.getFrozen())) {
            return AssessmentSubmissionStatus.hiddenTillOpen;
        }

        // are we past the hard end date? Or frozen?
        boolean over = ((assessment.getFrozen()) || ((assessment.getDates().getSubmitUntilDate() != null)
                && (now.after(assessment.getDates().getSubmitUntilDate()))));

        // todo (not over, not started)
        if ((getStartDate() == null) && !over) {
            // if overdue but ready
            if (assessment.getDates().getIsLate()) {
                return AssessmentSubmissionStatus.overdueReady;
            }

            return AssessmentSubmissionStatus.ready;
        }

        // if in progress...
        if ((!getIsComplete()) && (getStartDate() != null)) {
            // if timed, add an alert
            if (assessment.getTimeLimit() != null) {
                return AssessmentSubmissionStatus.inProgressAlert;
            }

            return AssessmentSubmissionStatus.inProgress;
        }

        // completed
        if (getIsComplete()) {
            // if there are fewer sibs than allowed, add the todo image as well
            if (!over && (getSiblingCount() != null) && ((assessment.getTries() == null)
                    || (getSiblingCount().intValue() < assessment.getTries()))) {
                // if overdue but ready
                if (assessment.getDates().getIsLate()) {
                    return AssessmentSubmissionStatus.overdueCompleteReady;
                }

                return AssessmentSubmissionStatus.completeReady;
            }

            return AssessmentSubmissionStatus.complete;
        }

        // over, not in progress, never completed
        if (over) {
            return AssessmentSubmissionStatus.over;
        }

        return AssessmentSubmissionStatus.other;
    }

    /**
     * {@inheritDoc}
     */
    public Submission getBest() {
        return this.bestSubmissionId == null ? this : this.submissionService.getSubmission(this.bestSubmissionId);
    }

    /**
     * {@inheritDoc}
     */
    public String getBlockedByDetails() {
        // if no advisor, not blocked
        if (this.accessAdvisor == null)
            return null;

        // check if we have blocked access - we will return this message if blocked (null if not blocked).
        String blockedByTitle = this.accessAdvisor.details("sakai.mneme", getAssessment().getContext(),
                getAssessment().getId(), getUserId());
        return blockedByTitle;
    }

    /**
     * {@inheritDoc}
     */
    public String getBlockedByTitle() {
        // if no advisor, not blocked
        if (this.accessAdvisor == null)
            return null;

        // check if we have blocked access - we will return this message if blocked (null if not blocked).
        String blockedByTitle = this.accessAdvisor.message("sakai.mneme", getAssessment().getContext(),
                getAssessment().getId(), getUserId());
        return blockedByTitle;
    }

    /**
     * {@inheritDoc}
     */
    public Reference getCertReference() {
        Reference ref = EntityManager.newReference("/mneme/" + AttachmentService.DOWNLOAD + "/"
                + AttachmentService.ASMT_CERT + "/" + this.getAssessment().getContext() + "/" + this.getId());
        return ref;
    }

    /**
     * {@inheritDoc}
     */
    public SubmissionCompletionStatus getCompletionStatus() {
        return this.completionStatus;
    }

    /**
     * {@inheritDoc}
     */
    public Long getElapsedTime() {
        if ((submittedDate == null) || (startDate == null))
            return null;

        return new Long(submittedDate.getTime() - startDate.getTime());
    }

    /**
     * {@inheritDoc}
     */
    public Date getEvaluatedDate() {
        Date rv = this.getEvaluation().getAttribution().getDate();
        for (Answer answer : this.answers) {
            Date d = answer.getEvaluation().getAttribution().getDate();
            if (d != null) {
                if (rv == null) {
                    rv = d;
                } else if (d.after(rv)) {
                    rv = d;
                }
            }
        }

        return rv;
    }

    /**
     * {@inheritDoc}
     */
    public SubmissionEvaluation getEvaluation() {
        return this.evaluation;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getEvaluationNotReviewed() {
        if (!getIsComplete())
            return Boolean.FALSE;
        if (getEvaluatedDate() == null)
            return Boolean.FALSE;
        if (!getStudentMayReview())
            return Boolean.FALSE;

        // submissions that are non-eval need to have a comment, and to be released, to allow a review - without this, don't mark it as eval not reviewed
        // was this submission completed by other than the evaluation process for a non-submit?
        if (this.completionStatus == SubmissionCompletionStatus.evaluationNonSubmit) {
            // not released
            if (!getIsReleased())
                return Boolean.FALSE;

            // find a comment
            boolean foundComment = false;
            if (getEvaluation().getComment() != null) {
                foundComment = true;
            } else {
                for (Answer answer : getAnswers()) {
                    if (answer.getEvaluation().getComment() != null) {
                        foundComment = true;
                        break;
                    }
                }
            }

            // if we find no comment
            if (!foundComment)
                return Boolean.FALSE;
        }

        // not released
        if (!getIsReleased())
            return Boolean.FALSE;

        // never reviewed
        if (getReviewedDate() == null)
            return Boolean.TRUE;

        // needs review if reviewed before the latest evaluation date
        return getReviewedDate().before(getEvaluatedDate());
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getEvaluationUsed() {
        // multiple answers or evaluation already in use (or phantom)
        if ((this.answers.size() > 1) || (this.getEvaluation().getDefined()) || this.getIsPhantom())
            return Boolean.TRUE;

        return Boolean.FALSE;
    }

    /**
     * {@inheritDoc}
     */
    public String getEvaluationVersion() {
        // start with the overall submission date (could be null)
        Date rv = getEvaluation().getAttribution().getDate();

        // see if any answer evaluation date is later
        for (Answer answer : getAnswers()) {
            if (answer.getEvaluation().getAttribution().getDate() != null) {
                if (rv == null) {
                    rv = answer.getEvaluation().getAttribution().getDate();
                } else if (answer.getEvaluation().getAttribution().getDate().after(rv)) {
                    rv = answer.getEvaluation().getAttribution().getDate();
                }
            }
        }

        if (rv == null)
            return null;
        return Long.toString(rv.getTime());
    }

    /**
     * {@inheritDoc}
     */
    public Expiration getExpiration() {
        // TODO: thread caching
        ExpirationImpl rv = new ExpirationImpl();

        Date now = new Date();

        // the end might be from a time limit, or because we are near the closed date
        long endTime = 0;

        // see if the assessment has a hard submission cutoff date (ignore for test drive)

        Date closedDate = null;
        if (!getIsTestDrive()) {
            closedDate = getAssessment().getDates().getSubmitUntilDate();
        }
        rv.time = closedDate;

        // if we have a time limit, compute the end time based on that limit
        Long limit = getAssessment().getTimeLimit();
        if (limit != null) {
            rv.limit = limit;

            // if we have started, compute the end from the start
            long startTime = 0;
            Date startDate = getStartDate();
            if (startDate != null) {
                startTime = startDate.getTime();
            }

            // if we have not started, compute the end from now
            else {
                startTime = now.getTime();
            }

            // a full time limit duration would end here
            endTime = startTime + limit.longValue();

            // if there's a closed date on the assessment, that falls before that full duration would be, that's the end time
            if ((closedDate != null) && (closedDate.getTime() < endTime)) {
                endTime = closedDate.getTime();
                rv.cause = Expiration.Cause.closedDate;
            }

            else {
                rv.cause = Expiration.Cause.timeLimit;
            }
        }

        // if we are not timed, compute an end time based on the assessment's closed date
        else {
            // not timed, no close date, we don't expire
            if (closedDate == null)
                return null;

            // the closeDate is the end time
            endTime = closedDate.getTime();

            // if this closed date is more than 2 hours from now, ignore it and say we have no expiration
            if (endTime > now.getTime() + (2l * 60l * 60l * 1000l))
                return null;

            // set the limit to 2 hours
            rv.limit = 2l * 60l * 60l * 1000l;

            rv.cause = Expiration.Cause.closedDate;
        }

        // how long from now till endTime?
        long tillExpires = endTime - now.getTime();
        if (tillExpires <= 0)
            tillExpires = 0;

        rv.duration = new Long(tillExpires);

        return rv;
    }

    /**
     * {@inheritDoc}
     */
    public Question getFirstIncompleteQuestion() {
        Assessment assessment = getAssessment();

        for (Part part : assessment.getParts().getParts()) {
            for (Question question : part.getQuestions()) {
                if (!getIsCompleteQuestion(question).booleanValue()) {
                    return question;
                }
            }
        }

        return null;
    }

    /**
     * {@inheritDoc}
     */
    public Question getFirstQuestion() {
        Assessment assessment = getAssessment();
        if (assessment.getParts().getParts().isEmpty())
            return null;
        Part part = assessment.getParts().getParts().get(0);
        List<Question> questions = part.getQuestions();
        if (questions.isEmpty())
            return null;
        return questions.get(0);
    }

    /**
     * {@inheritDoc}
     */
    public GradingSubmissionStatus getGradingStatus() {
        Date now = new Date();
        Assessment assessment = getAssessment();

        // not started yet: future or notStarted or closed
        if (getStartDate() == null) {
            // if not open yet...
            if ((assessment.getDates().getOpenDate() != null) && now.before(assessment.getDates().getOpenDate())) {
                return GradingSubmissionStatus.future;
            }

            // // if closed for submission
            // if (assessment.getIsClosed())
            // {
            // return GradingSubmissionStatus.closed;
            // }

            return GradingSubmissionStatus.notStarted;
        }

        // if in progress...
        if (!getIsComplete()) {
            return GradingSubmissionStatus.inProgress;
        }

        // complete: released, evaluated or submitted

        // released?
        if (getIsReleased()) {
            return GradingSubmissionStatus.released;
        }

        // evaluated?
        if (getEvaluation().getEvaluated()) {
            return GradingSubmissionStatus.evaluated;
        }

        // submitted
        return GradingSubmissionStatus.submitted;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getHasUngradedSiblings() {
        return this.ungradedSiblings;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getHasUnscoredAnswers() {
        if (!getIsComplete())
            return Boolean.FALSE;

        // if marked as evaluated, it is no longer candidate for unscored
        if (this.evaluation.getEvaluated().booleanValue())
            return Boolean.FALSE;

        // submissions created in the evaluation process are never considered unscored
        if (this.getCompletionStatus() == SubmissionCompletionStatus.evaluationNonSubmit)
            return Boolean.FALSE;

        // if the overall score has been set, none of the answers are considered unscored
        if (this.evaluation.getScore() != null)
            return Boolean.FALSE;

        for (Answer answer : getAnswers()) {
            if ((answer.getIsAnswered()) && (answer.getTotalScore() == null)
                    && (!answer.getQuestion().getIsSurvey().booleanValue())) {
                return Boolean.TRUE;
            }
        }

        return Boolean.FALSE;
    }

    /**
     * {@inheritDoc}
     */
    public String getId() {
        return this.id;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsAnswered() {
        Assessment a = getAssessment();

        // for each section / question, make sure we have an answer and not a mark for review
        // only consider questions that are selected to be part of the test for this submission!
        for (Part part : a.getParts().getParts()) {
            for (Question question : part.getQuestions()) {
                Answer answer = this.findAnswer(question.getId());
                if (answer == null)
                    return Boolean.FALSE;
                if (answer.getSubmittedDate() == null)
                    return Boolean.FALSE;
                if ((answer.getIsAnswered() == null) || (!answer.getIsAnswered().booleanValue()))
                    return Boolean.FALSE;
                if ((answer.getMarkedForReview() != null && (answer.getMarkedForReview().booleanValue())))
                    return Boolean.FALSE;
            }
        }

        return Boolean.TRUE;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsAutoCompleted() {
        if (!getIsComplete())
            return Boolean.FALSE;

        if (this.completionStatus == SubmissionCompletionStatus.unknown) {
            // TODO: try to determine
            return Boolean.FALSE;
        }

        return this.completionStatus == SubmissionCompletionStatus.autoComplete;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsComplete() {
        return this.isComplete;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsCompletedLate() {
        // must be completed
        if (!getIsComplete())
            return Boolean.FALSE;

        // assessment must have accept until and due dates
        if ((getAssessment().getDates().getAcceptUntilDate() == null)
                || (getAssessment().getDates().getDueDate() == null))
            return Boolean.FALSE;

        // if after the due date, we were completed late
        if ((getSubmittedDate() != null) && getSubmittedDate().after(getAssessment().getDates().getDueDate()))
            return Boolean.TRUE;

        return Boolean.FALSE;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsCompleteQuestion(Question question) {
        if (question == null)
            throw new IllegalArgumentException();

        Answer answer = findAnswer(question.getId());
        if ((answer != null) && (answer.getIsComplete().booleanValue())) {
            return true;
        }

        return false;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsNonEvalOrCommented() {
        // was this submission completed by other than the evaluation process for a non-submit?
        if (this.completionStatus != SubmissionCompletionStatus.evaluationNonSubmit)
            return Boolean.TRUE;

        // does this submission have any comments? Consider both at the submission and question level... only if released
        if (getIsReleased()) {
            if (getEvaluation().getComment() != null)
                return Boolean.TRUE;

            for (Answer answer : getAnswers()) {
                if (answer.getEvaluation().getComment() != null) {
                    return Boolean.TRUE;
                }
            }
        }

        return Boolean.FALSE;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsNonSubmit() {
        if (!getIsComplete())
            return Boolean.FALSE;

        if (this.completionStatus == SubmissionCompletionStatus.unknown) {
            // TODO: try to determine
            return Boolean.FALSE;
        }

        return this.completionStatus == SubmissionCompletionStatus.evaluationNonSubmit;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsOver(Date asOf, long grace) {
        Date over = getWhenOver();
        if (over == null)
            return Boolean.FALSE;

        // set the time to now if missing
        if (asOf == null)
            asOf = new Date();

        // if frozen, it's over
        if (getAssessment().getFrozen())
            return Boolean.TRUE;

        // otherwise check the date
        return Boolean.valueOf(asOf.getTime() > over.getTime() + grace);
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsPhantom() {
        return this.id.startsWith(SubmissionService.PHANTOM_PREFIX);
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsReleased() {
        return this.released;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsStaleEdit() {
        return this.staleEdit;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsStarted() {
        return this.startDate != null;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsTestDrive() {
        return this.testDrive;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getIsUnanswered() {
        Assessment a = getAssessment();

        // phantoms, or real submissions with no answers are unanswered
        if (getIsPhantom())
            return Boolean.TRUE;
        if (getAnswers().isEmpty())
            return Boolean.TRUE;

        // look for a question that is answered
        for (Part part : a.getParts().getParts()) {
            for (Question question : part.getQuestions()) {
                Answer answer = this.findAnswer(question.getId());
                if (answer == null)
                    continue;
                if (answer.getSubmittedDate() == null)
                    continue;
                if (answer.getIsAnswered() == Boolean.TRUE)
                    return Boolean.FALSE;
            }
        }

        return Boolean.TRUE;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getMayBegin() {
        // ASSSUMPTION: this will be a submission with sibling count, and it will be:
        // - the placeholder, if none other exist
        // - the one in progress, if there is one
        // - the "official" completed one, if any

        // not yet started
        if (getStartDate() != null)
            return Boolean.FALSE;

        // published (test drive need not be)
        if (!getIsTestDrive()) {
            if (!getAssessment().getPublished())
                return Boolean.FALSE;
        }

        // valid
        if (!getAssessment().getIsValid())
            return Boolean.FALSE;

        // not frozen
        if (!getIsTestDrive()) {
            if (getAssessment().getFrozen())
                return Boolean.FALSE;
        }

        // assessment is open (allow test drive submissions to skip this)
        if (!getIsTestDrive()) {
            if (!getAssessment().getDates().getIsOpen(Boolean.FALSE))
                return Boolean.FALSE;
        }

        // permission - userId must have SUBMIT_PERMISSION in the context of the assessment
        // test drive can instead use manage permission
        if (!getIsTestDrive()) {
            if (!this.securityService.checkSecurity(this.sessionManager.getCurrentSessionUserId(),
                    MnemeService.SUBMIT_PERMISSION, getAssessment().getContext()))
                return Boolean.FALSE;
        } else {
            if (!this.securityService.checkSecurity(this.sessionManager.getCurrentSessionUserId(),
                    MnemeService.MANAGE_PERMISSION, getAssessment().getContext()))
                return Boolean.FALSE;
        }

        // under limit (test drive can skip this)
        if (!getIsTestDrive()) {
            if ((getAssessment().getTries() != null) && (this.getSiblingCount() >= getAssessment().getTries()))
                return Boolean.FALSE;
        }

        // one last test! If not in test drive, and we have an access advisor, see if it wants to block things
        if ((this.accessAdvisor != null) && (!getIsTestDrive())) {
            if (this.accessAdvisor.denyAccess("sakai.mneme", getAssessment().getContext(), getAssessment().getId(),
                    this.sessionManager.getCurrentSessionUserId())) {
                return Boolean.FALSE;
            }
        }

        return Boolean.TRUE;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getMayBeginAgain() {
        return getMayBeginAgain(this.sessionManager.getCurrentSessionUserId());
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getMayBeginAgain(String userId) {
        // ASSSUMPTION: this will be a submission with sibling count, and it will be:
        // - the placeholder, if none other exist
        // - the one in progress, if there is one
        // - the "official" completed one, if any

        // submission is complete
        if (!getIsComplete())
            return Boolean.FALSE;

        // published (test drive need not be)
        if (!getIsTestDrive()) {
            if (!getAssessment().getPublished())
                return Boolean.FALSE;
        }

        // valid
        if (!getAssessment().getIsValid())
            return Boolean.FALSE;

        // not frozen
        if (!getIsTestDrive()) {
            if (getAssessment().getFrozen())
                return Boolean.FALSE;
        }

        // assessment is open (allow test drive submissions to skip this)
        if (!getIsTestDrive()) {
            if (!getAssessment().getDates().getIsOpen(Boolean.FALSE))
                return Boolean.FALSE;
        }

        // under limit (test drive can skip this)
        if (!getIsTestDrive()) {
            if ((getAssessment().getTries() != null)
                    && (this.getSiblingCount().intValue() >= getAssessment().getTries().intValue()))
                return Boolean.FALSE;
        }

        // permission - userId must have SUBMIT_PERMISSION in the context of the assessment
        // test drive can instead use manage permission
        if (!getIsTestDrive()) {
            if (!this.securityService.checkSecurity(userId, MnemeService.SUBMIT_PERMISSION,
                    getAssessment().getContext()))
                return Boolean.FALSE;
        } else {
            if (!this.securityService.checkSecurity(userId, MnemeService.MANAGE_PERMISSION,
                    getAssessment().getContext()))
                return Boolean.FALSE;
        }

        // one last test! If not in test drive, and we have an access advisor, see if it wants to block things
        if ((this.accessAdvisor != null) && (!getIsTestDrive())) {
            if (this.accessAdvisor.denyAccess("sakai.mneme", getAssessment().getContext(), getAssessment().getId(),
                    userId)) {
                return Boolean.FALSE;
            }
        }

        return Boolean.TRUE;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getMayContinue() {
        // submission has been started
        if (getStartDate() == null)
            return Boolean.FALSE;

        // submission not complete
        if (getIsComplete())
            return Boolean.FALSE;

        // same user
        if (!this.sessionManager.getCurrentSessionUserId().equals(getUserId()))
            return Boolean.FALSE;

        // published (test drive need not be)
        if (!getIsTestDrive()) {
            if (!getAssessment().getPublished())
                return Boolean.FALSE;
        }

        // valid
        if (!getAssessment().getIsValid())
            return Boolean.FALSE;

        // not frozen
        if (!getIsTestDrive()) {
            if (getAssessment().getFrozen())
                return Boolean.FALSE;
        }

        // assessment is open (allow test drive submissions to skip this)
        if (!getIsTestDrive()) {
            if (!getAssessment().getDates().getIsOpen(Boolean.FALSE))
                return Boolean.FALSE;
        }

        // permission - userId must have SUBMIT_PERMISSION in the context of the assessment
        // test drive can instead use manage permission
        if (!getIsTestDrive()) {
            if (!this.securityService.checkSecurity(this.sessionManager.getCurrentSessionUserId(),
                    MnemeService.SUBMIT_PERMISSION, getAssessment().getContext()))
                return Boolean.FALSE;
        } else {
            if (!this.securityService.checkSecurity(this.sessionManager.getCurrentSessionUserId(),
                    MnemeService.MANAGE_PERMISSION, getAssessment().getContext()))
                return Boolean.FALSE;
        }

        // one last test! If not in test drive, and we have an access advisor, see if it wants to block things
        if ((this.accessAdvisor != null) && (!getIsTestDrive())) {
            if (this.accessAdvisor.denyAccess("sakai.mneme", getAssessment().getContext(), getAssessment().getId(),
                    this.sessionManager.getCurrentSessionUserId())) {
                return Boolean.FALSE;
            }
        }

        return Boolean.TRUE;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getMayGuestView() {
        // ASSSUMPTION: this will be a placeholder submission

        // published
        if (!getAssessment().getPublished())
            return Boolean.FALSE;

        // valid
        if (!getAssessment().getIsValid())
            return Boolean.FALSE;

        // assessment is open
        if (!getAssessment().getDates().getIsOpen(Boolean.FALSE))
            return Boolean.FALSE;

        // permission - userId must have GUEST_PERMISSION in the context of the assessment
        if (!this.securityService.checkSecurity(this.sessionManager.getCurrentSessionUserId(),
                MnemeService.GUEST_PERMISSION, getAssessment().getContext()))
            return Boolean.FALSE;

        return Boolean.TRUE;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getMayReview() {
        // same user
        if (!this.sessionManager.getCurrentSessionUserId().equals(getUserId()))
            return Boolean.FALSE;

        return getStudentMayReview();
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getMayReviewLater() {
        // same user
        if (!this.sessionManager.getCurrentSessionUserId().equals(getUserId()))
            return Boolean.FALSE;

        // submission complete
        if (!getIsComplete().booleanValue())
            return Boolean.FALSE;

        // published (test drive need not be)
        if (!getIsTestDrive()) {
            if (!getAssessment().getPublished())
                return Boolean.FALSE;
        }

        // valid
        if (!getAssessment().getIsValid())
            return Boolean.FALSE;

        // assessment not set to no review
        if (getAssessment().getReview().getTiming() == ReviewTiming.never)
            return Boolean.FALSE;

        // or that it is set to date with no date
        // Note: I don't like the redundancy of this code with AssessmentReviewImpl -ggolden
        if ((getAssessment().getReview().getTiming() == ReviewTiming.date)
                && (getAssessment().getReview().getDate() == null))
            return Boolean.FALSE;

        // TODO: permission?

        return Boolean.TRUE;
    }

    /**
     * {@inheritDoc}
     */
    public Boolean getMayViewWork() {
        // user must have manage permission
        if (!this.securityService.checkSecurity(this.sessionManager.getCurrentSessionUserId(),
                MnemeService.MANAGE_PERMISSION, getAssessment().getContext()))
            return Boolean.FALSE;

        // submission must be complete
        if (!getIsComplete())
            return Boolean.FALSE;

        // assessment must not be formal eval or survey
        if (this.getAssessment().getFormalCourseEval())
            return Boolean.FALSE;
        if (this.getAssessment().getType() == AssessmentType.survey)
            return Boolean.FALSE;

        return Boolean.TRUE;
    }

    /**
     * {@inheritDoc}
     */
    public String getReference() {
        return this.submissionService.getSubmissionReference(this.id);
    }

    /**
     * {@inheritDoc}
     */
    public Date getReviewedDate() {
        return this.reviewedDate;
    }

    /**
     * {@inheritDoc}
     */
    public Integer getSiblingCount() {
        return this.siblingCount;
    }

    /**
     * {@inheritDoc}
     */
    public AssessmentAccess getSpecialAccess() {
        return getAssessment().getSpecialAccess().getUserAccess(getUserId());
    }

    /**
     * {@inheritDoc}
     */
    public Date getStartDate() {
        return this.startDate;
    }

    /**
     * {@inheritDoc}
     */
    public Date getSubmittedDate() {
        return this.submittedDate;
    }

    /**
     * {@inheritDoc}
     */
    public Float getTotalScore() {
        // phantoms don't have a total score
        if (getIsPhantom())
            return null;

        // add up the scores from the answers
        float total = 0;
        for (Answer answer : answers) {
            Float score = answer.getTotalScore();
            if (score != null) {
                total += score.floatValue();
            }
        }

        // add in the submission evaluation score if set
        if (this.evaluation.getScore() != null) {
            total += this.evaluation.getScore().floatValue();
        }

        // round away bogus decimals
        total = Math.round(total * 100.0f) / 100.0f;

        return Float.valueOf(total);
    }

    /**
     * {@inheritDoc}
     */
    public String getUserId() {
        return this.userId;
    }

    /**
     * {@inheritDoc}
     */
    public Date getWhenOver() {
        // if we have not been started, we are not over
        if (getStartDate() == null)
            return null;

        // if we are complete, we are not over
        if (getIsComplete())
            return null;

        Assessment a = getAssessment();
        Date rv = null;

        // for timed
        if ((a.getTimeLimit() != null) && (a.getTimeLimit() > 0)) {
            // pick up the end time
            rv = new Date(getStartDate().getTime() + a.getTimeLimit());
        }

        // for hard submit-until date (for test drive, ignore)
        if (!getIsTestDrive()) {
            if (a.getDates().getSubmitUntilDate() != null) {
                if ((rv == null) || (a.getDates().getSubmitUntilDate().before(rv))) {
                    rv = a.getDates().getSubmitUntilDate();
                }
            }
        }

        // if frozen
        if (a.getFrozen()) {
            // use now
            rv = new Date();
        }

        return rv;
    }

    /**
     * {@inheritDoc}
     */
    public int hashCode() {
        return getId() == null ? "null".hashCode() : getId().hashCode();
    }

    /**
     * Final initialization, once all dependencies are set.
     */
    public void init() {
        this.assessment = new SubmissionAssessmentImpl(null, this, this.assessmentService);
        this.evaluation = new SubmissionEvaluationImpl(this, this.attachmentService);

        // check if there is an access advisor - if not, that's ok.
        this.accessAdvisor = (AccessAdvisor) ComponentManager.get(AccessAdvisor.class);
    }

    /**
     * Dependency: AssessmentService.
     * 
     * @param service
     *        The AssessmentService.
     */
    public void setAssessmentService(AssessmentService service) {
        this.assessmentService = service;
    }

    /**
     * Dependency: AttachmentService.
     * 
     * @param service
     *        The AttachmentService.
     */
    public void setAttachmentService(AttachmentService service) {
        this.attachmentService = service;
    }

    /**
     * {@inheritDoc}
     */
    public void setCompletionStatus(SubmissionCompletionStatus status) {
        this.completionStatus = status;
    }

    /**
     * {@inheritDoc}
     */
    public void setEvaluationVersion(String version) {
        // if this date is not the same as the current computed evaluation date, we may be dealing with old data being edited
        String expected = getEvaluationVersion();
        if (Different.different(version, expected)) {
            this.staleEdit = true;
        }
    }

    /**
     * {@inheritDoc}
     */
    public void setIsComplete(Boolean complete) {
        if (complete == null)
            throw new IllegalArgumentException();
        this.isComplete = complete;
    }

    /**
     * {@inheritDoc}
     */
    public void setIsReleased(Boolean released) {
        if (released == null)
            throw new IllegalArgumentException();
        if (this.released.equals(released))
            return;

        this.released = released;

        this.releasedChanged.setChanged();
    }

    /**
     * {@inheritDoc}
     */
    public void setReviewedDate(Date date) {
        this.reviewedDate = date;
    }

    /**
     * Dependency: SecurityService.
     * 
     * @param service
     *        The SecurityService.
     */
    public void setSecurityService(SecurityService service) {
        this.securityService = service;
    }

    /**
     * Dependency: SessionManager.
     * 
     * @param service
     *        The SessionManager.
     */
    public void setSessionManager(SessionManager service) {
        this.sessionManager = service;
    }

    /**
     * {@inheritDoc}
     */
    public void setStartDate(Date startDate) {
        this.startDate = startDate;
    }

    /**
     * Dependency: SubmissionService.
     * 
     * @param service
     *        The SubmissionService.
     */
    public void setSubmissionService(SubmissionServiceImpl service) {
        this.submissionService = service;
    }

    /**
     * {@inheritDoc}
     */
    public void setSubmittedDate(Date submittedDate) {
        this.submittedDate = submittedDate;
    }

    /**
     * {@inheritDoc}
     */
    public void setTotalScore(Float score) {
        // save to process in consolidate
        this.totalScoreToBe = score;
        this.totalScoreToBeSet = true;
    }

    /**
     * Clear all answer information.
     */
    protected void clearAnswers() {
        this.answers.clear();
    }

    /**
     * Clear the changed flags.
     */
    protected void clearIsChanged() {
        this.releasedChanged.clearChanged();
        this.evaluation.clearIsChanged();
        if (this.answers != null) {
            for (Answer a : this.answers) {
                ((AnswerImpl) a).clearIsChanged();
            }
        }
    }

    /**
     * Clear the released changed flags.
     */
    protected void clearReleasedIsChanged() {
        this.releasedChanged.clearChanged();
    }

    /**
     * Find an existing answer in the submission for this question id.
     * 
     * @param questionId
     *        The question id.
     * @return The existing answer in the submission for this question id, or null if not found.
     */
    protected Answer findAnswer(String questionId) {
        // find the answer to this assessment question
        for (Answer answer : this.answers) {
            if (((AnswerImpl) answer).questionId.equals(questionId)) {
                return answer;
            }
        }

        return null;
    }

    /**
     * Access the assessment id.
     * 
     * @return The assessment id.
     */
    protected String getAssessmentId() {
        return this.assessment.getId();
    }

    /**
     * Check if there were any changes.
     * 
     * @return TRUE if any changes, FALSE if not.
     */
    protected Boolean getIsChanged() {
        return this.releasedChanged.getChanged() || this.evaluation.getIsChanged();
    }

    /**
     * Check if there were any changes to the released setting.
     * 
     * @return TRUE if any changes to the released settings, FALSE if not.
     */
    protected Boolean getIsReleasedChanged() {
        return this.releasedChanged.getChanged();
    }

    /**
     * @return getMayReview test, but don't check if the current user is the student.
     */
    protected Boolean getStudentMayReview() {
        // submission complete
        if (!getIsComplete())
            return Boolean.FALSE;

        // published (test drive need not be)
        if (!getIsTestDrive()) {
            if (!getAssessment().getPublished())
                return Boolean.FALSE;
        }

        // valid
        if (!getAssessment().getIsValid())
            return Boolean.FALSE;

        // assessment review enabled
        if (!getAssessment().getReview().getNowAvailable())
            return Boolean.FALSE;

        // TODO: permission?

        return Boolean.TRUE;
    }

    /**
     * Establish another answer.
     * 
     * @param answer
     *        The answer.
     */
    protected void initAnswer(AnswerImpl answer) {
        answer.initSubmission(this);
        this.answers.add(answer);
    }

    /**
     * Initialize the assessment id property.
     * 
     * @param id
     *        The assessment id property.
     */
    protected void initAssessmentId(String id) {
        this.assessment.assessmentId = id;
    }

    /**
     * Initialize the best.
     */
    protected void initBest(Submission best) {
        this.bestSubmissionId = best.getId();
    }

    /**
     * Initialize the completion status.
     */
    protected void initCompletionStatus(String statusCode) {
        if ((statusCode != null) && (statusCode.length() > 0)) {
            this.completionStatus = SubmissionCompletionStatus.getFromEncoding(statusCode);
        } else {
            this.completionStatus = SubmissionCompletionStatus.unknown;
        }
    }

    /**
     * Initialize the id property.
     * 
     * @param id
     *        The id property.
     */
    protected void initId(String id) {
        this.id = id;
    }

    /**
     * Initialize the released setting.
     * 
     * @param released
     *        The released setting.
     */
    protected void initReleased(Boolean released) {
        this.released = released;
    }

    /**
     * Initialize the sibling count.
     */
    protected void initSiblingCount(Integer count) {
        this.siblingCount = count;
    }

    /**
     * Initialize the test-drive setting.
     * 
     * @param testDrive
     *        The test-drive setting.
     */
    protected void initTestDrive(Boolean testDrive) {
        this.testDrive = testDrive;
    }

    /**
     * Initialize the ungraded siblings setting.
     */
    protected void initUngradedSiblings(Boolean unscoredSiblings) {
        this.ungradedSiblings = unscoredSiblings;
    }

    /**
     * Initialize the user id property.
     * 
     * @param userId
     *        The user id property.
     */
    protected void initUserId(String userId) {
        this.userId = userId;
    }

    /**
     * Replace an existing answer with this one, or add it if there is no existing one.
     * 
     * @param answer
     *        The answer.
     */
    protected void replaceAnswer(AnswerImpl answer) {
        // preserve the (question) order
        for (Answer current : this.answers) {
            if (((AnswerImpl) current).questionId.equals(answer.questionId)) {
                ((AnswerImpl) current).set(answer, this);
                return;
            }
        }

        // add it
        answer.initSubmission(this);
        this.answers.add(answer);
    }

    /**
     * Set as a copy of another.
     * 
     * @param other
     *        The other to copy.
     */
    protected void set(SubmissionImpl other) {
        this.answers.clear();
        for (Answer answer : other.answers) {
            AnswerImpl a = new AnswerImpl((AnswerImpl) answer, this);
            this.answers.add(a);
        }

        setMain(other);
    }

    /**
     * Set as a copy of another - main parts only, no answers.
     * 
     * @param other
     *        The other to copy.
     */
    protected void setMain(SubmissionImpl other) {
        this.assessment = new SubmissionAssessmentImpl(other.assessment, this);
        this.assessmentService = other.assessmentService;
        this.attachmentService = other.attachmentService;
        this.bestSubmissionId = other.bestSubmissionId;
        this.completionStatus = other.completionStatus;
        this.evaluation = new SubmissionEvaluationImpl(other.evaluation, this);
        this.released = other.released;
        this.releasedChanged = new ChangeableImpl(other.releasedChanged);
        this.id = other.id;
        this.isComplete = other.isComplete;
        this.reviewedDate = other.reviewedDate;
        this.securityService = other.securityService;
        this.sessionManager = other.sessionManager;
        this.siblingCount = other.siblingCount;
        this.startDate = other.startDate;
        this.submissionService = other.submissionService;
        this.submittedDate = other.submittedDate;
        this.testDrive = other.testDrive;
        this.ungradedSiblings = other.ungradedSiblings;
        this.userId = other.userId;
    }
}