Merge branch 'MDL-43089' of git://github.com/mkassaei/moodle

This commit is contained in:
Dan Poltawski
2014-10-06 12:11:33 +01:00
114 changed files with 12239 additions and 2344 deletions
+3 -2
View File
@@ -73,7 +73,8 @@ $qcobject = new question_category_object(
null,
$contexts->having_cap('moodle/question:add'));
$mform = new quiz_add_random_form(new moodle_url('/mod/quiz/addrandom.php'), $contexts);
$mform = new quiz_add_random_form(new moodle_url('/mod/quiz/addrandom.php'),
array('contexts' => $contexts, 'cat' => $pagevars['cat']));
if ($mform->is_cancelled()) {
redirect($returnurl);
@@ -96,7 +97,7 @@ if ($data = $mform->get_data()) {
'It seems a form was submitted without any button being pressed???');
}
quiz_add_random_questions($quiz, $addonpage, $categoryid, 1, $includesubcategories);
quiz_add_random_questions($quiz, $addonpage, $categoryid, $data->numbertoadd, $includesubcategories);
quiz_delete_previews($quiz);
quiz_update_sumgrades($quiz);
redirect($returnurl);
+22 -3
View File
@@ -40,7 +40,7 @@ class quiz_add_random_form extends moodleform {
global $CFG, $DB;
$mform =& $this->_form;
$contexts = $this->_customdata;
$contexts = $this->_customdata['contexts'];
$usablecontexts = $contexts->having_cap('moodle/question:useall');
// Random from existing category section.
@@ -49,9 +49,13 @@ class quiz_add_random_form extends moodleform {
$mform->addElement('questioncategory', 'category', get_string('category'),
array('contexts' => $usablecontexts, 'top' => false));
$mform->setDefault('category', $this->_customdata['cat']);
$mform->addElement('checkbox', 'includesubcategories', '', get_string('recurse', 'quiz'));
$mform->addElement('select', 'numbertoadd', get_string('randomnumber', 'quiz'),
$this->get_number_of_questions_to_add_choices());
$mform->addElement('submit', 'existingcategory', get_string('addrandomquestion', 'quiz'));
// Random from a new category section.
@@ -68,7 +72,7 @@ class quiz_add_random_form extends moodleform {
$mform->addElement('submit', 'newcategory',
get_string('createcategoryandaddrandomquestion', 'quiz'));
// Submit buttons.
// Cancel button.
$mform->addElement('cancel');
$mform->closeHeaderBefore('cancel');
@@ -89,5 +93,20 @@ class quiz_add_random_form extends moodleform {
return $errors;
}
}
/**
* Return an arbitrary array for the dropdown menu
* @return array of integers array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
*/
private function get_number_of_questions_to_add_choices() {
$maxrand = 100;
$randomcount = array();
for ($i = 1; $i <= min(10, $maxrand); $i++) {
$randomcount[$i] = $i;
}
for ($i = 20; $i <= min(100, $maxrand); $i += 10) {
$randomcount[$i] = $i;
}
return $randomcount;
}
}
+12 -2
View File
@@ -97,7 +97,7 @@ class quiz {
* @param int $userid the the userid.
* @return quiz the new quiz object
*/
public static function create($quizid, $userid) {
public static function create($quizid, $userid = null) {
global $DB;
$quiz = quiz_access_manager::load_quiz_and_settings($quizid);
@@ -105,7 +105,9 @@ class quiz {
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id, false, MUST_EXIST);
// Update quiz with override information.
$quiz = quiz_update_effective_access($quiz, $userid);
if ($userid) {
$quiz = quiz_update_effective_access($quiz, $userid);
}
return new quiz($quiz, $cm, $course);
}
@@ -153,6 +155,14 @@ class quiz {
get_question_options($questionstoprocess);
}
/**
* Get an instance of the {@link \mod_quiz\structure} class for this quiz.
* @return \mod_quiz\structure describes the questions in the quiz.
*/
public function get_structure() {
return \mod_quiz\structure::create_for_quiz($this);
}
// Simple getters ==========================================================
/** @return int the course id. */
public function get_courseid() {
+2 -2
View File
@@ -15,7 +15,7 @@
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Admin settings class for the quiz review opitions.
* Admin settings class for the quiz review options.
*
* @package mod_quiz
* @copyright 2008 Tim Hunt
@@ -27,7 +27,7 @@ defined('MOODLE_INTERNAL') || die();
/**
* Admin settings class for the quiz review opitions.
* Admin settings class for the quiz review options.
*
* @copyright 2008 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+924
View File
@@ -0,0 +1,924 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Renderer outputting the quiz editing UI.
*
* @package mod_quiz
* @copyright 2013 The Open University.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz\output;
defined('MOODLE_INTERNAL') || die();
use \mod_quiz\structure;
use \html_writer;
/**
* Renderer outputting the quiz editing UI.
*
* @copyright 2013 The Open University.
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @since Moodle 2.7
*/
class edit_renderer extends \plugin_renderer_base {
/**
* Render the edit page
*
* @param \quiz $quizobj object containing all the quiz settings information.
* @param structure $structure object containing the structure of the quiz.
* @param \question_edit_contexts $contexts the relevant question bank contexts.
* @param \moodle_url $pageurl the canonical URL of this page.
* @param array $pagevars the variables from {@link question_edit_setup()}.
* @return string HTML to output.
*/
public function edit_page(\quiz $quizobj, structure $structure,
\question_edit_contexts $contexts, \moodle_url $pageurl, array $pagevars) {
$output = '';
// Page title.
$output .= $this->heading_with_help(get_string('editingquizx', 'quiz',
format_string($quizobj->get_quiz_name())), 'editingquiz', 'quiz', '',
get_string('basicideasofquiz', 'quiz'), 2);
// 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->repaginate_button($structure, $pageurl);
$output .= $this->total_marks($quizobj->get_quiz());
// Show the questions organised into sections and pages.
$output .= $this->start_section_list();
$sections = $structure->get_quiz_sections();
$lastsection = end($sections);
foreach ($sections as $section) {
$output .= $this->start_section($section);
$output .= $this->questions_in_section($structure, $section, $contexts, $pagevars, $pageurl);
if ($section === $lastsection) {
$output .= \html_writer::start_div('last-add-menu');
$output .= html_writer::tag('span', $this->add_menu_actions($structure, 0,
$pageurl, $contexts, $pagevars), array('class' => 'add-menu-outer'));
$output .= \html_writer::end_div();
}
$output .= $this->end_section();
}
$output .= $this->end_section_list();
// Inialise the JavaScript.
$this->initialise_editing_javascript($quizobj->get_course(), $quizobj->get_quiz());
// Include the contents of any other popups required.
if ($structure->can_be_edited()) {
$popups = '';
$popups .= $this->question_bank_loading();
$this->page->requires->yui_module('moodle-mod_quiz-quizquestionbank',
'M.mod_quiz.quizquestionbank.init',
array('class' => 'questionbank', 'cmid' => $structure->get_cmid()));
$popups .= $this->random_question_form($pageurl, $contexts, $pagevars);
$this->page->requires->yui_module('moodle-mod_quiz-randomquestion',
'M.mod_quiz.randomquestion.init');
$output .= html_writer::div($popups, 'mod_quiz_edit_forms');
// Include the question chooser.
$output .= $this->question_chooser();
$this->page->requires->yui_module('moodle-mod_quiz-questionchooser', 'M.mod_quiz.init_questionchooser');
}
return $output;
}
/**
* Render any warnings that might be required about the state of the quiz,
* e.g. if it has been attempted, or if the shuffle questions option is
* turned on.
*
* @param structure $structure the quiz structure.
* @return string HTML to output.
*/
public function quiz_state_warnings(structure $structure) {
$warnings = $structure->get_edit_page_warnings();
if (empty($warnings)) {
return '';
}
$output = array();
foreach ($warnings as $warning) {
$output[] = \html_writer::tag('p', $warning);
}
return $this->box(implode("\n", $output), 'statusdisplay');
}
/**
* Render the status bar.
*
* @param structure $structure the quiz structure.
* @return string HTML to output.
*/
public function quiz_information(structure $structure) {
list($currentstatus, $explanation) = $structure->get_dates_summary();
$output = html_writer::span(
get_string('numquestionsx', 'quiz', $structure->get_question_count()),
'numberofquestions') . ' | ' .
html_writer::span($currentstatus, 'quizopeningstatus',
array('title' => $explanation));
return html_writer::div($output, 'statusbar');
}
/**
* Render the form for setting a quiz' overall grade
*
* @param \stdClass $quiz the quiz settings from the database.
* @param \moodle_url $pageurl the canonical URL of this page.
* @return string HTML to output.
*/
public function maximum_grade_input($quiz, \moodle_url $pageurl) {
$output = '';
$output .= html_writer::start_div('maxgrade');
$output .= html_writer::start_tag('form', array('method' => 'post', 'action' => 'edit.php',
'class' => 'quizsavegradesform'));
$output .= html_writer::start_tag('fieldset', array('class' => 'invisiblefieldset'));
$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)));
$output .= html_writer::tag('label', get_string('maximumgradex', '', $a),
array('for' => 'inputmaxgrade'));
$output .= html_writer::empty_tag('input', array('type' => 'submit',
'name' => 'savechanges', 'value' => get_string('save', 'quiz')));
$output .= html_writer::end_tag('fieldset');
$output .= html_writer::end_tag('form');
$output .= html_writer::end_tag('div');
return $output;
}
/**
* Return the repaginate button
* @param structure $structure the structure of the quiz being edited.
* @param \moodle_url $pageurl the canonical URL of this page.
* @return string HTML to output.
*/
protected function repaginate_button(structure $structure, \moodle_url $pageurl) {
$header = html_writer::tag('span', get_string('repaginatecommand', 'quiz'), array('class' => 'repaginatecommand'));
$form = $this->repaginate_form($structure, $pageurl);
$containeroptions = array(
'class' => 'rpcontainerclass',
'cmid' => $structure->get_cmid(),
'header' => $header,
'form' => $form,
);
$buttonoptions = array(
'type' => 'submit',
'name' => 'repaginate',
'id' => 'repaginatecommand',
'value' => get_string('repaginatecommand', 'quiz'),
);
if (!$structure->can_be_repaginated()) {
$buttonoptions['disabled'] = 'disabled';
} else {
$this->page->requires->yui_module('moodle-mod_quiz-repaginate', 'M.mod_quiz.repaginate.init');
}
return html_writer::tag('div',
html_writer::empty_tag('input', $buttonoptions), $containeroptions);
}
/**
* Return the repaginate form
* @param structure $structure the structure of the quiz being edited.
* @param \moodle_url $pageurl the canonical URL of this page.
* @return string HTML to output.
*/
protected function repaginate_form(structure $structure, \moodle_url $pageurl) {
$perpage = array();
$perpage[0] = get_string('allinone', 'quiz');
for ($i = 1; $i <= 50; ++$i) {
$perpage[$i] = $i;
}
$hiddenurl = clone($pageurl);
$hiddenurl->param('sesskey', sesskey());
$select = html_writer::select($perpage, 'questionsperpage',
$structure->get_questions_per_page(), false);
$buttonattributes = array('type' => 'submit', 'name' => 'repaginate', 'value' => get_string('go'));
$formcontent = html_writer::tag('form', html_writer::div(
html_writer::input_hidden_params($hiddenurl) .
get_string('repaginate', 'quiz', $select) .
html_writer::empty_tag('input', $buttonattributes)
), array('action' => 'edit.php', 'method' => 'post'));
return html_writer::div($formcontent, '', array('id' => 'repaginatedialog'));
}
/**
* Render the total marks available for the quiz.
*
* @param \stdClass $quiz the quiz settings from the database.
* @return string HTML to output.
*/
public function total_marks($quiz) {
$totalmark = html_writer::span(quiz_format_grade($quiz, $quiz->sumgrades), 'mod_quiz_summarks');
return html_writer::tag('span',
get_string('totalmarksx', 'quiz', $totalmark),
array('class' => 'totalpoints'));
}
/**
* Generate the starting container html for the start of a list of sections
* @return string HTML to output.
*/
protected function start_section_list() {
return html_writer::start_tag('ul', array('class' => 'slots'));
}
/**
* Generate the closing container html for the end of a list of sections
* @return string HTML to output.
*/
protected function end_section_list() {
return html_writer::end_tag('ul');
}
/**
* Display the start of a section, before the questions.
*
* @param \stdClass $section The quiz_section entry from DB
* @return string HTML to output.
*/
protected function start_section($section) {
$output = '';
$sectionstyle = '';
$output .= html_writer::start_tag('li', array('id' => 'section-'.$section->id,
'class' => 'section main clearfix'.$sectionstyle, 'role' => 'region',
'aria-label' => $section->heading));
$leftcontent = $this->section_left_content($section);
$output .= html_writer::div($leftcontent, 'left side');
$rightcontent = $this->section_right_content($section);
$output .= html_writer::div($rightcontent, 'right side');
$output .= html_writer::start_div('content');
return $output;
}
/**
* Display the end of a section, after the questions.
*
* @return string HTML to output.
*/
protected function end_section() {
$output = html_writer::end_tag('div');
$output .= html_writer::end_tag('li');
return $output;
}
/**
* Generate the content to be displayed on the left part of a section.
*
* @param \stdClass $section The quiz_section entry from DB
* @return string HTML to output.
*/
protected function section_left_content($section) {
return $this->output->spacer();
}
/**
* Generate the content to displayed on the right part of a section.
*
* @param \stdClass $section The quiz_section entry from DB
* @return string HTML to output.
*/
protected function section_right_content($section) {
return $this->output->spacer();
}
/**
* Renders HTML to display the questions in a section of the quiz.
*
* This function calls {@link core_course_renderer::quiz_section_question()}
*
* @param structure $structure object containing the structure of the quiz.
* @param \stdClass $section information about the section.
* @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 questions_in_section(structure $structure, $section,
$contexts, $pagevars, $pageurl) {
$output = '';
foreach ($structure->get_questions_in_section($section->id) as $question) {
$output .= $this->question_row($structure, $question, $contexts, $pagevars, $pageurl);
}
return html_writer::tag('ul', $output, array('class' => 'section img-text'));
}
/**
* 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 \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) {
$output = '';
// Put page in a span for easier styling.
$page = html_writer::tag('span', get_string('page') . ' ' . $question->page,
array('class' => 'text'));
if ($structure->is_first_slot_on_page($question->slot)) {
// Add the add-menu at the page level.
$addmenu = html_writer::tag('span', $this->add_menu_actions($structure,
$question->page, $pageurl, $contexts, $pagevars),
array('class' => 'add-menu-outer'));
$addquestionform = $this->add_question_form($structure,
$question->page, $pageurl, $pagevars);
$output .= html_writer::tag('li', $page . $addmenu . $addquestionform,
array('class' => 'pagenumber activity yui3-dd-drop page', 'id' => 'page-' . $question->page));
}
// Page split/join icon.
$joinhtml = '';
if (!$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));
}
// Question HTML.
$questionhtml = $this->question($structure, $question, $pageurl);
$questionclasses = 'activity ' . $question->qtype . ' qtype_' . $question->qtype . ' slot';
$output .= html_writer::tag('li', $questionhtml . $joinhtml,
array('class' => $questionclasses, 'id' => 'slot-' . $question->slotid));
return $output;
}
/**
* Returns the add menu that is output once per page.
* @param structure $structure object containing the structure of the quiz.
* @param int $page the page number that this menu will add to.
* @param \moodle_url $pageurl the canonical URL of this page.
* @param \question_edit_contexts $contexts the relevant question bank contexts.
* @param array $pagevars the variables from {@link \question_edit_setup()}.
* @return string HTML to output.
*/
public function add_menu_actions(structure $structure, $page, \moodle_url $pageurl,
\question_edit_contexts $contexts, array $pagevars) {
$actions = $this->edit_menu_actions($structure, $page, $pageurl, $pagevars);
if (empty($actions)) {
return '';
}
$menu = new \action_menu();
$menu->set_alignment(\action_menu::TR, \action_menu::BR);
$trigger = html_writer::tag('span', get_string('add', 'quiz'), array('class' => 'add-menu'));
$menu->set_menu_trigger($trigger);
// Disable the link if quiz has attempts.
if (!$structure->can_be_edited()) {
return $this->render($menu);
}
foreach ($actions as $action) {
if ($action instanceof \action_menu_link) {
$action->add_class('add-menu');
}
$menu->add($action);
}
$menu->attributes['class'] .= ' page-add-actions commands';
// Prioritise the menu ahead of all other actions.
$menu->prioritise = true;
return $this->render($menu);
}
/**
* Returns the list of actions to go in the add menu.
* @param structure $structure object containing the structure of the quiz.
* @param int $page the page number that this menu will add to.
* @param \moodle_url $pageurl the canonical URL of this page.
* @param array $pagevars the variables from {@link \question_edit_setup()}.
* @return array the actions.
*/
public function edit_menu_actions(structure $structure, $page,
\moodle_url $pageurl, array $pagevars) {
$questioncategoryid = question_get_category_id_from_pagevars($pagevars);
static $str;
if (!isset($str)) {
$str = get_strings(array('addaquestion', 'addarandomquestion',
'addarandomselectedquestion', 'questionbank'), 'quiz');
}
// Get section, page, slotnumber and maxmark.
$actions = array();
// Add a new question to the quiz.
$returnurl = new \moodle_url($pageurl, array('addonpage' => $page));
$params = array('returnurl' => $returnurl->out_as_local_url(false),
'cmid' => $structure->get_cmid(), 'category' => $questioncategoryid,
'addonpage' => $page, 'appendqnumstring' => 'addquestion');
$actions['addaquestion'] = new \action_menu_link_secondary(
new \moodle_url('/question/addquestion.php', $params),
new \pix_icon('t/add', $str->addaquestion, 'moodle', array('class' => 'iconsmall', 'title' => '')),
$str->addaquestion, array('class' => 'cm-edit-action addquestion', 'data-action' => 'addquestion')
);
// Call question bank.
$icon = new \pix_icon('t/add', $str->questionbank, 'moodle', array('class' => 'iconsmall', 'title' => ''));
$title = get_string('addquestionfrombanktopage', 'quiz', $page);
$attributes = array('class' => 'cm-edit-action questionbank',
'data-header' => $title, 'data-action' => 'questionbank', 'data-addonpage' => $page);
$actions['questionbank'] = new \action_menu_link_secondary($pageurl, $icon, $str->questionbank, $attributes);
// Add a random question.
$returnurl = new \moodle_url('/mod/quiz/edit.php', array('cmid' => $structure->get_cmid(), 'data-addonpage' => $page));
$params = array('returnurl' => $returnurl, 'cmid' => $structure->get_cmid(), 'appendqnumstring' => 'addarandomquestion');
$url = new \moodle_url('/mod/quiz/addrandom.php', $params);
$icon = new \pix_icon('t/add', $str->addarandomquestion, 'moodle', array('class' => 'iconsmall', 'title' => ''));
$attributes = array('class' => 'cm-edit-action addarandomquestion', 'data-action' => 'addarandomquestion');
$title = get_string('addrandomquestiontopage', 'quiz', $page);
$attributes = array_merge(array('data-header' => $title, 'data-addonpage' => $page), $attributes);
$actions['addarandomquestion'] = new \action_menu_link_secondary($url, $icon, $str->addarandomquestion, $attributes);
return $actions;
}
/**
* Render the form that contains the data for adding a new question to the quiz.
*
* @param structure $structure object containing the structure of the quiz.
* @param int $page the page number that this menu will add to.
* @param \moodle_url $pageurl the canonical URL of this page.
* @param array $pagevars the variables from {@link \question_edit_setup()}.
* @return string HTML to output.
*/
protected function add_question_form(structure $structure, $page, \moodle_url $pageurl, array $pagevars) {
$questioncategoryid = question_get_category_id_from_pagevars($pagevars);
$output = html_writer::tag('input', null,
array('type' => 'hidden', 'name' => 'returnurl',
'value' => $pageurl->out_as_local_url(false, array('addonpage' => $page))));
$output .= html_writer::tag('input', null,
array('type' => 'hidden', 'name' => 'cmid', 'value' => $structure->get_cmid()));
$output .= html_writer::tag('input', null,
array('type' => 'hidden', 'name' => 'appendqnumstring', 'value' => 'addquestion'));
$output .= html_writer::tag('input', null,
array('type' => 'hidden', 'name' => 'category', 'value' => $questioncategoryid));
return html_writer::tag('form', html_writer::div($output),
array('class' => 'addnewquestion', 'method' => 'post',
'action' => new \moodle_url('/question/addquestion.php')));
}
/**
* 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 \moodle_url $pageurl the canonical URL of this page.
* @return string HTML to output.
*/
public function question(structure $structure, $question, \moodle_url $pageurl) {
$output = '';
$output .= html_writer::start_tag('div');
if ($structure->can_be_edited()) {
$output .= $this->question_move_icon($question);
}
$output .= html_writer::start_div('mod-indent-outer');
$output .= $this->question_number($question->displayednumber);
// 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);
} else {
$questionname = $this->question_name($structure, $question, $pageurl);
}
// Start the div for the activity title, excluding the edit icons.
$output .= html_writer::start_div('activityinstance');
$output .= $questionname;
// Closing the tag which contains everything but edit icons. Content part of the module should not be part of this.
$output .= html_writer::end_tag('div'); // .activityinstance.
// Action icons.
$questionicons = '';
$questionicons .= $this->question_preview_icon($structure->get_quiz(), $question);
if ($structure->can_be_edited()) {
$questionicons .= $this->question_remove_icon($question, $pageurl);
}
$questionicons .= $this->marked_out_of_field($structure->get_quiz(), $question);
$output .= html_writer::span($questionicons, 'actions'); // Required to add js spinner icon.
// End of indentation div.
$output .= html_writer::end_tag('div');
$output .= html_writer::end_tag('div');
return $output;
}
/**
* 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.
*/
public function question_move_icon($question) {
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')
);
}
/**
* Output the question number.
* @param string $number The number, or 'i'.
* @return string HTML to output.
*/
public function question_number($number) {
if (is_numeric($number)) {
$number = html_writer::span(get_string('question'), 'accesshide') .
' ' . $number;
}
return html_writer::tag('span', $number, array('class' => 'slotnumber'));
}
/**
* Render the preview icon.
*
* @param \stdClass $quiz the quiz settings from the database.
* @param \stdClass $question data from the question and quiz_slots tables.
* @param bool $label if true, show the preview question label after the icon
* @return string HTML to output.
*/
public function question_preview_icon($quiz, $question, $label = null) {
$url = quiz_question_preview_url($quiz, $question);
// Do we want a label?
$strpreviewlabel = '';
if ($label) {
$strpreviewlabel = ' ' . get_string('preview', 'quiz');
}
// Build the icon.
$strpreviewquestion = get_string('previewquestion', 'quiz');
$image = $this->pix_icon('t/preview', $strpreviewquestion);
$action = new \popup_action('click', $url, 'questionpreview',
question_preview_popup_params());
return $this->action_link($url, $image . $strpreviewlabel, $action,
array('title' => $strpreviewquestion, 'class' => 'preview'));
}
/**
* Render an icon to remove a question from the quiz.
*
* @param object $question The module to produce a move button for.
* @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));
$strdelete = get_string('delete');
$image = $this->pix_icon('t/delete', $strdelete);
return $this->action_link($url, $image, null, array('title' => $strdelete,
'class' => 'cm-edit-action editing_delete', 'data-action' => 'delete'));
}
/**
* 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 string $insertpagebreak if true, show an insert page break icon.
* Else show a join pages icon.
* @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()));
if ($insertpagebreak) {
$title = get_string('splitpages', 'quiz');
$image = $this->pix_icon('e/insert_page_break', $title);
$action = 'unlinkpage';
} else {
$title = get_string('joinpages', 'quiz');
$image = $this->pix_icon('e/remove_page_break', $title);
$action = 'linkpage';
}
// Disable the link if quiz has attempts.
$disabled = null;
if (quiz_has_attempts($quiz->id)) {
$disabled = "disabled";
}
return html_writer::span($this->action_link($url, $image, null, array('title' => $title,
'class' => 'page_split_join', 'disabled' => $disabled, 'data-action' => $action)),
'page_split_join_wrapper');
}
/**
* Renders html to display a name with the link to the question on a quiz edit page
*
* If the user does not have permission to edi the question, it is rendered
* 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 \moodle_url $pageurl the canonical URL of this page.
* @return string HTML to output.
*/
public function question_name(structure $structure, $question, $pageurl) {
$output = '';
$editurl = new \moodle_url('/question/question.php', array(
'returnurl' => $pageurl->out_as_local_url(),
'cmid' => $structure->get_cmid(), 'id' => $question->id));
$instancename = quiz_question_tostring($question);
$qtype = \question_bank::get_qtype($question->qtype, false);
$namestr = $qtype->local_name();
$icon = $this->pix_icon('icon', $namestr, $qtype->plugin_name(), array('title' => $namestr,
'class' => 'icon activityicon', 'alt' => ' ', 'role' => 'presentation'));
$editicon = $this->pix_icon('t/edit', '', 'moodle', array('title' => ''));
// Need plain question name without html tags for link title.
$title = shorten_text(format_string($question->name), 100);
// Display the link itself.
$activitylink = $icon . html_writer::tag('span', $editicon . $instancename, array('class' => 'instancename'));
$output .= html_writer::link($editurl, $activitylink,
array('title' => get_string('editquestion', 'quiz').' '.$title));
return $output;
}
/**
* Renders html to display a random question the link to edit the configuration
* 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 \moodle_url $pageurl the canonical URL of this page.
* @return string HTML to output.
*/
public function random_question(structure $structure, $question, $pageurl) {
$editurl = new \moodle_url('/question/question.php', array(
'returnurl' => $pageurl->out_as_local_url(),
'cmid' => $structure->get_cmid(), 'id' => $question->id));
$temp = clone($question);
$temp->questiontext = '';
$instancename = quiz_question_tostring($temp);
$configuretitle = get_string('configurerandomquestion', 'quiz');
$qtype = \question_bank::get_qtype($question->qtype, false);
$namestr = $qtype->local_name();
$icon = $this->pix_icon('icon', $namestr, $qtype->plugin_name(), array('title' => $namestr,
'class' => 'icon activityicon', 'alt' => ' ', 'role' => 'presentation'));
$editicon = $this->pix_icon('t/edit', $configuretitle, 'moodle', array('title' => ''));
// If this is a random question, display a link to show the questions
// selected from in the question bank.
$qbankurl = new \moodle_url('/question/edit.php', array(
'cmid' => $structure->get_cmid(),
'cat' => $question->category . ',' . $question->contextid,
'recurse' => !empty($question->questiontext)));
$qbanklink = ' ' . \html_writer::link($qbankurl,
get_string('seequestions', 'quiz'), array('class' => 'mod_quiz_random_qbank_link'));
return html_writer::link($editurl, $icon . $editicon, array('title' => $configuretitle)) .
' ' . $instancename . ' ' . $qbanklink;
}
/**
* 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.
* @return string HTML to output.
*/
public function marked_out_of_field($quiz, $question) {
$output = html_writer::span(quiz_format_question_grade($quiz, $question->maxmark),
'instancemaxmark decimalplaces_' . quiz_get_grade_format($quiz),
array('title' => get_string('maxmark', 'quiz')));
$output .= html_writer::span(
html_writer::link(
new \moodle_url('#'),
$this->pix_icon('t/editstring', '', 'moodle', array('class' => 'smallicon visibleifjs', 'title' => '')),
array(
'class' => 'editing_maxmark',
'data-action' => 'editmaxmark',
'title' => get_string('editmaxmark', 'quiz'),
)
)
);
return html_writer::span($output, 'instancemaxmarkcontainer');
}
/**
* Render the question type chooser dialogue.
* @return string HTML to output.
*/
public function question_chooser() {
$container = html_writer::div(print_choose_qtype_to_add_form(array(), null, false), '',
array('id' => 'qtypechoicecontainer'));
return html_writer::div($container, 'createnewquestion');
}
/**
* Render the contents of the question bank pop-up in its initial state,
* when it just contains a loading progress indicator.
* @return string HTML to output.
*/
public function question_bank_loading() {
return html_writer::div(html_writer::empty_tag('img',
array('alt' => 'loading', 'class' => 'loading-icon', 'src' => $this->pix_url('i/loading'))),
'questionbankloading');
}
/**
* Return random question form.
* @param \moodle_url $thispageurl the canonical URL of this page.
* @param \question_edit_contexts $contexts the relevant question bank contexts.
* @param array $pagevars the variables from {@link \question_edit_setup()}.
* @return string HTML to output.
*/
protected function random_question_form(\moodle_url $thispageurl, \question_edit_contexts $contexts, array $pagevars) {
if (!$contexts->have_cap('moodle/question:useall')) {
return '';
}
$randomform = new \quiz_add_random_form(new \moodle_url('/mod/quiz/addrandom.php'),
array('contexts' => $contexts, 'cat' => $pagevars['cat']));
$randomform->set_data(array(
'category' => $pagevars['cat'],
'returnurl' => $thispageurl->out_as_local_url(true),
'randomnumber' => 1,
'cmid' => $thispageurl->param('cmid'),
));
return html_writer::div($randomform->render(), 'randomquestionformforpopup');
}
/**
* 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.
* @return bool Always returns true
*/
protected function initialise_editing_javascript($course, $quiz) {
$config = new \stdClass();
$config->resourceurl = '/mod/quiz/edit_rest.php';
$config->sectionurl = '/mod/quiz/edit_rest.php';
$config->pageparams = array();
$config->questiondecimalpoints = $quiz->questiondecimalpoints;
// Include toolboxes.
$this->page->requires->yui_module('moodle-mod_quiz-toolboxes',
'M.mod_quiz.init_resource_toolbox',
array(array(
'courseid' => $course->id,
'quizid' => $quiz->id,
'ajaxurl' => $config->resourceurl,
'config' => $config,
))
);
$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,
'ajaxurl' => $config->sectionurl,
'config' => $config,
))
);
// Include course dragdrop.
$this->page->requires->yui_module('moodle-mod_quiz-dragdrop', 'M.mod_quiz.init_section_dragdrop',
array(array(
'courseid' => $course->id,
'quizid' => $quiz->id,
'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,
'ajaxurl' => $config->resourceurl,
'config' => $config,
)), null, true);
// Require various strings for the command toolbox.
$this->page->requires->strings_for_js(array(
'moveleft',
'deletechecktype',
'deletechecktypename',
'edittitle',
'edittitleinstructions',
'show',
'hide',
'clicktochangeinbrackets',
'markthistopic',
'markedthistopic',
'move',
'movesection',
'movecontent',
'selectall',
'tocontent',
'emptydragdropregion'
), 'moodle');
$this->page->requires->strings_for_js(array(
'confirmremovequestion',
'dragtoafter',
'dragtostart',
), 'quiz');
foreach (\question_bank::get_all_qtypes() as $qtype => $notused) {
$this->page->requires->string_for_js('pluginname', 'qtype_' . $qtype);
}
return true;
}
/**
* Return the contents of the question bank, to be displayed in the question-bank pop-up.
*
* @param \quiz_question_bank_view $questionbank the question bank view object.
* @param array $pagevars the variables from {@link \question_edit_setup()}.
* @return string HTML to output / send back in response to an AJAX request.
*/
public function question_bank_contents(\quiz_question_bank_view $questionbank, array $pagevars) {
$qbank = $questionbank->render('editq', $pagevars['qpage'], $pagevars['qperpage'],
$pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'], $pagevars['qbshowtext']);
return html_writer::div(html_writer::div($qbank, 'bd'), 'questionbankformforpopup');
}
}
+234
View File
@@ -0,0 +1,234 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines the quiz repaginate class.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz;
defined('MOODLE_INTERNAL') || die();
/**
* The repaginate class will rearrange questions in pages.
*
* The quiz setting allows users to write quizzes with one question per page,
* n questions per page, or all questions on one page.
*
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class repaginate {
/** @var int means join pages. */
const LINK = 1;
/** @var int means split pages. */
const UNLINK = 2;
/** @var int the id of the quiz being manipulated. */
private $quizid;
/** @var array the quiz_slots for that quiz. */
private $slots;
/**
* Constructor.
* @param int $quizid the id of the quiz being manipulated.
* @param stdClass[] $slots the quiz_slots for that quiz.
*/
public function __construct($quizid = 0, $slots = null) {
global $DB;
$this->quizid = $quizid;
if (!$this->quizid) {
$this->slots = array();
}
if (!$slots) {
$this->slots = $DB->get_records('quiz_slots', array('quizid' => $this->quizid), 'slot');
} else {
$this->slots = $slots;
}
}
/**
* Repaginate a given slot with the given pagenumber.
* @param stdClass $slot
* @param int $newpagenumber
* @return stdClass
*/
protected function repaginate_this_slot($slot, $newpagenumber) {
$newslot = clone($slot);
$newslot->page = $newpagenumber;
return $newslot;
}
/**
* Return current slot object.
* @param array $slots
* @param int $slotnumber
* @return stdClass $slot
*/
protected function get_this_slot($slots, $slotnumber) {
foreach ($slots as $key => $slot) {
if ($slot->slot == $slotnumber) {
return $slot;
}
}
return null;
}
/**
* Return array of slots with slot number as key
* @param stdClass[] $slots
* @return stdClass[]
*/
protected function get_slots_by_slot_number($slots) {
if (!$slots) {
return array();
}
$newslots = array();
foreach ($slots as $slot) {
$newslots[$slot->slot] = $slot;
}
return $newslots;
}
/**
* Return array of slots with slot id as key
* @param stdClass[] $slots
* @return stdClass[]
*/
protected function get_slots_by_slotid($slots) {
if (!$slots) {
return array();
}
$newslots = array();
foreach ($slots as $slot) {
$newslots[$slot->id] = $slot;
}
return $newslots;
}
/**
* Repaginate, update DB and slots object
* @param int $nextslotnumber
* @param int $type repaginate::LINK or repaginate::UNLINK.
*/
public function repaginate_slots($nextslotnumber, $type) {
global $DB;
$this->slots = $DB->get_records('quiz_slots', array('quizid' => $this->quizid), 'slot');
$nextslot = null;
$newslots = array();
foreach ($this->slots as $slot) {
if ($slot->slot < $nextslotnumber) {
$newslots[$slot->id] = $slot;
} else if ($slot->slot == $nextslotnumber) {
$nextslot = $this->repaginate_next_slot($nextslotnumber, $type);
// Update DB.
$DB->update_record('quiz_slots', $nextslot, true);
// Update returning object.
$newslots[$slot->id] = $nextslot;
}
}
if ($nextslot) {
$newslots = array_merge($newslots, $this->repaginate_the_rest($this->slots, $nextslotnumber, $type));
$this->slots = $this->get_slots_by_slotid($newslots);
}
}
/**
* Repaginate next slot and return the modified slot object
* @param int $nextslotnumber
* @param int $type repaginate::LINK or repaginate::UNLINK.
* @return stdClass|null
*/
public function repaginate_next_slot($nextslotnumber, $type) {
$currentslotnumber = $nextslotnumber - 1;
if (!($currentslotnumber && $nextslotnumber)) {
return null;
}
$currentslot = $this->get_this_slot($this->slots, $currentslotnumber);
$nextslot = $this->get_this_slot($this->slots, $nextslotnumber);
if ($type === self::LINK) {
return $this->repaginate_this_slot($nextslot, $currentslot->page);
} else if ($type === self::UNLINK) {
return $this->repaginate_this_slot($nextslot, $nextslot->page + 1);
}
return null;
}
/**
* Return the slots with the new pagination, regardless of current pagination.
* @param stdClass[] $slots the slots to repaginate.
* @param int $number number of question per page
* @return stdClass[] the updated slots.
*/
public function repaginate_n_question_per_page($slots, $number) {
$slots = $this->get_slots_by_slot_number($slots);
$newslots = array();
$count = 0;
$page = 1;
foreach ($slots as $key => $slot) {
for ($page + $count; $page < ($number + $count + 1); $page++) {
if ($slot->slot >= $page) {
$slot->page = $page;
$count++;
}
}
$newslots[$slot->id] = $slot;
}
return $newslots;
}
/**
* Repaginate the rest.
* @param stdClass[] $quizslots
* @param int $slotfrom
* @param int $type
* @param bool $dbupdate
* @return stdClass[]
*/
public function repaginate_the_rest($quizslots, $slotfrom, $type, $dbupdate = true) {
global $DB;
if (!$quizslots) {
return null;
}
$newslots = array();
foreach ($quizslots as $slot) {
if ($type == self::LINK) {
if ($slot->slot <= $slotfrom) {
continue;
}
$slot->page = $slot->page - 1;
} else if ($type == self::UNLINK) {
if ($slot->slot <= $slotfrom - 1) {
continue;
}
$slot->page = $slot->page + 1;
}
// Update DB.
if ($dbupdate) {
$DB->update_record('quiz_slots', $slot);
}
$newslots[$slot->id] = $slot;
}
return $newslots;
}
}
+671
View File
@@ -0,0 +1,671 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Defines the \mod_quiz\structure class.
*
* @package mod_quiz
* @copyright 2013 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_quiz;
defined('MOODLE_INTERNAL') || die();
/**
* Quiz structure class.
*
* The structure of the quiz. That is, which questions it is built up
* from. This is used on the Edit quiz page (edit.php) and also when
* starting an attempt at the quiz (startattempt.php). Once an attempt
* has been started, then the attempt holds the specific set of questions
* that that student should answer, and we no longer use this class.
*
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class structure {
/** @var \quiz the quiz this is the structure of. */
protected $quizobj = null;
/**
* @var \stdClass[] the questions in this quiz. Contains the row from the questions
* table, with the data from the quiz_slots table added, and also question_categories.contextid.
*/
protected $questions = array();
/** @var \stdClass[] quiz_slots.id => the quiz_slots rows for this quiz, agumented by sectionid. */
protected $slots = array();
/** @var \stdClass[] quiz_slots.slot => the quiz_slots rows for this quiz, agumented by sectionid. */
protected $slotsinorder = array();
/**
* @var \stdClass[] currently a dummy. Holds data that will match the
* quiz_sections, once it exists.
*/
protected $sections = array();
/** @var bool caches the results of can_be_edited. */
protected $canbeedited = null;
/**
* Create an instance of this class representing an empty quiz.
* @return structure
*/
public static function create() {
return new self();
}
/**
* Create an instance of this class representing the structure of a given quiz.
* @param \stdClass $quiz the quiz settings.
* @return structure
*/
public static function create_for($quiz) {
$structure = self::create();
$structure->populate_structure($quiz);
return $structure;
}
/**
* Create an instance of this class representing the structure of a given quiz.
* @param \quiz $quizobj the quiz.
* @return structure
*/
public static function create_for_quiz($quizobj) {
$structure = self::create_for($quizobj->get_quiz());
$structure->quizobj = $quizobj;
return $structure;
}
/**
* Whether there are any questions in the quiz.
* @return bool true if there is at least one question in the quiz.
*/
public function has_questions() {
return !empty($this->questions);
}
/**
* Get the number of questions in the quiz.
* @return int the number of questions in the quiz.
*/
public function get_question_count() {
return count($this->questions);
}
/**
* Get the information about the question with this id.
* @param int $questionid The question id.
* @return \stdClass the data from the questions table, augmented with
* question_category.contextid, and the quiz_slots data for the question in this quiz.
*/
public function get_question_by_id($questionid) {
return $this->questions[$questionid];
}
/**
* Get the information about the question in a given slot.
* @param int $slotnumber the index of the slot in question.
* @return \stdClass the data from the questions table, augmented with
* question_category.contextid, and the quiz_slots data for the question in this quiz.
*/
public function get_question_in_slot($slotnumber) {
return $this->questions[$this->slotsinorder[$slotnumber]->questionid];
}
/**
* Get the course module id of the quiz.
* @return int the course_modules.id for the quiz.
*/
public function get_cmid() {
return $this->quizobj->get_cmid();
}
/**
* Get id of the quiz.
* @return int the quiz.id for the quiz.
*/
public function get_quizid() {
return $this->quizobj->get_quizid();
}
/**
* Get the quiz object.
* @return \stdClass the quiz settings row from the database.
*/
public function get_quiz() {
return $this->quizobj->get_quiz();
}
/**
* Whether the question in the quiz are shuffled for each attempt.
* @return bool true if the questions are shuffled.
*/
public function is_shuffled() {
return $this->quizobj->get_quiz()->shufflequestions;
}
/**
* Quizzes can only be repaginated if they have not been attempted, the
* questions are not shuffled, and there are two or more questions.
* @return bool whether this quiz can be repaginated.
*/
public function can_be_repaginated() {
return !$this->is_shuffled() && $this->can_be_edited()
&& $this->get_question_count() >= 2;
}
/**
* Quizzes can only be edited if they have not been attempted.
* @return bool whether the quiz can be edited.
*/
public function can_be_edited() {
if ($this->canbeedited === null) {
$this->canbeedited = !quiz_has_attempts($this->quizobj->get_quizid());
}
return $this->canbeedited;
}
/**
* How many questions are allowed per page in the quiz.
* This setting controls how frequently extra page-breaks should be inserted
* automatically when questions are added to the quiz.
* @return int the number of questions that should be on each page of the
* quiz by default.
*/
public function get_questions_per_page() {
return $this->quizobj->get_quiz()->questionsperpage;
}
/**
* Get quiz slots.
* @return \stdClass[] the slots in this quiz.
*/
public function get_slots() {
return $this->slots;
}
/**
* Is this slot the first one on its page?
* @param int $slotnumber the index of the slot in question.
* @return bool whether this slot the first one on its page.
*/
public function is_first_slot_on_page($slotnumber) {
if ($slotnumber == 1) {
return true;
}
return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber - 1]->page;
}
/**
* Is this slot the last one on its page?
* @param int $slotnumber the index of the slot in question.
* @return bool whether this slot the last one on its page.
*/
public function is_last_slot_on_page($slotnumber) {
if (!isset($this->slotsinorder[$slotnumber + 1])) {
return true;
}
return $this->slotsinorder[$slotnumber]->page != $this->slotsinorder[$slotnumber + 1]->page;
}
/**
* Is this slot the last one in the quiz?
* @param int $slotnumber the index of the slot in question.
* @return bool whether this slot the last one in the quiz.
*/
public function is_last_slot_in_quiz($slotnumber) {
end($this->slotsinorder);
return $slotnumber == key($this->slotsinorder);
}
/**
* Get the final slot in the quiz.
* @return \stdClass the quiz_slots for for the final slot in the quiz.
*/
public function get_last_slot() {
return end($this->slotsinorder);
}
/**
* Get a slot by it's id. Throws an exception if it is missing.
* @param int $slotid the slot id.
* @return \stdClass the requested quiz_slots row.
*/
public function get_slot_by_id($slotid) {
if (!array_key_exists($slotid, $this->slots)) {
throw new \coding_exception('The \'slotid\' could not be found.');
}
return $this->slots[$slotid];
}
/**
* Get all the questions in a section of the quiz.
* @param int $sectionid the section id.
* @return \stdClass[] of question/slot objects.
*/
public function get_questions_in_section($sectionid) {
$questions = array();
foreach ($this->slotsinorder as $slot) {
if ($slot->sectionid == $sectionid) {
$questions[] = $this->questions[$slot->questionid];
}
}
return $questions;
}
/**
* Get all the sections of the quiz.
* @return \stdClass[] the sections in this quiz.
*/
public function get_quiz_sections() {
return $this->sections;
}
/**
* Get any warnings to show at the top of the edit page.
* @return string[] array of strings.
*/
public function get_edit_page_warnings() {
$warnings = array();
if (quiz_has_attempts($this->quizobj->get_quizid())) {
$reviewlink = quiz_attempt_summary_link_to_reports($this->quizobj->get_quiz(),
$this->quizobj->get_cm(), $this->quizobj->get_context());
$warnings[] = get_string('cannoteditafterattempts', 'quiz', $reviewlink);
}
if ($this->is_shuffled()) {
$updateurl = new \moodle_url('/course/mod.php',
array('return' => 'true', 'update' => $this->quizobj->get_cmid(), 'sesskey' => sesskey()));
$updatelink = '<a href="'.$updateurl->out().'">' . get_string('updatethis', '',
get_string('modulename', 'quiz')) . '</a>';
$warnings[] = get_string('shufflequestionsselected', 'quiz', $updatelink);
}
return $warnings;
}
/**
* Get the date information about the current state of the quiz.
* @return string[] array of two strings. First a short summary, then a longer
* explanation of the current state, e.g. for a tool-tip.
*/
public function get_dates_summary() {
$timenow = time();
$quiz = $this->quizobj->get_quiz();
// Exact open and close dates for the tool-tip.
$dates = array();
if ($quiz->timeopen > 0) {
if ($timenow > $quiz->timeopen) {
$dates[] = get_string('quizopenedon', 'quiz', userdate($quiz->timeopen));
} else {
$dates[] = get_string('quizwillopen', 'quiz', userdate($quiz->timeopen));
}
}
if ($quiz->timeclose > 0) {
if ($timenow > $quiz->timeclose) {
$dates[] = get_string('quizclosed', 'quiz', userdate($quiz->timeclose));
} else {
$dates[] = get_string('quizcloseson', 'quiz', userdate($quiz->timeclose));
}
}
if (empty($dates)) {
$dates[] = get_string('alwaysavailable', 'quiz');
}
$explanation = implode(', ', $dates);
// Brief summary on the page.
if ($timenow < $quiz->timeopen) {
$currentstatus = get_string('quizisclosedwillopen', 'quiz',
userdate($quiz->timeopen, get_string('strftimedatetimeshort', 'langconfig')));
} else if ($quiz->timeclose && $timenow <= $quiz->timeclose) {
$currentstatus = get_string('quizisopenwillclose', 'quiz',
userdate($quiz->timeclose, get_string('strftimedatetimeshort', 'langconfig')));
} else if ($quiz->timeclose && $timenow > $quiz->timeclose) {
$currentstatus = get_string('quizisclosed', 'quiz');
} else {
$currentstatus = get_string('quizisopen', 'quiz');
}
return array($currentstatus, $explanation);
}
/**
* Set up this class with the structure for a given quiz.
* @param \stdClass $quiz the quiz settings.
*/
public function populate_structure($quiz) {
global $DB;
$slots = $DB->get_records_sql("
SELECT slot.id AS slotid, slot.slot, slot.questionid, slot.page, slot.maxmark,
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
WHERE slot.quizid = ?
ORDER BY slot.slot", array($quiz->id));
$slots = $this->populate_missing_questions($slots);
$this->questions = array();
$this->slots = array();
$this->slotsinorder = array();
foreach ($slots as $slotdata) {
$this->questions[$slotdata->questionid] = $slotdata;
$slot = new \stdClass();
$slot->id = $slotdata->slotid;
$slot->slot = $slotdata->slot;
$slot->quizid = $quiz->id;
$slot->page = $slotdata->page;
$slot->questionid = $slotdata->questionid;
$slot->maxmark = $slotdata->maxmark;
$this->slots[$slot->id] = $slot;
$this->slotsinorder[$slot->slot] = $slot;
}
$section = new \stdClass();
$section->id = 1;
$section->quizid = $quiz->id;
$section->heading = '';
$section->firstslot = 1;
$section->shuffle = false;
$this->sections = array(1 => $section);
$this->populate_slots_with_sectionids();
$this->populate_question_numbers();
}
/**
* Used by populate. Make up fake data for any missing questions.
* @param \stdClass[] $slots the data about the slots and questions in the quiz.
* @return \stdClass[] updated $slots array.
*/
protected function populate_missing_questions($slots) {
// Address missing question types.
foreach ($slots as $slot) {
if ($slot->qtype === null) {
// If the questiontype is missing change the question type.
$slot->id = $slot->questionid;
$slot->category = 0;
$slot->qtype = 'missingtype';
$slot->name = get_string('missingquestion', 'quiz');
$slot->slot = $slot->slot;
$slot->maxmark = 0;
$slot->questiontext = ' ';
$slot->questiontextformat = FORMAT_HTML;
$slot->length = 1;
} else if (!\question_bank::qtype_exists($slot->qtype)) {
$slot->qtype = 'missingtype';
}
}
return $slots;
}
/**
* Fill in the section ids for each slot.
*/
public function populate_slots_with_sectionids() {
$nextsection = reset($this->sections);
foreach ($this->slotsinorder as $slot) {
if ($slot->slot == $nextsection->firstslot) {
$currentsectionid = $nextsection->id;
$nextsection = next($this->sections);
if (!$nextsection) {
$nextsection = new \stdClass();
$nextsection->firstslot = -1;
}
}
$slot->sectionid = $currentsectionid;
}
}
/**
* Number the questions.
*/
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');
} else {
$question->displayednumber = $number;
$number += 1;
}
}
}
/**
* Move a slot from its current location to a new location.
*
* After callig this method, this class will be in an invalid state, and
* should be discarded if you want to manipulate the structure further.
*
* @param int $idmove id of slot to be moved
* @param int $idbefore id of slot to come before slot being moved
* @param int $page new page number of slot being moved
* @return void
*/
public function move_slot($idmove, $idbefore, $page) {
global $DB;
$movingslot = $this->slots[$idmove];
if (empty($movingslot)) {
throw new moodle_exception('Bad slot ID ' . $idmove);
}
$movingslotnumber = (int) $movingslot->slot;
// Empty target slot means move slot to first.
if (empty($idbefore)) {
$targetslotnumber = 0;
} else {
$targetslotnumber = (int) $this->slots[$idbefore]->slot;
}
// Work out how things are being moved.
$slotreorder = array();
if ($targetslotnumber > $movingslotnumber) {
$slotreorder[$movingslotnumber] = $targetslotnumber;
for ($i = $movingslotnumber; $i < $targetslotnumber; $i++) {
$slotreorder[$i + 1] = $i;
}
} else if ($targetslotnumber < $movingslotnumber - 1) {
$slotreorder[$movingslotnumber] = $targetslotnumber + 1;
for ($i = $targetslotnumber + 1; $i < $movingslotnumber; $i++) {
$slotreorder[$i] = $i + 1;
}
}
$trans = $DB->start_delegated_transaction();
// Slot has moved record new order.
if ($slotreorder) {
update_field_with_unique_index('quiz_slots', 'slot', $slotreorder,
array('quizid' => $this->get_quizid()));
}
// Page has changed. Record it.
if (!$page) {
$page = 1;
}
if ($movingslot->page != $page) {
$DB->set_field('quiz_slots', 'page', $page,
array('id' => $movingslot->id));
}
$emptypages = $DB->get_fieldset_sql("
SELECT DISTINCT page - 1
FROM {quiz_slots} slot
WHERE quizid = ?
AND page > 1
AND NOT EXISTS (SELECT 1 FROM {quiz_slots} WHERE quizid = ? AND page = slot.page - 1)
ORDER BY page - 1 DESC
", array($this->get_quizid(), $this->get_quizid()));
foreach ($emptypages as $page) {
$DB->execute("
UPDATE {quiz_slots}
SET page = page - 1
WHERE quizid = ?
AND page > ?
", array($this->get_quizid(), $page));
}
$trans->allow_commit();
}
/**
* Refresh page numbering of quiz slots.
* @param \stdClass $quiz the quiz object.
* @param \stdClass[] $slots (optional) array of slot objects.
* @return \stdClass[] array of slot objects.
*/
public function refresh_page_numbers($quiz, $slots=array()) {
global $DB;
// Get slots ordered by page then slot.
if (!count($slots)) {
$slots = $DB->get_records('quiz_slots', array('quizid' => $quiz->id), 'slot, page');
}
// Loop slots. Start Page number at 1 and increment as required.
$pagenumbers = array('new' => 0, 'old' => 0);
foreach ($slots as $slot) {
if ($slot->page !== $pagenumbers['old']) {
$pagenumbers['old'] = $slot->page;
++$pagenumbers['new'];
}
if ($pagenumbers['new'] == $slot->page) {
continue;
}
$slot->page = $pagenumbers['new'];
}
return $slots;
}
/**
* Refresh page numbering of quiz slots and save to the database.
* @param \stdClass $quiz the quiz object.
* @return \stdClass[] array of slot objects.
*/
public function refresh_page_numbers_and_update_db($quiz) {
global $DB;
$slots = $this->refresh_page_numbers($quiz);
// Record new page order.
foreach ($slots as $slot) {
$DB->set_field('quiz_slots', 'page', $slot->page,
array('id' => $slot->id));
}
return $slots;
}
/**
* Remove a slot from a quiz
* @param \stdClass $quiz the quiz object.
* @param int $slotnumber The number of the slot to be deleted.
*/
public function remove_slot($quiz, $slotnumber) {
global $DB;
$slot = $DB->get_record('quiz_slots', array('quizid' => $quiz->id, 'slot' => $slotnumber));
$maxslot = $DB->get_field_sql('SELECT MAX(slot) FROM {quiz_slots} WHERE quizid = ?', array($quiz->id));
if (!$slot) {
return;
}
$trans = $DB->start_delegated_transaction();
$DB->delete_records('quiz_slots', array('id' => $slot->id));
for ($i = $slot->slot + 1; $i <= $maxslot; $i++) {
$DB->set_field('quiz_slots', 'slot', $i - 1,
array('quizid' => $quiz->id, 'slot' => $i));
}
$qtype = $DB->get_field('question', 'qtype', array('id' => $slot->questionid));
if ($qtype === 'random') {
// This function automatically checks if the question is in use, and won't delete if it is.
question_delete_question($slot->questionid);
}
$this->refresh_page_numbers_and_update_db($quiz);
$trans->allow_commit();
}
/**
* Change the max mark for a slot.
*
* Saves changes to the question grades in the quiz_slots table and any
* corresponding question_attempts.
* It does not update 'sumgrades' in the quiz table.
*
* @param \stdClass $slot row from the quiz_slots table.
* @param float $maxmark the new maxmark.
* @return bool true if the new grade is different from the old one.
*/
public function update_slot_maxmark($slot, $maxmark) {
global $DB;
if (abs($maxmark - $slot->maxmark) < 1e-7) {
// Grade has not changed. Nothing to do.
return false;
}
$trans = $DB->start_delegated_transaction();
$slot->maxmark = $maxmark;
$DB->update_record('quiz_slots', $slot);
\question_engine::set_max_mark_in_attempts(new \qubaids_for_quiz($slot->quizid),
$slot->slot, $maxmark);
$trans->allow_commit();
return true;
}
/**
* Add/Remove a pagebreak.
*
* Saves changes to the slot page relationship in the quiz_slots table and reorders the paging
* for subsequent slots.
*
* @param \stdClass $quiz the quiz object.
* @param int $slotid id of slot.
* @param int $type repaginate::LINK or repaginate::UNLINK.
* @return \stdClass[] array of slot objects.
*/
public function update_page_break($quiz, $slotid, $type) {
global $DB;
$quizslots = $DB->get_records('quiz_slots', array('quizid' => $quiz->id), 'slot');
$repaginate = new \mod_quiz\repaginate($quiz->id, $quizslots);
$repaginate->repaginate_slots($quizslots[$slotid]->slot, $type);
$slots = $this->refresh_page_numbers_and_update_db($quiz);
return $slots;
}
}
-158
View File
@@ -1,158 +0,0 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* JavaScript library for the quiz module editing interface.
*
* @package mod
* @subpackage quiz
* @copyright 2008 Olli Savolainen
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// Initialise everything on the quiz edit/order and paging page.
var quiz_edit = {};
function quiz_edit_init(Y) {
M.core_scroll_manager.scroll_to_saved_pos(Y);
Y.on('submit', function(e) {
M.core_scroll_manager.save_scroll_pos(Y, 'id_existingcategory');
}, '#mform1');
Y.on('submit', function(e) {
M.core_scroll_manager.save_scroll_pos(Y, e.target.get('firstChild'));
}, '.quizsavegradesform');
// Add random question dialogue --------------------------------------------
var randomquestiondialog = Y.YUI2.util.Dom.get('randomquestiondialog');
if (randomquestiondialog) {
Y.YUI2.util.Dom.get(document.body).appendChild(randomquestiondialog);
}
quiz_edit.randomquestiondialog = new Y.YUI2.widget.Dialog('randomquestiondialog', {
modal: true,
width: '100%',
iframe: true,
zIndex: 1000, // zIndex must be way above 99 to be above the active quiz tab
fixedcenter: true,
visible: false,
close: true,
constraintoviewport: true,
postmethod: 'form'
});
quiz_edit.randomquestiondialog.render();
var div = document.getElementById('randomquestiondialog');
if (div) {
div.style.display = 'block';
}
// Show the form on button click.
Y.YUI2.util.Event.addListener(quiz_edit_config.dialoglisteners, 'click', function(e) {
// Transfer the page number from the button form to the pop-up form.
var addrandombutton = Y.YUI2.util.Event.getTarget(e);
var addpagehidden = Y.YUI2.util.Dom.getElementsByClassName('addonpage_formelement', 'input', addrandombutton.form);
document.getElementById('rform_qpage').value = addpagehidden[0].value;
// Show the dialogue and stop the default action.
quiz_edit.randomquestiondialog.show();
Y.YUI2.util.Event.stopEvent(e);
});
// Make escape close the dialogue.
quiz_edit.randomquestiondialog.cfg.setProperty('keylisteners', [new Y.YUI2.util.KeyListener(
document, {keys:[27]}, function(types, args, obj) { quiz_edit.randomquestiondialog.hide();
})]);
// Make the form cancel button close the dialogue.
Y.YUI2.util.Event.addListener('id_cancel', 'click', function(e) {
quiz_edit.randomquestiondialog.hide();
Y.YUI2.util.Event.preventDefault(e);
});
Y.YUI2.util.Event.addListener('id_existingcategory', 'click', quiz_yui_workaround);
Y.YUI2.util.Event.addListener('id_newcategory', 'click', quiz_yui_workaround);
// Repaginate dialogue -----------------------------------------------------
quiz_edit.repaginatedialog = new Y.YUI2.widget.Dialog('repaginatedialog', {
modal: true,
width: '30em',
iframe: true,
zIndex: 1000,
context: ['repaginatecommand', 'tr', 'br', ['beforeShow']],
visible: false,
close: true,
constraintoviewport: true,
postmethod: 'form'
});
quiz_edit.repaginatedialog.render();
quiz_edit.randomquestiondialog.render();
var div = document.getElementById('repaginatedialog');
if (div) {
div.style.display = 'block';
}
// Show the form on button click.
Y.YUI2.util.Event.addListener('repaginatecommand', 'click', function() {
quiz_edit.repaginatedialog.show();
});
// Reposition the dialogue when the window resizes. For some reason this was not working automatically.
Y.YUI2.widget.Overlay.windowResizeEvent.subscribe(function() {
quiz_edit.repaginatedialog.cfg.setProperty('context', ['repaginatecommand', 'tr', 'br', ['beforeShow']]);
});
// Make escape close the dialogue.
quiz_edit.repaginatedialog.cfg.setProperty('keylisteners', [new Y.YUI2.util.KeyListener(
document, {keys:[27]}, function(types, args, obj) { quiz_edit.repaginatedialog.hide();
})]);
// Nasty hack, remove once the YUI bug causing MDL-17594 is fixed.
// https://sourceforge.net/tracker/index.php?func=detail&aid=2493426&group_id=165715&atid=836476
var elementcauseinglayoutproblem = document.getElementById('_yuiResizeMonitor');
if (elementcauseinglayoutproblem) {
elementcauseinglayoutproblem.style.left = '0px';
}
}
function quiz_yui_workaround(e) {
YUI().use('yui2-event', function(Y) {
// YUI does not send the button pressed with the form submission, so copy
// the button name to a hidden input.
var submitbutton = Y.YUI2.util.Event.getTarget(e);
var input = document.createElement('input');
input.type = 'hidden';
input.name = submitbutton.name;
input.value = 1;
submitbutton.form.appendChild(input);
});
}
// Initialise everything on the quiz settings form.
function quiz_settings_init(Y) {
var repaginatecheckbox = document.getElementById('id_repaginatenow');
if (!repaginatecheckbox) {
// This checkbox does not appear on the create new quiz form.
return;
}
var qppselect = document.getElementById('id_questionsperpage');
var qppinitialvalue = qppselect.value;
Y.YUI2.util.Event.addListener([qppselect, 'id_shufflequestions'] , 'change', function() {
setTimeout(function() { // Annoyingly, this handler runs before the formlib disabledif code, hence the timeout.
if (!repaginatecheckbox.disabled) {
repaginatecheckbox.checked = qppselect.value != qppinitialvalue;
}
}, 50);
});
}
+21 -306
View File
@@ -46,74 +46,8 @@ require_once($CFG->dirroot . '/mod/quiz/editlib.php');
require_once($CFG->dirroot . '/mod/quiz/addrandomform.php');
require_once($CFG->dirroot . '/question/category_class.php');
/**
* Callback function called from question_list() function
* (which is called from showbank())
* Displays button in form with checkboxes for each question.
*/
function module_specific_buttons($cmid, $cmoptions) {
global $OUTPUT;
$params = array(
'type' => 'submit',
'name' => 'add',
'value' => $OUTPUT->larrow() . ' ' . get_string('addtoquiz', 'quiz'),
);
if ($cmoptions->hasattempts) {
$params['disabled'] = 'disabled';
}
return html_writer::empty_tag('input', $params);
}
/**
* Callback function called from question_list() function
* (which is called from showbank())
*/
function module_specific_controls($totalnumber, $recurse, $category, $cmid, $cmoptions) {
global $OUTPUT;
$out = '';
$catcontext = context::instance_by_id($category->contextid);
if (has_capability('moodle/question:useall', $catcontext)) {
if ($cmoptions->hasattempts) {
$disabled = ' disabled="disabled"';
} else {
$disabled = '';
}
$randomusablequestions =
question_bank::get_qtype('random')->get_available_questions_from_category(
$category->id, $recurse);
$maxrand = count($randomusablequestions);
if ($maxrand > 0) {
for ($i = 1; $i <= min(10, $maxrand); $i++) {
$randomcount[$i] = $i;
}
for ($i = 20; $i <= min(100, $maxrand); $i += 10) {
$randomcount[$i] = $i;
}
} else {
$randomcount[0] = 0;
$disabled = ' disabled="disabled"';
}
$out = '<strong><label for="menurandomcount">'.get_string('addrandomfromcategory', 'quiz').
'</label></strong><br />';
$attributes = array();
$attributes['disabled'] = $disabled ? 'disabled' : null;
$select = html_writer::select($randomcount, 'randomcount', '1', null, $attributes);
$out .= get_string('addrandom', 'quiz', $select);
$out .= '<input type="hidden" name="recurse" value="'.$recurse.'" />';
$out .= '<input type="hidden" name="categoryid" value="' . $category->id . '" />';
$out .= ' <input type="submit" name="addrandom" value="'.
get_string('addtoquiz', 'quiz').'"' . $disabled . ' />';
$out .= $OUTPUT->help_icon('addarandomquestion', 'quiz');
}
return $out;
}
// These params are only passed from page request to request while we stay on
// this page otherwise they would go in question_edit_setup.
$quiz_reordertool = optional_param('reordertool', -1, PARAM_BOOL);
$quiz_qbanktool = optional_param('qbanktool', -1, PARAM_BOOL);
$scrollpos = optional_param('scrollpos', '', PARAM_INT);
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
@@ -122,35 +56,14 @@ list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
$defaultcategoryobj = question_make_default_categories($contexts->all());
$defaultcategory = $defaultcategoryobj->id . ',' . $defaultcategoryobj->contextid;
if ($quiz_qbanktool > -1) {
$thispageurl->param('qbanktool', $quiz_qbanktool);
set_user_preference('quiz_qbanktool_open', $quiz_qbanktool);
} else {
$quiz_qbanktool = get_user_preferences('quiz_qbanktool_open', 0);
}
if ($quiz_reordertool > -1) {
$thispageurl->param('reordertool', $quiz_reordertool);
set_user_preference('quiz_reordertab', $quiz_reordertool);
} else {
$quiz_reordertool = get_user_preferences('quiz_reordertab', 0);
}
$canaddrandom = $contexts->have_cap('moodle/question:useall');
$canaddquestion = (bool) $contexts->having_add_and_use();
$quizhasattempts = quiz_has_attempts($quiz->id);
$PAGE->set_url($thispageurl);
// Get the course object and related bits.
$course = $DB->get_record('course', array('id' => $quiz->course));
if (!$course) {
print_error('invalidcourseid', 'error');
}
$questionbank = new quiz_question_bank_view($contexts, $thispageurl, $course, $cm, $quiz);
$questionbank->set_quiz_has_attempts($quizhasattempts);
$course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
$quizobj = new quiz($quiz, $cm, $course);
$structure = $quizobj->get_structure();
// You need mod/quiz:manage in addition to question capabilities to access this page.
require_capability('mod/quiz:manage', $contexts->lowest());
@@ -166,7 +79,7 @@ $params = array(
$event = \mod_quiz\event\edit_page_viewed::create($params);
$event->trigger();
// Process commands ============================================================
// Process commands ============================================================.
// Get the list of question ids had their check-boxes ticked.
$selectedslots = array();
@@ -181,17 +94,6 @@ $afteractionurl = new moodle_url($thispageurl);
if ($scrollpos) {
$afteractionurl->param('scrollpos', $scrollpos);
}
if (($up = optional_param('up', false, PARAM_INT)) && confirm_sesskey()) {
quiz_move_question_up($quiz, $up);
quiz_delete_previews($quiz);
redirect($afteractionurl);
}
if (($down = optional_param('down', false, PARAM_INT)) && confirm_sesskey()) {
quiz_move_question_down($quiz, $down);
quiz_delete_previews($quiz);
redirect($afteractionurl);
}
if (optional_param('repaginate', false, PARAM_BOOL) && confirm_sesskey()) {
// Re-paginate the quiz.
@@ -213,13 +115,14 @@ if (($addquestion = optional_param('addquestion', 0, PARAM_INT)) && confirm_sess
}
if (optional_param('add', false, PARAM_BOOL) && confirm_sesskey()) {
$addonpage = optional_param('addonpage', 0, PARAM_INT);
// Add selected questions to the current quiz.
$rawdata = (array) data_submitted();
foreach ($rawdata as $key => $value) { // Parse input for question ids.
if (preg_match('!^q([0-9]+)$!', $key, $matches)) {
$key = $matches[1];
quiz_require_question_use($key);
quiz_add_quiz_question($key, $quiz);
quiz_add_quiz_question($key, $quiz, $addonpage);
}
}
quiz_delete_previews($quiz);
@@ -240,54 +143,6 @@ if ((optional_param('addrandom', false, PARAM_BOOL)) && confirm_sesskey()) {
redirect($afteractionurl);
}
if (optional_param('addnewpagesafterselected', null, PARAM_CLEAN) &&
!empty($selectedslots) && confirm_sesskey()) {
foreach ($selectedslots as $slot) {
quiz_add_page_break_after_slot($quiz, $slot);
}
quiz_delete_previews($quiz);
redirect($afteractionurl);
}
$addpage = optional_param('addpage', false, PARAM_INT);
if ($addpage !== false && confirm_sesskey()) {
quiz_add_page_break_after_slot($quiz, $addpage);
quiz_delete_previews($quiz);
redirect($afteractionurl);
}
$deleteemptypage = optional_param('deleteemptypage', false, PARAM_INT);
if (($deleteemptypage !== false) && confirm_sesskey()) {
quiz_delete_empty_page($quiz, $deleteemptypage);
quiz_delete_previews($quiz);
redirect($afteractionurl);
}
$remove = optional_param('remove', false, PARAM_INT);
if ($remove && confirm_sesskey() && quiz_has_question_use($quiz, $remove)) {
// Remove a question from the quiz.
// We require the user to have the 'use' capability on the question,
// so that then can add it back if they remove the wrong one by mistake,
// but, if the question is missing, it can always be removed.
quiz_remove_slot($quiz, $remove);
quiz_delete_previews($quiz);
quiz_update_sumgrades($quiz);
redirect($afteractionurl);
}
if (optional_param('quizdeleteselected', false, PARAM_BOOL) &&
!empty($selectedslots) && confirm_sesskey()) {
// Work backwards, since removing a question renumbers following slots.
foreach (array_reverse($selectedslots) as $slot) {
if (quiz_has_question_use($quiz, $slot)) {
quiz_remove_slot($quiz, $slot);
}
}
quiz_delete_previews($quiz);
quiz_update_sumgrades($quiz);
redirect($afteractionurl);
}
if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
$deletepreviews = false;
$recomputesummarks = false;
@@ -425,9 +280,17 @@ if (optional_param('savechanges', false, PARAM_BOOL) && confirm_sesskey()) {
redirect($afteractionurl);
}
// Get the question bank view.
$questionbank = new quiz_question_bank_view($contexts, $thispageurl, $course, $cm, $quiz);
$questionbank->set_quiz_has_attempts($quizhasattempts);
$questionbank->process_actions($thispageurl, $cm);
// End of process commands =====================================================
// End of process commands =====================================================.
$PAGE->set_pagelayout('incourse');
$PAGE->set_pagetype('mod-quiz-edit');
$output = $PAGE->get_renderer('mod_quiz', 'edit');
$PAGE->requires->skip_link_to('questionbank',
get_string('skipto', 'access', get_string('questionbank', 'question')));
@@ -453,164 +316,16 @@ $numberoflisteners = $DB->get_field_sql("
for ($pageiter = 1; $pageiter <= $numberoflisteners; $pageiter++) {
$quizeditconfig->dialoglisteners[] = 'addrandomdialoglaunch_' . $pageiter;
}
$PAGE->requires->data_for_js('quiz_edit_config', $quizeditconfig);
$PAGE->requires->js('/question/qengine.js');
$module = array(
'name' => 'mod_quiz_edit',
'fullpath' => '/mod/quiz/edit.js',
'requires' => array('yui2-dom', 'yui2-event', 'yui2-container'),
'strings' => array(),
'async' => false,
);
$PAGE->requires->js_init_call('quiz_edit_init', null, false, $module);
// Print the tabs to switch mode.
if ($quiz_reordertool) {
$currenttab = 'reorder';
} else {
$currenttab = 'edit';
}
$tabs = array(array(
new tabobject('edit', new moodle_url($thispageurl,
array('reordertool' => 0)), get_string('editingquiz', 'quiz')),
new tabobject('reorder', new moodle_url($thispageurl,
array('reordertool' => 1)), get_string('orderingquiz', 'quiz')),
));
print_tabs($tabs, $currenttab);
// Questions wrapper start.
echo html_writer::start_tag('div', array('class' => 'mod-quiz-edit-content'));
if ($quiz_qbanktool) {
$bankclass = '';
$quizcontentsclass = '';
} else {
$bankclass = 'collapsed ';
$quizcontentsclass = 'quizwhenbankcollapsed';
}
echo $output->edit_page($quizobj, $structure, $contexts, $thispageurl, $pagevars);
echo '<div class="questionbankwindow ' . $bankclass . 'block">';
echo '<div class="header"><div class="title"><h2>';
echo get_string('questionbankcontents', 'quiz') .
'&nbsp;[<a href="' . $thispageurl->out(true, array('qbanktool' => '1')) .
'" id="showbankcmd">' . get_string('show').
'</a><a href="' . $thispageurl->out(true, array('qbanktool' => '0')) .
'" id="hidebankcmd">' . get_string('hide').
'</a>]';
echo '</h2></div></div><div class="content">';
// Questions wrapper end.
echo html_writer::end_tag('div');
echo '<span id="questionbank"></span>';
echo '<div class="container">';
echo '<div id="module" class="module">';
echo '<div class="bd">';
$questionbank->display('editq',
$pagevars['qpage'],
$pagevars['qperpage'],
$pagevars['cat'], $pagevars['recurse'], $pagevars['showhidden'],
$pagevars['qbshowtext']);
echo '</div>';
echo '</div>';
echo '</div>';
echo '</div></div>';
echo '<div class="quizcontents ' . $quizcontentsclass . '" id="quizcontentsblock">';
if ($quiz->shufflequestions) {
$repaginatingdisabledhtml = 'disabled="disabled"';
$repaginatingdisabled = true;
} else {
$repaginatingdisabledhtml = '';
$repaginatingdisabled = false;
}
if ($quiz_reordertool) {
echo '<div class="repaginatecommand"><button id="repaginatecommand" ' .
$repaginatingdisabledhtml.'>'.
get_string('repaginatecommand', 'quiz').'...</button>';
echo '</div>';
}
if ($quiz_reordertool) {
echo $OUTPUT->heading_with_help(get_string('orderingquizx', 'quiz', format_string($quiz->name)),
'orderandpaging', 'quiz');
} else {
echo $OUTPUT->heading(get_string('editingquizx', 'quiz', format_string($quiz->name)), 2);
echo $OUTPUT->help_icon('editingquiz', 'quiz', get_string('basicideasofquiz', 'quiz'));
}
quiz_print_status_bar($quiz);
$tabindex = 0;
quiz_print_grading_form($quiz, $thispageurl, $tabindex);
$notifystrings = array();
if ($quizhasattempts) {
$reviewlink = quiz_attempt_summary_link_to_reports($quiz, $cm, $contexts->lowest());
$notifystrings[] = get_string('cannoteditafterattempts', 'quiz', $reviewlink);
}
if ($quiz->shufflequestions) {
$updateurl = new moodle_url("$CFG->wwwroot/course/mod.php",
array('return' => 'true', 'update' => $quiz->cmid, 'sesskey' => sesskey()));
$updatelink = '<a href="'.$updateurl->out().'">' . get_string('updatethis', '',
get_string('modulename', 'quiz')) . '</a>';
$notifystrings[] = get_string('shufflequestionsselected', 'quiz', $updatelink);
}
if (!empty($notifystrings)) {
echo $OUTPUT->box('<p>' . implode('</p><p>', $notifystrings) . '</p>', 'statusdisplay');
}
if ($quiz_reordertool) {
$perpage = array();
$perpage[0] = get_string('allinone', 'quiz');
for ($i = 1; $i <= 50; ++$i) {
$perpage[$i] = $i;
}
$gostring = get_string('go');
echo '<div id="repaginatedialog"><div class="hd">';
echo get_string('repaginatecommand', 'quiz');
echo '</div><div class="bd">';
echo '<form action="edit.php" method="post">';
echo '<fieldset class="invisiblefieldset">';
echo html_writer::input_hidden_params($thispageurl);
echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
// YUI does not submit the value of the submit button so we need to add the value.
echo '<input type="hidden" name="repaginate" value="'.$gostring.'" />';
$attributes = array();
$attributes['disabled'] = $repaginatingdisabledhtml ? 'disabled' : null;
$select = html_writer::select(
$perpage, 'questionsperpage', $quiz->questionsperpage, null, $attributes);
print_string('repaginate', 'quiz', $select);
echo '<div class="quizquestionlistcontrols">';
echo ' <input type="submit" name="repaginate" value="'. $gostring . '" ' .
$repaginatingdisabledhtml.' />';
echo '</div></fieldset></form></div></div>';
}
if ($quiz_reordertool) {
echo '<div class="reorder">';
} else {
echo '<div class="editq">';
}
quiz_print_question_list($quiz, $thispageurl, true, $quiz_reordertool, $quiz_qbanktool,
$quizhasattempts, $defaultcategoryobj, $canaddquestion, $canaddrandom);
echo '</div>';
// Close <div class="quizcontents">.
echo '</div>';
if (!$quiz_reordertool && $canaddrandom) {
$randomform = new quiz_add_random_form(new moodle_url('/mod/quiz/addrandom.php'), $contexts);
$randomform->set_data(array(
'category' => $pagevars['cat'],
'returnurl' => $thispageurl->out_as_local_url(false),
'cmid' => $cm->id,
));
?>
<div id="randomquestiondialog">
<div class="hd"><?php print_string('addrandomquestiontoquiz', 'quiz', $quiz->name); ?>
<span id="pagenumber"><!-- JavaScript will insert the page number here. -->
</span>
</div>
<div class="bd"><?php
$randomform->display();
?></div>
</div>
<?php
}
echo $OUTPUT->footer();
+141
View File
@@ -0,0 +1,141 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Rest endpoint for ajax editing of quiz structure.
*
* @package mod_quiz
* @copyright 1999 Martin Dougiamas http://dougiamas.com
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
if (!defined('AJAX_SCRIPT')) {
define('AJAX_SCRIPT', true);
}
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/mod/quiz/editlib.php');
// Initialise ALL the incoming parameters here, up front.
$quizid = required_param('quizid', PARAM_INT);
$class = required_param('class', PARAM_ALPHA);
$field = optional_param('field', '', PARAM_ALPHA);
$instanceid = optional_param('instanceId', 0, PARAM_INT);
$sectionid = optional_param('sectionId', 0, PARAM_INT);
$previousid = optional_param('previousid', 0, PARAM_INT);
$value = optional_param('value', 0, PARAM_INT);
$column = optional_param('column', 0, PARAM_ALPHA);
$id = optional_param('id', 0, PARAM_INT);
$summary = optional_param('summary', '', PARAM_RAW);
$sequence = optional_param('sequence', '', PARAM_SEQUENCE);
$visible = optional_param('visible', 0, PARAM_INT);
$pageaction = optional_param('action', '', PARAM_ALPHA); // Used to simulate a DELETE command.
$maxmark = optional_param('maxmark', '', PARAM_FLOAT);
$page = optional_param('page', '', PARAM_INT);
$PAGE->set_url('/mod/quiz/edit-rest.php',
array('quizid' => $quizid, 'class' => $class));
require_sesskey();
$quiz = $DB->get_record('quiz', array('id' => $quizid), '*', MUST_EXIST);
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $quiz->course);
$course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
require_login($course, false, $cm);
$quizobj = new quiz($quiz, $cm, $course);
$structure = $quizobj->get_structure();
$modcontext = context_module::instance($cm->id);
echo $OUTPUT->header(); // Send headers.
// OK, now let's process the parameters and do stuff
// MDL-10221 the DELETE method is not allowed on some web servers,
// so we simulate it with the action URL param.
$requestmethod = $_SERVER['REQUEST_METHOD'];
if ($pageaction == 'DELETE') {
$requestmethod = 'DELETE';
}
switch($requestmethod) {
case 'POST':
case 'GET': // For debugging.
switch ($class) {
case 'section':
break;
case 'resource':
switch ($field) {
case 'move':
require_capability('mod/quiz:manage', $modcontext);
$structure->move_slot($id, $previousid, $page);
quiz_delete_previews($quiz);
echo json_encode(array('visible' => true));
break;
case 'getmaxmark':
require_capability('mod/quiz:manage', $modcontext);
$slot = $DB->get_record('quiz_slots', array('id' => $id), '*', MUST_EXIST);
echo json_encode(array('instancemaxmark' =>
quiz_format_question_grade($quiz, $slot->maxmark)));
break;
case 'updatemaxmark':
require_capability('mod/quiz:manage', $modcontext);
$slot = $structure->get_slot_by_id($id);
if ($structure->update_slot_maxmark($slot, $maxmark)) {
// Grade has really changed.
quiz_delete_previews($quiz);
quiz_update_sumgrades($quiz);
quiz_update_all_attempt_sumgrades($quiz);
quiz_update_all_final_grades($quiz);
quiz_update_grades($quiz, 0, true);
}
echo json_encode(array('instancemaxmark' => quiz_format_question_grade($quiz, $maxmark),
'newsummarks' => quiz_format_grade($quiz, $quiz->sumgrades)));
break;
case 'linkslottopage':
require_capability('mod/quiz:manage', $modcontext);
$slots = $structure->update_page_break($quiz, $id, $value);
$json = array();
foreach ($slots as $slot) {
$json[$slot->slot] = array('id' => $slot->id, 'slot' => $slot->slot,
'page' => $slot->page);
}
echo json_encode(array('slots' => $json));
break;
}
break;
case 'course':
break;
}
break;
case 'DELETE':
switch ($class) {
case 'resource':
require_capability('mod/quiz:manage', $modcontext);
if (!$slot = $DB->get_record('quiz_slots', array('quizid' => $quiz->id, 'id' => $id))) {
throw new moodle_exception('AJAX commands.php: Bad slot ID '.$id);
}
$structure->remove_slot($quiz, $slot->slot);
quiz_delete_previews($quiz);
quiz_update_sumgrades($quiz);
echo json_encode(array('newsummarks' => quiz_format_grade($quiz, $quiz->sumgrades)));
break;
}
break;
}
+49 -945
View File
File diff suppressed because it is too large Load Diff
+20 -4
View File
@@ -27,10 +27,12 @@ $string['action'] = 'Action';
$string['activityoverview'] = 'You have quizzes that are due';
$string['adaptive'] = 'Adaptive mode';
$string['adaptive_help'] = 'If enabled, multiple responses to a question are allowed within the same attempt at the quiz. So for example if a response is marked as incorrect, the student will be allowed to try again immediately. However, depending on the "Apply penalties" setting, a penalty will usually be subtracted for each wrong attempt.';
$string['addaquestion'] = 'Add a question ...';
$string['addarandomquestion'] = 'Add a random question ...';
$string['add'] = 'Add';
$string['addaquestion'] = 'a new question';
$string['addarandomquestion'] = 'a random question';
$string['addarandomquestion_help'] = 'When a random question is added, it results in a randomly-chosen question from the category being inserted into the quiz. This means that different students are likely to get a different selection of questions, and when a quiz allows multiple attempts then each attempt is likely to contain a new selection of questions.';
$string['adddescriptionlabel'] = 'Add description/label';
$string['addarandomselectedquestion'] = 'Add a random selected question ...';
$string['adddescriptionlabel'] = 'Add a description item';
$string['addingquestion'] = 'Adding a question';
$string['addingquestions'] = '<p>This side of the page is where you manage your database of questions. Questions are stored in categories to help you keep them organised, and can be used by any quiz in your course or even other courses if you choose to \'publish\' them.</p>
<p>After you select or create a question category you will be able to create or edit questions. You can select any of these questions to add to your quiz over on the other side of this page.</p>';
@@ -41,15 +43,18 @@ $string['addnewquestionsqbank'] = 'Add questions to the category {$a->catname}:
$string['addnewuseroverride'] = 'Add user override';
$string['addpagehere'] = 'Add page here';
$string['addquestion'] = 'Add question';
$string['addquestionfrombanktopage'] = 'Add from the question bank to page {$a}';
$string['addquestions'] = 'Add questions';
$string['addquestionstoquiz'] = 'Add questions to current quiz';
$string['addrandom'] = 'Add {$a} random questions';
$string['addrandomfromcategory'] = 'Add random questions from category:';
$string['addrandomquestion'] = 'Add random question';
$string['addarandomquestion_help'] = 'When a random question is added, it results in a randomly-chosen question from the category being inserted into the quiz. This means that different students are likely to get a different selection of questions, and when a quiz allows multiple attempts then each attempt is likely to contain a new selection of questions.';
$string['addrandomquestiontopage'] = 'Add a random question to page {$a}';
$string['addrandomquestiontoquiz'] = 'Add a random question to quiz {$a}';
$string['addrandom1'] = '<< Add';
$string['addrandom2'] = 'random questions';
$string['addselectedquestionstoquiz'] = 'Add selected questions to the quiz';
$string['addselectedtoquiz'] = 'Add selected to quiz';
$string['addtoquiz'] = 'Add to quiz';
$string['affectedstudents'] = 'Affected {$a}';
@@ -183,6 +188,7 @@ $string['configtimelimit'] = 'Default time limit for quizzes in minutes. 0 mean
$string['configtimelimitsec'] = 'Default time limit for quizzes in seconds. 0 mean no time limit.';
$string['configurerandomquestion'] = 'Configure question';
$string['confirmclose'] = 'Once you submit, you will no longer be able to change your answers for this attempt.';
$string['confirmremovequestion'] = 'Are you sure you want to remove this {$a} question?';
$string['confirmserverdelete'] = 'Are you sure you want to remove the server <b>{$a}</b> from the list?';
$string['confirmstartattemptlimit'] = 'Number of attempts allowed: {$a}. You are about to start a new attempt. Do you wish to proceed?';
$string['confirmstartattempttimelimit'] = 'This quiz has a time limit and is limited to {$a} attempt(s). You are about to start a new attempt. Do you wish to proceed?';
@@ -244,6 +250,8 @@ $string['displayoptions'] = 'Display options';
$string['donotuseautosave'] = 'Do not use auto-save';
$string['download'] = 'Click to download the exported category file';
$string['downloadextra'] = '(file is also stored in the course files in the /backupdata/quiz folder)';
$string['dragtoafter'] = 'After {$a}';
$string['dragtostart'] = 'To the start';
$string['duplicateresponse'] = 'This submission has been ignored because you gave an equivalent answer earlier.';
$string['eachattemptbuildsonthelast'] = 'Each attempt builds on the last';
$string['eachattemptbuildsonthelast_help'] = 'If multiple attempts are allowed and this setting is enabled, each new quiz attempt will contain the results of the previous attempt. This allows a quiz to be completed over several attempts.';
@@ -259,8 +267,10 @@ $string['editingquiz_help'] = 'When creating a quiz, the main concepts are:
* Random questions - A student gets different questions each time they attempt the quiz and different students can get different questions';
$string['editingquiz_link'] = 'mod/quiz/edit';
$string['editingquizx'] = 'Editing quiz: {$a}';
$string['editmaxmark'] = 'Edit maximum mark';
$string['editoverride'] = 'Edit override';
$string['editqcats'] = 'Edit questions categories';
$string['editquestion'] = 'Edit question';
$string['editquestions'] = 'Edit questions';
$string['editquiz'] = 'Edit quiz';
$string['editquizquestions'] = 'Edit quiz questions';
@@ -421,6 +431,7 @@ $string['invalidquizid'] = 'Invalid quiz ID';
$string['invalidsource'] = 'The source is not accepted as valid.';
$string['invalidsourcetype'] = 'Invalid source type.';
$string['invalidstateid'] = 'Invalid state id';
$string['joinpages'] = 'Remove page break';
$string['lastanswer'] = 'Your last answer was';
$string['layout'] = 'Layout';
$string['layoutasshown'] = 'Page layout as shown.';
@@ -444,6 +455,7 @@ $string['messageprovider:attempt_overdue'] = 'Warning when your quiz attempt bec
$string['messageprovider:confirmation'] = 'Confirmation of your own quiz submissions';
$string['messageprovider:submission'] = 'Notification of quiz submissions';
$string['max'] = 'Max';
$string['maxmark'] = 'Maximum mark';
$string['min'] = 'Min';
$string['minutes'] = 'Minutes';
$string['missingcorrectanswer'] = 'Correct answer must be specified';
@@ -563,6 +575,7 @@ $string['overridegroupeventname'] = '{$a->quiz} - {$a->group}';
$string['overrides'] = 'Overrides';
$string['overrideuser'] = 'Override user';
$string['overrideusereventname'] = '{$a->quiz} - Override';
$string['pageshort'] = 'P';
$string['page-mod-quiz-x'] = 'Any quiz module page';
$string['page-mod-quiz-attempt'] = 'Attempt quiz page';
$string['page-mod-quiz-edit'] = 'Edit quiz page';
@@ -598,7 +611,7 @@ $string['qbrief'] = 'Q. {$a}';
$string['qti'] = 'IMS QTI format';
$string['qtypename'] = 'type, name';
$string['question'] = 'Question';
$string['questionbankcontents'] = 'Question bank contents';
$string['questionbank'] = 'from question bank';
$string['questionbankmanagement'] = 'Question bank management';
$string['questionbehaviour'] = 'Question behaviour';
$string['questioncats'] = 'Question categories';
@@ -663,6 +676,7 @@ $string['random'] = 'Random question';
$string['randomcreate'] = 'Create random questions';
$string['randomfromcategory'] = 'Random question from category:';
$string['randomfromexistingcategory'] = 'Random question from an existing category';
$string['randomnumber'] = 'Number of random questions';
$string['randomnosubcat'] = 'Questions from this category only, not its subcategories.';
$string['randomquestionusinganewcategory'] = 'Random question using a new category';
$string['randomwithsubcat'] = 'Questions from this category and its subcategories.';
@@ -770,6 +784,7 @@ $string['savingnewgradeforquestion'] = 'Saving new grade for question id {$a}.';
$string['savingnewmaximumgrade'] = 'Saving new maximum grade.';
$string['score'] = 'Raw score';
$string['scores'] = 'Scores';
$string['seequestions'] = '(See questions)';
$string['select'] = 'Select';
$string['selectall'] = 'Select all';
$string['selectcategory'] = 'Select category';
@@ -818,6 +833,7 @@ $string['sortsubmit'] = 'Sort questions';
$string['sorttypealpha'] = 'Sort by type, name';
$string['specificapathnotonquestion'] = 'The specified file path is not on the specified question';
$string['specificquestionnotonquiz'] = 'Specified question is not on the specified quiz';
$string['splitpages'] = 'Add page break';
$string['startagain'] = 'Start again';
$string['startattempt'] = 'Start attempt';
$string['startedon'] = 'Started on';
+20 -10
View File
@@ -634,21 +634,32 @@ function quiz_format_grade($quiz, $grade) {
}
/**
* Round a grade to to the correct number of decimal places, and format it for display.
* Determine the correct number of decimal places required to format a grade.
*
* @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
* @return integer
*/
function quiz_get_grade_format($quiz) {
if (empty($quiz->questiondecimalpoints)) {
$quiz->questiondecimalpoints = -1;
}
if ($quiz->questiondecimalpoints == -1) {
return $quiz->decimalpoints;
}
return $quiz->questiondecimalpoints;
}
/**
* Round a grade to the correct number of decimal places, and format it for display.
*
* @param object $quiz The quiz table row, only $quiz->decimalpoints is used.
* @param float $grade The grade to round.
* @return float
*/
function quiz_format_question_grade($quiz, $grade) {
if (empty($quiz->questiondecimalpoints)) {
$quiz->questiondecimalpoints = -1;
}
if ($quiz->questiondecimalpoints == -1) {
return format_float($grade, $quiz->decimalpoints);
} else {
return format_float($grade, $quiz->questiondecimalpoints);
}
return format_float($grade, quiz_get_grade_format($quiz));
}
/**
@@ -1791,7 +1802,6 @@ function quiz_get_navigation_options() {
);
}
/**
* Obtains the automatic completion state for this quiz on any conditions
* in quiz settings, such as if all attempts are used or a certain grade is achieved.
+46 -17
View File
@@ -1119,6 +1119,20 @@ function quiz_get_user_image_options() {
);
}
/**
* Get the choices to offer for the 'Questions per page' option.
* @return array int => string.
*/
function quiz_questions_per_page_options() {
$pageoptions = array();
$pageoptions[0] = get_string('neverallononepage', 'quiz');
$pageoptions[1] = get_string('everyquestion', 'quiz');
for ($i = 2; $i <= QUIZ_MAX_QPP_OPTION; ++$i) {
$pageoptions[$i] = get_string('everynquestions', 'quiz', $i);
}
return $pageoptions;
}
/**
* Get the human-readable name for a quiz attempt state.
* @param string $state one of the state constants like {@link quiz_attempt::IN_PROGRESS}.
@@ -1231,27 +1245,12 @@ function quiz_question_preview_url($quiz, $question) {
* @return the HTML for a preview question icon.
*/
function quiz_question_preview_button($quiz, $question, $label = false) {
global $CFG, $OUTPUT;
global $PAGE;
if (!question_has_capability_on($question, 'use', $question->category)) {
return '';
}
$url = quiz_question_preview_url($quiz, $question);
// Do we want a label?
$strpreviewlabel = '';
if ($label) {
$strpreviewlabel = get_string('preview', 'quiz');
}
// Build the icon.
$strpreviewquestion = get_string('previewquestion', 'quiz');
$image = $OUTPUT->pix_icon('t/preview', $strpreviewquestion);
$action = new popup_action('click', $url, 'questionpreview',
question_preview_popup_params());
return $OUTPUT->action_link($url, $image, $action, array('title' => $strpreviewquestion));
return $PAGE->get_renderer('mod_quiz', 'edit')->question_preview_icon($quiz, $question, $label);
}
/**
@@ -1876,3 +1875,33 @@ class qubaids_for_quiz extends qubaid_join {
parent::__construct('{quiz_attempts} quiza', 'quiza.uniqueid', $where, $params);
}
}
/**
* Creates a textual representation of a question for display.
*
* @param object $question A question object from the database questions table
* @param bool $showicon If true, show the question's icon with the question. False by default.
* @param bool $showquestiontext If true (default), show question text after question name.
* If false, show only question name.
* @return string
*/
function quiz_question_tostring($question, $showicon = false, $showquestiontext = true) {
$result = '';
$name = shorten_text(format_string($question->name), 200);
if ($showicon) {
$name .= print_question_icon($question) . ' ' . $name;
}
$result .= html_writer::span($name, 'questionname');
if ($showquestiontext) {
$questiontext = question_utils::to_plain_text($question->questiontext,
$question->questiontextformat, array('noclean' => true, 'para' => false));
$questiontext = shorten_text($questiontext, 200);
if ($questiontext) {
$result .= ' ' . html_writer::span(s($questiontext), 'questiontext');
}
}
return $result;
}
+3 -19
View File
@@ -156,33 +156,15 @@ class mod_quiz_mod_form extends moodleform_mod {
$mform->setAdvanced('shufflequestions', $quizconfig->shufflequestions_adv);
$mform->setDefault('shufflequestions', $quizconfig->shufflequestions);
// Questions per page.
$pageoptions = array();
$pageoptions[0] = get_string('neverallononepage', 'quiz');
$pageoptions[1] = get_string('everyquestion', 'quiz');
for ($i = 2; $i <= QUIZ_MAX_QPP_OPTION; ++$i) {
$pageoptions[$i] = get_string('everynquestions', 'quiz', $i);
}
$pagegroup = array();
$pagegroup[] = $mform->createElement('select', 'questionsperpage',
get_string('newpage', 'quiz'), $pageoptions, array('id' => 'id_questionsperpage'));
get_string('newpage', 'quiz'), quiz_questions_per_page_options(), array('id' => 'id_questionsperpage'));
$mform->setDefault('questionsperpage', $quizconfig->questionsperpage);
if (!empty($this->_cm)) {
$pagegroup[] = $mform->createElement('checkbox', 'repaginatenow', '',
get_string('repaginatenow', 'quiz'), array('id' => 'id_repaginatenow'));
$mform->disabledIf('repaginatenow', 'shufflequestions', 'eq', 1);
$PAGE->requires->js('/question/qengine.js');
$module = array(
'name' => 'mod_quiz_edit',
'fullpath' => '/mod/quiz/edit.js',
'requires' => array('yui2-dom', 'yui2-event', 'yui2-container'),
'strings' => array(),
'async' => false,
);
$PAGE->requires->js_init_call('quiz_settings_init', null, false, $module);
}
$mform->addGroup($pagegroup, 'questionsperpagegrp',
@@ -408,6 +390,8 @@ class mod_quiz_mod_form extends moodleform_mod {
// -------------------------------------------------------------------------------
$this->add_action_buttons();
$PAGE->requires->yui_module('moodle-mod_quiz-modform', 'M.mod_quiz.modform.init');
}
protected function add_review_options_group($mform, $quizconfig, $whenname,
+49
View File
@@ -0,0 +1,49 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Ajax script to update the contents of the question bank dialogue.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define('AJAX_SCRIPT', true);
require_once('../../config.php');
require_once($CFG->dirroot . '/mod/quiz/editlib.php');
list($thispageurl, $contexts, $cmid, $cm, $quiz, $pagevars) =
question_edit_setup('editq', '/mod/quiz/edit.php', true);
// Get the course object and related bits.
$course = $DB->get_record('course', array('id' => $quiz->course), '*', MUST_EXIST);
require_capability('mod/quiz:manage', $contexts->lowest());
// Create quiz question bank view.
$questionbank = new quiz_question_bank_view($contexts, $thispageurl, $course, $cm, $quiz);
$questionbank->set_quiz_has_attempts(quiz_has_attempts($quiz->id));
// Output.
$output = $PAGE->get_renderer('mod_quiz', 'edit');
$contents = $output->question_bank_contents($questionbank, $pagevars);
echo json_encode(array(
'status' => 'OK',
'contents' => $contents,
));
+5 -2
View File
@@ -1174,11 +1174,13 @@ class mod_quiz_renderer extends plugin_renderer_base {
$options = array('filter' => false, 'newlines' => false);
$warning = format_text(get_string('connectionerror', 'quiz'), FORMAT_MARKDOWN, $options);
$ok = format_text(get_string('connectionok', 'quiz'), FORMAT_MARKDOWN, $options);
return html_writer::tag('div', $warning, array('id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert')) .
html_writer::tag('div', $ok, array('id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert'));
return html_writer::tag('div', $warning,
array('id' => 'connection-error', 'style' => 'display: none;', 'role' => 'alert')) .
html_writer::tag('div', $ok, array('id' => 'connection-ok', 'style' => 'display: none;', 'role' => 'alert'));
}
}
class mod_quiz_links_to_other_attempts implements renderable {
/**
* @var array string attempt number => url, or null for the current attempt.
@@ -1186,6 +1188,7 @@ class mod_quiz_links_to_other_attempts implements renderable {
public $links = array();
}
class mod_quiz_view_object {
/** @var array $infomessages of messages with information to display about the quiz. */
public $infomessages;
+45
View File
@@ -0,0 +1,45 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Rest endpoint for ajax editing for paging operations on the quiz structure.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../config.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
$cmid = required_param('cmid', PARAM_INT);
$quizid = required_param('quizid', PARAM_INT);
$slotnumber = required_param('slot', PARAM_INT);
$repagtype = required_param('repag', PARAM_INT);
require_sesskey();
$quizobj = quiz::create($quizid);
require_login($quizobj->get_course(), false, $quizobj->get_cm());
require_capability('mod/quiz:manage', $quizobj->get_context());
$slotnumber++;
$repage = new \mod_quiz\repaginate($quizid);
$repage->repaginate_slots($slotnumber, $repagtype);
$structure = $quizobj->get_structure();
$slots = $structure->refresh_page_numbers_and_update_db($structure->get_quiz());
redirect(new moodle_url('edit.php', array('cmid' => $quizobj->get_cmid())));
+262 -681
View File
File diff suppressed because it is too large Load Diff
+159 -9
View File
@@ -17,10 +17,10 @@
/**
* Steps definitions related to mod_quiz.
*
* @package mod_quiz
* @category test
* @copyright 2014 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @package mod_quiz
* @category test
* @copyright 2014 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
@@ -34,10 +34,8 @@ use Behat\Behat\Context\Step\Given as Given,
/**
* Steps definitions related to mod_quiz.
*
* @package mod_quiz
* @category test
* @copyright 2014 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @copyright 2014 Marina Glancy
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_mod_quiz extends behat_question_base {
/**
@@ -54,10 +52,162 @@ class behat_mod_quiz extends behat_question_base {
$quizname = $this->escape($quizname);
$editquiz = $this->escape(get_string('editquiz', 'quiz'));
$addaquestion = $this->escape(get_string('addaquestion', 'quiz'));
$menuxpath = "//div[contains(@class, ' page-add-actions ')][last()]//a[contains(@class, ' textmenu')]";
$itemxpath = "//div[contains(@class, ' page-add-actions ')][last()]//a[contains(@class, ' addquestion ')]";
return array_merge(array(
new Given("I follow \"$quizname\""),
new Given("I follow \"$editquiz\""),
new Given("I press \"$addaquestion\""),
new Given("I click on \"$menuxpath\" \"xpath_element\""),
new Given("I click on \"$itemxpath\" \"xpath_element\""),
), $this->finish_adding_question($questiontype, $questiondata));
}
/**
* Set the max mark for a question on the Edit quiz page.
*
* @When /^I set the max mark for question "(?P<question_name_string>(?:[^"]|\\")*)" to "(?P<new_mark_string>(?:[^"]|\\")*)"$/
* @param string $questionname the name of the question to set the max mark for.
* @param string $newmark the mark to set
*/
public function i_set_the_max_mark_for_quiz_question($questionname, $newmark) {
return array(
new Given('I follow "' . $this->escape(get_string('editmaxmark', 'quiz')) . '"'),
new Given('I wait until "li input[name=maxmark]" "css_element" exists'),
new Given('I should see "' . $this->escape(get_string('edittitleinstructions')) . '"'),
new Given('I set the field "maxmark" to "' . $this->escape($newmark) . chr(10) . '"'),
);
}
/**
* Open the add menu on a given page, or at the end of the Edit quiz page.
* @Given /^I open the "(?P<page_n_or_last_string>(?:[^"]|\\")*)" add to quiz menu$/
* @param string $pageorlast either "Page n" or "last".
*/
public function i_open_the_add_to_quiz_menu_for($pageorlast) {
if (!$this->running_javascript()) {
throw new DriverException('Activities actions menu not available when Javascript is disabled');
}
if ($pageorlast == 'last') {
$xpath = "//div[@class = 'last-add-menu']//a[contains(@class, 'textmenu') and contains(., 'Add')]";
} else if (preg_match('~Page (\d+)~', $pageorlast, $matches)) {
$xpath = "//li[@id = 'page-{$matches[1]}']//a[contains(@class, 'textmenu') and contains(., 'Add')]";
} else {
throw new ExpectationException("The I open the add to quiz menu step must specify either 'Page N' or 'last'.");
}
$menu = $this->find('xpath', $xpath)->click();
}
/**
* Click on a given link in the moodle-actionmenu that is currently open.
* @Given /^I follow "(?P<link_string>(?:[^"]|\\")*)" in the open menu$/
* @param string $linkstring the text (or id, etc.) of the link to click.
* @return array of steps.
*/
public function i_follow_in_the_open_menu($linkstring) {
$openmenuxpath = "//div[contains(@class, 'moodle-actionmenu') and contains(@class, 'show')]";
return array(
new Given('I click on "' . $linkstring . '" "link" in the "' . $openmenuxpath . '" "xpath_element"'),
);
}
/**
* Check whether a particular question is on a particular page of the quiz on the Edit quiz page.
* @Given /^I should see "(?P<question_name>(?:[^"]|\\")*)" on quiz page "(?P<page_number>\d+)"$/
* @param string $questionname the name of the question we are looking for.
* @param number $pagenumber the page it should be found on.
* @return array of steps.
*/
public function i_should_see_on_quiz_page($questionname, $pagenumber) {
$xpath = "//li[contains(., '" . $this->escape($questionname) .
"')][./preceding-sibling::li[contains(@class, 'pagenumber')][1][contains(., 'Page " .
$pagenumber . "')]]";
return array(
new Given('"' . $xpath . '" "xpath_element" should exist'),
);
}
/**
* Check whether one question comes before another on the Edit quiz page.
* The two questions must be on the same page.
* @Given /^I should see "(?P<first_q_name>(?:[^"]|\\")*)" before "(?P<second_q_name>(?:[^"]|\\")*)" on the edit quiz page$/
* @param string $firstquestionname the name of the question that should come first in order.
* @param string $secondquestionname the name of the question that should come immediately after it in order.
* @return array of steps.
*/
public function i_should_see_before_on_the_edit_quiz_page($firstquestionname, $secondquestionname) {
$xpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($firstquestionname) .
"')]/following-sibling::li[contains(@class, ' slot ')][1]" .
"[contains(., '" . $this->escape($secondquestionname) . "')]";
return array(
new Given('"' . $xpath . '" "xpath_element" should exist'),
);
}
/**
* Check the number displayed alongside a question on the Edit quiz page.
* @Given /^"(?P<question_name>(?:[^"]|\\")*)" should have number "(?P<number>(?:[^"]|\\")*)" on the edit quiz page$/
* @param string $questionname the name of the question we are looking for.
* @param number $number the number (or 'i') that should be displayed beside that question.
* @return array of steps.
*/
public function should_have_number_on_the_edit_quiz_page($questionname, $number) {
$xpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
"')]//span[@class = 'slotnumber' and normalize-space(text()) = '" . $this->escape($number) . "']";
return array(
new Given('"' . $xpath . '" "xpath_element" should exist'),
);
}
/**
* Click the add or remove page-break icon after a particular question.
* @When /^I click on the "(Add|Remove)" page break icon after question "(?P<question_name>(?:[^"]|\\")*)"$/
* @param string $addorremoves 'Add' or 'Remove'.
* @param string $questionname the name of the question before the icon to click.
* @return array of steps.
*/
public function i_click_on_the_page_break_icon_after_question($addorremoves, $questionname) {
$xpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
"')]//a[@class = 'page_split_join' and @title = '" . $addorremoves . " page break']";
return array(
new Given('I click on "' . $xpath . '" "xpath_element"'),
);
}
/**
* Move a question on the Edit quiz page by first clicking on the Move icon,
* then clicking one of the "After ..." links.
* @When /^I move "(?P<question_name>(?:[^"]|\\")*)" to "(?P<target>(?:[^"]|\\")*)" in the quiz by clicking the move icon$/
* @param string $questionname the name of the question we are looking for.
* @param string $target the target place to move to. One of the links in the pop-up like
* "After Page 1" or "After Question N".
* @return array of steps.
*/
public function i_move_question_after_item_by_clicking_the_move_icon($questionname, $target) {
$iconxpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
"')]//span[contains(@class, 'editing_move')]";
return array(
new Given('I click on "' . $iconxpath . '" "xpath_element"'),
new Given('I click on "' . $this->escape($target) . '" "text"'),
);
}
/**
* Move a question on the Edit quiz page by dragging a given question on top of another item.
* @When /^I move "(?P<question_name>(?:[^"]|\\")*)" to "(?P<target>(?:[^"]|\\")*)" in the quiz by dragging$/
* @param string $questionname the name of the question we are looking for.
* @param string $target the target place to move to. Ether a question name, or "Page N"
* @return array of steps.
*/
public function i_move_question_after_item_by_dragging($questionname, $target) {
$iconxpath = "//li[contains(@class, ' slot ') and contains(., '" . $this->escape($questionname) .
"')]//span[contains(@class, 'editing_move')]//img";
$destinationxpath = "//li[contains(@class, ' slot ') or contains(@class, 'pagenumber ')]" .
"[contains(., '" . $this->escape($target) . "')]";
return array(
new Given('I drag "' . $iconxpath . '" "xpath_element" ' .
'and I drop it in "' . $destinationxpath . '" "xpath_element"'),
);
}
}
+255
View File
@@ -0,0 +1,255 @@
@mod @mod_quiz
Feature: Edit quiz page - adding things
In order to build the quiz I want my students to attempt
As a teacher
I need to be able to add questions to the quiz.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 for testing the Add menu | C1 | quiz1 |
And I log in as "teacher1"
And I follow "Course 1"
And I follow "Quiz 1"
And I navigate to "Edit quiz" node in "Quiz administration"
Then I should see "Editing quiz: Quiz 1"
@javascript
Scenario: Add some new question to the quiz using '+ a new question' options of the 'Add' menu.
And I open the "last" add to quiz menu
And I follow "a new question"
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 01 new"
And I set the field "Question text" to "Please write 200 words about Essay 01"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 01 new" on quiz page "1"
And I open the "Page 1" add to quiz menu
And I follow "a new question"
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 02 new"
And I set the field "Question text" to "Please write 200 words about Essay 02"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I open the "Page 1" add to quiz menu
And I follow "a new question"
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 03 new"
And I set the field "Question text" to "Please write 300 words about Essay 03"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I open the "Page 1" add to quiz menu
And I follow "a new question"
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 04 new"
And I set the field "Question text" to "Please write 300 words about Essay 04"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I should see "Essay 04 new" on quiz page "1"
# Repaginate as two questions per page.
And I should not see "Page 2"
When I press "Repaginate"
Then I should see "Repaginate with"
And I set the field "menuquestionsperpage" to "2"
When I press "Go"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "2"
And I should see "Essay 04 new" on quiz page "2"
# Add a question to page 2.
When I open the "Page 2" add to quiz menu
And I follow "a new question" in the open menu
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
When I set the field "Question name" to "Essay for page 2"
And I set the field "Question text" to "Please write 200 words about Essay for page 2"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "2"
And I should see "Essay 04 new" on quiz page "2"
And I should see "Essay for page 2" on quiz page "2"
@javascript
Scenario: Add questions from question bank to the quiz. In order to be able to
add questions from question bank to the quiz, first we create some new questions
in various categories and add them to the question bank.
# Create a couple of sub categories.
And I follow "Course 1"
And I navigate to "Categories" node in "Course administration > Question bank"
Then I should see "Add category"
Then I set the field "Parent category" to "Default for C1"
And I set the field "Name" to "Subcat 1"
And I set the field "Category info" to "This is sub category 1"
Then I press "id_submitbutton"
And I should see "Subcat 1"
Then I set the field "Parent category" to "Default for C1"
And I set the field "Name" to "Subcat 2"
And I set the field "Category info" to "This is sub category 2"
Then I press "id_submitbutton"
And I should see "Subcat 2"
And I navigate to "Questions" node in "Course administration > Question bank"
Then I should see "Question bank"
And I should see "Select a category"
# Create the Essay 01 question.
When I press "Create a new question ..."
And I set the field "qtype_qtype_essay" to "1"
And I press "Add"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 01"
And I set the field "Question text" to "Please write 100 words about Essay 01"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "Essay 01"
# Create the Essay 02 question.
And I should see "Select a category"
And I set the field "Select a category:" to "Subcat 1"
When I press "Create a new question ..."
And I set the field "qtype_qtype_essay" to "1"
And I press "Add"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 02"
And I set the field "Question text" to "Please write 200 words about Essay 02"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "Essay 02"
# Create the Essay 03 question.
And I set the field "Select a category" to "Default for C1"
When I press "Create a new question ..."
And I set the field "qtype_qtype_essay" to "1"
And I press "Add"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 03"
And I set the field "Question text" to "Please write 300 words about Essay 03"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "Essay 03"
# Create the TF 01 question.
When I press "Create a new question ..."
And I set the field "qtype_qtype_truefalse" to "1"
And I press "Add"
Then I should see "Adding a True/False question"
And I set the field "Question name" to "TF 01"
And I set the field "Question text" to "The correct answer is true"
And I set the field "Correct answer" to "True"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "TF 01"
# Create the TF 02 question.
When I press "Create a new question ..."
And I set the field "qtype_qtype_truefalse" to "1"
And I press "Add"
Then I should see "Adding a True/False question"
And I set the field "Question name" to "TF 02"
And I set the field "Question text" to "The correct answer is false"
And I set the field "Correct answer" to "False"
And I press "id_submitbutton"
Then I should see "Question bank"
And I should see "TF 02"
# Add questions from question bank using the Add menu.
# Add Essay 03 from question bank.
And I follow "Course 1"
And I follow "Quiz 1"
And I follow "Edit quiz"
And I open the "last" add to quiz menu
And I follow "from question bank"
And I click on "Add to quiz" "link" in the "Essay 03" "table_row"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 03" on quiz page "1"
# Add Essay 01 from question bank.
And I open the "Page 1" add to quiz menu
And I follow "from question bank"
And I click on "Add to quiz" "link" in the "Essay 01" "table_row"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "1"
# Add Esay 02 from question bank.
And I open the "Page 1" add to quiz menu
And I follow "from question bank"
And I should see "Select a category"
And I set the field "Select a category" to "Subcat 1"
And I click on "Add to quiz" "link" in the "Essay 02" "table_row"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "1"
And I should see "Essay 02" on quiz page "1"
# Add a random question.
And I open the "Page 1" add to quiz menu
And I follow "a random question"
And I press "Add random question"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "1"
And I should see "Essay 02" on quiz page "1"
And I should see "Random" on quiz page "1"
# Repaginate as one question per page.
And I should not see "Page 2"
When I press "Repaginate"
Then I should see "Repaginate with"
And I set the field "menuquestionsperpage" to "1"
When I press "Go"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "2"
And I should see "Essay 02" on quiz page "3"
And I should see "Random" on quiz page "4"
# Add a random question to page 4.
And I open the "Page 4" add to quiz menu
And I follow "a new question" in the open menu
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay for page 4"
And I set the field "Question text" to "Please write 200 words about Essay for page 4"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 03" on quiz page "1"
And I should see "Essay 01" on quiz page "2"
And I should see "Essay 02" on quiz page "3"
And I should see "Random" on quiz page "4"
And I should see "Essay for page 4" on quiz page "4"
@@ -0,0 +1,98 @@
@mod @mod_quiz
Feature: Edit quiz page - drag-and-drop
In order to change the layout of a quiz I built
As a teacher
I need to be able to drag and drop questions to reorder them.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | course | idnumber |
| quiz | Quiz 1 | C1 | quiz1 |
And I log in as "teacher1"
And I follow "Course 1"
And I add a "True/False" question to the "Quiz 1" quiz with:
| Question name | Question A |
| Question text | Answer me |
And I add a "True/False" question to the "Quiz 1" quiz with:
| Question name | Question B |
| Question text | Answer again |
And I add a "True/False" question to the "Quiz 1" quiz with:
| Question name | Question C |
| Question text | And again |
And I click on the "Add" page break icon after question "Question B"
@javascript
Scenario: Re-order questions by clicking on the move icon.
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "2"
When I move "Question A" to "After Question 2" in the quiz by clicking the move icon
Then I should see "Question B" on quiz page "1"
And I should see "Question A" on quiz page "1"
And I should see "Question B" before "Question A" on the edit quiz page
And I should see "Question C" on quiz page "2"
When I move "Question A" to "After Page 2" in the quiz by clicking the move icon
Then I should see "Question B" on quiz page "1"
And I should see "Question A" on quiz page "2"
And I should see "Question C" on quiz page "2"
And I should see "Question A" before "Question C" on the edit quiz page
When I move "Question B" to "After Question 2" in the quiz by clicking the move icon
Then I should see "Question A" on quiz page "1"
And I should see "Question B" on quiz page "1"
And I should see "Question C" on quiz page "1"
And I should see "Question A" before "Question B" on the edit quiz page
And I should see "Question B" before "Question C" on the edit quiz page
When I move "Question B" to "After Page 1" in the quiz by clicking the move icon
Then I should see "Question B" on quiz page "1"
And I should see "Question A" on quiz page "1"
And I should see "Question C" on quiz page "1"
And I should see "Question B" before "Question A" on the edit quiz page
And I should see "Question A" before "Question C" on the edit quiz page
When I click on the "Add" page break icon after question "Question A"
When I open the "Page 2" add to quiz menu
And I follow "a new question" in the open menu
And I set the field "qtype_qtype_description" to "1"
And I press "submitbutton"
Then I should see "Adding a description"
And I set the following fields to these values:
| Question name | Question D |
| Question text | Useful info |
And I press "id_submitbutton"
Then I should see "Question B" on quiz page "1"
And I should see "Question A" on quiz page "1"
And I should see "Question C" on quiz page "2"
And I should see "Question D" on quiz page "2"
And I should see "Question B" before "Question A" on the edit quiz page
And I should see "Question C" before "Question D" on the edit quiz page
And "Question B" should have number "1" on the edit quiz page
And "Question A" should have number "2" on the edit quiz page
And "Question C" should have number "3" on the edit quiz page
And "Question D" should have number "i" on the edit quiz page
When I move "Question D" to "After Question 2" in the quiz by clicking the move icon
Then I should see "Question B" on quiz page "1"
And I should see "Question D" on quiz page "1"
And I should see "Question A" on quiz page "1"
And I should see "Question C" on quiz page "2"
And I should see "Question B" before "Question A" on the edit quiz page
And I should see "Question A" before "Question D" on the edit quiz page
And "Question B" should have number "1" on the edit quiz page
And "Question D" should have number "i" on the edit quiz page
And "Question A" should have number "2" on the edit quiz page
And "Question C" should have number "3" on the edit quiz page
@@ -0,0 +1,154 @@
@mod @mod_quiz
Feature: Edit quiz page - pagination
In order to build a quiz laid out in pages the way I want
As a teacher
I need to be able to add and remove pages, and repaginate.
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "activities" exist:
| activity | name | intro | course | idnumber |
| quiz | Quiz 1 | Quiz 1 description | C1 | quiz1 |
When I log in as "teacher1"
And I follow "Course 1"
And I follow "Quiz 1"
And I follow "Edit quiz"
@javascript
Scenario: Repaginate questions with N question(s) per page as well as clicking
on "add page break" or "Remove page break" icons to repaginate in any desired format.
Then I should see "Editing quiz: Quiz 1"
# Add the first Essay question.
And I follow "Add"
And I follow "a new question"
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 01 new"
And I set the field "Question text" to "Please write 100 words about Essay 01"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 01 new" on quiz page "1"
# Add the second Essay question.
And I follow "Add"
And I follow "a new question"
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 02 new"
And I set the field "Question text" to "Please write 200 words about Essay 02"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
# Start repaginating.
And I should not see "Page 2"
When I click on the "Add" page break icon after question "Essay 01 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
When I click on the "Remove" page break icon after question "Essay 01 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should not see "Page 2"
# Add the third Essay question.
And I follow "Add"
And I follow "a new question"
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
And I set the field "Question name" to "Essay 03 new"
And I set the field "Question text" to "Please write 200 words about Essay 03"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I should not see "Page 2"
And I should not see "Page 3"
When I click on the "Add" page break icon after question "Essay 02 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "2"
And I should not see "Page 3"
When I click on the "Add" page break icon after question "Essay 01 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "3"
When I click on the "Remove" page break icon after question "Essay 02 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "2"
And I should not see "Page 3"
When I click on the "Remove" page break icon after question "Essay 01 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I should not see "Page 2"
And I should not see "Page 3"
# Repaginate one question per page.
When I press "Repaginate"
And I set the field "menuquestionsperpage" to "1"
And I press "Go"
Then I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "3"
# Add the forth Essay question in a new page (Page 4).
When I open the "Page 3" add to quiz menu
And I follow "a new question" in the open menu
And I set the field "qtype_qtype_essay" to "1"
And I press "submitbutton"
Then I should see "Adding an Essay question"
When I set the field "Question name" to "Essay 04 new"
And I set the field "Question text" to "Please write 300 words about Essay 04"
And I press "id_submitbutton"
Then I should see "Editing quiz: Quiz 1"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "3"
And I should see "Essay 04 new" on quiz page "3"
When I click on the "Add" page break icon after question "Essay 03 new"
And I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "2"
And I should see "Essay 03 new" on quiz page "3"
And I should see "Essay 04 new" on quiz page "4"
# Repaginate with 2 questions per page.
When I press "Repaginate"
And I set the field "menuquestionsperpage" to "2"
And I press "Go"
Then I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "2"
And I should see "Essay 04 new" on quiz page "2"
# Repaginate with unlimited questions per page (All questions on Page 1).
When I press "Repaginate"
And I set the field "menuquestionsperpage" to "Unlimited"
And I press "Go"
Then I should see "Essay 01 new" on quiz page "1"
And I should see "Essay 02 new" on quiz page "1"
And I should see "Essay 03 new" on quiz page "1"
And I should see "Essay 04 new" on quiz page "1"
@@ -0,0 +1,77 @@
@mod @mod_quiz
Feature: In order to create a quiz that awards marks the way I want
As a teacher
I must be able to set the marks I want 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 "activities" exist:
| activity | name | course | idnumber | grade | decimalpoints | questiondecimalpoints |
| quiz | Quiz 1 | C1 | quiz1 | 20 | 2 | -1 |
And I log in as "teacher1"
And I follow "Course 1"
And I add a "True/False" question to the "Quiz 1" quiz with:
| Question name | First question |
| Question text | Answer me |
| Default mark | 2.0 |
And I add a "True/False" question to the "Quiz 1" quiz with:
| Question name | Second question |
| Question text | Answer again |
| Default mark | 3.0 |
@javascript
Scenario: Set the max mark for a question.
When I set the max mark for question "First question" to "7.0"
Then I should see "7.00"
And I should see "3.00"
And I should see "Total of marks: 10.00"
When I follow "Edit maximum mark"
And I wait until "li input[name=maxmark]" "css_element" exists
And I take focus off "li input[name=maxmark]" "css_element"
Then I should see "7.00"
And I should see "3.00"
And I should see "Total of marks: 10.00"
And "li input[name=maxmark]" "css_element" should not exist
@javascript
Scenario: Set the overall Maximum grade.
When I set the field "maxgrade" to "10.0"
And I press "savechanges"
Then the field "maxgrade" matches value "10.00"
And I should see "2.00"
And I should see "3.00"
And I should see "Total of marks: 5.00"
@javascript
Scenario: Verify the number of decimal places shown is what the quiz settings say it should be.
# Then the field "maxgrade" matches value "20.00" -- with exact match on decimal places.
Then "//input[@name = 'maxgrade' and @value = '20.00']" "xpath_element" should exist
And I should see "2.00"
And I should see "3.00"
And I should see "Total of marks: 5.00"
And I should not see "2.000"
And I should not see "3.000"
And I should not see "Total of marks: 5.000"
When I follow "Edit settings"
And I set the following fields to these values:
| Decimal places in grades | 3 |
| Decimal places in question grades | 5 |
And I press "Save and display"
And I follow "Edit quiz"
# Then the field "maxgrade" matches value "20.000" -- with exact match on decimal places.
Then "//input[@name = 'maxgrade' and @value = '20.000']" "xpath_element" should exist
And I should see "2.00000"
And I should see "3.00000"
And I should see "Total of marks: 5.000"
And I should not see "2.000000"
And I should not see "3.000000"
And I should not see "Total of marks: 5.0000"
-116
View File
@@ -1,116 +0,0 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for (some of) mod/quiz/editlib.php.
*
* @package mod_quiz
* @category phpunit
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/editlib.php');
/**
* Unit tests for (some of) mod/quiz/editlib.php.
*
* @copyright 2009 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_quiz_editlib_testcase extends advanced_testcase {
public function test_quiz_question_tostring() {
$question = new stdClass();
$question->qtype = 'multichoice';
$question->name = 'The question name';
$question->questiontext = '<p>What sort of <b>inequality</b> is x &lt; y<img alt="?" src="..."></p>';
$question->questiontextformat = FORMAT_HTML;
$summary = quiz_question_tostring($question);
$this->assertEquals('<span class="questionname">The question name</span>' .
'<span class="questiontext">What sort of INEQUALITY is x &lt; y[?]</span>', $summary);
}
/**
* Test removing slots from a quiz.
*/
public function test_quiz_remove_slot() {
global $SITE, $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
// Setup a quiz with 1 standard and 1 random question.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(array('course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0));
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$standardq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
quiz_add_quiz_question($standardq->id, $quiz);
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
// Get the random question.
$randomq = $DB->get_record('question', array('qtype' => 'random'));
$slotssql = "SELECT qs.*, q.qtype AS qtype
FROM {quiz_slots} qs
JOIN {question} q ON qs.questionid = q.id
WHERE qs.quizid = ?
ORDER BY qs.slot";
$slots = $DB->get_records_sql($slotssql, array($quiz->id));
// Check that the setup looks right.
$this->assertEquals(2, count($slots));
$slot = array_shift($slots);
$this->assertEquals($standardq->id, $slot->questionid);
$slot = array_shift($slots);
$this->assertEquals($randomq->id, $slot->questionid);
$this->assertEquals(2, $slot->slot);
// Remove the standard question.
quiz_remove_slot($quiz, 1);
$slots = $DB->get_records_sql($slotssql, array($quiz->id));
// Check the new ordering, and that the slot number was updated.
$this->assertEquals(1, count($slots));
$slot = array_shift($slots);
$this->assertEquals($randomq->id, $slot->questionid);
$this->assertEquals(1, $slot->slot);
// Check the the standard question was not deleted.
$count = $DB->count_records('question', array('id' => $standardq->id));
$this->assertEquals(1, $count);
// Remove the random question.
quiz_remove_slot($quiz, 1);
$slots = $DB->get_records_sql($slotssql, array($quiz->id));
// Check that new ordering.
$this->assertEquals(0, count($slots));
// Check that the random question was deleted.
$count = $DB->count_records('question', array('id' => $randomq->id));
$this->assertEquals(0, $count);
}
}
+15 -1
View File
@@ -18,7 +18,7 @@
* Unit tests for (some of) mod/quiz/locallib.php.
*
* @package mod_quiz
* @category phpunit
* @category test
* @copyright 2008 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
*/
@@ -58,6 +58,20 @@ class mod_quiz_lib_testcase extends advanced_testcase {
$this->assertEquals(quiz_format_grade($quiz, 0.12345678), '0');
}
public function test_quiz_get_grade_format() {
$quiz = new stdClass();
$quiz->decimalpoints = 2;
$this->assertEquals(quiz_get_grade_format($quiz), 2);
$this->assertEquals($quiz->questiondecimalpoints, -1);
$quiz->questiondecimalpoints = 2;
$this->assertEquals(quiz_get_grade_format($quiz), 2);
$quiz->decimalpoints = 3;
$quiz->questiondecimalpoints = -1;
$this->assertEquals(quiz_get_grade_format($quiz), 3);
$quiz->questiondecimalpoints = 4;
$this->assertEquals(quiz_get_grade_format($quiz), 4);
}
public function test_quiz_format_question_grade() {
$quiz = new stdClass();
$quiz->decimalpoints = 2;
+13 -1
View File
@@ -18,7 +18,7 @@
* Unit tests for (some of) mod/quiz/locallib.php.
*
* @package mod_quiz
* @category phpunit
* @category test
* @copyright 2008 Tim Hunt
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
@@ -142,4 +142,16 @@ class mod_quiz_locallib_testcase extends basic_testcase {
$this->assertEquals(mod_quiz_display_options::AFTER_CLOSE, quiz_attempt_state($quiz, $attempt));
}
public function test_quiz_question_tostring() {
$question = new stdClass();
$question->qtype = 'multichoice';
$question->name = 'The question name';
$question->questiontext = '<p>What sort of <b>inequality</b> is x &lt; y<img alt="?" src="..."></p>';
$question->questiontextformat = FORMAT_HTML;
$summary = quiz_question_tostring($question);
$this->assertEquals('<span class="questionname">The question name</span> ' .
'<span class="questiontext">What sort of INEQUALITY is x &lt; y[?]</span>', $summary);
}
}
+295
View File
@@ -0,0 +1,295 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Unit tests for the {@link \mod_quiz\repaginate} class.
* @package mod_quiz
* @category test
* @copyright 2014 The Open Univsersity
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/editlib.php');
require_once($CFG->dirroot . '/mod/quiz/locallib.php');
require_once($CFG->dirroot . '/mod/quiz/classes/repaginate.php');
/**
* Testable subclass, giving access to the protected methods of {@link \mod_quiz\repaginate}
* @copyright 2014 The Open Univsersity
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_quiz_repaginate_testable extends \mod_quiz\repaginate {
public function __construct($quizid = 0, $slots = null) {
return parent::__construct($quizid, $slots);
}
public function get_this_slot($slots, $slotnumber) {
return parent::get_this_slot($slots, $slotnumber);
}
public function get_slots_by_slotid($slots = null) {
return parent::get_slots_by_slotid($slots);
}
public function get_slots_by_slot_number($slots = null) {
return parent::get_slots_by_slot_number($slots);
}
public function repaginate_this_slot($slot, $newpagenumber) {
return parent::repaginate_this_slot($slot, $newpagenumber);
}
public function repaginate_next_slot($nextslotnumber, $type) {
return parent::repaginate_next_slot($nextslotnumber, $type);
}
}
/**
* Test for some parts of the repaginate class.
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_quiz_repaginate_test extends advanced_testcase {
/** @var array stores the slots. */
private $quizslots;
/** @var mod_quiz_repaginate_testable the object being tested. */
private $repaginate = null;
public function setUp() {
$this->set_quiz_slots($this->get_quiz_object()->get_slots());
$this->repaginate = new mod_quiz_repaginate_testable(0, $this->quizslots);
}
public function tearDown() {
$this->repaginate = null;
}
/**
* Create a quiz, add five questions to the quiz
* which are all on one page and return the quiz object.
*/
private function get_quiz_object() {
global $SITE;
$this->resetAfterTest(true);
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(array(
'course' => $SITE->id, 'questionsperpage' => 0, 'grade' => 100.0, 'sumgrades' => 2));
// Create five questions.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$shortanswer = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
$numerical = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
$essay = $questiongenerator->create_question('essay', null, array('category' => $cat->id));
$truefalse = $questiongenerator->create_question('truefalse', null, array('category' => $cat->id));
$match = $questiongenerator->create_question('match', null, array('category' => $cat->id));
// Add them to the quiz.
quiz_add_quiz_question($shortanswer->id, $quiz);
quiz_add_quiz_question($numerical->id, $quiz);
quiz_add_quiz_question($essay->id, $quiz);
quiz_add_quiz_question($truefalse->id, $quiz);
quiz_add_quiz_question($match->id, $quiz);
// Return the quiz object.
return \mod_quiz\structure::create_for($quiz);
}
/**
* Set the quiz slots
* @param string $slots
*/
private function set_quiz_slots($slots = null) {
if (!$slots) {
$this->quizslots = $this->get_quiz_object()->get_slots();
} else {
$this->quizslots = $slots;
}
}
/**
* Test the get_this_slot() method
*/
public function test_get_this_slot() {
$this->set_quiz_slots();
$actual = array();
$expected = $this->repaginate->get_slots_by_slot_number();
$this->assertEquals($expected, $actual);
$slotsbyno = $this->repaginate->get_slots_by_slot_number($this->quizslots);
$slotnumber = 5;
$thisslot = $this->repaginate->get_this_slot($this->quizslots, $slotnumber);
$this->assertEquals($slotsbyno[$slotnumber], $thisslot);
}
public function test_get_slots_by_slotnumber() {
$this->set_quiz_slots();
$expected = array();
$actual = $this->repaginate->get_slots_by_slot_number();
$this->assertEquals($expected, $actual);
foreach ($this->quizslots as $slot) {
$expected[$slot->slot] = $slot;
}
$actual = $this->repaginate->get_slots_by_slot_number($this->quizslots);
$this->assertEquals($expected, $actual);
}
public function test_get_slots_by_slotid() {
$this->set_quiz_slots();
$actual = $this->repaginate->get_slots_by_slotid();
$this->assertEquals(array(), $actual);
$slotsbyno = $this->repaginate->get_slots_by_slot_number($this->quizslots);
$actual = $this->repaginate->get_slots_by_slotid($slotsbyno);
$this->assertEquals($this->quizslots, $actual);
}
public function test_repaginate_n_questions_per_page() {
$this->set_quiz_slots();
// Expect 2 questions per page.
$expected = array();
foreach ($this->quizslots as $slot) {
// Page 1 contains Slots 1 and 2.
if ($slot->slot >= 1 && $slot->slot <= 2) {
$slot->page = 1;
}
// Page 2 contains slots 3 and 4.
if ($slot->slot >= 3 && $slot->slot <= 4) {
$slot->page = 2;
}
// Page 3 contains slots 5.
if ($slot->slot >= 5 && $slot->slot <= 6) {
$slot->page = 3;
}
$expected[$slot->id] = $slot;
}
$actual = $this->repaginate->repaginate_n_question_per_page($this->quizslots, 2);
$this->assertEquals($expected, $actual);
// Expect 3 questions per page.
$expected = array();
foreach ($this->quizslots as $slot) {
// Page 1 contains Slots 1, 2 and 3.
if ($slot->slot >= 1 && $slot->slot <= 3) {
$slot->page = 1;
}
// Page 2 contains slots 4 and 5.
if ($slot->slot >= 4 && $slot->slot <= 6) {
$slot->page = 2;
}
$expected[$slot->id] = $slot;
}
$actual = $this->repaginate->repaginate_n_question_per_page($this->quizslots, 3);
$this->assertEquals($expected, $actual);
// Expect 5 questions per page.
$expected = array();
foreach ($this->quizslots as $slot) {
// Page 1 contains Slots 1, 2, 3, 4 and 5.
if ($slot->slot > 0 && $slot->slot < 6) {
$slot->page = 1;
}
// Page 2 contains slots 6, 7, 8, 9 and 10.
if ($slot->slot > 5 && $slot->slot < 11) {
$slot->page = 2;
}
$expected[$slot->id] = $slot;
}
$actual = $this->repaginate->repaginate_n_question_per_page($this->quizslots, 5);
$this->assertEquals($expected, $actual);
// Expect 10 questions per page.
$expected = array();
foreach ($this->quizslots as $slot) {
// Page 1 contains Slots 1 to 10.
if ($slot->slot >= 1 && $slot->slot <= 10) {
$slot->page = 1;
}
// Page 2 contains slots 11 to 20.
if ($slot->slot >= 11 && $slot->slot <= 20) {
$slot->page = 2;
}
$expected[$slot->id] = $slot;
}
$actual = $this->repaginate->repaginate_n_question_per_page($this->quizslots, 10);
$this->assertEquals($expected, $actual);
// Expect 1 questions per page.
$expected = array();
$page = 1;
foreach ($this->quizslots as $slot) {
$slot->page = $page++;
$expected[$slot->id] = $slot;
}
$actual = $this->repaginate->repaginate_n_question_per_page($this->quizslots, 1);
$this->assertEquals($expected, $actual);
}
public function test_repaginate_this_slot() {
$this->set_quiz_slots();
$slotsbyslotno = $this->repaginate->get_slots_by_slot_number($this->quizslots);
$slotnumber = 3;
$newpagenumber = 2;
$thisslot = $slotsbyslotno[3];
$thisslot->page = $newpagenumber;
$expected = $thisslot;
$actual = $this->repaginate->repaginate_this_slot($slotsbyslotno[3], $newpagenumber);
$this->assertEquals($expected, $actual);
}
public function test_repaginate_the_rest() {
$this->set_quiz_slots();
$slotfrom = 1;
$type = \mod_quiz\repaginate::LINK;
$expected = array();
foreach ($this->quizslots as $slot) {
if ($slot->slot > $slotfrom) {
$slot->page = $slot->page - 1;
$expected[$slot->id] = $slot;
}
}
$actual = $this->repaginate->repaginate_the_rest($this->quizslots, $slotfrom, $type, false);
$this->assertEquals($expected, $actual);
$slotfrom = 2;
$newslots = array();
foreach ($this->quizslots as $s) {
if ($s->slot === $slotfrom) {
$s->page = $s->page - 1;
}
$newslots[$s->id] = $s;
}
$type = \mod_quiz\repaginate::UNLINK;
$expected = array();
foreach ($this->quizslots as $slot) {
if ($slot->slot > ($slotfrom - 1)) {
$slot->page = $slot->page - 1;
$expected[$slot->id] = $slot;
}
}
$actual = $this->repaginate->repaginate_the_rest($newslots, $slotfrom, $type, false);
$this->assertEquals($expected, $actual);
}
}
+407
View File
@@ -0,0 +1,407 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Quiz events tests.
*
* @package mod_quiz
* @category test
* @copyright 2013 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/mod/quiz/attemptlib.php');
require_once($CFG->dirroot . '/mod/quiz/editlib.php');
/**
* Unit tests for quiz events.
*
* @copyright 2013 Adrian Greeve
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class mod_quiz_structure_testcase extends advanced_testcase {
/**
* Prepare the quiz object with standard data. Ready for testing.
*/
protected function prepare_quiz_data() {
$this->resetAfterTest(true);
// Create a course.
$course = $this->getDataGenerator()->create_course();
// Make a quiz.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(array('course' => $course->id, 'questionsperpage' => 0,
'grade' => 100.0, 'sumgrades' => 2));
$cm = get_coursemodule_from_instance('quiz', $quiz->id, $course->id);
return array($quiz, $cm, $course);
}
/**
* Test getting the quiz slots.
*/
public function test_get_quiz_slots() {
// Get basic quiz.
list($quiz, $cm, $course) = $this->prepare_quiz_data();
$quizobj = new quiz($quiz, $cm, $course);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
// When no slots exist or slots propery is not set.
$slots = $structure->get_slots();
$this->assertInternalType('array', $slots);
$this->assertCount(0, $slots);
// Append slots to the quiz.
$this->add_eight_questions_to_the_quiz($quiz);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
// Are the correct slots returned?
$slots = $structure->get_slots();
$this->assertCount(8, $slots);
}
/**
* Test getting the quiz sections.
*/
public function test_get_quiz_sections() {
// Get basic quiz.
list($quiz, $cm, $course) = $this->prepare_quiz_data();
$quizobj = new quiz($quiz, $cm, $course);
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
// Are the correct sections returned?
$sections = $structure->get_quiz_sections();
$this->assertCount(1, $sections);
}
/**
* Verify that the given layout matches that expected.
* @param array $expectedlayout
* @param \mod_quiz\structure $structure
*/
protected function assert_quiz_layout($expectedlayout, \mod_quiz\structure $structure) {
$slotnumber = 0;
foreach ($expectedlayout as $slotid => $page) {
$slotnumber += 1;
$this->assertEquals($slotid, $structure->get_question_in_slot($slotnumber)->slotid,
'Wrong question in slot ' . $slotnumber);
$this->assertEquals($page, $structure->get_question_in_slot($slotnumber)->page,
'Wrong page number for slot ' . $slotnumber);
}
}
/**
* Test moving slots in the quiz.
*/
public function test_move_slot() {
// 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;
}
// Don't actually move anything. Check the layout is unchanged.
$idmove = $structure->get_question_in_slot(2)->slotid;
$idbefore = $structure->get_question_in_slot(1)->slotid;
$structure->move_slot($idmove, $idbefore, 2);
// Having called move, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[1] => 1,
$originalslotids[2] => 2,
$originalslotids[3] => 2,
$originalslotids[4] => 2,
$originalslotids[5] => 2,
$originalslotids[6] => 2,
$originalslotids[7] => 3,
$originalslotids[8] => 4,
), $structure);
// Slots don't move. Page changed.
$idmove = $structure->get_question_in_slot(2)->slotid;
$idbefore = $structure->get_question_in_slot(1)->slotid;
$structure->move_slot($idmove, $idbefore, 1);
// Having called move, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[1] => 1,
$originalslotids[2] => 1,
$originalslotids[3] => 2,
$originalslotids[4] => 2,
$originalslotids[5] => 2,
$originalslotids[6] => 2,
$originalslotids[7] => 3,
$originalslotids[8] => 4,
), $structure);
// Slots move 2 > 3. Page unchanged. Pages not reordered.
$idmove = $structure->get_question_in_slot(2)->slotid;
$idbefore = $structure->get_question_in_slot(3)->slotid;
$structure->move_slot($idmove, $idbefore, '2');
// Having called move, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[1] => 1,
$originalslotids[3] => 2,
$originalslotids[2] => 2,
$originalslotids[4] => 2,
$originalslotids[5] => 2,
$originalslotids[6] => 2,
$originalslotids[7] => 3,
$originalslotids[8] => 4,
), $structure);
// Slots move 6 > 7. Page changed. Pages not reordered.
$idmove = $structure->get_question_in_slot(6)->slotid;
$idbefore = $structure->get_question_in_slot(7)->slotid;
$structure->move_slot($idmove, $idbefore, '3');
// Having called move, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[1] => 1,
$originalslotids[3] => 2,
$originalslotids[2] => 2,
$originalslotids[4] => 2,
$originalslotids[5] => 2,
$originalslotids[7] => 3,
$originalslotids[6] => 3,
$originalslotids[8] => 4,
), $structure);
// Page changed slot 6 . Pages not reordered.
$idmove = $structure->get_question_in_slot(6)->slotid;
$idbefore = $structure->get_question_in_slot(5)->slotid;
$structure->move_slot($idmove, $idbefore, 2);
// Having called move, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[1] => 1,
$originalslotids[3] => 2,
$originalslotids[2] => 2,
$originalslotids[4] => 2,
$originalslotids[5] => 2,
$originalslotids[7] => 2,
$originalslotids[6] => 3,
$originalslotids[8] => 4,
), $structure);
// Slots move 1 > 2. Page changed. Page 2 becomes page 1. Pages reordered.
$idmove = $structure->get_question_in_slot(1)->slotid;
$idbefore = $structure->get_question_in_slot(2)->slotid;
$structure->move_slot($idmove, $idbefore, 2);
// Having called move, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[3] => 1,
$originalslotids[1] => 1,
$originalslotids[2] => 1,
$originalslotids[4] => 1,
$originalslotids[5] => 1,
$originalslotids[7] => 1,
$originalslotids[6] => 2,
$originalslotids[8] => 3,
), $structure);
// Slots move 7 > 3. Page changed. Page 3 becomes page 2. Pages reordered.
$idmove = $structure->get_question_in_slot(7)->slotid;
$idbefore = $structure->get_question_in_slot(2)->slotid;
$structure->move_slot($idmove, $idbefore, 1);
// Having called move, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[3] => 1,
$originalslotids[1] => 1,
$originalslotids[6] => 1,
$originalslotids[2] => 1,
$originalslotids[4] => 1,
$originalslotids[5] => 1,
$originalslotids[7] => 1,
$originalslotids[8] => 2,
), $structure);
// Slots move 2 > top. No page changes.
$idmove = $structure->get_question_in_slot(2)->slotid;
$structure->move_slot($idmove, 0, 1);
// Having called move, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[1] => 1,
$originalslotids[3] => 1,
$originalslotids[6] => 1,
$originalslotids[2] => 1,
$originalslotids[4] => 1,
$originalslotids[5] => 1,
$originalslotids[7] => 1,
$originalslotids[8] => 2,
), $structure);
}
/**
* Test removing slots from a quiz.
*/
public function test_quiz_remove_slot() {
global $SITE, $DB;
$this->resetAfterTest(true);
$this->setAdminUser();
// Setup a quiz with 1 standard and 1 random question.
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
$quiz = $quizgenerator->create_instance(array('course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0));
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
$standardq = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
quiz_add_quiz_question($standardq->id, $quiz);
quiz_add_random_questions($quiz, 0, $cat->id, 1, false);
// Get the random question.
$randomq = $DB->get_record('question', array('qtype' => 'random'));
$structure = \mod_quiz\structure::create_for($quiz);
// Check that the setup looks right.
$this->assertEquals(2, $structure->get_question_count());
$this->assertEquals($standardq->id, $structure->get_question_in_slot(1)->questionid);
$this->assertEquals($randomq->id, $structure->get_question_in_slot(2)->questionid);
// Remove the standard question.
$structure->remove_slot($quiz, 1);
$alteredstructure = \mod_quiz\structure::create_for($quiz);
// Check the new ordering, and that the slot number was updated.
$this->assertEquals(1, $alteredstructure->get_question_count());
$this->assertEquals($randomq->id, $alteredstructure->get_question_in_slot(1)->questionid);
// Check that the ordinary question was not deleted.
$this->assertTrue($DB->record_exists('question', array('id' => $standardq->id)));
// Remove the random question.
$structure->remove_slot($quiz, 1);
$alteredstructure = \mod_quiz\structure::create_for($quiz);
// Check that new ordering.
$this->assertEquals(0, $alteredstructure->get_question_count());
// Check that the random question was deleted.
$this->assertFalse($DB->record_exists('question', array('id' => $randomq->id)));
}
/**
* Test updating pagebreaks in the quiz.
*/
public function test_update_page_break() {
// 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 removing a page break.
$slotid = $structure->get_question_in_slot(2)->slotid;
$type = \mod_quiz\repaginate::LINK;
$slots = $structure->update_page_break($quiz, $slotid, $type);
// Having called update page break, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[1] => 1,
$originalslotids[2] => 1,
$originalslotids[3] => 1,
$originalslotids[4] => 1,
$originalslotids[5] => 1,
$originalslotids[6] => 1,
$originalslotids[7] => 2,
$originalslotids[8] => 3,
), $structure);
// Test adding a page break.
$slotid = $structure->get_question_in_slot(2)->slotid;
$type = \mod_quiz\repaginate::UNLINK;
$slots = $structure->update_page_break($quiz, $slotid, $type);
// Having called update page break, we need to reload $structure.
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
$this->assert_quiz_layout(array(
$originalslotids[1] => 1,
$originalslotids[2] => 2,
$originalslotids[3] => 2,
$originalslotids[4] => 2,
$originalslotids[5] => 2,
$originalslotids[6] => 2,
$originalslotids[7] => 3,
$originalslotids[8] => 4,
), $structure);
}
/**
* Populate quiz with eight questions.
* @param stdClass $quiz the quiz to add to.
*/
public function add_eight_questions_to_the_quiz($quiz) {
// We add 8 numerical questions with this layout:
// Slot 1 2 3 4 5 6 7 8
// Page 1 2 2 2 2 2 3 4.
// Create slots.
$pagenumber = 1;
$pagenumberdefaults = array(2, 7, 8);
// Create a couple of questions.
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
$cat = $questiongenerator->create_question_category();
for ($i = 0; $i < 8; $i ++) {
$numq = $questiongenerator->create_question('numerical', null, array('category' => $cat->id));
if (in_array($i + 1, $pagenumberdefaults)) {
$pagenumber++;
}
// Add them to the quiz.
quiz_add_quiz_question($numq->id, $quiz, $pagenumber);
}
}
}
+52
View File
@@ -1,5 +1,57 @@
This files describes API changes in the quiz code.
=== 2.8 ===
* Major changes to the Edit quiz page.
The goal of this work was to increase usability, and also clean up the page
enough that it will be possible to add new features in future.
Display of mod/quiz/edit.php is now entirely generated by
mod_quiz\output\edit_renderer. This uses a helper class mod_quiz\structure
to provide details of the structure of the quiz, and mod_quiz\repaginate to
alter that structure. (Actually, there are still some modification methods on
mod_quiz\structure. Expect that to be cleaned up in future.)
The new code uses much more ajax, and there are new scripts mod/quiz/edit_rest.php
and mod/quiz/repaginate.php to handle this. (Again, don't be surprised if those
two scripts get merged in future.) Also questionbank.ajax.php (which may, in
future, be made more generic, and moved into the core question bank code.)
As a result of this, mod/quiz/editlib.php is now much shorter than it was.
(In future, expect the remaining code in here to move into mod/quiz/classes.)
Here is a list of all the old functions or classes that have changed.
If you used any of these in custom code, you will need to update your code.
(Note that many of these functions should have been considered private internals
of the quiz module, and you should not have been using them!)
From editlib.php:
quiz_remove_slot
quiz_delete_empty_page
quiz_add_page_break_after_slot - Use methods of structure or repaginate
quiz_update_slot_maxmark - classes instead.
_quiz_move_question
quiz_move_question_up
quiz_move_question_down
quiz_print_question_list
quiz_print_pagecontrols
quiz_print_singlequestion - Use methods of edit_renderer instead.
quiz_print_randomquestion
quiz_print_singlequestion_reordertool
quiz_print_randomquestion_reordertool
print_random_option_icon
quiz_print_grading_form
quiz_print_status_bar
Moved from editlib.php to locallib.php:
quiz_question_tostring - now always returns a string (the only option used).
The $return argument has gone.
Old editing JavaScript (e.g. mod/quiz/edit.js) is gone. Replaced with YUI modules.
=== 2.7.1 ===
* The function quiz_fire_attempt_started_event has been removed. This function
+1 -1
View File
@@ -24,7 +24,7 @@
defined('MOODLE_INTERNAL') || die();
$plugin->version = 2014052801; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2014100200; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2014050800; // Requires this Moodle version.
$plugin->component = 'mod_quiz'; // Full name of the plugin (used for diagnostics).
$plugin->cron = 60;
@@ -0,0 +1,562 @@
YUI.add('moodle-mod_quiz-dragdrop', function (Y, NAME) {
/**
* Drag and Drop for Quiz sections and slots.
*
* @module moodle-mod-quiz-dragdrop
*/
var CSS = {
ACTIONAREA: '.actions',
ACTIVITY: 'activity',
ACTIVITYINSTANCE: 'activityinstance',
CONTENT: 'content',
COURSECONTENT: 'mod-quiz-edit-content',
EDITINGMOVE: 'editing_move',
ICONCLASS: 'iconsmall',
JUMPMENU: 'jumpmenu',
LEFT: 'left',
LIGHTBOX: 'lightbox',
MOVEDOWN: 'movedown',
MOVEUP: 'moveup',
PAGE : 'page',
PAGECONTENT: 'page-content',
RIGHT: 'right',
SECTION: 'section',
SECTIONADDMENUS: 'section_add_menus',
SECTIONHANDLE: 'section-handle',
SLOTS: 'slots',
SUMMARY: 'summary',
SECTIONDRAGGABLE: 'sectiondraggable'
},
// The CSS selectors we use.
SELECTOR = {
PAGE: 'li.page',
SLOT: 'li.slot'
};
/**
* Section drag and drop.
*
* @class M.mod_quiz.dragdrop.section
* @constructor
* @extends M.core.dragdrop
*/
var DRAGSECTION = function() {
DRAGSECTION.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGSECTION, M.core.dragdrop, {
sectionlistselector: null,
initializer: function() {
// Set group for parent class
this.groups = [ CSS.SECTIONDRAGGABLE ];
this.samenodeclass = M.mod_quiz.edit.get_sectionwrapperclass();
this.parentnodeclass = M.mod_quiz.edit.get_containerclass();
// Check if we are in single section mode
if (Y.Node.one('.' + CSS.JUMPMENU)) {
return false;
}
// Initialise sections dragging
this.sectionlistselector = M.mod_quiz.edit.get_section_wrapper(Y);
if (this.sectionlistselector) {
this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector;
this.setup_for_section(this.sectionlistselector);
// Make each li element in the lists of sections draggable
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: '.' + CSS.SECTIONDRAGGABLE,
target: true,
handles: ['.' + CSS.LEFT],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.PAGECONTENT,
stickY: true
});
del.dd.plug(Y.Plugin.DDWinScroll);
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
// Determine the section ID
var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode);
// We skip the top section as it is not draggable
if (sectionid > 0) {
// Remove move icons
var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN);
var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP);
// Add dragger icon
var title = M.util.get_string('movesection', 'moodle', sectionid);
var cssleft = sectionnode.one('.' + CSS.LEFT);
if ((movedown || moveup) && cssleft) {
cssleft.setStyle('cursor', 'move');
cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
if (moveup) {
moveup.remove();
}
if (movedown) {
movedown.remove();
}
// This section can be moved - add the class to indicate this to Y.DD.
sectionnode.addClass(CSS.SECTIONDRAGGABLE);
}
}
}, this);
},
/*
* Drag-dropping related functions
*/
drag_start: function(e) {
// Get our drag object
var drag = e.target;
// Creat a dummy structure of the outer elemnents for clean styles application
var containernode = Y.Node.create('<' + M.mod_quiz.edit.get_containernode() + '></' + M.mod_quiz.edit.get_containernode() + '>');
containernode.addClass(M.mod_quiz.edit.get_containerclass());
var sectionnode = Y.Node.create('<' + M.mod_quiz.edit.get_sectionwrappernode() + '></' + M.mod_quiz.edit.get_sectionwrappernode() + '>');
sectionnode.addClass( M.mod_quiz.edit.get_sectionwrapperclass());
sectionnode.setStyle('margin', 0);
sectionnode.setContent(drag.get('node').get('innerHTML'));
containernode.appendChild(sectionnode);
drag.get('dragNode').setContent(containernode);
drag.get('dragNode').addClass(CSS.COURSECONTENT);
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
get_section_index: function(node) {
var sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + M.mod_quiz.edit.get_section_selector(Y),
sectionList = Y.all(sectionlistselector),
nodeIndex = sectionList.indexOf(node),
zeroIndex = sectionList.indexOf(Y.one('#section-0'));
return (nodeIndex - zeroIndex);
},
drop_hit: function(e) {
var drag = e.drag;
// Get references to our nodes and their IDs.
var dragnode = drag.get('node'),
dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
loopstart = dragnodeid,
dropnodeindex = this.get_section_index(dragnode),
loopend = dropnodeindex;
if (dragnodeid === dropnodeindex) {
Y.log("Skipping move - same location moving " + dragnodeid + " to " + dropnodeindex, 'debug', 'moodle-mod_quiz-dragdrop');
return;
}
Y.log("Moving from position " + dragnodeid + " to position " + dropnodeindex, 'debug', 'moodle-mod_quiz-dragdrop');
if (loopstart > loopend) {
// If we're going up, we need to swap the loop order
// because loops can't go backwards.
loopstart = dropnodeindex;
loopend = dragnodeid;
}
// Get the list of nodes.
drag.get('dragNode').removeClass(CSS.COURSECONTENT);
var sectionlist = Y.Node.all(this.sectionlistselector);
// Add a lightbox if it's not there.
var lightbox = M.util.add_lightbox(Y, dragnode);
// Handle any variables which we must pass via AJAX.
var params = {},
pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
if (!pageparams.hasOwnProperty(varname)) {
continue;
}
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'section';
params.field = 'move';
params.id = dragnodeid;
params.value = dropnodeindex;
// Perform the AJAX request.
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
lightbox.show();
},
success: function(tid, response) {
// Update section titles, we can't simply swap them as
// they might have custom title
try {
var responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
M.mod_quiz.edit.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
} catch (e) {}
// Update all of the section IDs - first unset them, then set them
// to avoid duplicates in the DOM.
var index;
// Classic bubble sort algorithm is applied to the section
// nodes between original drag node location and the new one.
var swapped = false;
do {
swapped = false;
for (index = loopstart; index <= loopend; index++) {
if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
Y.log("Swapping " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) +
" with " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index)),
"debug", "moodle-mod_quiz-dragdrop");
// Swap section id.
var sectionid = sectionlist.item(index - 1).get('id');
sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
sectionlist.item(index).set('id', sectionid);
// See what format needs to swap.
M.mod_quiz.edit.swap_sections(Y, index - 1, index);
// Update flag.
swapped = true;
}
}
loopend = loopend - 1;
} while (swapped);
window.setTimeout(function() {
lightbox.hide();
}, 250);
},
failure: function(tid, response) {
this.ajax_failure(response);
lightbox.hide();
}
},
context:this
});
}
}, {
NAME: 'mod_quiz-dragdrop-section',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_section_dragdrop = function(params) {
new DRAGSECTION(params);
};
/**
* Resource drag and drop.
*
* @class M.course.dragdrop.resource
* @constructor
* @extends M.core.dragdrop
*/
var DRAGRESOURCE = function() {
DRAGRESOURCE.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGRESOURCE, M.core.dragdrop, {
initializer: function() {
// Set group for parent class
this.groups = ['resource'];
this.samenodeclass = CSS.ACTIVITY;
this.parentnodeclass = CSS.SECTION;
//this.resourcedraghandle = this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'), CSS.EDITINGMOVE, CSS.ICONCLASS, true);
this.resourcedraghandle = this.get_drag_handle(M.str.moodle.move, CSS.EDITINGMOVE, CSS.ICONCLASS, true);
this.samenodelabel = {
identifier: 'dragtoafter',
component: 'quiz'
};
this.parentnodelabel = {
identifier: 'dragtostart',
component: 'quiz'
};
// Go through all sections
var sectionlistselector = M.mod_quiz.edit.get_section_selector(Y);
if (sectionlistselector) {
sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + sectionlistselector;
this.setup_for_section(sectionlistselector);
// Initialise drag & drop for all resources/activities
var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length + 2) + ' li.' + CSS.ACTIVITY;
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: nodeselector,
target: true,
handles: ['.' + CSS.EDITINGMOVE],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false,
cloneNode: true
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.SLOTS
});
del.dd.plug(Y.Plugin.DDWinScroll);
M.mod_quiz.quizbase.register_module(this);
M.mod_quiz.dragres = this;
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
var resources = sectionnode.one('.' + CSS.CONTENT + ' ul.' + CSS.SECTION);
// See if resources ul exists, if not create one.
if (!resources) {
resources = Y.Node.create('<ul></ul>');
resources.addClass(CSS.SECTION);
sectionnode.one('.' + CSS.CONTENT + ' div.' + CSS.SUMMARY).insert(resources, 'after');
}
resources.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resources,
groups: this.groups,
padding: '20 0 20 0'
});
// Initialise each resource/activity in this section
this.setup_for_resource('#' + sectionnode.get('id') + ' li.' + CSS.ACTIVITY);
}, this);
},
/**
* Apply dragdrop features to the specified selector or node that refers to resource(s)
*
* @method setup_for_resource
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_resource: function(baseselector) {
Y.Node.all(baseselector).each(function(resourcesnode) {
// Replace move icons
var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
if (move) {
move.replace(this.resourcedraghandle.cloneNode(true));
}
}, this);
},
drag_start: function(e) {
// Get our drag object
var drag = e.target;
drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
drop_hit: function(e) {
var drag = e.drag;
// Get a reference to our drag node
var dragnode = drag.get('node');
var dropnode = e.drop.get('node');
// Add spinner if it not there
var actionarea = dragnode.one(CSS.ACTIONAREA);
var spinner = M.util.add_spinner(Y, actionarea);
var params = {};
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams;
var varname;
for (varname in pageparams) {
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'resource';
params.field = 'move';
params.id = Number(Y.Moodle.mod_quiz.util.slot.getId(dragnode));
params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.mod_quiz.edit.get_section_wrapper(Y), true));
var previousslot = dragnode.previous(SELECTOR.SLOT);
if (previousslot) {
params.previousid = Number(Y.Moodle.mod_quiz.util.slot.getId(previousslot));
}
var previouspage = dragnode.previous(SELECTOR.PAGE);
if (previouspage) {
params.page = Number(Y.Moodle.mod_quiz.util.page.getId(previouspage));
}
// Do AJAX request
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
this.lock_drag_handle(drag, CSS.EDITINGMOVE);
spinner.show();
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
var params = {element: dragnode, visible: responsetext.visible};
M.mod_quiz.quizbase.invoke_function('set_visibility_resource_ui', params);
Y.Moodle.mod_quiz.util.slot.reorder_slots();
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
window.setTimeout(function() {
spinner.hide();
}, 250);
window.location.reload(true);
},
failure: function(tid, response) {
this.ajax_failure(response);
this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
spinner.hide();
window.location.reload(true);
}
},
context:this
});
},
global_drop_over: function(e) {
//Overriding parent method so we can stop the slots being dragged before the first page node.
// Check that drop object belong to correct group.
if (!e.drop || !e.drop.inGroup(this.groups)) {
return;
}
// Get a reference to our drag and drop nodes.
var drag = e.drag.get('node'),
drop = e.drop.get('node');
// Save last drop target for the case of missed target processing.
this.lastdroptarget = e.drop;
// Are we dropping within the same parent node?
if (drop.hasClass(this.samenodeclass)) {
var where;
if (this.goingup) {
where = "before";
} else {
where = "after";
}
drop.insert(drag, where);
} else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
// We are dropping on parent node and it is empty
if (this.goingup) {
drop.append(drag);
} else {
drop.prepend(drag);
}
}
this.drop_over(e);
}
}, {
NAME: 'mod_quiz-dragdrop-resource',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_resource_dragdrop = function(params) {
new DRAGRESOURCE(params);
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"io",
"dom",
"dd",
"dd-scroll",
"moodle-core-dragdrop",
"moodle-core-notification",
"moodle-mod_quiz-quizbase",
"moodle-mod_quiz-util",
"moodle-course-util"
]
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,557 @@
YUI.add('moodle-mod_quiz-dragdrop', function (Y, NAME) {
/**
* Drag and Drop for Quiz sections and slots.
*
* @module moodle-mod-quiz-dragdrop
*/
var CSS = {
ACTIONAREA: '.actions',
ACTIVITY: 'activity',
ACTIVITYINSTANCE: 'activityinstance',
CONTENT: 'content',
COURSECONTENT: 'mod-quiz-edit-content',
EDITINGMOVE: 'editing_move',
ICONCLASS: 'iconsmall',
JUMPMENU: 'jumpmenu',
LEFT: 'left',
LIGHTBOX: 'lightbox',
MOVEDOWN: 'movedown',
MOVEUP: 'moveup',
PAGE : 'page',
PAGECONTENT: 'page-content',
RIGHT: 'right',
SECTION: 'section',
SECTIONADDMENUS: 'section_add_menus',
SECTIONHANDLE: 'section-handle',
SLOTS: 'slots',
SUMMARY: 'summary',
SECTIONDRAGGABLE: 'sectiondraggable'
},
// The CSS selectors we use.
SELECTOR = {
PAGE: 'li.page',
SLOT: 'li.slot'
};
/**
* Section drag and drop.
*
* @class M.mod_quiz.dragdrop.section
* @constructor
* @extends M.core.dragdrop
*/
var DRAGSECTION = function() {
DRAGSECTION.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGSECTION, M.core.dragdrop, {
sectionlistselector: null,
initializer: function() {
// Set group for parent class
this.groups = [ CSS.SECTIONDRAGGABLE ];
this.samenodeclass = M.mod_quiz.edit.get_sectionwrapperclass();
this.parentnodeclass = M.mod_quiz.edit.get_containerclass();
// Check if we are in single section mode
if (Y.Node.one('.' + CSS.JUMPMENU)) {
return false;
}
// Initialise sections dragging
this.sectionlistselector = M.mod_quiz.edit.get_section_wrapper(Y);
if (this.sectionlistselector) {
this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector;
this.setup_for_section(this.sectionlistselector);
// Make each li element in the lists of sections draggable
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: '.' + CSS.SECTIONDRAGGABLE,
target: true,
handles: ['.' + CSS.LEFT],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.PAGECONTENT,
stickY: true
});
del.dd.plug(Y.Plugin.DDWinScroll);
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
// Determine the section ID
var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode);
// We skip the top section as it is not draggable
if (sectionid > 0) {
// Remove move icons
var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN);
var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP);
// Add dragger icon
var title = M.util.get_string('movesection', 'moodle', sectionid);
var cssleft = sectionnode.one('.' + CSS.LEFT);
if ((movedown || moveup) && cssleft) {
cssleft.setStyle('cursor', 'move');
cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
if (moveup) {
moveup.remove();
}
if (movedown) {
movedown.remove();
}
// This section can be moved - add the class to indicate this to Y.DD.
sectionnode.addClass(CSS.SECTIONDRAGGABLE);
}
}
}, this);
},
/*
* Drag-dropping related functions
*/
drag_start: function(e) {
// Get our drag object
var drag = e.target;
// Creat a dummy structure of the outer elemnents for clean styles application
var containernode = Y.Node.create('<' + M.mod_quiz.edit.get_containernode() + '></' + M.mod_quiz.edit.get_containernode() + '>');
containernode.addClass(M.mod_quiz.edit.get_containerclass());
var sectionnode = Y.Node.create('<' + M.mod_quiz.edit.get_sectionwrappernode() + '></' + M.mod_quiz.edit.get_sectionwrappernode() + '>');
sectionnode.addClass( M.mod_quiz.edit.get_sectionwrapperclass());
sectionnode.setStyle('margin', 0);
sectionnode.setContent(drag.get('node').get('innerHTML'));
containernode.appendChild(sectionnode);
drag.get('dragNode').setContent(containernode);
drag.get('dragNode').addClass(CSS.COURSECONTENT);
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
get_section_index: function(node) {
var sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + M.mod_quiz.edit.get_section_selector(Y),
sectionList = Y.all(sectionlistselector),
nodeIndex = sectionList.indexOf(node),
zeroIndex = sectionList.indexOf(Y.one('#section-0'));
return (nodeIndex - zeroIndex);
},
drop_hit: function(e) {
var drag = e.drag;
// Get references to our nodes and their IDs.
var dragnode = drag.get('node'),
dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
loopstart = dragnodeid,
dropnodeindex = this.get_section_index(dragnode),
loopend = dropnodeindex;
if (dragnodeid === dropnodeindex) {
return;
}
if (loopstart > loopend) {
// If we're going up, we need to swap the loop order
// because loops can't go backwards.
loopstart = dropnodeindex;
loopend = dragnodeid;
}
// Get the list of nodes.
drag.get('dragNode').removeClass(CSS.COURSECONTENT);
var sectionlist = Y.Node.all(this.sectionlistselector);
// Add a lightbox if it's not there.
var lightbox = M.util.add_lightbox(Y, dragnode);
// Handle any variables which we must pass via AJAX.
var params = {},
pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
if (!pageparams.hasOwnProperty(varname)) {
continue;
}
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'section';
params.field = 'move';
params.id = dragnodeid;
params.value = dropnodeindex;
// Perform the AJAX request.
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
lightbox.show();
},
success: function(tid, response) {
// Update section titles, we can't simply swap them as
// they might have custom title
try {
var responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
M.mod_quiz.edit.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
} catch (e) {}
// Update all of the section IDs - first unset them, then set them
// to avoid duplicates in the DOM.
var index;
// Classic bubble sort algorithm is applied to the section
// nodes between original drag node location and the new one.
var swapped = false;
do {
swapped = false;
for (index = loopstart; index <= loopend; index++) {
if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
// Swap section id.
var sectionid = sectionlist.item(index - 1).get('id');
sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
sectionlist.item(index).set('id', sectionid);
// See what format needs to swap.
M.mod_quiz.edit.swap_sections(Y, index - 1, index);
// Update flag.
swapped = true;
}
}
loopend = loopend - 1;
} while (swapped);
window.setTimeout(function() {
lightbox.hide();
}, 250);
},
failure: function(tid, response) {
this.ajax_failure(response);
lightbox.hide();
}
},
context:this
});
}
}, {
NAME: 'mod_quiz-dragdrop-section',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_section_dragdrop = function(params) {
new DRAGSECTION(params);
};
/**
* Resource drag and drop.
*
* @class M.course.dragdrop.resource
* @constructor
* @extends M.core.dragdrop
*/
var DRAGRESOURCE = function() {
DRAGRESOURCE.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGRESOURCE, M.core.dragdrop, {
initializer: function() {
// Set group for parent class
this.groups = ['resource'];
this.samenodeclass = CSS.ACTIVITY;
this.parentnodeclass = CSS.SECTION;
//this.resourcedraghandle = this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'), CSS.EDITINGMOVE, CSS.ICONCLASS, true);
this.resourcedraghandle = this.get_drag_handle(M.str.moodle.move, CSS.EDITINGMOVE, CSS.ICONCLASS, true);
this.samenodelabel = {
identifier: 'dragtoafter',
component: 'quiz'
};
this.parentnodelabel = {
identifier: 'dragtostart',
component: 'quiz'
};
// Go through all sections
var sectionlistselector = M.mod_quiz.edit.get_section_selector(Y);
if (sectionlistselector) {
sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + sectionlistselector;
this.setup_for_section(sectionlistselector);
// Initialise drag & drop for all resources/activities
var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length + 2) + ' li.' + CSS.ACTIVITY;
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: nodeselector,
target: true,
handles: ['.' + CSS.EDITINGMOVE],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false,
cloneNode: true
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.SLOTS
});
del.dd.plug(Y.Plugin.DDWinScroll);
M.mod_quiz.quizbase.register_module(this);
M.mod_quiz.dragres = this;
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
var resources = sectionnode.one('.' + CSS.CONTENT + ' ul.' + CSS.SECTION);
// See if resources ul exists, if not create one.
if (!resources) {
resources = Y.Node.create('<ul></ul>');
resources.addClass(CSS.SECTION);
sectionnode.one('.' + CSS.CONTENT + ' div.' + CSS.SUMMARY).insert(resources, 'after');
}
resources.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resources,
groups: this.groups,
padding: '20 0 20 0'
});
// Initialise each resource/activity in this section
this.setup_for_resource('#' + sectionnode.get('id') + ' li.' + CSS.ACTIVITY);
}, this);
},
/**
* Apply dragdrop features to the specified selector or node that refers to resource(s)
*
* @method setup_for_resource
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_resource: function(baseselector) {
Y.Node.all(baseselector).each(function(resourcesnode) {
// Replace move icons
var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
if (move) {
move.replace(this.resourcedraghandle.cloneNode(true));
}
}, this);
},
drag_start: function(e) {
// Get our drag object
var drag = e.target;
drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
drop_hit: function(e) {
var drag = e.drag;
// Get a reference to our drag node
var dragnode = drag.get('node');
var dropnode = e.drop.get('node');
// Add spinner if it not there
var actionarea = dragnode.one(CSS.ACTIONAREA);
var spinner = M.util.add_spinner(Y, actionarea);
var params = {};
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams;
var varname;
for (varname in pageparams) {
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'resource';
params.field = 'move';
params.id = Number(Y.Moodle.mod_quiz.util.slot.getId(dragnode));
params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.mod_quiz.edit.get_section_wrapper(Y), true));
var previousslot = dragnode.previous(SELECTOR.SLOT);
if (previousslot) {
params.previousid = Number(Y.Moodle.mod_quiz.util.slot.getId(previousslot));
}
var previouspage = dragnode.previous(SELECTOR.PAGE);
if (previouspage) {
params.page = Number(Y.Moodle.mod_quiz.util.page.getId(previouspage));
}
// Do AJAX request
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
this.lock_drag_handle(drag, CSS.EDITINGMOVE);
spinner.show();
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
var params = {element: dragnode, visible: responsetext.visible};
M.mod_quiz.quizbase.invoke_function('set_visibility_resource_ui', params);
Y.Moodle.mod_quiz.util.slot.reorder_slots();
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
window.setTimeout(function() {
spinner.hide();
}, 250);
window.location.reload(true);
},
failure: function(tid, response) {
this.ajax_failure(response);
this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
spinner.hide();
window.location.reload(true);
}
},
context:this
});
},
global_drop_over: function(e) {
//Overriding parent method so we can stop the slots being dragged before the first page node.
// Check that drop object belong to correct group.
if (!e.drop || !e.drop.inGroup(this.groups)) {
return;
}
// Get a reference to our drag and drop nodes.
var drag = e.drag.get('node'),
drop = e.drop.get('node');
// Save last drop target for the case of missed target processing.
this.lastdroptarget = e.drop;
// Are we dropping within the same parent node?
if (drop.hasClass(this.samenodeclass)) {
var where;
if (this.goingup) {
where = "before";
} else {
where = "after";
}
drop.insert(drag, where);
} else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
// We are dropping on parent node and it is empty
if (this.goingup) {
drop.append(drag);
} else {
drop.prepend(drag);
}
}
this.drop_over(e);
}
}, {
NAME: 'mod_quiz-dragdrop-resource',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_resource_dragdrop = function(params) {
new DRAGRESOURCE(params);
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"io",
"dom",
"dd",
"dd-scroll",
"moodle-core-dragdrop",
"moodle-core-notification",
"moodle-mod_quiz-quizbase",
"moodle-mod_quiz-util",
"moodle-course-util"
]
});
@@ -0,0 +1,56 @@
YUI.add('moodle-mod_quiz-modform', function (Y, NAME) {
/**
* The modform class has all the JavaScript specific to mod/quiz/mod_form.php.
*
* @module moodle-mod_quiz-modform
*/
var MODFORM = function() {
MODFORM.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(MODFORM, Y.Base, {
repaginateCheckbox: null,
qppSelect: null,
qppInitialValue: 0,
initializer: function () {
this.repaginateCheckbox = Y.one('#id_repaginatenow');
if (!this.repaginateCheckbox) {
// The checkbox only appears when editing an existing quiz.
return;
}
this.qppSelect = Y.one('#id_questionsperpage');
this.qppInitialValue = this.qppSelect.get('value');
this.qppSelect.on('change', this.qppChanged, this);
Y.one('#id_shufflequestions').on('change', this.qppChanged, this);
},
qppChanged: function() {
Y.later(50, this, function() {
if (!this.repaginateCheckbox.get('disabled')) {
this.repaginateCheckbox.set('checked', this.qppSelect.get('value') !== this.qppInitialValue);
}
});
}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.modform = M.mod_quiz.modform || new MODFORM();
M.mod_quiz.modform.init = function() {
return new MODFORM();
};
}, '@VERSION@', {"requires": ["base", "node", "event"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-modform",function(e,t){var n=function(){n.superclass.constructor.apply(this,arguments)};e.extend(n,e.Base,{repaginateCheckbox:null,qppSelect:null,qppInitialValue:0,initializer:function(){this.repaginateCheckbox=e.one("#id_repaginatenow");if(!this.repaginateCheckbox)return;this.qppSelect=e.one("#id_questionsperpage"),this.qppInitialValue=this.qppSelect.get("value"),this.qppSelect.on("change",this.qppChanged,this),e.one("#id_shufflequestions").on("change",this.qppChanged,this)},qppChanged:function(){e.later(50,this,function(){this.repaginateCheckbox.get("disabled")||this.repaginateCheckbox.set("checked",this.qppSelect.get("value")!==this.qppInitialValue)})}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.modform=M.mod_quiz.modform||new n,M.mod_quiz.modform.init=function(){return new n}},"@VERSION@",{requires:["base","node","event"]});
@@ -0,0 +1,56 @@
YUI.add('moodle-mod_quiz-modform', function (Y, NAME) {
/**
* The modform class has all the JavaScript specific to mod/quiz/mod_form.php.
*
* @module moodle-mod_quiz-modform
*/
var MODFORM = function() {
MODFORM.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(MODFORM, Y.Base, {
repaginateCheckbox: null,
qppSelect: null,
qppInitialValue: 0,
initializer: function () {
this.repaginateCheckbox = Y.one('#id_repaginatenow');
if (!this.repaginateCheckbox) {
// The checkbox only appears when editing an existing quiz.
return;
}
this.qppSelect = Y.one('#id_questionsperpage');
this.qppInitialValue = this.qppSelect.get('value');
this.qppSelect.on('change', this.qppChanged, this);
Y.one('#id_shufflequestions').on('change', this.qppChanged, this);
},
qppChanged: function() {
Y.later(50, this, function() {
if (!this.repaginateCheckbox.get('disabled')) {
this.repaginateCheckbox.set('checked', this.qppSelect.get('value') !== this.qppInitialValue);
}
});
}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.modform = M.mod_quiz.modform || new MODFORM();
M.mod_quiz.modform.init = function() {
return new MODFORM();
};
}, '@VERSION@', {"requires": ["base", "node", "event"]});
@@ -0,0 +1,77 @@
YUI.add('moodle-mod_quiz-questionchooser', function (Y, NAME) {
var CSS = {
ADDNEWQUESTIONBUTTONS: 'ul.menu a.addquestion',
CREATENEWQUESTION: 'div.createnewquestion',
CHOOSERDIALOGUE: 'div.chooserdialogue',
CHOOSERHEADER: 'div.choosertitle'
};
/**
* The questionchooser class is responsible for instantiating and displaying the question chooser
* when viewing a quiz in editing mode.
*
* @class questionchooser
* @constructor
* @protected
* @extends M.core.chooserdialogue
*/
var QUESTIONCHOOSER = function() {
QUESTIONCHOOSER.superclass.constructor.apply(this, arguments);
};
Y.extend(QUESTIONCHOOSER, M.core.chooserdialogue, {
initializer: function() {
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDNEWQUESTIONBUTTONS, this);
},
display_dialogue: function(e) {
e.preventDefault();
var dialogue = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERDIALOGUE),
header = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERHEADER);
if (this.container === null) {
// Setup the dialogue, and then prepare the chooser if it's not already been set up.
this.setup_chooser_dialogue(dialogue, header, {});
this.prepare_chooser();
}
// Update all of the hidden fields within the questionbank form.
var parameters = Y.QueryString.parse(e.currentTarget.get('search').substring(1));
var form = this.container.one('form');
this.parameters_to_hidden_input(parameters, form, 'returnurl');
this.parameters_to_hidden_input(parameters, form, 'cmid');
this.parameters_to_hidden_input(parameters, form, 'category');
this.parameters_to_hidden_input(parameters, form, 'addonpage');
this.parameters_to_hidden_input(parameters, form, 'appendqnumstring');
// Display the chooser dialogue.
this.display_chooser(e);
},
parameters_to_hidden_input: function(parameters, form, name) {
var value;
if (parameters.hasOwnProperty(name)) {
value = parameters[name];
} else {
value = '';
}
var input = form.one('input[name=' + name + ']');
if (!input) {
input = form.appendChild('<input type="hidden">');
input.set('name', name);
}
input.set('value', value);
}
}, {
NAME: 'mod_quiz-questionchooser'
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_questionchooser = function() {
M.mod_quiz.question_chooser = new QUESTIONCHOOSER({});
return M.mod_quiz.question_chooser;
};
}, '@VERSION@', {"requires": ["moodle-core-chooserdialogue", "moodle-mod_quiz-util", "querystring-parse"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-questionchooser",function(e,t){var n={ADDNEWQUESTIONBUTTONS:"ul.menu a.addquestion",CREATENEWQUESTION:"div.createnewquestion",CHOOSERDIALOGUE:"div.chooserdialogue",CHOOSERHEADER:"div.choosertitle"},r=function(){r.superclass.constructor.apply(this,arguments)};e.extend(r,M.core.chooserdialogue,{initializer:function(){e.one("body").delegate("click",this.display_dialogue,n.ADDNEWQUESTIONBUTTONS,this)},display_dialogue:function(t){t.preventDefault();var r=e.one(n.CREATENEWQUESTION+" "+n.CHOOSERDIALOGUE),i=e.one(n.CREATENEWQUESTION+" "+n.CHOOSERHEADER);this.container===null&&(this.setup_chooser_dialogue(r,i,{}),this.prepare_chooser());var s=e.QueryString.parse(t.currentTarget.get("search").substring(1)),o=this.container.one("form");this.parameters_to_hidden_input(s,o,"returnurl"),this.parameters_to_hidden_input(s,o,"cmid"),this.parameters_to_hidden_input(s,o,"category"),this.parameters_to_hidden_input(s,o,"addonpage"),this.parameters_to_hidden_input(s,o,"appendqnumstring"),this.display_chooser(t)},parameters_to_hidden_input:function(e,t,n){var r;e.hasOwnProperty(n)?r=e[n]:r="";var i=t.one("input[name="+n+"]");i||(i=t.appendChild('<input type="hidden">'),i.set("name",n)),i.set("value",r)}},{NAME:"mod_quiz-questionchooser"}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.init_questionchooser=function(){return M.mod_quiz.question_chooser=new r({}),M.mod_quiz.question_chooser}},"@VERSION@",{requires:["moodle-core-chooserdialogue","moodle-mod_quiz-util","querystring-parse"]});
@@ -0,0 +1,77 @@
YUI.add('moodle-mod_quiz-questionchooser', function (Y, NAME) {
var CSS = {
ADDNEWQUESTIONBUTTONS: 'ul.menu a.addquestion',
CREATENEWQUESTION: 'div.createnewquestion',
CHOOSERDIALOGUE: 'div.chooserdialogue',
CHOOSERHEADER: 'div.choosertitle'
};
/**
* The questionchooser class is responsible for instantiating and displaying the question chooser
* when viewing a quiz in editing mode.
*
* @class questionchooser
* @constructor
* @protected
* @extends M.core.chooserdialogue
*/
var QUESTIONCHOOSER = function() {
QUESTIONCHOOSER.superclass.constructor.apply(this, arguments);
};
Y.extend(QUESTIONCHOOSER, M.core.chooserdialogue, {
initializer: function() {
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDNEWQUESTIONBUTTONS, this);
},
display_dialogue: function(e) {
e.preventDefault();
var dialogue = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERDIALOGUE),
header = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERHEADER);
if (this.container === null) {
// Setup the dialogue, and then prepare the chooser if it's not already been set up.
this.setup_chooser_dialogue(dialogue, header, {});
this.prepare_chooser();
}
// Update all of the hidden fields within the questionbank form.
var parameters = Y.QueryString.parse(e.currentTarget.get('search').substring(1));
var form = this.container.one('form');
this.parameters_to_hidden_input(parameters, form, 'returnurl');
this.parameters_to_hidden_input(parameters, form, 'cmid');
this.parameters_to_hidden_input(parameters, form, 'category');
this.parameters_to_hidden_input(parameters, form, 'addonpage');
this.parameters_to_hidden_input(parameters, form, 'appendqnumstring');
// Display the chooser dialogue.
this.display_chooser(e);
},
parameters_to_hidden_input: function(parameters, form, name) {
var value;
if (parameters.hasOwnProperty(name)) {
value = parameters[name];
} else {
value = '';
}
var input = form.one('input[name=' + name + ']');
if (!input) {
input = form.appendChild('<input type="hidden">');
input.set('name', name);
}
input.set('value', value);
}
}, {
NAME: 'mod_quiz-questionchooser'
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_questionchooser = function() {
M.mod_quiz.question_chooser = new QUESTIONCHOOSER({});
return M.mod_quiz.question_chooser;
};
}, '@VERSION@', {"requires": ["moodle-core-chooserdialogue", "moodle-mod_quiz-util", "querystring-parse"]});
@@ -0,0 +1,272 @@
YUI.add('moodle-mod_quiz-quizbase', function (Y, NAME) {
/**
* The quizbase class to provide shared functionality to Modules within Moodle.
*
* @module moodle-mod_quiz-quizbase
*/
var QUIZBASENAME = 'mod_quiz-quizbase';
var QUIZBASE = function() {
QUIZBASE.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(QUIZBASE, Y.Base, {
// Registered Modules
registermodules : [],
/**
* Register a new Javascript Module
*
* @method register_module
* @param {Object} The instantiated module to call functions on
* @chainable
*/
register_module : function(object) {
this.registermodules.push(object);
return this;
},
/**
* Invoke the specified function in all registered modules with the given arguments
*
* @method invoke_function
* @param {String} functionname The name of the function to call
* @param {mixed} args The argument supplied to the function
* @chainable
*/
invoke_function : function(functionname, args) {
var module;
for (module in this.registermodules) {
if (functionname in this.registermodules[module]) {
this.registermodules[module][functionname](args);
}
}
return this;
}
}, {
NAME : QUIZBASENAME,
ATTRS : {}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.quizbase = M.mod_quiz.quizbase || new QUIZBASE();
// Abstract functions that needs to be defined per format (course/format/somename/format.js)
M.mod_quiz.edit = M.mod_quiz.edit || {};
/**
* Swap section (should be defined in format.js if requred)
*
* @param {YUI} Y YUI3 instance
* @param {string} node1 node to swap to
* @param {string} node2 node to swap with
* @return {NodeList} section list
*/
M.mod_quiz.edit.swap_sections = function(Y, node1, node2) {
var CSS = {
COURSECONTENT : 'mod-quiz-edit-content',
SECTIONADDMENUS : 'section_add_menus'
};
var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.mod_quiz.edit.get_section_selector(Y));
// Swap menus.
sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
};
/**
* Process sections after ajax response (should be defined in format.js)
* If some response is expected, we pass it over to format, as it knows better
* hot to process it.
*
* @param {YUI} Y YUI3 instance
* @param {NodeList} list of sections
* @param {array} response ajax response
* @param {string} sectionfrom first affected section
* @param {string} sectionto last affected section
* @return void
*/
M.mod_quiz.edit.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
var CSS = {
SECTIONNAME : 'sectionname'
},
SELECTORS = {
SECTIONLEFTSIDE : '.left .section-handle img'
};
if (response.action === 'move') {
// If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates.
if (sectionfrom > sectionto) {
var temp = sectionto;
sectionto = sectionfrom;
sectionfrom = temp;
}
// Update titles and move icons in all affected sections.
var ele, str, stridx, newstr;
for (var i = sectionfrom; i <= sectionto; i++) {
// Update section title.
sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
// Update move icon.
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
str = ele.getAttribute('alt');
stridx = str.lastIndexOf(' ');
newstr = str.substr(0, stridx + 1) + i;
ele.setAttribute('alt', newstr);
ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
// Remove the current class as section has been moved.
sectionlist.item(i).removeClass('current');
}
// If there is a current section, apply corresponding class in order to highlight it.
if (response.current !== -1) {
// Add current class to the required section.
sectionlist.item(response.current).addClass('current');
}
}
};
/**
* Get sections config for this format, for examples see function definition
* in the formats.
*
* @return {object} section list configuration
*/
M.mod_quiz.edit.get_config = function() {
return {
container_node : 'ul',
container_class : 'slots',
section_node : 'li',
section_class : 'section'
};
};
/**
* Get section list for this format (usually items inside container_node.container_class selector)
*
* @param {YUI} Y YUI3 instance
* @return {string} section selector
*/
M.mod_quiz.edit.get_section_selector = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_node && config.section_class) {
return config.section_node + '.' + config.section_class;
}
Y.log('section_node and section_class are not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
return null;
};
/**
* Get section wraper for this format (only used in case when each
* container_node.container_class node is wrapped in some other element).
*
* @param {YUI} Y YUI3 instance
* @return {string} section wrapper selector or M.mod_quiz.format.get_section_selector
* if section_wrapper_node and section_wrapper_class are not defined in the format config.
*/
M.mod_quiz.edit.get_section_wrapper = function(Y) {
var config = M.mod_quiz.edit.get_config();
if (config.section_wrapper_node && config.section_wrapper_class) {
return config.section_wrapper_node + '.' + config.section_wrapper_class;
}
return M.mod_quiz.edit.get_section_selector(Y);
};
/**
* Get the tag of container node
*
* @return {string} tag of container node.
*/
M.mod_quiz.edit.get_containernode = function() {
var config = M.mod_quiz.edit.get_config();
if (config.container_node) {
return config.container_node;
} else {
Y.log('container_node is not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
}
};
/**
* Get the class of container node
*
* @return {string} class of the container node.
*/
M.mod_quiz.edit.get_containerclass = function() {
var config = M.mod_quiz.edit.get_config();
if (config.container_class) {
return config.container_class;
} else {
Y.log('container_class is not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
}
};
/**
* Get the tag of draggable node (section wrapper if exists, otherwise section)
*
* @return {string} tag of the draggable node.
*/
M.mod_quiz.edit.get_sectionwrappernode = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_wrapper_node) {
return config.section_wrapper_node;
} else {
return config.section_node;
}
};
/**
* Get the class of draggable node (section wrapper if exists, otherwise section)
*
* @return {string} class of the draggable node.
*/
M.mod_quiz.edit.get_sectionwrapperclass = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_wrapper_class) {
return config.section_wrapper_class;
} else {
return config.section_class;
}
};
/**
* Get the tag of section node
*
* @return {string} tag of section node.
*/
M.mod_quiz.edit.get_sectionnode = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_node) {
return config.section_node;
} else {
Y.log('section_node is not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
}
};
/**
* Get the class of section node
*
* @return {string} class of the section node.
*/
M.mod_quiz.edit.get_sectionclass = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_class) {
return config.section_class;
} else {
Y.log('section_class is not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
}
};
}, '@VERSION@', {"requires": ["base", "node"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-quizbase",function(e,t){var n="mod_quiz-quizbase",r=function(){r.superclass.constructor.apply(this,arguments)};e.extend(r,e.Base,{registermodules:[],register_module:function(e){return this.registermodules.push(e),this},invoke_function:function(e,t){var n;for(n in this.registermodules)e in this.registermodules[n]&&this.registermodules[n][e](t);return this}},{NAME:n,ATTRS:{}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.quizbase=M.mod_quiz.quizbase||new r,M.mod_quiz.edit=M.mod_quiz.edit||{},M.mod_quiz.edit.swap_sections=function(e,t,n){var r={COURSECONTENT:"mod-quiz-edit-content",SECTIONADDMENUS:"section_add_menus"},i=e.Node.all("."+r.COURSECONTENT+" "+M.mod_quiz.edit.get_section_selector(e));i.item(t).one("."+r.SECTIONADDMENUS).swap(i.item(n).one("."+r.SECTIONADDMENUS))},M.mod_quiz.edit.process_sections=function(e,t,n,r,i){var s={SECTIONNAME:"sectionname"},o={SECTIONLEFTSIDE:".left .section-handle img"};if(n.action==="move"){if(r>i){var u=i;i=r,r=u}var a,f,l,c;for(var h=r;h<=i;h++)t.item(h).one("."+s.SECTIONNAME).setContent(n.sectiontitles[h]),a=t.item(h).one(o.SECTIONLEFTSIDE),f=a.getAttribute("alt"),l=f.lastIndexOf(" "),c=f.substr(0,l+1)+h,a.setAttribute("alt",c),a.setAttribute("title",c),t.item(h).removeClass("current");n.current!==-1&&t.item(n.current).addClass("current")}},M.mod_quiz.edit.get_config=function(){return{container_node:"ul",container_class:"slots",section_node:"li",section_class:"section"}},M.mod_quiz.edit.get_section_selector=function(){var e=M.mod_quiz.edit.get_config();return e.section_node&&e.section_class?e.section_node+"."+e.section_class:null},M.mod_quiz.edit.get_section_wrapper=function(e){var t=M.mod_quiz.edit.get_config();return t.section_wrapper_node&&t.section_wrapper_class?t.section_wrapper_node+"."+t.section_wrapper_class:M.mod_quiz.edit.get_section_selector(e)},M.mod_quiz.edit.get_containernode=function(){var e=M.mod_quiz.edit.get_config();if(e.container_node)return e.container_node},M.mod_quiz.edit.get_containerclass=function(){var e=M.mod_quiz.edit.get_config();if(e.container_class)return e.container_class},M.mod_quiz.edit.get_sectionwrappernode=function(){var e=M.mod_quiz.edit.get_config();return e.section_wrapper_node?e.section_wrapper_node:e.section_node},M.mod_quiz.edit.get_sectionwrapperclass=function(){var e=M.mod_quiz.edit.get_config();return e.section_wrapper_class?e.section_wrapper_class:e.section_class},M.mod_quiz.edit.get_sectionnode=function(){var e=M.mod_quiz.edit.get_config();if(e.section_node)return e.section_node},M.mod_quiz.edit.get_sectionclass=function(){var e=M.mod_quiz.edit.get_config();if(e.section_class)return e.section_class}},"@VERSION@",{requires:["base","node"]});
@@ -0,0 +1,267 @@
YUI.add('moodle-mod_quiz-quizbase', function (Y, NAME) {
/**
* The quizbase class to provide shared functionality to Modules within Moodle.
*
* @module moodle-mod_quiz-quizbase
*/
var QUIZBASENAME = 'mod_quiz-quizbase';
var QUIZBASE = function() {
QUIZBASE.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(QUIZBASE, Y.Base, {
// Registered Modules
registermodules : [],
/**
* Register a new Javascript Module
*
* @method register_module
* @param {Object} The instantiated module to call functions on
* @chainable
*/
register_module : function(object) {
this.registermodules.push(object);
return this;
},
/**
* Invoke the specified function in all registered modules with the given arguments
*
* @method invoke_function
* @param {String} functionname The name of the function to call
* @param {mixed} args The argument supplied to the function
* @chainable
*/
invoke_function : function(functionname, args) {
var module;
for (module in this.registermodules) {
if (functionname in this.registermodules[module]) {
this.registermodules[module][functionname](args);
}
}
return this;
}
}, {
NAME : QUIZBASENAME,
ATTRS : {}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.quizbase = M.mod_quiz.quizbase || new QUIZBASE();
// Abstract functions that needs to be defined per format (course/format/somename/format.js)
M.mod_quiz.edit = M.mod_quiz.edit || {};
/**
* Swap section (should be defined in format.js if requred)
*
* @param {YUI} Y YUI3 instance
* @param {string} node1 node to swap to
* @param {string} node2 node to swap with
* @return {NodeList} section list
*/
M.mod_quiz.edit.swap_sections = function(Y, node1, node2) {
var CSS = {
COURSECONTENT : 'mod-quiz-edit-content',
SECTIONADDMENUS : 'section_add_menus'
};
var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.mod_quiz.edit.get_section_selector(Y));
// Swap menus.
sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
};
/**
* Process sections after ajax response (should be defined in format.js)
* If some response is expected, we pass it over to format, as it knows better
* hot to process it.
*
* @param {YUI} Y YUI3 instance
* @param {NodeList} list of sections
* @param {array} response ajax response
* @param {string} sectionfrom first affected section
* @param {string} sectionto last affected section
* @return void
*/
M.mod_quiz.edit.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
var CSS = {
SECTIONNAME : 'sectionname'
},
SELECTORS = {
SECTIONLEFTSIDE : '.left .section-handle img'
};
if (response.action === 'move') {
// If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates.
if (sectionfrom > sectionto) {
var temp = sectionto;
sectionto = sectionfrom;
sectionfrom = temp;
}
// Update titles and move icons in all affected sections.
var ele, str, stridx, newstr;
for (var i = sectionfrom; i <= sectionto; i++) {
// Update section title.
sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
// Update move icon.
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
str = ele.getAttribute('alt');
stridx = str.lastIndexOf(' ');
newstr = str.substr(0, stridx + 1) + i;
ele.setAttribute('alt', newstr);
ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
// Remove the current class as section has been moved.
sectionlist.item(i).removeClass('current');
}
// If there is a current section, apply corresponding class in order to highlight it.
if (response.current !== -1) {
// Add current class to the required section.
sectionlist.item(response.current).addClass('current');
}
}
};
/**
* Get sections config for this format, for examples see function definition
* in the formats.
*
* @return {object} section list configuration
*/
M.mod_quiz.edit.get_config = function() {
return {
container_node : 'ul',
container_class : 'slots',
section_node : 'li',
section_class : 'section'
};
};
/**
* Get section list for this format (usually items inside container_node.container_class selector)
*
* @param {YUI} Y YUI3 instance
* @return {string} section selector
*/
M.mod_quiz.edit.get_section_selector = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_node && config.section_class) {
return config.section_node + '.' + config.section_class;
}
return null;
};
/**
* Get section wraper for this format (only used in case when each
* container_node.container_class node is wrapped in some other element).
*
* @param {YUI} Y YUI3 instance
* @return {string} section wrapper selector or M.mod_quiz.format.get_section_selector
* if section_wrapper_node and section_wrapper_class are not defined in the format config.
*/
M.mod_quiz.edit.get_section_wrapper = function(Y) {
var config = M.mod_quiz.edit.get_config();
if (config.section_wrapper_node && config.section_wrapper_class) {
return config.section_wrapper_node + '.' + config.section_wrapper_class;
}
return M.mod_quiz.edit.get_section_selector(Y);
};
/**
* Get the tag of container node
*
* @return {string} tag of container node.
*/
M.mod_quiz.edit.get_containernode = function() {
var config = M.mod_quiz.edit.get_config();
if (config.container_node) {
return config.container_node;
} else {
}
};
/**
* Get the class of container node
*
* @return {string} class of the container node.
*/
M.mod_quiz.edit.get_containerclass = function() {
var config = M.mod_quiz.edit.get_config();
if (config.container_class) {
return config.container_class;
} else {
}
};
/**
* Get the tag of draggable node (section wrapper if exists, otherwise section)
*
* @return {string} tag of the draggable node.
*/
M.mod_quiz.edit.get_sectionwrappernode = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_wrapper_node) {
return config.section_wrapper_node;
} else {
return config.section_node;
}
};
/**
* Get the class of draggable node (section wrapper if exists, otherwise section)
*
* @return {string} class of the draggable node.
*/
M.mod_quiz.edit.get_sectionwrapperclass = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_wrapper_class) {
return config.section_wrapper_class;
} else {
return config.section_class;
}
};
/**
* Get the tag of section node
*
* @return {string} tag of section node.
*/
M.mod_quiz.edit.get_sectionnode = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_node) {
return config.section_node;
} else {
}
};
/**
* Get the class of section node
*
* @return {string} class of the section node.
*/
M.mod_quiz.edit.get_sectionclass = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_class) {
return config.section_class;
} else {
}
};
}, '@VERSION@', {"requires": ["base", "node"]});
@@ -0,0 +1,165 @@
YUI.add('moodle-mod_quiz-quizquestionbank', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Add questions from question bank functionality for a popup in quiz editing page.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var CSS = {
QBANKLOADING: 'div.questionbankloading',
ADDQUESTIONLINKS: 'ul.menu a.questionbank',
ADDTOQUIZCONTAINER: 'td.addtoquizaction'
};
var PARAMS = {
PAGE: 'addonpage',
HEADER: 'header'
};
var POPUP = function() {
POPUP.superclass.constructor.apply(this, arguments);
};
Y.extend(POPUP, Y.Base, {
loadingDiv: '',
dialogue: null,
addonpage: 0,
create_dialogue: function() {
// Create a dialogue on the page and hide it.
config = {
headerContent : '',
bodyContent : Y.one(CSS.QBANKLOADING),
draggable : true,
modal : true,
centered: true,
width: null,
visible: false,
postmethod: 'form',
footerContent: null,
extraClasses: ['mod_quiz_qbank_dialogue']
};
this.dialogue = new M.core.dialogue(config);
this.dialogue.bodyNode.delegate('click', this.link_clicked, 'a[href]', this);
this.dialogue.hide();
this.loadingDiv = this.dialogue.bodyNode.getHTML();
Y.later(100, this, function() {this.load_content(window.location.search);});
},
initializer : function() {
if (!Y.one(CSS.QBANKLOADING)) {
return;
}
this.create_dialogue();
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDQUESTIONLINKS, this);
},
display_dialogue : function (e) {
e.preventDefault();
this.dialogue.set('headerContent', e.currentTarget.getData(PARAMS.HEADER));
this.addonpage = e.currentTarget.getData(PARAMS.PAGE);
var controlsDiv = this.dialogue.bodyNode.one('.modulespecificbuttonscontainer');
if (controlsDiv) {
var hidden = controlsDiv.one('input[name=addonpage]');
if (!hidden) {
hidden = controlsDiv.appendChild('<input type="hidden" name="addonpage">');
}
hidden.set('value', this.addonpage);
}
this.dialogue.show();
},
load_content : function(queryString) {
Y.log('Starting load.');
this.dialogue.bodyNode.append(this.loadingDiv);
// If to support old IE.
if (window.history.replaceState) {
window.history.replaceState(null, '', M.cfg.wwwroot + '/mod/quiz/edit.php' + queryString);
}
Y.io(M.cfg.wwwroot + '/mod/quiz/questionbank.ajax.php' + queryString, {
method: 'GET',
on: {
success: this.load_done,
failure: this.load_failed
},
context: this
});
Y.log('Load started.');
},
load_done: function(transactionid, response) {
var result = JSON.parse(response.responseText);
if (!result.status || result.status !== 'OK') {
// Because IIS is useless, Moodle can't send proper HTTP response
// codes, so we have to detect failures manually.
this.load_failed(transactionid, response);
return;
}
Y.log('Load completed.');
this.dialogue.bodyNode.setHTML(result.contents);
Y.use('moodle-question-chooser', function() {M.question.init_chooser({});});
this.dialogue.bodyNode.one('form').delegate('change', this.options_changed, '.searchoptions', this);
if (this.dialogue.visible) {
Y.later(0, this.dialogue, this.dialogue.centerDialogue);
}
M.question.qbankmanager.init();
},
load_failed: function() {
Y.log('Load failed.');
},
link_clicked: function(e) {
if (e.currentTarget.ancestor(CSS.ADDTOQUIZCONTAINER)) {
// These links need to work like normal, after we modify the URL.
e.currentTarget.set('href', e.currentTarget.get('href') + '&addonpage=' + this.addonpage);
return;
}
e.preventDefault();
this.load_content(e.currentTarget.get('search'));
},
options_changed: function(e) {
e.preventDefault();
this.load_content('?' + Y.IO.stringify(e.currentTarget.get('form')));
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.quizquestionbank = M.mod_quiz.quizquestionbank || {};
M.mod_quiz.quizquestionbank.init = function() {
return new POPUP();
};
}, '@VERSION@', {"requires": ["base", "event", "node", "io", "io-form", "yui-later", "moodle-question-qbankmanager"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-quizquestionbank",function(e,t){var n={QBANKLOADING:"div.questionbankloading",ADDQUESTIONLINKS:"ul.menu a.questionbank",ADDTOQUIZCONTAINER:"td.addtoquizaction"},r={PAGE:"addonpage",HEADER:"header"},i=function(){i.superclass.constructor.apply(this,arguments)};e.extend(i,e.Base,{loadingDiv:"",dialogue:null,addonpage:0,create_dialogue:function(){config={headerContent:"",bodyContent:e.one(n.QBANKLOADING),draggable:!0,modal:!0,centered:!0,width:null,visible:!1,postmethod:"form",footerContent:null,extraClasses:["mod_quiz_qbank_dialogue"]},this.dialogue=new M.core.dialogue(config),this.dialogue.bodyNode.delegate("click",this.link_clicked,"a[href]",this),this.dialogue.hide(),this.loadingDiv=this.dialogue.bodyNode.getHTML(),e.later(100,this,function(){this.load_content(window.location.search)})},initializer:function(){if(!e.one(n.QBANKLOADING))return;this.create_dialogue(),e.one("body").delegate("click",this.display_dialogue,n.ADDQUESTIONLINKS,this)},display_dialogue:function(e){e.preventDefault(),this.dialogue.set("headerContent",e.currentTarget.getData(r.HEADER)),this.addonpage=e.currentTarget.getData(r.PAGE);var t=this.dialogue.bodyNode.one(".modulespecificbuttonscontainer");if(t){var n=t.one("input[name=addonpage]");n||(n=t.appendChild('<input type="hidden" name="addonpage">')),n.set("value",this.addonpage)}this.dialogue.show()},load_content:function(t){this.dialogue.bodyNode.append(this.loadingDiv),window.history.replaceState&&window.history.replaceState(null,"",M.cfg.wwwroot+"/mod/quiz/edit.php"+t),e.io(M.cfg.wwwroot+"/mod/quiz/questionbank.ajax.php"+t,{method:"GET",on:{success:this.load_done,failure:this.load_failed},context:this})},load_done:function(t,n){var r=JSON.parse(n.responseText);if(!r.status||r.status!=="OK"){this.load_failed(t,n);return}this.dialogue.bodyNode.setHTML(r.contents),e.use("moodle-question-chooser",function(){M.question.init_chooser({})}),this.dialogue.bodyNode.one("form").delegate("change",this.options_changed,".searchoptions",this),this.dialogue.visible&&e.later(0,this.dialogue,this.dialogue.centerDialogue),M.question.qbankmanager.init()},load_failed:function(){},link_clicked:function(e){if(e.currentTarget.ancestor(n.ADDTOQUIZCONTAINER)){e.currentTarget.set("href",e.currentTarget.get("href")+"&addonpage="+this.addonpage);return}e.preventDefault(),this.load_content(e.currentTarget.get("search"))},options_changed:function(t){t.preventDefault(),this.load_content("?"+e.IO.stringify(t.currentTarget.get("form")))}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.quizquestionbank=M.mod_quiz.quizquestionbank||{},M.mod_quiz.quizquestionbank.init=function(){return new i}},"@VERSION@",{requires:["base","event","node","io","io-form","yui-later","moodle-question-qbankmanager"]});
@@ -0,0 +1,161 @@
YUI.add('moodle-mod_quiz-quizquestionbank', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Add questions from question bank functionality for a popup in quiz editing page.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var CSS = {
QBANKLOADING: 'div.questionbankloading',
ADDQUESTIONLINKS: 'ul.menu a.questionbank',
ADDTOQUIZCONTAINER: 'td.addtoquizaction'
};
var PARAMS = {
PAGE: 'addonpage',
HEADER: 'header'
};
var POPUP = function() {
POPUP.superclass.constructor.apply(this, arguments);
};
Y.extend(POPUP, Y.Base, {
loadingDiv: '',
dialogue: null,
addonpage: 0,
create_dialogue: function() {
// Create a dialogue on the page and hide it.
config = {
headerContent : '',
bodyContent : Y.one(CSS.QBANKLOADING),
draggable : true,
modal : true,
centered: true,
width: null,
visible: false,
postmethod: 'form',
footerContent: null,
extraClasses: ['mod_quiz_qbank_dialogue']
};
this.dialogue = new M.core.dialogue(config);
this.dialogue.bodyNode.delegate('click', this.link_clicked, 'a[href]', this);
this.dialogue.hide();
this.loadingDiv = this.dialogue.bodyNode.getHTML();
Y.later(100, this, function() {this.load_content(window.location.search);});
},
initializer : function() {
if (!Y.one(CSS.QBANKLOADING)) {
return;
}
this.create_dialogue();
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDQUESTIONLINKS, this);
},
display_dialogue : function (e) {
e.preventDefault();
this.dialogue.set('headerContent', e.currentTarget.getData(PARAMS.HEADER));
this.addonpage = e.currentTarget.getData(PARAMS.PAGE);
var controlsDiv = this.dialogue.bodyNode.one('.modulespecificbuttonscontainer');
if (controlsDiv) {
var hidden = controlsDiv.one('input[name=addonpage]');
if (!hidden) {
hidden = controlsDiv.appendChild('<input type="hidden" name="addonpage">');
}
hidden.set('value', this.addonpage);
}
this.dialogue.show();
},
load_content : function(queryString) {
this.dialogue.bodyNode.append(this.loadingDiv);
// If to support old IE.
if (window.history.replaceState) {
window.history.replaceState(null, '', M.cfg.wwwroot + '/mod/quiz/edit.php' + queryString);
}
Y.io(M.cfg.wwwroot + '/mod/quiz/questionbank.ajax.php' + queryString, {
method: 'GET',
on: {
success: this.load_done,
failure: this.load_failed
},
context: this
});
},
load_done: function(transactionid, response) {
var result = JSON.parse(response.responseText);
if (!result.status || result.status !== 'OK') {
// Because IIS is useless, Moodle can't send proper HTTP response
// codes, so we have to detect failures manually.
this.load_failed(transactionid, response);
return;
}
this.dialogue.bodyNode.setHTML(result.contents);
Y.use('moodle-question-chooser', function() {M.question.init_chooser({});});
this.dialogue.bodyNode.one('form').delegate('change', this.options_changed, '.searchoptions', this);
if (this.dialogue.visible) {
Y.later(0, this.dialogue, this.dialogue.centerDialogue);
}
M.question.qbankmanager.init();
},
load_failed: function() {
},
link_clicked: function(e) {
if (e.currentTarget.ancestor(CSS.ADDTOQUIZCONTAINER)) {
// These links need to work like normal, after we modify the URL.
e.currentTarget.set('href', e.currentTarget.get('href') + '&addonpage=' + this.addonpage);
return;
}
e.preventDefault();
this.load_content(e.currentTarget.get('search'));
},
options_changed: function(e) {
e.preventDefault();
this.load_content('?' + Y.IO.stringify(e.currentTarget.get('form')));
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.quizquestionbank = M.mod_quiz.quizquestionbank || {};
M.mod_quiz.quizquestionbank.init = function() {
return new POPUP();
};
}, '@VERSION@', {"requires": ["base", "event", "node", "io", "io-form", "yui-later", "moodle-question-qbankmanager"]});
@@ -0,0 +1,85 @@
YUI.add('moodle-mod_quiz-randomquestion', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Add a random question functionality for a popup in quiz editing page.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var CSS = {
RANDOMQUESTIONFORM: 'div.randomquestionformforpopup',
PAGEHIDDENINPUT: 'input#rform_qpage',
RANDOMQUESTIONLINKS: 'ul.menu a.addarandomquestion'
};
var PARAMS = {
PAGE: 'addonpage',
HEADER: 'header',
FORM: 'form'
};
var POPUP = function() {
POPUP.superclass.constructor.apply(this, arguments);
};
Y.extend(POPUP, Y.Base, {
dialogue: function(header) {
// Create a dialogue on the page and hide it.
config = {
headerContent : header,
bodyContent : Y.one(CSS.RANDOMQUESTIONFORM),
draggable : true,
modal : true,
zIndex : 1000,
centered: false,
width: 'auto',
visible: false,
postmethod: 'form',
footerContent: null
};
var popup = { dialog: null };
popup.dialog = new M.core.dialogue(config);
popup.dialog.show();
},
initializer : function() {
Y.one('body').delegate('click', this.display_dialogue, CSS.RANDOMQUESTIONLINKS, this);
},
display_dialogue : function (e) {
e.preventDefault();
Y.one(CSS.RANDOMQUESTIONFORM + ' ' + CSS.PAGEHIDDENINPUT).set('value',
e.currentTarget.getData(PARAMS.PAGE));
this.dialogue(e.currentTarget.getData(PARAMS.HEADER));
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.randomquestion = M.mod_quiz.randomquestion || {};
M.mod_quiz.randomquestion.init = function() {
return new POPUP();
};
}, '@VERSION@', {"requires": ["base", "event", "node", "io", "moodle-core-notification-dialogue"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-randomquestion",function(e,t){var n={RANDOMQUESTIONFORM:"div.randomquestionformforpopup",PAGEHIDDENINPUT:"input#rform_qpage",RANDOMQUESTIONLINKS:"ul.menu a.addarandomquestion"},r={PAGE:"addonpage",HEADER:"header",FORM:"form"},i=function(){i.superclass.constructor.apply(this,arguments)};e.extend(i,e.Base,{dialogue:function(t){config={headerContent:t,bodyContent:e.one(n.RANDOMQUESTIONFORM),draggable:!0,modal:!0,zIndex:1e3,centered:!1,width:"auto",visible:!1,postmethod:"form",footerContent:null};var r={dialog:null};r.dialog=new M.core.dialogue(config),r.dialog.show()},initializer:function(){e.one("body").delegate("click",this.display_dialogue,n.RANDOMQUESTIONLINKS,this)},display_dialogue:function(t){t.preventDefault(),e.one(n.RANDOMQUESTIONFORM+" "+n.PAGEHIDDENINPUT).set("value",t.currentTarget.getData(r.PAGE)),this.dialogue(t.currentTarget.getData(r.HEADER))}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.randomquestion=M.mod_quiz.randomquestion||{},M.mod_quiz.randomquestion.init=function(){return new i}},"@VERSION@",{requires:["base","event","node","io","moodle-core-notification-dialogue"]});
@@ -0,0 +1,85 @@
YUI.add('moodle-mod_quiz-randomquestion', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Add a random question functionality for a popup in quiz editing page.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var CSS = {
RANDOMQUESTIONFORM: 'div.randomquestionformforpopup',
PAGEHIDDENINPUT: 'input#rform_qpage',
RANDOMQUESTIONLINKS: 'ul.menu a.addarandomquestion'
};
var PARAMS = {
PAGE: 'addonpage',
HEADER: 'header',
FORM: 'form'
};
var POPUP = function() {
POPUP.superclass.constructor.apply(this, arguments);
};
Y.extend(POPUP, Y.Base, {
dialogue: function(header) {
// Create a dialogue on the page and hide it.
config = {
headerContent : header,
bodyContent : Y.one(CSS.RANDOMQUESTIONFORM),
draggable : true,
modal : true,
zIndex : 1000,
centered: false,
width: 'auto',
visible: false,
postmethod: 'form',
footerContent: null
};
var popup = { dialog: null };
popup.dialog = new M.core.dialogue(config);
popup.dialog.show();
},
initializer : function() {
Y.one('body').delegate('click', this.display_dialogue, CSS.RANDOMQUESTIONLINKS, this);
},
display_dialogue : function (e) {
e.preventDefault();
Y.one(CSS.RANDOMQUESTIONFORM + ' ' + CSS.PAGEHIDDENINPUT).set('value',
e.currentTarget.getData(PARAMS.PAGE));
this.dialogue(e.currentTarget.getData(PARAMS.HEADER));
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.randomquestion = M.mod_quiz.randomquestion || {};
M.mod_quiz.randomquestion.init = function() {
return new POPUP();
};
}, '@VERSION@', {"requires": ["base", "event", "node", "io", "moodle-core-notification-dialogue"]});
@@ -0,0 +1,86 @@
YUI.add('moodle-mod_quiz-repaginate', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Repaginate functionality for a popup in quiz editing page.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var CSS = {
REPAGINATECONTAINERCLASS: '.rpcontainerclass',
REPAGINATECOMMAND: '#repaginatecommand'
};
var PARAMS = {
CMID: 'cmid',
HEADER: 'header',
FORM: 'form'
};
var POPUP = function() {
POPUP.superclass.constructor.apply(this, arguments);
};
Y.extend(POPUP, Y.Base, {
header: null,
body: null,
initializer : function() {
rpcontainerclass = Y.one(CSS.REPAGINATECONTAINERCLASS);
// Set popup header and body.
this.header = rpcontainerclass.getAttribute(PARAMS.HEADER);
this.body = rpcontainerclass.getAttribute(PARAMS.FORM);
Y.one(CSS.REPAGINATECOMMAND).on('click', this.display_dialog, this);
},
display_dialog : function (e) {
e.preventDefault();
// Configure the popup.
var config = {
headerContent : this.header,
bodyContent : this.body,
draggable : true,
modal : true,
zIndex : 1000,
context: [CSS.REPAGINATECOMMAND, 'tr', 'br', ['beforeShow']],
centered: false,
width: '30em',
visible: false,
postmethod: 'form',
footerContent: null
};
var popup = { dialog: null };
popup.dialog = new M.core.dialogue(config);
popup.dialog.show();
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.repaginate = M.mod_quiz.repaginate || {};
M.mod_quiz.repaginate.init = function() {
return new POPUP();
};
}, '@VERSION@', {"requires": ["base", "event", "node", "io", "moodle-core-notification-dialogue"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-repaginate",function(e,t){var n={REPAGINATECONTAINERCLASS:".rpcontainerclass",REPAGINATECOMMAND:"#repaginatecommand"},r={CMID:"cmid",HEADER:"header",FORM:"form"},i=function(){i.superclass.constructor.apply(this,arguments)};e.extend(i,e.Base,{header:null,body:null,initializer:function(){rpcontainerclass=e.one(n.REPAGINATECONTAINERCLASS),this.header=rpcontainerclass.getAttribute(r.HEADER),this.body=rpcontainerclass.getAttribute(r.FORM),e.one(n.REPAGINATECOMMAND).on("click",this.display_dialog,this)},display_dialog:function(e){e.preventDefault();var t={headerContent:this.header,bodyContent:this.body,draggable:!0,modal:!0,zIndex:1e3,context:[n.REPAGINATECOMMAND,"tr","br",["beforeShow"]],centered:!1,width:"30em",visible:!1,postmethod:"form",footerContent:null},r={dialog:null};r.dialog=new M.core.dialogue(t),r.dialog.show()}}),M.mod_quiz=M.mod_quiz||{},M.mod_quiz.repaginate=M.mod_quiz.repaginate||{},M.mod_quiz.repaginate.init=function(){return new i}},"@VERSION@",{requires:["base","event","node","io","moodle-core-notification-dialogue"]});
@@ -0,0 +1,86 @@
YUI.add('moodle-mod_quiz-repaginate', function (Y, NAME) {
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Repaginate functionality for a popup in quiz editing page.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var CSS = {
REPAGINATECONTAINERCLASS: '.rpcontainerclass',
REPAGINATECOMMAND: '#repaginatecommand'
};
var PARAMS = {
CMID: 'cmid',
HEADER: 'header',
FORM: 'form'
};
var POPUP = function() {
POPUP.superclass.constructor.apply(this, arguments);
};
Y.extend(POPUP, Y.Base, {
header: null,
body: null,
initializer : function() {
rpcontainerclass = Y.one(CSS.REPAGINATECONTAINERCLASS);
// Set popup header and body.
this.header = rpcontainerclass.getAttribute(PARAMS.HEADER);
this.body = rpcontainerclass.getAttribute(PARAMS.FORM);
Y.one(CSS.REPAGINATECOMMAND).on('click', this.display_dialog, this);
},
display_dialog : function (e) {
e.preventDefault();
// Configure the popup.
var config = {
headerContent : this.header,
bodyContent : this.body,
draggable : true,
modal : true,
zIndex : 1000,
context: [CSS.REPAGINATECOMMAND, 'tr', 'br', ['beforeShow']],
centered: false,
width: '30em',
visible: false,
postmethod: 'form',
footerContent: null
};
var popup = { dialog: null };
popup.dialog = new M.core.dialogue(config);
popup.dialog.show();
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.repaginate = M.mod_quiz.repaginate || {};
M.mod_quiz.repaginate.init = function() {
return new POPUP();
};
}, '@VERSION@', {"requires": ["base", "event", "node", "io", "moodle-core-notification-dialogue"]});
@@ -0,0 +1,867 @@
YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) {
/**
* Resource and activity toolbox class.
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a course in editing mode.
*
* @module moodle-course-toolboxes
* @namespace M.course.toolboxes
*/
// The CSS classes we use.
var CSS = {
ACTIVITYINSTANCE : 'activityinstance',
AVAILABILITYINFODIV : 'div.availabilityinfo',
CONTENTWITHOUTLINK : 'contentwithoutlink',
CONDITIONALHIDDEN : 'conditionalhidden',
DIMCLASS : 'dimmed',
DIMMEDTEXT : 'dimmed_text',
EDITINSTRUCTIONS : 'editinstructions',
EDITINGMAXMARK: 'editor_displayed',
HIDE : 'hide',
JOIN: 'page_join',
MODINDENTCOUNT : 'mod-indent-',
MODINDENTHUGE : 'mod-indent-huge',
MODULEIDPREFIX : 'slot-',
PAGE: 'page',
SECTIONHIDDENCLASS : 'hidden',
SECTIONIDPREFIX : 'section-',
SLOT : 'slot',
SHOW : 'editing_show',
TITLEEDITOR : 'titleeditor'
},
// The CSS selectors we use.
SELECTOR = {
ACTIONAREA: '.actions',
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',
ACTIVITYMAXMARK : 'input[name=maxmark]',
COMMANDSPAN : '.commands',
CONTENTAFTERLINK : 'div.contentafterlink',
CONTENTWITHOUTLINK : 'div.contentwithoutlink',
EDITMAXMARK: 'a.editing_maxmark',
HIDE : 'a.editing_hide',
HIGHLIGHT : 'a.editing_highlight',
INSTANCENAME : 'span.instancename',
INSTANCEMAXMARK : 'span.instancemaxmark',
MODINDENTDIV : '.mod-indent',
MODINDENTOUTER : '.mod-indent-outer',
PAGECONTENT : 'div#page-content',
PAGELI : 'li.page',
SECTIONUL : 'ul.section',
SHOW : 'a.' + CSS.SHOW,
SHOWHIDE : 'a.editing_showhide',
SLOTLI : 'li.slot',
SUMMARKS : '.mod_quiz_summarks'
},
BODY = Y.one(document.body);
// Setup the basic namespace.
M.mod_quiz = M.mod_quiz || {};
/**
* The toolbox class is a generic class which should never be directly
* instantiated. Please extend it instead.
*
* @class toolbox
* @constructor
* @protected
* @extends Base
*/
var TOOLBOX = function() {
TOOLBOX.superclass.constructor.apply(this, arguments);
};
Y.extend(TOOLBOX, Y.Base, {
/**
* Send a request using the REST API
*
* @method send_request
* @param {Object} data The data to submit with the AJAX request
* @param {Node} [statusspinner] A statusspinner which may contain a section loader
* @param {Function} success_callback The callback to use on success
* @param {Object} [optionalconfig] Any additional configuration to submit
* @chainable
*/
send_request: function(data, statusspinner, success_callback, optionalconfig) {
// Default data structure
if (!data) {
data = {};
}
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
data[varname] = pageparams[varname];
}
data.sesskey = M.cfg.sesskey;
data.courseid = this.get('courseid');
data.quizid = this.get('quizid');
var uri = M.cfg.wwwroot + this.get('ajaxurl');
// Define the configuration to send with the request
var responsetext = [];
var config = {
method: 'POST',
data: data,
on: {
success: function(tid, response) {
try {
responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
} catch (e) {}
// Run the callback if we have one.
if (responsetext.newsummarks) {
Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks);
}
if (success_callback) {
Y.bind(success_callback, this, responsetext)();
}
if (statusspinner) {
window.setTimeout(function() {
statusspinner.hide();
}, 400);
}
},
failure: function(tid, response) {
if (statusspinner) {
statusspinner.hide();
}
new M.core.ajaxException(response);
}
},
context: this
};
// Apply optional config
if (optionalconfig) {
for (varname in optionalconfig) {
config[varname] = optionalconfig[varname];
}
}
if (statusspinner) {
statusspinner.show();
}
// Send the request
Y.io(uri, config);
return this;
}
},
{
NAME: 'mod_quiz-toolbox',
ATTRS: {
/**
* The ID of the Moodle Course being edited.
*
* @attribute courseid
* @default 0
* @type Number
*/
courseid: {
'value': 0
},
/**
* The Moodle course format.
*
* @attribute format
* @default 'topics'
* @type String
*/
quizid: {
'value': 0
},
/**
* The URL to use when submitting requests.
* @attribute ajaxurl
* @default null
* @type String
*/
ajaxurl: {
'value': null
},
/**
* Any additional configuration passed when creating the instance.
*
* @attribute config
* @default {}
* @type Object
*/
config: {
'value': {}
}
}
}
);
/**
* Resource and activity toolbox class.
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a quiz in editing mode.
*
* @module mod_quiz-resource-toolbox
* @namespace M.mod_quiz.resource_toolbox
*/
/**
* Resource and activity toolbox class.
*
* This is a class extending TOOLBOX containing code specific to resources
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a quiz in editing mode.
*
* @class resources
* @constructor
* @extends M.course.toolboxes.toolbox
*/
var RESOURCETOOLBOX = function() {
RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
};
Y.extend(RESOURCETOOLBOX, TOOLBOX, {
/**
* An Array of events added when editing a max mark field.
* These should all be detached when editing is complete.
*
* @property editmaxmarkevents
* @protected
* @type Array
* @protected
*/
editmaxmarkevents: [],
/**
*
*/
NODE_PAGE: 1,
NODE_SLOT: 2,
NODE_JOIN: 3,
/**
* Initialize the resource toolbox
*
* For each activity the commands are updated and a reference to the activity is attached.
* This way it doesn't matter where the commands are going to called from they have a reference to the
* activity that they relate to.
* This is essential as some of the actions are displayed in an actionmenu which removes them from the
* page flow.
*
* This function also creates a single event delegate to manage all AJAX actions for all activities on
* the page.
*
* @method initializer
* @protected
*/
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);
},
/**
* Handles the delegation event. When this is fired someone has triggered an action.
*
* Note not all actions will result in an AJAX enhancement.
*
* @protected
* @method handle_data_action
* @param {EventFacade} ev The event that was triggered.
* @returns {boolean}
*/
handle_data_action: function(ev) {
// We need to get the anchor element that triggered this event.
var node = ev.target;
if (!node.test('a')) {
node = node.ancestor(SELECTOR.ACTIVITYACTION);
}
// From the anchor we can get both the activity (added during initialisation) and the action being
// performed (added by the UI as a data attribute).
var action = node.getData('action'),
activity = node.ancestor(SELECTOR.ACTIVITYLI);
if (!node.test('a') || !action || !activity) {
// It wasn't a valid action node.
return;
}
// Switch based upon the action and do the desired thing.
switch (action) {
case 'editmaxmark':
// The user wishes to edit the maxmark of the resource.
this.edit_maxmark(ev, node, activity, action);
break;
case 'delete':
// The user is deleting the activity.
this.delete_with_confirmation(ev, node, activity, action);
break;
case 'linkpage':
case 'unlinkpage':
// The user is linking or unlinking pages.
this.link_page(ev, node, activity, action);
break;
default:
// Nothing to do here!
break;
}
},
/**
* Add a loading icon to the specified activity.
*
* The icon is added within the action area.
*
* @method add_spinner
* @param {Node} activity The activity to add a loading icon to
* @return {Node|null} The newly created icon, or null if the action area was not found.
*/
add_spinner: function(activity) {
var actionarea = activity.one(SELECTOR.ACTIONAREA);
if (actionarea) {
return M.util.add_spinner(Y, actionarea);
}
return null;
},
/**
* Deletes the given activity or resource after confirmation.
*
* @protected
* @method delete_with_confirmation
* @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.
* @chainable
*/
delete_with_confirmation: function(ev, button, activity) {
// Prevent the default button action
ev.preventDefault();
// Get the element we're working on
var element = activity,
// Create confirm string (different if element has or does not have name)
confirmstring = '',
qtypename = M.util.get_string('pluginname',
'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1]);
confirmstring = M.util.get_string('confirmremovequestion', 'quiz', qtypename);
// Create the confirmation dialogue.
var confirm = new M.core.confirm({
question: confirmstring,
modal: true
});
// If it is confirmed.
confirm.on('complete-yes', function() {
// Actually remove the element.
element.remove();
Y.Moodle.mod_quiz.util.slot.reorder_slots();
var data = {
'class': 'resource',
'action': 'DELETE',
'id': Y.Moodle.mod_quiz.util.slot.getId(element)
};
this.send_request(data);
if (M.core.actionmenu && M.core.actionmenu.instance) {
M.core.actionmenu.instance.hideMenu();
}
window.location.reload(true);
}, this);
return this;
},
/**
* Edit the maxmark for the resource
*
* @protected
* @method edit_maxmark
* @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 that has been requested.
* @return Boolean
*/
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),
instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
currentmaxmark = instancemaxmark.get('firstChild'),
oldmaxmark = currentmaxmark.get('data'),
maxmarktext = oldmaxmark,
thisevent,
anchor = instancemaxmark,// Grab the anchor so that we can swap it with the edit form.
data = {
'class' : 'resource',
'field' : 'getmaxmark',
'id' : activityid
};
// Prevent the default actions.
ev.preventDefault();
this.send_request(data, null, function(response) {
if (M.core.actionmenu && M.core.actionmenu.instance) {
M.core.actionmenu.instance.hideMenu();
}
// Try to retrieve the existing string from the server
if (response.instancemaxmark) {
maxmarktext = response.instancemaxmark;
}
// Create the editor and submit button
var editform = Y.Node.create('<form action="#" />');
var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
.set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
var editor = Y.Node.create('<input name="maxmark" type="text" class="' + CSS.TITLEEDITOR + '" />').setAttrs({
'value' : maxmarktext,
'autocomplete' : 'off',
'aria-describedby' : 'id_editinstructions',
'maxLength' : '12',
'size' : parseInt(this.get('config').questiondecimalpoints, 10) + 2
});
// Clear the existing content and put the editor in
editform.appendChild(editor);
editform.setData('anchor', anchor);
instance.insert(editinstructions, 'before');
anchor.replace(editform);
// Force the editing instruction to match the mod-indent position.
var padside = 'left';
if (right_to_left()) {
padside = 'right';
}
// We hide various components whilst editing:
activity.addClass(CSS.EDITINGMAXMARK);
// Focus and select the editor text
editor.focus().select();
// Cancel the edit if we lose focus or the escape key is pressed.
thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false);
this.editmaxmarkevents.push(thisevent);
thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true);
this.editmaxmarkevents.push(thisevent);
// Handle form submission.
thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark);
this.editmaxmarkevents.push(thisevent);
});
},
/**
* Handles the submit event when editing the activity or resources maxmark.
*
* @protected
* @method edit_maxmark_submit
* @param {EventFacade} ev The event that triggered this.
* @param {Node} activity The activity whose maxmark we are altering.
* @param {String} originalmaxmark The original maxmark the activity or resource had.
*/
edit_maxmark_submit : function(ev, activity, originalmaxmark) {
// We don't actually want to submit anything
ev.preventDefault();
var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value'));
var spinner = this.add_spinner(activity);
this.edit_maxmark_clear(activity);
activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark);
if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) {
var data = {
'class' : 'resource',
'field' : 'updatemaxmark',
'maxmark' : newmaxmark,
'id' : Y.Moodle.mod_quiz.util.slot.getId(activity)
};
this.send_request(data, spinner, function(response) {
if (response.instancemaxmark) {
activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark);
}
});
}
},
/**
* Handles the cancel event when editing the activity or resources maxmark.
*
* @protected
* @method edit_maxmark_cancel
* @param {EventFacade} ev The event that triggered this.
* @param {Node} activity The activity whose maxmark we are altering.
* @param {Boolean} preventdefault If true we should prevent the default action from occuring.
*/
edit_maxmark_cancel : function(ev, activity, preventdefault) {
if (preventdefault) {
ev.preventDefault();
}
this.edit_maxmark_clear(activity);
},
/**
* Handles clearing the editing UI and returning things to the original state they were in.
*
* @protected
* @method edit_maxmark_clear
* @param {Node} activity The activity whose maxmark we were altering.
*/
edit_maxmark_clear : function(activity) {
// Detach all listen events to prevent duplicate triggers
new Y.EventHandle(this.editmaxmarkevents).detach();
var editform = activity.one(SELECTOR.ACTIVITYFORM),
instructions = activity.one('#id_editinstructions');
if (editform) {
editform.replace(editform.getData('anchor'));
}
if (instructions) {
instructions.remove();
}
// Remove the editing class again to revert the display.
activity.removeClass(CSS.EDITINGMAXMARK);
// Refocus the link which was clicked originally so the user can continue using keyboard nav.
Y.later(100, this, function() {
activity.one(SELECTOR.EDITMAXMARK).focus();
});
// This hack is to keep Behat happy until they release a version of
// MinkSelenium2Driver that fixes
// https://github.com/Behat/MinkSelenium2Driver/issues/80.
if (!Y.one('input[name=maxmark')) {
Y.one('body').append('<input type="text" name="maxmark" style="display: none">');
}
},
/**
* Joins or separates the given slot with the page of the previous slot. Reorders the pages of
* the other slots
*
* @protected
* @method link_page
* @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.
* @chainable
*/
link_page: function(ev, button, activity, action) {
// Prevent the default button action
ev.preventDefault();
activity = activity.next('li.activity.slot');
var spinner = this.add_spinner(activity),
slotid = 0;
var value = action === 'linkpage' ? 1:2;
var data = {
'class': 'resource',
'field': 'linkslottopage',
'id': slotid,
'value': value
};
slotid = Y.Moodle.mod_quiz.util.slot.getId(activity);
if (slotid) {
data.id = Number(slotid);
}
this.send_request(data, spinner, function(response) {
window.location.reload(true);
// if (response.slots) {
// this.repaginate_slots(response.slots);
// }
});
return this;
},
repaginate_slots: function(slots) {
this.slots = slots;
var section = Y.one(SELECTOR.PAGECONTENT + ' ' + SELECTOR.SECTIONUL),
activities = section.all(SELECTOR.ACTIVITYLI);
activities.each(function(node) {
// What element is it? page/slot/link
// what is the current slot?
var type;
var slot;
if(node.hasClass(CSS.PAGE)){
type = this.NODE_PAGE;
slot = node.next(SELECTOR.SLOTLI);
} else if (node.hasClass(CSS.SLOT)){
type = this.NODE_SLOT;
slot = node;
} else if (node.hasClass(CSS.JOIN)){
type = this.NODE_JOIN;
slot = node.previous(SELECTOR.SLOTLI);
}
// getSlotnumber() Should be a method of util.slot
var slotnumber = Number(Y.Moodle.mod_quiz.util.slot.getNumber(slot));
if(!type){
// Nothing we can do.
return;
}
// Is it correct?
if(!this.slots.hasOwnProperty(slotnumber)){
// An error. We should handle this.
return;
}
var slotdata = this.slots[slotnumber];
if(type === this.NODE_PAGE){
// Get page number
var pagenumber = Y.Moodle.mod_quiz.util.page.getNumber(node);
// Is the page number correct?
if (slotdata.page === pagenumber) {
console.log('slotdata.page == pagenumber return');
return;
}
if (pagenumber < slotdata.page) {
// Remove page node.
node.remove();
}
else {
// Add page node.
console.log('pagenumber > slotdata.page update page number');
}
}
}, this);
},
NAME : 'mod_quiz-resource-toolbox',
ATTRS : {
courseid : {
'value' : 0
},
quizid : {
'value' : 0
}
}
});
M.mod_quiz.resource_toolbox = null;
M.mod_quiz.init_resource_toolbox = function(config) {
M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config);
return M.mod_quiz.resource_toolbox;
};
/**
* Resource and activity toolbox class.
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a course in editing mode.
*
* @module moodle-mod_quiz-toolboxes
* @namespace M.mod_quiz.toolboxes
*/
/**
* Section toolbox class.
*
* This class is responsible for managing AJAX interactions with sections
* when viewing a course in editing mode.
*
* @class section
* @constructor
* @extends M.mod_quiz.toolboxes.toolbox
*/
var SECTIONTOOLBOX = function() {
SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
};
Y.extend(SECTIONTOOLBOX, TOOLBOX, {
/**
* Initialize the section toolboxes module.
*
* Updates all span.commands with relevant handlers and other required changes.
*
* @method initializer
* @protected
*/
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',
ATTRS : {
courseid : {
'value' : 0
},
quizid : {
'value' : 0
},
format : {
'value' : 'topics'
}
}
});
M.mod_quiz.init_section_toolbox = function(config) {
return new SECTIONTOOLBOX(config);
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"event",
"event-key",
"io",
"moodle-mod_quiz-quizbase",
"moodle-mod_quiz-util-slot",
"moodle-core-notification-ajaxexception"
]
});
File diff suppressed because one or more lines are too long
@@ -0,0 +1,867 @@
YUI.add('moodle-mod_quiz-toolboxes', function (Y, NAME) {
/**
* Resource and activity toolbox class.
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a course in editing mode.
*
* @module moodle-course-toolboxes
* @namespace M.course.toolboxes
*/
// The CSS classes we use.
var CSS = {
ACTIVITYINSTANCE : 'activityinstance',
AVAILABILITYINFODIV : 'div.availabilityinfo',
CONTENTWITHOUTLINK : 'contentwithoutlink',
CONDITIONALHIDDEN : 'conditionalhidden',
DIMCLASS : 'dimmed',
DIMMEDTEXT : 'dimmed_text',
EDITINSTRUCTIONS : 'editinstructions',
EDITINGMAXMARK: 'editor_displayed',
HIDE : 'hide',
JOIN: 'page_join',
MODINDENTCOUNT : 'mod-indent-',
MODINDENTHUGE : 'mod-indent-huge',
MODULEIDPREFIX : 'slot-',
PAGE: 'page',
SECTIONHIDDENCLASS : 'hidden',
SECTIONIDPREFIX : 'section-',
SLOT : 'slot',
SHOW : 'editing_show',
TITLEEDITOR : 'titleeditor'
},
// The CSS selectors we use.
SELECTOR = {
ACTIONAREA: '.actions',
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',
ACTIVITYMAXMARK : 'input[name=maxmark]',
COMMANDSPAN : '.commands',
CONTENTAFTERLINK : 'div.contentafterlink',
CONTENTWITHOUTLINK : 'div.contentwithoutlink',
EDITMAXMARK: 'a.editing_maxmark',
HIDE : 'a.editing_hide',
HIGHLIGHT : 'a.editing_highlight',
INSTANCENAME : 'span.instancename',
INSTANCEMAXMARK : 'span.instancemaxmark',
MODINDENTDIV : '.mod-indent',
MODINDENTOUTER : '.mod-indent-outer',
PAGECONTENT : 'div#page-content',
PAGELI : 'li.page',
SECTIONUL : 'ul.section',
SHOW : 'a.' + CSS.SHOW,
SHOWHIDE : 'a.editing_showhide',
SLOTLI : 'li.slot',
SUMMARKS : '.mod_quiz_summarks'
},
BODY = Y.one(document.body);
// Setup the basic namespace.
M.mod_quiz = M.mod_quiz || {};
/**
* The toolbox class is a generic class which should never be directly
* instantiated. Please extend it instead.
*
* @class toolbox
* @constructor
* @protected
* @extends Base
*/
var TOOLBOX = function() {
TOOLBOX.superclass.constructor.apply(this, arguments);
};
Y.extend(TOOLBOX, Y.Base, {
/**
* Send a request using the REST API
*
* @method send_request
* @param {Object} data The data to submit with the AJAX request
* @param {Node} [statusspinner] A statusspinner which may contain a section loader
* @param {Function} success_callback The callback to use on success
* @param {Object} [optionalconfig] Any additional configuration to submit
* @chainable
*/
send_request: function(data, statusspinner, success_callback, optionalconfig) {
// Default data structure
if (!data) {
data = {};
}
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
data[varname] = pageparams[varname];
}
data.sesskey = M.cfg.sesskey;
data.courseid = this.get('courseid');
data.quizid = this.get('quizid');
var uri = M.cfg.wwwroot + this.get('ajaxurl');
// Define the configuration to send with the request
var responsetext = [];
var config = {
method: 'POST',
data: data,
on: {
success: function(tid, response) {
try {
responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
} catch (e) {}
// Run the callback if we have one.
if (responsetext.newsummarks) {
Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks);
}
if (success_callback) {
Y.bind(success_callback, this, responsetext)();
}
if (statusspinner) {
window.setTimeout(function() {
statusspinner.hide();
}, 400);
}
},
failure: function(tid, response) {
if (statusspinner) {
statusspinner.hide();
}
new M.core.ajaxException(response);
}
},
context: this
};
// Apply optional config
if (optionalconfig) {
for (varname in optionalconfig) {
config[varname] = optionalconfig[varname];
}
}
if (statusspinner) {
statusspinner.show();
}
// Send the request
Y.io(uri, config);
return this;
}
},
{
NAME: 'mod_quiz-toolbox',
ATTRS: {
/**
* The ID of the Moodle Course being edited.
*
* @attribute courseid
* @default 0
* @type Number
*/
courseid: {
'value': 0
},
/**
* The Moodle course format.
*
* @attribute format
* @default 'topics'
* @type String
*/
quizid: {
'value': 0
},
/**
* The URL to use when submitting requests.
* @attribute ajaxurl
* @default null
* @type String
*/
ajaxurl: {
'value': null
},
/**
* Any additional configuration passed when creating the instance.
*
* @attribute config
* @default {}
* @type Object
*/
config: {
'value': {}
}
}
}
);
/**
* Resource and activity toolbox class.
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a quiz in editing mode.
*
* @module mod_quiz-resource-toolbox
* @namespace M.mod_quiz.resource_toolbox
*/
/**
* Resource and activity toolbox class.
*
* This is a class extending TOOLBOX containing code specific to resources
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a quiz in editing mode.
*
* @class resources
* @constructor
* @extends M.course.toolboxes.toolbox
*/
var RESOURCETOOLBOX = function() {
RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
};
Y.extend(RESOURCETOOLBOX, TOOLBOX, {
/**
* An Array of events added when editing a max mark field.
* These should all be detached when editing is complete.
*
* @property editmaxmarkevents
* @protected
* @type Array
* @protected
*/
editmaxmarkevents: [],
/**
*
*/
NODE_PAGE: 1,
NODE_SLOT: 2,
NODE_JOIN: 3,
/**
* Initialize the resource toolbox
*
* For each activity the commands are updated and a reference to the activity is attached.
* This way it doesn't matter where the commands are going to called from they have a reference to the
* activity that they relate to.
* This is essential as some of the actions are displayed in an actionmenu which removes them from the
* page flow.
*
* This function also creates a single event delegate to manage all AJAX actions for all activities on
* the page.
*
* @method initializer
* @protected
*/
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);
},
/**
* Handles the delegation event. When this is fired someone has triggered an action.
*
* Note not all actions will result in an AJAX enhancement.
*
* @protected
* @method handle_data_action
* @param {EventFacade} ev The event that was triggered.
* @returns {boolean}
*/
handle_data_action: function(ev) {
// We need to get the anchor element that triggered this event.
var node = ev.target;
if (!node.test('a')) {
node = node.ancestor(SELECTOR.ACTIVITYACTION);
}
// From the anchor we can get both the activity (added during initialisation) and the action being
// performed (added by the UI as a data attribute).
var action = node.getData('action'),
activity = node.ancestor(SELECTOR.ACTIVITYLI);
if (!node.test('a') || !action || !activity) {
// It wasn't a valid action node.
return;
}
// Switch based upon the action and do the desired thing.
switch (action) {
case 'editmaxmark':
// The user wishes to edit the maxmark of the resource.
this.edit_maxmark(ev, node, activity, action);
break;
case 'delete':
// The user is deleting the activity.
this.delete_with_confirmation(ev, node, activity, action);
break;
case 'linkpage':
case 'unlinkpage':
// The user is linking or unlinking pages.
this.link_page(ev, node, activity, action);
break;
default:
// Nothing to do here!
break;
}
},
/**
* Add a loading icon to the specified activity.
*
* The icon is added within the action area.
*
* @method add_spinner
* @param {Node} activity The activity to add a loading icon to
* @return {Node|null} The newly created icon, or null if the action area was not found.
*/
add_spinner: function(activity) {
var actionarea = activity.one(SELECTOR.ACTIONAREA);
if (actionarea) {
return M.util.add_spinner(Y, actionarea);
}
return null;
},
/**
* Deletes the given activity or resource after confirmation.
*
* @protected
* @method delete_with_confirmation
* @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.
* @chainable
*/
delete_with_confirmation: function(ev, button, activity) {
// Prevent the default button action
ev.preventDefault();
// Get the element we're working on
var element = activity,
// Create confirm string (different if element has or does not have name)
confirmstring = '',
qtypename = M.util.get_string('pluginname',
'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1]);
confirmstring = M.util.get_string('confirmremovequestion', 'quiz', qtypename);
// Create the confirmation dialogue.
var confirm = new M.core.confirm({
question: confirmstring,
modal: true
});
// If it is confirmed.
confirm.on('complete-yes', function() {
// Actually remove the element.
element.remove();
Y.Moodle.mod_quiz.util.slot.reorder_slots();
var data = {
'class': 'resource',
'action': 'DELETE',
'id': Y.Moodle.mod_quiz.util.slot.getId(element)
};
this.send_request(data);
if (M.core.actionmenu && M.core.actionmenu.instance) {
M.core.actionmenu.instance.hideMenu();
}
window.location.reload(true);
}, this);
return this;
},
/**
* Edit the maxmark for the resource
*
* @protected
* @method edit_maxmark
* @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 that has been requested.
* @return Boolean
*/
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),
instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
currentmaxmark = instancemaxmark.get('firstChild'),
oldmaxmark = currentmaxmark.get('data'),
maxmarktext = oldmaxmark,
thisevent,
anchor = instancemaxmark,// Grab the anchor so that we can swap it with the edit form.
data = {
'class' : 'resource',
'field' : 'getmaxmark',
'id' : activityid
};
// Prevent the default actions.
ev.preventDefault();
this.send_request(data, null, function(response) {
if (M.core.actionmenu && M.core.actionmenu.instance) {
M.core.actionmenu.instance.hideMenu();
}
// Try to retrieve the existing string from the server
if (response.instancemaxmark) {
maxmarktext = response.instancemaxmark;
}
// Create the editor and submit button
var editform = Y.Node.create('<form action="#" />');
var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
.set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
var editor = Y.Node.create('<input name="maxmark" type="text" class="' + CSS.TITLEEDITOR + '" />').setAttrs({
'value' : maxmarktext,
'autocomplete' : 'off',
'aria-describedby' : 'id_editinstructions',
'maxLength' : '12',
'size' : parseInt(this.get('config').questiondecimalpoints, 10) + 2
});
// Clear the existing content and put the editor in
editform.appendChild(editor);
editform.setData('anchor', anchor);
instance.insert(editinstructions, 'before');
anchor.replace(editform);
// Force the editing instruction to match the mod-indent position.
var padside = 'left';
if (right_to_left()) {
padside = 'right';
}
// We hide various components whilst editing:
activity.addClass(CSS.EDITINGMAXMARK);
// Focus and select the editor text
editor.focus().select();
// Cancel the edit if we lose focus or the escape key is pressed.
thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false);
this.editmaxmarkevents.push(thisevent);
thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true);
this.editmaxmarkevents.push(thisevent);
// Handle form submission.
thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark);
this.editmaxmarkevents.push(thisevent);
});
},
/**
* Handles the submit event when editing the activity or resources maxmark.
*
* @protected
* @method edit_maxmark_submit
* @param {EventFacade} ev The event that triggered this.
* @param {Node} activity The activity whose maxmark we are altering.
* @param {String} originalmaxmark The original maxmark the activity or resource had.
*/
edit_maxmark_submit : function(ev, activity, originalmaxmark) {
// We don't actually want to submit anything
ev.preventDefault();
var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value'));
var spinner = this.add_spinner(activity);
this.edit_maxmark_clear(activity);
activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark);
if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) {
var data = {
'class' : 'resource',
'field' : 'updatemaxmark',
'maxmark' : newmaxmark,
'id' : Y.Moodle.mod_quiz.util.slot.getId(activity)
};
this.send_request(data, spinner, function(response) {
if (response.instancemaxmark) {
activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark);
}
});
}
},
/**
* Handles the cancel event when editing the activity or resources maxmark.
*
* @protected
* @method edit_maxmark_cancel
* @param {EventFacade} ev The event that triggered this.
* @param {Node} activity The activity whose maxmark we are altering.
* @param {Boolean} preventdefault If true we should prevent the default action from occuring.
*/
edit_maxmark_cancel : function(ev, activity, preventdefault) {
if (preventdefault) {
ev.preventDefault();
}
this.edit_maxmark_clear(activity);
},
/**
* Handles clearing the editing UI and returning things to the original state they were in.
*
* @protected
* @method edit_maxmark_clear
* @param {Node} activity The activity whose maxmark we were altering.
*/
edit_maxmark_clear : function(activity) {
// Detach all listen events to prevent duplicate triggers
new Y.EventHandle(this.editmaxmarkevents).detach();
var editform = activity.one(SELECTOR.ACTIVITYFORM),
instructions = activity.one('#id_editinstructions');
if (editform) {
editform.replace(editform.getData('anchor'));
}
if (instructions) {
instructions.remove();
}
// Remove the editing class again to revert the display.
activity.removeClass(CSS.EDITINGMAXMARK);
// Refocus the link which was clicked originally so the user can continue using keyboard nav.
Y.later(100, this, function() {
activity.one(SELECTOR.EDITMAXMARK).focus();
});
// This hack is to keep Behat happy until they release a version of
// MinkSelenium2Driver that fixes
// https://github.com/Behat/MinkSelenium2Driver/issues/80.
if (!Y.one('input[name=maxmark')) {
Y.one('body').append('<input type="text" name="maxmark" style="display: none">');
}
},
/**
* Joins or separates the given slot with the page of the previous slot. Reorders the pages of
* the other slots
*
* @protected
* @method link_page
* @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.
* @chainable
*/
link_page: function(ev, button, activity, action) {
// Prevent the default button action
ev.preventDefault();
activity = activity.next('li.activity.slot');
var spinner = this.add_spinner(activity),
slotid = 0;
var value = action === 'linkpage' ? 1:2;
var data = {
'class': 'resource',
'field': 'linkslottopage',
'id': slotid,
'value': value
};
slotid = Y.Moodle.mod_quiz.util.slot.getId(activity);
if (slotid) {
data.id = Number(slotid);
}
this.send_request(data, spinner, function(response) {
window.location.reload(true);
// if (response.slots) {
// this.repaginate_slots(response.slots);
// }
});
return this;
},
repaginate_slots: function(slots) {
this.slots = slots;
var section = Y.one(SELECTOR.PAGECONTENT + ' ' + SELECTOR.SECTIONUL),
activities = section.all(SELECTOR.ACTIVITYLI);
activities.each(function(node) {
// What element is it? page/slot/link
// what is the current slot?
var type;
var slot;
if(node.hasClass(CSS.PAGE)){
type = this.NODE_PAGE;
slot = node.next(SELECTOR.SLOTLI);
} else if (node.hasClass(CSS.SLOT)){
type = this.NODE_SLOT;
slot = node;
} else if (node.hasClass(CSS.JOIN)){
type = this.NODE_JOIN;
slot = node.previous(SELECTOR.SLOTLI);
}
// getSlotnumber() Should be a method of util.slot
var slotnumber = Number(Y.Moodle.mod_quiz.util.slot.getNumber(slot));
if(!type){
// Nothing we can do.
return;
}
// Is it correct?
if(!this.slots.hasOwnProperty(slotnumber)){
// An error. We should handle this.
return;
}
var slotdata = this.slots[slotnumber];
if(type === this.NODE_PAGE){
// Get page number
var pagenumber = Y.Moodle.mod_quiz.util.page.getNumber(node);
// Is the page number correct?
if (slotdata.page === pagenumber) {
console.log('slotdata.page == pagenumber return');
return;
}
if (pagenumber < slotdata.page) {
// Remove page node.
node.remove();
}
else {
// Add page node.
console.log('pagenumber > slotdata.page update page number');
}
}
}, this);
},
NAME : 'mod_quiz-resource-toolbox',
ATTRS : {
courseid : {
'value' : 0
},
quizid : {
'value' : 0
}
}
});
M.mod_quiz.resource_toolbox = null;
M.mod_quiz.init_resource_toolbox = function(config) {
M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config);
return M.mod_quiz.resource_toolbox;
};
/**
* Resource and activity toolbox class.
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a course in editing mode.
*
* @module moodle-mod_quiz-toolboxes
* @namespace M.mod_quiz.toolboxes
*/
/**
* Section toolbox class.
*
* This class is responsible for managing AJAX interactions with sections
* when viewing a course in editing mode.
*
* @class section
* @constructor
* @extends M.mod_quiz.toolboxes.toolbox
*/
var SECTIONTOOLBOX = function() {
SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
};
Y.extend(SECTIONTOOLBOX, TOOLBOX, {
/**
* Initialize the section toolboxes module.
*
* Updates all span.commands with relevant handlers and other required changes.
*
* @method initializer
* @protected
*/
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',
ATTRS : {
courseid : {
'value' : 0
},
quizid : {
'value' : 0
},
format : {
'value' : 'topics'
}
}
});
M.mod_quiz.init_section_toolbox = function(config) {
return new SECTIONTOOLBOX(config);
};
}, '@VERSION@', {
"requires": [
"base",
"node",
"event",
"event-key",
"io",
"moodle-mod_quiz-quizbase",
"moodle-mod_quiz-util-slot",
"moodle-core-notification-ajaxexception"
]
});
@@ -0,0 +1,20 @@
YUI.add('moodle-mod_quiz-util-base', function (Y, NAME) {
/**
* The Moodle.mod_quiz.util classes provide quiz-related utility functions.
*
* @module moodle-mod_quiz-util
* @main
*/
Y.namespace('Moodle.mod_quiz.util');
/**
* A collection of general utility functions for use in quiz.
*
* @class Moodle.mod_quiz.util
* @static
*/
}, '@VERSION@');
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-util-base",function(e,t){e.namespace("Moodle.mod_quiz.util")},"@VERSION@");
@@ -0,0 +1,20 @@
YUI.add('moodle-mod_quiz-util-base', function (Y, NAME) {
/**
* The Moodle.mod_quiz.util classes provide quiz-related utility functions.
*
* @module moodle-mod_quiz-util
* @main
*/
Y.namespace('Moodle.mod_quiz.util');
/**
* A collection of general utility functions for use in quiz.
*
* @class Moodle.mod_quiz.util
* @static
*/
}, '@VERSION@');
@@ -0,0 +1,96 @@
YUI.add('moodle-mod_quiz-util-page', function (Y, NAME) {
/**
* A collection of utility classes for use with pages.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-page
*/
Y.namespace('Moodle.mod_quiz.util.page');
/**
* A collection of utility classes for use with pages.
*
* @class Moodle.mod_quiz.util.page
* @static
*/
Y.Moodle.mod_quiz.util.page = {
CONSTANTS: {
PAGEIDPREFIX : 'page-',
PAGENUMBERPREFIX : 'Page '
},
SELECTORS: {
PAGE: 'li.page',
INSTANCENAME: '.instancename'
},
/**
* Retrieve the page item from one of it's child Nodes.
*
* @method getPageFromComponent
* @param pagecomponent {Node} The component Node.
* @return {Node|null} The Page Node.
*/
getPageFromComponent: function(pagecomponent) {
return Y.one(pagecomponent).ancestor(this.SELECTORS.PAGE, true);
},
/**
* Determines the page ID for the provided page.
*
* @method getId
* @param page {Node} The page to find an ID for.
* @return {Number|false} The ID of the page in question or false if no ID was found.
*/
getId: function(page) {
// We perform a simple substitution operation to get the ID.
var id = page.get('id').replace(
this.CONSTANTS.PAGEIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the page name for the provided page.
*
* @method getName
* @param page {Node} The page to find a name for.
* @return {string|false} The name of the page in question or false if no ID was found.
*/
getName: function(page) {
var instance = page.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the page number for the provided page.
*
* @method getNumber
* @param page {Node} The page to find a number for.
* @return {Number|false} The number of the page in question or false if no number was found.
*/
getNumber: function(page) {
// We perform a simple substitution operation to get the ID.
var number = page.get('text').replace(
this.CONSTANTS.PAGENUMBERPREFIX, '');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
}
};
}, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-util-page",function(e,t){e.namespace("Moodle.mod_quiz.util.page"),e.Moodle.mod_quiz.util.page={CONSTANTS:{PAGEIDPREFIX:"page-",PAGENUMBERPREFIX:"Page "},SELECTORS:{PAGE:"li.page",INSTANCENAME:".instancename"},getPageFromComponent:function(t){return e.one(t).ancestor(this.SELECTORS.PAGE,!0)},getId:function(e){var t=e.get("id").replace(this.CONSTANTS.PAGEIDPREFIX,"");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){var t=e.get("text").replace(this.CONSTANTS.PAGENUMBERPREFIX,"");return t=parseInt(t,10),typeof t=="number"&&isFinite(t)?t:!1}}},"@VERSION@",{requires:["node","moodle-mod_quiz-util-base"]});
@@ -0,0 +1,96 @@
YUI.add('moodle-mod_quiz-util-page', function (Y, NAME) {
/**
* A collection of utility classes for use with pages.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-page
*/
Y.namespace('Moodle.mod_quiz.util.page');
/**
* A collection of utility classes for use with pages.
*
* @class Moodle.mod_quiz.util.page
* @static
*/
Y.Moodle.mod_quiz.util.page = {
CONSTANTS: {
PAGEIDPREFIX : 'page-',
PAGENUMBERPREFIX : 'Page '
},
SELECTORS: {
PAGE: 'li.page',
INSTANCENAME: '.instancename'
},
/**
* Retrieve the page item from one of it's child Nodes.
*
* @method getPageFromComponent
* @param pagecomponent {Node} The component Node.
* @return {Node|null} The Page Node.
*/
getPageFromComponent: function(pagecomponent) {
return Y.one(pagecomponent).ancestor(this.SELECTORS.PAGE, true);
},
/**
* Determines the page ID for the provided page.
*
* @method getId
* @param page {Node} The page to find an ID for.
* @return {Number|false} The ID of the page in question or false if no ID was found.
*/
getId: function(page) {
// We perform a simple substitution operation to get the ID.
var id = page.get('id').replace(
this.CONSTANTS.PAGEIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the page name for the provided page.
*
* @method getName
* @param page {Node} The page to find a name for.
* @return {string|false} The name of the page in question or false if no ID was found.
*/
getName: function(page) {
var instance = page.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the page number for the provided page.
*
* @method getNumber
* @param page {Node} The page to find a number for.
* @return {Number|false} The number of the page in question or false if no number was found.
*/
getNumber: function(page) {
// We perform a simple substitution operation to get the ID.
var number = page.get('text').replace(
this.CONSTANTS.PAGENUMBERPREFIX, '');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
}
};
}, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]});
@@ -0,0 +1,150 @@
YUI.add('moodle-mod_quiz-util-slot', function (Y, NAME) {
/**
* A collection of utility classes for use with slots.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-slot
*/
Y.namespace('Moodle.mod_quiz.util.slot');
/**
* A collection of utility classes for use with slots.
*
* @class Moodle.mod_quiz.util.slot
* @static
*/
Y.Moodle.mod_quiz.util.slot = {
CONSTANTS: {
SLOTIDPREFIX : 'slot-'
},
SELECTORS: {
SLOT: 'li.slot',
INSTANCENAME: '.instancename',
NUMBER: 'span.slotnumber',
PAGECONTENT : 'div#page-content',
SECTIONUL : 'ul.section'
},
/**
* Retrieve the slot item from one of it's child Nodes.
*
* @method getSlotFromComponent
* @param slotcomponent {Node} The component Node.
* @return {Node|null} The Slot Node.
*/
getSlotFromComponent: function(slotcomponent) {
return Y.one(slotcomponent).ancestor(this.SELECTORS.SLOT, true);
},
/**
* Determines the slot ID for the provided slot.
*
* @method getId
* @param slot {Node} The slot to find an ID for.
* @return {Number|false} The ID of the slot in question or false if no ID was found.
*/
getId: function(slot) {
// We perform a simple substitution operation to get the ID.
var id = slot.get('id').replace(
this.CONSTANTS.SLOTIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the slot name for the provided slot.
*
* @method getName
* @param slot {Node} The slot to find a name for.
* @return {string|false} The name of the slot in question or false if no ID was found.
*/
getName: function(slot) {
var instance = slot.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the slot number for the provided slot.
*
* @method getNumber
* @param slot {Node} The slot to find the number for.
* @return {Number|false} The number of the slot in question or false if no number was found.
*/
getNumber: function(slot) {
var number = slot.one(this.SELECTORS.NUMBER).get('text');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
},
/**
* Updates the slot number for the provided slot.
*
* @method setNumber
* @param slot {Node} The slot to update the number for.
* @return void
*/
setNumber: function(slot, number) {
slot.one(this.SELECTORS.NUMBER).set('text', number);
},
/**
* Returns a list of all slot elements on the page.
*
* @method getSlots
* @return {node[]} An array containing slot nodes.
*/
getSlots: function() {
return Y.all(this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.SLOT);
},
/**
* Returns the previous slot to the give slot.
*
* @method getPrevious
* @param slot Slot node
* @return {node|false} The previous slot node or false.
*/
getPrevious: function(slot) {
return slot.previous(this.SELECTORS.SLOT);
},
/**
* Reset the order of the numbers given to each slot.
*
* @method reorder_slots
* @return void
*/
reorder_slots: function() {
// Get list of slot nodes.
var slots = this.getSlots();
// Loop through slots incrementing the number each time.
slots.each(function(slot) {
var previousSlot = this.getPrevious(slot),
previousslotnumber = 0;
if(previousSlot){
previousslotnumber = this.getNumber(previousSlot);
}
// Set slot number.
this.setNumber(slot, previousslotnumber + 1);
}, this);
}
};
}, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]});
@@ -0,0 +1 @@
YUI.add("moodle-mod_quiz-util-slot",function(e,t){e.namespace("Moodle.mod_quiz.util.slot"),e.Moodle.mod_quiz.util.slot={CONSTANTS:{SLOTIDPREFIX:"slot-"},SELECTORS:{SLOT:"li.slot",INSTANCENAME:".instancename",NUMBER:"span.slotnumber",PAGECONTENT:"div#page-content",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){var t=e.one(this.SELECTORS.NUMBER).get("text");return t=parseInt(t,10),typeof t=="number"&&isFinite(t)?t:!1},setNumber:function(e,t){e.one(this.SELECTORS.NUMBER).set("text",t)},getSlots:function(){return e.all(this.SELECTORS.PAGECONTENT+" "+this.SELECTORS.SECTIONUL+" "+this.SELECTORS.SLOT)},getPrevious:function(e){return e.previous(this.SELECTORS.SLOT)},reorder_slots:function(){var e=this.getSlots();e.each(function(e){var t=this.getPrevious(e),n=0;t&&(n=this.getNumber(t)),this.setNumber(e,n+1)},this)}}},"@VERSION@",{requires:["node","moodle-mod_quiz-util-base"]});
@@ -0,0 +1,150 @@
YUI.add('moodle-mod_quiz-util-slot', function (Y, NAME) {
/**
* A collection of utility classes for use with slots.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-slot
*/
Y.namespace('Moodle.mod_quiz.util.slot');
/**
* A collection of utility classes for use with slots.
*
* @class Moodle.mod_quiz.util.slot
* @static
*/
Y.Moodle.mod_quiz.util.slot = {
CONSTANTS: {
SLOTIDPREFIX : 'slot-'
},
SELECTORS: {
SLOT: 'li.slot',
INSTANCENAME: '.instancename',
NUMBER: 'span.slotnumber',
PAGECONTENT : 'div#page-content',
SECTIONUL : 'ul.section'
},
/**
* Retrieve the slot item from one of it's child Nodes.
*
* @method getSlotFromComponent
* @param slotcomponent {Node} The component Node.
* @return {Node|null} The Slot Node.
*/
getSlotFromComponent: function(slotcomponent) {
return Y.one(slotcomponent).ancestor(this.SELECTORS.SLOT, true);
},
/**
* Determines the slot ID for the provided slot.
*
* @method getId
* @param slot {Node} The slot to find an ID for.
* @return {Number|false} The ID of the slot in question or false if no ID was found.
*/
getId: function(slot) {
// We perform a simple substitution operation to get the ID.
var id = slot.get('id').replace(
this.CONSTANTS.SLOTIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the slot name for the provided slot.
*
* @method getName
* @param slot {Node} The slot to find a name for.
* @return {string|false} The name of the slot in question or false if no ID was found.
*/
getName: function(slot) {
var instance = slot.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the slot number for the provided slot.
*
* @method getNumber
* @param slot {Node} The slot to find the number for.
* @return {Number|false} The number of the slot in question or false if no number was found.
*/
getNumber: function(slot) {
var number = slot.one(this.SELECTORS.NUMBER).get('text');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
},
/**
* Updates the slot number for the provided slot.
*
* @method setNumber
* @param slot {Node} The slot to update the number for.
* @return void
*/
setNumber: function(slot, number) {
slot.one(this.SELECTORS.NUMBER).set('text', number);
},
/**
* Returns a list of all slot elements on the page.
*
* @method getSlots
* @return {node[]} An array containing slot nodes.
*/
getSlots: function() {
return Y.all(this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.SLOT);
},
/**
* Returns the previous slot to the give slot.
*
* @method getPrevious
* @param slot Slot node
* @return {node|false} The previous slot node or false.
*/
getPrevious: function(slot) {
return slot.previous(this.SELECTORS.SLOT);
},
/**
* Reset the order of the numbers given to each slot.
*
* @method reorder_slots
* @return void
*/
reorder_slots: function() {
// Get list of slot nodes.
var slots = this.getSlots();
// Loop through slots incrementing the number each time.
slots.each(function(slot) {
var previousSlot = this.getPrevious(slot),
previousslotnumber = 0;
if(previousSlot){
previousslotnumber = this.getNumber(previousSlot);
}
// Set slot number.
this.setNumber(slot, previousslotnumber + 1);
}, this);
}
};
}, '@VERSION@', {"requires": ["node", "moodle-mod_quiz-util-base"]});
+12
View File
@@ -0,0 +1,12 @@
{
"name": "moodle-mod_quiz-dragdrop",
"builds": {
"moodle-mod_quiz-dragdrop": {
"jsfiles": [
"dragdrop.js",
"section.js",
"resource.js"
]
}
}
}
+34
View File
@@ -0,0 +1,34 @@
/**
* Drag and Drop for Quiz sections and slots.
*
* @module moodle-mod-quiz-dragdrop
*/
var CSS = {
ACTIONAREA: '.actions',
ACTIVITY: 'activity',
ACTIVITYINSTANCE: 'activityinstance',
CONTENT: 'content',
COURSECONTENT: 'mod-quiz-edit-content',
EDITINGMOVE: 'editing_move',
ICONCLASS: 'iconsmall',
JUMPMENU: 'jumpmenu',
LEFT: 'left',
LIGHTBOX: 'lightbox',
MOVEDOWN: 'movedown',
MOVEUP: 'moveup',
PAGE : 'page',
PAGECONTENT: 'page-content',
RIGHT: 'right',
SECTION: 'section',
SECTIONADDMENUS: 'section_add_menus',
SECTIONHANDLE: 'section-handle',
SLOTS: 'slots',
SUMMARY: 'summary',
SECTIONDRAGGABLE: 'sectiondraggable'
},
// The CSS selectors we use.
SELECTOR = {
PAGE: 'li.page',
SLOT: 'li.slot'
};
+246
View File
@@ -0,0 +1,246 @@
/**
* Resource drag and drop.
*
* @class M.course.dragdrop.resource
* @constructor
* @extends M.core.dragdrop
*/
var DRAGRESOURCE = function() {
DRAGRESOURCE.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGRESOURCE, M.core.dragdrop, {
initializer: function() {
// Set group for parent class
this.groups = ['resource'];
this.samenodeclass = CSS.ACTIVITY;
this.parentnodeclass = CSS.SECTION;
//this.resourcedraghandle = this.get_drag_handle(M.util.get_string('movecoursemodule', 'moodle'), CSS.EDITINGMOVE, CSS.ICONCLASS, true);
this.resourcedraghandle = this.get_drag_handle(M.str.moodle.move, CSS.EDITINGMOVE, CSS.ICONCLASS, true);
this.samenodelabel = {
identifier: 'dragtoafter',
component: 'quiz'
};
this.parentnodelabel = {
identifier: 'dragtostart',
component: 'quiz'
};
// Go through all sections
var sectionlistselector = M.mod_quiz.edit.get_section_selector(Y);
if (sectionlistselector) {
sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + sectionlistselector;
this.setup_for_section(sectionlistselector);
// Initialise drag & drop for all resources/activities
var nodeselector = sectionlistselector.slice(CSS.COURSECONTENT.length + 2) + ' li.' + CSS.ACTIVITY;
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: nodeselector,
target: true,
handles: ['.' + CSS.EDITINGMOVE],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false,
cloneNode: true
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.SLOTS
});
del.dd.plug(Y.Plugin.DDWinScroll);
M.mod_quiz.quizbase.register_module(this);
M.mod_quiz.dragres = this;
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
var resources = sectionnode.one('.' + CSS.CONTENT + ' ul.' + CSS.SECTION);
// See if resources ul exists, if not create one.
if (!resources) {
resources = Y.Node.create('<ul></ul>');
resources.addClass(CSS.SECTION);
sectionnode.one('.' + CSS.CONTENT + ' div.' + CSS.SUMMARY).insert(resources, 'after');
}
resources.setAttribute('data-draggroups', this.groups.join(' '));
// Define empty ul as droptarget, so that item could be moved to empty list
new Y.DD.Drop({
node: resources,
groups: this.groups,
padding: '20 0 20 0'
});
// Initialise each resource/activity in this section
this.setup_for_resource('#' + sectionnode.get('id') + ' li.' + CSS.ACTIVITY);
}, this);
},
/**
* Apply dragdrop features to the specified selector or node that refers to resource(s)
*
* @method setup_for_resource
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_resource: function(baseselector) {
Y.Node.all(baseselector).each(function(resourcesnode) {
// Replace move icons
var move = resourcesnode.one('a.' + CSS.EDITINGMOVE);
if (move) {
move.replace(this.resourcedraghandle.cloneNode(true));
}
}, this);
},
drag_start: function(e) {
// Get our drag object
var drag = e.target;
drag.get('dragNode').setContent(drag.get('node').get('innerHTML'));
drag.get('dragNode').all('img.iconsmall').setStyle('vertical-align', 'baseline');
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
drop_hit: function(e) {
var drag = e.drag;
// Get a reference to our drag node
var dragnode = drag.get('node');
var dropnode = e.drop.get('node');
// Add spinner if it not there
var actionarea = dragnode.one(CSS.ACTIONAREA);
var spinner = M.util.add_spinner(Y, actionarea);
var params = {};
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams;
var varname;
for (varname in pageparams) {
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'resource';
params.field = 'move';
params.id = Number(Y.Moodle.mod_quiz.util.slot.getId(dragnode));
params.sectionId = Y.Moodle.core_course.util.section.getId(dropnode.ancestor(M.mod_quiz.edit.get_section_wrapper(Y), true));
var previousslot = dragnode.previous(SELECTOR.SLOT);
if (previousslot) {
params.previousid = Number(Y.Moodle.mod_quiz.util.slot.getId(previousslot));
}
var previouspage = dragnode.previous(SELECTOR.PAGE);
if (previouspage) {
params.page = Number(Y.Moodle.mod_quiz.util.page.getId(previouspage));
}
// Do AJAX request
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
this.lock_drag_handle(drag, CSS.EDITINGMOVE);
spinner.show();
},
success: function(tid, response) {
var responsetext = Y.JSON.parse(response.responseText);
var params = {element: dragnode, visible: responsetext.visible};
M.mod_quiz.quizbase.invoke_function('set_visibility_resource_ui', params);
Y.Moodle.mod_quiz.util.slot.reorder_slots();
this.unlock_drag_handle(drag, CSS.EDITINGMOVE);
window.setTimeout(function() {
spinner.hide();
}, 250);
window.location.reload(true);
},
failure: function(tid, response) {
this.ajax_failure(response);
this.unlock_drag_handle(drag, CSS.SECTIONHANDLE);
spinner.hide();
window.location.reload(true);
}
},
context:this
});
},
global_drop_over: function(e) {
//Overriding parent method so we can stop the slots being dragged before the first page node.
// Check that drop object belong to correct group.
if (!e.drop || !e.drop.inGroup(this.groups)) {
return;
}
// Get a reference to our drag and drop nodes.
var drag = e.drag.get('node'),
drop = e.drop.get('node');
// Save last drop target for the case of missed target processing.
this.lastdroptarget = e.drop;
// Are we dropping within the same parent node?
if (drop.hasClass(this.samenodeclass)) {
var where;
if (this.goingup) {
where = "before";
} else {
where = "after";
}
drop.insert(drag, where);
} else if ((drop.hasClass(this.parentnodeclass) || drop.test('[data-droptarget="1"]')) && !drop.contains(drag)) {
// We are dropping on parent node and it is empty
if (this.goingup) {
drop.append(drag);
} else {
drop.prepend(drag);
}
}
this.drop_over(e);
}
}, {
NAME: 'mod_quiz-dragdrop-resource',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_resource_dragdrop = function(params) {
new DRAGRESOURCE(params);
};
+263
View File
@@ -0,0 +1,263 @@
/**
* Section drag and drop.
*
* @class M.mod_quiz.dragdrop.section
* @constructor
* @extends M.core.dragdrop
*/
var DRAGSECTION = function() {
DRAGSECTION.superclass.constructor.apply(this, arguments);
};
Y.extend(DRAGSECTION, M.core.dragdrop, {
sectionlistselector: null,
initializer: function() {
// Set group for parent class
this.groups = [ CSS.SECTIONDRAGGABLE ];
this.samenodeclass = M.mod_quiz.edit.get_sectionwrapperclass();
this.parentnodeclass = M.mod_quiz.edit.get_containerclass();
// Check if we are in single section mode
if (Y.Node.one('.' + CSS.JUMPMENU)) {
return false;
}
// Initialise sections dragging
this.sectionlistselector = M.mod_quiz.edit.get_section_wrapper(Y);
if (this.sectionlistselector) {
this.sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + this.sectionlistselector;
this.setup_for_section(this.sectionlistselector);
// Make each li element in the lists of sections draggable
var del = new Y.DD.Delegate({
container: '.' + CSS.COURSECONTENT,
nodes: '.' + CSS.SECTIONDRAGGABLE,
target: true,
handles: ['.' + CSS.LEFT],
dragConfig: {groups: this.groups}
});
del.dd.plug(Y.Plugin.DDProxy, {
// Don't move the node at the end of the drag
moveOnEnd: false
});
del.dd.plug(Y.Plugin.DDConstrained, {
// Keep it inside the .mod-quiz-edit-content
constrain: '#' + CSS.PAGECONTENT,
stickY: true
});
del.dd.plug(Y.Plugin.DDWinScroll);
}
},
/**
* Apply dragdrop features to the specified selector or node that refers to section(s)
*
* @method setup_for_section
* @param {String} baseselector The CSS selector or node to limit scope to
*/
setup_for_section: function(baseselector) {
Y.Node.all(baseselector).each(function(sectionnode) {
// Determine the section ID
var sectionid = Y.Moodle.core_course.util.section.getId(sectionnode);
// We skip the top section as it is not draggable
if (sectionid > 0) {
// Remove move icons
var movedown = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEDOWN);
var moveup = sectionnode.one('.' + CSS.RIGHT + ' a.' + CSS.MOVEUP);
// Add dragger icon
var title = M.util.get_string('movesection', 'moodle', sectionid);
var cssleft = sectionnode.one('.' + CSS.LEFT);
if ((movedown || moveup) && cssleft) {
cssleft.setStyle('cursor', 'move');
cssleft.appendChild(this.get_drag_handle(title, CSS.SECTIONHANDLE, 'icon', true));
if (moveup) {
moveup.remove();
}
if (movedown) {
movedown.remove();
}
// This section can be moved - add the class to indicate this to Y.DD.
sectionnode.addClass(CSS.SECTIONDRAGGABLE);
}
}
}, this);
},
/*
* Drag-dropping related functions
*/
drag_start: function(e) {
// Get our drag object
var drag = e.target;
// Creat a dummy structure of the outer elemnents for clean styles application
var containernode = Y.Node.create('<' + M.mod_quiz.edit.get_containernode() + '></' + M.mod_quiz.edit.get_containernode() + '>');
containernode.addClass(M.mod_quiz.edit.get_containerclass());
var sectionnode = Y.Node.create('<' + M.mod_quiz.edit.get_sectionwrappernode() + '></' + M.mod_quiz.edit.get_sectionwrappernode() + '>');
sectionnode.addClass( M.mod_quiz.edit.get_sectionwrapperclass());
sectionnode.setStyle('margin', 0);
sectionnode.setContent(drag.get('node').get('innerHTML'));
containernode.appendChild(sectionnode);
drag.get('dragNode').setContent(containernode);
drag.get('dragNode').addClass(CSS.COURSECONTENT);
},
drag_dropmiss: function(e) {
// Missed the target, but we assume the user intended to drop it
// on the last last ghost node location, e.drag and e.drop should be
// prepared by global_drag_dropmiss parent so simulate drop_hit(e).
this.drop_hit(e);
},
get_section_index: function(node) {
var sectionlistselector = '.' + CSS.COURSECONTENT + ' ' + M.mod_quiz.edit.get_section_selector(Y),
sectionList = Y.all(sectionlistselector),
nodeIndex = sectionList.indexOf(node),
zeroIndex = sectionList.indexOf(Y.one('#section-0'));
return (nodeIndex - zeroIndex);
},
drop_hit: function(e) {
var drag = e.drag;
// Get references to our nodes and their IDs.
var dragnode = drag.get('node'),
dragnodeid = Y.Moodle.core_course.util.section.getId(dragnode),
loopstart = dragnodeid,
dropnodeindex = this.get_section_index(dragnode),
loopend = dropnodeindex;
if (dragnodeid === dropnodeindex) {
Y.log("Skipping move - same location moving " + dragnodeid + " to " + dropnodeindex, 'debug', 'moodle-mod_quiz-dragdrop');
return;
}
Y.log("Moving from position " + dragnodeid + " to position " + dropnodeindex, 'debug', 'moodle-mod_quiz-dragdrop');
if (loopstart > loopend) {
// If we're going up, we need to swap the loop order
// because loops can't go backwards.
loopstart = dropnodeindex;
loopend = dragnodeid;
}
// Get the list of nodes.
drag.get('dragNode').removeClass(CSS.COURSECONTENT);
var sectionlist = Y.Node.all(this.sectionlistselector);
// Add a lightbox if it's not there.
var lightbox = M.util.add_lightbox(Y, dragnode);
// Handle any variables which we must pass via AJAX.
var params = {},
pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
if (!pageparams.hasOwnProperty(varname)) {
continue;
}
params[varname] = pageparams[varname];
}
// Prepare request parameters
params.sesskey = M.cfg.sesskey;
params.courseid = this.get('courseid');
params.quizid = this.get('quizid');
params['class'] = 'section';
params.field = 'move';
params.id = dragnodeid;
params.value = dropnodeindex;
// Perform the AJAX request.
var uri = M.cfg.wwwroot + this.get('ajaxurl');
Y.io(uri, {
method: 'POST',
data: params,
on: {
start: function() {
lightbox.show();
},
success: function(tid, response) {
// Update section titles, we can't simply swap them as
// they might have custom title
try {
var responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
M.mod_quiz.edit.process_sections(Y, sectionlist, responsetext, loopstart, loopend);
} catch (e) {}
// Update all of the section IDs - first unset them, then set them
// to avoid duplicates in the DOM.
var index;
// Classic bubble sort algorithm is applied to the section
// nodes between original drag node location and the new one.
var swapped = false;
do {
swapped = false;
for (index = loopstart; index <= loopend; index++) {
if (Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) >
Y.Moodle.core_course.util.section.getId(sectionlist.item(index))) {
Y.log("Swapping " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index - 1)) +
" with " + Y.Moodle.core_course.util.section.getId(sectionlist.item(index)),
"debug", "moodle-mod_quiz-dragdrop");
// Swap section id.
var sectionid = sectionlist.item(index - 1).get('id');
sectionlist.item(index - 1).set('id', sectionlist.item(index).get('id'));
sectionlist.item(index).set('id', sectionid);
// See what format needs to swap.
M.mod_quiz.edit.swap_sections(Y, index - 1, index);
// Update flag.
swapped = true;
}
}
loopend = loopend - 1;
} while (swapped);
window.setTimeout(function() {
lightbox.hide();
}, 250);
},
failure: function(tid, response) {
this.ajax_failure(response);
lightbox.hide();
}
},
context:this
});
}
}, {
NAME: 'mod_quiz-dragdrop-section',
ATTRS: {
courseid: {
value: null
},
quizid: {
value: null
},
ajaxurl: {
value: 0
},
config: {
value: 0
}
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_section_dragdrop = function(params) {
new DRAGSECTION(params);
};
@@ -0,0 +1,17 @@
{
"moodle-mod_quiz-dragdrop": {
"requires": [
"base",
"node",
"io",
"dom",
"dd",
"dd-scroll",
"moodle-core-dragdrop",
"moodle-core-notification",
"moodle-mod_quiz-quizbase",
"moodle-mod_quiz-util",
"moodle-course-util"
]
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "moodle-mod_quiz-modform",
"builds": {
"moodle-mod_quiz-modform": {
"jsfiles": [
"modform.js"
]
}
}
}
+51
View File
@@ -0,0 +1,51 @@
/**
* The modform class has all the JavaScript specific to mod/quiz/mod_form.php.
*
* @module moodle-mod_quiz-modform
*/
var MODFORM = function() {
MODFORM.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(MODFORM, Y.Base, {
repaginateCheckbox: null,
qppSelect: null,
qppInitialValue: 0,
initializer: function () {
this.repaginateCheckbox = Y.one('#id_repaginatenow');
if (!this.repaginateCheckbox) {
// The checkbox only appears when editing an existing quiz.
return;
}
this.qppSelect = Y.one('#id_questionsperpage');
this.qppInitialValue = this.qppSelect.get('value');
this.qppSelect.on('change', this.qppChanged, this);
Y.one('#id_shufflequestions').on('change', this.qppChanged, this);
},
qppChanged: function() {
Y.later(50, this, function() {
if (!this.repaginateCheckbox.get('disabled')) {
this.repaginateCheckbox.set('checked', this.qppSelect.get('value') !== this.qppInitialValue);
}
});
}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.modform = M.mod_quiz.modform || new MODFORM();
M.mod_quiz.modform.init = function() {
return new MODFORM();
};
@@ -0,0 +1,9 @@
{
"moodle-mod_quiz-modform": {
"requires": [
"base",
"node",
"event"
]
}
}
@@ -0,0 +1,10 @@
{
"name": "moodle-mod_quiz-questionchooser",
"builds": {
"moodle-mod_quiz-questionchooser": {
"jsfiles": [
"questionchooser.js"
]
}
}
}
+72
View File
@@ -0,0 +1,72 @@
var CSS = {
ADDNEWQUESTIONBUTTONS: 'ul.menu a.addquestion',
CREATENEWQUESTION: 'div.createnewquestion',
CHOOSERDIALOGUE: 'div.chooserdialogue',
CHOOSERHEADER: 'div.choosertitle'
};
/**
* The questionchooser class is responsible for instantiating and displaying the question chooser
* when viewing a quiz in editing mode.
*
* @class questionchooser
* @constructor
* @protected
* @extends M.core.chooserdialogue
*/
var QUESTIONCHOOSER = function() {
QUESTIONCHOOSER.superclass.constructor.apply(this, arguments);
};
Y.extend(QUESTIONCHOOSER, M.core.chooserdialogue, {
initializer: function() {
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDNEWQUESTIONBUTTONS, this);
},
display_dialogue: function(e) {
e.preventDefault();
var dialogue = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERDIALOGUE),
header = Y.one(CSS.CREATENEWQUESTION + ' ' + CSS.CHOOSERHEADER);
if (this.container === null) {
// Setup the dialogue, and then prepare the chooser if it's not already been set up.
this.setup_chooser_dialogue(dialogue, header, {});
this.prepare_chooser();
}
// Update all of the hidden fields within the questionbank form.
var parameters = Y.QueryString.parse(e.currentTarget.get('search').substring(1));
var form = this.container.one('form');
this.parameters_to_hidden_input(parameters, form, 'returnurl');
this.parameters_to_hidden_input(parameters, form, 'cmid');
this.parameters_to_hidden_input(parameters, form, 'category');
this.parameters_to_hidden_input(parameters, form, 'addonpage');
this.parameters_to_hidden_input(parameters, form, 'appendqnumstring');
// Display the chooser dialogue.
this.display_chooser(e);
},
parameters_to_hidden_input: function(parameters, form, name) {
var value;
if (parameters.hasOwnProperty(name)) {
value = parameters[name];
} else {
value = '';
}
var input = form.one('input[name=' + name + ']');
if (!input) {
input = form.appendChild('<input type="hidden">');
input.set('name', name);
}
input.set('value', value);
}
}, {
NAME: 'mod_quiz-questionchooser'
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.init_questionchooser = function() {
M.mod_quiz.question_chooser = new QUESTIONCHOOSER({});
return M.mod_quiz.question_chooser;
};
@@ -0,0 +1,9 @@
{
"moodle-mod_quiz-questionchooser": {
"requires": [
"moodle-core-chooserdialogue",
"moodle-mod_quiz-util",
"querystring-parse"
]
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "moodle-mod_quiz-quizbase",
"builds": {
"moodle-mod_quiz-quizbase": {
"jsfiles": [
"quizbase.js"
]
}
}
}
+267
View File
@@ -0,0 +1,267 @@
/**
* The quizbase class to provide shared functionality to Modules within Moodle.
*
* @module moodle-mod_quiz-quizbase
*/
var QUIZBASENAME = 'mod_quiz-quizbase';
var QUIZBASE = function() {
QUIZBASE.superclass.constructor.apply(this, arguments);
};
/**
* The coursebase class to provide shared functionality to Modules within
* Moodle.
*
* @class M.course.coursebase
* @constructor
*/
Y.extend(QUIZBASE, Y.Base, {
// Registered Modules
registermodules : [],
/**
* Register a new Javascript Module
*
* @method register_module
* @param {Object} The instantiated module to call functions on
* @chainable
*/
register_module : function(object) {
this.registermodules.push(object);
return this;
},
/**
* Invoke the specified function in all registered modules with the given arguments
*
* @method invoke_function
* @param {String} functionname The name of the function to call
* @param {mixed} args The argument supplied to the function
* @chainable
*/
invoke_function : function(functionname, args) {
var module;
for (module in this.registermodules) {
if (functionname in this.registermodules[module]) {
this.registermodules[module][functionname](args);
}
}
return this;
}
}, {
NAME : QUIZBASENAME,
ATTRS : {}
});
// Ensure that M.course exists and that coursebase is initialised correctly
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.quizbase = M.mod_quiz.quizbase || new QUIZBASE();
// Abstract functions that needs to be defined per format (course/format/somename/format.js)
M.mod_quiz.edit = M.mod_quiz.edit || {};
/**
* Swap section (should be defined in format.js if requred)
*
* @param {YUI} Y YUI3 instance
* @param {string} node1 node to swap to
* @param {string} node2 node to swap with
* @return {NodeList} section list
*/
M.mod_quiz.edit.swap_sections = function(Y, node1, node2) {
var CSS = {
COURSECONTENT : 'mod-quiz-edit-content',
SECTIONADDMENUS : 'section_add_menus'
};
var sectionlist = Y.Node.all('.'+CSS.COURSECONTENT+' '+M.mod_quiz.edit.get_section_selector(Y));
// Swap menus.
sectionlist.item(node1).one('.'+CSS.SECTIONADDMENUS).swap(sectionlist.item(node2).one('.'+CSS.SECTIONADDMENUS));
};
/**
* Process sections after ajax response (should be defined in format.js)
* If some response is expected, we pass it over to format, as it knows better
* hot to process it.
*
* @param {YUI} Y YUI3 instance
* @param {NodeList} list of sections
* @param {array} response ajax response
* @param {string} sectionfrom first affected section
* @param {string} sectionto last affected section
* @return void
*/
M.mod_quiz.edit.process_sections = function(Y, sectionlist, response, sectionfrom, sectionto) {
var CSS = {
SECTIONNAME : 'sectionname'
},
SELECTORS = {
SECTIONLEFTSIDE : '.left .section-handle img'
};
if (response.action === 'move') {
// If moving up swap around 'sectionfrom' and 'sectionto' so the that loop operates.
if (sectionfrom > sectionto) {
var temp = sectionto;
sectionto = sectionfrom;
sectionfrom = temp;
}
// Update titles and move icons in all affected sections.
var ele, str, stridx, newstr;
for (var i = sectionfrom; i <= sectionto; i++) {
// Update section title.
sectionlist.item(i).one('.'+CSS.SECTIONNAME).setContent(response.sectiontitles[i]);
// Update move icon.
ele = sectionlist.item(i).one(SELECTORS.SECTIONLEFTSIDE);
str = ele.getAttribute('alt');
stridx = str.lastIndexOf(' ');
newstr = str.substr(0, stridx + 1) + i;
ele.setAttribute('alt', newstr);
ele.setAttribute('title', newstr); // For FireFox as 'alt' is not refreshed.
// Remove the current class as section has been moved.
sectionlist.item(i).removeClass('current');
}
// If there is a current section, apply corresponding class in order to highlight it.
if (response.current !== -1) {
// Add current class to the required section.
sectionlist.item(response.current).addClass('current');
}
}
};
/**
* Get sections config for this format, for examples see function definition
* in the formats.
*
* @return {object} section list configuration
*/
M.mod_quiz.edit.get_config = function() {
return {
container_node : 'ul',
container_class : 'slots',
section_node : 'li',
section_class : 'section'
};
};
/**
* Get section list for this format (usually items inside container_node.container_class selector)
*
* @param {YUI} Y YUI3 instance
* @return {string} section selector
*/
M.mod_quiz.edit.get_section_selector = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_node && config.section_class) {
return config.section_node + '.' + config.section_class;
}
Y.log('section_node and section_class are not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
return null;
};
/**
* Get section wraper for this format (only used in case when each
* container_node.container_class node is wrapped in some other element).
*
* @param {YUI} Y YUI3 instance
* @return {string} section wrapper selector or M.mod_quiz.format.get_section_selector
* if section_wrapper_node and section_wrapper_class are not defined in the format config.
*/
M.mod_quiz.edit.get_section_wrapper = function(Y) {
var config = M.mod_quiz.edit.get_config();
if (config.section_wrapper_node && config.section_wrapper_class) {
return config.section_wrapper_node + '.' + config.section_wrapper_class;
}
return M.mod_quiz.edit.get_section_selector(Y);
};
/**
* Get the tag of container node
*
* @return {string} tag of container node.
*/
M.mod_quiz.edit.get_containernode = function() {
var config = M.mod_quiz.edit.get_config();
if (config.container_node) {
return config.container_node;
} else {
Y.log('container_node is not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
}
};
/**
* Get the class of container node
*
* @return {string} class of the container node.
*/
M.mod_quiz.edit.get_containerclass = function() {
var config = M.mod_quiz.edit.get_config();
if (config.container_class) {
return config.container_class;
} else {
Y.log('container_class is not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
}
};
/**
* Get the tag of draggable node (section wrapper if exists, otherwise section)
*
* @return {string} tag of the draggable node.
*/
M.mod_quiz.edit.get_sectionwrappernode = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_wrapper_node) {
return config.section_wrapper_node;
} else {
return config.section_node;
}
};
/**
* Get the class of draggable node (section wrapper if exists, otherwise section)
*
* @return {string} class of the draggable node.
*/
M.mod_quiz.edit.get_sectionwrapperclass = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_wrapper_class) {
return config.section_wrapper_class;
} else {
return config.section_class;
}
};
/**
* Get the tag of section node
*
* @return {string} tag of section node.
*/
M.mod_quiz.edit.get_sectionnode = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_node) {
return config.section_node;
} else {
Y.log('section_node is not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
}
};
/**
* Get the class of section node
*
* @return {string} class of the section node.
*/
M.mod_quiz.edit.get_sectionclass = function() {
var config = M.mod_quiz.edit.get_config();
if (config.section_class) {
return config.section_class;
} else {
Y.log('section_class is not defined in M.mod_quiz.edit.get_config', 'warn', 'moodle-mod_quiz-quizbase');
}
};
@@ -0,0 +1,8 @@
{
"moodle-mod_quiz-quizbase": {
"requires": [
"base",
"node"
]
}
}
@@ -0,0 +1,10 @@
{
"name": "moodle-mod_quiz-quizquestionbank",
"builds": {
"moodle-mod_quiz-quizquestionbank": {
"jsfiles": [
"quizquestionbank.js"
]
}
}
}
+160
View File
@@ -0,0 +1,160 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Add questions from question bank functionality for a popup in quiz editing page.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var CSS = {
QBANKLOADING: 'div.questionbankloading',
ADDQUESTIONLINKS: 'ul.menu a.questionbank',
ADDTOQUIZCONTAINER: 'td.addtoquizaction'
};
var PARAMS = {
PAGE: 'addonpage',
HEADER: 'header'
};
var POPUP = function() {
POPUP.superclass.constructor.apply(this, arguments);
};
Y.extend(POPUP, Y.Base, {
loadingDiv: '',
dialogue: null,
addonpage: 0,
create_dialogue: function() {
// Create a dialogue on the page and hide it.
config = {
headerContent : '',
bodyContent : Y.one(CSS.QBANKLOADING),
draggable : true,
modal : true,
centered: true,
width: null,
visible: false,
postmethod: 'form',
footerContent: null,
extraClasses: ['mod_quiz_qbank_dialogue']
};
this.dialogue = new M.core.dialogue(config);
this.dialogue.bodyNode.delegate('click', this.link_clicked, 'a[href]', this);
this.dialogue.hide();
this.loadingDiv = this.dialogue.bodyNode.getHTML();
Y.later(100, this, function() {this.load_content(window.location.search);});
},
initializer : function() {
if (!Y.one(CSS.QBANKLOADING)) {
return;
}
this.create_dialogue();
Y.one('body').delegate('click', this.display_dialogue, CSS.ADDQUESTIONLINKS, this);
},
display_dialogue : function (e) {
e.preventDefault();
this.dialogue.set('headerContent', e.currentTarget.getData(PARAMS.HEADER));
this.addonpage = e.currentTarget.getData(PARAMS.PAGE);
var controlsDiv = this.dialogue.bodyNode.one('.modulespecificbuttonscontainer');
if (controlsDiv) {
var hidden = controlsDiv.one('input[name=addonpage]');
if (!hidden) {
hidden = controlsDiv.appendChild('<input type="hidden" name="addonpage">');
}
hidden.set('value', this.addonpage);
}
this.dialogue.show();
},
load_content : function(queryString) {
Y.log('Starting load.', 'debug', 'moodle-mod_quiz-quizquestionbank');
this.dialogue.bodyNode.append(this.loadingDiv);
// If to support old IE.
if (window.history.replaceState) {
window.history.replaceState(null, '', M.cfg.wwwroot + '/mod/quiz/edit.php' + queryString);
}
Y.io(M.cfg.wwwroot + '/mod/quiz/questionbank.ajax.php' + queryString, {
method: 'GET',
on: {
success: this.load_done,
failure: this.load_failed
},
context: this
});
Y.log('Load request sent.', 'debug', 'moodle-mod_quiz-quizquestionbank');
},
load_done: function(transactionid, response) {
var result = JSON.parse(response.responseText);
if (!result.status || result.status !== 'OK') {
// Because IIS is useless, Moodle can't send proper HTTP response
// codes, so we have to detect failures manually.
this.load_failed(transactionid, response);
return;
}
Y.log('Load completed.', 'debug', 'moodle-mod_quiz-quizquestionbank');
this.dialogue.bodyNode.setHTML(result.contents);
Y.use('moodle-question-chooser', function() {M.question.init_chooser({});});
this.dialogue.bodyNode.one('form').delegate('change', this.options_changed, '.searchoptions', this);
if (this.dialogue.visible) {
Y.later(0, this.dialogue, this.dialogue.centerDialogue);
}
M.question.qbankmanager.init();
},
load_failed: function() {
Y.log('Load failed.', 'debug', 'moodle-mod_quiz-quizquestionbank');
},
link_clicked: function(e) {
if (e.currentTarget.ancestor(CSS.ADDTOQUIZCONTAINER)) {
// These links need to work like normal, after we modify the URL.
e.currentTarget.set('href', e.currentTarget.get('href') + '&addonpage=' + this.addonpage);
return;
}
e.preventDefault();
this.load_content(e.currentTarget.get('search'));
},
options_changed: function(e) {
e.preventDefault();
this.load_content('?' + Y.IO.stringify(e.currentTarget.get('form')));
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.quizquestionbank = M.mod_quiz.quizquestionbank || {};
M.mod_quiz.quizquestionbank.init = function() {
return new POPUP();
};
@@ -0,0 +1,14 @@
{
"moodle-mod_quiz-quizquestionbank": {
"requires": [
"base",
"event",
"node",
"io",
"io-form",
"yui-later",
"moodle-question-qbankmanager"
"moodle-core-notification-dialogue"
]
}
}
@@ -0,0 +1,10 @@
{
"name": "moodle-mod_quiz-randomquestion",
"builds": {
"moodle-mod_quiz-randomquestion": {
"jsfiles": [
"randomquestion.js"
]
}
}
}
+80
View File
@@ -0,0 +1,80 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Add a random question functionality for a popup in quiz editing page.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var CSS = {
RANDOMQUESTIONFORM: 'div.randomquestionformforpopup',
PAGEHIDDENINPUT: 'input#rform_qpage',
RANDOMQUESTIONLINKS: 'ul.menu a.addarandomquestion'
};
var PARAMS = {
PAGE: 'addonpage',
HEADER: 'header',
FORM: 'form'
};
var POPUP = function() {
POPUP.superclass.constructor.apply(this, arguments);
};
Y.extend(POPUP, Y.Base, {
dialogue: function(header) {
// Create a dialogue on the page and hide it.
config = {
headerContent : header,
bodyContent : Y.one(CSS.RANDOMQUESTIONFORM),
draggable : true,
modal : true,
zIndex : 1000,
centered: false,
width: 'auto',
visible: false,
postmethod: 'form',
footerContent: null
};
var popup = { dialog: null };
popup.dialog = new M.core.dialogue(config);
popup.dialog.show();
},
initializer : function() {
Y.one('body').delegate('click', this.display_dialogue, CSS.RANDOMQUESTIONLINKS, this);
},
display_dialogue : function (e) {
e.preventDefault();
Y.one(CSS.RANDOMQUESTIONFORM + ' ' + CSS.PAGEHIDDENINPUT).set('value',
e.currentTarget.getData(PARAMS.PAGE));
this.dialogue(e.currentTarget.getData(PARAMS.HEADER));
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.randomquestion = M.mod_quiz.randomquestion || {};
M.mod_quiz.randomquestion.init = function() {
return new POPUP();
};
@@ -0,0 +1,11 @@
{
"moodle-mod_quiz-randomquestion": {
"requires": [
"base",
"event",
"node",
"io",
"moodle-core-notification-dialogue"
]
}
}
+10
View File
@@ -0,0 +1,10 @@
{
"name": "moodle-mod_quiz-repaginate",
"builds": {
"moodle-mod_quiz-repaginate": {
"jsfiles": [
"repaginate.js"
]
}
}
}
+81
View File
@@ -0,0 +1,81 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Repaginate functionality for a popup in quiz editing page.
*
* @package mod_quiz
* @copyright 2014 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
var CSS = {
REPAGINATECONTAINERCLASS: '.rpcontainerclass',
REPAGINATECOMMAND: '#repaginatecommand'
};
var PARAMS = {
CMID: 'cmid',
HEADER: 'header',
FORM: 'form'
};
var POPUP = function() {
POPUP.superclass.constructor.apply(this, arguments);
};
Y.extend(POPUP, Y.Base, {
header: null,
body: null,
initializer : function() {
rpcontainerclass = Y.one(CSS.REPAGINATECONTAINERCLASS);
// Set popup header and body.
this.header = rpcontainerclass.getAttribute(PARAMS.HEADER);
this.body = rpcontainerclass.getAttribute(PARAMS.FORM);
Y.one(CSS.REPAGINATECOMMAND).on('click', this.display_dialog, this);
},
display_dialog : function (e) {
e.preventDefault();
// Configure the popup.
var config = {
headerContent : this.header,
bodyContent : this.body,
draggable : true,
modal : true,
zIndex : 1000,
context: [CSS.REPAGINATECOMMAND, 'tr', 'br', ['beforeShow']],
centered: false,
width: '30em',
visible: false,
postmethod: 'form',
footerContent: null
};
var popup = { dialog: null };
popup.dialog = new M.core.dialogue(config);
popup.dialog.show();
}
});
M.mod_quiz = M.mod_quiz || {};
M.mod_quiz.repaginate = M.mod_quiz.repaginate || {};
M.mod_quiz.repaginate.init = function() {
return new POPUP();
};
@@ -0,0 +1,11 @@
{
"moodle-mod_quiz-repaginate": {
"requires": [
"base",
"event",
"node",
"io",
"moodle-core-notification-dialogue"
]
}
}
+12
View File
@@ -0,0 +1,12 @@
{
"name": "moodle-mod_quiz-toolboxes",
"builds": {
"moodle-mod_quiz-toolboxes": {
"jsfiles": [
"toolbox.js",
"resource.js",
"section.js"
]
}
}
}
+461
View File
@@ -0,0 +1,461 @@
/**
* Resource and activity toolbox class.
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a quiz in editing mode.
*
* @module mod_quiz-resource-toolbox
* @namespace M.mod_quiz.resource_toolbox
*/
/**
* Resource and activity toolbox class.
*
* This is a class extending TOOLBOX containing code specific to resources
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a quiz in editing mode.
*
* @class resources
* @constructor
* @extends M.course.toolboxes.toolbox
*/
var RESOURCETOOLBOX = function() {
RESOURCETOOLBOX.superclass.constructor.apply(this, arguments);
};
Y.extend(RESOURCETOOLBOX, TOOLBOX, {
/**
* An Array of events added when editing a max mark field.
* These should all be detached when editing is complete.
*
* @property editmaxmarkevents
* @protected
* @type Array
* @protected
*/
editmaxmarkevents: [],
/**
*
*/
NODE_PAGE: 1,
NODE_SLOT: 2,
NODE_JOIN: 3,
/**
* Initialize the resource toolbox
*
* For each activity the commands are updated and a reference to the activity is attached.
* This way it doesn't matter where the commands are going to called from they have a reference to the
* activity that they relate to.
* This is essential as some of the actions are displayed in an actionmenu which removes them from the
* page flow.
*
* This function also creates a single event delegate to manage all AJAX actions for all activities on
* the page.
*
* @method initializer
* @protected
*/
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);
},
/**
* Handles the delegation event. When this is fired someone has triggered an action.
*
* Note not all actions will result in an AJAX enhancement.
*
* @protected
* @method handle_data_action
* @param {EventFacade} ev The event that was triggered.
* @returns {boolean}
*/
handle_data_action: function(ev) {
// We need to get the anchor element that triggered this event.
var node = ev.target;
if (!node.test('a')) {
node = node.ancestor(SELECTOR.ACTIVITYACTION);
}
// From the anchor we can get both the activity (added during initialisation) and the action being
// performed (added by the UI as a data attribute).
var action = node.getData('action'),
activity = node.ancestor(SELECTOR.ACTIVITYLI);
if (!node.test('a') || !action || !activity) {
// It wasn't a valid action node.
return;
}
// Switch based upon the action and do the desired thing.
switch (action) {
case 'editmaxmark':
// The user wishes to edit the maxmark of the resource.
this.edit_maxmark(ev, node, activity, action);
break;
case 'delete':
// The user is deleting the activity.
this.delete_with_confirmation(ev, node, activity, action);
break;
case 'linkpage':
case 'unlinkpage':
// The user is linking or unlinking pages.
this.link_page(ev, node, activity, action);
break;
default:
// Nothing to do here!
break;
}
},
/**
* Add a loading icon to the specified activity.
*
* The icon is added within the action area.
*
* @method add_spinner
* @param {Node} activity The activity to add a loading icon to
* @return {Node|null} The newly created icon, or null if the action area was not found.
*/
add_spinner: function(activity) {
var actionarea = activity.one(SELECTOR.ACTIONAREA);
if (actionarea) {
return M.util.add_spinner(Y, actionarea);
}
return null;
},
/**
* Deletes the given activity or resource after confirmation.
*
* @protected
* @method delete_with_confirmation
* @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.
* @chainable
*/
delete_with_confirmation: function(ev, button, activity) {
// Prevent the default button action
ev.preventDefault();
// Get the element we're working on
var element = activity,
// Create confirm string (different if element has or does not have name)
confirmstring = '',
qtypename = M.util.get_string('pluginname',
'qtype_' + element.getAttribute('class').match(/qtype_([^\s]*)/)[1]);
confirmstring = M.util.get_string('confirmremovequestion', 'quiz', qtypename);
// Create the confirmation dialogue.
var confirm = new M.core.confirm({
question: confirmstring,
modal: true
});
// If it is confirmed.
confirm.on('complete-yes', function() {
// Actually remove the element.
element.remove();
Y.Moodle.mod_quiz.util.slot.reorder_slots();
var data = {
'class': 'resource',
'action': 'DELETE',
'id': Y.Moodle.mod_quiz.util.slot.getId(element)
};
this.send_request(data);
if (M.core.actionmenu && M.core.actionmenu.instance) {
M.core.actionmenu.instance.hideMenu();
}
window.location.reload(true);
}, this);
return this;
},
/**
* Edit the maxmark for the resource
*
* @protected
* @method edit_maxmark
* @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 that has been requested.
* @return Boolean
*/
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),
instance = activity.one(SELECTOR.ACTIVITYINSTANCE),
currentmaxmark = instancemaxmark.get('firstChild'),
oldmaxmark = currentmaxmark.get('data'),
maxmarktext = oldmaxmark,
thisevent,
anchor = instancemaxmark,// Grab the anchor so that we can swap it with the edit form.
data = {
'class' : 'resource',
'field' : 'getmaxmark',
'id' : activityid
};
// Prevent the default actions.
ev.preventDefault();
this.send_request(data, null, function(response) {
if (M.core.actionmenu && M.core.actionmenu.instance) {
M.core.actionmenu.instance.hideMenu();
}
// Try to retrieve the existing string from the server
if (response.instancemaxmark) {
maxmarktext = response.instancemaxmark;
}
// Create the editor and submit button
var editform = Y.Node.create('<form action="#" />');
var editinstructions = Y.Node.create('<span class="' + CSS.EDITINSTRUCTIONS + '" id="id_editinstructions" />')
.set('innerHTML', M.util.get_string('edittitleinstructions', 'moodle'));
var editor = Y.Node.create('<input name="maxmark" type="text" class="' + CSS.TITLEEDITOR + '" />').setAttrs({
'value' : maxmarktext,
'autocomplete' : 'off',
'aria-describedby' : 'id_editinstructions',
'maxLength' : '12',
'size' : parseInt(this.get('config').questiondecimalpoints, 10) + 2
});
// Clear the existing content and put the editor in
editform.appendChild(editor);
editform.setData('anchor', anchor);
instance.insert(editinstructions, 'before');
anchor.replace(editform);
// Force the editing instruction to match the mod-indent position.
var padside = 'left';
if (right_to_left()) {
padside = 'right';
}
// We hide various components whilst editing:
activity.addClass(CSS.EDITINGMAXMARK);
// Focus and select the editor text
editor.focus().select();
// Cancel the edit if we lose focus or the escape key is pressed.
thisevent = editor.on('blur', this.edit_maxmark_cancel, this, activity, false);
this.editmaxmarkevents.push(thisevent);
thisevent = editor.on('key', this.edit_maxmark_cancel, 'esc', this, activity, true);
this.editmaxmarkevents.push(thisevent);
// Handle form submission.
thisevent = editform.on('submit', this.edit_maxmark_submit, this, activity, oldmaxmark);
this.editmaxmarkevents.push(thisevent);
});
},
/**
* Handles the submit event when editing the activity or resources maxmark.
*
* @protected
* @method edit_maxmark_submit
* @param {EventFacade} ev The event that triggered this.
* @param {Node} activity The activity whose maxmark we are altering.
* @param {String} originalmaxmark The original maxmark the activity or resource had.
*/
edit_maxmark_submit : function(ev, activity, originalmaxmark) {
// We don't actually want to submit anything
ev.preventDefault();
var newmaxmark = Y.Lang.trim(activity.one(SELECTOR.ACTIVITYFORM + ' ' + SELECTOR.ACTIVITYMAXMARK).get('value'));
var spinner = this.add_spinner(activity);
this.edit_maxmark_clear(activity);
activity.one(SELECTOR.INSTANCEMAXMARK).setContent(newmaxmark);
if (newmaxmark !== null && newmaxmark !== "" && newmaxmark !== originalmaxmark) {
var data = {
'class' : 'resource',
'field' : 'updatemaxmark',
'maxmark' : newmaxmark,
'id' : Y.Moodle.mod_quiz.util.slot.getId(activity)
};
this.send_request(data, spinner, function(response) {
if (response.instancemaxmark) {
activity.one(SELECTOR.INSTANCEMAXMARK).setContent(response.instancemaxmark);
}
});
}
},
/**
* Handles the cancel event when editing the activity or resources maxmark.
*
* @protected
* @method edit_maxmark_cancel
* @param {EventFacade} ev The event that triggered this.
* @param {Node} activity The activity whose maxmark we are altering.
* @param {Boolean} preventdefault If true we should prevent the default action from occuring.
*/
edit_maxmark_cancel : function(ev, activity, preventdefault) {
if (preventdefault) {
ev.preventDefault();
}
this.edit_maxmark_clear(activity);
},
/**
* Handles clearing the editing UI and returning things to the original state they were in.
*
* @protected
* @method edit_maxmark_clear
* @param {Node} activity The activity whose maxmark we were altering.
*/
edit_maxmark_clear : function(activity) {
// Detach all listen events to prevent duplicate triggers
new Y.EventHandle(this.editmaxmarkevents).detach();
var editform = activity.one(SELECTOR.ACTIVITYFORM),
instructions = activity.one('#id_editinstructions');
if (editform) {
editform.replace(editform.getData('anchor'));
}
if (instructions) {
instructions.remove();
}
// Remove the editing class again to revert the display.
activity.removeClass(CSS.EDITINGMAXMARK);
// Refocus the link which was clicked originally so the user can continue using keyboard nav.
Y.later(100, this, function() {
activity.one(SELECTOR.EDITMAXMARK).focus();
});
// This hack is to keep Behat happy until they release a version of
// MinkSelenium2Driver that fixes
// https://github.com/Behat/MinkSelenium2Driver/issues/80.
if (!Y.one('input[name=maxmark')) {
Y.one('body').append('<input type="text" name="maxmark" style="display: none">');
}
},
/**
* Joins or separates the given slot with the page of the previous slot. Reorders the pages of
* the other slots
*
* @protected
* @method link_page
* @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.
* @chainable
*/
link_page: function(ev, button, activity, action) {
// Prevent the default button action
ev.preventDefault();
activity = activity.next('li.activity.slot');
var spinner = this.add_spinner(activity),
slotid = 0;
var value = action === 'linkpage' ? 1:2;
var data = {
'class': 'resource',
'field': 'linkslottopage',
'id': slotid,
'value': value
};
slotid = Y.Moodle.mod_quiz.util.slot.getId(activity);
if (slotid) {
data.id = Number(slotid);
}
this.send_request(data, spinner, function(response) {
window.location.reload(true);
// if (response.slots) {
// this.repaginate_slots(response.slots);
// }
});
return this;
},
repaginate_slots: function(slots) {
this.slots = slots;
var section = Y.one(SELECTOR.PAGECONTENT + ' ' + SELECTOR.SECTIONUL),
activities = section.all(SELECTOR.ACTIVITYLI);
activities.each(function(node) {
// What element is it? page/slot/link
// what is the current slot?
var type;
var slot;
if(node.hasClass(CSS.PAGE)){
type = this.NODE_PAGE;
slot = node.next(SELECTOR.SLOTLI);
} else if (node.hasClass(CSS.SLOT)){
type = this.NODE_SLOT;
slot = node;
} else if (node.hasClass(CSS.JOIN)){
type = this.NODE_JOIN;
slot = node.previous(SELECTOR.SLOTLI);
}
// getSlotnumber() Should be a method of util.slot
var slotnumber = Number(Y.Moodle.mod_quiz.util.slot.getNumber(slot));
if(!type){
// Nothing we can do.
return;
}
// Is it correct?
if(!this.slots.hasOwnProperty(slotnumber)){
// An error. We should handle this.
return;
}
var slotdata = this.slots[slotnumber];
if(type === this.NODE_PAGE){
// Get page number
var pagenumber = Y.Moodle.mod_quiz.util.page.getNumber(node);
// Is the page number correct?
if (slotdata.page === pagenumber) {
console.log('slotdata.page == pagenumber return');
return;
}
if (pagenumber < slotdata.page) {
// Remove page node.
node.remove();
}
else {
// Add page node.
console.log('pagenumber > slotdata.page update page number');
}
}
}, this);
},
NAME : 'mod_quiz-resource-toolbox',
ATTRS : {
courseid : {
'value' : 0
},
quizid : {
'value' : 0
}
}
});
M.mod_quiz.resource_toolbox = null;
M.mod_quiz.init_resource_toolbox = function(config) {
M.mod_quiz.resource_toolbox = new RESOURCETOOLBOX(config);
return M.mod_quiz.resource_toolbox;
};
+182
View File
@@ -0,0 +1,182 @@
/**
* Resource and activity toolbox class.
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a course in editing mode.
*
* @module moodle-mod_quiz-toolboxes
* @namespace M.mod_quiz.toolboxes
*/
/**
* Section toolbox class.
*
* This class is responsible for managing AJAX interactions with sections
* when viewing a course in editing mode.
*
* @class section
* @constructor
* @extends M.mod_quiz.toolboxes.toolbox
*/
var SECTIONTOOLBOX = function() {
SECTIONTOOLBOX.superclass.constructor.apply(this, arguments);
};
Y.extend(SECTIONTOOLBOX, TOOLBOX, {
/**
* Initialize the section toolboxes module.
*
* Updates all span.commands with relevant handlers and other required changes.
*
* @method initializer
* @protected
*/
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',
ATTRS : {
courseid : {
'value' : 0
},
quizid : {
'value' : 0
},
format : {
'value' : 'topics'
}
}
});
M.mod_quiz.init_section_toolbox = function(config) {
return new SECTIONTOOLBOX(config);
};
+208
View File
@@ -0,0 +1,208 @@
/**
* Resource and activity toolbox class.
*
* This class is responsible for managing AJAX interactions with activities and resources
* when viewing a course in editing mode.
*
* @module moodle-course-toolboxes
* @namespace M.course.toolboxes
*/
// The CSS classes we use.
var CSS = {
ACTIVITYINSTANCE : 'activityinstance',
AVAILABILITYINFODIV : 'div.availabilityinfo',
CONTENTWITHOUTLINK : 'contentwithoutlink',
CONDITIONALHIDDEN : 'conditionalhidden',
DIMCLASS : 'dimmed',
DIMMEDTEXT : 'dimmed_text',
EDITINSTRUCTIONS : 'editinstructions',
EDITINGMAXMARK: 'editor_displayed',
HIDE : 'hide',
JOIN: 'page_join',
MODINDENTCOUNT : 'mod-indent-',
MODINDENTHUGE : 'mod-indent-huge',
MODULEIDPREFIX : 'slot-',
PAGE: 'page',
SECTIONHIDDENCLASS : 'hidden',
SECTIONIDPREFIX : 'section-',
SLOT : 'slot',
SHOW : 'editing_show',
TITLEEDITOR : 'titleeditor'
},
// The CSS selectors we use.
SELECTOR = {
ACTIONAREA: '.actions',
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',
ACTIVITYMAXMARK : 'input[name=maxmark]',
COMMANDSPAN : '.commands',
CONTENTAFTERLINK : 'div.contentafterlink',
CONTENTWITHOUTLINK : 'div.contentwithoutlink',
EDITMAXMARK: 'a.editing_maxmark',
HIDE : 'a.editing_hide',
HIGHLIGHT : 'a.editing_highlight',
INSTANCENAME : 'span.instancename',
INSTANCEMAXMARK : 'span.instancemaxmark',
MODINDENTDIV : '.mod-indent',
MODINDENTOUTER : '.mod-indent-outer',
PAGECONTENT : 'div#page-content',
PAGELI : 'li.page',
SECTIONUL : 'ul.section',
SHOW : 'a.' + CSS.SHOW,
SHOWHIDE : 'a.editing_showhide',
SLOTLI : 'li.slot',
SUMMARKS : '.mod_quiz_summarks'
},
BODY = Y.one(document.body);
// Setup the basic namespace.
M.mod_quiz = M.mod_quiz || {};
/**
* The toolbox class is a generic class which should never be directly
* instantiated. Please extend it instead.
*
* @class toolbox
* @constructor
* @protected
* @extends Base
*/
var TOOLBOX = function() {
TOOLBOX.superclass.constructor.apply(this, arguments);
};
Y.extend(TOOLBOX, Y.Base, {
/**
* Send a request using the REST API
*
* @method send_request
* @param {Object} data The data to submit with the AJAX request
* @param {Node} [statusspinner] A statusspinner which may contain a section loader
* @param {Function} success_callback The callback to use on success
* @param {Object} [optionalconfig] Any additional configuration to submit
* @chainable
*/
send_request: function(data, statusspinner, success_callback, optionalconfig) {
// Default data structure
if (!data) {
data = {};
}
// Handle any variables which we must pass back through to
var pageparams = this.get('config').pageparams,
varname;
for (varname in pageparams) {
data[varname] = pageparams[varname];
}
data.sesskey = M.cfg.sesskey;
data.courseid = this.get('courseid');
data.quizid = this.get('quizid');
var uri = M.cfg.wwwroot + this.get('ajaxurl');
// Define the configuration to send with the request
var responsetext = [];
var config = {
method: 'POST',
data: data,
on: {
success: function(tid, response) {
try {
responsetext = Y.JSON.parse(response.responseText);
if (responsetext.error) {
new M.core.ajaxException(responsetext);
}
} catch (e) {}
// Run the callback if we have one.
if (responsetext.newsummarks) {
Y.one(SELECTOR.SUMMARKS).setHTML(responsetext.newsummarks);
}
if (success_callback) {
Y.bind(success_callback, this, responsetext)();
}
if (statusspinner) {
window.setTimeout(function() {
statusspinner.hide();
}, 400);
}
},
failure: function(tid, response) {
if (statusspinner) {
statusspinner.hide();
}
new M.core.ajaxException(response);
}
},
context: this
};
// Apply optional config
if (optionalconfig) {
for (varname in optionalconfig) {
config[varname] = optionalconfig[varname];
}
}
if (statusspinner) {
statusspinner.show();
}
// Send the request
Y.io(uri, config);
return this;
}
},
{
NAME: 'mod_quiz-toolbox',
ATTRS: {
/**
* The ID of the Moodle Course being edited.
*
* @attribute courseid
* @default 0
* @type Number
*/
courseid: {
'value': 0
},
/**
* The Moodle course format.
*
* @attribute format
* @default 'topics'
* @type String
*/
quizid: {
'value': 0
},
/**
* The URL to use when submitting requests.
* @attribute ajaxurl
* @default null
* @type String
*/
ajaxurl: {
'value': null
},
/**
* Any additional configuration passed when creating the instance.
*
* @attribute config
* @default {}
* @type Object
*/
config: {
'value': {}
}
}
}
);
@@ -0,0 +1,14 @@
{
"moodle-mod_quiz-toolboxes": {
"requires": [
"base",
"node",
"event",
"event-key",
"io",
"moodle-mod_quiz-quizbase",
"moodle-mod_quiz-util-slot",
"moodle-core-notification-ajaxexception"
]
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"name": "moodle-mod_quiz-util",
"builds": {
"moodle-mod_quiz-util-base": {
"jsfiles": [
"base.js"
]
},
"moodle-mod_quiz-util-slot": {
"jsfiles": [
"slot.js"
]
}
,
"moodle-mod_quiz-util-page": {
"jsfiles": [
"page.js"
]
}
}
}
+15
View File
@@ -0,0 +1,15 @@
/**
* The Moodle.mod_quiz.util classes provide quiz-related utility functions.
*
* @module moodle-mod_quiz-util
* @main
*/
Y.namespace('Moodle.mod_quiz.util');
/**
* A collection of general utility functions for use in quiz.
*
* @class Moodle.mod_quiz.util
* @static
*/
+91
View File
@@ -0,0 +1,91 @@
/**
* A collection of utility classes for use with pages.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-page
*/
Y.namespace('Moodle.mod_quiz.util.page');
/**
* A collection of utility classes for use with pages.
*
* @class Moodle.mod_quiz.util.page
* @static
*/
Y.Moodle.mod_quiz.util.page = {
CONSTANTS: {
PAGEIDPREFIX : 'page-',
PAGENUMBERPREFIX : 'Page '
},
SELECTORS: {
PAGE: 'li.page',
INSTANCENAME: '.instancename'
},
/**
* Retrieve the page item from one of it's child Nodes.
*
* @method getPageFromComponent
* @param pagecomponent {Node} The component Node.
* @return {Node|null} The Page Node.
*/
getPageFromComponent: function(pagecomponent) {
return Y.one(pagecomponent).ancestor(this.SELECTORS.PAGE, true);
},
/**
* Determines the page ID for the provided page.
*
* @method getId
* @param page {Node} The page to find an ID for.
* @return {Number|false} The ID of the page in question or false if no ID was found.
*/
getId: function(page) {
// We perform a simple substitution operation to get the ID.
var id = page.get('id').replace(
this.CONSTANTS.PAGEIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the page name for the provided page.
*
* @method getName
* @param page {Node} The page to find a name for.
* @return {string|false} The name of the page in question or false if no ID was found.
*/
getName: function(page) {
var instance = page.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the page number for the provided page.
*
* @method getNumber
* @param page {Node} The page to find a number for.
* @return {Number|false} The number of the page in question or false if no number was found.
*/
getNumber: function(page) {
// We perform a simple substitution operation to get the ID.
var number = page.get('text').replace(
this.CONSTANTS.PAGENUMBERPREFIX, '');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
}
};
+145
View File
@@ -0,0 +1,145 @@
/**
* A collection of utility classes for use with slots.
*
* @module moodle-mod_quiz-util
* @submodule moodle-mod_quiz-util-slot
*/
Y.namespace('Moodle.mod_quiz.util.slot');
/**
* A collection of utility classes for use with slots.
*
* @class Moodle.mod_quiz.util.slot
* @static
*/
Y.Moodle.mod_quiz.util.slot = {
CONSTANTS: {
SLOTIDPREFIX : 'slot-'
},
SELECTORS: {
SLOT: 'li.slot',
INSTANCENAME: '.instancename',
NUMBER: 'span.slotnumber',
PAGECONTENT : 'div#page-content',
SECTIONUL : 'ul.section'
},
/**
* Retrieve the slot item from one of it's child Nodes.
*
* @method getSlotFromComponent
* @param slotcomponent {Node} The component Node.
* @return {Node|null} The Slot Node.
*/
getSlotFromComponent: function(slotcomponent) {
return Y.one(slotcomponent).ancestor(this.SELECTORS.SLOT, true);
},
/**
* Determines the slot ID for the provided slot.
*
* @method getId
* @param slot {Node} The slot to find an ID for.
* @return {Number|false} The ID of the slot in question or false if no ID was found.
*/
getId: function(slot) {
// We perform a simple substitution operation to get the ID.
var id = slot.get('id').replace(
this.CONSTANTS.SLOTIDPREFIX, '');
// Attempt to validate the ID.
id = parseInt(id, 10);
if (typeof id === 'number' && isFinite(id)) {
return id;
}
return false;
},
/**
* Determines the slot name for the provided slot.
*
* @method getName
* @param slot {Node} The slot to find a name for.
* @return {string|false} The name of the slot in question or false if no ID was found.
*/
getName: function(slot) {
var instance = slot.one(this.SELECTORS.INSTANCENAME);
if (instance) {
return instance.get('firstChild').get('data');
}
return null;
},
/**
* Determines the slot number for the provided slot.
*
* @method getNumber
* @param slot {Node} The slot to find the number for.
* @return {Number|false} The number of the slot in question or false if no number was found.
*/
getNumber: function(slot) {
var number = slot.one(this.SELECTORS.NUMBER).get('text');
// Attempt to validate the ID.
number = parseInt(number, 10);
if (typeof number === 'number' && isFinite(number)) {
return number;
}
return false;
},
/**
* Updates the slot number for the provided slot.
*
* @method setNumber
* @param slot {Node} The slot to update the number for.
* @return void
*/
setNumber: function(slot, number) {
slot.one(this.SELECTORS.NUMBER).set('text', number);
},
/**
* Returns a list of all slot elements on the page.
*
* @method getSlots
* @return {node[]} An array containing slot nodes.
*/
getSlots: function() {
return Y.all(this.SELECTORS.PAGECONTENT + ' ' + this.SELECTORS.SECTIONUL + ' ' + this.SELECTORS.SLOT);
},
/**
* Returns the previous slot to the give slot.
*
* @method getPrevious
* @param slot Slot node
* @return {node|false} The previous slot node or false.
*/
getPrevious: function(slot) {
return slot.previous(this.SELECTORS.SLOT);
},
/**
* Reset the order of the numbers given to each slot.
*
* @method reorder_slots
* @return void
*/
reorder_slots: function() {
// Get list of slot nodes.
var slots = this.getSlots();
// Loop through slots incrementing the number each time.
slots.each(function(slot) {
var previousSlot = this.getPrevious(slot),
previousslotnumber = 0;
if(previousSlot){
previousslotnumber = this.getNumber(previousSlot);
}
// Set slot number.
this.setNumber(slot, previousslotnumber + 1);
}, this);
}
};
+26
View File
@@ -0,0 +1,26 @@
{
"moodle-mod_quiz-util": {
"requires": [
"node"
],
"use": [
"moodle-mod_quiz-util-base"
],
"submodules": {
"moodle-mod_quiz-util-base": {
},
"moodle-mod_quiz-util-slot": {
"requires": [
"node",
"moodle-mod_quiz-util-base"
]
},
"moodle-mod_quiz-util-page": {
"requires": [
"node",
"moodle-mod_quiz-util-base"
]
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

+161
View File
@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="12"
height="12"
id="svg2"
version="1.1"
inkscape:version="0.48.4 r9939"
sodipodi:docname="split.svg"
inkscape:export-filename="\\userdata\documents\tjh238\Desktop\split.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90">
<defs
id="defs4">
<marker
inkscape:stockid="TriangleOutS"
orient="auto"
refY="0.0"
refX="0.0"
id="TriangleOutS"
style="overflow:visible">
<path
id="path3933"
d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt"
transform="scale(0.2)" />
</marker>
<marker
inkscape:stockid="Arrow1Send"
orient="auto"
refY="0.0"
refX="0.0"
id="Arrow1Send"
style="overflow:visible;">
<path
id="path3800"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;"
transform="scale(0.2) rotate(180) translate(6,0)" />
</marker>
<marker
inkscape:stockid="TriangleOutM"
orient="auto"
refY="0.0"
refX="0.0"
id="TriangleOutM"
style="overflow:visible">
<path
id="path3930"
d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt"
transform="scale(0.4)" />
</marker>
<marker
inkscape:stockid="Arrow1Lend"
orient="auto"
refY="0.0"
refX="0.0"
id="Arrow1Lend"
style="overflow:visible;">
<path
id="path3788"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#000000;stroke-width:1.0pt;"
transform="scale(0.8) rotate(180) translate(12.5,0)" />
</marker>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="20"
inkscape:cx="-5.4358209"
inkscape:cy="2.7571732"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="true"
inkscape:window-width="862"
inkscape:window-height="786"
inkscape:window-x="1402"
inkscape:window-y="68"
inkscape:window-maximized="0">
<inkscape:grid
type="xygrid"
id="grid3755"
empspacing="5"
visible="true"
enabled="true"
snapvisiblegridlinesonly="true"
spacingx="0.5px"
spacingy="0.5px" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-1040.3622)">
<path
style="fill:#ffffff;fill-opacity:1;stroke:#7f7f7f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 0.5,1051.3622 0,-5.5 10,0 0,5.5"
id="path5206-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" />
<path
style="fill:#ffffff;stroke:#7f7f7f;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;fill-opacity:1"
d="m 0.5,0 0,5.5 10,0 0,-5.5"
id="path5206"
inkscape:connector-curvature="0"
transform="translate(0,1040.3622)"
sodipodi:nodetypes="cccc" />
<path
style="fill:none;stroke:#7f7f7f;stroke-width:1px;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1;marker-end:none"
d="m 5.5,1042.3622 0,1.5"
id="path3777"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:#7f7f7f;fill-opacity:1;stroke:none"
d="m 2.5,1048.3622 3,3 3,-3"
id="path5169"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#7f7f7f;stroke-width:1px;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1;marker-end:none"
d="m 5.5,1049.3622 0,-1.5"
id="path3777-7"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<path
style="fill:#7f7f7f;fill-opacity:1;stroke:none"
d="m 2.5,1043.3622 3,-3 3,3"
id="path5169-4"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Some files were not shown because too many files have changed in this diff Show More