diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php
index 3c2fa1a7aac..5f6dfc1f8a8 100644
--- a/mod/quiz/attemptlib.php
+++ b/mod/quiz/attemptlib.php
@@ -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
diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php
index 86eafdd96ba..12fb25d4184 100644
--- a/mod/quiz/classes/output/edit_renderer.php
+++ b/mod/quiz/classes/output/edit_renderer.php
@@ -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
*
diff --git a/mod/quiz/classes/structure.php b/mod/quiz/classes/structure.php
index d1f477b51be..fa871fb0e8b 100644
--- a/mod/quiz/classes/structure.php
+++ b/mod/quiz/classes/structure.php
@@ -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.
*
diff --git a/mod/quiz/db/install.xml b/mod/quiz/db/install.xml
index 85fe13d328d..cf2207fed9e 100644
--- a/mod/quiz/db/install.xml
+++ b/mod/quiz/db/install.xml
@@ -60,6 +60,7 @@
+
diff --git a/mod/quiz/db/upgrade.php b/mod/quiz/db/upgrade.php
index 5519b49a525..37a271433a6 100644
--- a/mod/quiz/db/upgrade.php
+++ b/mod/quiz/db/upgrade.php
@@ -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;
}
diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php
index 967d6ca9f69..e4a2b870478 100644
--- a/mod/quiz/lang/en/quiz.php
+++ b/mod/quiz/lang/en/quiz.php
@@ -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:
{$a->quiznames}
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
index 00000000000..7baf852e2a4
--- /dev/null
+++ b/mod/quiz/questiondependency.php
@@ -0,0 +1,43 @@
+.
+
+/**
+ * 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())));
diff --git a/mod/quiz/renderer.php b/mod/quiz/renderer.php
index 9b746718655..ff4db1a82b9 100644
--- a/mod/quiz/renderer.php
+++ b/mod/quiz/renderer.php
@@ -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'));
diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css
index 274cfb49736..b42c024d887 100644
--- a/mod/quiz/styles.css
+++ b/mod/quiz/styles.css
@@ -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
index 00000000000..89c90ea8018
--- /dev/null
+++ b/mod/quiz/tests/behat/editing_questiondependency.feature
@@ -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
diff --git a/question/engine/questionusage.php b/question/engine/questionusage.php
index c4ce6e90489..005a69079fe 100644
--- a/question/engine/questionusage.php
+++ b/question/engine/questionusage.php
@@ -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