MDL-40990 quiz: option to require prev Q finished before next shown
authorM Kassaei <m.kassaei@open.ac.uk>
Thu, 9 Oct 2014 08:21:52 +0000 (09:21 +0100)
committerTim Hunt <T.J.Hunt@open.ac.uk>
Tue, 17 Mar 2015 17:10:13 +0000 (17:10 +0000)
mod/quiz/attemptlib.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/lang/en/quiz.php
mod/quiz/questiondependency.php [new file with mode: 0644]
mod/quiz/renderer.php
mod/quiz/styles.css
mod/quiz/tests/behat/editing_questiondependency.feature [new file with mode: 0644]
question/engine/questionusage.php

index 3c2fa1a..5f6dfc1 100644 (file)
@@ -1168,6 +1168,36 @@ class quiz_attempt {
         return new moodle_url('/mod/quiz/processattempt.php');
     }
 
+    /**
+     * Return slot object for the given slotnumber in a given quizid
+     *
+     * @param int $quizid
+     * @param int $slotnumber
+     */
+    public function get_slot_object($quizid, $slotnumber) {
+        global $DB;
+        return $DB->get_record('quiz_slots', array('slot' => $slotnumber, 'quizid' => $quizid));
+    }
+
+    /**
+     * Checks whether it requires previous question. If the previous question is not completed
+     * return a message in descripyiom question type format, otherwise returns null
+     *
+     * @param int $slot
+     */
+    public function require_previous_question($slot) {
+        $quiz = $this->get_quiz();
+        $currentslot = $this->get_slot_object($quiz->id, $slot);
+        $previousslot = $this->get_slot_object($quiz->id, $currentslot->slot - 1);
+
+        if ($currentslot->requireprevious && $previousslot) {
+            if ($this->get_question_status($previousslot->slot, false) == 'Not yet answered') {
+                return $this->quba->replace_question_with_a_description_qtye($currentslot);
+            }
+        }
+        return null;
+    }
+
     /**
      * @param int $slot indicates which question to link to.
      * @param int $page if specified, the URL of this particular page of the attempt, otherwise
index 86eafdd..12fb25d 100644 (file)
@@ -342,10 +342,11 @@ class edit_renderer extends \plugin_renderer_base {
             $contexts, $pagevars, $pageurl) {
 
         $output = '';
+        $previousquestion = null;
         foreach ($structure->get_questions_in_section($section->id) as $question) {
-            $output .= $this->question_row($structure, $question, $contexts, $pagevars, $pageurl);
+            $output .= $this->question_row($structure, $question, $previousquestion, $contexts, $pagevars, $pageurl);
+            $previousquestion = $question;
         }
-
         return html_writer::tag('ul', $output, array('class' => 'section img-text'));
     }
 
@@ -354,12 +355,13 @@ class edit_renderer extends \plugin_renderer_base {
      *
      * @param structure $structure object containing the structure of the quiz.
      * @param \stdClass $question data from the question and quiz_slots tables.
+     * @param \stdClass $previousquestion data from the question and quiz_slots tables.
      * @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, $question, $previousquestion, $contexts, $pagevars, $pageurl) {
         $output = '';
 
         $output .= $this->page_row($structure, $question, $contexts, $pagevars, $pageurl);
@@ -372,7 +374,7 @@ class edit_renderer extends \plugin_renderer_base {
         }
 
         // Question HTML.
-        $questionhtml = $this->question($structure, $question, $pageurl);
+        $questionhtml = $this->question($structure, $question, $previousquestion, $pageurl);
         $questionclasses = 'activity ' . $question->qtype . ' qtype_' . $question->qtype . ' slot';
 
         $output .= html_writer::tag('li', $questionhtml . $joinhtml,
@@ -543,12 +545,12 @@ class edit_renderer extends \plugin_renderer_base {
      *
      * @param structure $structure object containing the structure of the quiz.
      * @param \stdClass $question data from the question and quiz_slots tables.
+     * @param \stdClass $previousquestion data from the question and quiz_slots tables.
      * @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, $question, $previousquestion, \moodle_url $pageurl) {
         $output = '';
-
         $output .= html_writer::start_tag('div');
 
         if ($structure->can_be_edited()) {
@@ -580,6 +582,7 @@ class edit_renderer extends \plugin_renderer_base {
         $questionicons .= $this->question_preview_icon($structure->get_quiz(), $question);
         if ($structure->can_be_edited()) {
             $questionicons .= $this->question_remove_icon($question, $pageurl);
+            $questionicons .= $this->question_dependency_icon($structure->get_quiz(), $question, $previousquestion);
         }
         $questionicons .= $this->marked_out_of_field($structure->get_quiz(), $question);
         $output .= html_writer::span($questionicons, 'actions'); // Required to add js spinner icon.
@@ -611,8 +614,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'));
     }
@@ -695,6 +697,58 @@ class edit_renderer extends \plugin_renderer_base {
                 'page_split_join_wrapper');
     }
 
+    /**
+     * Display an icon Add/Remove dependency
+     *
+     * @param object $quiz
+     * @param object $question
+     * @param object $previousquestion
+     * @param bool $dependencysetting
+     */
+    public function question_dependency_icon($quiz, $question, $previousquestion, $dependencysetting = true) {
+        if (!$dependencysetting) {
+            return null;
+        }
+        // Check whether the current question qualifies for dependency.
+        // What about random questions?
+        $unqualifiedqtypes = array('description', 'essay');
+
+        // Current question is not the first question in the quiz.
+        if ($question->slot == 1) {
+            return ' ';
+        }
+        // Current question is not a description or an essay question type.
+        if (in_array($question->qtype, $unqualifiedqtypes)) {
+            return ' ';
+        }
+        // Previous question is not a description or an essay question type.
+        if (in_array($previousquestion->qtype, $unqualifiedqtypes)) {
+            return ' ';
+        }
+        // Process qualified questions.
+        $url = new \moodle_url('questiondependency.php', array('cmid' => $quiz->cmid, 'quizid' => $quiz->id,
+                'slotid' => $question->slotid, 'sesskey' => sesskey()));
+
+        if ($question->requireprevious) {
+            $title = get_string('removequestiondependency', 'quiz');
+            $image = $this->pix_icon('e/remove_page_break', $title);
+            $action = 'linkpage';
+        } else {
+            $title = get_string('addquestiondependency', 'quiz');
+            $image = $this->pix_icon('e/insert_page_break', $title);
+            $action = 'unlinkpage';
+        }
+
+        // Disable the link if quiz has attempts.
+        $disabled = null;
+        if (quiz_has_attempts($quiz->id)) {
+            $disabled = "disabled";
+        }
+        return html_writer::span($this->action_link($url, $image, null, array('title' => $title,
+                'class' => 'question_dependency_icon', 'disabled' => $disabled, 'data-action' => $action)),
+                'page_split_join_wrapper');
+    }
+
     /**
      * Renders html to display a name with the link to the question on a quiz edit page
      *
index d1f477b..fa871fb 100644 (file)
@@ -359,7 +359,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 +381,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 +415,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;
@@ -658,6 +660,24 @@ class structure {
         return true;
     }
 
+    /**
+     * Change require previous for a slot..
+     * @param \stdClass $slot row from the quiz_slots table.
+     */
+    public function update_question_dependency($slot) {
+        global $DB;
+        $trans = $DB->start_delegated_transaction();
+
+        // Swap dependency setting.
+        if ($slot->requireprevious == 1) {
+            $slot->requireprevious = 0;
+        } else {
+            $slot->requireprevious = 1;
+        }
+        $DB->update_record('quiz_slots', $slot);
+        $trans->allow_commit();
+    }
+
     /**
      * 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..37a2714 100644 (file)
@@ -402,10 +402,6 @@ function xmldb_quiz_upgrade($oldversion) {
         upgrade_mod_savepoint(true, 2013031900, 'quiz');
     }
 
-    // Moodle v2.5.0 release upgrade line.
-    // Put any upgrade step following this.
-
-
     // Moodle v2.6.0 release upgrade line.
     // Put any upgrade step following this.
 
@@ -807,6 +803,19 @@ function xmldb_quiz_upgrade($oldversion) {
     // Moodle v2.8.0 release upgrade line.
     // Put any upgrade step following this.
 
+    if ($oldversion < 2014111000) {
+        // 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, null, null, null, 'maxmark');
+
+        // Conditionally launch add field page.
+        if (!$dbman->field_exists($table, $field)) {
+            $dbman->add_field($table, $field);
+        }
+
+        // Quiz savepoint reached.
+        upgrade_mod_savepoint(true, 2014111000, 'quiz');
+    }
     return true;
 }
 
index 967d6ca..e4a2b87 100644 (file)
@@ -45,6 +45,7 @@ $string['addpagebreak'] = 'Add page break';
 $string['addpagehere'] = 'Add page here';
 $string['addquestion'] = 'Add question';
 $string['addquestionfrombanktopage'] = 'Add from the question bank to page {$a}';
+$string['addquestiondependency'] = 'Add question dependency';
 $string['addquestions'] = 'Add questions';
 $string['addquestionstoquiz'] = 'Add questions to current quiz';
 $string['addrandom'] = 'Add {$a} random questions';
@@ -615,6 +616,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['questiondependency'] = 'Question dependency';
+$string['configquestiondependency'] = 'Question dependency can be used for displaying question n+1 only if students have completed question n, because question n +1 may exposed the answer for question n.';
+$string['questiondependency_help'] = 'Question dependency can be used for displaying question n+1 only if students have completed question n, because question n +1 may exposed the answer for question n.';
+$string['questiondependsonprevious'] = 'You have to complete the previous question first, then you would be able to see the content of this question.';
 $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';
@@ -695,6 +700,7 @@ $string['removeallquizattempts'] = 'Delete all quiz attempts';
 $string['removeemptypage'] = 'Remove empty page';
 $string['removepagebreak'] = 'Remove page break';
 $string['removeselected'] = 'Remove selected';
+$string['removequestiondependency'] = 'Remove question dependency';
 $string['rename'] = 'Rename';
 $string['renderingserverconnectfailed'] = 'The server {$a} failed to process an RQP request. Check that the URL is correct.';
 $string['reorderquestions'] = 'Reorder questions';
diff --git a/mod/quiz/questiondependency.php b/mod/quiz/questiondependency.php
new file mode 100644 (file)
index 0000000..7baf852
--- /dev/null
@@ -0,0 +1,43 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Set question dependency.
+ *
+ * @package   mod_quiz
+ * @copyright 2014 The Open University
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+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);
+$slotid = required_param('slotid', PARAM_INT);
+
+require_sesskey();
+$quizobj = quiz::create($quizid);
+require_login($quizobj->get_course(), false, $quizobj->get_cm());
+require_capability('mod/quiz:manage', $quizobj->get_context());
+
+$structure = $quizobj->get_structure();
+
+// Update dependency settings on this slot.
+$slot = $structure->get_slot_by_id($slotid);
+$structure->update_question_dependency($slot);
+
+redirect(new moodle_url('edit.php', array('cmid' => $quizobj->get_cmid())));
index 9b74671..ff4db1a 100644 (file)
@@ -456,11 +456,14 @@ class mod_quiz_renderer extends plugin_renderer_base {
                 'enctype' => 'multipart/form-data', 'accept-charset' => 'utf-8',
                 'id' => 'responseform'));
         $output .= html_writer::start_tag('div');
-
-        // Print all the questions.
         foreach ($slots as $slot) {
-            $output .= $attemptobj->render_question($slot, false,
-                    $attemptobj->attempt_url($slot, $page));
+            $requireprevious = $attemptobj->require_previous_question($slot);
+            if ($requireprevious) {
+                $output .= $requireprevious;
+            } else {
+                $output .= $attemptobj->render_question($slot, false,
+                        $attemptobj->attempt_url($slot, $page));
+            }
         }
 
         $output .= html_writer::start_tag('div', array('class' => 'submitbtns'));
index 274cfb4..b42c024 100644 (file)
@@ -690,6 +690,13 @@ table.quizreviewsummary td.cell {
     margin: 0 2px;
 }
 
+#page-mod-quiz-edit ul.slots li.section li.activity .question_dependency_icon {
+    -position: relative;
+    vertical-align: top;
+    margin-left: 5px;
+    margin-right: 5px;
+}
+
 #page-mod-quiz-edit ul.slots li.section li.activity .activityinstance {
     display: block;
     min-height: 1.7em;
diff --git a/mod/quiz/tests/behat/editing_questiondependency.feature b/mod/quiz/tests/behat/editing_questiondependency.feature
new file mode 100644 (file)
index 0000000..89c90ea
--- /dev/null
@@ -0,0 +1,83 @@
+
+@mod @mod_quiz @questiondependency
+Feature: Edit quiz page - pagination
+  In order to build a quiz laid out in pages with n question(s) on each page, where n >=1.
+  I need to be able to add and remove question dependency on any qualified question
+  in quiz editing 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 "activities" exist:
+      | activity   | name   | intro              | course | idnumber |
+      | quiz       | Quiz 1 | Quiz 1 description | C1     | quiz1    |
+
+    When I log in as "teacher1"
+    And I follow "Course 1"
+    And I follow "Quiz 1"
+    And I follow "Edit quiz"
+
+  @javascript
+  Scenario: There is no dependency setting on question in the quiz.
+  I can add or remove dependency to a given question by clicking on  "Add question dependency"
+  or "Remove question dependency" icons. Then I can attempt the quiz and see the effect of the
+  dependency settings.
+
+    Then I should see "Editing quiz: Quiz 1"
+
+    # Add the first true false question.
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+      | Question name                      | TF 001                          |
+      | Question text                      | Answer the TF 001 question              |
+      | General feedback                   | Thank you, this is the general feedback |
+      | Correct answer                     | False                                   |
+      | Feedback for the response 'True'.  | So you think it is true                 |
+      | Feedback for the response 'False'. | So you think it is false                |
+    And I should see "TF 001"
+
+    # Add the second true false question.
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+      | Question name                      | TF 002                          |
+      | Question text                      | Answer the TF 002 question              |
+      | General feedback                   | Thank you, this is the general feedback |
+      | Correct answer                     | False                                   |
+      | Feedback for the response 'True'.  | So you think it is true                 |
+      | Feedback for the response 'False'. | So you think it is false                |
+    And I should see "TF 001"
+    #And I should not see "Add question dependency" "link" in the "TF 001" "table_row"
+    And I should see "TF 002"
+    And I follow "Add question dependency"
+
+    # Add the third true false question.
+    And I add a "True/False" question to the "Quiz 1" quiz with:
+      | Question name                      | TF 003                          |
+      | Question text                      | Answer the TF 003 question              |
+      | General feedback                   | Thank you, this is the general feedback |
+      | Correct answer                     | False                                   |
+      | Feedback for the response 'True'.  | So you think it is true                 |
+      | Feedback for the response 'False'. | So you think it is false                |
+    And I should see "TF 001"
+    And I should see "TF 002"
+    And I should see "TF 003"
+
+    # Attempt the quiz
+    And I follow "Quiz 1"
+    When I press "Preview quiz now"
+    And I should see "You have to complete the previous question first, then you would be able to see the content of this question."
+
+    # Back to the quiz editing page
+    And I follow "Quiz 1"
+    When I follow "Edit quiz"
+    Then I should see "Editing quiz: Quiz 1"
+    And I follow "Remove question dependency"
+    And I follow "Quiz 1"
+    When I press "Continue the last preview"
+    And I press "Start a new preview"
+    And I should not see "You have to complete the previous question first, then you would be able to see the content of this question."
\ No newline at end of file
index c4ce6e9..005a690 100644 (file)
@@ -821,6 +821,35 @@ class question_usage_by_activity {
         $this->observer->notify_attempt_modified($newqa);
     }
 
+    /**
+     * Replace a question with a dummy description question in this usage.
+     *
+     * @param object $slot
+     */
+    public function replace_question_with_a_description_qtye($slot) {
+        global $OUTPUT;
+        // Create a description qtye for the message.
+        question_bank::load_question_definition_classes('description');
+        $q = new qtype_description_question();
+        $q->id = $slot->questionid;
+        $q->name = 'Description';
+        $q->questiontext = get_string('questiondependsonprevious', 'quiz');
+        $q->generalfeedback = '';
+        $q->qtype = question_bank::get_qtype('description');
+        $q->options = new question_display_options();
+        $q->options->flags = 0;
+
+        $oldqa = $this->get_question_attempt($slot->slot);
+        $newqa = new question_attempt($q, $oldqa->get_usage_id(), $this->observer, $slot->maxmark);
+        $newqa->get_question()->options->flags = 1;
+
+        $newqa->set_database_id($oldqa->get_database_id());
+        $newqa->set_slot($slot->slot);
+        $this->questionattempts[$slot->slot] = $newqa;
+        $this->start_question($slot->slot);
+        $this->render_question($slot->slot, $q->options);
+    }
+
     /**
      * Regrade all the questions in this usage (without changing their max mark).
      * @param bool $finished whether each question should be forced to be finished