]> git.mjollnir.org Git - moodle.git/commitdiff
MDL-15452 - Put the OU quiz navigation improvements into the Moodle codebase - quite...
authortjhunt <tjhunt>
Tue, 8 Jul 2008 16:33:47 +0000 (16:33 +0000)
committertjhunt <tjhunt>
Tue, 8 Jul 2008 16:33:47 +0000 (16:33 +0000)
MDL-15537 - create oo attemptlib.php to hold shared code between attempt, summary and review.php
MDL-15541 - Refactor starting a new attempt into a new file startattempt.php
MDL-15538 - Rework attempt.php to use attemptlib.php

lang/en_utf8/quiz.php
lib/questionlib.php
mod/quiz/accessrules.php
mod/quiz/attempt.php
mod/quiz/attemptlib.php
mod/quiz/locallib.php
mod/quiz/review.php
mod/quiz/startattempt.php [new file with mode: 0644]
mod/quiz/summary.php
mod/quiz/view.php

index 2953dfc30261a7b1b1211dad6a98e431cc67c311..3d681b5cbab79c730d6ff9e51b6b040b57190c4a 100644 (file)
@@ -398,6 +398,7 @@ $string['numberabbr'] = '#';
 $string['numerical'] = 'Numerical';
 $string['onlyteachersexport'] = 'Only teachers can export questions';
 $string['onlyteachersimport'] = 'Only teachers with editing rights can import questions';
+$string['open'] = 'Started';
 $string['openclosedatesupdated'] = 'Quiz open and close dates updated';
 $string['optional'] = 'optional';
 $string['outof'] = '$a->grade out of a maximum of $a->maxgrade';
index d0c1028bce428e98d2cef31e3e0be7307e5cf071..ebdf50bab24c5c35cfe4cb9c75f58cfac0b7e49c 100644 (file)
@@ -756,32 +756,61 @@ function questionbank_navigation_tabs(&$row, $contexts, $querystring) {
 }
 
 /**
- * Load a set of questions, given a list of ids. The $join and $extrafields arguments can be used
- * together to pull in extra data. See, for example, the usage in mod/quiz/attempt.php, and
- * read the code below to see how the SQL is assembled.
+ * Given a list of ids, load the basic information about a set of questions from the questions table.
+ * The $join and $extrafields arguments can be used together to pull in extra data.
+ * See, for example, the usage in mod/quiz/attemptlib.php, and
+ * read the code below to see how the SQL is assembled. Throws exceptions on error.
  *
- * @param string $questionlist list of comma-separated question ids.
- * @param string $extrafields
- * @param string $join
+ * @param array $questionids array of question ids.
+ * @param string $extrafields extra SQL code to be added to the query.
+ * @param string $join extra SQL code to be added to the query.
+ * @param array $extraparams values for any placeholders in $join.
+ * You are strongly recommended to use named placeholder.
  *
- * @return mixed array of question objects on success, a string error message on failure.
+ * @return array partially complete question objects. You need to call get_question_options
+ * on them before they can be properly used.
  */
-function question_load_questions($questionlist, $extrafields = '', $join = '') {
+function question_preload_questions($questionids, $extrafields = '', $join = '', $extraparams = array()) {
     global $CFG, $DB;
     if ($join) {
-        $join = ' JOIN '.$join.'';
+        $join = ' JOIN '.$join;
     }
     if ($extrafields) {
         $extrafields = ', ' . $extrafields;
     }
+    list($questionidcondition, $params) = $DB->get_in_or_equal(
+            $questionids, SQL_PARAMS_NAMED, 'qid0000');
     $sql = 'SELECT q.*' . $extrafields . ' FROM {question} q' . $join .
-            ' WHERE q.id IN (' . $questionlist . ')';
+            ' WHERE q.id ' . $questionidcondition;
 
     // Load the questions
-    if (!$questions = $DB->get_records_sql($sql)) {
+    if (!$questions = $DB->get_records_sql($sql, $extraparams + $params)) {
         return 'Could not load questions.';
     }
 
+    foreach ($questions as $question) {
+        $question->_partiallyloaded = true;
+    }
+
+    return $questions;
+}
+
+/**
+ * Load a set of questions, given a list of ids. The $join and $extrafields arguments can be used
+ * together to pull in extra data. See, for example, the usage in mod/quiz/attempt.php, and
+ * read the code below to see how the SQL is assembled. Throws exceptions on error.
+ *
+ * @param array $questionids array of question ids.
+ * @param string $extrafields extra SQL code to be added to the query.
+ * @param string $join extra SQL code to be added to the query.
+ * @param array $extraparams values for any placeholders in $join.
+ * You are strongly recommended to use named placeholder.
+ *
+ * @return array question objects.
+ */
+function question_load_questions($questionids, $extrafields = '', $join = '') {
+    $questions = question_preload_questions($questionids, $extrafields, $join);
+
     // Load the question type specific information
     if (!get_question_options($questions)) {
         return 'Could not load the question options';
@@ -803,7 +832,12 @@ function _tidy_question(&$question) {
         $question->questiontext = '<p>' . get_string('warningmissingtype', 'quiz') . '</p>' . $question->questiontext;
     }
     $question->name_prefix = question_make_name_prefix($question->id);
-    return $QTYPES[$question->qtype]->get_question_options($question);
+    if ($success = $QTYPES[$question->qtype]->get_question_options($question)) {
+        if (isset($question->_partiallyloaded)) {
+            unset($question->_partiallyloaded);
+        }
+    }
+    return $success;
 }
 
 /**
index 91e0896aadecd4edca02a7c16deeed8ad62ec229..fc9dbac6c1411d77af4af0b1b8d4a4234e1a5a17 100644 (file)
@@ -4,7 +4,7 @@
  * quiz, with convinient methods for seeing whether access is allowed.
  */
 class quiz_access_manager {
-    private $_quiz;
+    private $_quizobj;
     private $_timenow;
     private $_passwordrule = null;
     private $_securewindowrule = null;
@@ -17,32 +17,33 @@ class quiz_access_manager {
      * @param boolean $canpreview whether the current user has the
      * @param boolean $ignoretimelimits
      */
-    public function __construct($quiz, $timenow, $canignoretimelimits) {
-        $this->_quiz = $quiz;
+    public function __construct($quizobj, $timenow, $canignoretimelimits) {
+        $this->_quizobj = $quizobj;
         $this->_timenow = $timenow;
         $this->create_standard_rules($canignoretimelimits);
     }
 
     private function create_standard_rules($canignoretimelimits) {
-        if ($this->_quiz->attempts > 0) {
-            $this->_rules[] = new num_attempts_access_rule($this->_quiz, $this->_timenow);
+        $quiz = $this->_quizobj->get_quiz();
+        if ($quiz->attempts > 0) {
+            $this->_rules[] = new num_attempts_access_rule($this->_quizobj, $this->_timenow);
         }
-        $this->_rules[] = new open_close_date_access_rule($this->_quiz, $this->_timenow);
-        if ($this->_quiz->timelimit && !$canignoretimelimits) {
-            $this->_rules[] = new time_limit_access_rule($this->_quiz, $this->_timenow);
+        $this->_rules[] = new open_close_date_access_rule($this->_quizobj, $this->_timenow);
+        if ($quiz->timelimit && !$canignoretimelimits) {
+            $this->_rules[] = new time_limit_access_rule($this->_quizobj, $this->_timenow);
         }
-        if ($this->_quiz->delay1 || $this->_quiz->delay2) {
-            $this->_rules[] = new inter_attempt_delay_access_rule($this->_quiz, $this->_timenow);
+        if ($quiz->delay1 || $quiz->delay2) {
+            $this->_rules[] = new inter_attempt_delay_access_rule($this->_quizobj, $this->_timenow);
         }
-        if ($this->_quiz->subnet) {
-            $this->_rules[] = new ipaddress_access_rule($this->_quiz, $this->_timenow);
+        if ($quiz->subnet) {
+            $this->_rules[] = new ipaddress_access_rule($this->_quizobj, $this->_timenow);
         }
-        if ($this->_quiz->password) {
-            $this->_passwordrule = new password_access_rule($this->_quiz, $this->_timenow);
+        if ($quiz->password) {
+            $this->_passwordrule = new password_access_rule($this->_quizobj, $this->_timenow);
             $this->_rules[] = $this->_passwordrule;
         }
-        if ($this->_quiz->popup) {
-            $this->_securewindowrule = new securewindow_access_rule($this->_quiz, $this->_timenow);
+        if ($quiz->popup) {
+            $this->_securewindowrule = new securewindow_access_rule($this->_quizobj, $this->_timenow);
             $this->_rules[] = $this->_securewindowrule;
         }
     }
@@ -211,8 +212,9 @@ class quiz_access_manager {
         if ($this->securewindow_required($canpreview)) {
             $this->_securewindowrule->print_start_attempt_button($buttontext, $strconfirmstartattempt);
         } else {
-            print_single_button("attempt.php", array('q' => $this->_quiz->id), $buttontext,
-                    'get', '', false, '', false, $strconfirmstartattempt);
+            print_single_button($this->_quizobj->start_attempt_url(),
+                    array('cmid' => $this->_quizobj->get_cmid(), 'sesskey' => sesskey()),
+                    $buttontext, 'post', '', false, '', false, $strconfirmstartattempt);
         }
         echo "</div>\n";
 
@@ -238,7 +240,7 @@ class quiz_access_manager {
      */
     public function back_to_view_page($canpreview, $message = '') {
         global $CFG;
-        $url = $CFG->wwwroot . '/mod/quiz/view.php?q=' . $this->_quiz->id;
+        $url = $this->_quizobj->view_url();
         if (securewindow_required($canpreview)) {
             print_header();
             print_box_start();
@@ -268,7 +270,7 @@ class quiz_access_manager {
      */
     public function print_finish_review_link($canpreview) {
         global $CFG;
-        $url = $CFG->wwwroot . '/mod/quiz/view.php?q=' . $this->_quiz->id;
+        $url = $this->_quizobj->view_url();
         echo '<div class="finishreview">';
         if ($this->securewindow_required($canpreview)) {
             $url = addslashes_js(htmlspecialchars($url));
@@ -313,12 +315,13 @@ class quiz_access_manager {
      * in a javascript alert on the start attempt button.
      */
     public function confirm_start_attempt_message() {
-        if ($this->_quiz->timelimit && $this->_quiz->attempts) {
-            return get_string('confirmstartattempttimelimit','quiz', $this->_quiz->attempts);
-        } else if ($this->_quiz->timelimit) {
+        $quiz = $this->_quizobj->get_quiz();
+        if ($quiz->timelimit && $quiz->attempts) {
+            return get_string('confirmstartattempttimelimit','quiz', $quiz->attempts);
+        } else if ($quiz->timelimit) {
             return get_string('confirmstarttimelimit','quiz');
-        } else if ($this->_quiz->attempts) {
-            return get_string('confirmstartattemptlimit','quiz', $this->_quiz->attempts);
+        } else if ($quiz->attempts) {
+            return get_string('confirmstartattemptlimit','quiz', $quiz->attempts);
         }
         return '';
     }
@@ -342,8 +345,7 @@ class quiz_access_manager {
         if ($this->securewindow_required($canpreview)) {
             return $this->_securewindowrule->make_review_link($linktext, $attempt->id);
         } else {
-            return '<a href="' . $CFG->wwwroot . '/mod/quiz/review.php?q=' . $this->_quiz->id .
-                    '&amp;attempt=' . $attempt->id . '">' . $linktext . '</a>';
+            return '<a href="' . $this->_quizobj->review_url($attempt->id) . '">' . $linktext . '</a>';
         }
     }
     /**
@@ -354,11 +356,12 @@ class quiz_access_manager {
      * @return string an appropraite message.
      */
     public function cannot_review_message($reviewoptions) {
+        $quiz = $this->_quizobj->get_quiz();
         if ($reviewoptions->quizstate == QUIZ_STATE_IMMEDIATELY) {
             return '';
-        } else if ($reviewoptions->quizstate == QUIZ_STATE_OPEN && $this->_quiz->timeclose &&
-                    ($this->_quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_RESPONSES)) {
-            return get_string('noreviewuntil', 'quiz', userdate($this->_quiz->timeclose));
+        } else if ($reviewoptions->quizstate == QUIZ_STATE_OPEN && $quiz->timeclose &&
+                    ($quiz->review & QUIZ_REVIEW_CLOSED & QUIZ_REVIEW_RESPONSES)) {
+            return get_string('noreviewuntil', 'quiz', userdate($quiz->timeclose));
         } else {
             return get_string('noreview', 'quiz');
         }
@@ -376,13 +379,15 @@ class quiz_access_manager {
  */
 abstract class quiz_access_rule_base {
     protected $_quiz;
+    protected $_quizobj;
     protected $_timenow;
     /**
      * Create an instance of this rule for a particular quiz.
      * @param object $quiz the quiz we will be controlling access to.
      */
-    public function __construct($quiz, $timenow) {
-        $this->_quiz = $quiz;
+    public function __construct($quizobj, $timenow) {
+        $this->_quizobj = $quizobj;
+        $this->_quiz = $quizobj->get_quiz();
         $this->_timenow = $timenow;
     }
     /**
@@ -620,7 +625,7 @@ class password_access_rule extends quiz_access_rule_base {
     /// Print the password entry form.
         $output .= '<p>' . get_string('requirepasswordmessage', 'quiz') . "</p>\n";
         $output .= '<form id="passwordform" method="post" action="' . $CFG->wwwroot .
-                '/mod/quiz/attempt.php?q=' . $this->_quiz->id .
+                '/mod/quiz/startattempt.php?q=' . $this->_quiz->id .
                 '" onclick="this.autocomplete=\'off\'">' . "\n";
         $output .= "<div>\n";
         $output .= '<label for="quizpassword">' . get_string('password') . "</label>\n";
@@ -674,7 +679,8 @@ class securewindow_access_rule extends quiz_access_rule_base {
     public function print_start_attempt_button($buttontext, $strconfirmstartattempt) {
         global $CFG, $SESSION;
 
-        $attempturl = $CFG->wwwroot . '/mod/quiz/attempt.php?q=' . $this->_quiz->id;
+        $attempturl = $this->_quizobj->start_attempt_url() . '?cmid=' . $this->_quizobj->get_cmid() .
+                '&sesskey=' . sesskey();
         $window = 'quizpopup';
 
         if (!empty($CFG->usesid) && !isset($_COOKIE[session_name()])) {
@@ -696,9 +702,8 @@ class securewindow_access_rule extends quiz_access_rule_base {
      * @return string HTML for the link.
      */
     public function make_review_link($linktext, $attemptid) {
-        global $CFG;
-        return link_to_popup_window($CFG->wwwroot . '/mod/quiz/review.php?q=' . $this->_quiz->id .
-                 '&amp;attempt=' . $attemptid, 'quizpopup', $linktext, '', '', '', $this->windowoptions, true);
+        return link_to_popup_window($this->_quizobj->review_url($attemptid),
+                'quizpopup', $linktext, '', '', '', $this->windowoptions, true);
     }
 
     /**
index b99f4c1897a78d1d421404463d19082d3b0c4ab5..75e00ce25ffc2404289e5357bdfdd276f5c9d459 100644 (file)
@@ -10,7 +10,7 @@
  * @package quiz
  */
 
-    require_once('../../config.php');
+    require_once(dirname(__FILE__) . '/../../config.php');
     require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 
 /// remember the current time as the time any responses were submitted
     $timenow = time();
 
 /// Get submitted parameters.
-    $id = optional_param('id', 0, PARAM_INT);               // Course Module ID
-    $q = optional_param('q', 0, PARAM_INT);                 // or quiz ID
+    $attemptid = required_param('attempt', PARAM_INT);
     $page = optional_param('page', 0, PARAM_INT);
-    $questionids = optional_param('questionids', '');
+    $submittedquestionids = optional_param('questionids', '', PARAM_SEQUENCE);
     $finishattempt = optional_param('finishattempt', 0, PARAM_BOOL);
     $timeup = optional_param('timeup', 0, PARAM_BOOL); // True if form was submitted by timer.
-    $forcenew = optional_param('forcenew', false, PARAM_BOOL); // Teacher has requested new preview
 
-    if ($id) {
-        if (! $cm = get_coursemodule_from_id('quiz', $id)) {
-            print_error('invalidcoursemodule');
-        }
-        if (! $course = $DB->get_record('course', array('id' => $cm->course))) {
-            print_error("coursemisconf");
-        }
-        if (! $quiz = $DB->get_record('quiz', array('id' => $cm->instance))) {
-            print_error('invalidcoursemodule');
-        }
-    } else {
-        if (! $quiz = $DB->get_record('quiz', array('id' => $q))) {
-            print_error('invalidcoursemodule');
-        }
-        if (! $course = $DB->get_record('course', array('id' => $quiz->course))) {
-            print_error('invalidcourseid');
-        }
-        if (! $cm = get_coursemodule_from_instance("quiz", $quiz->id, $course->id)) {
-            print_error('invalidcoursemodule');
-        }
-    }
+    $attemptobj = new quiz_attempt($attemptid);
 
 /// We treat automatically closed attempts just like normally closed attempts
     if ($timeup) {
     }
 
 /// Check login and get contexts.
-    require_login($course->id, false, $cm);
-    $coursecontext = get_context_instance(CONTEXT_COURSE, $cm->course);
-    $context = get_context_instance(CONTEXT_MODULE, $cm->id);
-    $canpreview = has_capability('mod/quiz:preview', $context);
-
-/// Create an object to manage all the other (non-roles) access rules.
-    $accessmanager = new quiz_access_manager($quiz, $timenow,
-            has_capability('mod/quiz:ignoretimelimits', $context, NULL, false));
-    if ($canpreview && $forcenew) {
-        $accessmanager->clear_password_access();
-    }
-
-/// if no questions have been set up yet redirect to edit.php
-    if (!$quiz->questions && has_capability('mod/quiz:manage', $context)) {
-        redirect($CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $cm->id);
-    }
+    require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
 
 /// Check capabilites.
-    if (!$canpreview) {
+    if (!$attemptobj->is_preview_user()) {
         require_capability('mod/quiz:attempt', $context);
     }
-/// We intentionally do not check otehr access rules until after we have processed
-/// any submitted responses (which would be sesskey protected). This is so that when
-/// someone submits close to the exact moment when the quiz closes, there responses are not lost.
-
-/// Load attempt or create a new attempt if there is no unfinished one
-
-/// Check to see if a new preview was requested.
-    if ($canpreview && $forcenew) {
-    /// Teacher wants a new preview, so we set a finish time on the
-    /// current attempt (if any). It will then automatically be deleted below
-        $DB->set_field('quiz_attempts', 'timefinish', $timenow, array('quiz' => $quiz->id, 'userid' => $USER->id));
-    }
-
-/// Look for an existing attempt.
-    $newattempt = false;
-    $lastattempt = quiz_get_latest_attempt_by_user($quiz->id, $USER->id);
-
-    if ($lastattempt && !$lastattempt->timefinish) {
-    /// Continuation of an attempt.
-        $attempt = $lastattempt;
-        $lastattemptid = false;
-
-    /// Log it, but only if some time has elapsed.
-        if (($timenow - $attempt->timemodified) > QUIZ_CONTINUE_ATTEMPT_LOG_INTERVAL) {
-        /// This action used to be 'continue attempt' but the database field has only 15 characters.
-            add_to_log($course->id, 'quiz', 'continue attemp', "review.php?attempt=$attempt->id",
-                    "$quiz->id", $cm->id);
-        }
-
-    } else {
-    /// Start a new attempt.
-        $newattempt = true;
-
-    /// Get number for the next or unfinished attempt
-        if ($lastattempt && !$lastattempt->preview && !$canpreview) {
-            $attemptnumber = $lastattempt->attempt + 1;
-            $lastattemptid = $lastattempt->id;
-        } else {
-            $lastattempt = false;
-            $lastattemptid = false;
-            $attemptnumber = 1;
-        }
-
-    /// Check access.
-        $messages = $accessmanager->prevent_access() +
-                $accessmanager->prevent_new_attempt($attemptnumber - 1, $lastattempt);
-        if (!$canpreview && $messages) {
-            //TODO: need more detailed error info
-            print_error('attempterror', 'quiz', $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz->id);
-        }
-        $accessmanager->do_password_check($canpreview);
-
-    /// Delete any previous preview attempts belonging to this user.
-        if ($oldattempts = $DB->get_records_select('quiz_attempts', "quiz = ?
-                AND userid = ? AND preview = 1", array($quiz->id, $USER->id))) {
-            foreach ($oldattempts as $oldattempt) {
-                quiz_delete_attempt($oldattempt, $quiz);
-            }
-        }
-
-    /// Create the new attempt and initialize the question sessions
-        $attempt = quiz_create_attempt($quiz, $attemptnumber, $lastattempt, $timenow, $canpreview);
-
-    /// Save the attempt in the database.
-        if (!$attempt->id = $DB->insert_record('quiz_attempts', $attempt)) {
-            quiz_error($quiz, 'newattemptfail');
-        }
-
-    /// Log the new attempt.
-        if ($attempt->preview) {
-            add_to_log($course->id, 'quiz', 'preview', "attempt.php?id=$cm->id",
-                    "$quiz->id", $cm->id);
-        } else {
-            add_to_log($course->id, 'quiz', 'attempt', "review.php?attempt=$attempt->id",
-                    "$quiz->id", $cm->id);
-        }
-    }
 
-/// This shouldn't really happen, just for robustness
-    if (!$attempt->timestart) {
-        debugging('timestart was not set for this attempt. That should be impossible.', DEBUG_DEVELOPER);
-        $attempt->timestart = $timenow - 1;
+/// Log continuation of the attempt, but only if some time has passed.
+    if (($timenow - $attemptobj->get_attempt()->timemodified) > QUIZ_CONTINUE_ATTEMPT_LOG_INTERVAL) {
+    /// This action used to be 'continue attempt' but the database field has only 15 characters.
+        add_to_log($attemptobj->get_courseid(), 'quiz', 'continue attemp',
+                'review.php?attempt=' . $attemptobj->get_attemptid(),
+                $attemptobj->get_quizid(), $attemptobj->get_cmid());
     }
 
-/// Load all the questions and states needed by this script
+/// Work out which questions we need.
+    $attemptobj->preload_questions();
 
 /// Get the list of questions needed by this page.
-    $pagelist = quiz_questions_on_page($attempt->layout, $page);
-
-    if ($newattempt || $finishattempt) {
-        $questionlist = quiz_questions_in_quiz($attempt->layout);
+    if ($finishattempt) {
+        $questionids = $attemptobj->get_question_ids();
+    } else if ($page >= 0) {
+        $questionids = $attemptobj->get_question_ids($page);
     } else {
-        $questionlist = $pagelist;
+        $questionids = array();
     }
 
 /// Add all questions that are on the submitted form
-    if ($questionids) {
-        $questionlist .= ','.$questionids;
+    if ($submittedquestionids) {
+        $submittedquestionids = explode(',', $submittedquestionids);
+        $questionids = $questionids + $submittedquestionids;
+    } else {
+        $submittedquestionids = array();
     }
 
-    if (!$questionlist) {
+/// Check.
+    if (empty($questionids)) {
         quiz_error($quiz, 'noquestionsfound');
     }
 
-    $questions = question_load_questions($questionlist, 'qqi.grade AS maxgrade, qqi.id AS instance',
-            '{quiz_question_instances} qqi ON qqi.quiz = ' . $quiz->id . ' AND q.id = qqi.question');
-    if (is_string($questions)) {
-        quiz_error($quiz, 'loadingquestionsfailed', $questions);
-    }
+/// Load those questions and the associated states.
+    $attemptobj->load_questions($questionids);
+    $attemptobj->load_question_states($questionids);
 
-/// Restore the question sessions to their most recent states creating new sessions where required.
-    if (!$states = get_question_states($questions, $quiz, $attempt, $lastattemptid)) {
-        print_error('cannotrestore', 'quiz');
-    }
-
-/// If we are starting a new attempt, save all the newly created states.
-    if ($newattempt) {
-        foreach ($questions as $i => $question) {
-            save_question_session($questions[$i], $states[$i]);
-        }
-    }
 
 /// Process form data /////////////////////////////////////////////////
 
         unset($responses->forcenewattempt);
 
     /// Extract the responses. $actions will be an array indexed by the questions ids.
-        $actions = question_extract_responses($questions, $responses, $event);
+        $actions = question_extract_responses($attemptobj->get_questions(), $responses, $event);
 
     /// Process each question in turn
-        $questionidarray = explode(',', $questionids);
         $success = true;
-        foreach($questionidarray as $i) {
-            if (!isset($actions[$i])) {
-                $actions[$i]->responses = array('' => '');
-                $actions[$i]->event = QUESTION_EVENTOPEN;
+        foreach($submittedquestionids as $id) {
+            if (!isset($actions[$id])) {
+                $actions[$id]->responses = array('' => '');
+                $actions[$id]->event = QUESTION_EVENTOPEN;
             }
-            $actions[$i]->timestamp = $timenow;
-            if (question_process_responses($questions[$i], $states[$i], $actions[$i], $quiz, $attempt)) {
-                save_question_session($questions[$i], $states[$i]);
+            $actions[$id]->timestamp = $timenow;
+            if (question_process_responses($attemptobj->get_question($id),
+                    $attemptobj->get_question_state($id), $actions[$id],
+                    $attemptobj->get_quiz(), $attemptobj->get_attempt())) {
+                save_question_session($attemptobj->get_question($id),
+                        $attemptobj->get_question_state($id));
             } else {
                 $success = false;
             }
         }
 
         if (!$success) {
-            $pagebit = '';
-            if ($page) {
-                $pagebit = '&amp;page=' . $page;
-            }
-            print_error('errorprocessingresponses', 'question',
-                    $CFG->wwwroot . '/mod/quiz/attempt.php?q=' . $quiz->id . $pagebit);
+            print_error('errorprocessingresponses', 'question', $attemptobj->attempt_url(0, $page));
         }
 
+        $attempt = $attemptobj->get_attempt();
         $attempt->timemodified = $timenow;
         if (!$DB->update_record('quiz_attempts', $attempt)) {
             quiz_error($quiz, 'saveattemptfailed');
 /// Finish attempt if requested
     if ($finishattempt) {
 
-    /// Set the attempt to be finished
-        $attempt->timefinish = $timenow;
-
     /// Move each question to the closed state.
         $success = true;
-        foreach ($questions as $key => $question) {
+        foreach ($attemptobj->get_questions() as $id => $question) {
+            $action = new stdClass;
             $action->event = QUESTION_EVENTCLOSE;
-            $action->responses = $states[$key]->responses;
-            $action->timestamp = $states[$key]->timestamp;
-            if (question_process_responses($question, $closestates[$key], $action, $quiz, $attempt)) {
-                save_question_session($question, $closestates[$key]);
+            $action->responses = $attemptobj->get_question_state($id)->responses;
+            $action->timestamp = $attemptobj->get_question_state($id)->timestamp;
+            if (question_process_responses($attemptobj->get_question($id),
+                    $attemptobj->get_question_state($id), $action,
+                    $attemptobj->get_quiz(), $attemptobj->get_attempt())) {
+                save_question_session($attemptobj->get_question($id),
+                        $attemptobj->get_question_state($id));
             } else {
                 $success = false;
             }
         }
 
         if (!$success) {
-            $pagebit = '';
-            if ($page) {
-                $pagebit = '&amp;page=' . $page;
-            }
-            print_error('errorprocessingresponses', 'question',
-                    $CFG->wwwroot . '/mod/quiz/attempt.php?q=' . $quiz->id . $pagebit);
+            print_error('errorprocessingresponses', 'question', $attemptobj->attempt_url(0, $page));
         }
 
     /// Log the end of this attempt.
-        add_to_log($course->id, 'quiz', 'close attempt', "review.php?attempt=$attempt->id",
-                "$quiz->id", $cm->id);
+        add_to_log($attemptobj->get_courseid(), 'quiz', 'close attempt',
+                'review.php?attempt=' . $attemptobj->get_attemptid(),
+                $attemptobj->get_quizid(), $attemptobj->get_cmid());
 
     /// Update the quiz attempt record.
+        $attempt = $attemptobj->get_attempt();
+        $attempt->timemodified = $timenow;
+        $attempt->timefinish = $timenow;
         if (!$DB->update_record('quiz_attempts', $attempt)) {
             quiz_error($quiz, 'saveattemptfailed');
         }
             quiz_save_best_grade($quiz);
 
         /// Send any notification emails (if this is not a preview).
-            quiz_send_notification_emails($course, $quiz, $attempt, $context, $cm);
+            $attemptobj->quiz_send_notification_emails();
         }
 
     /// Clear the password check flag in the session.
+        $accessmanager = $attemptobj->get_access_manager($timenow);
         $accessmanager->clear_password_access();
 
     /// Send the user to the review page.
-        redirect($CFG->wwwroot . '/mod/quiz/review.php?attempt='.$attempt->id, 0);
+        redirect($attemptobj->review_url());
     }
 
-/// Now is the right time to check access (unless we are starting a new attempt, and did it above).
-    if (!$newattempt) {
-        $messages = $accessmanager->prevent_access();
-        if (!$canpreview && $messages) {
-            //TODO: need more detailed error info
-            print_error('attempterror', 'quiz', $CFG->wwwroot . '/mod/quiz/view.php?q=' . $quiz->id);
-        }
-        $accessmanager->do_password_check($canpreview);
+/// Now is the right time to check access.
+    $accessmanager = $attemptobj->get_access_manager($timenow);
+    $messages = $accessmanager->prevent_access();
+    if (!$attemptobj->is_preview_user() && $messages) {
+        print_error('attempterror', 'quiz', $quizobj->view_url(),
+                $accessmanager->print_messages($messages, true));
     }
+    $accessmanager->do_password_check($attemptobj->is_preview_user());
+
+/// Having processed the responses, we want to go to the summary page.
+if ($page == -1) {
+    redirect($attemptobj->summary_url());
+}
 
 /// Print the quiz page ////////////////////////////////////////////////////////
 
     // Print the page header
     require_js($CFG->wwwroot . '/mod/quiz/quiz.js');
-    $pagequestions = explode(',', $pagelist);
-    $strattemptnum = get_string('attempt', 'quiz', $attempt->attempt);
-    $headtags = get_html_head_contributions($pagequestions, $questions, $states);
-    if ($accessmanager->securewindow_required($canpreview)) {
-        $accessmanager->setup_secure_page($course->shortname.': '.format_string($quiz->name), $headtags);
+    $title = get_string('attempt', 'quiz', $attemptobj->get_attempt_number());
+    $headtags = $attemptobj->get_html_head_contributions($page);
+    if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) {
+        $accessmanager->setup_secure_page($attemptobj->get_course()->shortname . ': ' .
+                format_string($attemptobj->get_quiz_name()), $headtags);
     } else {
-        $strupdatemodule = has_capability('moodle/course:manageactivities', $coursecontext)
-                    ? update_module_button($cm->id, $course->id, get_string('modulename', 'quiz'))
-                    : "";
-        $navigation = build_navigation($strattemptnum, $cm);
-        print_header_simple(format_string($quiz->name), "", $navigation, "", $headtags, true, $strupdatemodule);
+        print_header_simple(format_string($attemptobj->get_quiz_name()), '', $attemptobj->navigation($title),
+                '', $headtags, true, $attemptobj->update_module_button());
     }
     echo '<div id="overDiv" style="position:absolute; visibility:hidden; z-index:1000;"></div>'; // for overlib
 
-    if ($canpreview) {
+    if ($attemptobj->is_preview_user()) {
     /// Show the tab bar.
         $currenttab = 'preview';
         include('tabs.php');
 
     /// Heading and tab bar.
         print_heading(get_string('previewquiz', 'quiz', format_string($quiz->name)));
-        print_restart_preview_button($quiz);
+        $attemptobj->print_restart_preview_button();
 
     /// Inform teachers of any restrictions that would apply to students at this point.
         if ($messages) {
     } else {
     /// Just a heading.
         if ($quiz->attempts != 1) {
-            print_heading(format_string($quiz->name).' - '.$strattemptnum);
+            print_heading(format_string($quiz->name).' - '.$title);
         } else {
             print_heading(format_string($quiz->name));
         }
     }
 
     // Start the form
-    echo '<form id="responseform" method="post" action="attempt.php?q=', s($quiz->id), '&amp;page=', s($page),
+    echo '<form id="responseform" method="post" action="', $attemptobj->attempt_url(0, $page),
             '" enctype="multipart/form-data"' .
             ' onclick="this.autocomplete=\'off\'" onkeypress="return check_enter(event);">', "\n";
-    if($quiz->timelimit > 0) {
+    if($attemptobj->get_quiz()->timelimit > 0) {
         // Make sure javascript is enabled for time limited quizzes
         ?>
         <script type="text/javascript">
     echo '<div>';
 
 /// Print the navigation panel if required
-    $numpages = quiz_number_of_pages($attempt->layout);
-    if ($numpages > 1) {
-        quiz_print_navigation_panel($page, $numpages);
-    }
+    // TODO!!!
+    quiz_print_navigation_panel($page, $attemptobj->get_num_pages());
 
 /// Print all the questions
-    $number = quiz_first_questionnumber($attempt->layout, $pagelist);
-    foreach ($pagequestions as $i) {
-        $options = quiz_get_renderoptions($quiz->review, $states[$i]);
-        // Print the question
-        print_question($questions[$i], $states[$i], $number, $quiz, $options);
-        save_question_session($questions[$i], $states[$i]);
-        $number += $questions[$i]->length;
+    foreach ($attemptobj->get_question_ids($page) as $id) {
+        $attemptobj->print_question($id);
     }
 
-/// Print the submit buttons
-    $strconfirmattempt = get_string("confirmclose", "quiz");
-    $onclick = "return confirm('$strconfirmattempt')";
+/// Print a link to the next page.
     echo "<div class=\"submitbtns mdl-align\">\n";
-
-    echo "<input type=\"submit\" name=\"saveattempt\" value=\"".get_string("savenosubmit", "quiz")."\" />\n";
-    if ($quiz->optionflags & QUESTION_ADAPTIVE) {
-        echo "<input type=\"submit\" name=\"markall\" value=\"".get_string("markall", "quiz")."\" />\n";
+    if ($attemptobj->is_last_page($page)) {
+        $nextpage = -1;
+    } else {
+        $nextpage = $page + 1;
     }
-    echo "<input type=\"submit\" name=\"finishattempt\" value=\"".get_string("finishattempt", "quiz")."\" onclick=\"$onclick\" />\n";
-
+    echo link_arrow_right(get_string('next'), 'javascript:navigate(' . $nextpage . ')');
     echo "</div>";
 
-    // Print the navigation panel if required
-    if ($numpages > 1) {
-        quiz_print_navigation_panel($page, $numpages);
-    }
-
     // Finish the form
     echo '</div>';
     echo '<input type="hidden" name="timeup" id="timeup" value="0" />';
     // Add a hidden field with questionids. Do this at the end of the form, so
     // if you navigate before the form has finished loading, it does not wipe all
     // the student's answers.
-    echo '<input type="hidden" name="questionids" value="'.$pagelist."\" />\n";
+    echo '<input type="hidden" name="questionids" value="' .
+            implode(',', $attemptobj->get_question_ids($page)) . "\" />\n";
 
     echo "</form>\n";
 
     // Finish the page
-    $accessmanager->show_attempt_timer_if_needed($attempt, time());
-    if ($accessmanager->securewindow_required($canpreview)) {
+    $accessmanager->show_attempt_timer_if_needed($attemptobj->get_attempt(), time());
+    if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) {
         print_footer('empty');
     } else {
-        print_footer($course);
+        print_footer($attemptobj->get_course());
     }
 ?>
index a521c729257263a32753ffd46a1a68f9c268d12a..a271e65c2ffa934a52733e251d3917c1b56e59fa 100644 (file)
@@ -32,13 +32,14 @@ class quiz {
     protected $cm;
     protected $quiz;
     protected $context;
+    protected $questionids; // All question ids in order that they appear in the quiz.
+    protected $pagequestionids; // array page no => array of questionids on the page in order.
     
     // Fields set later if that data is needed.
+    protected $questions = null;
     protected $accessmanager = null;
     protected $reviewoptions = null;
     protected $ispreviewuser = null;
-    protected $questions = array();
-    protected $questionsnumbered = false;
 
     // Constructor =========================================================================
     /**
@@ -53,29 +54,41 @@ class quiz {
         $this->cm = $cm;
         $this->course = $course;
         $this->context = get_context_instance(CONTEXT_MODULE, $cm->id);
+        $this->determine_layout();
     }
 
     // Functions for loading more data =====================================================
     public function load_questions_on_page($page) {
-        $this->load_questions(quiz_questions_on_page($this->quiz->layout, $page));
+        $this->load_questions($this->pagequestionids[$page]);
+    }
+
+    public function preload_questions() {
+        if (empty($this->questionids)) {
+            throw new moodle_quiz_exception($this, 'noquestions', $this->edit_url());
+        }
+        $this->questions = question_preload_questions($this->questionids,
+                'qqi.grade AS maxgrade, qqi.id AS instance',
+                '{quiz_question_instances} qqi ON qqi.quiz = :quizid AND q.id = qqi.question',
+                array('quizid' => $this->quiz->id));
+        $this->number_questions();
     }
 
     /**
      * Load some or all of the queestions for this quiz.
      *
-     * @param string $questionlist comma-separate list of question ids. Blank for all.
+     * @param array $questionids question ids of the questions to load. null for all.
      */
-    public function load_questions($questionlist = '') {
-        if (!$questionlist) {
-            $questionlist = quiz_questions_in_quiz($this->quiz->layout);
+    public function load_questions($questionids = null) {
+        if (is_null($questionids)) {
+            $questionids = $this->questionids;
+        }
+        $questionstoprocess = array();
+        foreach ($questionids as $id) {
+            $questionstoprocess[$id] = $this->questions[$id];
         }
-        $newquestions = question_load_questions($questionlist, 'qqi.grade AS maxgrade, qqi.id AS instance',
-                '{quiz_question_instances} qqi ON qqi.quiz = ' . $this->quiz->id . ' AND q.id = qqi.question');
-        if (is_string($newquestions)) {
-            throw new moodle_quiz_exception($this, 'loadingquestionsfailed', $newquestions);
+        if (!get_question_options($questionstoprocess)) {
+            throw new moodle_quiz_exception($this, 'loadingquestionsfailed', implode(', ', $questionids));
         }
-        $this->questions = $this->questions + $newquestions;
-        $this->questionsnumbered = false;
     }
 
     // Simple getters ======================================================================
@@ -84,6 +97,11 @@ class quiz {
         return $this->course->id;
     }
 
+    /** @return object the row of the course table. */
+    public function get_course() {
+        return $this->course;
+    }
+
     /** @return integer the quiz id. */
     public function get_quizid() {
         return $this->quiz->id;
@@ -120,6 +138,22 @@ class quiz {
         return $this->ispreviewuser;
     }
 
+    /**
+     * @return integer number fo pages in this quiz.
+     */
+    public function get_num_pages() {
+        return count($this->pagequestionids);
+    }
+    
+
+    /**
+     * @param int $page page number
+     * @return boolean true if this is the last page of the quiz.
+     */
+    public function is_last_page($page) {
+        return $page == count($this->pagequestionids) - 1;
+    }
+
     /**
      * @param integer $id the question id.
      * @return object the question object with that id.
@@ -129,18 +163,68 @@ class quiz {
         return $this->questions[$id];
     }
 
+    /**
+     * @param array $questionids question ids of the questions to load. null for all.
+     */
+    public function get_questions($questionids = null) {
+        if (is_null($questionids)) {
+            $questionids = $this->questionids;
+        }
+        $questions = array();
+        foreach ($questionids as $id) {
+            $questions[$id] = $this->questions[$id];
+            $this->ensure_question_loaded($id);
+        }
+        return $questions;
+    }
+
+    /**
+     * Return the list of question ids for either a given page of the quiz, or for the 
+     * whole quiz.
+     *
+     * @param mixed $page string 'all' or integer page number.
+     * @return array the reqested list of question ids.
+     */
+    public function get_question_ids($page = 'all') {
+        if ($page == 'all') {
+            $list = $this->questionids;
+        } else {
+            $list = $this->pagequestionids[$page];
+        }
+        // Clone the array, so our private arrays cannot be modified.
+        $result = array();
+        foreach ($list as $id) {
+            $result[] = $id;
+        }
+        return $result;
+    }
+
     /**
      * @param integer $timenow the current time as a unix timestamp.
      * @return object and instance of the quiz_access_manager class for this quiz at this time.
      */
     public function get_access_manager($timenow) {
         if (is_null($this->accessmanager)) {
-            $this->accessmanager = new quiz_access_manager($this->quiz, $timenow,
+            $this->accessmanager = new quiz_access_manager($this, $timenow,
                     has_capability('mod/quiz:ignoretimelimits', $this->context, NULL, false));
         }
         return $this->accessmanager;
     }
 
+    /**
+     * Wrapper round the has_capability funciton that automatically passes in the quiz context.
+     */
+    public function has_capability($capability, $userid = NULL, $doanything = true) {
+        return has_capability($capability, $this->context, $userid, $doanything);
+    }
+
+    /**
+     * Wrapper round the require_capability funciton that automatically passes in the quiz context.
+     */
+    public function require_capability($capability, $userid = NULL, $doanything = true) {
+        return require_capability($capability, $this->context, $userid, $doanything);
+    }
+
     // URLs related to this attempt ========================================================
     /**
      * @return string the URL of this quiz's view page.
@@ -150,6 +234,40 @@ class quiz {
         return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $this->cm->id;
     }
 
+    /**
+     * @return string the URL of this quiz's edit page.
+     */
+    public function edit_url() {
+        global $CFG;
+        return $CFG->wwwroot . '/mod/quiz/edit.php?cmid=' . $this->cm->id;
+    }
+
+    /**
+     * @param integer $attemptid the id of an attempt.
+     * @return string the URL of that attempt.
+     */
+    public function attempt_url($attemptid) {
+        global $CFG;
+        return $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $attemptid . '&amp;page=0';
+    }
+
+    /**
+     * @return string the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
+     */
+    public function start_attempt_url() {
+        global $CFG;
+        return $CFG->wwwroot . '/mod/quiz/startattempt.php';
+    }
+
+    /**
+     * @param integer $attemptid the id of an attempt.
+     * @return string the URL of the review of that attempt.
+     */
+    public function review_url($attemptid) {
+        global $CFG;
+        return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $attemptid;
+    }
+
     // Bits of content =====================================================================
     /**
      * @return string the HTML snipped that needs to be supplied to print_header_simple
@@ -175,11 +293,67 @@ class quiz {
 
     // Private methods =====================================================================
     // Check that the definition of a particular question is loaded, and if not throw an exception.
-    private function ensure_question_loaded($id) {
-        if (!array_key_exists($id, $this->questions)) {
+    protected function ensure_question_loaded($id) {
+        if (isset($this->questions[$id]->_partiallyloaded)) {
             throw new moodle_quiz_exception($this, 'questionnotloaded', $id);
         }
     }
+
+    private function determine_layout() {
+        $this->questionids = array();
+        $this->pagequestionids = array();
+
+        // Get the appropriate layout string (from quiz or attempt).
+        $layout = $this->get_layout_string();
+        if (empty($layout)) {
+            // Nothing to do.
+            return;
+        }
+
+        // Break up the layout string into pages.
+        $pagelayouts = explode(',0', $layout);
+
+        // Strip off any empty last page (normally there is one).
+        if (end($pagelayouts) == '') {
+            array_pop($pagelayouts);
+        }
+
+        // File the ids into the arrays.
+        $this->questionids = array();
+        $this->pagequestionids = array();
+        foreach ($pagelayouts as $page => $pagelayout) {
+            $pagelayout = trim($pagelayout, ',');
+            if ($pagelayout == '') continue;
+            $this->pagequestionids[$page] = explode(',', $pagelayout);
+            foreach ($this->pagequestionids[$page] as $id) {
+                $this->questionids[] = $id;
+            }
+        }
+    }
+
+    // Number the questions.
+    private function number_questions() {
+        $number = 1;
+        foreach ($this->pagequestionids as $page => $questionids) {
+            foreach ($questionids as $id) {
+                if ($this->questions[$id]->length > 0) {
+                    $this->questions[$id]->_number = $number;
+                    $number += $this->questions[$id]->length;
+                } else {
+                    $this->questions[$id]->_number = get_string('infoshort', 'quiz');
+                }
+                $this->questions[$id]->_page = $page;
+            }
+        }
+    }
+
+    /**
+     * @return string the layout of this quiz. Used by number_questions to
+     * work out which questions are on which pages. 
+     */
+    protected function get_layout_string() {
+        return $this->quiz->questions;
+    }
 }
 
 /**
@@ -215,59 +389,30 @@ class quiz_attempt extends quiz {
             throw new moodle_exception('invalidcoursemodule');
         }
         parent::__construct($quiz, $cm, $course);
+        $this->preload_questions();
     }
 
     // Functions for loading more data =====================================================
-    public function load_questions_on_page($page) {
-        $this->load_questions(quiz_questions_on_page($this->attempt->layout, $page));
-    }
-
     /**
-     * Load some or all of the queestions for this quiz.
+     * Load the state of a number of questions that have already been loaded.
      *
-     * @param string $questionlist comma-separate list of question ids. Blank for all.
+     * @param array $questionids question ids to process. Blank = all.
      */
-    public function load_questions($questionlist = '') {
-        if (!$questionlist) {
-            $questionlist = quiz_questions_in_quiz($this->attempt->layout);
+    public function load_question_states($questionids = null) {
+        if (is_null($questionids)) {
+            $questionids = $this->questionids;
         }
-        parent::load_questions($questionlist);
-    }
-
-    public function load_question_states() {
-        $questionstodo = array_diff_key($this->questions, $this->states);
-        if (!$newstates = get_question_states($questionstodo, $this->quiz, $this->attempt)) {
+        $questionstoprocess = array();
+        foreach ($questionids as $id) {
+            $this->ensure_question_loaded($id);
+            $questionstoprocess[$id] = $this->questions[$id];
+        }
+        if (!$newstates = get_question_states($questionstoprocess, $this->quiz, $this->attempt)) {
             throw new moodle_quiz_exception($this, 'cannotrestore');
         }
         $this->states = $this->states + $newstates;
     }
 
-    /**
-     * Number the loaded questions.
-     * 
-     * At the moment, this assumes for simplicity that the loaded questions are contiguous.
-     */
-    public function number_questions($page = 'all') {
-        if ($this->questionsnumbered) {
-            return;
-        }
-        if ($page != 'all') {
-            $pagelist = quiz_questions_in_page($this->attempt->layout, $page);
-            $number = quiz_first_questionnumber($this->attempt->layout, $pagelist);
-        } else {
-            $number = 1;
-        }
-        $questionids = $this->get_question_ids($page);
-        foreach ($questionids as $id) {
-            if ($this->questions[$id]->length > 0) {
-                $this->questions[$id]->number = $number;
-                $number += $this->questions[$id]->length;
-            } else {
-                $this->questions[$id]->number = get_string('infoshort', 'quiz');
-            }
-        }
-    }
-
     // Simple getters ======================================================================
     /** @return integer the attempt id. */
     public function get_attemptid() {
@@ -279,6 +424,11 @@ class quiz_attempt extends quiz {
         return $this->attempt;
     }
 
+    /** @return integer the number of this attemp (is it this user's first, second, ... attempt). */
+    public function get_attempt_number() {
+        return $this->attempt->attempt;
+    }
+
     /** @return integer the id of the user this attempt belongs to. */
     public function get_userid() {
         return $this->attempt->userid;
@@ -289,6 +439,11 @@ class quiz_attempt extends quiz {
         return $this->attempt->timefinish != 0;
     }
 
+    public function get_question_state($questionid) {
+        $this->ensure_state_loaded($questionid);
+        return $this->states[$questionid];
+    }
+
     /**
      * Wrapper that calls quiz_get_reviewoptions with the appropriate arguments.
      *
@@ -301,22 +456,6 @@ class quiz_attempt extends quiz {
         return $this->reviewoptions;
     }
 
-    /**
-     * Return the list of question ids for either a given page of the quiz, or for the 
-     * whole quiz.
-     *
-     * @param mixed $page string 'all' or integer page number.
-     * @return array the reqested list of question ids.
-     */
-    public function get_question_ids($page = 'all') {
-        if ($page == 'all') {
-            $questionlist = quiz_questions_in_quiz($this->attempt->layout);
-        } else {
-            $questionlist = quiz_questions_in_page($this->attempt->layout, $page);
-        }
-        return explode(',', $questionlist);
-    }
-
     /**
      * Get a quiz_attempt_question_iterator for either a page of the quiz, or a whole quiz.
      * You must have called load_questions with an appropriate argument first.
@@ -389,18 +528,14 @@ class quiz_attempt extends quiz {
     /**
      * @param integer $page if specified, the URL of this particular page of the attempt, otherwise
      * the URL will go to the first page.
-     * @param integer $question a question id. If set, will add a fragment to the URL
+     * @param integer $questionid a question id. If set, will add a fragment to the URL
      * to jump to a particuar question on the page.
      * @return string the URL to continue this attempt.
      */
-    public function attempt_url($page = 0, $question = false) {
+    public function attempt_url($questionid = 0, $page = -1) {
         global $CFG;
-        $fragment = '';
-        if ($question) {
-            $fragment = '#q' . $question;
-        }
-        return $CFG->wwwroot . '/mod/quiz/attempt.php?id=' .
-                $this->cm->id . '$amp;page=' . $page . $fragment;
+        return $CFG->wwwroot . '/mod/quiz/attempt.php?attempt=' . $this->attempt->id . '&' .
+                $this->page_and_question_fragment($questionid, $page);
     }
 
     /**
@@ -414,28 +549,43 @@ class quiz_attempt extends quiz {
     /**
      * @param integer $page if specified, the URL of this particular page of the attempt, otherwise
      * the URL will go to the first page.
-     * @param integer $question a question id. If set, will add a fragment to the URL
+     * @param integer $questionid a question id. If set, will add a fragment to the URL
      * to jump to a particuar question on the page.
      * @param boolean $showall if true, the URL will be to review the entire attempt on one page,
      * and $page will be ignored.
      * @return string the URL to review this attempt.
      */
-    public function review_url($page = 0, $question = false, $showall = false) {
+    public function review_url($questionid = 0, $page = -1, $showall = false) {
         global $CFG;
-        $fragment = '';
-        if ($question) {
-            $fragment = '#q' . $question;
-        }
-        $param = '';
-        if ($showall) {
-            $param = '$amp;showall=1';
-        } else if ($page) {
-            $param = '$amp;page=' . $page;
-        }
-        return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' .
-                $this->attempt->id . $param . $fragment;
+        return $CFG->wwwroot . '/mod/quiz/review.php?attempt=' . $this->attempt->id . '&' .
+                $this->page_and_question_fragment($questionid, $page, $showall);
+    }
+
+    // Bits of content =====================================================================
+    public function get_html_head_contributions($page = 'all') {
+        return get_html_head_contributions($this->get_question_ids($page),
+                $this->questions, $this->states);
+    }
+
+    public function print_restart_preview_button() {
+        global $CFG;
+        echo '<div class="controls">';
+        print_single_button($this->start_attempt_url(), array('cmid' => $this->cm->id,
+                'forcenew' => true, 'sesskey' => sesskey()), get_string('startagain', 'quiz'), 'post');
+        echo '</div>';
     }
 
+    public function print_question($id) {
+        $options = quiz_get_renderoptions($this->quiz->review, $this->states[$id]);
+        print_question($this->questions[$id], $this->states[$id], $this->questions[$id]->_number,
+                $this->quiz, $options);
+    }
+
+    public function quiz_send_notification_emails() {
+        quiz_send_notification_emails($this->course, $this->quiz, $this->attempt,
+                $this->context, $this->cm);
+    }
+    
 
     // Private methods =====================================================================
     // Check that the state of a particular question is loaded, and if not throw an exception.
@@ -444,6 +594,47 @@ class quiz_attempt extends quiz {
             throw new moodle_quiz_exception($this, 'statenotloaded', $id);
         }
     }
+
+    /**
+     * @return string the layout of this quiz. Used by number_questions to
+     * work out which questions are on which pages. 
+     */
+    protected function get_layout_string() {
+        return $this->attempt->layout;
+    }
+
+    /**
+     * Enter description here...
+     *
+     * @param unknown_type $questionid the id of a particular question on the page to jump to.
+     * @param integer $page -1 to look up the page number from the questionid, otherwise the page number to use.
+     * @param boolean $showall
+     * @return string bit to add to the end of a URL.
+     */
+    private function page_and_question_fragment($questionid, $page, $showall = false) {
+        if ($page = -1) {
+            if ($questionid) {
+                $page = $this->questions[$questionid]->_page;
+            } else {
+                $page = 0;
+            }
+        }
+        if ($showall) {
+            $page = 0;
+        }
+        $fragment = '';
+        if ($questionid && $questionid != reset($this->pagequestionids[$page])) {
+            $fragment = '#q' . $questionid;
+        }
+        $param = '';
+        if ($showall) {
+            $param = 'showall=1';
+        } else if (/*$page > 1*/ true) {
+            // TODO currently needed by the navigate JS, but clean this up later.
+            $param = 'page=' . $page;
+        }
+        return $param . $fragment;
+    }
 }
 
 /**
@@ -463,7 +654,6 @@ class quiz_attempt_question_iterator implements Iterator {
      */
     public function __construct(quiz_attempt $attemptobj, $page = 'all') {
         $this->attemptobj = $attemptobj;
-        $attemptobj->number_questions($page);
         $this->questionids = $attemptobj->get_question_ids($page);
     }
 
@@ -484,11 +674,10 @@ class quiz_attempt_question_iterator implements Iterator {
     public function key() {
         $id = current($this->questionids);
         if ($id) {
-            return $this->attemptobj->get_question($id)->number;
+            return $this->attemptobj->get_question($id)->_number;
         } else {
             return false;
         }
-        return $this->attemptobj->get_question(current($this->questionids))->number;
     }
 
     public function next() {
index dd09695cdc7e35b246a5d86ff2664fa0311faf0f..9ed0bd7bdd334cb56920a7fa8cefd582ab7a4aba 100644 (file)
@@ -22,6 +22,7 @@
 require_once($CFG->dirroot . '/mod/quiz/lib.php');
 require_once($CFG->dirroot . '/mod/quiz/accessrules.php');
 require_once($CFG->dirroot . '/question/editlib.php');
+require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
 
 /// Constants ///////////////////////////////////////////////////////////////////
 
@@ -891,14 +892,6 @@ function quiz_get_combined_reviewoptions($quiz, $attempts, $context=null) {
     return array($someoptions, $alloptions);
 }
 
-function print_restart_preview_button($quiz) {
-    global $CFG;
-    echo '<div class="controls">';
-    print_single_button($CFG->wwwroot . '/mod/quiz/attempt.php',
-            array('q' => $quiz->id, 'forcenew' => true), get_string('startagain', 'quiz'));
-    echo '</div>';
-}
-
 /// FUNCTIONS FOR SENDING NOTIFICATION EMAILS ///////////////////////////////
 
 /**
index a96a89b64f4be4351f7144abe85f6fb9825a42d8..9d13a62acd8a0a55fcd9adc2737d0f58d60a1236 100644 (file)
@@ -38,7 +38,7 @@
 
 /// Create an object to manage all the other (non-roles) access rules.
     $timenow = time();
-    $accessmanager = new quiz_access_manager($quiz, $timenow,
+    $accessmanager = new quiz_access_manager(new quiz($quiz, $cm, $course), $timenow,
             has_capability('mod/quiz:ignoretimelimits', $context, NULL, false));
     $options = quiz_get_reviewoptions($quiz, $attempt, $context);
 
 /// Print heading.
     print_heading(format_string($quiz->name));
     if ($canpreview && $reviewofownattempt) {
-        print_restart_preview_button($quiz);
+        $attemptobj = new quiz_attempt($attemptid);
+        $attemptobj->print_restart_preview_button();
     }
     print_heading($strreviewtitle);
 
diff --git a/mod/quiz/startattempt.php b/mod/quiz/startattempt.php
new file mode 100644 (file)
index 0000000..1e10471
--- /dev/null
@@ -0,0 +1,135 @@
+<?php  // $Id$
+/**
+ * This page deals with starting a new attempt at a quiz.
+ *
+ * Normally, it will end up redirecting to attempt.php - unless a password form is displayed.
+ *
+ * @author Tim Hunt.
+ * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
+ * @package quiz
+ */
+
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
+
+/// Get submitted parameters.
+$id = required_param('cmid', PARAM_INT); // Course Module ID
+$forcenew = optional_param('forcenew', false, PARAM_BOOL); // Used to force a new preview
+
+if (!$cm = get_coursemodule_from_id('quiz', $id)) {
+    print_error('invalidcoursemodule');
+}
+if (!$course = $DB->get_record('course', array('id' => $cm->course))) {
+    print_error("coursemisconf");
+}
+if (!$quiz = $DB->get_record('quiz', array('id' => $cm->instance))) {
+    print_error('invalidcoursemodule');
+}
+
+$quizobj = new quiz($quiz, $cm, $course);
+
+/// Check login and get contexts.
+require_login($quizobj->get_courseid(), false, $quizobj->get_cm());
+
+/// if no questions have been set up yet redirect to edit.php
+if (!$quizobj->get_question_ids() && $quizobj->has_capability('mod/quiz:manage')) {
+    redirect($quizobj->edit_url());
+}
+
+/// Create an object to manage all the other (non-roles) access rules.
+$accessmanager = $quizobj->get_access_manager(time());
+if ($quizobj->is_preview_user() && $forcenew) {
+    $accessmanager->clear_password_access();
+}
+
+// This page should only respond to post requests, if not, redirect to the view page.
+// However, becuase 'secure' mode opens in a new window, we cannot do enforce this rule for them.
+if (!data_submitted() && !$accessmanager->securewindow_required($quizobj->is_preview_user())) {
+    redirect($quizobj->view_url());
+}
+if (!confirm_sesskey()) {
+    throw new moodle_exception('confirmsesskeybad', 'error', $quizobj->view_url());
+}
+
+/// Check capabilites.
+if (!$quizobj->is_preview_user()) {
+    $quizobj->require_capability('mod/quiz:attempt');
+}
+
+/// Check to see if a new preview was requested.
+if ($quizobj->is_preview_user() && $forcenew) {
+/// To force the creation of a new preview, we set a finish time on the
+/// current attempt (if any). It will then automatically be deleted below
+    $DB->set_field('quiz_attempts', 'timefinish', time(), array('quiz' => $quiz->id, 'userid' => $USER->id));
+}
+
+/// Look for an existing attempt.
+$lastattempt = quiz_get_latest_attempt_by_user($quiz->id, $USER->id);
+
+if ($lastattempt && !$lastattempt->timefinish) {
+/// Continuation of an attempt - check password then redirect.
+    $accessmanager->do_password_check($quizobj->is_preview_user());
+    redirect($quizobj->attempt_url($lastattempt->id));
+}
+
+/// Get number for the next or unfinished attempt
+if ($lastattempt && !$lastattempt->preview && !$quizobj->is_preview_user()) {
+    $lastattemptid = $lastattempt->id;
+    $attemptnumber = $lastattempt->attempt + 1;
+} else {
+    $lastattempt = false;
+    $lastattemptid = false;
+    $attemptnumber = 1;
+}
+
+/// Check access.
+$messages = $accessmanager->prevent_access() +
+        $accessmanager->prevent_new_attempt($attemptnumber - 1, $lastattempt);
+if (!$quizobj->is_preview_user() && $messages) {
+    print_error('attempterror', 'quiz', $quizobj->view_url(),
+            $accessmanager->print_messages($messages, true));
+}
+$accessmanager->do_password_check($quizobj->is_preview_user());
+
+/// Delete any previous preview attempts belonging to this user.
+if ($oldattempts = $DB->get_records_select('quiz_attempts', "quiz = ?
+        AND userid = ? AND preview = 1", array($quiz->id, $USER->id))) {
+    foreach ($oldattempts as $oldattempt) {
+        quiz_delete_attempt($oldattempt, $quiz);
+    }
+}
+
+/// Create the new attempt and initialize the question sessions
+$attempt = quiz_create_attempt($quiz, $attemptnumber, $lastattempt, time(), $quizobj->is_preview_user());
+
+/// Save the attempt in the database.
+if (!$attempt->id = $DB->insert_record('quiz_attempts', $attempt)) {
+    quiz_error($quiz, 'newattemptfail');
+}
+
+/// Log the new attempt.
+if ($attempt->preview) {
+    add_to_log($course->id, 'quiz', 'preview', "attempt.php?id=$cm->id",
+            "$quiz->id", $cm->id);
+} else {
+    add_to_log($course->id, 'quiz', 'attempt', "review.php?attempt=$attempt->id",
+            "$quiz->id", $cm->id);
+}
+
+/// Fully load all the questions in this quiz.
+$quizobj->preload_questions();
+$quizobj->load_questions();
+
+/// Create initial states for all questions in this quiz.
+if (!$states = get_question_states($quizobj->get_questions(), $quizobj->get_quiz(), $attempt, $lastattemptid)) {
+    print_error('cannotrestore', 'quiz');
+}
+
+/// Save all the newly created states.
+foreach ($quizobj->get_questions() as $i => $question) {
+    save_question_session($question, $states[$i]);
+}
+
+/// Redirect to the attempt page.
+redirect($quizobj->attempt_url($attempt->id));
+?>
\ No newline at end of file
index ddb0efe3df41bf5ef59f3b73709356302fd14279..ee5fd23ec7a52bf92d4b29df99f4b3088e87efa8 100644 (file)
@@ -7,14 +7,13 @@
  * @package quiz
  */
 
-require_once("../../config.php");
-require_once("locallib.php");
-require_once("attemptlib.php");
+require_once(dirname(__FILE__) . '/../../config.php');
+require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 
 $attemptid = required_param('attempt', PARAM_INT); // The attempt to summarise.
 $attemptobj = new quiz_attempt($attemptid);
 
-/// Check login and get contexts.
+/// Check login.
 require_login($attemptobj->get_courseid(), false, $attemptobj->get_cm());
 
 /// If this is not our own attempt, display an error.
@@ -22,7 +21,7 @@ if ($attemptobj->get_userid() != $USER->id) {
     print_error('notyourattempt', 'quiz', $attemptobj->view_url());
 }
 
-/// If the attempt is already closed, redirect them to the review page.
+/// If the attempt is alreadyuj closed, redirect them to the review page.
 if ($attemptobj->is_finished()) {
     redirect($attemptobj->review_url());
 }
@@ -48,7 +47,8 @@ $attemptobj->load_question_states();
 require_js($CFG->wwwroot . '/mod/quiz/quiz.js');
 $title = get_string('summaryofattempt', 'quiz');
 if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) {
-    $accessmanager->setup_secure_page($course->shortname.': '.format_string($quiz->name), $headtags);
+    $accessmanager->setup_secure_page($attemptobj->get_course()->shortname . ': ' .
+            format_string($attemptobj->get_quiz_name()), '');
 } else {
     print_header_simple(format_string($attemptobj->get_quiz_name()), '',
             $attemptobj->navigation($title), '', '', true, $attemptobj->update_module_button());
@@ -63,7 +63,7 @@ if ($attemptobj->is_preview_user()) {
 /// Print heading.
 print_heading(format_string($attemptobj->get_quiz_name()));
 if ($attemptobj->is_preview_user()) {
-    print_restart_preview_button($quiz);
+    $attemptobj->print_restart_preview_button();
 }
 print_heading($title);
 
@@ -83,7 +83,8 @@ $table->data = array();
 /// Get the summary info for each question.
 $questionids = $attemptobj->get_question_ids();
 foreach ($attemptobj->get_question_iterator() as $number => $question) {
-    $row = array($number, $attemptobj->get_question_status($question->id));
+    $row = array('<a href="' . $attemptobj->attempt_url($question->id) . '">' . $number . '</a>',
+            get_string($attemptobj->get_question_status($question->id), 'quiz'));
     if ($scorescolumn) {
         $row[] = $attemptobj->get_question_score($question->id);
     }
@@ -110,7 +111,7 @@ $accessmanager->show_attempt_timer_if_needed($attemptobj->get_attempt(), time())
 if ($accessmanager->securewindow_required($attemptobj->is_preview_user())) {
     print_footer('empty');
 } else {
-    print_footer($course);
+    print_footer($attemptobj->get_course());
 }
 
 ?>
index c1261e2c83754e2c92141d1f730ef2312020c0f2..317b3ab7da4f239cd2f1321d7568b929a3c80797 100644 (file)
@@ -44,7 +44,7 @@
 
 /// Create an object to manage all the other (non-roles) access rules.
     $timenow = time();
-    $accessmanager = new quiz_access_manager($quiz, $timenow,
+    $accessmanager = new quiz_access_manager(new quiz($quiz, $cm, $course), $timenow,
             has_capability('mod/quiz:ignoretimelimits', $context, NULL, false));
 
 /// If no questions have been set up yet redirect to edit.php