diff --git a/backup/tests/quiz_restore_decode_links_test.php b/backup/tests/quiz_restore_decode_links_test.php index 3aa9a3f8e82..85c82ffd44e 100644 --- a/backup/tests/quiz_restore_decode_links_test.php +++ b/backup/tests/quiz_restore_decode_links_test.php @@ -80,10 +80,13 @@ class restore_quiz_decode_testcase extends \core_privacy\tests\provider_testcase $newcm = duplicate_module($course, get_fast_modinfo($course)->get_cm($quiz->cmid)); - $quizquestions = \mod_quiz\question\bank\qbank_helper::get_question_structure_data($newcm->instance); + $quizquestions = \mod_quiz\question\bank\qbank_helper::get_question_structure( + $newcm->instance, context_module::instance($newcm->id)); $questionids = []; foreach ($quizquestions as $quizquestion) { - $questionids[] = $quizquestion->id; + if ($quizquestion->questionid) { + $questionids[] = $quizquestion->questionid; + } } list($condition, $param) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid'); $condition = 'WHERE qa.question ' . $condition; diff --git a/mod/quiz/attemptlib.php b/mod/quiz/attemptlib.php index febc31505b2..22e03edccdb 100644 --- a/mod/quiz/attemptlib.php +++ b/mod/quiz/attemptlib.php @@ -25,9 +25,10 @@ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); +use mod_quiz\question\bank\qbank_helper; + /** * Class for quiz exceptions. Just saves a couple of arguments on the @@ -79,7 +80,11 @@ class quiz { /** @var context the quiz context. */ protected $context; - /** @var stdClass[] of questions augmented with slot information. */ + /** + * @var stdClass[] of questions augmented with slot information. For non-random + * questions, the array key is question id. For random quesions it is 's' . $slotid. + * probalby best to use ->questionid field of the object instead. + */ protected $questions = null; /** @var stdClass[] of quiz_section rows. */ protected $sections = null; @@ -145,35 +150,33 @@ class quiz { * Load just basic information about all the questions in this quiz. */ public function preload_questions() { - $specificquestionids = \mod_quiz\question\bank\qbank_helper::get_specific_version_question_ids($this->quiz->id); - $latestquestionids = \mod_quiz\question\bank\qbank_helper::get_always_latest_version_question_ids($this->quiz->id); - $questionids = array_merge($specificquestionids, $latestquestionids); - $questiondata = []; - if (!empty($questionids)) { - $questiondata = \mod_quiz\question\bank\qbank_helper::get_question_structure_data($this->quiz->id, $questionids, true); + $slots = qbank_helper::get_question_structure($this->quiz->id, $this->context); + $this->questions = []; + foreach ($slots as $slot) { + $this->questions[$slot->questionid] = $slot; } - $allquestiondata = \mod_quiz\question\bank\qbank_helper::question_load_random_questions( - $this->quiz->id, $questiondata); - $this->questions = $allquestiondata; } /** * Fully load some or all of the questions for this quiz. You must call * {@link preload_questions()} first. * - * @param array|null $questionids question ids of the questions to load. null for all. + * @param array|null $deprecated no longer supported (it was not used). */ - public function load_questions($questionids = null) { + public function load_questions($deprecated = null) { + if ($deprecated !== null) { + debugging('The argument to quiz::load_questions is no longer supported. ' . + 'All questions are always loaded.', DEBUG_DEVELOPER); + } if ($this->questions === null) { throw new coding_exception('You must call preload_questions before calling load_questions.'); } - if (is_null($questionids)) { - $questionids = array_keys($this->questions); - } - $questionstoprocess = array(); - foreach ($questionids as $id) { - if (array_key_exists($id, $this->questions)) { - $questionstoprocess[$id] = $this->questions[$id]; + + $questionstoprocess = []; + foreach ($this->questions as $question) { + if (is_number($question->questionid)) { + $question->id = $question->questionid; + $questionstoprocess[$question->questionid] = $question; } } get_question_options($questionstoprocess); @@ -540,8 +543,17 @@ class quiz { $qcategories = array(); foreach ($this->get_questions() as $questiondata) { - if (!in_array($questiondata->qtype, $questiontypes)) { - $questiontypes[] = $questiondata->qtype; + if ($questiondata->qtype == 'random' and $includepotential) { + if (!isset($qcategories[$questiondata->category])) { + $qcategories[$questiondata->category] = false; + } + if ($questiondata->includingsubcategories) { + $qcategories[$questiondata->category] = true; + } + } else { + if (!in_array($questiondata->qtype, $questiontypes)) { + $questiontypes[] = $questiondata->qtype; + } } } diff --git a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php index 1f1b588766f..1638e97fab5 100644 --- a/mod/quiz/backup/moodle2/restore_quiz_stepslib.php +++ b/mod/quiz/backup/moodle2/restore_quiz_stepslib.php @@ -353,7 +353,7 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st $questionreference->questionarea = 'slot'; $questionreference->itemid = $data->id; $questionreference->questionbankentryid = $question->questionbankentryid; - $questionreference->version = $question->version; + $questionreference->version = null; // Default to Always latest. $DB->insert_record('question_references', $questionreference); } } diff --git a/mod/quiz/classes/output/edit_renderer.php b/mod/quiz/classes/output/edit_renderer.php index eb6924bc705..0eac3937dec 100644 --- a/mod/quiz/classes/output/edit_renderer.php +++ b/mod/quiz/classes/output/edit_renderer.php @@ -756,7 +756,7 @@ class edit_renderer extends \plugin_renderer_base { $data['versionoptions'] = []; if ($structure->get_slot_by_number($slot)->qtype !== 'random') { $data['versionselection'] = true; - $data['versionoption'] = qbank_helper::get_question_version_info($structure->get_question_in_slot($slot)->id, $slotid); + $data['versionoption'] = $structure->get_version_choices_for_slot($slot); $this->page->requires->js_call_amd('mod_quiz/question_slot', 'init', [$slotid]); } @@ -819,8 +819,9 @@ class edit_renderer extends \plugin_renderer_base { $qtype = $structure->get_question_type_for_slot($slot); $questionicons = ''; if ($qtype !== 'random') { - $questionicons .= $this->question_preview_icon($structure->get_quiz(), $structure->get_question_in_slot($slot), - null, null, $qtype); + $questionicons .= $this->question_preview_icon($structure->get_quiz(), + $structure->get_question_in_slot($slot), + null, null, $qtype); } if ($structure->can_be_edited()) { $questionicons .= $this->question_remove_icon($structure, $slot, $pageurl); @@ -860,13 +861,19 @@ class edit_renderer extends \plugin_renderer_base { * 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 \stdClass $questiondata which question to preview. + * If ->questionid is set, that is used instead of ->id. * @param bool $label if true, show the preview question label after the icon * @param int $variant which question variant to preview (optional). - * @param string $qtype the type of question * @return string HTML to output. */ - public function question_preview_icon($quiz, $question, $label = null, $variant = null, $qtype = null) { + public function question_preview_icon($quiz, $questiondata, $label = null, $variant = null) { + $question = clone($questiondata); + if (isset($question->questionid)) { + + $question->id = $question->questionid; + } + $url = quiz_question_preview_url($quiz, $question, $variant); // Do we want a label? @@ -992,7 +999,7 @@ class edit_renderer extends \plugin_renderer_base { $question = $structure->get_question_in_slot($slot); $editurl = new \moodle_url('/question/bank/editquestion/question.php', array( 'returnurl' => $pageurl->out_as_local_url(), - 'cmid' => $structure->get_cmid(), 'id' => $question->id)); + 'cmid' => $structure->get_cmid(), 'id' => $question->questionid)); $instancename = quiz_question_tostring($question); @@ -1034,9 +1041,6 @@ class edit_renderer extends \plugin_renderer_base { $temp->questiontext = ''; $instancename = quiz_question_tostring($temp); - $setreference = qbank_helper::get_random_question_data_from_slot($slot->id); - $filtercondition = json_decode($setreference->filtercondition); - $configuretitle = get_string('configurerandomquestion', 'quiz'); $qtype = \question_bank::get_qtype($question->qtype, false); $namestr = $qtype->local_name(); @@ -1044,15 +1048,15 @@ class edit_renderer extends \plugin_renderer_base { 'class' => 'icon activityicon', 'alt' => ' ', 'role' => 'presentation')); $editicon = $this->pix_icon('t/edit', $configuretitle, 'moodle', array('title' => '')); - $qbankurlparams = array( - 'cmid' => $structure->get_cmid(), - 'cat' => $filtercondition->questioncategoryid . ',' . $setreference->questionscontextid, - 'recurse' => !empty($setreference->questionscontextid) - ); + $qbankurlparams = [ + 'cmid' => $structure->get_cmid(), + 'cat' => $slot->category . ',' . $slot->contextid, + 'recurse' => $slot->randomrecurse, + ]; $slottags = []; - if (isset($filtercondition->tags)) { - $slottags = $filtercondition->tags; + if (isset($slot->randomtags)) { + $slottags = $slot->randomtags; } foreach ($slottags as $index => $slottag) { $slottag = explode(',', $slottag); diff --git a/mod/quiz/classes/question/bank/qbank_helper.php b/mod/quiz/classes/question/bank/qbank_helper.php index 2b05627be48..8b0b355f039 100644 --- a/mod/quiz/classes/question/bank/qbank_helper.php +++ b/mod/quiz/classes/question/bank/qbank_helper.php @@ -16,6 +16,8 @@ namespace mod_quiz\question\bank; +use core_question\local\bank\question_version_status; + defined('MOODLE_INTERNAL') || die(); require_once($CFG->dirroot . '/mod/quiz/accessmanager.php'); @@ -38,7 +40,7 @@ class qbank_helper { * @param int $slotid * @return bool */ - public static function is_random($slotid): bool { + public static function is_random(int $slotid): bool { global $DB; $params = [ 'itemid' => $slotid, @@ -49,23 +51,31 @@ class qbank_helper { } /** - * Get the version options for the question. + * Get the available versions of a question where one of the version has the given question id. * - * @param int $questionid - * @return array + * @param int $questionid id of a question. + * @return \stdClass[] other versions of this question. Each object has fields versionid, + * version and questionid. Array is returned most recent version first. */ - public static function get_version_options($questionid): array { + public static function get_version_options(int $questionid): array { global $DB; - $sql = "SELECT qv.id AS versionid, qv.version, qv.questionid - FROM {question_versions} qv - WHERE qv.questionbankentryid = (SELECT DISTINCT qbe.id - FROM {question_bank_entries} qbe - JOIN {question_versions} qv ON qbe.id = qv.questionbankentryid - JOIN {question} q ON qv.questionid = q.id - WHERE q.id = ?) - ORDER BY qv.version DESC"; - return $DB->get_records_sql($sql, [$questionid]); + return $DB->get_records_sql(" + SELECT allversions.id AS versionid, + allversions.version, + allversions.questionid + + FROM {question_versions} allversions + + WHERE allversions.questionbankentryid = ( + SELECT givenversion.questionbankentryid + FROM {question_versions} givenversion + WHERE givenversion.questionid = ? + ) + AND allversions.status <> ? + + ORDER BY allversions.version DESC + ", [$questionid, question_version_status::QUESTION_STATUS_DRAFT]); } /** @@ -74,7 +84,7 @@ class qbank_helper { * @param int $slotid * @return mixed */ - public static function get_question_for_redo($slotid) { + public static function get_question_for_redo(int $slotid) { global $DB; $params = [ 'itemid' => $slotid, @@ -102,236 +112,120 @@ class qbank_helper { } /** - * Get random question object from the slot id. + * Get the information about which questions should be used to create a quiz attempt. * - * @param int $slotid - * @return false|mixed|\stdClass + * Each element in the returned array is indexed by slot.slot (slot number) an each object hass: + * - All the field of the slot table. + * - contextid for where the question(s) come from. + * - category id for where the questions come from. + * - For non-random questions, All the fields of the question table (but id is in questionid). + * Also question version and question bankentryid. + * - For random questions, filtercondition, which is also unpacked into category, randomrecurse, + * randomtags, and note that these also have a ->name set and ->qtype set to 'random'. + * + * @param int $quizid the id of the quiz to load the data for. + * @param \context $quizcontext + * @return array indexed by slot, with information about the content of each slot. */ - public static function get_random_question_data_from_slot($slotid) { + public static function get_question_structure(int $quizid, \context $quizcontext) { global $DB; - $params = [ - 'itemid' => $slotid, - 'component' => 'mod_quiz', - 'questionarea' => 'slot' - ]; - return $DB->get_record('question_set_references', $params); - } - /** - * Get the question ids for specific question version. - * - * @param int $quizid - * @return array - */ - public static function get_specific_version_question_ids($quizid) { - global $DB; - $questionids = []; - $sql = 'SELECT qv.questionid - FROM {quiz_slots} qs - JOIN {question_references} qr ON qr.itemid = qs.id - JOIN {question_versions} qv ON qv.questionbankentryid = qr.questionbankentryid - AND qv.version = qr.version - WHERE qr.version IS NOT NULL - AND qs.quizid = ? - AND qr.component = ? - AND qr.questionarea = ?'; - $questions = $DB->get_records_sql($sql, [$quizid, 'mod_quiz', 'slot']); - foreach ($questions as $question) { - $questionids [] = $question->questionid; - } - return $questionids; - } - - /** - * Get the question ids for always latest options. - * - * @param int $quizid - * @return array - */ - public static function get_always_latest_version_question_ids($quizid) { - global $DB; - $questionids = []; - $sql = 'SELECT qr.questionbankentryid as entry - FROM {quiz_slots} qs - JOIN {question_references} qr ON qr.itemid = qs.id - WHERE qr.version IS NULL - AND qs.quizid = ? - AND qr.component = ? - AND qr.questionarea = ?'; - $entryids = $DB->get_records_sql($sql, [$quizid, 'mod_quiz', 'slot']); - $questionentries = []; - foreach ($entryids as $entryid) { - $questionentries [] = $entryid->entry; - } - if (empty($questionentries)) { - return $questionids; - } - list($questionidcondition, $params) = $DB->get_in_or_equal($questionentries); - $extracondition = 'AND qv.questionbankentryid ' . $questionidcondition; - $questionsql = "SELECT q.id - FROM {question} q - JOIN {question_versions} qv ON qv.questionid = q.id - WHERE qv.version = (SELECT MAX(v.version) - FROM {question_versions} v - JOIN {question_bank_entries} be - ON be.id = v.questionbankentryid - WHERE be.id = qv.questionbankentryid) - $extracondition"; - $questions = $DB->get_records_sql($questionsql, $params); - foreach ($questions as $question) { - $questionids [] = $question->id; - } - return $questionids; - } - - /** - * Get the question structure data for the given quiz or question ids. - * - * @param int $quizid ID of the quiz to load data for. - * @param array $questionids - * @param bool $attempt if false (default) array key is slot number, else array key is question.id. - * @return array the question data, ordered by slot number. - */ - public static function get_question_structure_data($quizid, $questionids = [], $attempt = false) { - global $DB; - $params = ['quizid' => $quizid]; - $condition = ''; - $joinon = 'AND qr.version = qv.version'; - if (!empty($questionids)) { - list($condition, $param) = $DB->get_in_or_equal($questionids, SQL_PARAMS_NAMED, 'questionid'); - $condition = 'AND q.id ' . $condition; - $joinon = ''; - $params = array_merge($params, $param); - } - if ($attempt) { - $selectstart = 'q.*, slot.id AS slotid, slot.slot,'; - } else { - $selectstart = 'slot.slot, slot.id AS slotid, q.*,'; - } - $sql = "SELECT $selectstart - q.id AS questionid, - q.name, - q.qtype, - q.length, + // Load all the data about each slot. + $slotdata = $DB->get_records_sql(" + SELECT slot.slot, + slot.id AS slotid, slot.page, slot.maxmark, slot.requireprevious, - qc.id as category, - qc.contextid,qv.status, - qv.id as versionid, + qsr.filtercondition, + qv.status, + qv.id AS versionid, qv.version, - qv.questionbankentryid + qr.version AS requestedversion, + qv.questionbankentryid, + q.id AS questionid, + q.*, + qc.id AS category, + COALESCE(qc.contextid, qsr.questionscontextid) AS contextid + FROM {quiz_slots} slot - LEFT JOIN {question_references} qr ON qr.itemid = slot.id AND qr.component = 'mod_quiz' AND qr.questionarea = 'slot' + + -- case where a particular question has been added to the quiz. + LEFT JOIN {question_references} qr ON qr.usingcontextid = :quizcontextid AND qr.component = 'mod_quiz' + AND qr.questionarea = 'slot' AND qr.itemid = slot.id LEFT JOIN {question_bank_entries} qbe ON qbe.id = qr.questionbankentryid - LEFT JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id $joinon + LEFT JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id + -- Either specified version, or latest ready version. + AND qv.version = COALESCE(qr.version, ( + SELECT MAX(version) + FROM {question_versions} + WHERE questionbankentryid = qbe.id AND status <> :draft + )) LEFT JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid LEFT JOIN {question} q ON q.id = qv.questionid + + -- Case where a random question has been added. + LEFT JOIN {question_set_references} qsr ON qsr.usingcontextid = :quizcontextid2 AND qsr.component = 'mod_quiz' + AND qsr.questionarea = 'slot' AND qsr.itemid = slot.id + WHERE slot.quizid = :quizid - $condition - ORDER BY slot.slot"; - $questiondatas = $DB->get_records_sql($sql, $params); - foreach ($questiondatas as $questiondata) { - $questiondata->_partiallyloaded = true; - } - return $questiondatas; - } + ORDER BY slot.slot + ", ['draft' => question_version_status::QUESTION_STATUS_DRAFT, + 'quizcontextid' => $quizcontext->id, 'quizcontextid2' => $quizcontext->id, + 'quizid' => $quizid]); - /** - * Get question structure. - * - * @param int $quizid - * @return array - */ - public static function get_question_structure($quizid) { - $firstslotsets = self::get_question_structure_data($quizid); - $latestquestionids = self::get_always_latest_version_question_ids($quizid); - $secondslotsets = self::get_question_structure_data($quizid, $latestquestionids); - foreach ($firstslotsets as $key => $firstslotset) { - foreach ($secondslotsets as $secondslotset) { - if ($firstslotset->slotid === $secondslotset->slotid) { - unset($firstslotsets[$key]); - } - } - } + // Uppack the random info from question_set_reference. + foreach ($slotdata as $slot) { + // Ensure the right id is the id. + $slot->id = $slot->slotid; - $data = array_merge($firstslotsets, $secondslotsets); - ksort($data); - return $data; - } - - /** - * Load random questions. - * - * @param int $quizid - * @param array $questiondata - * @return array - */ - public static function question_load_random_questions($quizid, $questiondata) { - global $DB, $USER; - $sql = 'SELECT slot.id AS slotid, - slot.maxmark, - slot.slot, - slot.page, - qsr.filtercondition - FROM {question_set_references} qsr - JOIN {quiz_slots} slot ON slot.id = qsr.itemid - WHERE slot.quizid = ? - AND qsr.component = ? - AND qsr.questionarea = ?'; - $randomquestiondatas = $DB->get_records_sql($sql, [$quizid, 'mod_quiz', 'slot']); - - $randomquestions = []; - // Questions already added. - $usedquestionids = []; - foreach ($questiondata as $question) { - if (isset($usedquestions[$question->id])) { - $usedquestionids[$question->id] += 1; + if ($slot->filtercondition) { + // Unpack the information about a random question. + $filtercondition = json_decode($slot->filtercondition); + $slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique. + $slot->category = $filtercondition->questioncategoryid; + $slot->randomrecurse = $filtercondition->includingsubcategories; + $slot->randomtags = isset($filtercondition->tags) ? (array) $filtercondition->tags : []; + $slot->qtype = 'random'; + $slot->name = get_string('random', 'quiz'); + $slot->length = 1; + } else if ($slot->qtype === null) { + // This question must have gone missing. Put in a placeholder. + $slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique. + $slot->category = 0; + $slot->qtype = 'missingtype'; + $slot->name = get_string('missingquestion', 'quiz'); + $slot->maxmark = 0; + $slot->questiontext = ' '; + $slot->questiontextformat = FORMAT_HTML; + $slot->length = 1; + } else if (!\question_bank::qtype_exists($slot->qtype)) { + // Question of unknown type found in the database. Set to placeholder question types instead. + $slot->qtype = 'missingtype'; } else { - $usedquestionids[$question->id] = 1; + $slot->_partiallyloaded = 1; } } - // Usages for this user's previous quiz attempts. - $qubaids = new \mod_quiz\question\qubaids_for_users_attempts($quizid, $USER->id); - $randomloader = new \core_question\local\bank\random_question_loader($qubaids, $usedquestionids); - foreach ($randomquestiondatas as $randomquestiondata) { - $filtercondition = json_decode($randomquestiondata->filtercondition); - $tagids = []; - if (isset($filtercondition->tags)) { - foreach ($filtercondition->tags as $tag) { - $tagstring = explode(',', $tag); - $tagids [] = $tagstring[0]; - } - } - $randomquestiondata->randomfromcategory = $filtercondition->questioncategoryid; - $randomquestiondata->randomincludingsubcategories = $filtercondition->includingsubcategories; - $randomquestiondata->questionid = $randomloader->get_next_question_id($randomquestiondata->randomfromcategory, - $randomquestiondata->randomincludingsubcategories, $tagids); - $randomquestions[] = $randomquestiondata; + return $slotdata; + } + + /** + * Get this list of random selection tag ids from one of the slots returned by get_question_structure. + * + * @param \stdClass $slotdata one of the array elements returend by get_question_structure. + * @return array list of tag ids. + */ + public static function get_tag_ids_for_slot(\stdClass $slotdata): array { + if (empty($slot->randomtags)) { + return []; } - foreach ($randomquestions as $randomquestion) { - // Should not add if there is no question found from the random question loader, maybe empty category. - if ($randomquestion->questionid === null) { - continue; - } - $question = new \stdClass(); - $question->slotid = $randomquestion->slotid; - $question->maxmark = $randomquestion->maxmark; - $question->slot = $randomquestion->slot; - $question->page = $randomquestion->page; - $qdatas = question_preload_questions($randomquestion->questionid); - $qdatas = reset($qdatas); - foreach ($qdatas as $key => $qdata) { - $question->$key = $qdata; - } - $questiondata[$question->id] = $question; + $tagids = []; + foreach ($slotdata->randomtags as $tag) { + $tagids[] = $tag->id; } - - uasort($questiondata, function (\stdClass $a, \stdClass $b): int { return ($a->slot <=> $b->slot); }); - - return $questiondata; + return $tagids; } /** @@ -366,48 +260,4 @@ class qbank_helper { } return $newqusetionid; } - - /** - * Get the version information for a question to show in the version selection dropdown. - * - * @param int $questionid - * @param int $slotid - * @return array - */ - public static function get_question_version_info($questionid, $slotid): array { - global $DB; - $versiondata = []; - $versionsoptions = self::get_version_options($questionid); - $latestversion = reset($versionsoptions); - // Object for using the latest version. - $alwaysuselatest = new \stdClass(); - $alwaysuselatest->versionid = 0; - $alwaysuselatest->version = 0; - $alwaysuselatest->versionvalue = get_string('alwayslatest', 'quiz'); - array_unshift($versionsoptions, $alwaysuselatest); - $referencedata = $DB->get_record('question_references', - ['itemid' => $slotid, 'component' => 'mod_quiz', 'questionarea' => 'slot']); - if (!isset($referencedata->version) || ($referencedata->version === null)) { - $currentversion = 0; - } else { - $currentversion = $referencedata->version; - } - - foreach ($versionsoptions as $versionsoption) { - $versionsoption->selected = false; - if ($versionsoption->version === $currentversion) { - $versionsoption->selected = true; - } - if (!isset($versionsoption->versionvalue)) { - if ($versionsoption->version === $latestversion->version) { - $versionsoption->versionvalue = get_string('questionversionlatest', 'quiz', $versionsoption->version); - } else { - $versionsoption->versionvalue = get_string('questionversion', 'quiz', $versionsoption->version); - } - } - - $versiondata[] = $versionsoption; - } - return $versiondata; - } } diff --git a/mod/quiz/classes/structure.php b/mod/quiz/classes/structure.php index 87a9a3a5918..a5f7cde24d4 100644 --- a/mod/quiz/classes/structure.php +++ b/mod/quiz/classes/structure.php @@ -88,7 +88,7 @@ class structure { public static function create_for_quiz($quizobj) { $structure = self::create(); $structure->quizobj = $quizobj; - $structure->populate_structure($quizobj->get_quiz()); + $structure->populate_structure(); return $structure; } @@ -612,83 +612,28 @@ class structure { /** * Set up this class with the structure for a given quiz. - * @param \stdClass $quiz the quiz settings. */ - public function populate_structure($quiz) { + protected function populate_structure() { global $DB; - $slots = qbank_helper::get_question_structure($quiz->id); - - $slots = $this->populate_missing_questions($slots); + $slots = qbank_helper::get_question_structure($this->quizobj->get_quizid(), $this->quizobj->get_context()); $this->questions = []; $this->slotsinorder = []; foreach ($slots as $slotdata) { $this->questions[$slotdata->questionid] = $slotdata; - $slot = new \stdClass(); - $slot->id = $slotdata->slotid; - $slot->name = $slotdata->name; - $slot->slot = $slotdata->slot; - $slot->quizid = $quiz->id; - $slot->page = $slotdata->page; - $slot->questionid = $slotdata->questionid; - $slot->maxmark = $slotdata->maxmark; - $slot->requireprevious = $slotdata->requireprevious; - $slot->qtype = $slotdata->qtype; - $slot->length = $slotdata->length; - $slot->category = $slotdata->category; - $slot->questionbankentryid = $slotdata->questionbankentryid ?? null; - $slot->version = $slotdata->version ?? null; - + $slot = clone($slotdata); + $slot->quizid = $this->quizobj->get_quizid(); $this->slotsinorder[$slot->slot] = $slot; } // Get quiz sections in ascending order of the firstslot. - $this->sections = $DB->get_records('quiz_sections', array('quizid' => $quiz->id), 'firstslot ASC'); + $this->sections = $DB->get_records('quiz_sections', ['quizid' => $this->quizobj->get_quizid()], 'firstslot'); $this->populate_slots_with_sections(); $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) { - global $DB; - // Address missing/random question types. - foreach ($slots as $slot) { - if ($slot->qtype === null) { - // Check if the question is random. - if ($setreference = $DB->get_record('question_set_references', - ['itemid' => $slot->slotid, 'component' => 'mod_quiz', 'questionarea' => 'slot'])) { - $filtercondition = json_decode($setreference->filtercondition); - $slot->id = $slot->slotid; - $slot->category = $filtercondition->questioncategoryid; - $slot->qtype = 'random'; - $slot->name = get_string('random', 'quiz'); - $slot->length = 1; - } else { - // 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->maxmark = 0; - $slot->requireprevious = 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. */ @@ -721,6 +666,45 @@ class structure { } } + /** + * Get the version options to show on the Questions page for a particular question. + * + * @param int $slotnumber which slot to get the choices for. + * @return \stdClass[] other versions of this question. Each object has fields versionid, + * version and selected. Array is returned most recent version first. + */ + public function get_version_choices_for_slot(int $slotnumber): array { + $slot = $this->get_slot_by_number($slotnumber); + + // Get all the versions which exist. + $versions = qbank_helper::get_version_options($slot->questionid); + $latestversion = reset($versions); + + // Format the choices for display. + $versionoptions = []; + foreach ($versions as $version) { + $version->selected = $version->version === $slot->requestedversion; + + if ($version->version === $latestversion->version) { + $version->versionvalue = get_string('questionversionlatest', 'quiz', $version->version); + } else { + $version->versionvalue = get_string('questionversion', 'quiz', $version->version); + } + + $versionoptions[] = $version; + } + + // Make a choice for 'Always latest'. + $alwaysuselatest = new \stdClass(); + $alwaysuselatest->versionid = 0; + $alwaysuselatest->version = 0; + $alwaysuselatest->versionvalue = get_string('alwayslatest', 'quiz'); + $alwaysuselatest->selected = $slot->requestedversion === null; + array_unshift($versionoptions, $alwaysuselatest); + + return $versionoptions; + } + /** * Move a slot from its current location to a new location. * diff --git a/mod/quiz/locallib.php b/mod/quiz/locallib.php index 12268bf5956..b3f6de1720a 100644 --- a/mod/quiz/locallib.php +++ b/mod/quiz/locallib.php @@ -39,6 +39,7 @@ require_once($CFG->libdir . '/completionlib.php'); require_once($CFG->libdir . '/filelib.php'); require_once($CFG->libdir . '/questionlib.php'); +use mod_quiz\question\bank\qbank_helper; /** * @var int We show the countdown timer if there is less than this amount of time left before the @@ -170,12 +171,10 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time // First load all the non-random questions. $randomfound = false; - $randomtestfound = false; $slot = 0; $questions = array(); $maxmark = array(); $page = array(); - $questiondatarandom = []; foreach ($quizobj->get_questions() as $questiondata) { $slot += 1; $maxmark[$slot] = $questiondata->maxmark; @@ -184,34 +183,55 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time $randomfound = true; continue; } - - // Intended for testing purposes only. - foreach ($questionids as $key => $questionid) { - if ($questionid !== (int)$questiondata->id && $slot === $key) { - $randomtestfound = true; - $questiondatarandom[$key] = $questiondata; - continue 2; - } - } - if (!$quizobj->get_quiz()->shuffleanswers) { $questiondata->options->shuffleanswers = false; } $questions[$slot] = question_bank::make_question($questiondata); } - // Then find a question throw an error as something horribly wrong might have happened. + // Then find a question to go in place of each random question. if ($randomfound) { - throw new coding_exception( - 'Using "random" questions directly in an attempt is deprecated. Please use question_set_references table instead.' - ); - } + $slot = 0; + $usedquestionids = array(); + foreach ($questions as $question) { + if ($question->id && isset($usedquestions[$question->id])) { + $usedquestionids[$question->id] += 1; + } else { + $usedquestionids[$question->id] = 1; + } + } + $randomloader = new \core_question\local\bank\random_question_loader($qubaids, $usedquestionids); + + foreach ($quizobj->get_questions() as $questiondata) { + $slot += 1; + if ($questiondata->qtype != 'random') { + continue; + } + + $tagids = qbank_helper::get_tag_ids_for_slot($questiondata); - // Then find a question to go in place of each random question. Intended for testing purposes only. - if ($randomtestfound) { - foreach ($questiondatarandom as $slot => $questiondata) { // Deal with fixed random choices for testing. - $questions[$slot] = question_bank::load_question($questionids[$slot], $quizobj->get_quiz()->shuffleanswers); + if (isset($questionids[$quba->next_slot_number()])) { + if ($randomloader->is_question_available($questiondata->category, + (bool) $questiondata->questiontext, $questionids[$quba->next_slot_number()], $tagids)) { + $questions[$slot] = question_bank::load_question( + $questionids[$quba->next_slot_number()], $quizobj->get_quiz()->shuffleanswers); + continue; + } else { + throw new coding_exception('Forced question id not available.'); + } + } + + // Normal case, pick one at random. + $questionid = $randomloader->get_next_question_id($questiondata->category, + $questiondata->randomrecurse, $tagids); + if ($questionid === null) { + throw new moodle_exception('notenoughrandomquestions', 'quiz', + $quizobj->view_url(), $questiondata); + } + + $questions[$slot] = question_bank::load_question($questionid, + $quizobj->get_quiz()->shuffleanswers); } } diff --git a/mod/quiz/tests/behat/quiz_question_versions.feature b/mod/quiz/tests/behat/quiz_question_versions.feature index fcc80577c63..189602e01fc 100644 --- a/mod/quiz/tests/behat/quiz_question_versions.feature +++ b/mod/quiz/tests/behat/quiz_question_versions.feature @@ -21,82 +21,93 @@ Feature: Quiz question versioning | activity | name | course | idnumber | | quiz | Quiz 1 | C1 | quiz1 | And the following "questions" exist: - | questioncategory | qtype | name | questiontext | answer 1 | - | Test questions | truefalse | First question | Answer the first question | True | + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | First question | Answer the first question | + | Test questions | truefalse | Other question | Answer the first question | And quiz "Quiz 1" contains the following questions: | question | page | | First question | 1 | - And I log in as "teacher1" - And I am on "Course 1" course homepage @javascript - Scenario: Approriate question version should be displayed when not edited - When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1" - And I should see "First question" + Scenario: Appropriate question version should be displayed when not edited + When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher" + Then I should see "First question" And I should see "Answer the first question" - And I should see "v1 (latest)" + And the field "version" matches value "Always latest" + And "v1 (latest)" "option" should exist in the "version" "select" # We check that the corresponding version is the appropriate one in preview And I click on "Preview question" "link" And I switch to "questionpreview" window And I should see "Version 1 (latest)" And I should see "Answer the first question" - And I press "Display options" - And I set the following fields to these values: - | id_feedback | Not shown | - | id_generalfeedback | Not shown | - | id_rightanswer | Shown | - And I press "id_saveupdate" - And I click on "finish" "button" - And I should see "The correct answer is 'True'." + And I click on "Submit and finish" "button" + And I should see "You should have selected true." @javascript Scenario: Approriate question version should be displayed when edited - When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1" + When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher" And I click on "Edit question First question" "link" # We edit the question with new informations to generate a second version And I set the following fields to these values: - | id_name | Second question | - | id_questiontext | This is the second question text | - | id_correctanswer | False | + | Question name | First question (v2) | + | Question text | Answer the new first question | + | Correct answer | False | And I press "id_submitbutton" - And I set the field "version" to "v2" - And I should see "Second question" - And I should see "This is the second question text" + And the field "version" matches value "Always latest" + And "v1" "option" should exist in the "version" "select" + And I set the field "version" to "v2 (latest)" + Then I should see "First question (v2)" + And I should see "Answer the new first question" And I click on "Preview question" "link" And I switch to "questionpreview" window # We check that the corresponding version is the appropriate one in preview - # We also check that the new informations are properly displayed + # We also check that the new information is properly displayed And I should see "Version 2 (latest)" - And I should see "This is the second question text" - And I press "Display options" + And I should see "Answer the new first question" + + @javascript + Scenario: Appropriate question version displayed when later draft version exists + # Edit the question in the question bank to add a new draft version. + Given I am on the "First question" "core_question > edit" page logged in as teacher And I set the following fields to these values: - | id_feedback | Not shown | - | id_generalfeedback | Not shown | - | id_rightanswer | Shown | - And I press "id_saveupdate" - And I click on "finish" "button" - Then I should see "The correct answer is 'False'." + | Question name | First question (v2) | + | Question text | Answer the new first question | + | Correct answer | False | + | Question status | Draft | + And I press "id_submitbutton" + When I am on the "Quiz 1" "mod_quiz > Edit" page + Then I should see "First question" + And I should see "Answer the first question" + And the field "version" matches value "Always latest" + And "v1 (latest)" "option" should exist in the "version" "select" + And "v2" "option" should not exist in the "version" "select" + And "v2 (latest)" "option" should not exist in the "version" "select" + And I am on the "Quiz 1" "mod_quiz > View" page + And I press "Preview quiz" + And I should see "Answer the first question" @javascript Scenario: Creating a new question should have always latest in the version selection - When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1" - And I click on "Add" "link" + When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher" + # Change the version of the existing question, to ensure it does not match later. + And I set the field "version" to "v1 (latest)" + And I open the "Page 1" add to quiz menu And I follow "a new question" And I set the field "item_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 set the following fields to these values: + | Question name | New essay | + | Question text | Write 200 words about quizzes. | And I press "id_submitbutton" - And I should see "Essay 01 new" on quiz page "1" - And I should see "Always latest" on quiz page "1" + And I should see "New essay" on quiz page "1" + And the field "version" in the "New essay" "list_item" matches value "Always latest" @javascript Scenario: Adding a question from question bank should have always latest in the version selection - When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1" - And I click on "Add" "link" + When I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher" + And I open the "Page 1" add to quiz menu And I follow "from question bank" - And I set the field with xpath "//input[@type='checkbox' and @id='qbheadercheckbox']" to "1" + And I click on "Select" "checkbox" in the "Other question" "table_row" And I press "Add selected questions to the quiz" - And I should see "First question" on quiz page "1" - And I should see "Always latest" on quiz page "1" + Then I should see "Other question" on quiz page "1" + And the field "version" in the "Other question" "list_item" matches value "Always latest" diff --git a/mod/quiz/tests/local_structure_slot_random_test.php b/mod/quiz/tests/local_structure_slot_random_test.php index b979885e937..baddd24c616 100644 --- a/mod/quiz/tests/local_structure_slot_random_test.php +++ b/mod/quiz/tests/local_structure_slot_random_test.php @@ -412,15 +412,13 @@ class mod_quiz_local_structure_slot_random_test extends advanced_testcase { $randomslot->set_filter_condition($filtercondition); $randomslot->insert(1); // Put the question on the first page of the quiz. - // Get the random question's quiz_slot. It is at the first slot. - $quizslot = $DB->get_record('quiz_slots', array('quizid' => $quiz->id, 'slot' => 1)); - // Get the random question's tags from quiz_slot_tags. It is at the first slot. - $setreference = \mod_quiz\question\bank\qbank_helper::get_random_question_data_from_slot($quizslot->id); + $slots = qbank_helper::get_question_structure($quiz->id, $quizcontext); + $quizslot = reset($slots); - $this->assertEquals($category->id, json_decode($setreference->filtercondition)->questioncategoryid); - $this->assertEquals(1, json_decode($setreference->filtercondition)->includingsubcategories); + $this->assertEquals($category->id, $quizslot->category); + $this->assertEquals(1, $quizslot->randomrecurse); $this->assertEquals(1, $quizslot->maxmark); - $tagspropery = (array)json_decode($setreference->filtercondition)->tags; + $tagspropery = $quizslot->randomtags; $this->assertCount(2, $tagspropery); $this->assertEqualsCanonicalizing( diff --git a/mod/quiz/tests/qbank_helper_test.php b/mod/quiz/tests/qbank_helper_test.php index 7491e907817..9216057a8fb 100644 --- a/mod/quiz/tests/qbank_helper_test.php +++ b/mod/quiz/tests/qbank_helper_test.php @@ -65,17 +65,14 @@ class qbank_helper_test extends \advanced_testcase { // Test for questions from a different context. $context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); - $this->add_random_questions($questiongenerator, $quiz, ['contextid' => $context->id]); + $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $context->id]); // Create the quiz object. $quizobj = \quiz::create($quiz->id); - $structure = \mod_quiz\structure::create_for_quiz($quizobj); + $structure = structure::create_for_quiz($quizobj); $slots = $structure->get_slots(); foreach ($slots as $slot) { $this->assertEquals(true, qbank_helper::is_random($slot->id)); - // Test random data for slot. - $this->assertEquals($slot->id, qbank_helper::get_random_question_data_from_slot($slot->id)->itemid); } - } /** @@ -83,7 +80,6 @@ class qbank_helper_test extends \advanced_testcase { * * @covers ::get_version_options * @covers ::get_question_for_redo - * @covers ::get_always_latest_version_question_ids */ public function test_reference_records() { $this->resetAfterTest(); @@ -119,8 +115,10 @@ class qbank_helper_test extends \advanced_testcase { $questions = $quizobj->get_questions(); $question = reset($questions); $this->assertEquals($question->id, qbank_helper::get_question_for_redo($slot->id)); + // Create another version. $questiongenerator->update_question($numq, null, ['name' => 'This is the latest version']); + // Change to always latest. submit_question_version::execute($slot->id, 0); $quizobj->preload_questions(); @@ -128,108 +126,46 @@ class qbank_helper_test extends \advanced_testcase { $questions = $quizobj->get_questions(); $question = reset($questions); $this->assertEquals($question->id, qbank_helper::get_question_for_redo($slot->id)); - - // Test always latest version question ids. - $latestquestionids = qbank_helper::get_always_latest_version_question_ids($quiz->id); - $this->assertEquals($question->id, reset($latestquestionids)); } /** * Test question structure data. * * @covers ::get_question_structure - * @covers ::get_question_structure_data * @covers ::get_always_latest_version_question_ids - * @covers ::question_load_random_questions */ public function test_get_question_structure() { $this->resetAfterTest(); + + // Create a quiz. $quiz = $this->create_test_quiz($this->course); - // Test for questions from a different context. - $context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id); - // Create a couple of questions. + $quizcontext = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id); + + // Create a question in the quiz question bank. + /** @var \core_question_generator $questiongenerator */ $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); - $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); - $numq = $questiongenerator->create_question('essay', null, + $cat = $questiongenerator->create_question_category(['contextid' => $quizcontext->id]); + $q = $questiongenerator->create_question('essay', null, ['category' => $cat->id, 'name' => 'This is the first version']); - // Create two version. - $questiongenerator->update_question($numq, null, ['name' => 'This is the second version']); - $questiongenerator->update_question($numq, null, ['name' => 'This is the third version']); - quiz_add_quiz_question($numq->id, $quiz); - // Create the quiz object. + + // Edit it to create a second and third version. + $questiongenerator->update_question($q, null, ['name' => 'This is the second version']); + $finalq = $questiongenerator->update_question($q, null, ['name' => 'This is the third version']); + + // Add the question to the quiz. + quiz_add_quiz_question($q->id, $quiz); + + // Load the quiz object and check. $quizobj = \quiz::create($quiz->id); $quizobj->preload_questions(); $quizobj->load_questions(); $questions = $quizobj->get_questions(); $question = reset($questions); - $structure = \mod_quiz\structure::create_for_quiz($quizobj); + $this->assertEquals($finalq->id, $question->id); + + $structure = structure::create_for_quiz($quizobj); $slots = $structure->get_slots(); $slot = reset($slots); - $structuredatas = qbank_helper::get_question_structure($quiz->id); - $structuredata = reset($structuredatas); - $this->assertEquals($structuredata->slotid, $slot->id); - $this->assertEquals($structuredata->id, $question->id); + $this->assertEquals($finalq->id, $slot->questionid); } - - /** - * Test to get the version information for a question to show in the version selection dropdown. - * - * @covers ::get_question_version_info - */ - public function test_get_question_version_info() { - $this->resetAfterTest(); - $quiz = $this->create_test_quiz($this->course); - // Test for questions from a different context. - $context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id); - // Create a couple of questions. - $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); - $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); - $numq = $questiongenerator->create_question('essay', null, - ['category' => $cat->id, 'name' => 'This is the first version']); - // Create two version. - $questiongenerator->update_question($numq, null, ['name' => 'This is the second version']); - $questiongenerator->update_question($numq, null, ['name' => 'This is the third version']); - quiz_add_quiz_question($numq->id, $quiz); - // Create the quiz object. - $quizobj = \quiz::create($quiz->id); - $quizobj->preload_questions(); - $quizobj->load_questions(); - $questions = $quizobj->get_questions(); - $question = reset($questions); - $structure = \mod_quiz\structure::create_for_quiz($quizobj); - $slots = $structure->get_slots(); - $slot = reset($slots); - $versiondata = qbank_helper::get_question_version_info($question->id, $slot->id); - $this->assertEquals(4, count($versiondata)); - $this->assertEquals('Always latest', $versiondata[0]->versionvalue); - $this->assertEquals('v3 (latest)', $versiondata[1]->versionvalue); - $this->assertEquals('v1', $versiondata[3]->versionvalue); - } - - /** - * Test get the question ids for specific question version. - * - * @covers ::get_specific_version_question_ids - */ - public function test_get_specific_version_question_ids() { - global $DB; - $this->resetAfterTest(); - $quiz = $this->create_test_quiz($this->course); - // Test for questions from a different context. - $context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id); - // Create a couple of questions. - $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); - $cat = $questiongenerator->create_question_category(['contextid' => $context->id]); - $numq = $questiongenerator->create_question('essay', null, - ['category' => $cat->id, 'name' => 'This is the first version']); - // Create two version. - $questiongenerator->update_question($numq, null, ['name' => 'This is the second version']); - $questiongenerator->update_question($numq, null, ['name' => 'This is the third version']); - quiz_add_quiz_question($numq->id, $quiz); - submit_question_version::execute($DB->get_field('quiz_slots', 'id', ['quizid' => $quiz->id, 'slot' => 1]), 3); - $specificversionquestionid = qbank_helper::get_specific_version_question_ids($quiz->id); - $specificversionquestionid = reset($specificversionquestionid); - $this->assertEquals($numq->id, $specificversionquestionid); - } - } diff --git a/mod/quiz/tests/quiz_question_restore_test.php b/mod/quiz/tests/quiz_question_restore_test.php index 257c2cb627c..1758749b8cd 100644 --- a/mod/quiz/tests/quiz_question_restore_test.php +++ b/mod/quiz/tests/quiz_question_restore_test.php @@ -61,23 +61,37 @@ class quiz_question_restore_test extends \advanced_testcase { * @covers ::get_question_structure */ public function test_quiz_restore_in_a_different_course_using_course_question_bank() { - global $DB; $this->resetAfterTest(); + + // Create the test quiz. $quiz = $this->create_test_quiz($this->course); + $oldquizcontext = \context_module::instance($quiz->cmid); // Test for questions from a different context. - $context = \context_course::instance($this->course->id); + $coursecontext = \context_course::instance($this->course->id); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); - $this->add_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]); - $this->add_random_questions($questiongenerator, $quiz, ['contextid' => $context->id]); + $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $coursecontext->id]); + $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $coursecontext->id]); + + // Make the backup. $backupid = $this->backup_quiz($quiz, $this->user); + // Delete the current course to make sure there is no data. delete_course($this->course, false); - // Check if the questions and associated datas are deleted properly. - $this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($quiz->id))); + + // Check if the questions and associated data are deleted properly. + $this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure( + $quiz->id, $oldquizcontext))); + + /// Restore the course. $newcourse = $this->getDataGenerator()->create_course(); $this->restore_quiz($backupid, $newcourse, $this->user); - $module = $DB->get_record('quiz', ['course' => $newcourse->id]); - $this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($module->id))); + + // Verify. + $modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz'); + $module = reset($modules); + $questions = \mod_quiz\question\bank\qbank_helper::get_question_structure( + $module->instance, $module->context); + $this->assertCount(3, $questions); } /** @@ -103,11 +117,18 @@ class quiz_question_restore_test extends \advanced_testcase { delete_course($this->course, false); // Check if the questions and associated datas are deleted properly. - $this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($quiz->id))); + $this->assertEquals(0, count(\mod_quiz\question\bank\qbank_helper::get_question_structure( + $quiz->id, $quizcontext))); + + /// Restore the course. $newcourse = $this->getDataGenerator()->create_course(); $this->restore_quiz($backupid, $newcourse, $this->user); - $module = $DB->get_record('quiz', ['course' => $newcourse->id]); - $this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($module->id))); + + // Verify. + $modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz'); + $module = reset($modules); + $this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure( + $module->instance, $module->context))); } /** @@ -179,28 +200,37 @@ class quiz_question_restore_test extends \advanced_testcase { * @covers ::get_question_structure */ public function test_quiz_restore_with_attempts() { - global $DB; $this->resetAfterTest(); + + // Create a quiz. $quiz = $this->create_test_quiz($this->course); - // Test for questions from a different context. - $context = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id); + $quizcontext = \context_module::instance($quiz->cmid); $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); - $this->add_regular_questions($questiongenerator, $quiz, ['contextid' => $context->id]); - $this->add_random_questions($questiongenerator, $quiz, ['contextid' => $context->id]); - list($quizobj, $quba, $attemptobj) = $this->attempt_quiz($quiz, $this->student); - $userattempts = quiz_get_user_attempts($quiz->id, $this->student->id); - // Count the attempts for this quiz. + $this->add_two_regular_questions($questiongenerator, $quiz, ['contextid' => $quizcontext->id]); + $this->add_one_random_question($questiongenerator, $quiz, ['contextid' => $quizcontext->id]); + + // Attempt it as a student, and check. + /** @var \question_usage_by_activity $quba */ + [, $quba] = $this->attempt_quiz($quiz, $this->student); $this->assertEquals(3, $quba->question_count()); - $this->assertEquals(1, count($userattempts)); + $this->assertCount(1, quiz_get_user_attempts($quiz->id, $this->student->id)); + + // Make the backup. $backupid = $this->backup_quiz($quiz, $this->user); + // Delete the current course to make sure there is no data. delete_course($this->course, false); + + // Restore the backup. $newcourse = $this->getDataGenerator()->create_course(); $this->restore_quiz($backupid, $newcourse, $this->user); - $module = $DB->get_record('quiz', ['course' => $newcourse->id]); - $userattempts = quiz_get_user_attempts($module->id, $this->student->id); - $this->assertEquals(1, count($userattempts)); - $this->assertEquals(3, count(\mod_quiz\question\bank\qbank_helper::get_question_structure($module->id))); + + // Verify. + $modules = get_fast_modinfo($newcourse->id)->get_instances_of('quiz'); + $module = reset($modules); + $this->assertCount(1, quiz_get_user_attempts($module->instance, $this->student->id)); + $this->assertCount(3, \mod_quiz\question\bank\qbank_helper::get_question_structure( + $module->instance, $module->context)); } /** diff --git a/mod/quiz/tests/structure_test.php b/mod/quiz/tests/structure_test.php index 00767a55a7f..8c1b180bb74 100644 --- a/mod/quiz/tests/structure_test.php +++ b/mod/quiz/tests/structure_test.php @@ -930,4 +930,37 @@ class mod_quiz_structure_testcase extends advanced_testcase { $structure = structure::create_for_quiz($quiz); $this->assertFalse($structure->can_add_random_questions()); } + + /** + * Test to get the version information for a question to show in the version selection dropdown. + * + * @covers ::get_question_version_info + */ + public function test_get_version_choices_for_slot() { + $this->resetAfterTest(); + + $quizobj = $this->create_test_quiz([]); + + // Create a question with two versions. + $questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question'); + $cat = $questiongenerator->create_question_category(['contextid' => $quizobj->get_context()->id]); + $q = $questiongenerator->create_question('essay', null, + ['category' => $cat->id, 'name' => 'This is the first version']); + $questiongenerator->update_question($q, null, ['name' => 'This is the second version']); + $questiongenerator->update_question($q, null, ['name' => 'This is the third version']); + quiz_add_quiz_question($q->id, $quizobj->get_quiz()); + + // Create the quiz object. + $structure = structure::create_for_quiz($quizobj); + $versiondata = $structure->get_version_choices_for_slot(1); + $this->assertEquals(4, count($versiondata)); + $this->assertEquals('Always latest', $versiondata[0]->versionvalue); + $this->assertEquals('v3 (latest)', $versiondata[1]->versionvalue); + $this->assertEquals('v2', $versiondata[2]->versionvalue); + $this->assertEquals('v1', $versiondata[3]->versionvalue); + $this->assertTrue($versiondata[0]->selected); + $this->assertFalse($versiondata[1]->selected); + $this->assertFalse($versiondata[2]->selected); + $this->assertFalse($versiondata[3]->selected); + } } diff --git a/mod/quiz/tests/tags_test.php b/mod/quiz/tests/tags_test.php index ba565cd047f..63f3fae11f9 100644 --- a/mod/quiz/tests/tags_test.php +++ b/mod/quiz/tests/tags_test.php @@ -81,7 +81,7 @@ class mod_quiz_tags_testcase extends advanced_testcase { $this->assertEquals("{$tag2->id},{$tag2->name}", "{$slottags[0]},{$slottags[1]}"); $defaultcategory = question_get_default_category(context_course::instance($newcourseid)->id); - $this->assertEquals($defaultcategory->id, $question->categoryobject->id); + $this->assertEquals($defaultcategory->id, $question->category); $randomincludingsubcategories = $DB->get_record('question_set_references', ['itemid' => reset($slots)->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']); $filtercondition = json_decode($randomincludingsubcategories->filtercondition); @@ -95,7 +95,9 @@ class mod_quiz_tags_testcase extends advanced_testcase { * @return array the tags. */ protected function get_tags_for_slot(int $slotid): array { - $referencedata = \mod_quiz\question\bank\qbank_helper::get_random_question_data_from_slot($slotid); + global $DB; + $referencedata = $DB->get_record('question_set_references', + ['itemid' => $slotid, 'component' => 'mod_quiz', 'questionarea' => 'slot']); if (isset($referencedata->filtercondition)) { $filtercondition = json_decode($referencedata->filtercondition); if (isset($filtercondition->tags)) { diff --git a/mod/quiz/upgrade.txt b/mod/quiz/upgrade.txt index da7c4ebfeae..2419d36bf44 100644 --- a/mod/quiz/upgrade.txt +++ b/mod/quiz/upgrade.txt @@ -17,7 +17,7 @@ This files describes API changes in the quiz code. * The quiz_slots_tags database table has been removed entirely, as has the get_slot_tags_for_slot_id() method from mod/quiz/classes/structure.php and the the locallib.php functions quiz_retrieve_slot_tags and quiz_retrieve_slot_tag_ids. This information is now stored in question_set_references - and can be accessed using qbank_helper::get_random_question_data_from_slot. + and can be accessed in the results of qbank_helper::get_question_structure. === 3.11 ===