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
$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'));
}
*
* @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);
}
// 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,
*
* @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()) {
$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.
*/
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'));
}
'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
*
$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
$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;
$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;
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.
*
<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>
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.
// 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;
}
$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';
$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';
$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';
--- /dev/null
+<?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())));
'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'));
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;
--- /dev/null
+
+@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
$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