From 441d284ada20943b556aaa8d73a6cc3909f04f91 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 26 Feb 2015 11:53:00 +0000 Subject: [PATCH] MDL-40990 quiz: option to require prev Q finished before next shown Further improvements to this code, including resolving edge cases: * The new feature can only be used when it is possible for the previous question in the quiz to be complete. * Also, this new feature cannot be used in combination with shuffle questions, because that make no sense; nor in combination with sequential navigation, because to make that work properly would be a lot of effort. If someone needs that to work later, it should be possible for them to implement it. * There were changes in the edit renderer API, to try to make things more consistent, and to make it less likely we will need to change things again in the future. See mod/quiz/upgrade.txt. * As part of this change, the styling of the Edit quiz page was tweaked to make slighly more efficient use of the horizontal space, and to be more symmetrical. --- mod/quiz/attemptlib.php | 110 +++++--- .../backup/moodle2/backup_quiz_stepslib.php | 2 +- mod/quiz/classes/output/edit_renderer.php | 244 +++++++++--------- mod/quiz/classes/structure.php | 176 +++++++++++-- mod/quiz/db/upgrade.php | 12 +- mod/quiz/edit_rest.php | 12 +- mod/quiz/lang/en/quiz.php | 10 +- mod/quiz/questiondependency.php | 43 --- mod/quiz/renderer.php | 11 +- mod/quiz/repaginate.php | 3 +- mod/quiz/styles.css | 74 +++--- .../behat/attempt_require_previous.feature | 218 ++++++++++++++++ mod/quiz/tests/behat/behat_mod_quiz.php | 16 +- .../behat/editing_questiondependency.feature | 83 ------ .../behat/editing_require_previous.feature | 212 +++++++++++++++ mod/quiz/tests/structure_test.php | 34 ++- mod/quiz/upgrade.txt | 12 + mod/quiz/version.php | 6 +- .../moodle-mod_quiz-toolboxes-debug.js | 196 ++++---------- .../moodle-mod_quiz-toolboxes-min.js | 4 +- .../moodle-mod_quiz-toolboxes.js | 196 ++++---------- .../moodle-mod_quiz-util-slot-debug.js | 67 ++++- .../moodle-mod_quiz-util-slot-min.js | 2 +- .../moodle-mod_quiz-util-slot.js | 67 ++++- mod/quiz/yui/src/toolboxes/js/resource.js | 60 +++-- mod/quiz/yui/src/toolboxes/js/section.js | 131 ---------- mod/quiz/yui/src/toolboxes/js/toolbox.js | 5 +- mod/quiz/yui/src/util/js/slot.js | 67 ++++- question/engine/questionusage.php | 29 --- 29 files changed, 1248 insertions(+), 854 deletions(-) delete mode 100644 mod/quiz/questiondependency.php create mode 100644 mod/quiz/tests/behat/attempt_require_previous.feature delete mode 100644 mod/quiz/tests/behat/editing_questiondependency.feature create mode 100644 mod/quiz/tests/behat/editing_require_previous.feature diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index 5f6dfc1f8a8..91ac3d2ac47 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -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'. @@ -1168,36 +1194,6 @@ 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 @@ -1285,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. diff --git a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php index aaa9e01b95d..a15bb810d1d 100644 --- a/mod/quiz/backup/moodle2/backup_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/backup_quiz_stepslib.php @@ -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'); diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php index 12fb25d4184..60167f86d36 100644 --- a/mod/quiz/classes/output/edit_renderer.php +++ b/mod/quiz/classes/output/edit_renderer.php @@ -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,8 @@ 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, $previousquestion, $contexts, $pagevars, $pageurl); - $previousquestion = $question; + 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')); } @@ -354,31 +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 \stdClass $previousquestion 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, $previousquestion, $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, $previousquestion, $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; } @@ -387,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; @@ -544,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 \stdClass $previousquestion 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, $previousquestion, \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. @@ -579,13 +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_dependency_icon($structure->get_quiz(), $question, $previousquestion); + $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'); @@ -597,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') @@ -650,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); @@ -667,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'); @@ -689,8 +690,8 @@ 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)), @@ -698,55 +699,42 @@ class edit_renderer extends \plugin_renderer_base { } /** - * Display an icon Add/Remove dependency + * Display the icon for whether this question can only be seen if the previous + * one has been answered. * - * @param object $quiz - * @param object $question - * @param object $previousquestion - * @param bool $dependencysetting + * @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($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'; + 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('addquestiondependency', 'quiz'); - $image = $this->pix_icon('e/insert_page_break', $title); - $action = 'unlinkpage'; + $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 (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' => 'question_dependency_icon', 'disabled' => $disabled, 'data-action' => $action)), - 'page_split_join_wrapper'); + $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); } /** @@ -756,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)); @@ -793,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)); @@ -831,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' => '')), @@ -846,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( @@ -912,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, )) @@ -946,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, )) @@ -956,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); @@ -999,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) { @@ -1023,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') . ' '; @@ -1049,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('&slot=1&', '&slot=%%SLOT%%&', $html); } diff --git a/mod/quiz/classes/structure.php b/mod/quiz/classes/structure.php index fa871fb0e8b..72600f11e66 100644 --- a/mod/quiz/classes/structure.php +++ b/mod/quiz/classes/structure.php @@ -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. @@ -453,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; } } @@ -661,21 +799,13 @@ class structure { } /** - * Change require previous for a slot.. - * @param \stdClass $slot row from the quiz_slots table. + * 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($slot) { + public function update_question_dependency($slotid, $requireprevious) { 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(); + $DB->set_field('quiz_slots', 'requireprevious', $requireprevious, array('id' => $slotid)); } /** diff --git a/mod/quiz/db/upgrade.php b/mod/quiz/db/upgrade.php index 37a271433a6..0318ea07e47 100644 --- a/mod/quiz/db/upgrade.php +++ b/mod/quiz/db/upgrade.php @@ -402,6 +402,10 @@ 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. @@ -803,10 +807,10 @@ function xmldb_quiz_upgrade($oldversion) { // Moodle v2.8.0 release upgrade line. // Put any upgrade step following this. - if ($oldversion < 2014111000) { + 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, null, null, null, 'maxmark'); + $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)) { @@ -814,8 +818,8 @@ function xmldb_quiz_upgrade($oldversion) { } // Quiz savepoint reached. - upgrade_mod_savepoint(true, 2014111000, 'quiz'); + upgrade_mod_savepoint(true, 2015022600, 'quiz'); } + return true; } - diff --git a/mod/quiz/edit_rest.php b/mod/quiz/edit_rest.php index 7396ba739ae..8ffe131d27a 100644 --- a/mod/quiz/edit_rest.php +++ b/mod/quiz/edit_rest.php @@ -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; diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index e4a2b870478..29dfe7ee839 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -45,7 +45,6 @@ $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'; @@ -616,10 +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['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['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:
{$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'; @@ -700,7 +699,6 @@ $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 deleted file mode 100644 index 7baf852e2a4..00000000000 --- a/mod/quiz/questiondependency.php +++ /dev/null @@ -1,43 +0,0 @@ -. - -/** - * 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 ff4db1a82b9..9b746718655 100644 --- a/mod/quiz/renderer.php +++ b/mod/quiz/renderer.php @@ -456,14 +456,11 @@ 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) { - $requireprevious = $attemptobj->require_previous_question($slot); - if ($requireprevious) { - $output .= $requireprevious; - } else { - $output .= $attemptobj->render_question($slot, false, - $attemptobj->attempt_url($slot, $page)); - } + $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/repaginate.php b/mod/quiz/repaginate.php index 9b430144498..58aef9abbc8 100644 --- a/mod/quiz/repaginate.php +++ b/mod/quiz/repaginate.php @@ -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++; diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css index b42c024d887..e4fdb227758 100644 --- a/mod/quiz/styles.css +++ b/mod/quiz/styles.css @@ -24,6 +24,12 @@ 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,11 +686,29 @@ 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 .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 { diff --git a/mod/quiz/tests/behat/attempt_require_previous.feature b/mod/quiz/tests/behat/attempt_require_previous.feature new file mode 100644 index 00000000000..7903bfc3332 --- /dev/null +++ b/mod/quiz/tests/behat/attempt_require_previous.feature @@ -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." diff --git a/mod/quiz/tests/behat/behat_mod_quiz.php b/mod/quiz/tests/behat/behat_mod_quiz.php index 1e073e2c845..306e707e9ad 100644 --- a/mod/quiz/tests/behat/behat_mod_quiz.php +++ b/mod/quiz/tests/behat/behat_mod_quiz.php @@ -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_questiondependency.feature b/mod/quiz/tests/behat/editing_questiondependency.feature deleted file mode 100644 index 89c90ea8018..00000000000 --- a/mod/quiz/tests/behat/editing_questiondependency.feature +++ /dev/null @@ -1,83 +0,0 @@ - -@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/mod/quiz/tests/behat/editing_require_previous.feature b/mod/quiz/tests/behat/editing_require_previous.feature new file mode 100644 index 00000000000..fc0f2c92903 --- /dev/null +++ b/mod/quiz/tests/behat/editing_require_previous.feature @@ -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 diff --git a/mod/quiz/tests/structure_test.php b/mod/quiz/tests/structure_test.php index c9117c8c655..7ea030ba69d 100644 --- a/mod/quiz/tests/structure_test.php +++ b/mod/quiz/tests/structure_test.php @@ -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)); + } } diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index 682124c733d..5db9d21c156 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -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 diff --git a/mod/quiz/version.php b/mod/quiz/version.php index a2459a78437..d9ddae57917 100644 --- a/mod/quiz/version.php +++ b/mod/quiz/version.php @@ -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; diff --git a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js index 4bb67ed08bc..df0a58317c3 100644 --- a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js +++ b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-debug.js @@ -11,7 +11,7 @@ YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { */ // The CSS classes we use. - var CSS = { +var CSS = { ACTIVITYINSTANCE : 'activityinstance', AVAILABILITYINFODIV : 'div.availabilityinfo', CONTENTWITHOUTLINK : 'contentwithoutlink', @@ -24,7 +24,6 @@ YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { JOIN: 'page_join', MODINDENTCOUNT : 'mod-indent-', MODINDENTHUGE : 'mod-indent-huge', - MODULEIDPREFIX : 'slot-', PAGE: 'page', SECTIONHIDDENCLASS : 'hidden', SECTIONIDPREFIX : 'section-', @@ -38,7 +37,6 @@ YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { 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', @@ -58,7 +56,6 @@ YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { PAGELI : 'li.page', SECTIONUL : 'ul.section', SHOW : 'a.' + CSS.SHOW, - SHOWHIDE : 'a.editing_showhide', SLOTLI : 'li.slot', SUMMARKS : '.mod_quiz_summarks' }, @@ -276,8 +273,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); }, /** @@ -322,6 +319,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; @@ -390,8 +392,6 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, { if (M.core.actionmenu && M.core.actionmenu.instance) { M.core.actionmenu.instance.hideMenu(); } - } else { - window.location.reload(true); } }); @@ -414,8 +414,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'), @@ -425,7 +424,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. @@ -575,6 +574,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) { @@ -582,21 +582,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') { @@ -606,8 +601,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); } }); @@ -624,6 +650,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', @@ -677,134 +704,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', @@ -814,9 +713,6 @@ Y.extend(SECTIONTOOLBOX, TOOLBOX, { }, quizid : { 'value' : 0 - }, - format : { - 'value' : 'topics' } } }); diff --git a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js index 98349eb22be..06dce00fc05 100644 --- a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js +++ b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes-min.js @@ -1,2 +1,2 @@ -YUI.add("moodle-mod_quiz-toolboxes",function(e,t){var n={ACTIVITYINSTANCE:"activityinstance",AVAILABILITYINFODIV:"div.availabilityinfo",CONTENTWITHOUTLINK:"contentwithoutlink",CONDITIONALHIDDEN:"conditionalhidden",DIMCLASS:"dimmed",DIMMEDTEXT:"dimmed_text",EDITINSTRUCTIONS:"editinstructions",EDITINGMAXMARK:"editor_displayed",HIDE:"hide",JOIN:"page_join",MODINDENTCOUNT:"mod-indent-",MODINDENTHUGE:"mod-indent-huge",MODULEIDPREFIX:"slot-",PAGE:"page",SECTIONHIDDENCLASS:"hidden",SECTIONIDPREFIX:"section-",SLOT:"slot",SHOW:"editing_show",TITLEEDITOR:"titleeditor"},r={ACTIONAREA:".actions",ACTIONLINKTEXT:".actionlinktext",ACTIVITYACTION:"a.cm-edit-action[data-action], a.editing_maxmark",ACTIVITYFORM:"span.instancemaxmarkcontainer form",ACTIVITYICON:"img.activityicon",ACTIVITYINSTANCE:"."+n.ACTIVITYINSTANCE,ACTIVITYLINK:"."+n.ACTIVITYINSTANCE+" > a",ACTIVITYLI:"li.activity",ACTIVITYMAXMARK:"input[name=maxmark]",COMMANDSPAN:".commands",CONTENTAFTERLINK:"div.contentafterlink",CONTENTWITHOUTLINK:"div.contentwithoutlink",EDITMAXMARK:"a.editing_maxmark",HIDE:"a.editing_hide",HIGHLIGHT:"a.editing_highlight",INSTANCENAME:"span.instancename",INSTANCEMAXMARK:"span.instancemaxmark",MODINDENTDIV:".mod-indent",MODINDENTOUTER:".mod-indent-outer",NUMQUESTIONS:".numberofquestions",PAGECONTENT:"div#page-content",PAGELI:"li.page",SECTIONUL:"ul.section",SHOW:"a."+n.SHOW,SHOWHIDE:"a.editing_showhide",SLOTLI:"li.slot",SUMMARKS:".mod_quiz_summarks"},i=e.one(document.body);M.mod_quiz=M.mod_quiz||{};var s=function(){s.superclass.constructor.apply(this,arguments)};e.extend(s,e.Base,{send_request:function(t,n,i,s){t||(t={});var o=this.get("config").pageparams,u;for(u in o)t[u]=o[u];t.sesskey=M.cfg.sesskey,t.courseid=this.get("courseid"),t.quizid=this.get("quizid");var a=M.cfg.wwwroot+this.get("ajaxurl"),f=[],l={method:"POST",data:t,on:{success:function(t,s){try{f=e.JSON.parse(s.responseText),f.error&&new M.core.ajaxException(f)}catch(o){}f.hasOwnProperty("newsummarks")&&e.one(r.SUMMARKS).setHTML(f.newsummarks),f.hasOwnProperty("newnumquestions")&&e.one(r.NUMQUESTIONS).setHTML(M.util.get_string("numquestionsx","quiz",f.newnumquestions)),i&&e.bind(i,this,f)(),n&&window.setTimeout(function(){n.hide()},400)},failure:function(e,t){n&&n.hide(),new M.core.ajaxException(t)}},context:this};if(s)for(u in s)l[u]=s[u];return n&&n.show(),e.io(a,l),this}},{NAME:"mod_quiz-toolbox",ATTRS:{courseid:{value:0},quizid:{value:0},ajaxurl:{value:null},config:{value:{}}}});var o=function(){o.superclass.constructor.apply(this,arguments)};e.extend(o,s,{editmaxmarkevents:[],NODE_PAGE:1,NODE_SLOT:2,NODE_JOIN:3,initializer:function(){M.mod_quiz.quizbase.register_module(this),i.delegate("key",this.handle_data_action,"down:enter",r.ACTIVITYACTION,this),e.delegate("click",this.handle_data_action,i,r.ACTIVITYACTION,this)},handle_data_action:function(e){var t=e.target;t.test("a")||(t=t.ancestor(r.ACTIVITYACTION));var n=t.getData("action"),i=t.ancestor(r.ACTIVITYLI);if(!t.test("a")||!n||!i)return;switch(n){case"editmaxmark":this.edit_maxmark(e,t,i,n);break;case"delete":this.delete_with_confirmation(e,t,i,n);break;case"addpagebreak":case"removepagebreak":this.update_page_break(e,t,i,n);break;default:}},add_spinner:function(t){var n=t.one(r.ACTIONAREA);return n?M.util.add_spinner(e,n):null},delete_with_confirmation:function(t,n,r){t.preventDefault();var i=r,s="",o=M.util.get_string("pluginname","qtype_"+i.getAttribute("class").match(/qtype_([^\s]*)/)[1]);s=M.util.get_string("confirmremovequestion","quiz",o);var u=new M.core.confirm({question:s,modal:!0});return u.on("complete-yes",function(){var t=this.add_spinner(i),n={"class":"resource",action:"DELETE",id:e.Moodle.mod_quiz.util.slot.getId(i)};this.send_request(n,t,function(t){t.deleted?(e.Moodle.mod_quiz.util.slot.remove(i),this.reorganise_edit_page(),M.core.actionmenu&&M.core.actionmenu.instance&&M.core.actionmenu.instance.hideMenu()):window.location.reload(!0)})},this),this},edit_maxmark:function(t,i,s){var o=e.Moodle.mod_quiz.util.slot.getId(s),u=s.one(r.INSTANCEMAXMARK),a=s.one(r.ACTIVITYINSTANCE),f=u.get("firstChild"),l=f.get("data"),c=l,h,p=u,d={"class":"resource",field:"getmaxmark",id:o};t.preventDefault(),this.send_request(d,null,function(t){M.core.actionmenu&&M.core.actionmenu.instance&&M.core.actionmenu.instance.hideMenu(),t.instancemaxmark&&(c=t.instancemaxmark);var r=e.Node.create('
'),i=e.Node.create('').set("innerHTML",M.util.get_string("edittitleinstructions","moodle")),o=e.Node.create('').setAttrs({value:c,autocomplete:"off","aria-describedby":"id_editinstructions",maxLength:"12",size:parseInt(this.get("config").questiondecimalpoints,10)+2});r.appendChild(o),r.setData("anchor",p),a.insert(i,"before"),p.replace(r);var u="left";window.right_to_left()&&(u="right"),s.addClass(n.EDITINGMAXMARK),o.focus().select(),h=o.on("blur",this.edit_maxmark_cancel,this,s,!1),this.editmaxmarkevents.push(h),h=o.on("key",this.edit_maxmark_cancel,"esc",this,s,!0),this.editmaxmarkevents.push(h),h=r.on("submit",this.edit_maxmark_submit,this,s,l),this.editmaxmarkevents.push(h)})},edit_maxmark_submit:function(t,n,i){t.preventDefault();var s=e.Lang.trim(n.one(r.ACTIVITYFORM+" "+r.ACTIVITYMAXMARK).get("value")),o=this.add_spinner(n);this.edit_maxmark_clear(n),n.one(r.INSTANCEMAXMARK).setContent(s);if(s!==null&&s!==""&&s!==i){var u={"class":"resource",field:"updatemaxmark",maxmark:s,id:e.Moodle.mod_quiz.util.slot.getId(n)};this.send_request(u,o,function(e){e.instancemaxmark&&n.one(r.INSTANCEMAXMARK).setContent(e.instancemaxmark)})}},edit_maxmark_cancel:function(e,t,n){n&&e.preventDefault(),this.edit_maxmark_clear(t)},edit_maxmark_clear:function(t){(new e.EventHandle(this.editmaxmarkevents)).detach();var i=t.one(r.ACTIVITYFORM),s=t.one("#id_editinstructions");i&&i.replace(i.getData("anchor")),s&&s.remove(),t.removeClass(n.EDITINGMAXMARK),e.later(100,this,function(){t.one(r.EDITMAXMARK -).focus()}),e.one("input[name=maxmark")||e.one("body").append('')},update_page_break:function(t,n,r,i){t.preventDefault();var s=r.next("li.activity.slot"),o=this.add_spinner(s),u=0,a=i==="removepagebreak"?1:2,f={"class":"resource",field:"updatepagebreak",id:u,value:a};return u=e.Moodle.mod_quiz.util.slot.getId(s),u&&(f.id=Number(u)),this.send_request(f,o,function(t){if(t.slots){if(i==="addpagebreak")e.Moodle.mod_quiz.util.page.add(r);else{var n=r.next(e.Moodle.mod_quiz.util.page.SELECTORS.PAGE);e.Moodle.mod_quiz.util.page.remove(n,!0)}this.reorganise_edit_page()}else window.location.reload(!0)}),this},reorganise_edit_page:function(){e.Moodle.mod_quiz.util.slot.reorderSlots(),e.Moodle.mod_quiz.util.slot.reorderPageBreaks(),e.Moodle.mod_quiz.util.page.reorderPages()},NAME:"mod_quiz-resource-toolbox",ATTRS:{courseid:{value:0},quizid:{value:0}}}),M.mod_quiz.resource_toolbox=null,M.mod_quiz.init_resource_toolbox=function(e){return M.mod_quiz.resource_toolbox=new o(e),M.mod_quiz.resource_toolbox};var u=function(){u.superclass.constructor.apply(this,arguments)};e.extend(u,s,{initializer:function(){M.mod_quiz.quizbase.register_module(this),e.delegate("click",this.toggle_highlight,r.PAGECONTENT,r.SECTIONLI+" "+r.HIGHLIGHT,this),e.delegate("click",this.toggle_hide_section,r.PAGECONTENT,r.SECTIONLI+" "+r.SHOWHIDE,this)},toggle_hide_section:function(t){t.preventDefault();var i=t.target.ancestor(M.mod_quiz.format.get_section_selector(e)),s=t.target.ancestor("a",!0),o=s.one("img"),u,a,f;i.hasClass(n.SECTIONHIDDENCLASS)?(i.removeClass(n.SECTIONHIDDENCLASS),u=1,a="show",f="hide"):(i.addClass(n.SECTIONHIDDENCLASS),u=0,a="hide",f="show");var l=M.util.get_string(f+"fromothers","format_"+this.get("format"));o.setAttrs({alt:l,src:M.util.image_url("i/"+f)}),s.set("title",l);var c={"class":"section",field:"visible",id:e.Moodle.core_course.util.section.getId(i.ancestor(M.mod_quiz.edit.get_section_wrapper(e),!0)),value:u},h=M.util.add_lightbox(e,i);h.show(),this.send_request(c,h,function(t){var n=i.all(r.ACTIVITYLI);n.each(function(n){var i;n.one(r.SHOW)?i=n.one(r.SHOW):i=n.one(r.HIDE);var s=e.Moodle.mod_quiz.util.slot.getId(n);e.Array.indexOf(t.resourcestotoggle,""+s)!==-1&&M.mod_quiz.resource_toolbox.handle_resource_dim(i,n,a)},this)})},toggle_highlight:function(t){t.preventDefault();var n=t.target.ancestor(M.mod_quiz.edit.get_section_selector(e)),i=t.target.ancestor("a",!0),s=i.one("img"),o=n.hasClass("current"),u=0,a=M.util.get_string("markthistopic","moodle");e.one(r.PAGECONTENT).all(M.mod_quiz.edit.get_section_selector(e)+".current "+r.HIGHLIGHT).set("title",a),e.one(r.PAGECONTENT).all(M.mod_quiz.edit.get_section_selector(e)+".current "+r.HIGHLIGHT+" img").set("alt",a).set("src",M.util.image_url("i/marker")),e.one(r.PAGECONTENT).all(M.mod_quiz.edit.get_section_selector(e)).removeClass("current");if(!o){n.addClass("current"),u=e.Moodle.core_course.util.section.getId(n.ancestor(M.mod_quiz.edit.get_section_wrapper(e),!0));var f=M.util.get_string("markedthistopic","moodle");i.set("title",f),s.set("alt",f).set("src",M.util.image_url("i/marked"))}var l={"class":"course",field:"marker",value:u},c=M.util.add_lightbox(e,n);c.show(),this.send_request(l,c)}},{NAME:"mod_quiz-section-toolbox",ATTRS:{courseid:{value:0},quizid:{value:0},format:{value:"topics"}}}),M.mod_quiz.init_section_toolbox=function(e){return new u(e)}},"@VERSION@",{requires:["base","node","event","event-key","io","moodle-mod_quiz-quizbase","moodle-mod_quiz-util-slot","moodle-core-notification-ajaxexception"]}); +YUI.add("moodle-mod_quiz-toolboxes",function(e,t){var n={ACTIVITYINSTANCE:"activityinstance",AVAILABILITYINFODIV:"div.availabilityinfo",CONTENTWITHOUTLINK:"contentwithoutlink",CONDITIONALHIDDEN:"conditionalhidden",DIMCLASS:"dimmed",DIMMEDTEXT:"dimmed_text",EDITINSTRUCTIONS:"editinstructions",EDITINGMAXMARK:"editor_displayed",HIDE:"hide",JOIN:"page_join",MODINDENTCOUNT:"mod-indent-",MODINDENTHUGE:"mod-indent-huge",PAGE:"page",SECTIONHIDDENCLASS:"hidden",SECTIONIDPREFIX:"section-",SLOT:"slot",SHOW:"editing_show",TITLEEDITOR:"titleeditor"},r={ACTIONAREA:".actions",ACTIONLINKTEXT:".actionlinktext",ACTIVITYACTION:"a.cm-edit-action[data-action], a.editing_maxmark",ACTIVITYFORM:"span.instancemaxmarkcontainer form",ACTIVITYINSTANCE:"."+n.ACTIVITYINSTANCE,ACTIVITYLINK:"."+n.ACTIVITYINSTANCE+" > a",ACTIVITYLI:"li.activity",ACTIVITYMAXMARK:"input[name=maxmark]",COMMANDSPAN:".commands",CONTENTAFTERLINK:"div.contentafterlink",CONTENTWITHOUTLINK:"div.contentwithoutlink",EDITMAXMARK:"a.editing_maxmark",HIDE:"a.editing_hide",HIGHLIGHT:"a.editing_highlight",INSTANCENAME:"span.instancename",INSTANCEMAXMARK:"span.instancemaxmark",MODINDENTDIV:".mod-indent",MODINDENTOUTER:".mod-indent-outer",NUMQUESTIONS:".numberofquestions",PAGECONTENT:"div#page-content",PAGELI:"li.page",SECTIONUL:"ul.section",SHOW:"a."+n.SHOW,SLOTLI:"li.slot",SUMMARKS:".mod_quiz_summarks"},i=e.one(document.body);M.mod_quiz=M.mod_quiz||{};var s=function(){s.superclass.constructor.apply(this,arguments)};e.extend(s,e.Base,{send_request:function(t,n,i,s){t||(t={});var o=this.get("config").pageparams,u;for(u in o)t[u]=o[u];t.sesskey=M.cfg.sesskey,t.courseid=this.get("courseid"),t.quizid=this.get("quizid");var a=M.cfg.wwwroot+this.get("ajaxurl"),f=[],l={method:"POST",data:t,on:{success:function(t,s){try{f=e.JSON.parse(s.responseText),f.error&&new M.core.ajaxException(f)}catch(o){}f.hasOwnProperty("newsummarks")&&e.one(r.SUMMARKS).setHTML(f.newsummarks),f.hasOwnProperty("newnumquestions")&&e.one(r.NUMQUESTIONS).setHTML(M.util.get_string("numquestionsx","quiz",f.newnumquestions)),i&&e.bind(i,this,f)(),n&&window.setTimeout(function(){n.hide()},400)},failure:function(e,t){n&&n.hide(),new M.core.ajaxException(t)}},context:this};if(s)for(u in s)l[u]=s[u];return n&&n.show(),e.io(a,l),this}},{NAME:"mod_quiz-toolbox",ATTRS:{courseid:{value:0},quizid:{value:0},ajaxurl:{value:null},config:{value:{}}}});var o=function(){o.superclass.constructor.apply(this,arguments)};e.extend(o,s,{editmaxmarkevents:[],NODE_PAGE:1,NODE_SLOT:2,NODE_JOIN:3,initializer:function(){M.mod_quiz.quizbase.register_module(this),e.delegate("click",this.handle_data_action,i,r.ACTIVITYACTION,this),e.delegate("click",this.handle_data_action,i,r.DEPENDENCY_LINK,this)},handle_data_action:function(e){var t=e.target;t.test("a")||(t=t.ancestor(r.ACTIVITYACTION));var n=t.getData("action"),i=t.ancestor(r.ACTIVITYLI);if(!t.test("a")||!n||!i)return;switch(n){case"editmaxmark":this.edit_maxmark(e,t,i,n);break;case"delete":this.delete_with_confirmation(e,t,i,n);break;case"addpagebreak":case"removepagebreak":this.update_page_break(e,t,i,n);break;case"adddependency":case"removedependency":this.update_dependency(e,t,i,n);break;default:}},add_spinner:function(t){var n=t.one(r.ACTIONAREA);return n?M.util.add_spinner(e,n):null},delete_with_confirmation:function(t,n,r){t.preventDefault();var i=r,s="",o=M.util.get_string("pluginname","qtype_"+i.getAttribute("class").match(/qtype_([^\s]*)/)[1]);s=M.util.get_string("confirmremovequestion","quiz",o);var u=new M.core.confirm({question:s,modal:!0});return u.on("complete-yes",function(){var t=this.add_spinner(i),n={"class":"resource",action:"DELETE",id:e.Moodle.mod_quiz.util.slot.getId(i)};this.send_request(n,t,function(t){t.deleted&&(e.Moodle.mod_quiz.util.slot.remove(i),this.reorganise_edit_page(),M.core.actionmenu&&M.core.actionmenu.instance&&M.core.actionmenu.instance.hideMenu())})},this),this},edit_maxmark:function(t,i,s){var o=s.one(r.INSTANCEMAXMARK),u=s.one(r.ACTIVITYINSTANCE),a=o.get("firstChild"),f=a.get("data"),l=f,c,h=o,p={"class":"resource",field:"getmaxmark",id:e.Moodle.mod_quiz.util.slot.getId(s)};t.preventDefault(),this.send_request(p,null,function(t){M.core.actionmenu&&M.core.actionmenu.instance&&M.core.actionmenu.instance.hideMenu(),t.instancemaxmark&&(l=t.instancemaxmark);var r=e.Node.create(''),i=e.Node.create('').set("innerHTML",M.util.get_string("edittitleinstructions","moodle")),o=e.Node.create('').setAttrs({value:l,autocomplete:"off","aria-describedby":"id_editinstructions",maxLength:"12",size:parseInt(this.get("config").questiondecimalpoints,10)+2});r.appendChild(o),r.setData("anchor",h),u.insert(i,"before"),h.replace(r);var a="left";window.right_to_left()&&(a="right"),s.addClass(n.EDITINGMAXMARK),o.focus().select(),c=o.on("blur",this.edit_maxmark_cancel,this,s,!1),this.editmaxmarkevents.push(c),c=o.on("key",this.edit_maxmark_cancel,"esc",this,s,!0),this.editmaxmarkevents.push(c),c=r.on("submit",this.edit_maxmark_submit,this,s,f),this.editmaxmarkevents.push(c)})},edit_maxmark_submit:function(t,n,i){t.preventDefault();var s=e.Lang.trim(n.one(r.ACTIVITYFORM+" "+r.ACTIVITYMAXMARK).get("value")),o=this.add_spinner(n);this.edit_maxmark_clear(n),n.one(r.INSTANCEMAXMARK).setContent(s);if(s!==null&&s!==""&&s!==i){var u={"class":"resource",field:"updatemaxmark",maxmark:s,id:e.Moodle.mod_quiz.util.slot.getId(n)};this.send_request(u,o,function(e){e.instancemaxmark&&n.one(r.INSTANCEMAXMARK).setContent(e.instancemaxmark)})}},edit_maxmark_cancel:function(e,t,n){n&&e.preventDefault(),this.edit_maxmark_clear(t)},edit_maxmark_clear:function(t){(new e.EventHandle(this.editmaxmarkevents)).detach();var i=t.one(r.ACTIVITYFORM),s=t.one("#id_editinstructions");i&&i.replace(i.getData("anchor")),s&&s.remove(),t.removeClass(n.EDITINGMAXMARK),e.later(100,this,function(){t.one(r.EDITMAXMARK).focus()}),e.one("input[name=maxmark" +)||e.one("body").append('')},update_page_break:function(t,n,r,i){t.preventDefault();var s=r.next("li.activity.slot"),o=this.add_spinner(s),u=i==="removepagebreak"?1:2,a={"class":"resource",field:"updatepagebreak",id:e.Moodle.mod_quiz.util.slot.getId(s),value:u};return this.send_request(a,o,function(t){if(t.slots){if(i==="addpagebreak")e.Moodle.mod_quiz.util.page.add(r);else{var n=r.next(e.Moodle.mod_quiz.util.page.SELECTORS.PAGE);e.Moodle.mod_quiz.util.page.remove(n,!0)}this.reorganise_edit_page()}}),this},update_dependency:function(t,n,r,i){t.preventDefault();var s=this.add_spinner(r),o={"class":"resource",field:"updatedependency",id:e.Moodle.mod_quiz.util.slot.getId(r),value:i==="adddependency"?1:0};return this.send_request(o,s,function(t){t.hasOwnProperty("requireprevious")&&e.Moodle.mod_quiz.util.slot.updateDependencyIcon(r,t.requireprevious)}),this},reorganise_edit_page:function(){e.Moodle.mod_quiz.util.slot.reorderSlots(),e.Moodle.mod_quiz.util.slot.reorderPageBreaks(),e.Moodle.mod_quiz.util.page.reorderPages(),e.Moodle.mod_quiz.util.slot.updateAllDependencyIcons()},NAME:"mod_quiz-resource-toolbox",ATTRS:{courseid:{value:0},quizid:{value:0}}}),M.mod_quiz.resource_toolbox=null,M.mod_quiz.init_resource_toolbox=function(e){return M.mod_quiz.resource_toolbox=new o(e),M.mod_quiz.resource_toolbox};var u=function(){u.superclass.constructor.apply(this,arguments)};e.extend(u,s,{initializer:function(){M.mod_quiz.quizbase.register_module(this)}},{NAME:"mod_quiz-section-toolbox",ATTRS:{courseid:{value:0},quizid:{value:0}}}),M.mod_quiz.init_section_toolbox=function(e){return new u(e)}},"@VERSION@",{requires:["base","node","event","event-key","io","moodle-mod_quiz-quizbase","moodle-mod_quiz-util-slot","moodle-core-notification-ajaxexception"]}); diff --git a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js index 4bb67ed08bc..df0a58317c3 100644 --- a/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js +++ b/mod/quiz/yui/build/moodle-mod_quiz-toolboxes/moodle-mod_quiz-toolboxes.js @@ -11,7 +11,7 @@ YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { */ // The CSS classes we use. - var CSS = { +var CSS = { ACTIVITYINSTANCE : 'activityinstance', AVAILABILITYINFODIV : 'div.availabilityinfo', CONTENTWITHOUTLINK : 'contentwithoutlink', @@ -24,7 +24,6 @@ YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { JOIN: 'page_join', MODINDENTCOUNT : 'mod-indent-', MODINDENTHUGE : 'mod-indent-huge', - MODULEIDPREFIX : 'slot-', PAGE: 'page', SECTIONHIDDENCLASS : 'hidden', SECTIONIDPREFIX : 'section-', @@ -38,7 +37,6 @@ YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { 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', @@ -58,7 +56,6 @@ YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) { PAGELI : 'li.page', SECTIONUL : 'ul.section', SHOW : 'a.' + CSS.SHOW, - SHOWHIDE : 'a.editing_showhide', SLOTLI : 'li.slot', SUMMARKS : '.mod_quiz_summarks' }, @@ -276,8 +273,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); }, /** @@ -322,6 +319,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; @@ -390,8 +392,6 @@ Y.extend(RESOURCETOOLBOX, TOOLBOX, { if (M.core.actionmenu && M.core.actionmenu.instance) { M.core.actionmenu.instance.hideMenu(); } - } else { - window.location.reload(true); } }); @@ -414,8 +414,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'), @@ -425,7 +424,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. @@ -575,6 +574,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) { @@ -582,21 +582,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') { @@ -606,8 +601,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); } }); @@ -624,6 +650,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', @@ -677,134 +704,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', @@ -814,9 +713,6 @@ Y.extend(SECTIONTOOLBOX, TOOLBOX, { }, quizid : { 'value' : 0 - }, - format : { - 'value' : 'topics' } } }); diff --git a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js index 68436fa94a1..2a35d50b8f6 100644 --- a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js +++ b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-debug.js @@ -18,7 +18,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-', @@ -32,7 +33,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' }, /** @@ -333,6 +337,65 @@ 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')); + } } }; diff --git a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js index a090900ce84..c5936edc450 100644 --- a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js +++ b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot-min.js @@ -1 +1 @@ -YUI.add("moodle-mod_quiz-util-slot",function(e,t){e.namespace("Moodle.mod_quiz.util.slot"),e.Moodle.mod_quiz.util.slot={CSS:{SLOT:"slot",QUESTIONTYPEDESCRIPTION:"qtype_description"},CONSTANTS:{SLOTIDPREFIX:"slot-",QUESTION:M.util.get_string("question","moodle")},SELECTORS:{SLOT:"li.slot",INSTANCENAME:".instancename",NUMBER:"span.slotnumber",PAGECONTENT:"div#page-content",PAGEBREAK:"span.page_split_join_wrapper",ICON:"img.smallicon",QUESTIONTYPEDESCRIPTION:".qtype_description",SECTIONUL:"ul.section"},getSlotFromComponent:function(t){return e.one(t).ancestor(this.SELECTORS.SLOT,!0)},getId:function(e){var t=e.get("id").replace(this.CONSTANTS.SLOTIDPREFIX,"");return t=parseInt(t,10),typeof t=="number"&&isFinite(t)?t:!1},getName:function(e){var t=e.one(this.SELECTORS.INSTANCENAME);return t?t.get("firstChild").get("data"):null},getNumber:function(e){if(!e)return!1;var t=e.one(this.SELECTORS.NUMBER).get("text").replace(this.CONSTANTS.QUESTION,"");return t=parseInt(t,10),typeof t=="number"&&isFinite(t)?t:!1},setNumber:function(e,t){var n=e.one(this.SELECTORS.NUMBER);n.setHTML(''+this.CONSTANTS.QUESTION+" "+t)},getSlots:function(){return e.all(this.SELECTORS.PAGECONTENT+" "+this.SELECTORS.SECTIONUL+" "+this.SELECTORS.SLOT)},getNumberedSlots:function(){var t=this.SELECTORS.PAGECONTENT+" "+this.SELECTORS.SECTIONUL;return t+=" "+this.SELECTORS.SLOT+":not("+this.SELECTORS.QUESTIONTYPEDESCRIPTION+")",e.all(t)},getPrevious:function(e){return e.previous(this.SELECTORS.SLOT)},getPreviousNumbered:function(e){return e.previous(this.SELECTORS.SLOT+":not("+this.SELECTORS.QUESTIONTYPEDESCRIPTION+")")},reorderSlots:function(){var t=this.getSlots();t.each(function(t){if(!e.Moodle.mod_quiz.util.page.getPageFromSlot(t)){var n=t.next(e.Moodle.mod_quiz.util.page.SELECTORS.PAGE);t.swap(n)}var r=this.getPreviousNumbered(t),i=0;if(t.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION))return;r&&(i=this.getNumber(r)),this.setNumber(t,i+1)},this)},remove:function(t){var n=e.Moodle.mod_quiz.util.page.getPageFromSlot(t);t.remove();if(!e.Moodle.mod_quiz.util.page.isEmpty(n))return;e.Moodle.mod_quiz.util.page.remove(n)},getPageBreaks:function(){var t=this.SELECTORS.PAGECONTENT+" "+this.SELECTORS.SECTIONUL;return t+=" "+this.SELECTORS.SLOT+this.SELECTORS.PAGEBREAK,e.all(t)},getPageBreak:function(t){return e.one(t).one(this.SELECTORS.PAGEBREAK)},addPageBreak:function(t){var n=M.mod_quiz.resource_toolbox.get("config").addpageiconhtml;n=n.replace("%%SLOT%%",this.getNumber(t));var r=e.Node.create(n);return t.one("div").insert(r,"after"),r},removePageBreak:function(e){var t=this.getPageBreak(e);return t?(t.remove(),!0):!1},reorderPageBreaks:function(){var t=this.getSlots(),n=0;t.each(function(r,i){n++;var s=this.getPageBreak(r);if(!s&&i===t.size()-1)return;!s&&i!==t.size()-1&&(s=this.addPageBreak(r)),s&&i===t.size()-1&&this.removePageBreak(r);var o=s.get("childNodes").item(0),u="",a="";e.Moodle.mod_quiz.util.page.isPage(r.next("li.activity"))?(u="removepagebreak",a="e/remove_page_break"):(u="addpagebreak",a="e/insert_page_break"),o.set("title",M.util.get_string(u,"quiz")),o.setData("action",u);var f=o.one(this.SELECTORS.ICON);f.set("title",M.util.get_string(u,"quiz")),f.set("alt",M.util.get_string(u,"quiz")),f.set("src",M.util.image_url(a));var l=e.QueryString.parse(o.get("href"));l.slot=n;var c="";for(var h in l)c.length&&(c+="&"),c+=h+"="+l[h];o.set("href",c)},this)}}},"@VERSION@",{requires:["node","moodle-mod_quiz-util-base"]}); +YUI.add("moodle-mod_quiz-util-slot",function(e,t){e.namespace("Moodle.mod_quiz.util.slot"),e.Moodle.mod_quiz.util.slot={CSS:{SLOT:"slot",QUESTIONTYPEDESCRIPTION:"qtype_description",CANNOT_DEPEND:"question_dependency_cannot_depend"},CONSTANTS:{SLOTIDPREFIX:"slot-",QUESTION:M.util.get_string("question","moodle")},SELECTORS:{SLOT:"li.slot",INSTANCENAME:".instancename",NUMBER:"span.slotnumber",PAGECONTENT:"div#page-content",PAGEBREAK:"span.page_split_join_wrapper",ICON:"img.smallicon",QUESTIONTYPEDESCRIPTION:".qtype_description",SECTIONUL:"ul.section",DEPENDENCY_WRAPPER:".question_dependency_wrapper",DEPENDENCY_LINK:".question_dependency_wrapper .cm-edit-action",DEPENDENCY_ICON:".question_dependency_wrapper img"},getSlotFromComponent:function(t){return e.one(t).ancestor(this.SELECTORS.SLOT,!0)},getId:function(e){var t=e.get("id").replace(this.CONSTANTS.SLOTIDPREFIX,"");return t=parseInt(t,10),typeof t=="number"&&isFinite(t)?t:!1},getName:function(e){var t=e.one(this.SELECTORS.INSTANCENAME);return t?t.get("firstChild").get("data"):null},getNumber:function(e){if(!e)return!1;var t=e.one(this.SELECTORS.NUMBER).get("text").replace(this.CONSTANTS.QUESTION,"");return t=parseInt(t,10),typeof t=="number"&&isFinite(t)?t:!1},setNumber:function(e,t){var n=e.one(this.SELECTORS.NUMBER);n.setHTML(''+this.CONSTANTS.QUESTION+" "+t)},getSlots:function(){return e.all(this.SELECTORS.PAGECONTENT+" "+this.SELECTORS.SECTIONUL+" "+this.SELECTORS.SLOT)},getNumberedSlots:function(){var t=this.SELECTORS.PAGECONTENT+" "+this.SELECTORS.SECTIONUL;return t+=" "+this.SELECTORS.SLOT+":not("+this.SELECTORS.QUESTIONTYPEDESCRIPTION+")",e.all(t)},getPrevious:function(e){return e.previous(this.SELECTORS.SLOT)},getPreviousNumbered:function(e){return e.previous(this.SELECTORS.SLOT+":not("+this.SELECTORS.QUESTIONTYPEDESCRIPTION+")")},reorderSlots:function(){var t=this.getSlots();t.each(function(t){if(!e.Moodle.mod_quiz.util.page.getPageFromSlot(t)){var n=t.next(e.Moodle.mod_quiz.util.page.SELECTORS.PAGE);t.swap(n)}var r=this.getPreviousNumbered(t),i=0;if(t.hasClass(this.CSS.QUESTIONTYPEDESCRIPTION))return;r&&(i=this.getNumber(r)),this.setNumber(t,i+1)},this)},remove:function(t){var n=e.Moodle.mod_quiz.util.page.getPageFromSlot(t);t.remove();if(!e.Moodle.mod_quiz.util.page.isEmpty(n))return;e.Moodle.mod_quiz.util.page.remove(n)},getPageBreaks:function(){var t=this.SELECTORS.PAGECONTENT+" "+this.SELECTORS.SECTIONUL;return t+=" "+this.SELECTORS.SLOT+this.SELECTORS.PAGEBREAK,e.all(t)},getPageBreak:function(t){return e.one(t).one(this.SELECTORS.PAGEBREAK)},addPageBreak:function(t){var n=M.mod_quiz.resource_toolbox.get("config").addpageiconhtml;n=n.replace("%%SLOT%%",this.getNumber(t));var r=e.Node.create(n);return t.one("div").insert(r,"after"),r},removePageBreak:function(e){var t=this.getPageBreak(e);return t?(t.remove(),!0):!1},reorderPageBreaks:function(){var t=this.getSlots(),n=0;t.each(function(r,i){n++;var s=this.getPageBreak(r);if(!s&&i===t.size()-1)return;!s&&i!==t.size()-1&&(s=this.addPageBreak(r)),s&&i===t.size()-1&&this.removePageBreak(r);var o=s.get("childNodes").item(0),u="",a="";e.Moodle.mod_quiz.util.page.isPage(r.next("li.activity"))?(u="removepagebreak",a="e/remove_page_break"):(u="addpagebreak",a="e/insert_page_break"),o.set("title",M.util.get_string(u,"quiz")),o.setData("action",u);var f=o.one(this.SELECTORS.ICON);f.set("title",M.util.get_string(u,"quiz")),f.set("alt",M.util.get_string(u,"quiz")),f.set("src",M.util.image_url(a));var l=e.QueryString.parse(o.get("href"));l.slot=n;var c="";for(var h in l)c.length&&(c+="&"),c+=h+"="+l[h];o.set("href",c)},this)},updateAllDependencyIcons:function(){var e=this.getSlots(),t=0,n=null;e.each(function(e){t++,t==1||n.getData("canfinish")==="0"?e.one(this.SELECTORS.DEPENDENCY_WRAPPER).addClass(this.CSS.CANNOT_DEPEND):e.one(this.SELECTORS.DEPENDENCY_WRAPPER).removeClass(this.CSS.CANNOT_DEPEND),this.updateDependencyIcon(e,null),n=e},this)},updateDependencyIcon:function(e,t){var n=e.one(this.SELECTORS.DEPENDENCY_LINK),r=e.one(this.SELECTORS.DEPENDENCY_ICON),i=this.getPrevious(e),s={thisq:this.getNumber(e)};i&&(s.previousq=this.getNumber(i)),t===null&&(t=n.getData("action")==="removedependency"),t?(n.set("title",M.util.get_string("questiondependencyremove","quiz",s)),n.setData("action","removedependency"),r.set("alt",M.util.get_string("questiondependsonprevious","quiz")),r.set("src",M.util.image_url("t/locked","moodle"))):(n.set("title",M.util.get_string("questiondependencyadd","quiz",s)),n.setData("action","adddependency"),r.set("alt",M.util.get_string("questiondependencyfree","quiz")),r.set("src",M.util.image_url("t/unlocked","moodle")))}}},"@VERSION@",{requires:["node","moodle-mod_quiz-util-base"]}); diff --git a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js index 68436fa94a1..2a35d50b8f6 100644 --- a/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js +++ b/mod/quiz/yui/build/moodle-mod_quiz-util-slot/moodle-mod_quiz-util-slot.js @@ -18,7 +18,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-', @@ -32,7 +33,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' }, /** @@ -333,6 +337,65 @@ 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')); + } } }; diff --git a/mod/quiz/yui/src/toolboxes/js/resource.js b/mod/quiz/yui/src/toolboxes/js/resource.js index bca3e890299..a455a4d21f2 100644 --- a/mod/quiz/yui/src/toolboxes/js/resource.js +++ b/mod/quiz/yui/src/toolboxes/js/resource.js @@ -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', diff --git a/mod/quiz/yui/src/toolboxes/js/section.js b/mod/quiz/yui/src/toolboxes/js/section.js index 19051ebd2c7..0d0bd5cf20b 100644 --- a/mod/quiz/yui/src/toolboxes/js/section.js +++ b/mod/quiz/yui/src/toolboxes/js/section.js @@ -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' } } }); diff --git a/mod/quiz/yui/src/toolboxes/js/toolbox.js b/mod/quiz/yui/src/toolboxes/js/toolbox.js index 7929788e97a..be5eed1482a 100644 --- a/mod/quiz/yui/src/toolboxes/js/toolbox.js +++ b/mod/quiz/yui/src/toolboxes/js/toolbox.js @@ -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' }, diff --git a/mod/quiz/yui/src/util/js/slot.js b/mod/quiz/yui/src/util/js/slot.js index c969f5d1864..70a5790d0c1 100644 --- a/mod/quiz/yui/src/util/js/slot.js +++ b/mod/quiz/yui/src/util/js/slot.js @@ -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')); + } } }; diff --git a/question/engine/questionusage.php b/question/engine/questionusage.php index 005a69079fe..c4ce6e90489 100644 --- a/question/engine/questionusage.php +++ b/question/engine/questionusage.php @@ -821,35 +821,6 @@ 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