diff --git a/mod/quiz/classes/form/randomquestion_form.php b/mod/quiz/classes/form/randomquestion_form.php new file mode 100644 index 00000000000..ef138166a47 --- /dev/null +++ b/mod/quiz/classes/form/randomquestion_form.php @@ -0,0 +1,84 @@ +. + +/** + * Defines the editing form for random questions. + * + * @package mod_quiz + * @copyright 2018 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace mod_quiz\form; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->dirroot.'/lib/formslib.php'); + +/** + * Class randomquestion_form + * + * @package mod_quiz + * @copyright 2018 Shamim Rezaie + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class randomquestion_form extends \moodleform { + + /** + * Form definiton. + */ + public function definition() { + $mform = $this->_form; + + $contexts = $this->_customdata['contexts']; + $usablecontexts = $contexts->having_cap('moodle/question:useall'); + + // Standard fields at the start of the form. + $mform->addElement('header', 'generalheader', get_string("general", 'form')); + + $mform->addElement('questioncategory', 'category', get_string('category', 'question'), + array('contexts' => $usablecontexts, 'top' => true)); + + $mform->addElement('advcheckbox', 'includesubcategories', get_string('recurse', 'quiz'), null, null, array(0, 1)); + + $tops = question_get_top_categories_for_contexts(array_column($contexts->all(), 'id')); + $mform->hideIf('includesubcategories', 'category', 'in', $tops); + + $tags = \core_tag_tag::get_tags_by_area_in_contexts('core_question', 'question', $usablecontexts); + $tagstrings = array(); + foreach ($tags as $tag) { + $tagstrings["{$tag->id},{$tag->name}"] = $tag->name; + } + $options = array( + 'multiple' => true, + 'noselectionstring' => get_string('anytags', 'quiz'), + ); + $mform->addElement('autocomplete', 'fromtags', get_string('randomquestiontags', 'mod_quiz'), $tagstrings, $options); + $mform->addHelpButton('fromtags', 'randomquestiontags', 'mod_quiz'); + + $mform->addElement('hidden', 'slotid'); + $mform->setType('slotid', PARAM_INT); + + $mform->addElement('hidden', 'returnurl'); + $mform->setType('returnurl', PARAM_LOCALURL); + + $buttonarray = array(); + $buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('savechanges')); + $buttonarray[] = $mform->createElement('cancel'); + $mform->addGroup($buttonarray, 'buttonar', '', array(' '), false); + $mform->closeHeaderBefore('buttonar'); + } +} diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php index 49dbcd8c563..d3a33bff1d7 100644 --- a/mod/quiz/classes/output/edit_renderer.php +++ b/mod/quiz/classes/output/edit_renderer.php @@ -958,16 +958,16 @@ class edit_renderer extends \plugin_renderer_base { * and also to see that category in the question bank. * * @param structure $structure object containing the structure of the quiz. - * @param int $slot which slot we are outputting. + * @param int $slotnumber which slot we are outputting. * @param \moodle_url $pageurl the canonical URL of this page. * @return string HTML to output. */ - public function random_question(structure $structure, $slot, $pageurl) { + public function random_question(structure $structure, $slotnumber, $pageurl) { - $question = $structure->get_question_in_slot($slot); - $editurl = new \moodle_url('/question/question.php', array( - 'returnurl' => $pageurl->out_as_local_url(), - 'cmid' => $structure->get_cmid(), 'id' => $question->id)); + $question = $structure->get_question_in_slot($slotnumber); + $slot = $structure->get_slot_by_number($slotnumber); + $editurl = new \moodle_url('/mod/quiz/editrandom.php', + array('returnurl' => $pageurl->out_as_local_url(), 'slotid' => $slot->id)); $temp = clone($question); $temp->questiontext = ''; diff --git a/mod/quiz/classes/structure.php b/mod/quiz/classes/structure.php index 58a4c0804e4..342af00a7d2 100644 --- a/mod/quiz/classes/structure.php +++ b/mod/quiz/classes/structure.php @@ -409,6 +409,23 @@ class structure { return $this->slots[$slotid]; } + /** + * Get a slot by it's slot number. Throws an exception if it is missing. + * + * @param int $slotnumber The slot number + * @return \stdClass + * @throws \coding_exception + */ + public function get_slot_by_number($slotnumber) { + foreach ($this->slots as $slot) { + if ($slot->slot == $slotnumber) { + return $slot; + } + } + + throw new \coding_exception('The \'slotnumber\' could not be found.'); + } + /** * Check whether adding a section heading is possible * @param int $pagenumber the number of the page. diff --git a/mod/quiz/editrandom.php b/mod/quiz/editrandom.php new file mode 100644 index 00000000000..135f433351a --- /dev/null +++ b/mod/quiz/editrandom.php @@ -0,0 +1,152 @@ +. + +/** + * Page for editing random questions. + * + * @package mod_quiz + * @copyright 2018 Shamim Rezaie + * @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'); + +$slotid = required_param('slotid', PARAM_INT); +$returnurl = optional_param('returnurl', '', PARAM_LOCALURL); + +// Get the quiz slot. +$slot = $DB->get_record('quiz_slots', array('id' => $slotid)); +if (!$slot || empty($slot->questioncategoryid)) { + print_error('invalidrandomslot', 'mod_quiz'); +} + +if (!$quiz = $DB->get_record('quiz', array('id' => $slot->quizid))) { + print_error('invalidquizid', 'quiz'); +} + +$cm = get_coursemodule_from_instance('quiz', $slot->quizid, $quiz->course); + +require_login($cm->course, false, $cm); + +if ($returnurl) { + $returnurl = new moodle_url($returnurl); +} else { + $returnurl = new moodle_url('/mod/quiz/edit.php', array('cmid' => $cm->id)); +} + +$url = new moodle_url('/mod/quiz/editrandom.php', array('slotid' => $slotid)); +$PAGE->set_url($url); +$PAGE->set_pagelayout('admin'); + +if (!$question = $DB->get_record('question', array('id' => $slot->questionid))) { + print_error('questiondoesnotexist', 'question', $returnurl); +} + +$qtypeobj = question_bank::get_qtype('random'); + +// Validate the question category. +if (!$category = $DB->get_record('question_categories', array('id' => $question->category))) { + print_error('categorydoesnotexist', 'question', $returnurl); +} + +// Check permissions. +question_require_capability_on($question, 'edit'); + +$thiscontext = context_module::instance($cm->id); +$contexts = new question_edit_contexts($thiscontext); + +// Create the question editing form. +$mform = new mod_quiz\form\randomquestion_form(new moodle_url('/mod/quiz/editrandom.php'), + array('contexts' => $contexts)); + +// Send the question object and a few more parameters to the form. +$toform = fullclone($question); +$toform->category = "{$category->id},{$category->contextid}"; +$toform->includesubcategories = $slot->includingsubcategories; +$toform->fromtags = array(); +if ($slot->tags) { + $tags = quiz_extract_random_question_tags($slot->tags); + foreach ($tags as $tag) { + $toform->fromtags[] = "{$tag->id},{$tag->name}"; + } +} +$toform->returnurl = $returnurl; + +if ($cm !== null) { + $toform->cmid = $cm->id; + $toform->courseid = $cm->course; +} else { + $toform->courseid = $COURSE->id; +} + +$toform->slotid = $slotid; + +$mform->set_data($toform); + +if ($mform->is_cancelled()) { + redirect($returnurl); +} else if ($fromform = $mform->get_data()) { + + // If we are moving a question, check we have permission to move it from + // whence it came. Where we are moving to is validated by the form. + list($newcatid, $newcontextid) = explode(',', $fromform->category); + if (!empty($question->id) && $newcatid != $question->category) { + $contextid = $newcontextid; + question_require_capability_on($question, 'move'); + } else { + $contextid = $category->contextid; + } + + $question = $qtypeobj->save_question($question, $fromform); + + // We need to save some data into the quiz_slots table. + $slot->questioncategoryid = $fromform->category; + $slot->includingsubcategories = $fromform->includesubcategories; + + $tags = []; + foreach ($fromform->fromtags as $tagstring) { + list($tagid, $tagname) = explode(',', $tagstring); + $tags[] = (object) [ + 'id' => $tagid, + 'name' => $tagname + ]; + } + $slot->tags = quiz_build_random_question_tag_json($tags); + + $DB->update_record('quiz_slots', $slot); + + // Purge this question from the cache. + question_bank::notify_question_edited($question->id); + + $returnurl->param('lastchanged', $question->id); + redirect($returnurl); +} + +$streditingquestion = $qtypeobj->get_heading(); +$PAGE->set_title($streditingquestion); +$PAGE->set_heading($COURSE->fullname); +$PAGE->navbar->add($streditingquestion); + +// Display a heading, question editing form and possibly some extra content needed for +// for this question type. +echo $OUTPUT->header(); +$heading = get_string('randomediting', 'mod_quiz'); +echo $OUTPUT->heading_with_help($heading, 'randomquestion', 'mod_quiz'); + +$mform->display(); + +echo $OUTPUT->footer(); diff --git a/mod/quiz/lang/en/quiz.php b/mod/quiz/lang/en/quiz.php index aa1194e69bd..9171ba1a672 100644 --- a/mod/quiz/lang/en/quiz.php +++ b/mod/quiz/lang/en/quiz.php @@ -439,6 +439,7 @@ $string['invalidcategory'] = 'Category ID is invalid'; $string['invalidoverrideid'] = 'Invalid override id'; $string['invalidquestionid'] = 'Invalid question id'; $string['invalidquizid'] = 'Invalid quiz ID'; +$string['invalidrandomslot'] = 'Invalid random question slot id.'; $string['invalidsource'] = 'The source is not accepted as valid.'; $string['invalidsourcetype'] = 'Invalid source type.'; $string['invalidstateid'] = 'Invalid state id'; @@ -696,10 +697,13 @@ $string['quiztimer'] = 'Quiz Timer'; $string['quizwillopen'] = 'This quiz will open {$a}'; $string['random'] = 'Random question'; $string['randomcreate'] = 'Create random questions'; +$string['randomediting'] = 'Editing a random question'; $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['randomquestion'] = 'Random question'; +$string['randomquestion_help'] = 'A random question is a way of inserting a randomly-chosen question from a specified category or by a specified tag into an activity.'; $string['randomquestiontags'] = 'Tags'; $string['randomquestiontags_help'] = 'You can restrict the selection criteria further by specifying some question tags here. diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index f0ee68d540b..5761840b5b3 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -2436,3 +2436,60 @@ function quiz_is_overriden_calendar_event(\calendar_event $event) { return $DB->record_exists('quiz_overrides', $overrideparams); } + +/** + * Providing a list of tag records, this function validates each pair and builds a json string + * that can be stored in the quiz_slots.tags field. + * + * @param stdClass[] $tagrecords List of tag objects with id and name properties. + * @return string + */ +function quiz_build_random_question_tag_json($tagrecords) { + $tags = []; + foreach ($tagrecords as $tagrecord) { + if ($tag = core_tag_tag::get($tagrecord->id, 'id, name')) { + $tags[] = [ + 'id' => (int)$tagrecord->id, + 'name' => $tag->name + ]; + } else if ($tag = core_tag_tag::get_by_name(0, $tagrecord->name, 'id, name')) { + $tags[] = [ + 'id' => $tag->id, + 'name' => $tagrecord->name + ]; + } else { + $tags[] = [ + 'id' => null, + 'name' => $tagrecord->name + ]; + } + } + return json_encode($tags); +} + +/** + * Providing tags data in the JSON format, this function returns tag records containing the id and name properties. + * + * @param string $tagsjson The JSON string representing an array of tags in the [{"id":tagid,"name":"tagname"}] format. + * E.g. [{"id":1,"name":"tag1"},{"id":2,"name":"tag2"}] + * Usually equal to the value of the tags field retrieved from the quiz_slots table. + * @return array An array of tags containing the id and name properties, indexed by tag ids. + */ +function quiz_extract_random_question_tags($tagsjson) { + $tagrecords = []; + if (!empty($tagsjson)) { + $tags = json_decode($tagsjson); + // Only work with tags that exist. + foreach ($tags as $tagdata) { + if (!array_key_exists($tagdata->id, $tagrecords)) { + if ($tag = core_tag_tag::get($tagdata->id, 'id, name')) { + $tagrecords[$tag->id] = $tag->to_object(); + } else if ($tag = core_tag_tag::get_by_name(0, $tagdata->name, 'id, name')) { + $tagrecords[$tag->id] = $tag->to_object(); + } + } + } + } + + return $tagrecords; +} \ No newline at end of file