Merge branch 'MDL-40990' of git://github.com/timhunt/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 18 Mar 2015 00:22:06 +0000 (08:22 +0800)
committerDavid Monllao <davidm@moodle.com>
Wed, 18 Mar 2015 00:22:06 +0000 (08:22 +0800)
32 files changed:
mod/quiz/attemptlib.php
mod/quiz/backup/moodle2/backup_quiz_stepslib.php
mod/quiz/classes/output/edit_renderer.php
mod/quiz/classes/structure.php
mod/quiz/db/install.xml
mod/quiz/db/upgrade.php
mod/quiz/edit_rest.php
mod/quiz/lang/en/quiz.php
mod/quiz/repaginate.php
mod/quiz/styles.css
mod/quiz/tests/behat/attempt_require_previous.feature [new file with mode: 0644]
mod/quiz/tests/behat/behat_mod_quiz.php
mod/quiz/tests/behat/editing_require_previous.feature [new file with mode: 0644]
mod/quiz/tests/structure_test.php
mod/quiz/upgrade.txt
mod/quiz/version.php
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js
mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js
mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js
mod/quiz/yui/src/toolboxes/js/resource.js
mod/quiz/yui/src/toolboxes/js/section.js
mod/quiz/yui/src/toolboxes/js/toolbox.js
mod/quiz/yui/src/util/js/slot.js
question/behaviour/behaviourbase.php
question/behaviour/immediatefeedback/behaviour.php
question/behaviour/interactive/behaviour.php
question/behaviour/upgrade.txt
question/engine/questionattempt.php
question/engine/questionusage.php

index 3c2fa1a..91ac3d2 100644 (file)
@@ -446,13 +446,18 @@ class quiz_attempt {
     /** @var int maximum number of slots in the quiz for the review page to default to show all. */
     const MAX_SLOTS_FOR_DEFAULT_REVIEW_SHOW_ALL = 50;
 
-    // Basic data.
+    /** @var quiz object containing the quiz settings. */
     protected $quizobj;
+
+    /** @var stdClass the quiz_attempts row. */
     protected $attempt;
 
     /** @var question_usage_by_activity the question usage for this quiz attempt. */
     protected $quba;
 
+    /** @var array of quiz_slots rows. */
+    protected $slots;
+
     /** @var array page no => array of slot numbers on the page in order. */
     protected $pagelayout;
 
@@ -477,6 +482,8 @@ class quiz_attempt {
      *      of the state of each question. Else just set up the basic details of the attempt.
      */
     public function __construct($attempt, $quiz, $cm, $course, $loadquestions = true) {
+        global $DB;
+
         $this->attempt = $attempt;
         $this->quizobj = new quiz($quiz, $cm, $course);
 
@@ -485,6 +492,10 @@ class quiz_attempt {
         }
 
         $this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid);
+        $this->slots = $DB->get_records('quiz_slots',
+                array('quizid' => $this->get_quizid()), 'slot',
+                'slot, page, requireprevious, questionid, maxmark');
+
         $this->determine_layout();
         $this->number_questions();
     }
@@ -987,6 +998,21 @@ class quiz_attempt {
     }
 
     /**
+     * Checks whether the question in this slot requires the previous question to have been completed.
+     *
+     * @param int $slot the number used to identify this question within this attempt.
+     * @return bool whether the previous question must have been completed before this one can be seen.
+     */
+    public function is_blocked_by_previous_question($slot) {
+        return $slot > 1 && $this->slots[$slot]->requireprevious &&
+                !$this->get_quiz()->shufflequestions &&
+                $this->get_navigation_method() != QUIZ_NAVMETHOD_SEQ &&
+                !$this->quba->get_question_state($slot - 1)->is_finished() &&
+                $this->quba->can_question_finish_during_attempt($slot - 1);
+    }
+
+    /**
+     * Get the displayed question number for a slot.
      * @param int $slot the number used to identify this question within this attempt.
      * @return string the displayed question number for the question in this slot.
      *      For example '1', '2', '3' or 'i'.
@@ -1255,11 +1281,63 @@ class quiz_attempt {
      * @return string HTML for the question in its current state.
      */
     public function render_question($slot, $reviewing, $thispageurl = null) {
+        if ($this->is_blocked_by_previous_question($slot)) {
+            $placeholderqa = $this->make_blocked_question_placeholder($slot);
+
+            $displayoptions = $this->get_display_options($reviewing);
+            $displayoptions->manualcomment = question_display_options::HIDDEN;
+            $displayoptions->history = question_display_options::HIDDEN;
+            $displayoptions->readonly = true;
+
+            return html_writer::div($placeholderqa->render($displayoptions,
+                    $this->get_question_number($slot)),
+                    'mod_quiz-blocked_question_warning');
+        }
+
         return $this->quba->render_question($slot,
                 $this->get_display_options_with_edit_link($reviewing, $slot, $thispageurl),
                 $this->get_question_number($slot));
     }
 
+    /**
+     * Create a fake question to be displayed in place of a question that is blocked
+     * until the previous question has been answered.
+     *
+     * @param unknown $slot int slot number of the question to replace.
+     * @return question_definition the placeholde question.
+     */
+    protected function make_blocked_question_placeholder($slot) {
+        $replacedquestion = $this->get_question_attempt($slot)->get_question();
+
+        question_bank::load_question_definition_classes('description');
+        $question = new qtype_description_question();
+        $question->id = $replacedquestion->id;
+        $question->category = null;
+        $question->parent = 0;
+        $question->qtype = question_bank::get_qtype('description');
+        $question->name = '';
+        $question->questiontext = get_string('questiondependsonprevious', 'quiz');
+        $question->questiontextformat = FORMAT_HTML;
+        $question->generalfeedback = '';
+        $question->defaultmark = $this->quba->get_question_max_mark($slot);
+        $question->length = $replacedquestion->length;
+        $question->penalty = 0;
+        $question->stamp = '';
+        $question->version = 0;
+        $question->hidden = 0;
+        $question->timecreated = null;
+        $question->timemodified = null;
+        $question->createdby = null;
+        $question->modifiedby = null;
+
+        $placeholderqa = new question_attempt($question, $this->quba->get_id(),
+                null, $this->quba->get_question_max_mark($slot));
+        $placeholderqa->set_slot($slot);
+        $placeholderqa->start($this->get_quiz()->preferredbehaviour, 1);
+        $placeholderqa->set_flagged($this->is_question_flagged($slot));
+        return $placeholderqa;
+    }
+
     /**
      * Like {@link render_question()} but displays the question at the past step
      * indicated by $seq, rather than showing the latest step.
index aaa9e01..a15bb81 100644 (file)
@@ -57,7 +57,7 @@ class backup_quiz_activity_structure_step extends backup_questions_activity_stru
         $qinstances = new backup_nested_element('question_instances');
 
         $qinstance = new backup_nested_element('question_instance', array('id'), array(
-            'slot', 'page', 'questionid', 'maxmark'));
+            'slot', 'page', 'requireprevious', 'questionid', 'maxmark'));
 
         $feedbacks = new backup_nested_element('feedbacks');
 
index 86eafdd..60167f8 100644 (file)
@@ -59,7 +59,7 @@ class edit_renderer extends \plugin_renderer_base {
         // Information at the top.
         $output .= $this->quiz_state_warnings($structure);
         $output .= $this->quiz_information($structure);
-        $output .= $this->maximum_grade_input($quizobj->get_quiz(), $this->page->url);
+        $output .= $this->maximum_grade_input($structure, $pageurl);
         $output .= $this->repaginate_button($structure, $pageurl);
         $output .= $this->total_marks($quizobj->get_quiz());
 
@@ -83,8 +83,7 @@ class edit_renderer extends \plugin_renderer_base {
         $output .= $this->end_section_list();
 
         // Inialise the JavaScript.
-        $this->initialise_editing_javascript($quizobj->get_course(), $quizobj->get_quiz(),
-                $structure, $contexts, $pagevars, $pageurl);
+        $this->initialise_editing_javascript($structure, $contexts, $pagevars, $pageurl);
 
         // Include the contents of any other popups required.
         if ($structure->can_be_edited()) {
@@ -152,11 +151,11 @@ class edit_renderer extends \plugin_renderer_base {
     /**
      * Render the form for setting a quiz' overall grade
      *
-     * @param \stdClass $quiz the quiz settings from the database.
+     * @param structure $structure the quiz structure.
      * @param \moodle_url $pageurl the canonical URL of this page.
      * @return string HTML to output.
      */
-    public function maximum_grade_input($quiz, \moodle_url $pageurl) {
+    public function maximum_grade_input($structure, \moodle_url $pageurl) {
         $output = '';
         $output .= html_writer::start_div('maxgrade');
         $output .= html_writer::start_tag('form', array('method' => 'post', 'action' => 'edit.php',
@@ -165,8 +164,8 @@ class edit_renderer extends \plugin_renderer_base {
         $output .= html_writer::empty_tag('input', array('type' => 'hidden', 'name' => 'sesskey', 'value' => sesskey()));
         $output .= html_writer::input_hidden_params($pageurl);
         $a = html_writer::empty_tag('input', array('type' => 'text', 'id' => 'inputmaxgrade',
-                'name' => 'maxgrade', 'size' => ($quiz->decimalpoints + 2),
-                'value' => quiz_format_grade($quiz, $quiz->grade)));
+                'name' => 'maxgrade', 'size' => ($structure->get_decimal_places_for_grades() + 2),
+                'value' => $structure->formatted_quiz_grade()));
         $output .= html_writer::tag('label', get_string('maximumgradex', '', $a),
                 array('for' => 'inputmaxgrade'));
         $output .= html_writer::empty_tag('input', array('type' => 'submit',
@@ -342,10 +341,9 @@ class edit_renderer extends \plugin_renderer_base {
             $contexts, $pagevars, $pageurl) {
 
         $output = '';
-        foreach ($structure->get_questions_in_section($section->id) as $question) {
-            $output .= $this->question_row($structure, $question, $contexts, $pagevars, $pageurl);
+        foreach ($structure->get_slots_in_section($section->id) as $slot) {
+            $output .= $this->question_row($structure, $slot, $contexts, $pagevars, $pageurl);
         }
-
         return html_writer::tag('ul', $output, array('class' => 'section img-text'));
     }
 
@@ -353,30 +351,31 @@ class edit_renderer extends \plugin_renderer_base {
      * Displays one question with the surrounding controls.
      *
      * @param structure $structure object containing the structure of the quiz.
-     * @param \stdClass $question data from the question and quiz_slots tables.
+     * @param int $slot which slot we are outputting.
      * @param \question_edit_contexts $contexts the relevant question bank contexts.
      * @param array $pagevars the variables from {@link \question_edit_setup()}.
      * @param \moodle_url $pageurl the canonical URL of this page.
      * @return string HTML to output.
      */
-    public function question_row(structure $structure, $question, $contexts, $pagevars, $pageurl) {
+    public function question_row(structure $structure, $slot, $contexts, $pagevars, $pageurl) {
         $output = '';
 
-        $output .= $this->page_row($structure, $question, $contexts, $pagevars, $pageurl);
+        $output .= $this->page_row($structure, $slot, $contexts, $pagevars, $pageurl);
 
         // Page split/join icon.
         $joinhtml = '';
-        if ($structure->can_be_edited() && !$structure->is_last_slot_in_quiz($question->slot)) {
-            $joinhtml = $this->page_split_join_button($structure->get_quiz(),
-                    $question, !$structure->is_last_slot_on_page($question->slot));
+        if ($structure->can_be_edited() && !$structure->is_last_slot_in_quiz($slot)) {
+            $joinhtml = $this->page_split_join_button($structure, $slot);
         }
 
         // Question HTML.
-        $questionhtml = $this->question($structure, $question, $pageurl);
-        $questionclasses = 'activity ' . $question->qtype . ' qtype_' . $question->qtype . ' slot';
+        $questionhtml = $this->question($structure, $slot, $pageurl);
+        $qtype = $structure->get_question_type_for_slot($slot);
+        $questionclasses = 'activity ' . $qtype . ' qtype_' . $qtype . ' slot';
 
         $output .= html_writer::tag('li', $questionhtml . $joinhtml,
-                array('class' => $questionclasses, 'id' => 'slot-' . $question->slotid));
+                array('class' => $questionclasses, 'id' => 'slot-' . $structure->get_slot_id_for_slot($slot),
+                        'data-canfinish' => $structure->can_finish_during_the_attempt($slot)));
 
         return $output;
     }
@@ -385,30 +384,32 @@ class edit_renderer extends \plugin_renderer_base {
      * Displays one question with the surrounding controls.
      *
      * @param structure $structure object containing the structure of the quiz.
-     * @param \stdClass $question data from the question and quiz_slots tables.
+     * @param int $slot the first slot on the page we are outputting.
      * @param \question_edit_contexts $contexts the relevant question bank contexts.
      * @param array $pagevars the variables from {@link \question_edit_setup()}.
      * @param \moodle_url $pageurl the canonical URL of this page.
      * @return string HTML to output.
      */
-    public function page_row(structure $structure, $question, $contexts, $pagevars, $pageurl) {
+    public function page_row(structure $structure, $slot, $contexts, $pagevars, $pageurl) {
         $output = '';
 
+        $pagenumber = $structure->get_page_number_for_slot($slot);
+
         // Put page in a span for easier styling.
-        $page = html_writer::tag('span', get_string('page') . ' ' . $question->page,
+        $page = html_writer::tag('span', get_string('page') . ' ' . $pagenumber,
                 array('class' => 'text'));
 
-        if ($structure->is_first_slot_on_page($question->slot)) {
+        if ($structure->is_first_slot_on_page($slot)) {
             // Add the add-menu at the page level.
             $addmenu = html_writer::tag('span', $this->add_menu_actions($structure,
-                    $question->page, $pageurl, $contexts, $pagevars),
+                    $pagenumber, $pageurl, $contexts, $pagevars),
                     array('class' => 'add-menu-outer'));
 
             $addquestionform = $this->add_question_form($structure,
-                    $question->page, $pageurl, $pagevars);
+                    $pagenumber, $pageurl, $pagevars);
 
             $output .= html_writer::tag('li', $page . $addmenu . $addquestionform,
-                    array('class' => 'pagenumber activity yui3-dd-drop page', 'id' => 'page-' . $question->page));
+                    array('class' => 'pagenumber activity yui3-dd-drop page', 'id' => 'page-' . $pagenumber));
         }
 
         return $output;
@@ -542,30 +543,29 @@ class edit_renderer extends \plugin_renderer_base {
      * Display a question.
      *
      * @param structure $structure object containing the structure of the quiz.
-     * @param \stdClass $question data from the question and quiz_slots tables.
+     * @param int $slot the first slot on the page we are outputting.
      * @param \moodle_url $pageurl the canonical URL of this page.
      * @return string HTML to output.
      */
-    public function question(structure $structure, $question, \moodle_url $pageurl) {
+    public function question(structure $structure, $slot, \moodle_url $pageurl) {
         $output = '';
-
         $output .= html_writer::start_tag('div');
 
         if ($structure->can_be_edited()) {
-            $output .= $this->question_move_icon($question);
+            $output .= $this->question_move_icon($structure, $slot);
         }
 
         $output .= html_writer::start_div('mod-indent-outer');
-        $output .= $this->question_number($question->displayednumber);
+        $output .= $this->question_number($structure->get_displayed_number_for_slot($slot));
 
         // This div is used to indent the content.
         $output .= html_writer::div('', 'mod-indent');
 
         // Display the link to the question (or do nothing if question has no url).
-        if ($question->qtype == 'random') {
-            $questionname = $this->random_question($structure, $question, $pageurl);
+        if ($structure->get_question_type_for_slot($slot) == 'random') {
+            $questionname = $this->random_question($structure, $slot, $pageurl);
         } else {
-            $questionname = $this->question_name($structure, $question, $pageurl);
+            $questionname = $this->question_name($structure, $slot, $pageurl);
         }
 
         // Start the div for the activity title, excluding the edit icons.
@@ -577,12 +577,15 @@ class edit_renderer extends \plugin_renderer_base {
 
         // Action icons.
         $questionicons = '';
-        $questionicons .= $this->question_preview_icon($structure->get_quiz(), $question);
+        $questionicons .= $this->question_preview_icon($structure->get_quiz(), $structure->get_question_in_slot($slot));
         if ($structure->can_be_edited()) {
-            $questionicons .= $this->question_remove_icon($question, $pageurl);
+            $questionicons .= $this->question_remove_icon($structure, $slot, $pageurl);
         }
-        $questionicons .= $this->marked_out_of_field($structure->get_quiz(), $question);
+        $questionicons .= $this->marked_out_of_field($structure, $slot);
         $output .= html_writer::span($questionicons, 'actions'); // Required to add js spinner icon.
+        if ($structure->can_be_edited()) {
+            $output .= $this->question_dependency_icon($structure, $slot);
+        }
 
         // End of indentation div.
         $output .= html_writer::end_tag('div');
@@ -594,10 +597,11 @@ class edit_renderer extends \plugin_renderer_base {
     /**
      * Render the move icon.
      *
-     * @param \stdClass $question data from the question and quiz_slots tables.
-     * @return string The markup for the move action, or an empty string if not available.
+     * @param structure $structure object containing the structure of the quiz.
+     * @param int $slot the first slot on the page we are outputting.
+     * @return string The markup for the move action.
      */
-    public function question_move_icon($question) {
+    public function question_move_icon(structure $structure, $slot) {
         return html_writer::link(new \moodle_url('#'),
             $this->pix_icon('i/dragdrop', get_string('move'), 'moodle', array('class' => 'iconsmall', 'title' => '')),
             array('class' => 'editing_move', 'data-action' => 'move')
@@ -611,8 +615,7 @@ class edit_renderer extends \plugin_renderer_base {
      */
     public function question_number($number) {
         if (is_numeric($number)) {
-            $number = html_writer::span(get_string('question'), 'accesshide') .
-                    ' ' . $number;
+            $number = html_writer::span(get_string('question'), 'accesshide') . ' ' . $number;
         }
         return html_writer::tag('span', $number, array('class' => 'slotnumber'));
     }
@@ -648,12 +651,13 @@ class edit_renderer extends \plugin_renderer_base {
     /**
      * Render an icon to remove a question from the quiz.
      *
-     * @param object $question The module to produce a move button for.
+     * @param structure $structure object containing the structure of the quiz.
+     * @param int $slot the first slot on the page we are outputting.
      * @param \moodle_url $pageurl the canonical URL of the edit page.
      * @return string HTML to output.
      */
-    public function question_remove_icon($question, $pageurl) {
-        $url = new \moodle_url($pageurl, array('sesskey' => sesskey(), 'remove' => $question->slot));
+    public function question_remove_icon(structure $structure, $slot, $pageurl) {
+        $url = new \moodle_url($pageurl, array('sesskey' => sesskey(), 'remove' => $slot));
         $strdelete = get_string('delete');
 
         $image = $this->pix_icon('t/delete', $strdelete);
@@ -665,15 +669,14 @@ class edit_renderer extends \plugin_renderer_base {
     /**
      * Display an icon to split or join two pages of the quiz.
      *
-     * @param \stdClass $quiz the quiz settings from the database.
-     * @param \stdClass $question data from the question and quiz_slots tables.
-     * @param bool $insertpagebreak if true, show an insert page break icon.
-     *      else show a join pages icon.
+     * @param structure $structure object containing the structure of the quiz.
+     * @param int $slot the first slot on the page we are outputting.
      * @return string HTML to output.
      */
-    public function page_split_join_button($quiz, $question, $insertpagebreak) {
-        $url = new \moodle_url('repaginate.php', array('cmid' => $quiz->cmid, 'quizid' => $quiz->id,
-                    'slot' => $question->slot, 'repag' => $insertpagebreak ? 2 : 1, 'sesskey' => sesskey()));
+    public function page_split_join_button($structure, $slot) {
+        $insertpagebreak = !$structure->is_last_slot_on_page($slot);
+        $url = new \moodle_url('repaginate.php', array('quizid' => $structure->get_quizid(),
+                'slot' => $slot, 'repag' => $insertpagebreak ? 2 : 1, 'sesskey' => sesskey()));
 
         if ($insertpagebreak) {
             $title = get_string('addpagebreak', 'quiz');
@@ -687,14 +690,53 @@ class edit_renderer extends \plugin_renderer_base {
 
         // Disable the link if quiz has attempts.
         $disabled = null;
-        if (quiz_has_attempts($quiz->id)) {
-            $disabled = "disabled";
+        if (!$structure->can_be_edited()) {
+            $disabled = 'disabled';
         }
         return html_writer::span($this->action_link($url, $image, null, array('title' => $title,
                     'class' => 'page_split_join cm-edit-action', 'disabled' => $disabled, 'data-action' => $action)),
                 'page_split_join_wrapper');
     }
 
+    /**
+     * Display the icon for whether this question can only be seen if the previous
+     * one has been answered.
+     *
+     * @param structure $structure object containing the structure of the quiz.
+     * @param int $slot the first slot on the page we are outputting.
+     * @return string HTML to output.
+     */
+    public function question_dependency_icon($structure, $slot) {
+        $a = array(
+            'thisq' => $structure->get_displayed_number_for_slot($slot),
+            'previousq' => $structure->get_displayed_number_for_slot(max($slot - 1, 1)),
+        );
+        if ($structure->is_question_dependent_on_previous_slot($slot)) {
+            $title = get_string('questiondependencyremove', 'quiz', $a);
+            $image = $this->pix_icon('t/locked', get_string('questiondependsonprevious', 'quiz'),
+                    'moodle', array('title' => ''));
+            $action = 'removedependency';
+        } else {
+            $title = get_string('questiondependencyadd', 'quiz', $a);
+            $image = $this->pix_icon('t/unlocked', get_string('questiondependencyfree', 'quiz'),
+                    'moodle', array('title' => ''));
+            $action = 'adddependency';
+        }
+
+        // Disable the link if quiz has attempts.
+        $disabled = null;
+        if (!$structure->can_be_edited()) {
+            $disabled = 'disabled';
+        }
+        $extraclass = '';
+        if (!$structure->can_question_depend_on_previous_slot($slot)) {
+            $extraclass = ' question_dependency_cannot_depend';
+        }
+        return html_writer::span($this->action_link('#', $image, null, array('title' => $title,
+                'class' => 'cm-edit-action', 'disabled' => $disabled, 'data-action' => $action)),
+                'question_dependency_wrapper' . $extraclass);
+    }
+
     /**
      * Renders html to display a name with the link to the question on a quiz edit page
      *
@@ -702,13 +744,14 @@ class edit_renderer extends \plugin_renderer_base {
      * without a link
      *
      * @param structure $structure object containing the structure of the quiz.
-     * @param \stdClass $question data from the question and quiz_slots tables.
+     * @param int $slot which slot we are outputting.
      * @param \moodle_url $pageurl the canonical URL of this page.
      * @return string HTML to output.
      */
-    public function question_name(structure $structure, $question, $pageurl) {
+    public function question_name(structure $structure, $slot, $pageurl) {
         $output = '';
 
+        $question = $structure->get_question_in_slot($slot);
         $editurl = new \moodle_url('/question/question.php', array(
                 'returnurl' => $pageurl->out_as_local_url(),
                 'cmid' => $structure->get_cmid(), 'id' => $question->id));
@@ -739,12 +782,13 @@ class edit_renderer extends \plugin_renderer_base {
      * and also to see that category in the question bank.
      *
      * @param structure $structure object containing the structure of the quiz.
-     * @param \stdClass $question data from the question and quiz_slots tables.
+     * @param int $slot which slot we are outputting.
      * @param \moodle_url $pageurl the canonical URL of this page.
      * @return string HTML to output.
      */
-    public function random_question(structure $structure, $question, $pageurl) {
+    public function random_question(structure $structure, $slot, $pageurl) {
 
+        $question = $structure->get_question_in_slot($slot);
         $editurl = new \moodle_url('/question/question.php', array(
                 'returnurl' => $pageurl->out_as_local_url(),
                 'cmid' => $structure->get_cmid(), 'id' => $question->id));
@@ -777,14 +821,14 @@ class edit_renderer extends \plugin_renderer_base {
     /**
      * Display the 'marked out of' information for a question.
      * Along with the regrade action.
-     * @param \stdClass $quiz the quiz settings from the database.
-     * @param \stdClass $question data from the question and quiz_slots tables.
+     * @param structure $structure object containing the structure of the quiz.
+     * @param int $slot which slot we are outputting.
      * @return string HTML to output.
      */
-    public function marked_out_of_field($quiz, $question) {
-        if ($question->length == 0) {
+    public function marked_out_of_field(structure $structure, $slot) {
+        if (!$structure->is_real_question($slot)) {
             $output = html_writer::span('',
-                    'instancemaxmark decimalplaces_' . quiz_get_grade_format($quiz));
+                    'instancemaxmark decimalplaces_' . $structure->get_decimal_places_for_question_marks());
 
             $output .= html_writer::span(
                     $this->pix_icon('spacer', '', 'moodle', array('class' => 'editicon visibleifjs', 'title' => '')),
@@ -792,8 +836,8 @@ class edit_renderer extends \plugin_renderer_base {
             return html_writer::span($output, 'instancemaxmarkcontainer infoitem');
         }
 
-        $output = html_writer::span(quiz_format_question_grade($quiz, $question->maxmark),
-                'instancemaxmark decimalplaces_' . quiz_get_grade_format($quiz),
+        $output = html_writer::span($structure->formatted_question_grade($slot),
+                'instancemaxmark decimalplaces_' . $structure->get_decimal_places_for_question_marks(),
                 array('title' => get_string('maxmark', 'quiz')));
 
         $output .= html_writer::span(
@@ -858,30 +902,28 @@ class edit_renderer extends \plugin_renderer_base {
      * Initialise the JavaScript for the general editing. (JavaScript for popups
      * is handled with the specific code for those.)
      *
-     * @param \stdClass $course the course settings from the database.
-     * @param \stdClass $quiz the quiz settings from the database.
      * @param structure $structure object containing the structure of the quiz.
      * @param \question_edit_contexts $contexts the relevant question bank contexts.
      * @param array $pagevars the variables from {@link \question_edit_setup()}.
      * @param \moodle_url $pageurl the canonical URL of this page.
      * @return bool Always returns true
      */
-    protected function initialise_editing_javascript($course, $quiz, structure $structure,
+    protected function initialise_editing_javascript(structure $structure,
             \question_edit_contexts $contexts, array $pagevars, \moodle_url $pageurl) {
 
         $config = new \stdClass();
         $config->resourceurl = '/mod/quiz/edit_rest.php';
         $config->sectionurl = '/mod/quiz/edit_rest.php';
         $config->pageparams = array();
-        $config->questiondecimalpoints = $quiz->questiondecimalpoints;
+        $config->questiondecimalpoints = $structure->get_decimal_places_for_question_marks();
         $config->pagehtml = $this->new_page_template($structure, $contexts, $pagevars, $pageurl);
-        $config->addpageiconhtml = $this->add_page_icon_template($structure, $quiz);
+        $config->addpageiconhtml = $this->add_page_icon_template($structure);
 
         $this->page->requires->yui_module('moodle-mod_quiz-toolboxes',
                 'M.mod_quiz.init_resource_toolbox',
                 array(array(
-                        'courseid' => $course->id,
-                        'quizid' => $quiz->id,
+                        'courseid' => $structure->get_courseid(),
+                        'quizid' => $structure->get_quizid(),
                         'ajaxurl' => $config->resourceurl,
                         'config' => $config,
                 ))
@@ -892,9 +934,8 @@ class edit_renderer extends \plugin_renderer_base {
         $this->page->requires->yui_module('moodle-mod_quiz-toolboxes',
                 'M.mod_quiz.init_section_toolbox',
                 array(array(
-                        'courseid' => $course->id,
-                        'quizid' => $quiz->id,
-                        'format' => $course->format,
+                        'courseid' => $structure,
+                        'quizid' => $structure->get_quizid(),
                         'ajaxurl' => $config->sectionurl,
                         'config' => $config,
                 ))
@@ -902,16 +943,16 @@ class edit_renderer extends \plugin_renderer_base {
 
         $this->page->requires->yui_module('moodle-mod_quiz-dragdrop', 'M.mod_quiz.init_section_dragdrop',
                 array(array(
-                        'courseid' => $course->id,
-                        'quizid' => $quiz->id,
+                        'courseid' => $structure,
+                        'quizid' => $structure->get_quizid(),
                         'ajaxurl' => $config->sectionurl,
                         'config' => $config,
                 )), null, true);
 
         $this->page->requires->yui_module('moodle-mod_quiz-dragdrop', 'M.mod_quiz.init_resource_dragdrop',
                 array(array(
-                        'courseid' => $course->id,
-                        'quizid' => $quiz->id,
+                        'courseid' => $structure,
+                        'quizid' => $structure->get_quizid(),
                         'ajaxurl' => $config->resourceurl,
                         'config' => $config,
                 )), null, true);
@@ -945,6 +986,10 @@ class edit_renderer extends \plugin_renderer_base {
                 'dragtostart',
                 'numquestionsx',
                 'removepagebreak',
+                'questiondependencyadd',
+                'questiondependencyfree',
+                'questiondependencyremove',
+                'questiondependsonprevious',
         ), 'quiz');
 
         foreach (\question_bank::get_all_qtypes() as $qtype => $notused) {
@@ -969,11 +1014,10 @@ class edit_renderer extends \plugin_renderer_base {
             return '';
         }
 
-        $question = $structure->get_question_in_slot(1);
-        $pagehtml = $this->page_row($structure, $question, $contexts, $pagevars, $pageurl);
+        $pagehtml = $this->page_row($structure, 1, $contexts, $pagevars, $pageurl);
 
         // Normalise the page number.
-        $pagenumber = $question->page;
+        $pagenumber = $structure->get_page_number_for_slot(1);
         $strcontexts = array();
         $strcontexts[] = 'page-';
         $strcontexts[] = get_string('page') . ' ';
@@ -995,17 +1039,15 @@ class edit_renderer extends \plugin_renderer_base {
      * HTML for a page, with ids stripped, so it can be used as a javascript template.
      *
      * @param structure $structure object containing the structure of the quiz.
-     * @param \stdClass $quiz the quiz settings.
      * @return string HTML for a new icon
      */
-    protected function add_page_icon_template(structure $structure, $quiz) {
+    protected function add_page_icon_template(structure $structure) {
 
         if (!$structure->has_questions()) {
             return '';
         }
 
-        $question = $structure->get_question_in_slot(1);
-        $html = $this->page_split_join_button($quiz, $question, true);
+        $html = $this->page_split_join_button($structure, 1);
         return str_replace('&amp;slot=1&amp;', '&amp;slot=%%SLOT%%&amp;', $html);
     }
 
index d1f477b..72600f1 100644 (file)
@@ -118,6 +118,112 @@ class structure {
         return $this->questions[$this->slotsinorder[$slotnumber]->questionid];
     }
 
+    /**
+     * Get the displayed question number (or 'i') for a given slot.
+     * @param int $slotnumber the index of the slot in question.
+     * @return string the question number ot display for this slot.
+     */
+    public function get_displayed_number_for_slot($slotnumber) {
+        return $this->slotsinorder[$slotnumber]->displayednumber;
+    }
+
+    /**
+     * Get the page a given slot is on.
+     * @param int $slotnumber the index of the slot in question.
+     * @return int the page number of the page that slot is on.
+     */
+    public function get_page_number_for_slot($slotnumber) {
+        return $this->slotsinorder[$slotnumber]->page;
+    }
+
+    /**
+     * Get the slot id of a given slot slot.
+     * @param int $slotnumber the index of the slot in question.
+     * @return int the page number of the page that slot is on.
+     */
+    public function get_slot_id_for_slot($slotnumber) {
+        return $this->slotsinorder[$slotnumber]->id;
+    }
+
+    /**
+     * Get the question type in a given slot.
+     * @param int $slotnumber the index of the slot in question.
+     * @return string the question type (e.g. multichoice).
+     */
+    public function get_question_type_for_slot($slotnumber) {
+        return $this->questions[$this->slotsinorder[$slotnumber]->questionid]->qtype;
+    }
+
+    /**
+     * Whether it would be possible, given the question types, etc. for the
+     * question in the given slot to require that the previous question had been
+     * answered before this one is displayed.
+     * @param int $slotnumber the index of the slot in question.
+     * @return bool can this question require the previous one.
+     */
+    public function can_question_depend_on_previous_slot($slotnumber) {
+        return $slotnumber > 1 && $this->can_finish_during_the_attempt($slotnumber - 1);
+    }
+
+    /**
+     * Whether it is possible for another question to depend on this one finishing.
+     * Note that the answer is not exact, because of random questions, and sometimes
+     * questions cannot be depended upon because of quiz options.
+     * @param int $slotnumber the index of the slot in question.
+     * @return bool can this question finish naturally during the attempt?
+     */
+    public function can_finish_during_the_attempt($slotnumber) {
+        if ($this->quizobj->get_quiz()->shufflequestions ||
+                $this->quizobj->get_navigation_method() == QUIZ_NAVMETHOD_SEQ) {
+            return false;
+        }
+
+        if ($this->get_question_type_for_slot($slotnumber) == 'random') {
+            return true;
+        }
+
+        if (isset($this->slotsinorder[$slotnumber]->canfinish)) {
+            return $this->slotsinorder[$slotnumber]->canfinish;
+        }
+
+        $quba = \question_engine::make_questions_usage_by_activity('mod_quiz', $this->quizobj->get_context());
+        $tempslot = $quba->add_question(\question_bank::load_question(
+                $this->slotsinorder[$slotnumber]->questionid));
+        $quba->set_preferred_behaviour($this->quizobj->get_quiz()->preferredbehaviour);
+        $quba->start_all_questions();
+
+        $this->slotsinorder[$slotnumber]->canfinish = $quba->can_question_finish_during_attempt($tempslot);
+        return $this->slotsinorder[$slotnumber]->canfinish;
+    }
+
+    /**
+     * Whether it would be possible, given the question types, etc. for the
+     * question in the given slot to require that the previous question had been
+     * answered before this one is displayed.
+     * @param int $slotnumber the index of the slot in question.
+     * @return bool can this question require the previous one.
+     */
+    public function is_question_dependent_on_previous_slot($slotnumber) {
+        return $this->slotsinorder[$slotnumber]->requireprevious;
+    }
+
+    /**
+     * Is a particular question in this attempt a real question, or something like a description.
+     * @param int $slotnumber the index of the slot in question.
+     * @return bool whether that question is a real question.
+     */
+    public function is_real_question($slotnumber) {
+        return $this->get_question_in_slot($slotnumber)->length != 0;
+    }
+
+    /**
+     * Get the course id that the quiz belongs to.
+     * @return int the course.id for the quiz.
+     */
+    public function get_courseid() {
+        return $this->quizobj->get_courseid();
+    }
+
     /**
      * Get the course module id of the quiz.
      * @return int the course_modules.id for the quiz.
@@ -258,18 +364,18 @@ class structure {
     }
 
     /**
-     * Get all the questions in a section of the quiz.
+     * Get all the slots in a section of the quiz.
      * @param int $sectionid the section id.
-     * @return \stdClass[] of question/slot objects.
+     * @return int[] slot numbers.
      */
-    public function get_questions_in_section($sectionid) {
-        $questions = array();
+    public function get_slots_in_section($sectionid) {
+        $slots = array();
         foreach ($this->slotsinorder as $slot) {
             if ($slot->sectionid == $sectionid) {
-                $questions[] = $this->questions[$slot->questionid];
+                $slots[] = $slot->slot;
             }
         }
-        return $questions;
+        return $slots;
     }
 
     /**
@@ -280,6 +386,39 @@ class structure {
         return $this->sections;
     }
 
+    /**
+     * Get the overall quiz grade formatted for display.
+     * @return string the maximum grade for this quiz.
+     */
+    public function formatted_quiz_grade() {
+        return quiz_format_grade($this->get_quiz(), $this->get_quiz()->grade);
+    }
+
+    /**
+     * Get the maximum mark for a question, formatted for display.
+     * @param int $slotnumber the index of the slot in question.
+     * @return string the maximum mark for the question in this slot.
+     */
+    public function formatted_question_grade($slotnumber) {
+        return quiz_format_question_grade($this->get_quiz(), $this->slotsinorder[$slotnumber]->maxmark);
+    }
+
+    /**
+     * Get the number of decimal places for displyaing overall quiz grades or marks.
+     * @return int the number of decimal places.
+     */
+    public function get_decimal_places_for_grades() {
+        return $this->get_quiz()->decimalpoints;
+    }
+
+    /**
+     * Get the number of decimal places for displyaing question marks.
+     * @return int the number of decimal places.
+     */
+    public function get_decimal_places_for_question_marks() {
+        return quiz_get_grade_format($this->get_quiz());
+    }
+
     /**
      * Get any warnings to show at the top of the edit page.
      * @return string[] array of strings.
@@ -359,7 +498,7 @@ class structure {
 
         $slots = $DB->get_records_sql("
                 SELECT slot.id AS slotid, slot.slot, slot.questionid, slot.page, slot.maxmark,
-                       q.*, qc.contextid
+                        slot.requireprevious, q.*, qc.contextid
                   FROM {quiz_slots} slot
                   LEFT JOIN {question} q ON q.id = slot.questionid
                   LEFT JOIN {question_categories} qc ON qc.id = q.category
@@ -381,6 +520,7 @@ class structure {
             $slot->page = $slotdata->page;
             $slot->questionid = $slotdata->questionid;
             $slot->maxmark = $slotdata->maxmark;
+            $slot->requireprevious = $slotdata->requireprevious;
 
             $this->slots[$slot->id] = $slot;
             $this->slotsinorder[$slot->slot] = $slot;
@@ -414,6 +554,7 @@ class structure {
                 $slot->name = get_string('missingquestion', 'quiz');
                 $slot->slot = $slot->slot;
                 $slot->maxmark = 0;
+                $slot->requireprevious = 0;
                 $slot->questiontext = ' ';
                 $slot->questiontextformat = FORMAT_HTML;
                 $slot->length = 1;
@@ -451,11 +592,10 @@ class structure {
     protected function populate_question_numbers() {
         $number = 1;
         foreach ($this->slots as $slot) {
-            $question = $this->questions[$slot->questionid];
-            if ($question->length == 0) {
-                $question->displayednumber = get_string('infoshort', 'quiz');
+            if ($this->questions[$slot->questionid]->length == 0) {
+                $slot->displayednumber = get_string('infoshort', 'quiz');
             } else {
-                $question->displayednumber = $number;
+                $slot->displayednumber = $number;
                 $number += 1;
             }
         }
@@ -658,6 +798,16 @@ class structure {
         return true;
     }
 
+    /**
+     * Set whether the question in a particular slot requires the previous one.
+     * @param int $slotid id of slot.
+     * @param bool $requireprevious if true, set this question to require the previous one.
+     */
+    public function update_question_dependency($slotid, $requireprevious) {
+        global $DB;
+        $DB->set_field('quiz_slots', 'requireprevious', $requireprevious, array('id' => $slotid));
+    }
+
     /**
      * Add/Remove a pagebreak.
      *
index 85fe13d..cf2207f 100644 (file)
@@ -60,6 +60,7 @@
         <FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Where this question comes in order in the list of questions in this quiz. Like question_attempts.slot."/>
         <FIELD NAME="quizid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references quiz.id."/>
         <FIELD NAME="page" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The page number that this questions appears on. If the question in slot n appears on page p, then the question in slot n+1 must appear on page p or p+1. Well, except that when a quiz is being created, there may be empty pages, which would cause the page number to jump here."/>
+        <FIELD NAME="requireprevious" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Set to 1 when current question requires previous one to be answered first."/>
         <FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id."/>
         <FIELD NAME="maxmark" TYPE="number" LENGTH="12" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="7" COMMENT="How many marks this question contributes to quiz.sumgrades."/>
       </FIELDS>
index 5519b49..0318ea0 100644 (file)
@@ -807,6 +807,19 @@ function xmldb_quiz_upgrade($oldversion) {
     // Moodle v2.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2015022600) {
+        // Define field requireprevious to be added to quiz_slots.
+        $table = new xmldb_table('quiz_slots');
+        $field = new xmldb_field('requireprevious', XMLDB_TYPE_INTEGER, '4', null, XMLDB_NOTNULL, null, 0, 'page');
+
+        // Conditionally launch add field page.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2015022600, 'quiz');
+    }
+
     return true;
 }
-
index 7396ba7..8ffe131 100644 (file)
@@ -106,6 +106,7 @@ switch($requestmethod) {
                         echo json_encode(array('instancemaxmark' => quiz_format_question_grade($quiz, $maxmark),
                                 'newsummarks' => quiz_format_grade($quiz, $quiz->sumgrades)));
                         break;
+
                     case 'updatepagebreak':
                         require_capability('mod/quiz:manage', $modcontext);
                         $slots = $structure->update_page_break($quiz, $id, $value);
@@ -116,10 +117,15 @@ switch($requestmethod) {
                         }
                         echo json_encode(array('slots' => $json));
                         break;
-                }
-                break;
 
-            case 'course':
+                    case 'updatedependency':
+                        require_capability('mod/quiz:manage', $modcontext);
+                        $slot = $structure->get_slot_by_id($id);
+                        $value = (bool) $value;
+                        $structure->update_question_dependency($slot->id, $value);
+                        echo json_encode(array('requireprevious' => $value));
+                        break;
+                }
                 break;
         }
         break;
index 967d6ca..29dfe7e 100644 (file)
@@ -615,6 +615,10 @@ $string['questionbankmanagement'] = 'Question bank management';
 $string['questionbehaviour'] = 'Question behaviour';
 $string['questioncats'] = 'Question categories';
 $string['questiondeleted'] = 'This question has been deleted. Please contact your teacher';
+$string['questiondependencyadd'] = 'No restriction on when question {$a->thisq} can be attempted • Click to change';
+$string['questiondependencyfree'] = 'No restriction on this question';
+$string['questiondependencyremove'] = 'Question {$a->thisq} cannot be attempted until the previous question {$a->previousq} has been completed • Click to change';
+$string['questiondependsonprevious'] = 'This question cannot be attempted until the previous question has been completed.';
 $string['questioninuse'] = 'The question \'{$a->questionname}\' is currently being used in: <br />{$a->quiznames}<br />The question will not be deleted from these quizzes but only from the category list.';
 $string['questionmissing'] = 'Question for this session is missing';
 $string['questionname'] = 'Question name';
index 9b43014..58aef9a 100644 (file)
@@ -25,7 +25,6 @@
 require_once(__DIR__ . '/../../config.php');
 require_once($CFG->dirroot . '/mod/quiz/locallib.php');
 
-$cmid = required_param('cmid', PARAM_INT);
 $quizid = required_param('quizid', PARAM_INT);
 $slotnumber = required_param('slot', PARAM_INT);
 $repagtype = required_param('repag', PARAM_INT);
@@ -38,7 +37,7 @@ if (quiz_has_attempts($quizid)) {
     $reportlink = quiz_attempt_summary_link_to_reports($quizobj->get_quiz(),
                     $quizobj->get_cm(), $quizobj->get_context());
     throw new \moodle_exception('cannoteditafterattempts', 'quiz',
-            new moodle_url('/mod/quiz/edit.php', array('cmid' => $cmid)), $reportlink);
+            new moodle_url('/mod/quiz/edit.php', array('cmid' => $quizobj->get_cmid())), $reportlink);
 }
 
 $slotnumber++;
index 274cfb4..e4fdb22 100644 (file)
     text-align: right;
 }
 
+#page-mod-quiz-attempt .mod_quiz-blocked_question_warning .que .formulation,
+#page-mod-quiz-review .mod_quiz-blocked_question_warning .que .formulation {
+    background: #eee;
+    border: 1px solid #dcdcdc;
+}
+
 body.jsenabled .questionflagcheckbox {
     display: none;
 }
@@ -495,47 +501,32 @@ table.quizreviewsummary td.cell {
 }
 
 /** Mod quiz edit **/
-#page-mod-quiz-edit h2.main {
-    display: inline;
-    padding-right: 1em;
-    clear: left;
-}
-#page-mod-quiz-edit.dir-rtl h2.main {
-    padding-left: 1em;
-    padding-right: 0;
-}
-
 #page-mod-quiz-edit .statusbar {
     margin: 0.6em 0.4em;
 }
 #page-mod-quiz-edit .statusdisplay {
     background-color: #ffc;
     clear: both;
-    margin: 0.3em 1em 0.3em 0;
-    padding: 1px ;
-    /* Stop margin collapse. */
-}
-#page-mod-quiz-edit.dir-rtl .statusdisplay {
-    margin: 0.3em 0 0.3em 1em;
+    margin: 0.3em 0;
+    padding: 1px 10px;
 }
 #page-mod-quiz-edit .statusdisplay p {
-    margin: 0.4em;
+    margin: 4px 0;
 }
-
 #page-mod-quiz-edit .maxgrade,
 #page-mod-quiz-edit .totalpoints {
     display: block;
     float: right;
-    margin: -2.5em 1em 0em 1em;
+    margin: -2.5em 0 0;
     padding: .2em;
 }
-#page-mod-quiz-edit .maxgrade label {
-    display: inline;
-}
 #page-mod-quiz-edit.dir-rtl .maxgrade,
 #page-mod-quiz-edit.dir-rtl .totalpoints {
     float: left;
 }
+#page-mod-quiz-edit .maxgrade label {
+    display: inline;
+}
 
 #page-mod-quiz-edit li.activity > div,
 #page-mod-quiz-edit li.pagenumber {
@@ -545,6 +536,7 @@ table.quizreviewsummary td.cell {
 #page-mod-quiz-edit .last-add-menu {
     position: relative;
     height: 1.5em;
+    margin: 0 20px;
 }
 #page-mod-quiz-edit .add-menu-outer {
     position: absolute;
@@ -564,12 +556,16 @@ table.quizreviewsummary td.cell {
     display: inline-block;
 }
 
+#page-mod-quiz-edit ul.section {
+    margin: 0;
+    padding: 0 20px;
+}
 #page-mod-quiz-edit ul.slots li.section {
     border: 0;
 }
 #page-mod-quiz-edit ul.slots li.section .content {
     background-color:#FAFAFA;
-    padding:5px 10px;
+    padding: 0;
 }
 #page-mod-quiz-edit ul.slots li.section .content h3 {
     margin: 0;
@@ -587,7 +583,7 @@ table.quizreviewsummary td.cell {
 }
 #page-mod-quiz-edit ul.slots li.section {
     list-style: none;
-    margin: 0 0 5px 0;
+    margin: 0;
     padding: 0;
 }
 #page-mod-quiz-edit ul.slots li.section .left {
@@ -611,7 +607,7 @@ table.quizreviewsummary td.cell {
 }
 #page-mod-quiz-edit ul.slots li.section li.activity {
     background: #E6E6E6;
-    margin: 3px 0 3px 0;
+    margin: 3px 0;
     padding: 0.2em;
 }
 #page-mod-quiz-edit ul.slots li.section li.activity.page {
@@ -690,6 +686,31 @@ table.quizreviewsummary td.cell {
     margin: 0 2px;
 }
 
+#page-mod-quiz-edit ul.slots li.section li.activity .question_dependency_wrapper {
+    position: absolute;
+    top: 0;
+    right: 0;
+}
+#page-mod-quiz-edit.dir-rtl ul.slots li.section li.activity .question_dependency_wrapper {
+    left: 0;
+    right: auto;
+}
+#page-mod-quiz-edit ul.slots li.section li.activity .question_dependency_wrapper.question_dependency_cannot_depend {
+    display: none;
+}
+
+#page-mod-quiz-edit ul.slots li.section li.activity .question_dependency_wrapper .currentlink,
+#page-mod-quiz-edit ul.slots li.section li.activity .question_dependency_wrapper .cm-edit-action {
+    position: relative;
+    left: 20px;
+    top: -1em;
+}
+#page-mod-quiz-edit.dir-rtl ul.slots li.section li.activity .question_dependency_wrapper .currentlink,
+#page-mod-quiz-edit.dir-rtl ul.slots li.section li.activity .question_dependency_wrapper .cm-edit-action {
+    right: 20px;
+    left: auto;
+}
+
 #page-mod-quiz-edit ul.slots li.section li.activity .activityinstance {
     display: block;
     min-height: 1.7em;
diff --git a/mod/quiz/tests/behat/attempt_require_previous.feature b/mod/quiz/tests/behat/attempt_require_previous.feature
new file mode 100644 (file)
index 0000000..7903bfc
--- /dev/null
@@ -0,0 +1,218 @@
+@mod @mod_quiz
+Feature: Attemp a quiz where some questions require that the previous question has been answered.
+  In order to complete a quiz where questions require previous ones to be complete
+  As a student
+  I need later questions to appear once earlier ones have been answered.
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email              |
+      | student  | Student   | One      | student@moodle.com |
+      | teacher  | Teacher   | One      | teacher@moodle.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role    |
+      | student  | C1     | student |
+      | teacher  | C1     | teacher |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+
+  @javascript
+  Scenario: A question that requires the previous one is initally blocked
+    Given the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext    |
+      | Test questions   | truefalse   | TF1   | First question  |
+      | Test questions   | truefalse   | TF2   | Second question |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 0               |
+      | TF2      | 1    | 1               |
+
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+
+    Then I should see "First question"
+    And I should see "This question cannot be attempted until the previous question has been completed."
+    And I should not see "Second question"
+    And I log out
+    And I log in as "teacher"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Attempts: 1"
+    And I follow "Review attempt"
+    And I should see "First question"
+    And I should see "This question cannot be attempted until the previous question has been completed."
+    And I should not see "Second question"
+
+  @javascript
+  Scenario: A question requires the previous one becomes available when the first one is answered
+    Given the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext    |
+      | Test questions   | truefalse   | TF1   | First question  |
+      | Test questions   | truefalse   | TF2   | Second question |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 0               |
+      | TF2      | 1    | 1               |
+
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I click on "True" "radio" in the "First question" "question"
+    And I press "Check"
+
+    Then I should see "First question"
+    And I should not see "This question cannot be attempted until the previous question has been completed."
+    And I should see "Second question"
+
+  @javascript
+  Scenario: After quiz submitted, all questions show on the review page
+    Given the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext    |
+      | Test questions   | truefalse   | TF1   | First question  |
+      | Test questions   | truefalse   | TF2   | Second question |
+    And the following "activities" exist:
+      | activity | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz     | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 0               |
+      | TF2      | 1    | 1               |
+
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+    And I press "Next"
+    And I press "Submit all and finish"
+    And I click on "Submit all and finish" "button" in the "Confirmation" "dialogue"
+
+    Then the state of "First question" question is shown as "Not answered"
+    And the state of "Second question" question is shown as "Not answered"
+
+  @javascript
+  Scenario: A questions cannot be blocked in a deferred feedback quiz (despite what is set in the DB).
+    Given the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext    |
+      | Test questions   | truefalse   | TF1   | First question  |
+      | Test questions   | truefalse   | TF2   | Second question |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | deferredfeedback   |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 0               |
+      | TF2      | 1    | 1               |
+
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+
+    Then I should see "First question"
+    And I should see "Second question"
+    And I should not see "This question cannot be attempted until the previous question has been completed."
+
+  @javascript
+  Scenario: A questions cannot be blocked in a shuffled quiz (despite what is set in the DB).
+    Given the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext    |
+      | Test questions   | truefalse   | TF1   | First question  |
+      | Test questions   | truefalse   | TF2   | Second question |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour | shufflequestions | questionsperpage |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  | 1                | 2                |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 1               |
+      | TF2      | 1    | 1               |
+
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+
+    Then I should see "First question"
+    And I should see "Second question"
+    And I should not see "This question cannot be attempted until the previous question has been completed."
+
+  @javascript
+  Scenario: A questions cannot be blocked in sequential quiz (despite what is set in the DB).
+    Given the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext    |
+      | Test questions   | truefalse   | TF1   | First question  |
+      | Test questions   | truefalse   | TF2   | Second question |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour | navmethod  |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  | sequential |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 1               |
+      | TF2      | 1    | 1               |
+
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+
+    Then I should see "First question"
+    And I should see "Second question"
+    And I should not see "This question cannot be attempted until the previous question has been completed."
+
+  @javascript
+  Scenario: A questions not blocked if the previous one cannot finish, e.g. essay (despite what is set in the DB).
+    Given the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext    |
+      | Test questions   | essay       | Story | First question  |
+      | Test questions   | truefalse   | TF2   | Second question |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | Story    | 1    | 0               |
+      | TF2      | 1    | 1               |
+
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+
+    Then I should see "First question"
+    And I should see "Second question"
+    And I should not see "This question cannot be attempted until the previous question has been completed."
+
+  @javascript
+  Scenario: A questions not blocked if the previous one cannot finish, e.g. description (despite what is set in the DB).
+    Given the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext   |
+      | Test questions   | description | Info | Read me        |
+      | Test questions   | truefalse   | TF1  | First question |
+    And the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | Info     | 1    | 0               |
+      | TF1      | 1    | 1               |
+
+    When I log in as "student"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I press "Attempt quiz now"
+
+    Then I should see "Read me"
+    And I should see "First question"
+    And I should not see "This question cannot be attempted until the previous question has been completed."
index 1e073e2..306e707 100644 (file)
@@ -44,13 +44,14 @@ class behat_mod_quiz extends behat_question_base {
      * Put the specified questions on the specified pages of a given quiz.
      *
      * The first row should be column names:
-     * | question | page | maxmark |
+     * | question | page | maxmark | requireprevious |
      * The first two of those are required. The others are optional.
      *
      * question        needs to uniquely match a question name.
      * page            is a page number. Must start at 1, and on each following
      *                 row should be the same as the previous, or one more.
      * maxmark         What the question is marked out of. Defaults to question.defaultmark.
+     * requireprevious The question can only be attempted after the previous one was completed.
      *
      * Then there should be a number of rows of data, one for each question you want to add.
      *
@@ -130,6 +131,19 @@ class behat_mod_quiz extends behat_question_base {
 
             // Add the question.
             quiz_add_quiz_question($questionid, $quiz, $page, $maxmark);
+
+            // Require previous.
+            if (array_key_exists('requireprevious', $questiondata)) {
+                if ($questiondata['requireprevious'] === '1') {
+                    $slot = $DB->get_field('quiz_slots', 'MAX(slot)', array('quizid' => $quiz->id));
+                    $DB->set_field('quiz_slots', 'requireprevious', 1,
+                            array('quizid' => $quiz->id, 'slot' => $slot));
+                } else if ($questiondata['requireprevious'] !== '' && $questiondata['requireprevious'] !== '0') {
+                    throw new ExpectationException('Require previous for question "' .
+                            $questiondata['question'] . '" should be 0, 1 or blank.',
+                            $this->getSession());
+                }
+            }
         }
 
         quiz_update_sumgrades($quiz);
diff --git a/mod/quiz/tests/behat/editing_require_previous.feature b/mod/quiz/tests/behat/editing_require_previous.feature
new file mode 100644 (file)
index 0000000..fc0f2c9
--- /dev/null
@@ -0,0 +1,212 @@
+@mod @mod_quiz
+Feature: Edit quizzes where some questions require the previous one to have been completed
+  In order to create quizzes where later questions can only be seen after earlier ones are answered
+  As a teacher
+  I need to be able to configure this on the Edit quiz page
+
+  Background:
+    Given the following "users" exist:
+      | username | firstname | lastname | email               |
+      | teacher1 | T1        | Teacher1 | teacher1@moodle.com |
+    And the following "courses" exist:
+      | fullname | shortname | category |
+      | Course 1 | C1        | 0        |
+    And the following "course enrolments" exist:
+      | user     | course | role           |
+      | teacher1 | C1     | editingteacher |
+    And the following "question categories" exist:
+      | contextlevel | reference | name           |
+      | Course       | C1        | Test questions |
+    And I log in as "teacher1"
+
+  @javascript
+  Scenario: The first question cannot depend on the previous (whatever is in the DB)
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext    |
+      | Test questions   | truefalse   | TF1  | First question  |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 1               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    Then "be attempted" "link" should not be visible
+    # The text "be attempted" is used as a relatively unique string in both the add and remove links.
+
+  @javascript
+  Scenario: If the second question depends on the first, that is shown
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext    |
+      | Test questions   | truefalse   | TF1  | First question  |
+      | Test questions   | truefalse   | TF2  | Second question |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 0               |
+      | TF2      | 1    | 1               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    Then "This question cannot be attempted until the previous question has been completed." "link" should be visible
+
+  @javascript
+  Scenario: The second question can be set to depend on the first
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext    |
+      | Test questions   | truefalse   | TF1  | First question  |
+      | Test questions   | truefalse   | TF2  | Second question |
+      | Test questions   | truefalse   | TF3  | Third question  |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 0               |
+      | TF2      | 1    | 0               |
+      | TF3      | 1    | 0               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    When I follow "No restriction on when question 2 can be attempted • Click to change"
+    Then "Question 2 cannot be attempted until the previous question 1 has been completed • Click to change" "link" should be visible
+    And "No restriction on when question 3 can be attempted • Click to change" "link" should be visible
+
+  @javascript
+  Scenario: A question that did depend on the previous can be un-linked
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext    |
+      | Test questions   | truefalse   | TF1  | First question  |
+      | Test questions   | truefalse   | TF2  | Second question |
+      | Test questions   | truefalse   | TF3  | Third question  |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 0               |
+      | TF2      | 1    | 1               |
+      | TF3      | 1    | 1               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    When I follow "Question 3 cannot be attempted until the previous question 2 has been completed • Click to change"
+    Then "Question 2 cannot be attempted until the previous question 1 has been completed • Click to change" "link" should be visible
+    And "No restriction on when question 3 can be attempted • Click to change" "link" should be visible
+
+  @javascript
+  Scenario: Question dependency cannot apply to deferred feedback quizzes so UI is hidden
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | deferredfeedback   |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext    |
+      | Test questions   | truefalse   | TF1  | First question  |
+      | Test questions   | truefalse   | TF2  | Second question |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 0               |
+      | TF2      | 1    | 1               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    Then "be attempted" "link" in the "TF2" "list_item" should not be visible
+
+  @javascript
+  Scenario: Question dependency cannot apply to quizzes where the questions are shuffled so UI is hidden
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour | shufflequestions | questionsperpage |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  | 1                | 2                |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext    |
+      | Test questions   | truefalse   | TF1  | First question  |
+      | Test questions   | truefalse   | TF2  | Second question |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 1               |
+      | TF2      | 1    | 1               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    Then "be attempted" "link" in the "TF2" "list_item" should not be visible
+
+  @javascript
+  Scenario: Question dependency cannot apply to quizzes with sequential navigation so UI is hidden
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour | navmethod  |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  | sequential |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext    |
+      | Test questions   | truefalse   | TF1  | First question  |
+      | Test questions   | truefalse   | TF2  | Second question |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 1               |
+      | TF2      | 1    | 1               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    Then "be attempted" "link" in the "TF2" "list_item" should not be visible
+
+  @javascript
+  Scenario: A question can never depend on an essay
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name  | questiontext   |
+      | Test questions   | essay       | Story | First question |
+      | Test questions   | truefalse   | TF1   | First question |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | Story    | 1    | 0               |
+      | TF1      | 1    | 0               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    Then "be attempted" "link" in the "TF1" "list_item" should not be visible
+
+  @javascript
+  Scenario: A question can never depend on a description
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext   |
+      | Test questions   | description | Info | Read me        |
+      | Test questions   | truefalse   | TF1  | First question |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | Info     | 1    | 0               |
+      | TF1      | 1    | 0               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    Then "be attempted" "link" in the "TF1" "list_item" should not be visible
+
+  @javascript
+  Scenario: When questions are reordered, the dependency icons are updated correctly
+    Given the following "activities" exist:
+      | activity   | name   | intro              | course | idnumber | preferredbehaviour |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    | immediatefeedback  |
+    And the following "questions" exist:
+      | questioncategory | qtype       | name | questiontext    |
+      | Test questions   | truefalse   | TF1  | First question  |
+      | Test questions   | truefalse   | TF2  | Second question |
+      | Test questions   | truefalse   | TF3  | Third question  |
+    And quiz "Quiz 1" contains the following questions:
+      | question | page | requireprevious |
+      | TF1      | 1    | 0               |
+      | TF2      | 1    | 1               |
+      | TF3      | 1    | 1               |
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+    When I move "Question 1" to "After Question 3" in the quiz by clicking the move icon
+    Then "Question 2 cannot be attempted until the previous question 1 has been completed • Click to change" "link" should be visible
+    And "No restriction on when question 3 can be attempted • Click to change" "link" should be visible
+    And "be attempted" "link" in the "TF2" "list_item" should not be visible
index c9117c8..7ea030b 100644 (file)
@@ -50,7 +50,7 @@ class mod_quiz_structure_testcase extends advanced_testcase {
         $quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
 
         $quiz = $quizgenerator->create_instance(array('course' => $course->id, 'questionsperpage' => 0,
-            'grade' => 100.0, 'sumgrades' => 2));
+            'grade' => 100.0, 'sumgrades' => 2, 'preferredbehaviour' => 'immediatefeedback'));
 
         $cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id);
 
@@ -405,4 +405,36 @@ class mod_quiz_structure_testcase extends advanced_testcase {
             quiz_add_quiz_question($numq->id, $quiz, $pagenumber);
         }
     }
+
+    /**
+     * Test updating pagebreaks in the quiz.
+     */
+    public function test_update_question_dependency() {
+        // Create a test quiz with 8 questions.
+        list($quiz, $cm, $course) = $this->prepare_quiz_data();
+        $this->add_eight_questions_to_the_quiz($quiz);
+        $quizobj = new quiz($quiz, $cm, $course);
+        $structure = \mod_quiz\structure::create_for_quiz($quizobj);
+
+        // Store the original order of slots, so we can assert what has changed.
+        $originalslotids = array();
+        foreach ($structure->get_slots() as $slot) {
+            $originalslotids[$slot->slot] = $slot->id;
+        }
+
+        // Test adding a dependency.
+        $slotid = $structure->get_slot_id_for_slot(3);
+        $structure->update_question_dependency($slotid, true);
+
+        // Having called update page break, we need to reload $structure.
+        $structure = \mod_quiz\structure::create_for_quiz($quizobj);
+        $this->assertEquals(1, $structure->is_question_dependent_on_previous_slot(3));
+
+        // Test removing a dependency.
+        $structure->update_question_dependency($slotid, false);
+
+        // Having called update page break, we need to reload $structure.
+        $structure = \mod_quiz\structure::create_for_quiz($quizobj);
+        $this->assertEquals(0, $structure->is_question_dependent_on_previous_slot(3));
+    }
 }
index 682124c..5db9d21 100644 (file)
@@ -1,5 +1,17 @@
 This files describes API changes in the quiz code.
 
+=== 2.9 ===
+
+* There have been changes in classes/output/edit_renderer.php for MDL-40990.
+  + Some methods use to take $structure & $question as the first two arguments.
+    They now take $structure & $slot number. If you need $question, you can get
+    it using $question = $structure->get_question_in_slot($slot);
+  + Some methods used to take $quiz & $question. They now take $structure & $slot
+    number. You can get $question as above. $quiz is $structure->get_quiz().
+  + initialise_editing_javascript has had some redundant arguments removed.
+  Hopefully, with these changes, we will have less need to make other changes in future.
+
+
 === 2.8 ===
 
 * Classes that were defined in various lib files have been moved to the classes
index a2459a7..d9ddae5 100644 (file)
@@ -24,7 +24,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-$plugin->version   = 2014111000; // The current module version (Date: YYYYMMDDXX).
-$plugin->requires  = 2014110400; // Requires this Moodle version.
-$plugin->component = 'mod_quiz'; // Full name of the plugin (used for diagnostics).
+$plugin->version   = 2015030500;
+$plugin->requires  = 2014110400;
+$plugin->component = 'mod_quiz';
 $plugin->cron      = 60;
index 4bb67ed..df0a583 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js differ
index 98349eb..06dce00 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js differ
index 4bb67ed..df0a583 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js and b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js differ
index 68436fa..2a35d50 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js and b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js differ
index a090900..c5936ed 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js and b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js differ
index 68436fa..2a35d50 100644 (file)
Binary files a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js and b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js differ
index bca3e89..a455a4d 100644 (file)
@@ -60,8 +60,8 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
      */
     initializer: function() {
         M.mod_quiz.quizbase.register_module(this);
-        BODY.delegate('key', this.handle_data_action, 'down:enter', SELECTOR.ACTIVITYACTION, this);
         Y.delegate('click', this.handle_data_action, BODY, SELECTOR.ACTIVITYACTION, this);
+        Y.delegate('click', this.handle_data_action, BODY, SELECTOR.DEPENDENCY_LINK, this);
     },
 
     /**
@@ -106,6 +106,11 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
                 // The user is adding or removing a page break.
                 this.update_page_break(ev, node, activity, action);
                 break;
+            case 'adddependency':
+            case 'removedependency':
+                // The user is adding or removing a dependency between questions.
+                this.update_dependency(ev, node, activity, action);
+                break;
             default:
                 // Nothing to do here!
                 break;
@@ -174,8 +179,6 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
                     if (M.core.actionmenu && M.core.actionmenu.instance) {
                         M.core.actionmenu.instance.hideMenu();
                     }
-                } else {
-                    window.location.reload(true);
                 }
             });
 
@@ -198,8 +201,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
      */
     edit_maxmark : function(ev, button, activity) {
         // Get the element we're working on
-        var activityid = Y.Moodle.mod_quiz.util.slot.getId(activity),
-            instancemaxmark  = activity.one(SELECTOR.INSTANCEMAXMARK),
+        var instancemaxmark  = activity.one(SELECTOR.INSTANCEMAXMARK),
             instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
             currentmaxmark = instancemaxmark.get('firstChild'),
             oldmaxmark = currentmaxmark.get('data'),
@@ -209,7 +211,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
             data = {
                 'class'   : 'resource',
                 'field'   : 'getmaxmark',
-                'id'      : activityid
+                'id'      : Y.Moodle.mod_quiz.util.slot.getId(activity)
             };
 
         // Prevent the default actions.
@@ -359,6 +361,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
      * @param {EventFacade} ev The event that was fired.
      * @param {Node} button The button that triggered this action.
      * @param {Node} activity The activity node that this action will be performed on.
+     * @param {String} action The action, addpagebreak or removepagebreak.
      * @chainable
      */
     update_page_break: function(ev, button, activity, action) {
@@ -366,21 +369,16 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
         ev.preventDefault();
 
         var nextactivity = activity.next('li.activity.slot');
-        var spinner = this.add_spinner(nextactivity),
-            slotid = 0;
+        var spinner = this.add_spinner(nextactivity);
         var value = action === 'removepagebreak' ? 1 : 2;
 
         var data = {
             'class': 'resource',
             'field': 'updatepagebreak',
-            'id':    slotid,
+            'id':    Y.Moodle.mod_quiz.util.slot.getId(nextactivity),
             'value': value
         };
 
-        slotid = Y.Moodle.mod_quiz.util.slot.getId(nextactivity);
-        if (slotid) {
-            data.id = Number(slotid);
-        }
         this.send_request(data, spinner, function(response) {
             if (response.slots) {
                 if (action === 'addpagebreak') {
@@ -390,8 +388,39 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
                     Y.Moodle.mod_quiz.util.page.remove(page, true);
                 }
                 this.reorganise_edit_page();
-            } else {
-                window.location.reload(true);
+            }
+        });
+
+        return this;
+    },
+
+    /**
+     * Updates a slot to either require the question in the previous slot to
+     * have been answered, or not,
+     *
+     * @protected
+     * @method update_page_break
+     * @param {EventFacade} ev The event that was fired.
+     * @param {Node} button The button that triggered this action.
+     * @param {Node} activity The activity node that this action will be performed on.
+     * @param {String} action The action, adddependency or removedependency.
+     * @chainable
+     */
+    update_dependency: function(ev, button, activity, action) {
+        // Prevent the default button action.
+        ev.preventDefault();
+        var spinner = this.add_spinner(activity);
+
+        var data = {
+            'class': 'resource',
+            'field': 'updatedependency',
+            'id':    Y.Moodle.mod_quiz.util.slot.getId(activity),
+            'value': action === 'adddependency' ? 1 : 0
+        };
+
+        this.send_request(data, spinner, function(response) {
+            if (response.hasOwnProperty('requireprevious')) {
+                Y.Moodle.mod_quiz.util.slot.updateDependencyIcon(activity, response.requireprevious);
             }
         });
 
@@ -408,6 +437,7 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, {
         Y.Moodle.mod_quiz.util.slot.reorderSlots();
         Y.Moodle.mod_quiz.util.slot.reorderPageBreaks();
         Y.Moodle.mod_quiz.util.page.reorderPages();
+        Y.Moodle.mod_quiz.util.slot.updateAllDependencyIcons();
     },
 
     NAME : 'mod_quiz-resource-toolbox',
index 19051eb..0d0bd5c 100644 (file)
@@ -33,134 +33,6 @@ Y.extend(SECTIONTOOLBOX, TOOLBOX, {
      */
     initializer : function() {
         M.mod_quiz.quizbase.register_module(this);
-
-        // Section Highlighting.
-        Y.delegate('click', this.toggle_highlight, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.HIGHLIGHT, this);
-
-        // Section Visibility.
-        Y.delegate('click', this.toggle_hide_section, SELECTOR.PAGECONTENT, SELECTOR.SECTIONLI + ' ' + SELECTOR.SHOWHIDE, this);
-    },
-
-    toggle_hide_section : function(e) {
-        // Prevent the default button action.
-        e.preventDefault();
-
-        // Get the section we're working on.
-        var section = e.target.ancestor(M.mod_quiz.format.get_section_selector(Y)),
-            button = e.target.ancestor('a', true),
-            hideicon = button.one('img'),
-
-        // The value to submit
-            value,
-
-        // The text for strings and images. Also determines the icon to display.
-            action,
-            nextaction;
-
-        if (!section.hasClass(CSS.SECTIONHIDDENCLASS)) {
-            section.addClass(CSS.SECTIONHIDDENCLASS);
-            value = 0;
-            action = 'hide';
-            nextaction = 'show';
-        } else {
-            section.removeClass(CSS.SECTIONHIDDENCLASS);
-            value = 1;
-            action = 'show';
-            nextaction = 'hide';
-        }
-
-        var newstring = M.util.get_string(nextaction + 'fromothers', 'format_' + this.get('format'));
-        hideicon.setAttrs({
-            'alt' : newstring,
-            'src'   : M.util.image_url('i/' + nextaction)
-        });
-        button.set('title', newstring);
-
-        // Change the highlight status
-        var data = {
-            'class' : 'section',
-            'field' : 'visible',
-            'id'    : Y.Moodle.core_course.util.section.getId(section.ancestor(M.mod_quiz.edit.get_section_wrapper(Y), true)),
-            'value' : value
-        };
-
-        var lightbox = M.util.add_lightbox(Y, section);
-        lightbox.show();
-
-        this.send_request(data, lightbox, function(response) {
-            var activities = section.all(SELECTOR.ACTIVITYLI);
-            activities.each(function(node) {
-                var button;
-                if (node.one(SELECTOR.SHOW)) {
-                    button = node.one(SELECTOR.SHOW);
-                } else {
-                    button = node.one(SELECTOR.HIDE);
-                }
-                var activityid = Y.Moodle.mod_quiz.util.slot.getId(node);
-
-                // NOTE: resourcestotoggle is returned as a string instead
-                // of a Number so we must cast our activityid to a String.
-                if (Y.Array.indexOf(response.resourcestotoggle, "" + activityid) !== -1) {
-                    M.mod_quiz.resource_toolbox.handle_resource_dim(button, node, action);
-                }
-            }, this);
-        });
-    },
-
-    /**
-     * Toggle highlighting the current section.
-     *
-     * @method toggle_highlight
-     * @param {EventFacade} e
-     */
-    toggle_highlight : function(e) {
-        // Prevent the default button action.
-        e.preventDefault();
-
-        // Get the section we're working on.
-        var section = e.target.ancestor(M.mod_quiz.edit.get_section_selector(Y));
-        var button = e.target.ancestor('a', true);
-        var buttonicon = button.one('img');
-
-        // Determine whether the marker is currently set.
-        var togglestatus = section.hasClass('current');
-        var value = 0;
-
-        // Set the current highlighted item text.
-        var old_string = M.util.get_string('markthistopic', 'moodle');
-        Y.one(SELECTOR.PAGECONTENT)
-            .all(M.mod_quiz.edit.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT)
-            .set('title', old_string);
-        Y.one(SELECTOR.PAGECONTENT)
-            .all(M.mod_quiz.edit.get_section_selector(Y) + '.current ' + SELECTOR.HIGHLIGHT + ' img')
-            .set('alt', old_string)
-            .set('src', M.util.image_url('i/marker'));
-
-        // Remove the highlighting from all sections.
-        Y.one(SELECTOR.PAGECONTENT).all(M.mod_quiz.edit.get_section_selector(Y))
-            .removeClass('current');
-
-        // Then add it if required to the selected section.
-        if (!togglestatus) {
-            section.addClass('current');
-            value = Y.Moodle.core_course.util.section.getId(section.ancestor(M.mod_quiz.edit.get_section_wrapper(Y), true));
-            var new_string = M.util.get_string('markedthistopic', 'moodle');
-            button
-                .set('title', new_string);
-            buttonicon
-                .set('alt', new_string)
-                .set('src', M.util.image_url('i/marked'));
-        }
-
-        // Change the highlight status.
-        var data = {
-            'class' : 'course',
-            'field' : 'marker',
-            'value' : value
-        };
-        var lightbox = M.util.add_lightbox(Y, section);
-        lightbox.show();
-        this.send_request(data, lightbox);
     }
 },  {
     NAME : 'mod_quiz-section-toolbox',
@@ -170,9 +42,6 @@ Y.extend(SECTIONTOOLBOX, TOOLBOX, {
         },
         quizid : {
             'value' : 0
-        },
-        format : {
-            'value' : 'topics'
         }
     }
 });
index 7929788..be5eed1 100644 (file)
@@ -9,7 +9,7 @@
  */
 
 // The CSS classes we use.
-    var CSS = {
+var CSS = {
         ACTIVITYINSTANCE : 'activityinstance',
         AVAILABILITYINFODIV : 'div.availabilityinfo',
         CONTENTWITHOUTLINK : 'contentwithoutlink',
@@ -22,7 +22,6 @@
         JOIN: 'page_join',
         MODINDENTCOUNT : 'mod-indent-',
         MODINDENTHUGE : 'mod-indent-huge',
-        MODULEIDPREFIX : 'slot-',
         PAGE: 'page',
         SECTIONHIDDENCLASS : 'hidden',
         SECTIONIDPREFIX : 'section-',
@@ -36,7 +35,6 @@
         ACTIONLINKTEXT : '.actionlinktext',
         ACTIVITYACTION : 'a.cm-edit-action[data-action], a.editing_maxmark',
         ACTIVITYFORM : 'span.instancemaxmarkcontainer form',
-        ACTIVITYICON : 'img.activityicon',
         ACTIVITYINSTANCE : '.' + CSS.ACTIVITYINSTANCE,
         ACTIVITYLINK: '.' + CSS.ACTIVITYINSTANCE + ' > a',
         ACTIVITYLI : 'li.activity',
@@ -56,7 +54,6 @@
         PAGELI : 'li.page',
         SECTIONUL : 'ul.section',
         SHOW : 'a.' + CSS.SHOW,
-        SHOWHIDE : 'a.editing_showhide',
         SLOTLI : 'li.slot',
         SUMMARKS : '.mod_quiz_summarks'
     },
index c969f5d..70a5790 100644 (file)
@@ -16,7 +16,8 @@ Y.namespace('Moodle.mod_quiz.util.slot');
 Y.Moodle.mod_quiz.util.slot = {
     CSS: {
         SLOT : 'slot',
-        QUESTIONTYPEDESCRIPTION : 'qtype_description'
+        QUESTIONTYPEDESCRIPTION : 'qtype_description',
+        CANNOT_DEPEND: 'question_dependency_cannot_depend'
     },
     CONSTANTS: {
         SLOTIDPREFIX : 'slot-',
@@ -30,7 +31,10 @@ Y.Moodle.mod_quiz.util.slot = {
         PAGEBREAK : 'span.page_split_join_wrapper',
         ICON : 'img.smallicon',
         QUESTIONTYPEDESCRIPTION : '.qtype_description',
-        SECTIONUL : 'ul.section'
+        SECTIONUL : 'ul.section',
+        DEPENDENCY_WRAPPER : '.question_dependency_wrapper',
+        DEPENDENCY_LINK : '.question_dependency_wrapper .cm-edit-action',
+        DEPENDENCY_ICON : '.question_dependency_wrapper img'
     },
 
     /**
@@ -331,5 +335,64 @@ Y.Moodle.mod_quiz.util.slot = {
             // Update the anchor.
             pagebreaklink.set('href', newurl);
         }, this);
+    },
+
+    /**
+     * Update the dependency icons.
+     *
+     * @method updateAllDependencyIcons
+     * @return void
+     */
+    updateAllDependencyIcons: function() {
+        // Get list of slot nodes.
+        var slots = this.getSlots(),
+            slotnumber = 0,
+            previousslot = null;
+        // Loop through slots incrementing the number each time.
+        slots.each (function(slot) {
+            slotnumber++;
+
+            if (slotnumber == 1 || previousslot.getData('canfinish') === '0') {
+                slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).addClass(this.CSS.CANNOT_DEPEND);
+            } else {
+                slot.one(this.SELECTORS.DEPENDENCY_WRAPPER).removeClass(this.CSS.CANNOT_DEPEND);
+            }
+            this.updateDependencyIcon(slot, null);
+
+            previousslot = slot;
+        }, this);
+    },
+
+    /**
+     * Update the slot icon to indicate the new requiresprevious state.
+     *
+     * @method slot Slot node
+     * @method requiresprevious Whether this node now requires the previous one.
+     * @return void
+     */
+    updateDependencyIcon: function(slot, requiresprevious) {
+        var link = slot.one(this.SELECTORS.DEPENDENCY_LINK);
+        var icon = slot.one(this.SELECTORS.DEPENDENCY_ICON);
+        var previousSlot = this.getPrevious(slot);
+        var a = {thisq: this.getNumber(slot)};
+        if (previousSlot) {
+            a.previousq = this.getNumber(previousSlot);
+        }
+
+        if (requiresprevious === null) {
+            requiresprevious = link.getData('action') === 'removedependency';
+        }
+
+        if (requiresprevious) {
+            link.set('title', M.util.get_string('questiondependencyremove', 'quiz', a));
+            link.setData('action', 'removedependency');
+            icon.set('alt', M.util.get_string('questiondependsonprevious', 'quiz'));
+            icon.set('src', M.util.image_url('t/locked', 'moodle'));
+        } else {
+            link.set('title', M.util.get_string('questiondependencyadd', 'quiz', a));
+            link.setData('action', 'adddependency');
+            icon.set('alt', M.util.get_string('questiondependencyfree', 'quiz'));
+            icon.set('src', M.util.image_url('t/unlocked', 'moodle'));
+        }
     }
 };
index 3cd2511..e89ae69 100644 (file)
@@ -84,6 +84,16 @@ abstract class question_behaviour {
         return substr(get_class($this), 11);
     }
 
+    /**
+     * Whether the current attempt at this question could be completed just by the
+     * student interacting with the question, before $qa->finish() is called.
+     *
+     * @return boolean whether the attempt can finish naturally.
+     */
+    public function can_finish_during_attempt() {
+        return false;
+    }
+
     /**
      * Cause the question to be renderered. This gets the appropriate behaviour
      * renderer using {@link get_renderer()}, and adjusts the display
index f12db5d..37dd21c 100644 (file)
@@ -46,6 +46,10 @@ class qbehaviour_immediatefeedback extends question_behaviour_with_save {
         return $question instanceof question_automatically_gradable;
     }
 
+    public function can_finish_during_attempt() {
+        return true;
+    }
+
     public function get_min_fraction() {
         return $this->question->get_min_fraction();
     }
index 68b83d2..53ede5b 100644 (file)
@@ -54,6 +54,10 @@ class qbehaviour_interactive extends question_behaviour_with_multiple_tries {
         return $question instanceof question_automatically_gradable;
     }
 
+    public function can_finish_during_attempt() {
+        return true;
+    }
+
     public function get_right_answer_summary() {
         return $this->question->get_right_answer_summary();
     }
index 3cc770a..1b496d8 100644 (file)
@@ -1,14 +1,26 @@
 This files describes API changes for question behaviour plugins.
 
+=== 2.9 ===
+
+1) New method question_behaviour::can_finish_during_attempt. This returns false
+   by default. You should override it if, with your behaviour, questions may
+   finish just through the student interacting with them (e.g. by clicking the
+   Check button within the question.)
+
+
 === 2.7 ===
 
-1) question_behaviour_type has a new method allows_multiple_submitted_responses which defaults to false but should return
-      true if this question behaviour accepts multiple submissions of responses within one attempt eg. multiple tries for the
-      interactive or adaptive question behaviours.
-   question_behaviour has a new method step_has_a_submitted_response($step). For question behaviours where it is not only the
-      final response that is submitted by the student, you need to override this method to return true for other steps where a
-      student has submitted a response. See question_behaviour_with_multiple_tries::step_has_a_submitted_response($step) for
-      example. This method only needs to be overriden if you are returning true from allows_multiple_response_submissions
+1) question_behaviour_type has a new method allows_multiple_submitted_responses
+      which defaults to false but should return true if this question behaviour
+      accepts multiple submissions of responses within one attempt eg. multiple
+      tries for the interactive or adaptive question behaviours.
+   question_behaviour has a new method step_has_a_submitted_response($step). For
+      question behaviours where it is not only the final response that is
+      submitted by the student, you need to override this method to return true
+      for other steps where a student has submitted a response. See
+      question_behaviour_with_multiple_tries::step_has_a_submitted_response($step)
+      for example. This method only needs to be overriden if you are returning
+      true from allows_multiple_response_submissions.
 
 
 === 2.6 ===
index 8a59289..8a43d90 100644 (file)
@@ -1163,6 +1163,16 @@ class question_attempt {
         return $this->rightanswer;
     }
 
+    /**
+     * Whether this attempt at this question could be completed just by the
+     * student interacting with the question, before {@link finish()} is called.
+     *
+     * @return boolean whether this attempt can finish naturally.
+     */
+    public function can_finish_during_attempt() {
+        return $this->behaviour->can_finish_during_attempt();
+    }
+
     /**
      * Perform the action described by $submitteddata.
      * @param array $submitteddata the submitted data the determines the action.
index ddadbfb..c4ce6e9 100644 (file)
@@ -271,6 +271,17 @@ class question_usage_by_activity {
         return $this->get_question_attempt($slot)->get_state_class($showcorrectness);
     }
 
+    /**
+     * Whether this attempt at a given question could be completed just by the
+     * student interacting with the question, before {@link finish_question()} is called.
+     *
+     * @param int $slot the number used to identify this question within this usage.
+     * @return boolean whether the attempt at the given question can finish naturally.
+     */
+    public function can_question_finish_during_attempt($slot) {
+        return $this->get_question_attempt($slot)->can_finish_during_attempt();
+    }
+
     /**
      * Get the time of the most recent action performed on a question.
      * @param int $slot the number used to identify this question within this usage.