mirror of
https://github.com/moodle/moodle.git
synced 2025-08-04 08:26:37 +02:00
MDL-74452 quiz: Display an error if all versions are in draft status
This commit is contained in:
parent
f3bf17cdfb
commit
72e7818467
10 changed files with 180 additions and 7 deletions
|
@ -24,6 +24,7 @@
|
||||||
|
|
||||||
namespace mod_quiz\output;
|
namespace mod_quiz\output;
|
||||||
|
|
||||||
|
use core_question\local\bank\question_version_status;
|
||||||
use mod_quiz\question\bank\qbank_helper;
|
use mod_quiz\question\bank\qbank_helper;
|
||||||
use \mod_quiz\structure;
|
use \mod_quiz\structure;
|
||||||
use \html_writer;
|
use \html_writer;
|
||||||
|
@ -765,7 +766,8 @@ class edit_renderer extends \plugin_renderer_base {
|
||||||
'questionname' => $this->get_question_name_for_slot($structure, $slot, $pageurl),
|
'questionname' => $this->get_question_name_for_slot($structure, $slot, $pageurl),
|
||||||
'questionicons' => $this->get_action_icon($structure, $slot, $pageurl),
|
'questionicons' => $this->get_action_icon($structure, $slot, $pageurl),
|
||||||
'questiondependencyicon' => ($structure->can_be_edited() ? $this->question_dependency_icon($structure, $slot) : ''),
|
'questiondependencyicon' => ($structure->can_be_edited() ? $this->question_dependency_icon($structure, $slot) : ''),
|
||||||
'versionselection' => false
|
'versionselection' => false,
|
||||||
|
'draftversion' => $structure->get_question_in_slot($slot)->status == question_version_status::QUESTION_STATUS_DRAFT,
|
||||||
];
|
];
|
||||||
|
|
||||||
$data['versionoptions'] = [];
|
$data['versionoptions'] = [];
|
||||||
|
|
|
@ -134,7 +134,9 @@ class qbank_helper {
|
||||||
-- just before the commit that added this comment.
|
-- just before the commit that added this comment.
|
||||||
-- For relevant question_bank_entries, this gets the latest non-draft slot number.
|
-- For relevant question_bank_entries, this gets the latest non-draft slot number.
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT lv.questionbankentryid, MAX(lv.version) AS version
|
SELECT lv.questionbankentryid,
|
||||||
|
MAX(CASE WHEN lv.status <> :draft THEN lv.version END) AS usableversion,
|
||||||
|
MAX(lv.version) AS anyversion
|
||||||
FROM {quiz_slots} lslot
|
FROM {quiz_slots} lslot
|
||||||
JOIN {question_references} lqr ON lqr.usingcontextid = :quizcontextid2 AND lqr.component = 'mod_quiz'
|
JOIN {question_references} lqr ON lqr.usingcontextid = :quizcontextid2 AND lqr.component = 'mod_quiz'
|
||||||
AND lqr.questionarea = 'slot' AND lqr.itemid = lslot.id
|
AND lqr.questionarea = 'slot' AND lqr.itemid = lslot.id
|
||||||
|
@ -142,13 +144,14 @@ class qbank_helper {
|
||||||
WHERE lslot.quizid = :quizid2
|
WHERE lslot.quizid = :quizid2
|
||||||
$slotidtest2
|
$slotidtest2
|
||||||
AND lqr.version IS NULL
|
AND lqr.version IS NULL
|
||||||
AND lv.status <> :draft
|
|
||||||
GROUP BY lv.questionbankentryid
|
GROUP BY lv.questionbankentryid
|
||||||
) latestversions ON latestversions.questionbankentryid = qr.questionbankentryid
|
) latestversions ON latestversions.questionbankentryid = qr.questionbankentryid
|
||||||
|
|
||||||
LEFT JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
|
LEFT JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id
|
||||||
-- Either specified version, or latest ready version.
|
-- Either specified version, or latest usable version, or a draft version.
|
||||||
AND qv.version = COALESCE(qr.version, latestversions.version)
|
AND qv.version = COALESCE(qr.version,
|
||||||
|
latestversions.usableversion,
|
||||||
|
latestversions.anyversion)
|
||||||
LEFT JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
LEFT JOIN {question_categories} qc ON qc.id = qbe.questioncategoryid
|
||||||
LEFT JOIN {question} q ON q.id = qv.questionid
|
LEFT JOIN {question} q ON q.id = qv.questionid
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ use cm_info;
|
||||||
use coding_exception;
|
use coding_exception;
|
||||||
use context;
|
use context;
|
||||||
use context_module;
|
use context_module;
|
||||||
|
use core_question\local\bank\question_version_status;
|
||||||
use mod_quiz\question\bank\qbank_helper;
|
use mod_quiz\question\bank\qbank_helper;
|
||||||
use mod_quiz\question\display_options;
|
use mod_quiz\question\display_options;
|
||||||
use moodle_exception;
|
use moodle_exception;
|
||||||
|
@ -561,6 +562,10 @@ class quiz_settings {
|
||||||
$qcategories = [];
|
$qcategories = [];
|
||||||
|
|
||||||
foreach ($this->get_questions() as $questiondata) {
|
foreach ($this->get_questions() as $questiondata) {
|
||||||
|
if ($questiondata->status == question_version_status::QUESTION_STATUS_DRAFT) {
|
||||||
|
// Skip questions where all versions are draft.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if ($questiondata->qtype === 'random' && $includepotential) {
|
if ($questiondata->qtype === 'random' && $includepotential) {
|
||||||
if (!isset($qcategories[$questiondata->category])) {
|
if (!isset($qcategories[$questiondata->category])) {
|
||||||
$qcategories[$questiondata->category] = false;
|
$qcategories[$questiondata->category] = false;
|
||||||
|
|
|
@ -736,6 +736,9 @@ $string['questiondependencyadd'] = 'No restriction on when question {$a->thisq}
|
||||||
$string['questiondependencyfree'] = 'No restriction on this question';
|
$string['questiondependencyfree'] = 'No restriction on this question';
|
||||||
$string['questiondependencyremove'] = 'Question {$a->thisq} cannot be attempted until the previous question {$a->previousq} has been completed • Click to change';
|
$string['questiondependencyremove'] = 'Question {$a->thisq} cannot be attempted until the previous question {$a->previousq} has been completed • Click to change';
|
||||||
$string['questiondependsonprevious'] = 'This question cannot be attempted until the previous question has been completed.';
|
$string['questiondependsonprevious'] = 'This question cannot be attempted until the previous question has been completed.';
|
||||||
|
$string['questiondraftonly'] = 'The question \'{$a}\' has all versions in Draft status, so cannot be used. Visit the question bank and set the status to Ready.';
|
||||||
|
$string['questiondraftwillnotwork'] = 'This question has all versions in Draft status. The quiz will not work with this question in place. Remove this question,
|
||||||
|
or visit the question bank and set the status to Ready.';
|
||||||
$string['questioninuse'] = 'The question \'{$a->questionname}\' is currently being used in: <br />{$a->quiznames}<br />The question will not be deleted from these quizzes but only from the category list.';
|
$string['questioninuse'] = 'The question \'{$a->questionname}\' is currently being used in: <br />{$a->quiznames}<br />The question will not be deleted from these quizzes but only from the category list.';
|
||||||
$string['questionmissing'] = 'Question for this session is missing';
|
$string['questionmissing'] = 'Question for this session is missing';
|
||||||
$string['questionname'] = 'Question name';
|
$string['questionname'] = 'Question name';
|
||||||
|
|
|
@ -180,6 +180,9 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
|
||||||
$slot += 1;
|
$slot += 1;
|
||||||
$maxmark[$slot] = $questiondata->maxmark;
|
$maxmark[$slot] = $questiondata->maxmark;
|
||||||
$page[$slot] = $questiondata->page;
|
$page[$slot] = $questiondata->page;
|
||||||
|
if ($questiondata->status == \core_question\local\bank\question_version_status::QUESTION_STATUS_DRAFT) {
|
||||||
|
throw new moodle_exception('questiondraftonly', 'mod_quiz', '', $questiondata->name);
|
||||||
|
}
|
||||||
if ($questiondata->qtype == 'random') {
|
if ($questiondata->qtype == 'random') {
|
||||||
$randomfound = true;
|
$randomfound = true;
|
||||||
continue;
|
continue;
|
||||||
|
@ -318,7 +321,11 @@ function quiz_start_attempt_built_on_last($quba, $attempt, $lastattempt) {
|
||||||
|
|
||||||
$oldnumberstonew = [];
|
$oldnumberstonew = [];
|
||||||
foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) {
|
foreach ($oldquba->get_attempt_iterator() as $oldslot => $oldqa) {
|
||||||
$newslot = $quba->add_question($oldqa->get_question(false), $oldqa->get_max_mark());
|
$question = $oldqa->get_question(false);
|
||||||
|
if ($question->status == \core_question\local\bank\question_version_status::QUESTION_STATUS_DRAFT) {
|
||||||
|
throw new moodle_exception('questiondraftonly', 'mod_quiz', '', $question->name);
|
||||||
|
}
|
||||||
|
$newslot = $quba->add_question($question, $oldqa->get_max_mark());
|
||||||
|
|
||||||
$quba->start_question_based_on($newslot, $oldqa);
|
$quba->start_question_based_on($newslot, $oldqa);
|
||||||
|
|
||||||
|
|
|
@ -50,3 +50,6 @@
|
||||||
{{{questiondependencyicon}}}
|
{{{questiondependencyicon}}}
|
||||||
{{/canbeedited}}
|
{{/canbeedited}}
|
||||||
</div>
|
</div>
|
||||||
|
{{#draftversion}}
|
||||||
|
<div class="alert alert-danger" role="alert">{{#str}}questiondraftwillnotwork, mod_quiz{{/str}}</div>
|
||||||
|
{{/draftversion}}
|
||||||
|
|
|
@ -16,8 +16,8 @@
|
||||||
|
|
||||||
namespace mod_quiz;
|
namespace mod_quiz;
|
||||||
|
|
||||||
|
use core_question\local\bank\question_version_status;
|
||||||
use question_engine;
|
use question_engine;
|
||||||
use mod_quiz\quiz_settings;
|
|
||||||
|
|
||||||
defined('MOODLE_INTERNAL') || die();
|
defined('MOODLE_INTERNAL') || die();
|
||||||
|
|
||||||
|
@ -430,4 +430,79 @@ class attempt_test extends \advanced_testcase {
|
||||||
$this->assertFalse($attempt->check_page_access(4));
|
$this->assertFalse($attempt->check_page_access(4));
|
||||||
$this->assertFalse($attempt->check_page_access(1));
|
$this->assertFalse($attempt->check_page_access(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starting a new attempt with a question in draft status should throw an exception.
|
||||||
|
*
|
||||||
|
* @covers ::quiz_start_new_attempt()
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function test_start_new_attempt_with_draft(): void {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
|
||||||
|
// Create course.
|
||||||
|
$course = $this->getDataGenerator()->create_course();
|
||||||
|
// Create students.
|
||||||
|
$student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||||
|
$student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||||
|
// Create quiz.
|
||||||
|
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||||
|
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
|
||||||
|
// Create question and add it to quiz.
|
||||||
|
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||||
|
$cat = $questiongenerator->create_question_category();
|
||||||
|
$question = $questiongenerator->create_question('shortanswer', null,
|
||||||
|
['category' => $cat->id, 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
|
||||||
|
quiz_add_quiz_question($question->id, $quiz, 1);
|
||||||
|
|
||||||
|
$quizobj = quiz_settings::create($quiz->id);
|
||||||
|
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||||
|
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
|
||||||
|
$attempt = quiz_create_attempt($quizobj, 1, false, time(), false, $student1->id);
|
||||||
|
|
||||||
|
$this->expectExceptionObject(new \moodle_exception('questiondraftonly', 'mod_quiz', '', $question->name));
|
||||||
|
quiz_start_new_attempt($quizobj, $quba, $attempt, 1, time());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starting a new attempt built on last with a question in draft status should throw an exception.
|
||||||
|
*
|
||||||
|
* @covers ::quiz_start_attempt_built_on_last()
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function test_quiz_start_attempt_built_on_last_with_draft(): void {
|
||||||
|
global $DB;
|
||||||
|
$this->resetAfterTest();
|
||||||
|
|
||||||
|
// Create course.
|
||||||
|
$course = $this->getDataGenerator()->create_course();
|
||||||
|
// Create students.
|
||||||
|
$student1 = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||||
|
$student2 = $this->getDataGenerator()->create_and_enrol($course, 'student');
|
||||||
|
// Create quiz.
|
||||||
|
$quizgenerator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||||
|
$quiz = $quizgenerator->create_instance(['course' => $course->id, 'grade' => 100.0, 'sumgrades' => 2, 'layout' => '1,0']);
|
||||||
|
// Create question and add it to quiz.
|
||||||
|
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||||
|
$cat = $questiongenerator->create_question_category();
|
||||||
|
$question = $questiongenerator->create_question('shortanswer', null, ['category' => $cat->id]);
|
||||||
|
quiz_add_quiz_question($question->id, $quiz, 1);
|
||||||
|
|
||||||
|
$quizobj = quiz_settings::create($quiz->id);
|
||||||
|
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||||
|
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
|
||||||
|
$attempt = quiz_create_attempt($quizobj, 1, false, time(), false, $student1->id);
|
||||||
|
$attempt = quiz_start_new_attempt($quizobj, $quba, $attempt, 1, time());
|
||||||
|
$attempt = quiz_attempt_save_started($quizobj, $quba, $attempt);
|
||||||
|
|
||||||
|
$DB->set_field('question_versions', 'status', question_version_status::QUESTION_STATUS_DRAFT,
|
||||||
|
['questionid' => $question->id]);
|
||||||
|
$quizobj = quiz_settings::create($quiz->id);
|
||||||
|
$quba = question_engine::make_questions_usage_by_activity('mod_quiz', $quizobj->get_context());
|
||||||
|
$quba->set_preferred_behaviour($quizobj->get_quiz()->preferredbehaviour);
|
||||||
|
$newattempt = quiz_create_attempt($quizobj, 2, $attempt, time(), false, $student1->id);
|
||||||
|
|
||||||
|
$this->expectExceptionObject(new \moodle_exception('questiondraftonly', 'mod_quiz', '', $question->name));
|
||||||
|
quiz_start_attempt_built_on_last($quba, $newattempt, $attempt);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -111,3 +111,13 @@ Feature: Quiz question versioning
|
||||||
And I press "Add selected questions to the quiz"
|
And I press "Add selected questions to the quiz"
|
||||||
Then I should see "Other question" on quiz page "1"
|
Then I should see "Other question" on quiz page "1"
|
||||||
And the field "version" in the "Other question" "list_item" matches value "Always latest"
|
And the field "version" in the "Other question" "list_item" matches value "Always latest"
|
||||||
|
|
||||||
|
@javascript
|
||||||
|
Scenario: Adding a question where all available versions are drafts should display a helpful message.
|
||||||
|
Given quiz "Quiz 1" contains the following questions:
|
||||||
|
| question | page |
|
||||||
|
| First question | 1 |
|
||||||
|
And I am on the "Quiz 1" "mod_quiz > Question bank" page logged in as teacher
|
||||||
|
And I set the field "question_status_dropdown" in the "First question" "table_row" to "Draft"
|
||||||
|
When I am on the "Quiz 1" "mod_quiz > Edit" page
|
||||||
|
Then I should see "This question has all versions in Draft status. The quiz will not work with this question in place."
|
||||||
|
|
5
mod/quiz/tests/external/external_test.php
vendored
5
mod/quiz/tests/external/external_test.php
vendored
|
@ -27,6 +27,7 @@
|
||||||
namespace mod_quiz\external;
|
namespace mod_quiz\external;
|
||||||
|
|
||||||
use core_external\external_api;
|
use core_external\external_api;
|
||||||
|
use core_question\local\bank\question_version_status;
|
||||||
use externallib_advanced_testcase;
|
use externallib_advanced_testcase;
|
||||||
use mod_quiz\question\display_options;
|
use mod_quiz\question\display_options;
|
||||||
use mod_quiz\quiz_attempt;
|
use mod_quiz\quiz_attempt;
|
||||||
|
@ -1974,6 +1975,10 @@ class external_test extends externallib_advanced_testcase {
|
||||||
$question = $questiongenerator->create_question('essay', null, ['category' => $cat->id]);
|
$question = $questiongenerator->create_question('essay', null, ['category' => $cat->id]);
|
||||||
quiz_add_quiz_question($question->id, $quiz);
|
quiz_add_quiz_question($question->id, $quiz);
|
||||||
|
|
||||||
|
$question = $questiongenerator->create_question('multichoice', null,
|
||||||
|
['category' => $cat->id, 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
|
||||||
|
quiz_add_quiz_question($question->id, $quiz);
|
||||||
|
|
||||||
$this->setUser($this->student);
|
$this->setUser($this->student);
|
||||||
|
|
||||||
$result = mod_quiz_external::get_quiz_required_qtypes($quiz->id);
|
$result = mod_quiz_external::get_quiz_required_qtypes($quiz->id);
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
|
|
||||||
namespace mod_quiz;
|
namespace mod_quiz;
|
||||||
|
|
||||||
|
use core_question\local\bank\question_version_status;
|
||||||
use mod_quiz\external\submit_question_version;
|
use mod_quiz\external\submit_question_version;
|
||||||
use mod_quiz\question\bank\qbank_helper;
|
use mod_quiz\question\bank\qbank_helper;
|
||||||
|
|
||||||
|
@ -143,4 +144,63 @@ class qbank_helper_test extends \advanced_testcase {
|
||||||
$slot = reset($slots);
|
$slot = reset($slots);
|
||||||
$this->assertEquals($finalq->id, $slot->questionid);
|
$this->assertEquals($finalq->id, $slot->questionid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a question only has draft versions, we should get those and not a dummy question.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
* @covers ::get_question_structure
|
||||||
|
*/
|
||||||
|
public function test_get_question_structure_with_drafts(): void {
|
||||||
|
$this->resetAfterTest();
|
||||||
|
|
||||||
|
// Create a quiz.
|
||||||
|
$quiz = $this->create_test_quiz($this->course);
|
||||||
|
$quizcontext = \context_module::instance(get_coursemodule_from_instance("quiz", $quiz->id, $this->course->id)->id);
|
||||||
|
|
||||||
|
// Create some questions with drafts in the quiz question bank.
|
||||||
|
/** @var \core_question_generator $questiongenerator */
|
||||||
|
$questiongenerator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||||
|
$cat = $questiongenerator->create_question_category(['contextid' => $quizcontext->id]);
|
||||||
|
$q1 = $questiongenerator->create_question('essay', null,
|
||||||
|
['category' => $cat->id, 'name' => 'This is q1 the first version']);
|
||||||
|
$q2 = $questiongenerator->create_question('essay', null,
|
||||||
|
['category' => $cat->id, 'name' => 'This is q2 the first version',
|
||||||
|
'status' => question_version_status::QUESTION_STATUS_DRAFT]);
|
||||||
|
$q3 = $questiongenerator->create_question('essay', null,
|
||||||
|
['category' => $cat->id, 'name' => 'This is q3 the first version',
|
||||||
|
'status' => question_version_status::QUESTION_STATUS_DRAFT]);
|
||||||
|
|
||||||
|
// Create a new draft version of a question.
|
||||||
|
$q1final = $questiongenerator->update_question(clone $q1, null,
|
||||||
|
['name' => 'This is q1 the second version', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
|
||||||
|
$q3final = $questiongenerator->update_question(clone $q3, null,
|
||||||
|
['name' => 'This is q3 the second version', 'status' => question_version_status::QUESTION_STATUS_DRAFT]);
|
||||||
|
|
||||||
|
// Add the questions to the quiz.
|
||||||
|
quiz_add_quiz_question($q1->id, $quiz);
|
||||||
|
quiz_add_quiz_question($q2->id, $quiz);
|
||||||
|
quiz_add_quiz_question($q3->id, $quiz);
|
||||||
|
|
||||||
|
// Load the quiz object and check.
|
||||||
|
$quizobj = \mod_quiz\quiz_settings::create($quiz->id);
|
||||||
|
$quizobj->preload_questions();
|
||||||
|
$quizobj->load_questions();
|
||||||
|
$questions = $quizobj->get_questions();
|
||||||
|
$this->assertCount(3, $questions);
|
||||||
|
// When a question has a Ready version, we should get that and not he draft.
|
||||||
|
$this->assertTrue(array_key_exists($q1->id, $questions));
|
||||||
|
$this->assertFalse(array_key_exists($q1final->id, $questions));
|
||||||
|
$this->assertEquals(question_version_status::QUESTION_STATUS_READY, $questions[$q1->id]->status);
|
||||||
|
$this->assertEquals('essay', $questions[$q1->id]->qtype);
|
||||||
|
// When a question only has a draft, we should get that.
|
||||||
|
$this->assertTrue(array_key_exists($q2->id, $questions));
|
||||||
|
$this->assertEquals(question_version_status::QUESTION_STATUS_DRAFT, $questions[$q2->id]->status);
|
||||||
|
$this->assertEquals('essay', $questions[$q2->id]->qtype);
|
||||||
|
// When a question has several versions but all draft, we should get the latest draft.
|
||||||
|
$this->assertFalse(array_key_exists($q3->id, $questions));
|
||||||
|
$this->assertTrue(array_key_exists($q3final->id, $questions));
|
||||||
|
$this->assertEquals(question_version_status::QUESTION_STATUS_DRAFT, $questions[$q3final->id]->status);
|
||||||
|
$this->assertEquals('essay', $questions[$q3final->id]->qtype);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue