org.etudes.mneme.tool.QuestionView.java Source code

Java tutorial

Introduction

Here is the source code for org.etudes.mneme.tool.QuestionView.java

Source

/**********************************************************************************
 * $URL$
 * $Id$
 ***********************************************************************************
 *
 * Copyright (c) 2008, 2009, 2010, 2011, 2012, 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.tool;

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

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.etudes.ambrosia.api.Context;
import org.etudes.ambrosia.util.ControllerImpl;
import org.etudes.mneme.api.Answer;
import org.etudes.mneme.api.Assessment;
import org.etudes.mneme.api.AssessmentClosedException;
import org.etudes.mneme.api.AssessmentCompletedException;
import org.etudes.mneme.api.AssessmentPermissionException;
import org.etudes.mneme.api.AssessmentService;
import org.etudes.mneme.api.Part;
import org.etudes.mneme.api.Question;
import org.etudes.mneme.api.QuestionGrouping;
import org.etudes.mneme.api.Submission;
import org.etudes.mneme.api.SubmissionCompletedException;
import org.etudes.mneme.api.SubmissionService;
import org.sakaiproject.tool.api.ToolManager;
import org.sakaiproject.util.StringUtil;
import org.sakaiproject.util.Web;

/**
 * The /question view for the mneme tool.
 */
public class QuestionView extends ControllerImpl {
    /** Our log. */
    private static Log M_log = LogFactory.getLog(QuestionView.class);

    /** Dependency: AssessmentService. */
    protected AssessmentService assessmentService = null;

    /** Dependency: SubmissionService. */
    protected SubmissionService submissionService = null;

    /** tool manager reference. */
    protected ToolManager toolManager = null;

    /**
     * Shutdown.
     */
    public void destroy() {
        M_log.info("destroy()");
    }

    /**
     * {@inheritDoc}
     */
    public void get(HttpServletRequest req, HttpServletResponse res, Context context, String[] params)
            throws IOException {
        // sid/question selector/anchor and return
        // question selector may end in "!" for "last chance" - linear repeat question when it was not answered
        if (params.length < 5) {
            throw new IllegalArgumentException();
        }

        String submissionId = params[2];
        String questionSelector = params[3];
        boolean lastChance = false;
        if (questionSelector.endsWith("!")) {
            questionSelector = questionSelector.substring(0, questionSelector.length() - 1);
            lastChance = true;
        }
        String anchor = params[4];
        if (anchor.equals("-"))
            anchor = null;

        String destination = null;
        if (params.length > 5) {
            destination = "/" + StringUtil.unsplit(params, 5, params.length - 5, "/");
        }
        // if not specified, go to the main list view
        else {
            destination = "/list";
        }
        context.put("return", destination);

        // adjust the current destination to remove the anchor
        params[4] = "-";
        String curDestination = "/" + StringUtil.unsplit(params, 1, params.length - 1, "/");
        context.put("curDestination", curDestination);

        Submission submission = submissionService.getSubmission(submissionId);

        if (submission == null) {
            // redirect to error
            res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.invalid)));
            return;
        }

        // if not in-progress (i.e. already closed)
        if (submission.getIsComplete()) {
            // redirect to error
            res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.over)));
            return;
        }

        // validity check
        if (!submission.getAssessment().getIsValid()) {
            // redirect to error
            res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.unauthorized)));
            return;
        }

        // handle our 'z' selector - redirect to the appropriate question to re-enter this submission
        if ("z".equals(questionSelector)) {
            try {
                // Note: enterSubmission() can create a new submission, if the submission is not in progress.
                // Our submission has no sibling count (=0), since we used getSubmission(), which does not set it.
                // enterSubmission() will think there are no other submissions, so would create one even if the max allowed
                // for the user has been exceeded. Since we check getIsComplete() above, this should be avoided -ggolden
                this.submissionService.enterSubmission(submission);
            } catch (AssessmentPermissionException e) {
                // redirect to error
                res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.unauthorized)));
                return;
            } catch (AssessmentClosedException e) {
                // redirect to error
                res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.invalid)));
                return;
            } catch (AssessmentCompletedException e) {
                // redirect to error
                res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.invalid)));
                return;
            }

            redirectToQuestion(req, res, submission, true, false, destination);
            return;
        }

        // if the submission has past a hard deadline or ran out of time, close it and tell the user
        if (submission.completeIfOver()) {
            // redirect to error
            res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.over)));
            return;
        }

        context.put("actionTitle", messages.getString("question-header-work"));

        // collect the questions (actually their answers) to put on the page
        List<Answer> answers = new ArrayList<Answer>();

        Errors err = questionSetup(submission, questionSelector, context, answers, true);
        if (err != null) {
            // redirect to error
            res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + err + "/" + submissionId)));
            return;
        }

        // for the tool navigation
        if (this.assessmentService.allowManageAssessments(toolManager.getCurrentPlacement().getContext())) {
            context.put("maintainer", Boolean.TRUE);
        }

        if (anchor != null)
            context.put("anchor", anchor);
        if (lastChance)
            context.put("lastChance", Boolean.TRUE);
        new CKSetup().setCKCollectionAttrib(getDocsPath(), toolManager.getCurrentPlacement().getContext());

        // render
        uiService.render(ui, context);
    }

    /**
     * Final initialization, once all dependencies are set.
     */
    public void init() {
        super.init();
        M_log.info("init()");
    }

    /**
     * {@inheritDoc}
     */
    public void post(HttpServletRequest req, HttpServletResponse res, Context context, String[] params)
            throws IOException {
        // we need two parameters (sid/question selector) and optional anchor
        // question selector may end in "!" for "last chance" - linear repeat question when it was not answered
        if (params.length < 5) {
            throw new IllegalArgumentException();
        }

        String submissionId = params[2];
        String questionSelector = params[3];
        boolean lastChance = false;
        if (questionSelector.endsWith("!")) {
            questionSelector = questionSelector.substring(0, questionSelector.length() - 1);
            lastChance = true;
        }

        String returnDestination = null;
        if (params.length > 5) {
            returnDestination = "/" + StringUtil.unsplit(params, 5, params.length - 5, "/");
        }
        // if not specified, go to the main list view
        else {
            returnDestination = "/list";
        }

        // if (!context.getPostExpected())
        // {
        // // redirect to error
        // res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.unexpected)));
        // return;
        // }

        // collect the questions (actually their answers) to put on the page
        List<Answer> answers = new ArrayList<Answer>();

        // get the submission
        Submission submission = submissionService.getSubmission(submissionId);
        if (submission == null) {
            // redirect to error
            res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.invalid)));
            return;
        }

        // setup receiving context
        Errors err = questionSetup(submission, questionSelector, context, answers, false);
        if (Errors.invalid == err)
            err = Errors.invalidpost;
        if (err != null) {
            // redirect to error
            res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + err)));
            return;
        }

        // read form
        String destination = uiService.decode(req, context);

        String redirectUrl = null;
        if (destination.startsWith("REDIRECT:")) {
            String[] parts = StringUtil.splitFirst(destination, ":");
            redirectUrl = "/!PORTAL!/" + parts[1];
            destination = "LIST";
        }

        // check for file upload error
        boolean uploadError = ((req.getAttribute("upload.status") != null)
                && (!req.getAttribute("upload.status").equals("ok")));

        // if we are going to submitted, we must complete the submission (unless there was an upload error)
        Boolean complete = Boolean.valueOf((!uploadError) && destination.startsWith("/submitted"));

        // unless we are going to list, instructions, or this very same question, or we have a file upload error, mark the
        // answers as complete
        Boolean answersComplete = Boolean.valueOf(!(uploadError || destination.startsWith("LIST")
                || destination.startsWith("STAY") || destination.startsWith("/instructions")
                || destination.equals(context.getPreviousDestination())));

        // and if we are working in a random access test, answers are always complete
        if (submission.getAssessment().getRandomAccess())
            answersComplete = Boolean.TRUE;

        // post-process the answers
        for (Answer answer : answers) {
            answer.getTypeSpecificAnswer().consolidate(destination);
        }

        // linear order check - unless this was a last chance display
        boolean repeat = false;
        if (!lastChance) {
            if (!submission.getAssessment().getRandomAccess().booleanValue()) {
                // linear order is always presented by-question
                if (answers.size() == 1) {
                    Answer answer = answers.get(0);
                    boolean answered = answer.getIsAnswered().booleanValue();
                    if (!answered) {
                        repeat = true;

                        // don't mark the answer as complete, so we can re-enter
                        answersComplete = false;
                    }
                }
            }
        }

        // where are we going?
        destination = questionChooseDestination(context, destination, questionSelector, submissionId, params,
                repeat, returnDestination);

        Boolean auto = Boolean.FALSE;

        // submit all answers
        try {
            // if we are doing AUTO, and the submission is really timed out, set complete
            if (destination.equals("AUTO")) {
                // check if this submission is really done (time over, past deadline)
                if (submission.getIsOver(new Date(), 0)) {
                    auto = Boolean.TRUE;
                    complete = Boolean.TRUE;
                    destination = "/submitted/" + submissionId + returnDestination;
                } else {
                    // if for some reason we are here (AUTO) but the submission is not yet really over, just return to list
                    destination = "/list";
                    complete = Boolean.FALSE;
                }
            }

            submissionService.submitAnswers(answers, answersComplete, complete, auto);

            // if there was an upload error, send to the upload error
            if ((req.getAttribute("upload.status") != null) && (!req.getAttribute("upload.status").equals("ok"))) {
                res.sendRedirect(res.encodeRedirectURL(
                        Web.returnUrl(req, "/error/" + Errors.upload + "/" + req.getAttribute("upload.limit"))));
                return;
            }

            if (destination.equals("SUBMIT")) {
                // get the submission again, to make sure that the answers we just posted are reflected
                submission = submissionService.getSubmission(submissionId);

                // if linear, or the submission is all answered, or this is a single question assessment, we can complete the submission and go to submitted
                if ((!submission.getAssessment().getRandomAccess()) || (submission.getIsAnswered())
                        || (submission.getAssessment().getIsSingleQuestion())) {
                    submissionService.completeSubmission(submission, Boolean.FALSE);

                    destination = "/submitted/" + submissionId + returnDestination;
                }

                // if not linear, and there are unanswered parts, send to final review
                else {
                    destination = "/final_review/" + submissionId + returnDestination;
                }
            }

            if (redirectUrl != null) {
                // redirect to the full URL
                res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, redirectUrl)));
                return;
            }

            // redirect to the next destination
            res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, destination)));
            return;
        } catch (AssessmentClosedException e) {
        } catch (SubmissionCompletedException e) {
        } catch (AssessmentPermissionException e) {
        }

        // redirect to error
        res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, "/error/" + Errors.unauthorized)));
    }

    /**
     * Set the assessment service.
     * 
     * @param service
     *        The assessment service.
     */
    public void setAssessmentService(AssessmentService service) {
        this.assessmentService = service;
    }

    /**
     * Set the submission service.
     * 
     * @param service
     *        The submission service.
     */
    public void setSubmissionService(SubmissionService service) {
        this.submissionService = service;
    }

    /**
     * Set the tool manager.
     * 
     * @param manager
     *        The tool manager.
     */
    public void setToolManager(ToolManager manager) {
        toolManager = manager;
    }

    /**
     * for PREV and NEXT, choose the destination.
     * 
     * @param destination
     *        The destination encoded in the request.
     * @param questionSelector
     *        Which question(s) to put on the page: q followed by a questionId picks one, s followed by a sectionId picks a sections
     * @param submisssionId
     *        The selected submission id.
     * @param params
     *        The current destination parameters.
     * @param repeat
     *        If true, and we would be sending the user to another question, repeat the current question.
     * @param returnDestination
     *        The final return destination path.
     */
    protected String questionChooseDestination(Context context, String destination, String questionSelector,
            String submissionId, String[] params, boolean repeat, String returnDestination) {
        // get the submission
        Submission submission = submissionService.getSubmission(submissionId);
        if (submission == null) {
            return "/error/" + Errors.invalid;
        }

        // for LIST do the return destination
        if (destination.startsWith("LIST")) {
            return returnDestination;
        }

        // if we are staying here
        if (destination.startsWith("STAY_")) {
            String[] parts = StringUtil.splitFirst(destination, ":");
            if (parts.length == 2) {
                String[] anchor = StringUtil.splitFirst(parts[1], ":");
                if (anchor.length > 0) {
                    // replace the original anchor
                    params[4] = anchor[0];
                }
            } else {
                params[4] = "-";
            }

            String curDestination = "/" + StringUtil.unsplit(params, 1, params.length - 1, "/");
            return curDestination;
        }

        // for requests for a single question
        if (questionSelector.startsWith("q")) {
            // make sure by-question is valid for this assessment
            if (submission.getAssessment().getQuestionGrouping() != QuestionGrouping.question) {
                return "/error/" + Errors.invalid;
            }

            String questionId = questionSelector.substring(1);
            Question question = submission.getAssessment().getParts().getQuestion(questionId);
            if (question == null) {
                return "/error/" + Errors.invalid;
            }

            if ("NEXT".equals(destination)) {
                // only for NEXT, if repeat, do the question again with a "!" to indicate a second chance
                if (repeat) {
                    return "/question/" + submissionId + "/q" + question.getId() + "!" + "/-" + returnDestination;
                }

                // if the question is not the last of the part, go to the next question
                if (!question.getPartOrdering().getIsLast()) {
                    return "/question/" + submissionId + "/q" + question.getAssessmentOrdering().getNext().getId()
                            + "/-" + returnDestination;
                }

                // if there's a next part
                if (!question.getPart().getOrdering().getIsLast()) {
                    // if showing part presentation
                    Part next = question.getPart().getOrdering().getNext();
                    if (submission.getAssessment().getParts().getShowPresentation()) {
                        // choose the part instructions
                        return "/part_instructions/" + submissionId + "/" + next.getId() + returnDestination;
                    }

                    // otherwise choose the first question of the next part
                    return "/question/" + submissionId + "/q" + next.getFirstQuestion().getId() + "/-"
                            + returnDestination;
                }

                // no next part, this is an error
                return "/error/" + Errors.invalid;
            }

            else if ("PREV".equals(destination)) {
                // if the question is not the first of the part, go to the prev question
                if (!question.getPartOrdering().getIsFirst()) {
                    return "/question/" + submissionId + "/q"
                            + question.getAssessmentOrdering().getPrevious().getId() + "/-" + returnDestination;
                }

                // prev into this part's instructions... if showing part presentation
                Part part = question.getPart();
                if (submission.getAssessment().getParts().getShowPresentation()) {
                    // choose the part instructions
                    return "/part_instructions/" + submissionId + "/" + part.getId() + returnDestination;
                }

                // otherwise choose the last question of the prev part, if we have one
                Part prev = part.getOrdering().getPrevious();
                if (prev != null) {
                    return "/question/" + submissionId + "/q" + prev.getLastQuestion().getId() + "/-"
                            + returnDestination;
                }

                // no prev part, this is an error
                return "/error/" + Errors.invalid;
            }
        }

        // for part-per-page
        else if (questionSelector.startsWith("p")) {
            // make sure by-part is valid for this assessment
            if (submission.getAssessment().getQuestionGrouping() != QuestionGrouping.part) {
                return "/error /" + Errors.invalid;
            }

            String sectionId = questionSelector.substring(1);
            Part part = submission.getAssessment().getParts().getPart(sectionId);
            if (part == null) {
                return "/error/" + Errors.invalid;
            }

            if ("NEXT".equals(destination)) {
                // if there's a next part, go there
                if (!part.getOrdering().getIsLast()) {
                    Part next = part.getOrdering().getNext();
                    return "/question/" + submissionId + "/p" + next.getId() + "/-" + returnDestination;
                }

                // no next part, this is an error
                return "/error/" + Errors.invalid;
            }

            else if ("PREV".equals(destination)) {
                // if there's a prev part, choose to enter that
                if (!part.getOrdering().getIsFirst()) {
                    Part prev = part.getOrdering().getPrevious();
                    return "/question/" + submissionId + "/p" + prev.getId() + "/-" + returnDestination;
                }

                // no prev part, this is an error
                return "/error/" + Errors.invalid;
            }
        }

        return destination;
    }

    /**
     * Setup the context for question get and post
     * 
     * @param submisssion
     *        The selected submission.
     * @param questionSelector
     *        Which question(s) to put on the page: q followed by a questionId picks one, s followed by a sectionId picks a sections worth, and a picks them all.
     * @param context
     *        UiContext.
     * @param answers
     *        A list to fill in with the answers for this page.
     * @param out
     *        Output writer.
     * @return null if all went well, else an Errors to indicate what went wrong.
     */
    protected Errors questionSetup(Submission submission, String questionSelector, Context context,
            List<Answer> answers, boolean linearCheck) {
        // not in review mode
        context.put("review", Boolean.FALSE);
        context.put("viewWork", Boolean.FALSE);

        // put in the selector
        context.put("questionSelector", questionSelector);

        if (!submissionService.allowCompleteSubmission(submission)) {
            return Errors.unauthorized;
        }

        context.put("submission", submission);

        // for requests for a single question
        if (questionSelector.startsWith("q")) {
            // TODO: assure the test is by-question

            String questionId = questionSelector.substring(1);
            Question question = submission.getAssessment().getParts().getQuestion(questionId);
            if (question == null) {
                return Errors.invalid;
            }

            // if we need to do our linear assessment check, and this is a linear assessment,
            // we will reject if the question has been marked as 'complete'
            if (linearCheck && !question.getPart().getAssessment().getRandomAccess()
                    && submission.getIsCompleteQuestion(question)) {
                return Errors.linear;
            }

            // find the answer (or have one created) for this submission / question
            Answer answer = submission.getAnswer(question);
            if (answer != null) {
                answers.add(answer);
            }

            // tell the UI that we are doing single question
            context.put("question", question);
        }

        // for requests for a part
        else if (questionSelector.startsWith("p")) {
            // TODO: assure the test is by-part

            String sectionId = questionSelector.substring(1);
            Part part = submission.getAssessment().getParts().getPart(sectionId);
            if (part == null) {
                return Errors.invalid;
            }

            // get all the answers for this part
            for (Question question : part.getQuestions()) {
                Answer answer = submission.getAnswer(question);
                if (answer != null) {
                    answers.add(answer);
                }
            }

            // tell the UI that we are doing single part
            context.put("part", part);
        }

        // for requests for the entire assessment
        else if (questionSelector.startsWith("a")) {
            // TODO: assure the test is by-test

            answers.addAll(submission.getAnswers());
        }

        context.put("answers", answers);
        return null;
    }

    /**
     * Redirect to the appropriate question screen for this submission
     * 
     * @param req
     *        Servlet request.
     * @param res
     *        Servlet response.
     * @param submission
     *        The submission.
     * @param toc
     *        if true, send to TOC if possible (not possible for linear).
     * @param instructions
     *        if true, send to part instructions for first question.
     * @param returnDestination
     *        The final return destination path.
     */
    protected void redirectToQuestion(HttpServletRequest req, HttpServletResponse res, Submission submission,
            boolean toc, boolean instructions, String returnDestination) throws IOException {
        String destination = null;
        Assessment assessment = submission.getAssessment();

        // if we are random access, and not a single question, and allowed, send to TOC
        if (toc && assessment.getRandomAccess() && !assessment.getIsSingleQuestion()) {
            destination = "/toc/" + submission.getId() + returnDestination;
        }

        else {
            // find the first incomplete question
            Question question = submission.getFirstIncompleteQuestion();

            // if not found, and we have only one, go there
            if ((question == null) && (assessment.getIsSingleQuestion())) {
                question = submission.getFirstQuestion();
            }

            // if we don't have one, we will go to the toc (or final_review for linear)
            if (question == null) {
                if (!assessment.getRandomAccess()) {
                    destination = "/final_review/" + submission.getId() + returnDestination;
                } else {
                    destination = "/toc/" + submission.getId() + returnDestination;
                }
            }

            else {
                // send to the part instructions if it's a first question and by-question
                if (instructions && (question.getPartOrdering().getIsFirst())
                        && (assessment.getParts().getShowPresentation())
                        && (assessment.getQuestionGrouping() == QuestionGrouping.question)) {
                    // to instructions
                    destination = "/part_instructions/" + submission.getId() + "/" + question.getPart().getId()
                            + returnDestination;
                }

                // or to the question
                else {
                    if (assessment.getQuestionGrouping() == QuestionGrouping.question) {
                        destination = "/question/" + submission.getId() + "/q" + question.getId() + "/-"
                                + returnDestination;
                    } else if (assessment.getQuestionGrouping() == QuestionGrouping.part) {
                        destination = "/question/" + submission.getId() + "/p" + question.getPart().getId();

                        // include the question target if not the first question in the part
                        if (!question.getPartOrdering().getIsFirst()) {
                            destination = destination + "/" + question.getId();
                        } else {
                            destination = destination + "/-";
                        }

                        destination = destination + returnDestination;
                    } else {
                        destination = "/question/" + submission.getId() + "/a";

                        // include the question target if not the first question in the assessment
                        if (!question.getAssessmentOrdering().getIsFirst()) {
                            destination = destination + "/" + question.getId();
                        } else {
                            destination = destination + "/-";
                        }

                        destination = destination + returnDestination;
                    }
                }
            }
        }

        res.sendRedirect(res.encodeRedirectURL(Web.returnUrl(req, destination)));
        return;
    }
}