Merge branch 'master_MDL-71696-versioning-integration' of https://github.com/catalyst/moodle-MDL-70329

This commit is contained in:
Sara Arjona 2022-02-03 13:25:27 +01:00
commit b841a811be
223 changed files with 7768 additions and 2899 deletions

View file

@ -1402,10 +1402,9 @@
<INDEX NAME="contextididnumber" UNIQUE="true" FIELDS="contextid, idnumber"/>
</INDEXES>
</TABLE>
<TABLE NAME="question" COMMENT="The questions themselves">
<TABLE NAME="question" COMMENT="This table stores the definition of one version of a question.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="category" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="parent" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="name" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="questiontext" TYPE="text" NOTNULL="true" SEQUENCE="false"/>
@ -1417,26 +1416,83 @@
<FIELD NAME="qtype" TYPE="char" LENGTH="20" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="length" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="1" SEQUENCE="false"/>
<FIELD NAME="stamp" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="version" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="hidden" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="time question was created"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="time that question was last modified"/>
<FIELD NAME="createdby" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="userid of person who created this question"/>
<FIELD NAME="modifiedby" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="userid of person who last edited this question"/>
<FIELD NAME="idnumber" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="category" TYPE="foreign" FIELDS="category" REFTABLE="question_categories" REFFIELDS="id"/>
<KEY NAME="parent" TYPE="foreign" FIELDS="parent" REFTABLE="question" REFFIELDS="id" COMMENT="note that to make this recursive FK working someday, the parent field must be declared NULL"/>
<KEY NAME="createdby" TYPE="foreign" FIELDS="createdby" REFTABLE="user" REFFIELDS="id" COMMENT="foreign (createdby) references user (id)"/>
<KEY NAME="modifiedby" TYPE="foreign" FIELDS="modifiedby" REFTABLE="user" REFFIELDS="id" COMMENT="foreign (modifiedby) references user (id)"/>
</KEYS>
<INDEXES>
<INDEX NAME="qtype" UNIQUE="false" FIELDS="qtype"/>
<INDEX NAME="categoryidnumber" UNIQUE="true" FIELDS="category, idnumber"/>
</INDEXES>
</TABLE>
<TABLE NAME="question_bank_entries" COMMENT="Each question bank entry. This table has one row for each question that appears in the question bank.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="questioncategoryid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="ID of the category this question is part of."/>
<FIELD NAME="idnumber" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="Unique identifier, useful especially for mapping to external entities."/>
<FIELD NAME="ownerid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="userid of person who owns this question bank entry."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="questioncategoryid" TYPE="foreign" FIELDS="questioncategoryid" REFTABLE="question_categories" REFFIELDS="id"/>
<KEY NAME="ownerid" TYPE="foreign" FIELDS="ownerid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
<INDEXES>
<INDEX NAME="categoryidnumber" UNIQUE="true" FIELDS="questioncategoryid, idnumber"/>
</INDEXES>
</TABLE>
<TABLE NAME="question_versions" COMMENT="A join table linking the different question version definitions in the question table to the question_bank_entires.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="questionbankentryid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="ID of the question bank entry this question version is part of."/>
<FIELD NAME="version" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Version number for the question where the first version is always 1."/>
<FIELD NAME="questionid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The question ID."/>
<FIELD NAME="status" TYPE="char" LENGTH="10" NOTNULL="false" DEFAULT="ready" SEQUENCE="false" COMMENT="If the question is ready, hidden or draft"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="questionbankentryid" TYPE="foreign" FIELDS="questionbankentryid" REFTABLE="question_bank_entries" REFFIELDS="id"/>
<KEY NAME="questionid" TYPE="foreign" FIELDS="questionid" REFTABLE="question" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="question_references" COMMENT="Records where a specific question is used.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="usingcontextid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Context where question is used."/>
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="Component (e.g. mod_quiz or core_question)"/>
<FIELD NAME="questionarea" TYPE="char" LENGTH="50" NOTNULL="false" SEQUENCE="false" COMMENT="Depending on the component, which area the question is used in (e.g. slot for quiz)."/>
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Plugin specific id (e.g. slotid for quiz) where its used."/>
<FIELD NAME="questionbankentryid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="ID of the question bank entry this question is part of."/>
<FIELD NAME="version" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Version number for the question where NULL means use the latest ready version."/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="usingcontextid" TYPE="foreign" FIELDS="usingcontextid" REFTABLE="context" REFFIELDS="id"/>
<KEY NAME="questionbankentryid" TYPE="foreign" FIELDS="questionbankentryid" REFTABLE="question_bank_entries" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="question_set_references" COMMENT="Records where groups of questions are used.">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="usingcontextid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Context where question is used."/>
<FIELD NAME="component" TYPE="char" LENGTH="100" NOTNULL="false" SEQUENCE="false" COMMENT="Component (e.g. mod_quiz)"/>
<FIELD NAME="questionarea" TYPE="char" LENGTH="50" NOTNULL="false" SEQUENCE="false" COMMENT="Depending on the component, which area the question is used in (e.g. slot for quiz)."/>
<FIELD NAME="itemid" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Plugin specific id (e.g. slotid for quiz) where its used."/>
<FIELD NAME="questionscontextid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Context questions come from."/>
<FIELD NAME="filtercondition" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Filter expression in json format"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="usingcontextid" TYPE="foreign" FIELDS="usingcontextid" REFTABLE="context" REFFIELDS="id"/>
<KEY NAME="questionscontextid" TYPE="foreign" FIELDS="questionscontextid" REFTABLE="context" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="question_answers" COMMENT="Answers, with a fractional grade (0-1) and feedback">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>

View file

@ -61,7 +61,6 @@ $renamedclasses = [
'core_question\\bank\\copy_action_column' => 'qbank_editquestion\\copy_action_column',
'core_question\\bank\\edit_action_column' => 'qbank_editquestion\\edit_action_column',
'core_question\\bank\\creator_name_column' => 'qbank_viewcreator\\creator_name_column',
'core_question\\bank\\modifier_name_column' => 'qbank_viewcreator\\modifier_name_column',
'core_question\\bank\\question_name_column' => 'qbank_viewquestionname\\viewquestionname_column_helper',
'core_question\\bank\\question_name_idnumber_tags_column' => 'qbank_viewquestionname\\question_name_idnumber_tags_column',
'core_question\\bank\\delete_action_column' => 'qbank_deletequestion\\delete_action_column',
@ -81,5 +80,7 @@ $renamedclasses = [
'export_form' => 'qbank_exportquestions\\form\\export_form',
'preview_options_form' => 'qbank_previewquestion\\form\\preview_options_form',
'question_preview_options' => 'qbank_previewquestion\\output\\question_preview_options',
'core_question\\form\\tags' => '\qbank_tagquestion\\form\\tags_form'
'core_question\\form\\tags' => 'qbank_tagquestion\\form\\tags_form',
'context_to_string_translator' => 'core_question\\local\\bank\\context_to_string_translator',
'question_edit_contexts' => 'core_question\\local\\bank\\question_edit_contexts',
];

View file

@ -3809,5 +3809,158 @@ privatefiles,moodle|/user/files.php';
upgrade_main_savepoint(true, 2022012100.02);
}
// Introduce question versioning to core.
// First, create the new tables.
if ($oldversion < 2022020200.01) {
// Define table question_bank_entries to be created.
$table = new xmldb_table('question_bank_entries');
// Adding fields to table question_bank_entries.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('questioncategoryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
$table->add_field('idnumber', XMLDB_TYPE_CHAR, '100', null, null, null, null);
$table->add_field('ownerid', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
// Adding keys to table question_bank_entries.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('questioncategoryid', XMLDB_KEY_FOREIGN, ['questioncategoryid'], 'question_categories', ['id']);
$table->add_key('ownerid', XMLDB_KEY_FOREIGN, ['ownerid'], 'user', ['id']);
// Conditionally launch create table for question_bank_entries.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Create category id and id number index.
$index = new xmldb_index('categoryidnumber', XMLDB_INDEX_UNIQUE, ['questioncategoryid', 'idnumber']);
// Conditionally launch add index categoryidnumber.
if (!$dbman->index_exists($table, $index)) {
$dbman->add_index($table, $index);
}
// Define table question_versions to be created.
$table = new xmldb_table('question_versions');
// Adding fields to table question_versions.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('questionbankentryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('version', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 1);
$table->add_field('questionid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
$table->add_field('status', XMLDB_TYPE_CHAR, '10', null, XMLDB_NOTNULL, null, 'ready');
// Adding keys to table question_versions.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('questionbankentryid', XMLDB_KEY_FOREIGN, ['questionbankentryid'], 'question_bank_entries', ['id']);
$table->add_key('questionid', XMLDB_KEY_FOREIGN, ['questionid'], 'question', ['id']);
// Conditionally launch create table for question_versions.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Define table question_references to be created.
$table = new xmldb_table('question_references');
// Adding fields to table question_references.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('usingcontextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
$table->add_field('component', XMLDB_TYPE_CHAR, '100', null, null, null, null);
$table->add_field('questionarea', XMLDB_TYPE_CHAR, '50', null, null, null, null);
$table->add_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('questionbankentryid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('version', XMLDB_TYPE_INTEGER, '10', null, null, null, null);
// Adding keys to table question_references.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('usingcontextid', XMLDB_KEY_FOREIGN, ['usingcontextid'], 'context', ['id']);
$table->add_key('questionbankentryid', XMLDB_KEY_FOREIGN, ['questionbankentryid'], 'question_bank_entries', ['id']);
// Conditionally launch create table for question_references.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Define table question_set_references to be created.
$table = new xmldb_table('question_set_references');
// Adding fields to table question_set_references.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('usingcontextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
$table->add_field('component', XMLDB_TYPE_CHAR, '100', null, null, null, null);
$table->add_field('questionarea', XMLDB_TYPE_CHAR, '50', null, null, null, null);
$table->add_field('itemid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('questionscontextid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, 0);
$table->add_field('filtercondition', XMLDB_TYPE_TEXT, null, null, null, null, null);
// Adding keys to table question_set_references.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('usingcontextid', XMLDB_KEY_FOREIGN, ['usingcontextid'], 'context', ['id']);
$table->add_key('itemid', XMLDB_KEY_FOREIGN, ['itemid'], 'quiz_slots', ['id']);
$table->add_key('questionscontextid', XMLDB_KEY_FOREIGN, ['questionscontextid'], 'context', ['id']);
// Conditionally launch create table for question_set_references.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2022020200.01);
}
if ($oldversion < 2022020200.02) {
// Next, split question records into the new tables.
upgrade_migrate_question_table();
// Main savepoint reached.
upgrade_main_savepoint(true, 2022020200.02);
}
// Finally, drop fields from question table.
if ($oldversion < 2022020200.03) {
// Define fields to be dropped from questions.
$table = new xmldb_table('question');
$field = new xmldb_field('version');
// Conditionally launch drop field version.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
$field = new xmldb_field('hidden');
// Conditionally launch drop field hidden.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
// Define index categoryidnumber (not unique) to be dropped form question.
$index = new xmldb_index('categoryidnumber', XMLDB_INDEX_UNIQUE, ['category', 'idnumber']);
// Conditionally launch drop index categoryidnumber.
if ($dbman->index_exists($table, $index)) {
$dbman->drop_index($table, $index);
}
// Define key category (foreign) to be dropped form questions.
$key = new xmldb_key('category', XMLDB_KEY_FOREIGN, ['category'], 'question_categories', ['id']);
// Launch drop key category.
$dbman->drop_key($table, $key);
$field = new xmldb_field('idnumber');
// Conditionally launch drop field idnumber.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
$field = new xmldb_field('category');
// Conditionally launch drop field category.
if ($dbman->field_exists($table, $field)) {
$dbman->drop_field($table, $field);
}
// Main savepoint reached.
upgrade_main_savepoint(true, 2022020200.03);
}
return true;
}

View file

@ -1274,3 +1274,137 @@ function upgrade_calendar_override_events_fix(stdClass $info, bool $output = tru
upgrade_calendar_events_mtrace('', $output);
return $return;
}
/**
* Split question table in 2 new tables:
*
* question_bank_entries
* question_versions
*
* Move the random questions records to the following table:
* question_set_reference
*
* Move the question related records from quiz_slots table to:
* question_reference
*
* Move the tag related data from quiz_slot_tags to:
* question_references
*
* For more information: https://moodle.org/mod/forum/discuss.php?d=417599#p1688163
*/
function upgrade_migrate_question_table(): void {
global $DB;
// Maximum size of array.
$maxlength = 30000;
// Array of question_versions objects.
$questionversions = [];
// Array of question_set_references objects.
$questionsetreferences = [];
// The actual update/insert done with multiple DB access, so we do it in a transaction.
$transaction = $DB->start_delegated_transaction();
// Count all questions to be migrated (for progress bar).
$total = $DB->count_records('question');
$pbar = new progress_bar('migratequestions', 1000, true);
$i = 0;
// Get all records in question table, we dont need the subquestions, just regular questions and random questions.
$questions = $DB->get_recordset('question');
foreach ($questions as $question) {
upgrade_set_timeout(60);
// Populate table question_bank_entries.
$questionbankentry = new \stdClass();
$questionbankentry->questioncategoryid = $question->category;
$questionbankentry->idnumber = $question->idnumber;
$questionbankentry->ownerid = $question->createdby;
// Insert a question_bank_entries record here as the id is required to populate other tables.
$questionbankentry->id = $DB->insert_record('question_bank_entries', $questionbankentry);
// Create question_versions records to be added.
$questionversion = new \stdClass();
$questionversion->questionbankentryid = $questionbankentry->id;
$questionversion->questionid = $question->id;
$questionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
if ((int)$question->hidden === 1) {
$questionstatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_HIDDEN;
}
$questionversion->status = $questionstatus;
$questionversions[] = $questionversion;
// Insert the records if the array limit is reached.
if (count($questionversions) >= $maxlength) {
$DB->insert_records('question_versions', $questionversions);
$questionversions = [];
}
// Create question_set_references records to be added.
// Only if the question type is random and the question is used in a quiz.
if ($question->qtype === 'random') {
$quizslots = $DB->get_records('quiz_slots', ['questionid' => $question->id]);
foreach ($quizslots as $quizslot) {
$questionsetreference = new \stdClass();
$cm = get_coursemodule_from_instance('quiz', $quizslot->quizid);
$questionsetreference->usingcontextid = context_module::instance($cm->id)->id;
$questionsetreference->component = 'mod_quiz';
$questionsetreference->questionarea = 'slot';
$questionsetreference->itemid = $quizslot->id;
$catcontext = $DB->get_field('question_categories', 'contextid', ['id' => $question->category]);
$questionsetreference->questionscontextid = $catcontext;
// Migration of the slot tags and filter identifiers from slot table to filtercondition.
$filtercondition = new stdClass();
$filtercondition->questioncategoryid = $question->category;
$filtercondition->includingsubcategories = $quizslot->includingsubcategories;
$tags = $DB->get_records('quiz_slot_tags', ['slotid' => $quizslot->id]);
$tagstrings = [];
foreach ($tags as $tag) {
$tagstrings [] = "{$tag->id},{$tag->name}";
}
if (!empty($tagstrings)) {
$filtercondition->tags = $tagstrings;
}
$questionsetreference->filtercondition = json_encode($filtercondition);
$questionsetreferences[] = $questionsetreference;
// Insert the records if the array limit is reached.
if (count($questionsetreferences) >= $maxlength) {
$DB->insert_records('question_set_references', $questionsetreferences);
$questionsetreferences = [];
}
}
}
// Update progress.
$i++;
$pbar->update($i, $total, "Migrating questions - $i/$total.");
}
$questions->close();
// Insert the remaining question_versions records.
if ($questionversions) {
$DB->insert_records('question_versions', $questionversions);
}
// Insert the remaining question_set_references records.
if ($questionsetreferences) {
$DB->insert_records('question_set_references', $questionsetreferences);
}
// Create question_references record for each question.
// Except if qtype is random. That case is handled by question_set_reference.
$sql = "INSERT INTO {question_references}
(usingcontextid, component, questionarea, itemid, questionbankentryid)
SELECT c.id, 'mod_quiz', 'slot', qs.id, qv.questionbankentryid
FROM {question} q
JOIN {question_versions} qv ON q.id = qv.questionid
JOIN {quiz_slots} qs ON q.id = qs.questionid
JOIN {modules} m ON m.name = 'quiz'
JOIN {course_modules} cm ON cm.module = m.id AND cm.instance = qs.quizid
JOIN {context} c ON c.instanceid = cm.id AND c.contextlevel = " . CONTEXT_MODULE . "
WHERE q.qtype <> 'random'";
$DB->execute($sql);
$transaction->allow_commit();
}

File diff suppressed because it is too large Load diff

View file

@ -109,6 +109,18 @@ class core_questionlib_testcase extends advanced_testcase {
return array($category, $course, $quiz, $qcat, $questions);
}
/**
* Assert that a category contains a specific number of questions.
*
* @param int $categoryid int Category id.
* @param int $numberofquestions Number of question in a category.
* @return void Questions in a category.
*/
protected function assert_category_contains_questions(int $categoryid, int $numberofquestions): void {
$questionsid = question_bank::get_finder()->get_questions_from_categories([$categoryid], null);
$this->assertEquals($numberofquestions, count($questionsid));
}
public function test_question_reorder_qtypes() {
$this->assertEquals(
array(0 => 't2', 1 => 't1', 2 => 't3'),
@ -222,8 +234,7 @@ class core_questionlib_testcase extends advanced_testcase {
// Create some question categories and questions in this course.
$coursecontext = context_course::instance($course->id);
$questioncat = $questiongenerator->create_question_category(array('contextid' =>
$coursecontext->id));
$questioncat = $questiongenerator->create_question_category(array('contextid' => $coursecontext->id));
$question1 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
$question2 = $questiongenerator->create_question('shortanswer', null, array('category' => $questioncat->id));
@ -256,8 +267,14 @@ class core_questionlib_testcase extends advanced_testcase {
array(context_course::instance($course2->id)->id), '*', MUST_EXIST);
// Check that there are two questions in the restored to course's context.
$this->assertEquals(2, $DB->count_records('question', array('category' => $restoredcategory->id)));
$this->assertEquals(2, $DB->get_record_sql('SELECT COUNT(q.id) as questioncount
FROM {question} q
JOIN {question_versions} qv
ON qv.questionid = q.id
JOIN {question_bank_entries} qbe
ON qbe.id = qv.questionbankentryid
WHERE qbe.questioncategoryid = ?',
[$restoredcategory->id])->questioncount);
$rc->destroy();
}
@ -327,8 +344,7 @@ class core_questionlib_testcase extends advanced_testcase {
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
// Verify questions deleted or moved.
$criteria = array('category' => $qcat->id);
$this->assertEquals(0, $DB->count_records('question', $criteria));
$this->assert_category_contains_questions($qcat->id, 0);
// Verify question not deleted.
$criteria = array('id' => $questions[0]->id);
@ -355,8 +371,7 @@ class core_questionlib_testcase extends advanced_testcase {
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
// Verify questions deleted or moved.
$criteria = array('category' => $qcat->id);
$this->assertEquals(0, $DB->count_records('question', $criteria));
$this->assert_category_contains_questions($qcat->id, 0);
}
/**
@ -377,8 +392,7 @@ class core_questionlib_testcase extends advanced_testcase {
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
// Verify questions deleted or moved.
$criteria = array('category' => $qcat->id);
$this->assertEquals(0, $DB->count_records('question', $criteria));
$this->assert_category_contains_questions($qcat->id, 0);
}
/**
@ -399,8 +413,7 @@ class core_questionlib_testcase extends advanced_testcase {
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
// Verify questions deleted or moved.
$criteria = array('category' => $qcat->id);
$this->assertEquals(0, $DB->count_records('question', $criteria));
$this->assert_category_contains_questions($qcat->id, 0);
}
/**
@ -421,8 +434,7 @@ class core_questionlib_testcase extends advanced_testcase {
$this->assertEquals(0, $DB->count_records('question_categories', $criteria));
// Verify questions deleted or moved.
$criteria = array('category' => $qcat->id);
$this->assertEquals(0, $DB->count_records('question', $criteria));
$this->assert_category_contains_questions($qcat->id, 0);
}
/**
@ -449,9 +461,11 @@ class core_questionlib_testcase extends advanced_testcase {
// Verify questions are moved.
$params = array($qcat2->contextid);
$actualquestionscount = $DB->count_records_sql("SELECT COUNT(*)
FROM {question} q
JOIN {question_categories} qc ON q.category = qc.id
WHERE qc.contextid = ?", $params);
FROM {question} q
JOIN {question_versions} qv ON qv.questionid = q.id
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
WHERE qc.contextid = ?", $params);
$this->assertEquals($questionsinqcat1 + $questionsinqcat2, $actualquestionscount);
// Verify there is just a single top-level category.
@ -1728,16 +1742,11 @@ class core_questionlib_testcase extends advanced_testcase {
$qtype = 'truefalse';
$overrides = [
'category' => $questioncat->id,
'createdby' => ($isowner) ? $user->id : $otheruser->id,
];
$question = $questiongenerator->create_question($qtype, null, $overrides);
// The question generator does not support setting of the createdby for some reason.
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
$fromform = test_question_maker::get_question_form_data($qtype, null);
$fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
question_bank::get_qtype($qtype)->save_question($question, $fromform);
$this->setUser($user);
$result = question_has_capability_on($question, $capability);
$this->assertEquals($expect, $result);
@ -1779,16 +1788,11 @@ class core_questionlib_testcase extends advanced_testcase {
$qtype = 'truefalse';
$overrides = [
'category' => $questioncat->id,
'createdby' => ($isowner) ? $user->id : $otheruser->id,
];
$question = $questiongenerator->create_question($qtype, null, $overrides);
// The question generator does not support setting of the createdby for some reason.
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
$fromform = test_question_maker::get_question_form_data($qtype, null);
$fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
question_bank::get_qtype($qtype)->save_question($question, $fromform);
$this->setUser($user);
$result = question_has_capability_on($question->id, $capability);
$this->assertEquals($expect, $result);
@ -1830,16 +1834,11 @@ class core_questionlib_testcase extends advanced_testcase {
$qtype = 'truefalse';
$overrides = [
'category' => $questioncat->id,
'createdby' => ($isowner) ? $user->id : $otheruser->id,
];
$question = $questiongenerator->create_question($qtype, null, $overrides);
// The question generator does not support setting of the createdby for some reason.
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
$fromform = test_question_maker::get_question_form_data($qtype, null);
$fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
question_bank::get_qtype($qtype)->save_question($question, $fromform);
$this->setUser($user);
$result = question_has_capability_on((string) $question->id, $capability);
$this->assertEquals($expect, $result);
@ -1887,16 +1886,11 @@ class core_questionlib_testcase extends advanced_testcase {
$qtype = 'truefalse';
$overrides = [
'category' => $questioncat->id,
'createdby' => ($isowner) ? $user->id : $otheruser->id,
];
$question = $questiongenerator->create_question($qtype, null, $overrides);
// The question generator does not support setting of the createdby for some reason.
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
$fromform = test_question_maker::get_question_form_data($qtype, null);
$fromform = (object) $generator->combine_defaults_and_record((array) $fromform, $overrides);
question_bank::get_qtype($qtype)->save_question($question, $fromform);
// Move the question.
question_move_questions_to_category([$question->id], $newquestioncat->id);
@ -1941,12 +1935,10 @@ class core_questionlib_testcase extends advanced_testcase {
// Create the question.
$question = $questiongenerator->create_question('truefalse', null, [
'category' => $questioncat->id,
'createdby' => ($isowner) ? $user->id : $otheruser->id,
]);
$question = question_bank::load_question_data($question->id);
// The question generator does not support setting of the createdby for some reason.
$question->createdby = ($isowner) ? $user->id : $otheruser->id;
$this->setUser($user);
$result = question_has_capability_on($question, $capability);
$this->assertEquals($expect, $result);
@ -1970,12 +1962,10 @@ class core_questionlib_testcase extends advanced_testcase {
// Create the question.
$question = $questiongenerator->create_question('truefalse', null, [
'category' => $questioncat->id,
'createdby' => $user->id,
]);
$question = question_bank::load_question_data($question->id);
// The question generator does not support setting of the createdby for some reason.
$question->createdby = $user->id;
$this->setUser($user);
$result = question_has_capability_on((string)$question->id, 'tag');
$this->assertFalse($result);
@ -2057,4 +2047,218 @@ class core_questionlib_testcase extends advanced_testcase {
$this->assertSame('id11', core_question_find_next_unused_idnumber('id9', $category->id));
$this->assertSame('id11', core_question_find_next_unused_idnumber('id8', $category->id));
}
/**
* Tests for the question_move_questions_to_category function.
*
* @covers ::question_move_questions_to_category
*/
public function test_question_move_questions_to_category() {
$this->resetAfterTest();
// Create the test data.
list($category1, $course1, $quiz1, $questioncat1, $questions1) = $this->setup_quiz_and_questions();
list($category2, $course2, $quiz2, $questioncat2, $questions2) = $this->setup_quiz_and_questions();
$this->assertCount(2, $questions1);
$this->assertCount(2, $questions2);
$questionsidtomove = [];
foreach ($questions1 as $question1) {
$questionsidtomove[] = $question1->id;
}
// Move the question from quiz 1 to quiz 2.
question_move_questions_to_category($questionsidtomove, $questioncat2->id);
$this->assert_category_contains_questions($questioncat2->id, 4);
}
/**
* Tests for the idnumber_exist_in_question_category function.
*
* @covers ::idnumber_exist_in_question_category
*/
public function test_idnumber_exist_in_question_category() {
global $DB;
$this->resetAfterTest();
// Create the test data.
list($category1, $course1, $quiz1, $questioncat1, $questions1) = $this->setup_quiz_and_questions();
list($category2, $course2, $quiz2, $questioncat2, $questions2) = $this->setup_quiz_and_questions();
$questionbankentry1 = get_question_bank_entry($questions1[0]->id);
$entry = new stdClass();
$entry->id = $questionbankentry1->id;
$entry->idnumber = 1;
$DB->update_record('question_bank_entries', $entry);
$questionbankentry2 = get_question_bank_entry($questions2[0]->id);
$entry2 = new stdClass();
$entry2->id = $questionbankentry2->id;
$entry2->idnumber = 1;
$DB->update_record('question_bank_entries', $entry2);
$questionbe = $DB->get_record('question_bank_entries', ['id' => $questionbankentry1->id]);
// Validate that a first stage of an idnumber exists (this format: xxxx_x).
list($response, $record) = idnumber_exist_in_question_category($questionbe->idnumber, $questioncat1->id);
$this->assertEquals([], $record);
$this->assertEquals(true, $response);
// Move the question to a category that has a question with the same idnumber.
question_move_questions_to_category($questions1[0]->id, $questioncat2->id);
// Validate that function return the last record used for the idnumber.
list($response, $record) = idnumber_exist_in_question_category($questionbe->idnumber, $questioncat2->id);
$record = reset($record);
$idnumber = $record->idnumber;
$this->assertEquals($idnumber, '1_1');
$this->assertEquals(true, $response);
}
/**
* Test method is_latest().
*
* @covers ::is_latest
*
*/
public function test_is_latest() {
global $DB;
$this->resetAfterTest();
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
$qcat1 = $generator->create_question_category(['name' => 'My category', 'sortorder' => 1, 'idnumber' => 'myqcat']);
$question = $generator->create_question('shortanswer', null, ['name' => 'q1', 'category' => $qcat1->id]);
$record = $DB->get_record('question_versions', ['questionid' => $question->id]);
$firstversion = $record->version;
$questionbankentryid = $record->questionbankentryid;
$islatest = is_latest($firstversion, $questionbankentryid);
$this->assertTrue($islatest);
}
/**
* Test question bank entry deletion.
*
* @covers ::delete_question_bank_entry
*/
public function test_delete_question_bank_entry() {
global $DB;
$this->resetAfterTest();
// Setup.
$context = context_system::instance();
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
$qcat = $qgen->create_question_category(array('contextid' => $context->id));
$q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
// Make sure there is an entry in the entry table.
$sql = 'SELECT qbe.id as id,
qv.id as versionid
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 = ?';
$records = $DB->get_records_sql($sql, [$q1->id]);
$this->assertCount(1, $records);
// Delete the record.
$record = reset($records);
delete_question_bank_entry($record->id);
$records = $DB->get_records('question_bank_entries', ['id' => $record->id]);
// As the version record exists, it wont delete the data to resolve any errors.
$this->assertCount(1, $records);
$DB->delete_records('question_versions', ['id' => $record->versionid]);
delete_question_bank_entry($record->id);
$records = $DB->get_records('question_bank_entries', ['id' => $record->id]);
$this->assertCount(0, $records);
}
/**
* Test question bank entry object.
*
* @covers ::get_question_bank_entry
*/
public function test_get_question_bank_entry() {
global $DB;
$this->resetAfterTest();
// Setup.
$context = context_system::instance();
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
$qcat = $qgen->create_question_category(array('contextid' => $context->id));
$q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
// Make sure there is an entry in the entry table.
$sql = 'SELECT qbe.id as id,
qv.id as versionid
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 = ?';
$records = $DB->get_records_sql($sql, [$q1->id]);
$this->assertCount(1, $records);
$record = reset($records);
$questionbankentry = get_question_bank_entry($q1->id);
$this->assertEquals($questionbankentry->id, $record->id);
}
/**
* Test the version objects for a question.
*
* @covers ::get_question_version
*/
public function test_get_question_version() {
global $DB;
$this->resetAfterTest();
// Setup.
$context = context_system::instance();
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
$qcat = $qgen->create_question_category(array('contextid' => $context->id));
$q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
// Make sure there is an entry in the entry table.
$sql = 'SELECT qbe.id as id,
qv.id as versionid
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 = ?';
$records = $DB->get_records_sql($sql, [$q1->id]);
$this->assertCount(1, $records);
$record = reset($records);
$questionversions = get_question_version($q1->id);
$questionversion = reset($questionversions);
$this->assertEquals($questionversion->id, $record->versionid);
}
/**
* Test get next version of a question.
*
* @covers ::get_next_version
*/
public function test_get_next_version() {
global $DB;
$this->resetAfterTest();
// Setup.
$context = context_system::instance();
$qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
$qcat = $qgen->create_question_category(array('contextid' => $context->id));
$q1 = $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
// Make sure there is an entry in the entry table.
$sql = 'SELECT qbe.id as id,
qv.id as versionid,
qv.version
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 = ?';
$records = $DB->get_records_sql($sql, [$q1->id]);
$this->assertCount(1, $records);
$record = reset($records);
$this->assertEquals(1, $record->version);
$nextversion = get_next_version($record->id);
$this->assertEquals(2, $nextversion);
}
}

View file

@ -99,6 +99,51 @@ information provided here is intended especially for developers.
- question_preview_popup_params() is moved to \qbank_previewquestion\helper::question_preview_popup_params()
Calling these functions in the question will point to the plugin, but the deprecation message will be activated in MDL-72004.
The deprecated codes are removed from the questionlib for those two methods.
* Function question_hash() from questionlib.php is deprecated without replacement.
* Some of the new and old methods in the questionlib.php now using type hinting. Please make a note of this while making changes
or implementing any question bank related feature in a plugin. These are the list of methods:
- is_latest()
- get_next_version()
- get_question_version()
- get_question_bank_entry()
- core_question_find_next_unused_idnumber()
- question_module_uses_questions()
- question_page_type_list()
- core_question_question_preview_pluginfile()
- question_rewrite_question_preview_urls()
- question_rewrite_question_urls()
- question_get_all_capabilities()
- question_get_question_capabilities()
- question_require_capability_on()
- question_has_capability_on()
- question_default_export_filename()
- get_import_export_formats()
- question_categorylist_parents()
- question_categorylist()
- question_make_default_categories()
- question_get_top_categories_for_contexts()
- sort_categories_by_tree()
- print_question_icon()
- question_sort_tags()
- _tidy_question()
- question_preload_questions()
- question_move_category_to_context()
- move_question_set_references()
- question_move_questions_to_category()
- idnumber_exist_in_question_category()
- question_move_question_tags_to_new_context()
- question_delete_activity()
- question_delete_course_category()
- question_delete_course()
- question_delete_context()
- question_delete_question()
- delete_question_bank_entry()
- question_category_in_use()
- question_category_delete_safe()
- question_context_has_any_questions()
- questions_in_use()
- question_save_qtype_order()
- question_reorder_qtypes()
* The postgres driver now wraps calls to pg_field_type() and caches them in databasemeta to save an invisible internal
DB call on every request.
* The default type of 'core/toast' messages has been changed to 'information' (callers can still explicitely set the type)
@ -116,8 +161,19 @@ completely removed from Moodle core too.
Refer to upgrade.php to see transitioning from similar plugin criteria to core
Refer to completion/upgrade.txt for additional information.
* The method enable_plugin() has been added to the core_plugininfo\base class and it has been implemented by all the plugininfo
classes extending it. When possible, the enable_plugin() method will store these changes into the config_log table, to let admins
check when and who has enabled/disabled plugins.
classes extending it. When possible, the enable_plugin() method will store these changes into the config_log table, to let admins
check when and who has enabled/disabled plugins.
* New tables are included as a part of https://docs.moodle.org/dev/Question_bank_improvements_for_Moodle_4.0
- question_bank_entries -> Each question bank entry. This table has one row for each question that appears in the question bank.
- question_versions -> Versions of the question. Store the data that defines how a particular version of the question works.
- question_references -> Records where a specific question is used.
- question_set_references -> Records where groups of questions are used (e.g.: Random questions).
Also, some tables have been updated or removed:
- question (fields migrated to the new tables)
- quiz_slot (fields removed)
- quiz_slot_tags (table removed)
During the upgrade, data from the question table will be copied to the new tables. After this process,
the data copied will be removed from question table quiz_slot and finally the the quiz_slot_tags table will be removed.
* Final deprecation: The following functions along with associated tests have been removed:
- core_grades_external::get_grades
- core_grades_external::get_grade_item
@ -176,6 +232,7 @@ value to get the list of blocks that won't be displayed for a theme.
* A new parameter $strength of type int is added to method search_for_active_node. This parameter would help us to search for the active nodes based on the
$strength passed to it.
=== 3.11.4 ===
* A new option dontforcesvgdownload has been added to the $options parameter of the send_file() function.
Note: This option overrides the forced download of directly accessed SVGs, so should only be used where the calling method is