mirror of
https://github.com/moodle/moodle.git
synced 2025-04-22 08:55:15 +02:00
MDL-61614 Quiz: Maintain slot tags when editing random questions
This commit is contained in:
parent
d62793fdee
commit
66aa172cbb
@ -136,7 +136,7 @@ class quiz {
|
||||
public function preload_questions() {
|
||||
$this->questions = question_preload_questions(null,
|
||||
'slot.maxmark, slot.id AS slotid, slot.slot, slot.page,
|
||||
slot.questioncategoryid AS randomfromcategory, slot.tags AS randomfromtags,
|
||||
slot.questioncategoryid AS randomfromcategory,
|
||||
slot.includingsubcategories AS randomincludingsubcategories',
|
||||
'{quiz_slots} slot ON slot.quizid = :quizid AND q.id = slot.questionid',
|
||||
array('quizid' => $this->quiz->id), 'slot.slot');
|
||||
@ -569,7 +569,7 @@ class quiz_attempt {
|
||||
$this->quba = question_engine::load_questions_usage_by_activity($this->attempt->uniqueid);
|
||||
$this->slots = $DB->get_records('quiz_slots',
|
||||
array('quizid' => $this->get_quizid()), 'slot',
|
||||
'slot, requireprevious, questionid, includingsubcategories, tags');
|
||||
'slot, requireprevious, questionid, includingsubcategories');
|
||||
$this->sections = array_values($DB->get_records('quiz_sections',
|
||||
array('quizid' => $this->get_quizid()), 'firstslot'));
|
||||
|
||||
@ -1874,7 +1874,7 @@ class quiz_attempt {
|
||||
if ($questiondata->qtype != 'random') {
|
||||
$newqusetionid = $questiondata->id;
|
||||
} else {
|
||||
$tagids = quiz_extract_random_question_tag_ids($this->slots[$slot]->tags);
|
||||
$tagids = quiz_retrieve_slot_tag_ids($this->slots[$slot]->id);
|
||||
|
||||
$randomloader = new \core_question\bank\random_question_loader($qubaids, array());
|
||||
$newqusetionid = $randomloader->get_next_question_id($questiondata->category,
|
||||
|
@ -78,11 +78,9 @@ $toform = fullclone($question);
|
||||
$toform->category = "{$category->id},{$category->contextid}";
|
||||
$toform->includesubcategories = $slot->includingsubcategories;
|
||||
$toform->fromtags = array();
|
||||
if ($slot->tags) {
|
||||
$tags = quiz_extract_random_question_tags($slot->tags);
|
||||
foreach ($tags as $tag) {
|
||||
$toform->fromtags[] = "{$tag->id},{$tag->name}";
|
||||
}
|
||||
$currentslottags = quiz_retrieve_slot_tags($slot->id);
|
||||
foreach ($currentslottags as $slottag) {
|
||||
$toform->fromtags[] = "{$slottag->tagid},{$slottag->tagname}";
|
||||
}
|
||||
$toform->returnurl = $returnurl;
|
||||
|
||||
@ -117,6 +115,8 @@ if ($mform->is_cancelled()) {
|
||||
$slot->questioncategoryid = $fromform->category;
|
||||
$slot->includingsubcategories = $fromform->includesubcategories;
|
||||
|
||||
$DB->update_record('quiz_slots', $slot);
|
||||
|
||||
$tags = [];
|
||||
foreach ($fromform->fromtags as $tagstring) {
|
||||
list($tagid, $tagname) = explode(',', $tagstring);
|
||||
@ -125,9 +125,39 @@ if ($mform->is_cancelled()) {
|
||||
'name' => $tagname
|
||||
];
|
||||
}
|
||||
$slot->tags = quiz_build_random_question_tag_json($tags);
|
||||
|
||||
$DB->update_record('quiz_slots', $slot);
|
||||
$recordstokeep = [];
|
||||
$recordstoinsert = [];
|
||||
$searchableslottags = array_map(function($slottag) {
|
||||
return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
|
||||
}, $currentslottags);
|
||||
|
||||
foreach ($tags as $tag) {
|
||||
if ($key = array_search(['tagid' => $tag->id, 'tagname' => $tag->name], $searchableslottags)) {
|
||||
// If found, $key would be the id field in the quiz_slot_tags table.
|
||||
// Therefore, there was no need to check !== false here.
|
||||
$recordstokeep[] = $key;
|
||||
} else {
|
||||
$recordstoinsert[] = (object)[
|
||||
'slotid' => $slot->id,
|
||||
'tagid' => $tag->id,
|
||||
'tagname' => $tag->name
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Now, delete the remaining records.
|
||||
if (!empty($recordstokeep)) {
|
||||
list($select, $params) = $DB->get_in_or_equal($recordstokeep, SQL_PARAMS_QM, 'param', false);
|
||||
$DB->delete_records_select('quiz_slot_tags', "id $select", $params);
|
||||
} else {
|
||||
$DB->delete_records('quiz_slot_tags', array('slotid' => $slot->id));
|
||||
}
|
||||
|
||||
// And now, insert the extra records if there is any.
|
||||
if (!empty($recordstoinsert)) {
|
||||
$DB->insert_records('quiz_slot_tags', $recordstoinsert);
|
||||
}
|
||||
|
||||
// Purge this question from the cache.
|
||||
question_bank::notify_question_edited($question->id);
|
||||
|
@ -208,7 +208,7 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
|
||||
continue;
|
||||
}
|
||||
|
||||
$tagids = quiz_extract_random_question_tag_ids($questiondata->randomfromtags);
|
||||
$tagids = quiz_retrieve_slot_tag_ids($questiondata->slotid);
|
||||
|
||||
// Deal with fixed random choices for testing.
|
||||
if (isset($questionids[$quba->next_slot_number()])) {
|
||||
@ -2494,20 +2494,58 @@ function quiz_extract_random_question_tags($tagsjson, $matchbyid = true) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Providing tags data in the JSON format, this function returns tagids.
|
||||
* Retrieves tag information for the given quiz slot.
|
||||
* A quiz slot have some tags if and only if it is representing a random question by tags.
|
||||
*
|
||||
* @param string $tagsjson The JSON string representing an array of tags in the [{"id":tagid,"name":"tagname"}] format.
|
||||
* E.g. [{"id":1,"name":"tag1"},{"id":2,"name":"tag2"}]
|
||||
* Usually equal to the value of the tags field retrieved from the {quiz_slots} table.
|
||||
* @param bool $matchbyid If set to true, then this function relies on the tag ids that are stored in $tagsjson to find tags.
|
||||
* If no tag is found by the tag id or if $matchbyid is set to false, then this function tries to find the tag by its name.
|
||||
* @return int[] List of tag ids.
|
||||
* @param int $slotid The id of the quiz slot.
|
||||
* @return stdClass[] List of quiz_slot_tags records.
|
||||
*/
|
||||
function quiz_extract_random_question_tag_ids($tagsjson, $matchbyid = true) {
|
||||
$tags = quiz_extract_random_question_tags($tagsjson, $matchbyid);
|
||||
function quiz_retrieve_slot_tags($slotid) {
|
||||
global $DB;
|
||||
|
||||
$slottags = $DB->get_records('quiz_slot_tags', ['slotid' => $slotid]);
|
||||
|
||||
$tagsbyid = core_tag_tag::get_bulk(array_filter(array_column($slottags, 'tagid')), 'id, name');
|
||||
|
||||
$tagcollid = core_tag_area::get_collection('core', 'question');
|
||||
$tagsbyname = false; // It will be loaded later if required.
|
||||
|
||||
foreach ($slottags as $slottag) {
|
||||
if (isset($tagsbyid[$slottag->tagid])) {
|
||||
$slottag->tagname = $tagsbyid[$slottag->tagid]->name; // Make sure that we're returning the most updated tag name.
|
||||
} else {
|
||||
if ($tagsbyname === false) {
|
||||
// We were hoping that this query could be avoided, but life showed its other side to us!
|
||||
$tagsbyname = core_tag_tag::get_by_name_bulk($tagcollid, array_column($slottags, 'tagname'), 'id, name');
|
||||
}
|
||||
if (isset($tagsbyname[$slottag->tagname])) {
|
||||
$slottag->tagid = $tagsbyname[$slottag->tagname]->id; // Make sure that we're returning the current tag id
|
||||
// that matches the given tag name.
|
||||
} else {
|
||||
$slottag->tagid = null; // The tag does not exist anymore (neither the tag id nor the tag name
|
||||
// matches an existing tag).
|
||||
// We still need to include this row in the result as some callers might
|
||||
// be interested in these rows. An example is the editing forms that still
|
||||
// need to display tag names even if they don't exist anymore.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $slottags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves tag ids for the given quiz slot.
|
||||
* A quiz slot have some tags if and only if it is representing a random question by tags.
|
||||
*
|
||||
* @param int $slotid The id of the quiz slot.
|
||||
* @return int[]
|
||||
*/
|
||||
function quiz_retrieve_slot_tag_ids($slotid) {
|
||||
$tags = quiz_retrieve_slot_tags($slotid);
|
||||
|
||||
// Only work with tags that exist.
|
||||
return array_filter(array_column($tags, 'id'));
|
||||
return array_filter(array_column($tags, 'tagid'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -568,76 +568,168 @@ class mod_quiz_locallib_testcase extends advanced_testcase {
|
||||
$this->assertEquals($expectedrecords, quiz_extract_random_question_tags($tagjson));
|
||||
}
|
||||
|
||||
public function test_quiz_extract_random_question_tag_ids() {
|
||||
/**
|
||||
* This function creates a quiz with some standard (non-random) and some random questions.
|
||||
* The standard questions are created first and then random questions follow them.
|
||||
* So in a quiz with 3 standard question and 2 random question, the first random question is at slot 4.
|
||||
*
|
||||
* @param int $qnum Number of standard questions that should be created in the quiz.
|
||||
* @param int $randomqnum Number of random questions that should be created in the quiz.
|
||||
* @param array $questiontags Tags to be used for random questions.
|
||||
* This is an array in the following format:
|
||||
* [
|
||||
* 0 => ['foo', 'bar'],
|
||||
* 1 => ['baz', 'qux']
|
||||
* ]
|
||||
* @param string[] $unusedtags Some additional tags to be created.
|
||||
* @return array An array of 2 elements: $quiz and $tagobjects.
|
||||
* $tagobjects is an associative array of all created tag objects with its key being tag names.
|
||||
*/
|
||||
private function setup_quiz_and_tags($qnum, $randomqnum, $questiontags = [], $unusedtags = []) {
|
||||
global $SITE;
|
||||
|
||||
$tagobjects = [];
|
||||
|
||||
// Get all the tags that need to be created.
|
||||
$alltags = [];
|
||||
foreach ($questiontags as $questiontag) {
|
||||
$alltags = array_merge($alltags, $questiontag);
|
||||
}
|
||||
$alltags = array_merge($alltags, $unusedtags);
|
||||
$alltags = array_unique($alltags);
|
||||
|
||||
// Create tags.
|
||||
foreach ($alltags as $tagname) {
|
||||
$tagrecord = array(
|
||||
'isstandard' => 1,
|
||||
'flag' => 0,
|
||||
'rawname' => $tagname,
|
||||
'description' => $tagname . ' desc'
|
||||
);
|
||||
$tagobjects[$tagname] = $this->getDataGenerator()->create_tag($tagrecord);
|
||||
}
|
||||
|
||||
// Create a quiz.
|
||||
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
$quiz = $quizgenerator->create_instance(array('course' => $SITE->id, 'questionsperpage' => 3, 'grade' => 100.0));
|
||||
|
||||
// Create a question category in the system context.
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $questiongenerator->create_question_category();
|
||||
|
||||
// Setup standard questions.
|
||||
for ($i = 0; $i < $qnum; $i++) {
|
||||
$question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
quiz_add_quiz_question($question->id, $quiz);
|
||||
}
|
||||
// Setup random questions.
|
||||
for ($i = 0; $i < $randomqnum; $i++) {
|
||||
// Just create a standard question first, so there would be enough questions to pick a random question from.
|
||||
$question = $questiongenerator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$tagids = [];
|
||||
if (!empty($questiontags[$i])) {
|
||||
foreach ($questiontags[$i] as $tagname) {
|
||||
$tagids[] = $tagobjects[$tagname]->id;
|
||||
}
|
||||
}
|
||||
quiz_add_random_questions($quiz, 0, $cat->id, 1, false, $tagids);
|
||||
}
|
||||
|
||||
return array($quiz, $tagobjects);
|
||||
}
|
||||
|
||||
public function test_quiz_retrieve_slot_tags() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
// Setup test data.
|
||||
$footagrecord = array(
|
||||
'isstandard' => 1,
|
||||
'flag' => 0,
|
||||
'rawname' => 'foo',
|
||||
'description' => 'foo desc'
|
||||
);
|
||||
$footag = $this->getDataGenerator()->create_tag($footagrecord);
|
||||
$bartagrecord = array(
|
||||
'isstandard' => 1,
|
||||
'flag' => 0,
|
||||
'rawname' => 'bar',
|
||||
'description' => 'bar desc'
|
||||
);
|
||||
$bartag = $this->getDataGenerator()->create_tag($bartagrecord);
|
||||
$baztagrecord = array(
|
||||
'isstandard' => 1,
|
||||
'flag' => 0,
|
||||
'rawname' => 'baz',
|
||||
'description' => 'baz desc'
|
||||
);
|
||||
$baztag = $this->getDataGenerator()->create_tag($baztagrecord);
|
||||
$quxtagrecord = array(
|
||||
'isstandard' => 1,
|
||||
'flag' => 0,
|
||||
'rawname' => 'qux',
|
||||
'description' => 'qux desc'
|
||||
);
|
||||
$quxtag = $this->getDataGenerator()->create_tag($quxtagrecord);
|
||||
$quuxtagrecord = array(
|
||||
'isstandard' => 1,
|
||||
'flag' => 0,
|
||||
'rawname' => 'quux',
|
||||
'description' => 'quux desc'
|
||||
);
|
||||
$quuxtag = $this->getDataGenerator()->create_tag($quuxtagrecord);
|
||||
list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
|
||||
|
||||
$tagjson = json_encode(array(
|
||||
[
|
||||
'id' => $footag->id,
|
||||
'name' => 'foo'
|
||||
],
|
||||
[
|
||||
'id' => 999, // An invalid tag id.
|
||||
'name' => 'bar'
|
||||
],
|
||||
[
|
||||
'id' => null,
|
||||
'name' => 'baz'
|
||||
],
|
||||
[
|
||||
'id' => $quxtag->id,
|
||||
'name' => 'invalidqux' // An invalid tag name.
|
||||
],
|
||||
[
|
||||
'id' => 999, // An invalid tag id.
|
||||
'name' => 'invalidquux' // An invalid tag name.
|
||||
],
|
||||
));
|
||||
// Get the random question's slotid. It is at the second slot.
|
||||
$slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 2));
|
||||
$slottags = quiz_retrieve_slot_tags($slotid);
|
||||
|
||||
$expectedrecords = array(
|
||||
$footag->id,
|
||||
$bartag->id,
|
||||
$baztag->id,
|
||||
$quxtag->id,
|
||||
);
|
||||
$this->assertEquals(
|
||||
[
|
||||
['tagid' => $tags['foo']->id, 'tagname' => $tags['foo']->name],
|
||||
['tagid' => $tags['bar']->id, 'tagname' => $tags['bar']->name]
|
||||
],
|
||||
array_map(function($slottag) {
|
||||
return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
|
||||
}, $slottags),
|
||||
'', 0.0, 10, true);
|
||||
}
|
||||
|
||||
$this->assertEquals($expectedrecords, quiz_extract_random_question_tag_ids($tagjson));
|
||||
public function test_quiz_retrieve_slot_tags_with_removed_tag() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
|
||||
|
||||
// Get the random question's slotid. It is at the second slot.
|
||||
$slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 2));
|
||||
$slottags = quiz_retrieve_slot_tags($slotid);
|
||||
|
||||
// Now remove the foo tag and check again.
|
||||
core_tag_tag::delete_tags([$tags['foo']->id]);
|
||||
$slottags = quiz_retrieve_slot_tags($slotid);
|
||||
|
||||
$this->assertEquals(
|
||||
[
|
||||
['tagid' => null, 'tagname' => $tags['foo']->name],
|
||||
['tagid' => $tags['bar']->id, 'tagname' => $tags['bar']->name]
|
||||
],
|
||||
array_map(function($slottag) {
|
||||
return ['tagid' => $slottag->tagid, 'tagname' => $slottag->tagname];
|
||||
}, $slottags),
|
||||
'', 0.0, 10, true);
|
||||
}
|
||||
|
||||
public function test_quiz_retrieve_slot_tags_for_standard_question() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']]);
|
||||
|
||||
// Get the standard question's slotid. It is at the first slot.
|
||||
$slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 1));
|
||||
|
||||
// There should be no slot tags for a non-random question.
|
||||
$this->assertCount(0, quiz_retrieve_slot_tags($slotid));
|
||||
}
|
||||
|
||||
public function test_quiz_retrieve_slot_tag_ids() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
|
||||
|
||||
// Get the random question's slotid. It is at the second slot.
|
||||
$slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 2));
|
||||
$tagids = quiz_retrieve_slot_tag_ids($slotid);
|
||||
|
||||
$this->assertEquals([$tags['foo']->id, $tags['bar']->id], $tagids, '', 0.0, 10, true);
|
||||
}
|
||||
|
||||
public function test_quiz_retrieve_slot_tag_ids_for_standard_question() {
|
||||
global $DB;
|
||||
|
||||
$this->resetAfterTest();
|
||||
$this->setAdminUser();
|
||||
|
||||
list($quiz, $tags) = $this->setup_quiz_and_tags(1, 1, [['foo', 'bar']], ['baz']);
|
||||
|
||||
// Get the standard question's slotid. It is at the first slot.
|
||||
$slotid = $DB->get_field('quiz_slots', 'id', array('quizid' => $quiz->id, 'slot' => 1));
|
||||
$tagids = quiz_retrieve_slot_tag_ids($slotid);
|
||||
|
||||
$this->assertEquals([], $tagids, '', 0.0, 10, true);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user