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

Java tutorial

Introduction

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

Source

/**********************************************************************************
 * $URL$
 * $Id$
 ***********************************************************************************
 *
 * Copyright (c) 2007 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.muse.mneme.tool;

import java.io.IOException;
import java.util.ArrayList;
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.muse.ambrosia.api.Context;
import org.muse.ambrosia.util.ControllerImpl;
import org.muse.mneme.api.Answer;
import org.muse.mneme.api.Assessment;
import org.muse.mneme.api.AssessmentClosedException;
import org.muse.mneme.api.AssessmentCompletedException;
import org.muse.mneme.api.AssessmentPermissionException;
import org.muse.mneme.api.AssessmentService;
import org.muse.mneme.api.Part;
import org.muse.mneme.api.Question;
import org.muse.mneme.api.QuestionGrouping;
import org.muse.mneme.api.Submission;
import org.muse.mneme.api.SubmissionCompletedException;
import org.muse.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 {
        // we need two parameters (sid/question selector) and optional anchor
        if ((params.length != 4) && (params.length != 5)) {
            throw new IllegalArgumentException();
        }

        String submissionId = params[2];
        String questionSelector = params[3];
        String anchor = (params.length == 5) ? params[4] : null;

        // adjust the current destination to remove the anchor
        String curDestination = context.getDestination();
        if (anchor != null) {
            int pos = curDestination.lastIndexOf("/");
            curDestination = curDestination.substring(0, pos);
        }
        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;
        }

        // handle our 'z' selector - redirect to the appropriate question to re-enter this submission
        if ("z".equals(questionSelector)) {
            try {
                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);
            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);

        // 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
        if ((params.length != 4) && (params.length != 5)) {
            throw new IllegalArgumentException();
        }

        String submissionId = params[2];
        String questionSelector = params[3];
        String anchor = (params.length == 5) ? params[4] : null;

        // adjust the current destination to remove the anchor
        String curDestination = context.getDestination();
        if (anchor != null) {
            int pos = curDestination.lastIndexOf("/");
            curDestination = curDestination.substring(0, pos);
        }

        // 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);

        // 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);
        }

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

        // submit all answers
        try {
            submissionService.submitAnswers(answers, answersComplete, complete);

            // 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, we can complete the submission and go to submitted
                if ((!submission.getAssessment().getRandomAccess()) || (submission.getIsAnswered())) {
                    submissionService.completeSubmission(submission);

                    destination = "/submitted/" + submissionId;
                }

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

            // 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 curDestination
     *        The current destination, adjusted to remove anchor.
     */
    protected String questionChooseDestination(Context context, String destination, String questionSelector,
            String submissionId, String curDestination) {
        // get the submission
        Submission submission = submissionService.getSubmission(submissionId);
        if (submission == null) {
            return "/error/" + Errors.invalid;
        }

        // if we are staying heres
        if (destination.startsWith("STAY_")) {
            String rv = curDestination;

            String[] parts = StringUtil.splitFirst(destination, ":");
            if (parts.length == 2) {
                String[] anchor = StringUtil.splitFirst(parts[1], ":");
                if (anchor.length > 0) {
                    rv += "/" + anchor[0];
                }
            }
            return rv;
        }

        // 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)) {
                // if the question is not the last of the part, go to the next quesiton
                if (!question.getPartOrdering().getIsLast()) {
                    return "/question/" + submissionId + "/q" + question.getAssessmentOrdering().getNext().getId();
                }

                // 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();
                    }

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

                // 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 quesiton
                if (!question.getPartOrdering().getIsFirst()) {
                    return "/question/" + submissionId + "/q"
                            + question.getAssessmentOrdering().getPrevious().getId();
                }

                // 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();
                }

                // 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();
                }

                // 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();
                }

                // 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();
                }

                // 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);

        // 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.getAnswersOrdered());
        }

        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.
     */
    protected void redirectToQuestion(HttpServletRequest req, HttpServletResponse res, Submission submission,
            boolean toc, boolean instructions) throws IOException {
        String destination = null;
        Assessment assessment = submission.getAssessment();

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

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

            // 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();
                } else {
                    destination = "/toc/" + submission.getId();
                }
            }

            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();
                }

                // or to the question
                else {
                    if (assessment.getQuestionGrouping() == QuestionGrouping.question) {
                        destination = "/question/" + submission.getId() + "/q" + question.getId();
                    } 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 = "/question/" + submission.getId() + "/a";

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

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