Merge branch 'MDL-86798_main' of https://github.com/marxjohnson/moodle
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
issueNumber: MDL-86798
|
||||
notes:
|
||||
core_question:
|
||||
- message: >
|
||||
In order to prevent re-use of question version numbers after a version
|
||||
is deleted, the `nextversion` column was added to
|
||||
`question_bank_entries`. This serves as a counter incremented each time
|
||||
a version is created.
|
||||
|
||||
Do not query this field directly. Instead use
|
||||
`core_question\versions::get_next_version()` to read the value, which
|
||||
will initialise it based on the existing versions if it is not set yet.
|
||||
By default, it will increment the version number automatically, unless
|
||||
you pass `increment: false`. Because of this, it is advisable to call
|
||||
it inside a transaction, that is only committed after the version number
|
||||
is used in a `question_versions` record.
|
||||
type: fixed
|
||||
@@ -0,0 +1,7 @@
|
||||
issueNumber: MDL-86798
|
||||
notes:
|
||||
core_question:
|
||||
- message: >-
|
||||
`get_next_version()` from questionlib.php is now deprecated. Use
|
||||
`\core_question\versions::get_next_version()` instead.
|
||||
type: deprecated
|
||||
@@ -2649,6 +2649,7 @@ class backup_questions_structure_step extends backup_structure_step {
|
||||
'questioncategoryid',
|
||||
'idnumber',
|
||||
'ownerid',
|
||||
'nextversion',
|
||||
]);
|
||||
|
||||
$questionversions = new backup_nested_element('question_version');
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
use core_question\versions;
|
||||
|
||||
/**
|
||||
* delete old directories and conditionally create backup_temp_ids table
|
||||
*/
|
||||
@@ -5301,9 +5304,55 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
$oldqvid = $this->latestversion->id;
|
||||
$this->latestversion->questionbankentryid = $this->latestqbe->newid;
|
||||
$this->latestversion->questionid = $newitemid;
|
||||
// In case the backed up version was deleted and a new one created in its place, increase the version numbers of
|
||||
// conflicting versions to make room for this one.
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
if (
|
||||
$DB->record_exists(
|
||||
'question_versions',
|
||||
[
|
||||
'questionbankentryid' => $this->latestversion->questionbankentryid,
|
||||
'version' => $this->latestversion->version,
|
||||
],
|
||||
)
|
||||
) {
|
||||
// We'll update each higher version and any references one-at-a-time, starting with the highest, to avoid
|
||||
// creating a duplicate questionbankentryid-version combination in question_versions.
|
||||
$moveversions = $DB->get_records_select(
|
||||
'question_versions',
|
||||
'questionbankentryid = :questionbankentryid AND version >= :oldversion',
|
||||
[
|
||||
'questionbankentryid' => $this->latestversion->questionbankentryid,
|
||||
'oldversion' => $this->latestversion->version,
|
||||
],
|
||||
'version DESC',
|
||||
);
|
||||
foreach ($moveversions as $moveversion) {
|
||||
$DB->set_field(
|
||||
'question_versions',
|
||||
'version',
|
||||
$moveversion->version + 1,
|
||||
[
|
||||
'questionbankentryid' => $moveversion->questionbankentryid,
|
||||
'version' => $moveversion->version,
|
||||
]
|
||||
);
|
||||
$DB->set_field(
|
||||
'question_references',
|
||||
'version',
|
||||
$moveversion->version + 1,
|
||||
[
|
||||
'questionbankentryid' => $moveversion->questionbankentryid,
|
||||
'version' => $moveversion->version,
|
||||
]
|
||||
);
|
||||
}
|
||||
// Ensure the nextversion value has been initialised, and increment it to account for the additional version.
|
||||
versions::get_next_version($this->latestversion->questionbankentryid);
|
||||
}
|
||||
$newqvid = $DB->insert_record('question_versions', $this->latestversion);
|
||||
$this->set_mapping('question_versions', $oldqvid, $newqvid);
|
||||
|
||||
$transaction->allow_commit();
|
||||
} else {
|
||||
// By performing this set_mapping() we make get_old/new_parentid() to work for all the
|
||||
// children elements of the 'question' one (so qtype plugins will know the question they belong to).
|
||||
@@ -5311,6 +5360,18 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
|
||||
// Also create the question_bank_entry and version mappings, if required.
|
||||
$newquestionversion = $DB->get_record('question_versions', ['questionid' => $questionmapping->newitemid]);
|
||||
// Restore the version to ready state if it has been hidden.
|
||||
if (
|
||||
$newquestionversion->status == question_version_status::QUESTION_STATUS_HIDDEN
|
||||
&& $this->latestversion->status == question_version_status::QUESTION_STATUS_READY
|
||||
) {
|
||||
$DB->set_field(
|
||||
'question_versions',
|
||||
'status',
|
||||
question_version_status::QUESTION_STATUS_READY,
|
||||
['questionid' => $questionmapping->newitemid],
|
||||
);
|
||||
}
|
||||
$this->set_mapping('question_versions', $this->latestversion->id, $newquestionversion->id);
|
||||
if (empty($this->latestqbe->newid)) {
|
||||
$this->latestqbe->oldid = $this->latestqbe->id;
|
||||
@@ -5485,7 +5546,11 @@ class restore_move_module_questions_categories extends restore_execution_step {
|
||||
// but if that context still exists on the site and the user has access then point question references
|
||||
// to the originals.
|
||||
$originalcontext = context::instance_by_id($contextid, IGNORE_MISSING);
|
||||
if ($originalcontext && has_capability('mod/qbank:view', $originalcontext)) {
|
||||
if (
|
||||
$this->task->is_samesite()
|
||||
&& $originalcontext
|
||||
&& has_capability('mod/qbank:view', $originalcontext)
|
||||
) {
|
||||
$originalquestions = get_questions_category(question_get_top_category($contextid), false);
|
||||
$targetcoursecontext = context_course::instance($this->get_courseid());
|
||||
foreach ($originalquestions as $originalquestion) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<XMLDB PATH="lib/db" VERSION="20251118" COMMENT="XMLDB file for core Moodle tables"
|
||||
<XMLDB PATH="lib/db" VERSION="20251205" COMMENT="XMLDB file for core Moodle tables"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
|
||||
>
|
||||
@@ -1484,6 +1484,7 @@
|
||||
<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."/>
|
||||
<FIELD NAME="nextversion" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="The next version number for this question bank entry. This must be incremented each time a new question_version is created."/>
|
||||
</FIELDS>
|
||||
<KEYS>
|
||||
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
|
||||
|
||||
@@ -1618,5 +1618,17 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2025121200.01);
|
||||
}
|
||||
|
||||
if ($oldversion < 2025121900.01) {
|
||||
// Define field nextversion to be added to question_bank_entries.
|
||||
$table = new xmldb_table('question_bank_entries');
|
||||
$field = new xmldb_field('nextversion', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'ownerid');
|
||||
|
||||
// Conditionally launch add field nextversion.
|
||||
if (!$dbman->field_exists($table, $field)) {
|
||||
$dbman->add_field($table, $field);
|
||||
}
|
||||
upgrade_main_savepoint(true, 2025121900.01);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1935,21 +1935,14 @@ function get_question_version($questionid): array {
|
||||
* @return int next version number.
|
||||
* @throws dml_exception
|
||||
*/
|
||||
#[\core\attribute\deprecated(
|
||||
'\core_question\versions::get_next_version()',
|
||||
'5.2',
|
||||
'The next version is now an incrementing number stored in the database, to prevent duplicate version numbers',
|
||||
'MDL-86798',
|
||||
)]
|
||||
function get_next_version(int $questionbankentryid): int {
|
||||
global $DB;
|
||||
|
||||
$sql = "SELECT MAX(qv.version)
|
||||
FROM {question_versions} qv
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE qbe.id = :id";
|
||||
|
||||
$nextversion = $DB->get_field_sql($sql, ['id' => $questionbankentryid]);
|
||||
|
||||
if ($nextversion) {
|
||||
return (int)$nextversion + 1;
|
||||
}
|
||||
|
||||
return 1;
|
||||
return \core_question\versions::get_next_version($questionbankentryid);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?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;
|
||||
|
||||
/**
|
||||
* Methods for finding and manipulating question versions
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class versions {
|
||||
/**
|
||||
* Get the next version number for a question bank entry.
|
||||
*
|
||||
* This uses the value in the question bank entry record, but if it's not set, it will calculate it based on the
|
||||
* current highest version in question_versions.
|
||||
*
|
||||
* If calling this with $increment = true, it is a good idea to do this inside a transaction which only commits after the
|
||||
* new version number has been used to save a new version of the question. This avoids wasting version numbers if an
|
||||
* error happens.
|
||||
*
|
||||
* @param int $questionbankentryid
|
||||
* @param bool $increment If true, increment the version number by 1 after it is read.
|
||||
* @return int The number of the next version.
|
||||
*/
|
||||
public static function get_next_version(int $questionbankentryid, bool $increment = true): int {
|
||||
global $DB;
|
||||
$transaction = $DB->start_delegated_transaction();
|
||||
$nextversion = $DB->get_field('question_bank_entries', 'nextversion', ['id' => $questionbankentryid]);
|
||||
if (is_null($nextversion)) {
|
||||
$nextversion = $DB->get_field_sql(
|
||||
"SELECT COALESCE(MAX(qv.version), 0) + 1
|
||||
FROM {question_versions} qv
|
||||
WHERE qv.questionbankentryid = :qbeid",
|
||||
['qbeid' => $questionbankentryid],
|
||||
);
|
||||
$DB->set_field('question_bank_entries', 'nextversion', $nextversion, ['id' => $questionbankentryid]);
|
||||
}
|
||||
if ($increment) {
|
||||
self::increment_next_version($questionbankentryid);
|
||||
}
|
||||
$transaction->allow_commit();
|
||||
return $nextversion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment the next version by 1 for the question bank entry
|
||||
*
|
||||
* @param int $questionbankentryid
|
||||
*/
|
||||
protected static function increment_next_version(int $questionbankentryid): bool {
|
||||
global $DB;
|
||||
return $DB->execute(
|
||||
"UPDATE {question_bank_entries}
|
||||
SET nextversion = nextversion + 1
|
||||
WHERE id = :id",
|
||||
['id' => $questionbankentryid],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -472,6 +472,7 @@ class qformat_default {
|
||||
$questionbankentry->questioncategoryid = $question->category;
|
||||
$questionbankentry->idnumber = $question->idnumber ?? null;
|
||||
$questionbankentry->ownerid = $question->createdby;
|
||||
$questionbankentry->nextversion = 2;
|
||||
$questionbankentry->id = $DB->insert_record('question_bank_entries', $questionbankentry);
|
||||
// Create a version for each question imported.
|
||||
$questionversion = new \stdClass();
|
||||
|
||||
@@ -24,7 +24,9 @@ use question_bank;
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
use backup;
|
||||
use core\context\module;
|
||||
use core_question\local\bank\question_bank_helper;
|
||||
use core_question\local\bank\question_version_status;
|
||||
use restore_controller;
|
||||
use restore_dbops;
|
||||
|
||||
@@ -863,4 +865,264 @@ final class backup_test extends \advanced_testcase {
|
||||
$quizcatq = reset($quizcatqs);
|
||||
$this->assertEquals($expectedidentifiers[$i], $quizcatq->name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a backup containing question versions that were deleted, after new versions were created in their place.
|
||||
*
|
||||
* The new versions and any references to them should have their version numbers bumped up, and the original versions
|
||||
* restored to their original numbers.
|
||||
*/
|
||||
public function test_restore_backup_containing_deleted_versions(): void {
|
||||
global $DB;
|
||||
self::setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
$questiongenerator = self::getDataGenerator()->get_plugin_generator('core_question');
|
||||
$testdata = $this->add_course_quiz_and_qbank();
|
||||
$questionv1 = $testdata->qbankquestion;
|
||||
$questionv2 = $questiongenerator->update_question($questionv1, null, ['name' => 'Version 2']);
|
||||
$questionv3 = $questiongenerator->update_question($questionv2, null, ['name' => 'Version 3']);
|
||||
$questionv4 = $questiongenerator->update_question($questionv3, null, ['name' => 'Version 4']);
|
||||
|
||||
$quizsettings = quiz_settings::create($testdata->quiz->id);
|
||||
$structure1 = $quizsettings->get_structure();
|
||||
|
||||
// Set the usage of the question to specifically use version 2.
|
||||
quiz_add_quiz_question($questionv2->id, $testdata->quiz);
|
||||
$structure1->update_slot_version($structure1->get_slot_id_for_slot(1), 2);
|
||||
|
||||
$backupid = $this->backup_course($testdata->course);
|
||||
|
||||
question_delete_question($questionv4->id); // Actually deleted.
|
||||
question_delete_question($questionv3->id); // Actually deleted.
|
||||
question_delete_question($questionv2->id); // Hidden, it's being used explicitly.
|
||||
|
||||
$questionv5 = $questiongenerator->update_question($questionv1, null, ['name' => 'Version 5']);
|
||||
$DB->set_field('question_versions', 'version', 3, ['questionid' => $questionv5->id]);
|
||||
$questionv6 = $questiongenerator->update_question($questionv5, null, ['name' => 'Version 6']);
|
||||
$DB->set_field('question_versions', 'version', 4, ['questionid' => $questionv6->id]);
|
||||
$DB->set_field('question_bank_entries', 'nextversion', 5, ['id' => $questionv1->questionbankentryid]);
|
||||
|
||||
// Add a quiz specifically using "version 5" (with version number 3).
|
||||
$quiz2 = self::getDataGenerator()->create_module('quiz', ['course' => $testdata->course->id]);
|
||||
$quiz2settings = quiz_settings::create($quiz2->id);
|
||||
quiz_add_quiz_question($questionv5->id, $quiz2);
|
||||
$structure2 = $quiz2settings->get_structure();
|
||||
$structure2->update_slot_version($structure2->get_last_slot()->id, 3);
|
||||
|
||||
// Add another quiz using the "always latest" version of the question.
|
||||
$quiz3 = self::getDataGenerator()->create_module('quiz', ['course' => $testdata->course->id]);
|
||||
$quiz3settings = quiz_settings::create($quiz3->id);
|
||||
quiz_add_quiz_question($questionv6->id, $quiz3);
|
||||
|
||||
$qbe = get_question_bank_entry($questionv1->id);
|
||||
$versions = $DB->get_records(
|
||||
'question_versions',
|
||||
['questionbankentryid' => $qbe->id],
|
||||
fields: 'version, questionid, status',
|
||||
);
|
||||
|
||||
$this->assertCount(4, $versions);
|
||||
|
||||
$this->assertEquals($questionv1->id, $versions[1]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[1]->status);
|
||||
$this->assertEquals($questionv2->id, $versions[2]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_HIDDEN, $versions[2]->status);
|
||||
$this->assertEquals($questionv5->id, $versions[3]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[3]->status);
|
||||
$this->assertEquals($questionv6->id, $versions[4]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[4]->status);
|
||||
|
||||
$structure1 = $quizsettings->get_structure();
|
||||
$this->assertEquals($questionv2->id, $structure1->get_question_in_slot(1)->questionid);
|
||||
|
||||
$structure2 = $quiz2settings->get_structure();
|
||||
$this->assertEquals($questionv5->id, $structure2->get_question_in_slot(1)->questionid);
|
||||
|
||||
$structure3 = $quiz3settings->get_structure();
|
||||
$this->assertEquals($questionv6->id, $structure3->get_question_in_slot(1)->questionid);
|
||||
|
||||
$this->restore_to_course($backupid, $testdata->course->id);
|
||||
|
||||
$versions = $DB->get_records(
|
||||
'question_versions',
|
||||
['questionbankentryid' => $qbe->id],
|
||||
fields: 'version, questionid, status',
|
||||
);
|
||||
|
||||
$this->assertCount(6, $versions);
|
||||
|
||||
$this->assertEquals($questionv1->id, $versions[1]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[1]->status);
|
||||
// Version 2 is restored to "ready" status.
|
||||
$this->assertEquals($questionv2->id, $versions[2]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[2]->status);
|
||||
// Version 3 is a new copy of $questionv3 with a different ID but the same content.
|
||||
$this->assertEquals($questionv3->name, $DB->get_field('question', 'name', ['id' => $versions[3]->questionid]));
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[3]->status);
|
||||
// Version 4 is a new copy of $questionv4 with a different ID but the same content.
|
||||
$this->assertEquals($questionv4->name, $DB->get_field('question', 'name', ['id' => $versions[4]->questionid]));
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[4]->status);
|
||||
// Versions 5 and 6 have had their version numbers bumped up by 1.
|
||||
$this->assertEquals($questionv5->id, $versions[5]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[5]->status);
|
||||
$this->assertEquals($questionv6->id, $versions[6]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[6]->status);
|
||||
|
||||
// Questions referencing specific versions still point to the same question with its new version number.
|
||||
$structure1 = $quizsettings->get_structure();
|
||||
$this->assertEquals($questionv2->id, $structure1->get_question_in_slot(1)->questionid);
|
||||
|
||||
$structure2 = $quiz2settings->get_structure();
|
||||
$this->assertEquals($questionv5->id, $structure2->get_question_in_slot(1)->questionid);
|
||||
|
||||
$structure3 = $quiz3settings->get_structure();
|
||||
$this->assertEquals($questionv6->id, $structure3->get_question_in_slot(1)->questionid);
|
||||
|
||||
// The nextversion was incremented when the old versions were inserted.
|
||||
$this->assertEquals(7, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $questionv1->questionbankentryid]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore a backup containing question versions that were deleted, after new versions were created.
|
||||
*
|
||||
* Unlike {@see test_restore_backup_containing_deleted_versions}, we do not mess with the version numbers or nextversion
|
||||
* counter, meaning a gap should be left for the deleted question, and it should be restore in its original place.
|
||||
*/
|
||||
public function test_restore_backup_containing_deleted_versions_using_nextversion(): void {
|
||||
global $DB;
|
||||
self::setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
$questiongenerator = self::getDataGenerator()->get_plugin_generator('core_question');
|
||||
$testdata = $this->add_course_quiz_and_qbank();
|
||||
$questionv1 = $testdata->qbankquestion;
|
||||
$questionv2 = $questiongenerator->update_question($questionv1, null, ['name' => 'Version 2']);
|
||||
$questionv3 = $questiongenerator->update_question($questionv2, null, ['name' => 'Version 3']);
|
||||
$questionv4 = $questiongenerator->update_question($questionv3, null, ['name' => 'Version 4']);
|
||||
|
||||
$quizsettings = quiz_settings::create($testdata->quiz->id);
|
||||
$structure1 = $quizsettings->get_structure();
|
||||
|
||||
// Set the usage of the question to specifically use version 2.
|
||||
quiz_add_quiz_question($questionv2->id, $testdata->quiz);
|
||||
$structure1->update_slot_version($structure1->get_slot_id_for_slot(1), 2);
|
||||
|
||||
$backupid = $this->backup_course($testdata->course);
|
||||
|
||||
question_delete_question($questionv4->id); // Actually deleted.
|
||||
question_delete_question($questionv3->id); // Actually deleted.
|
||||
question_delete_question($questionv2->id); // Hidden, it's being used explicitly.
|
||||
|
||||
$questionv5 = $questiongenerator->update_question($questionv1, null, ['name' => 'Version 5']);
|
||||
$questionv6 = $questiongenerator->update_question($questionv5, null, ['name' => 'Version 6']);
|
||||
$this->assertEquals(7, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $questionv1->questionbankentryid]));
|
||||
|
||||
// Add a quiz specifically using "version 5" .
|
||||
$quiz2 = self::getDataGenerator()->create_module('quiz', ['course' => $testdata->course->id]);
|
||||
$quiz2settings = quiz_settings::create($quiz2->id);
|
||||
quiz_add_quiz_question($questionv5->id, $quiz2);
|
||||
$structure2 = $quiz2settings->get_structure();
|
||||
$structure2->update_slot_version($structure2->get_last_slot()->id, 5);
|
||||
|
||||
// Add another quiz using the "always latest" version of the question.
|
||||
$quiz3 = self::getDataGenerator()->create_module('quiz', ['course' => $testdata->course->id]);
|
||||
$quiz3settings = quiz_settings::create($quiz3->id);
|
||||
quiz_add_quiz_question($questionv6->id, $quiz3);
|
||||
|
||||
$qbe = get_question_bank_entry($questionv1->id);
|
||||
$versions = $DB->get_records(
|
||||
'question_versions',
|
||||
['questionbankentryid' => $qbe->id],
|
||||
fields: 'version, questionid, status',
|
||||
);
|
||||
|
||||
$this->assertCount(4, $versions);
|
||||
|
||||
$this->assertEquals($questionv1->id, $versions[1]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[1]->status);
|
||||
$this->assertEquals($questionv2->id, $versions[2]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_HIDDEN, $versions[2]->status);
|
||||
$this->assertArrayNotHasKey(3, $versions);
|
||||
$this->assertArrayNotHasKey(4, $versions);
|
||||
$this->assertEquals($questionv5->id, $versions[5]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[5]->status);
|
||||
$this->assertEquals($questionv6->id, $versions[6]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[6]->status);
|
||||
|
||||
$structure1 = $quizsettings->get_structure();
|
||||
$this->assertEquals($questionv2->id, $structure1->get_question_in_slot(1)->questionid);
|
||||
|
||||
$structure2 = $quiz2settings->get_structure();
|
||||
$this->assertEquals($questionv5->id, $structure2->get_question_in_slot(1)->questionid);
|
||||
|
||||
$structure3 = $quiz3settings->get_structure();
|
||||
$this->assertEquals($questionv6->id, $structure3->get_question_in_slot(1)->questionid);
|
||||
|
||||
$this->restore_to_course($backupid, $testdata->course->id);
|
||||
|
||||
$versions = $DB->get_records(
|
||||
'question_versions',
|
||||
['questionbankentryid' => $qbe->id],
|
||||
fields: 'version, questionid, status',
|
||||
);
|
||||
|
||||
$this->assertCount(6, $versions);
|
||||
|
||||
$this->assertEquals($questionv1->id, $versions[1]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[1]->status);
|
||||
// Version 2 is restored to "ready" status.
|
||||
$this->assertEquals($questionv2->id, $versions[2]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[2]->status);
|
||||
// Version 3 is a new copy of $questionv3 with a different ID but the same content.
|
||||
$this->assertEquals($questionv3->name, $DB->get_field('question', 'name', ['id' => $versions[3]->questionid]));
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[3]->status);
|
||||
// Version 4 is a new copy of $questionv4 with a different ID but the same content.
|
||||
$this->assertEquals($questionv4->name, $DB->get_field('question', 'name', ['id' => $versions[4]->questionid]));
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[4]->status);
|
||||
// Versions 5 and 6 have had their version numbers bumped up by 1.
|
||||
$this->assertEquals($questionv5->id, $versions[5]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[5]->status);
|
||||
$this->assertEquals($questionv6->id, $versions[6]->questionid);
|
||||
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $versions[6]->status);
|
||||
|
||||
// Questions referencing specific versions still point to the same question.
|
||||
$structure1 = $quizsettings->get_structure();
|
||||
$this->assertEquals($questionv2->id, $structure1->get_question_in_slot(1)->questionid);
|
||||
|
||||
$structure2 = $quiz2settings->get_structure();
|
||||
$this->assertEquals($questionv5->id, $structure2->get_question_in_slot(1)->questionid);
|
||||
|
||||
$structure3 = $quiz3settings->get_structure();
|
||||
$this->assertEquals($questionv6->id, $structure3->get_question_in_slot(1)->questionid);
|
||||
|
||||
// The nextversion has not changed during the restore.
|
||||
$this->assertEquals(7, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $questionv1->questionbankentryid]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restoring a question bank entry retains the original nextquestion value.
|
||||
*
|
||||
* @return void
|
||||
* @throws \dml_exception
|
||||
*/
|
||||
public function test_backup_question_nextquestion_retained(): void {
|
||||
global $DB;
|
||||
self::setAdminUser();
|
||||
$this->resetAfterTest();
|
||||
$questiongenerator = self::getDataGenerator()->get_plugin_generator('core_question');
|
||||
$testdata = $this->add_course_quiz_and_qbank();
|
||||
$questionv1 = $testdata->qbankquestion;
|
||||
$questiongenerator->update_question($questionv1, null, ['name' => 'Version 2']);
|
||||
$this->assertEquals(3, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $questionv1->questionbankentryid]));
|
||||
|
||||
$backupid = $this->backup_course($testdata->course);
|
||||
|
||||
$newcourse = $this->getDataGenerator()->create_course();
|
||||
$this->restore_to_course($backupid, $newcourse->id);
|
||||
$qbanks = get_fast_modinfo($newcourse)->get_instances_of('qbank');
|
||||
$qbank = reset($qbanks);
|
||||
$qbankcontext = module::instance($qbank->id);
|
||||
$category = question_get_default_category($qbankcontext->id);
|
||||
// The question bank entry for the restored question has the same nextqueston value as the original.
|
||||
$this->assertEquals(3, $DB->get_field('question_bank_entries', 'nextversion', ['questioncategoryid' => $category->id]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
<?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;
|
||||
|
||||
use core\context\module;
|
||||
|
||||
/**
|
||||
* Unit tests for versions
|
||||
*
|
||||
* @package core_question
|
||||
* @copyright 2025 onwards Catalyst IT EU {@link https://catalyst-eu.net}
|
||||
* @author Mark Johnson <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_question\versions
|
||||
*/
|
||||
final class versions_test extends \advanced_testcase {
|
||||
/**
|
||||
* Generate 3 questions - one with 3 versions, one with 2, and one with 1.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function create_question_versions(): array {
|
||||
$qbank = $this->getDataGenerator()->create_module('qbank', ['course' => SITEID]);
|
||||
$qbankcontext = module::instance($qbank->cmid);
|
||||
$category = question_get_default_category($qbankcontext->id);
|
||||
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$q1v1 = $questiongenerator->create_question('shortanswer', null, ['category' => $category->id]);
|
||||
$q1v2 = $questiongenerator->update_question($q1v1);
|
||||
$questiongenerator->update_question($q1v2);
|
||||
$q2v1 = $questiongenerator->create_question('shortanswer', null, ['category' => $category->id]);
|
||||
$questiongenerator->update_question($q2v1);
|
||||
$q3v1 = $questiongenerator->create_question('shortanswer', null, ['category' => $category->id]);
|
||||
|
||||
return [
|
||||
$q1v1->questionbankentryid,
|
||||
$q2v1->questionbankentryid,
|
||||
$q3v1->questionbankentryid,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* We should get the correct next version for each question bank entry.
|
||||
*/
|
||||
public function test_get_next_version(): void {
|
||||
$this->resetAfterTest();
|
||||
[$qbe1, $qbe2, $qbe3] = $this->create_question_versions();
|
||||
|
||||
$this->assertEquals(4, versions::get_next_version($qbe1));
|
||||
$this->assertEquals(3, versions::get_next_version($qbe2));
|
||||
$this->assertEquals(2, versions::get_next_version($qbe3));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the correct next version numbers for existing question bank entries.
|
||||
*/
|
||||
public function test_get_next_version_null(): void {
|
||||
global $DB;
|
||||
$this->resetAfterTest();
|
||||
[$qbe1, $qbe2, $qbe3] = $this->create_question_versions();
|
||||
|
||||
// Null the nextversion values so they have to be recalculated.
|
||||
$DB->set_field('question_bank_entries', 'nextversion', null);
|
||||
|
||||
$this->assertEquals(4, versions::get_next_version($qbe1));
|
||||
$this->assertEquals(3, versions::get_next_version($qbe2));
|
||||
$this->assertEquals(2, versions::get_next_version($qbe3));
|
||||
}
|
||||
|
||||
/**
|
||||
* The next version should be correctly incremented.
|
||||
*/
|
||||
public function test_increment_next_version(): void {
|
||||
$this->resetAfterTest();
|
||||
global $DB;
|
||||
[$qbe1, $qbe2, $qbe3] = $this->create_question_versions();
|
||||
|
||||
$this->assertEquals(4, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $qbe1]));
|
||||
$this->assertEquals(3, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $qbe2]));
|
||||
$this->assertEquals(2, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $qbe3]));
|
||||
|
||||
$this->assertEquals(3, versions::get_next_version($qbe2));
|
||||
|
||||
// The specified question bank entry has had its nextversion incremented, the others are the same.
|
||||
$this->assertEquals(4, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $qbe1]));
|
||||
$this->assertEquals(4, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $qbe2]));
|
||||
$this->assertEquals(2, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $qbe3]));
|
||||
}
|
||||
|
||||
/**
|
||||
* We can get the next version without incrementing it.
|
||||
*/
|
||||
public function test_get_without_increment(): void {
|
||||
$this->resetAfterTest();
|
||||
global $DB;
|
||||
[, $qbe2] = $this->create_question_versions();
|
||||
|
||||
$this->assertEquals(3, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $qbe2]));
|
||||
|
||||
$this->assertEquals(3, versions::get_next_version($qbe2, increment: false));
|
||||
|
||||
// The nextversion value has not changed.
|
||||
$this->assertEquals(3, $DB->get_field('question_bank_entries', 'nextversion', ['id' => $qbe2]));
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
require_once($CFG->dirroot . '/question/engine/lib.php');
|
||||
require_once($CFG->libdir . '/questionlib.php');
|
||||
|
||||
use core_question\versions;
|
||||
|
||||
/**
|
||||
* This is the base class for Moodle question types.
|
||||
@@ -494,7 +495,7 @@ class question_type {
|
||||
// Get the status field. It comes from the form, but for testing we can.
|
||||
$status = $form->status ?? $question->status ??
|
||||
\core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$questionversion->version = get_next_version($questionbankentry->id);
|
||||
$questionversion->version = versions::get_next_version($questionbankentry->id);
|
||||
$questionversion->status = $status;
|
||||
} else {
|
||||
$parentversion = get_question_version($form->parent);
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2025121900.00; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2025121900.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '5.2dev (Build: 20251219)'; // Human-friendly version name
|
||||
|
||||
Reference in New Issue
Block a user