MDL-74255 quiz: handle draft question status correctly

The main issue to fix is that questions vesions which should not have
been used (that is, hidden or draft versions) were getting offered
as an option and acutally being used.

As part of this I was able to substantially un-tangle
mod_quiz\question\bank\qbank_helper, which previously was
a mass of functions calling other functions in a complicated way.
Hopefully, it is now a bit easer to understand, and perhaps
less buggy.
This commit is contained in:
Tim Hunt 2022-04-05 16:44:17 +01:00
parent 5fff990e25
commit 839cccead4
14 changed files with 441 additions and 558 deletions

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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.
*

View File

@ -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);
}
}

View File

@ -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"

View File

@ -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(

View File

@ -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);
}
}

View File

@ -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));
}
/**

View File

@ -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);
}
}

View File

@ -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)) {

View File

@ -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 ===