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