MDL-61614 Quiz: Maintain slot tags when editing random questions

This commit is contained in:
Shamim Rezaie 2018-04-09 07:05:53 +10:00
parent d62793fdee
commit 66aa172cbb
4 changed files with 247 additions and 87 deletions

View File

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

View File

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

View File

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

View File

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