MDL-71696 core_question: Implement new database schema

This commit implements the new database structure for
versioning in question. It also does the migration of
current data to the new structure.

Co-Authored-By: Safat Shahin <safatshahin@catalyst-au.net>
Co-Authored-By: Guillermo Gomez Arias <guillermogomez@catalyst-au.net>
This commit is contained in:
Guillermo Gomez 2021-06-02 19:16:18 +10:00 committed by Safat Shahin
parent c352b70022
commit c34b89a3c2
8 changed files with 493 additions and 44 deletions

View File

@ -426,9 +426,11 @@ $string['privacy:metadata:database:question_attempts'] = 'The information about
$string['privacy:metadata:database:question_attempts:flagged'] = 'An indication that the user has flagged this question within the attempt.';
$string['privacy:metadata:database:question_attempts:responsesummary'] = 'A summary of the question response.';
$string['privacy:metadata:database:question_attempts:timemodified'] = 'The time that the question attempt was updated.';
$string['privacy:metadata:link:qbehaviour'] = 'The question subsystem makes use of the Question behaviours plugin type.';
$string['privacy:metadata:link:qformat'] = 'The question subsystem makes use of the Question import/export formats plugin type for the purpose of importing and exporting questions in different formats.';
$string['privacy:metadata:link:qtype'] = 'The question subsystem interacts with the Question types plugin type which contains the different types of questions.';
$string['privacy:metadata:database:question_bank_entries'] = 'The details about a specific question bank entry.';
$string['privacy:metadata:database:question_bank_entries:ownerid'] = 'The person who owns the question bank entry.';
$string['privacy:metadata:link:qbehaviour'] = 'The Question subsystem makes use of the Question Behaviour plugintype.';
$string['privacy:metadata:link:qformat'] = 'The Question subsystem makes use of the Question Format plugintype for the purpose of importing and exporting questions in different formats.';
$string['privacy:metadata:link:qtype'] = 'The Question subsystem interacts with the Question Type plugintype which contains the different types of questions.';
$string['questionbehaviouradminsetting'] = 'Question behaviour settings';
$string['questionbehavioursdisabled'] = 'Question behaviours to disable';
$string['questionbehavioursdisabledexplained'] = 'Enter a comma-separated list of behaviours you do not want to appear in the drop-down menu.';

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

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

View File

@ -114,8 +114,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

View File

@ -0,0 +1,43 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_question\local\bank;
/**
* Class question_version_status contains the statuses for a question.
*
* @package core_question
* @copyright 2021 Catalyst IT Australia Pty Ltd
* @author Safat Shahin <safatshahin@catalyst-au.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_version_status {
/**
* Const if the question is ready to use.
*/
const QUESTION_STATUS_READY = 'ready';
/**
* Const if the question is hidden.
*/
const QUESTION_STATUS_HIDDEN = 'hidden';
/**
* const if the question is in draft.
*/
const QUESTION_STATUS_DRAFT = 'draft';
}

View File

@ -126,6 +126,10 @@ class provider implements
// The 'question_statistics' table contains aggregated statistics about responses.
// It does not contain any identifiable user data.
$items->add_database_table('question_bank_entries', [
'ownerid' => 'privacy:metadata:database:question_bank_entries:ownerid',
], 'privacy:metadata:database:question_bank_entries');
// The question subsystem makes use of the qtype, qformat, and qbehaviour plugin types.
$items->add_plugintype_link('qtype', [], 'privacy:metadata:link:qtype');
$items->add_plugintype_link('qformat', [], 'privacy:metadata:link:qformat');
@ -336,12 +340,13 @@ class provider implements
// A user may have created or updated a question.
// Questions are linked against a question category, which has a contextid field.
$sql = "SELECT cat.contextid
$sql = "SELECT qc.contextid
FROM {question} q
INNER JOIN {question_categories} cat ON cat.id = q.category
WHERE
q.createdby = :useridcreated OR
q.modifiedby = :useridmodified";
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 q.createdby = :useridcreated
OR q.modifiedby = :useridmodified";
$params = [
'useridcreated' => $userid,
'useridmodified' => $userid,
@ -363,9 +368,10 @@ class provider implements
// Questions are linked against a question category, which has a contextid field.
$sql = "SELECT q.createdby, q.modifiedby
FROM {question} q
JOIN {question_categories} cat
ON cat.id = q.category
WHERE cat.contextid = :contextid";
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 = :contextid";
$params = [
'contextid' => $context->id
@ -487,7 +493,8 @@ class provider implements
/**
* Delete all data for all users in the specified context.
*
* @param context $context The specific context to delete data for.
* @param \context $context The specific context to delete data for.
* @throws \dml_exception
*/
public static function delete_data_for_all_users_in_context(\context $context) {
global $DB;
@ -496,17 +503,19 @@ class provider implements
// user. They are still exported in the list of a users data, but they are not removed.
// The userid is instead anonymised.
$DB->set_field_select('question', 'createdby', 0,
'category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)',
[
'contextid' => $context->id,
]);
$sql = 'SELECT q.*
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 = ?';
$DB->set_field_select('question', 'modifiedby', 0,
'category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)',
[
'contextid' => $context->id,
]);
$questions = $DB->get_records_sql($sql, [$context->id]);
foreach ($questions as $question) {
$question->createdby = 0;
$question->modifiedby = 0;
$DB->update_record('question', $question);
}
}
/**
@ -523,15 +532,36 @@ class provider implements
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['createdby'] = $contextlist->get_user()->id;
$DB->set_field_select('question', 'createdby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid {$contextsql})
AND createdby = :createdby", $contextparams);
$questiondata = $DB->get_records_sql(
"SELECT q.*
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 {$contextsql}
AND q.createdby = :createdby", $contextparams);
foreach ($questiondata as $question) {
$question->createdby = 0;
$DB->update_record('question', $question);
}
list($contextsql, $contextparams) = $DB->get_in_or_equal($contextlist->get_contextids(), SQL_PARAMS_NAMED);
$contextparams['modifiedby'] = $contextlist->get_user()->id;
$DB->set_field_select('question', 'modifiedby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid {$contextsql})
AND modifiedby = :modifiedby", $contextparams);
$questiondata = $DB->get_records_sql(
"SELECT q.*
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 {$contextsql}
AND q.modifiedby = :modifiedby", $contextparams);
foreach ($questiondata as $question) {
$question->modifiedby = 0;
$DB->update_record('question', $question);
}
}
/**
@ -554,12 +584,32 @@ class provider implements
$params = ['contextid' => $context->id];
$DB->set_field_select('question', 'createdby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)
AND createdby {$createdbysql}", $params + $createdbyparams);
$questiondata = $DB->get_records_sql(
"SELECT q.*
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 = :contextid
AND q.createdby {$createdbysql}", $params + $createdbyparams);
$DB->set_field_select('question', 'modifiedby', 0, "
category IN (SELECT id FROM {question_categories} WHERE contextid = :contextid)
AND modifiedby {$modifiedbysql}", $params + $modifiedbyparams);
foreach ($questiondata as $question) {
$question->createdby = 0;
$DB->update_record('question', $question);
}
$questiondata = $DB->get_records_sql(
"SELECT q.*
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 = :contextid
AND q.modifiedby {$modifiedbysql}", $params + $modifiedbyparams);
foreach ($questiondata as $question) {
$question->modifiedby = 0;
$DB->update_record('question', $question);
}
}
}

View File

@ -29,7 +29,7 @@
defined('MOODLE_INTERNAL') || die();
$version = 2022020200.00; // YYYYMMDD = weekly release date of this DEV branch.
$version = 2022020200.03; // YYYYMMDD = weekly release date of this DEV branch.
// RR = release increments - 00 in DEV branches.
// .XX = incremental changes.
$release = '4.0dev+ (Build: 20220202)'; // Human-friendly version name