Merge branch 'MDL-40990' of git://github.com/timhunt/moodle

This commit is contained in:
David Monllao
2015-03-18 08:22:06 +08:00
32 changed files with 1331 additions and 601 deletions
+79 -1
View File
@@ -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'.
@@ -1255,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.
@@ -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');
+128 -86
View File
@@ -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,9 @@ class edit_renderer extends \plugin_renderer_base {
$contexts, $pagevars, $pageurl) {
$output = '';
foreach ($structure->get_questions_in_section($section->id) as $question) {
$output .= $this->question_row($structure, $question, $contexts, $pagevars, $pageurl);
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'));
}
@@ -353,30 +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 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, $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, $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;
}
@@ -385,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;
@@ -542,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 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, \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.
@@ -577,12 +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_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');
@@ -594,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')
@@ -611,8 +615,7 @@ class edit_renderer extends \plugin_renderer_base {
*/
public function question_number($number) {
if (is_numeric($number)) {
$number = html_writer::span(get_string('question'), 'accesshide') .
' ' . $number;
$number = html_writer::span(get_string('question'), 'accesshide') . ' ' . $number;
}
return html_writer::tag('span', $number, array('class' => 'slotnumber'));
}
@@ -648,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);
@@ -665,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');
@@ -687,14 +690,53 @@ 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)),
'page_split_join_wrapper');
}
/**
* Display the icon for whether this question can only be seen if the previous
* one has been answered.
*
* @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($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('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 (!$structure->can_be_edited()) {
$disabled = 'disabled';
}
$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);
}
/**
* Renders html to display a name with the link to the question on a quiz edit page
*
@@ -702,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));
@@ -739,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));
@@ -777,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' => '')),
@@ -792,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(
@@ -858,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,
))
@@ -892,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,
))
@@ -902,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);
@@ -945,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) {
@@ -969,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') . ' ';
@@ -995,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);
}
+161 -11
View File
@@ -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.
@@ -359,7 +498,7 @@ class structure {
$slots = $DB->get_records_sql("
SELECT slot.id AS slotid, slot.slot, slot.questionid, slot.page, slot.maxmark,
q.*, qc.contextid
slot.requireprevious, q.*, qc.contextid
FROM {quiz_slots} slot
LEFT JOIN {question} q ON q.id = slot.questionid
LEFT JOIN {question_categories} qc ON qc.id = q.category
@@ -381,6 +520,7 @@ class structure {
$slot->page = $slotdata->page;
$slot->questionid = $slotdata->questionid;
$slot->maxmark = $slotdata->maxmark;
$slot->requireprevious = $slotdata->requireprevious;
$this->slots[$slot->id] = $slot;
$this->slotsinorder[$slot->slot] = $slot;
@@ -414,6 +554,7 @@ class structure {
$slot->name = get_string('missingquestion', 'quiz');
$slot->slot = $slot->slot;
$slot->maxmark = 0;
$slot->requireprevious = 0;
$slot->questiontext = ' ';
$slot->questiontextformat = FORMAT_HTML;
$slot->length = 1;
@@ -451,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;
}
}
@@ -658,6 +798,16 @@ class structure {
return true;
}
/**
* 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($slotid, $requireprevious) {
global $DB;
$DB->set_field('quiz_slots', 'requireprevious', $requireprevious, array('id' => $slotid));
}
/**
* Add/Remove a pagebreak.
*
+1
View File
@@ -60,6 +60,7 @@
<FIELD NAME="slot" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="Where this question comes in order in the list of questions in this quiz. Like question_attempts.slot."/>
<FIELD NAME="quizid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references quiz.id."/>
<FIELD NAME="page" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="The page number that this questions appears on. If the question in slot n appears on page p, then the question in slot n+1 must appear on page p or p+1. Well, except that when a quiz is being created, there may be empty pages, which would cause the page number to jump here."/>
<FIELD NAME="requireprevious" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Set to 1 when current question requires previous one to be answered first."/>
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Foreign key references question.id."/>
<FIELD NAME="maxmark" TYPE="number" LENGTH="12" NOTNULL="true" DEFAULT="0" SEQUENCE="false" DECIMALS="7" COMMENT="How many marks this question contributes to quiz.sumgrades."/>
</FIELDS>
+14 -1
View File
@@ -807,6 +807,19 @@ function xmldb_quiz_upgrade($oldversion) {
// Moodle v2.8.0 release upgrade line.
// Put any upgrade step following this.
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, XMLDB_NOTNULL, null, 0, 'page');
// Conditionally launch add field page.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Quiz savepoint reached.
upgrade_mod_savepoint(true, 2015022600, 'quiz');
}
return true;
}
+9 -3
View File
@@ -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;
+4
View File
@@ -615,6 +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['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: <br />{$a->quiznames}<br />The question will not be deleted from these quizzes but only from the category list.';
$string['questionmissing'] = 'Question for this session is missing';
$string['questionname'] = 'Question name';
+1 -2
View File
@@ -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++;
+46 -25
View File
@@ -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,6 +686,31 @@ table.quizreviewsummary td.cell {
margin: 0 2px;
}
#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 {
display: block;
min-height: 1.7em;
@@ -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."
+15 -1
View File
@@ -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);
@@ -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
+33 -1
View File
@@ -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));
}
}
+12
View File
@@ -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
+3 -3
View File
@@ -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;
@@ -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'
}
}
});
File diff suppressed because one or more lines are too long
@@ -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'
}
}
});
@@ -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'));
}
}
};
@@ -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('<span class="accesshide">'+this.CONSTANTS.QUESTION+"</span> "+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('<span class="accesshide">'+this.CONSTANTS.QUESTION+"</span> "+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"]});
@@ -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'));
}
}
};
+45 -15
View File
@@ -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',
-131
View File
@@ -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'
}
}
});
+1 -4
View File
@@ -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'
},
+65 -2
View File
@@ -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'));
}
}
};
+10
View File
@@ -84,6 +84,16 @@ abstract class question_behaviour {
return substr(get_class($this), 11);
}
/**
* Whether the current attempt at this question could be completed just by the
* student interacting with the question, before $qa->finish() is called.
*
* @return boolean whether the attempt can finish naturally.
*/
public function can_finish_during_attempt() {
return false;
}
/**
* Cause the question to be renderered. This gets the appropriate behaviour
* renderer using {@link get_renderer()}, and adjusts the display
@@ -46,6 +46,10 @@ class qbehaviour_immediatefeedback extends question_behaviour_with_save {
return $question instanceof question_automatically_gradable;
}
public function can_finish_during_attempt() {
return true;
}
public function get_min_fraction() {
return $this->question->get_min_fraction();
}
@@ -54,6 +54,10 @@ class qbehaviour_interactive extends question_behaviour_with_multiple_tries {
return $question instanceof question_automatically_gradable;
}
public function can_finish_during_attempt() {
return true;
}
public function get_right_answer_summary() {
return $this->question->get_right_answer_summary();
}
+19 -7
View File
@@ -1,14 +1,26 @@
This files describes API changes for question behaviour plugins.
=== 2.9 ===
1) New method question_behaviour::can_finish_during_attempt. This returns false
by default. You should override it if, with your behaviour, questions may
finish just through the student interacting with them (e.g. by clicking the
Check button within the question.)
=== 2.7 ===
1) question_behaviour_type has a new method allows_multiple_submitted_responses which defaults to false but should return
true if this question behaviour accepts multiple submissions of responses within one attempt eg. multiple tries for the
interactive or adaptive question behaviours.
question_behaviour has a new method step_has_a_submitted_response($step). For question behaviours where it is not only the
final response that is submitted by the student, you need to override this method to return true for other steps where a
student has submitted a response. See question_behaviour_with_multiple_tries::step_has_a_submitted_response($step) for
example. This method only needs to be overriden if you are returning true from allows_multiple_response_submissions
1) question_behaviour_type has a new method allows_multiple_submitted_responses
which defaults to false but should return true if this question behaviour
accepts multiple submissions of responses within one attempt eg. multiple
tries for the interactive or adaptive question behaviours.
question_behaviour has a new method step_has_a_submitted_response($step). For
question behaviours where it is not only the final response that is
submitted by the student, you need to override this method to return true
for other steps where a student has submitted a response. See
question_behaviour_with_multiple_tries::step_has_a_submitted_response($step)
for example. This method only needs to be overriden if you are returning
true from allows_multiple_response_submissions.
=== 2.6 ===
+10
View File
@@ -1163,6 +1163,16 @@ class question_attempt {
return $this->rightanswer;
}
/**
* Whether this attempt at this question could be completed just by the
* student interacting with the question, before {@link finish()} is called.
*
* @return boolean whether this attempt can finish naturally.
*/
public function can_finish_during_attempt() {
return $this->behaviour->can_finish_during_attempt();
}
/**
* Perform the action described by $submitteddata.
* @param array $submitteddata the submitted data the determines the action.
+11
View File
@@ -271,6 +271,17 @@ class question_usage_by_activity {
return $this->get_question_attempt($slot)->get_state_class($showcorrectness);
}
/**
* Whether this attempt at a given question could be completed just by the
* student interacting with the question, before {@link finish_question()} is called.
*
* @param int $slot the number used to identify this question within this usage.
* @return boolean whether the attempt at the given question can finish naturally.
*/
public function can_question_finish_during_attempt($slot) {
return $this->get_question_attempt($slot)->can_finish_during_attempt();
}
/**
* Get the time of the most recent action performed on a question.
* @param int $slot the number used to identify this question within this usage.