Merge branch 'MDL-73602_main' of https://github.com/marxjohnson/moodle
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
issueNumber: MDL-73602
|
||||
notes:
|
||||
core:
|
||||
- message: >-
|
||||
Removed `qtype_random` from core.
|
||||
`core\component::is_valid_plugin_name` has an additional check to ensure no-one can create a new plugin called qtype_random,
|
||||
as this would conflict with the support for restoring old backups.
|
||||
type: removed
|
||||
+3
-1
@@ -373,7 +373,6 @@
|
||||
"multichoice",
|
||||
"numerical",
|
||||
"ordering",
|
||||
"random",
|
||||
"randomsamatch",
|
||||
"shortanswer",
|
||||
"truefalse"
|
||||
@@ -642,6 +641,9 @@
|
||||
"learnwise",
|
||||
"webct"
|
||||
],
|
||||
"qtype": [
|
||||
"random"
|
||||
],
|
||||
"quizaccess": [
|
||||
"safebrowser"
|
||||
],
|
||||
|
||||
@@ -5229,6 +5229,18 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
// we have loaded qcatids there for all parsed questions.
|
||||
$data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid);
|
||||
$this->process_question_legacy_data($data);
|
||||
if ($data->qtype === 'random') {
|
||||
// Random questions do not exist anymore. Store the data in a temporary record so it can be converted to a set
|
||||
// reference by the activity that uses it.
|
||||
restore_dbops::set_backup_ids_record(
|
||||
$this->get_restoreid(),
|
||||
'qtype_random_data',
|
||||
$data->id,
|
||||
$questionmapping->id,
|
||||
info: $data,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// In the past, there were some very sloppy values of penalty. Fix them.
|
||||
@@ -5293,6 +5305,11 @@ class restore_create_categories_and_questions extends restore_structure_step {
|
||||
}
|
||||
|
||||
// Now store the question.
|
||||
if ($data->qtype === 'random') {
|
||||
throw new \core\exception\coding_exception(
|
||||
'You cannot restore a question with qtype "random". It must be converted to a set reference instead.',
|
||||
);
|
||||
}
|
||||
$newitemid = $DB->insert_record('question', $data);
|
||||
$this->set_mapping('question', $oldid, $newitemid);
|
||||
// Also annotate them as question_created, we need
|
||||
@@ -5674,8 +5691,8 @@ class restore_move_module_questions_categories extends restore_execution_step {
|
||||
$references = $DB->get_records('question_set_references', ['usingcontextid' => $newcontext->newitemid]);
|
||||
foreach ($references as $reference) {
|
||||
$filtercondition = json_decode($reference->filtercondition);
|
||||
if (!empty($filtercondition->questioncategoryid) &&
|
||||
in_array($filtercondition->questioncategoryid, $categoryids)) {
|
||||
$categoryid = reset($filtercondition->filter->category->values);
|
||||
if (!empty($categoryid) && in_array($categoryid, $categoryids)) {
|
||||
// This is one of ours, update the questionscontextid.
|
||||
$DB->set_field('question_set_references',
|
||||
'questionscontextid', $newcontext->newitemid,
|
||||
|
||||
@@ -327,13 +327,13 @@ final class step_test extends \advanced_testcase {
|
||||
$bs->add_plugin_structure('qtype', $el);
|
||||
$patheles = $bs->get_pathelements();
|
||||
// Verify some well-known qtype plugin restore_path_elements have been added.
|
||||
$keys = array(
|
||||
$keys = [
|
||||
'/some/path/to/question/plugin_qtype_calculated_question/answers/answer',
|
||||
'/some/path/to/question/plugin_qtype_calculated_question/dataset_definitions/dataset_definition',
|
||||
'/some/path/to/question/plugin_qtype_calculated_question/calculated_options/calculated_option',
|
||||
'/some/path/to/question/plugin_qtype_essay_question/essay',
|
||||
'/some/path/to/question/plugin_qtype_random_question',
|
||||
'/some/path/to/question/plugin_qtype_truefalse_question/answers/answer');
|
||||
'/some/path/to/question/plugin_qtype_truefalse_question/answers/answer',
|
||||
];
|
||||
foreach ($keys as $key) {
|
||||
// Verify the element exists.
|
||||
$this->assertArrayHasKey($key, $patheles);
|
||||
|
||||
@@ -1508,6 +1508,15 @@ $cache = ' . var_export($cache, true) . ';
|
||||
// Modules MUST NOT have any underscores,
|
||||
// component normalisation would break very badly otherwise!
|
||||
return !is_null($pluginname) && (bool) preg_match('/^[a-z][a-z0-9]*$/', $pluginname);
|
||||
} else if ($plugintype === 'qtype' && $pluginname === 'random') {
|
||||
// Special case, qtype_random no longer exists, and we must never have a plugin with that name due to special handling
|
||||
// in the backup/restore subsystem.
|
||||
debugging(
|
||||
'qtype_random was removed from core, and random questions now use question set references. '
|
||||
. 'You must not use the name qtype_random for another plugin, as the questions will not work correctly.',
|
||||
DEBUG_DEVELOPER,
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
return !is_null($pluginname) && (bool) preg_match('/^[a-z](?:[a-z0-9_](?!__))*[a-z0-9]+$/', $pluginname);
|
||||
}
|
||||
|
||||
@@ -1644,5 +1644,18 @@ function xmldb_main_upgrade($oldversion) {
|
||||
upgrade_main_savepoint(true, 2026010900.01);
|
||||
}
|
||||
|
||||
if ($oldversion < 2026010900.02) {
|
||||
// Delete any remaining instances of qtype_random questions.
|
||||
// At this point, such questions were created during a restore, but never used by anything (otherwise they would have
|
||||
// been converted to question set references and deleted already), so they are all safe to delete.
|
||||
$questions = $DB->get_records('question', ['qtype' => 'random']);
|
||||
foreach ($questions as $question) {
|
||||
question_delete_question($question->id);
|
||||
}
|
||||
// Finally, uninstall qtype_random as it's been removed.
|
||||
uninstall_plugin('qtype', 'random');
|
||||
upgrade_main_savepoint(true, 2026010900.02);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -45,11 +45,6 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
||||
/** @var stdClass */
|
||||
protected $oldquizlayout;
|
||||
|
||||
/**
|
||||
* @var array Track old question ids that need to be removed at the end of the restore.
|
||||
*/
|
||||
protected $oldquestionids = [];
|
||||
|
||||
protected function define_structure() {
|
||||
|
||||
$paths = [];
|
||||
@@ -365,7 +360,9 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
||||
$question = $DB->get_record_sql($sql, [$questionid]);
|
||||
$module = $DB->get_record('quiz', ['id' => $data->quizid]);
|
||||
|
||||
if ($question->qtype === 'random') {
|
||||
if (!$question) {
|
||||
$randomquestion = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'qtype_random_data', $data->questionid);
|
||||
$question = $randomquestion->info;
|
||||
// Set reference data.
|
||||
$questionsetreference = new stdClass();
|
||||
$questionsetreference->usingcontextid = $this->task->get_contextid();
|
||||
@@ -374,16 +371,20 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
||||
$questionsetreference->itemid = $data->id;
|
||||
// If, in the orginal quiz that was backed up, this random question was pointing to a
|
||||
// category in the quiz question bank, then (for reasons explained in {@see restore_move_module_questions_categories})
|
||||
// right now, $question->questioncontextid will incorrectly point to the course contextid.
|
||||
// right now, $questionsetreference->questionscontextid will incorrectly point to the course contextid.
|
||||
// This will get fixed up later in restore_move_module_questions_categories
|
||||
// as part of moving the question categories to the right place.
|
||||
$questionsetreference->questionscontextid = $question->questioncontextid;
|
||||
$filtercondition = new stdClass();
|
||||
$filtercondition->questioncategoryid = $question->category;
|
||||
$filtercondition->includingsubcategories = $data->includingsubcategories ?? false;
|
||||
$questionsetreference->questionscontextid = $DB->get_field(
|
||||
'question_categories',
|
||||
'contextid',
|
||||
['id' => $question->category],
|
||||
);
|
||||
$filtercondition = core_question\question_reference_manager::convert_legacy_set_reference_filter_condition([
|
||||
'questioncategoryid' => $question->category,
|
||||
'includingsubcategories' => $data->includingsubcategories ?? false,
|
||||
]);
|
||||
$questionsetreference->filtercondition = json_encode($filtercondition);
|
||||
$DB->insert_record('question_set_references', $questionsetreference);
|
||||
$this->oldquestionids[$question->questionid] = 1;
|
||||
} else {
|
||||
// Reference data.
|
||||
$questionreference = new \stdClass();
|
||||
@@ -469,16 +470,25 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
||||
$data = (object) $data;
|
||||
$slotid = $this->get_new_parentid('quiz_question_instance');
|
||||
|
||||
if ($this->task->is_samesite() && $tag = core_tag_tag::get($data->tagid, 'id, name')) {
|
||||
$data->tagname = $tag->name;
|
||||
} else if ($tag = core_tag_tag::get_by_name(0, $data->tagname, 'id, name')) {
|
||||
$data->tagid = $tag->id;
|
||||
} else {
|
||||
$data->tagid = null;
|
||||
$data->tagname = $tag->name;
|
||||
$tagid = $data->tagid;
|
||||
if (
|
||||
!$this->task->is_samesite()
|
||||
|| !core_tag_tag::get($data->tagid, 'id, name')
|
||||
) {
|
||||
// If we're on a different site, or the tag id doesn't exist anymore, look for a tag with the same name.
|
||||
$tag = core_tag_tag::get_by_name(0, $data->tagname, 'id, name');
|
||||
$tagid = $tag ? $tag->id : null;
|
||||
}
|
||||
|
||||
if (is_null($tagid)) {
|
||||
// There is no corresponding tag, so leave it out of the filter.
|
||||
$this->log(
|
||||
get_string('restorenotag', 'quiz', (object) ['tagname' => $data->tagname, 'slotid' => $slotid]),
|
||||
\backup::LOG_WARNING,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
$tagstring = "{$data->tagid},{$data->tagname}";
|
||||
$setreferencedata = $DB->get_record('question_set_references', [
|
||||
'usingcontextid' => $this->task->get_contextid(),
|
||||
'component' => 'mod_quiz',
|
||||
@@ -486,7 +496,13 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
||||
'itemid' => $slotid,
|
||||
]);
|
||||
$filtercondition = json_decode($setreferencedata->filtercondition);
|
||||
$filtercondition->tags[] = $tagstring;
|
||||
if (!isset($filtercondition->filter->qtagids)) {
|
||||
$filtercondition->filter->qtagids = (object) [
|
||||
'jointype' => \qbank_tagquestion\tag_condition::JOINTYPE_DEFAULT,
|
||||
'values' => [],
|
||||
];
|
||||
}
|
||||
$filtercondition->filter->qtagids->values[] = $tagid;
|
||||
$setreferencedata->filtercondition = json_encode($filtercondition);
|
||||
$DB->update_record('question_set_references', $setreferencedata);
|
||||
}
|
||||
@@ -651,12 +667,4 @@ class restore_quiz_activity_structure_step extends restore_questions_activity_st
|
||||
'shufflequestions' => $this->legacyshufflequestionsoption]);
|
||||
}
|
||||
}
|
||||
|
||||
protected function after_restore() {
|
||||
parent::after_restore();
|
||||
// Delete old random questions that have been converted to set references.
|
||||
foreach (array_keys($this->oldquestionids) as $oldquestionid) {
|
||||
question_delete_question($oldquestionid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
namespace mod_quiz\output;
|
||||
|
||||
use core_question\local\bank\question_version_status;
|
||||
use mod_quiz\question\bank\qbank_helper;
|
||||
use \mod_quiz\structure;
|
||||
use \html_writer;
|
||||
use qbank_previewquestion\question_preview_options;
|
||||
@@ -802,7 +803,7 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
];
|
||||
|
||||
$data['versionoptions'] = [];
|
||||
if ($structure->get_slot_by_number($slot)->qtype !== 'random') {
|
||||
if (!$structure->get_slot_by_number($slot)->random) {
|
||||
$data['versionselection'] = true;
|
||||
$data['versionoption'] = $structure->get_version_choices_for_slot($slot);
|
||||
}
|
||||
@@ -846,7 +847,7 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
*/
|
||||
public function get_question_name_for_slot(structure $structure, int $slot, \moodle_url $pageurl): string {
|
||||
// Display the link to the question (or do nothing if question has no url).
|
||||
if ($structure->get_question_type_for_slot($slot) === 'random') {
|
||||
if ($structure->get_slot_by_number($slot)->random) {
|
||||
$questionname = $this->random_question($structure, $slot, $pageurl);
|
||||
} else {
|
||||
$questionname = $this->question_name($structure, $slot, $pageurl);
|
||||
@@ -868,7 +869,7 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
$qtype = $structure->get_question_type_for_slot($slot);
|
||||
$slotinfo = $structure->get_slot_by_number($slot);
|
||||
$questionicons = '';
|
||||
if ($qtype !== 'random' && question_bank::is_qtype_usable($qtype)) {
|
||||
if (!$slotinfo->random && question_bank::is_qtype_usable($qtype)) {
|
||||
$questionicons .= $this->question_preview_icon($structure->get_quiz(),
|
||||
$structure->get_question_in_slot($slot),
|
||||
null, null, $slotinfo->requestedversion ?: question_preview_options::ALWAYS_LATEST);
|
||||
@@ -1128,9 +1129,8 @@ class edit_renderer extends \plugin_renderer_base {
|
||||
}
|
||||
|
||||
$configuretitle = get_string('configurerandomquestion', 'quiz');
|
||||
$qtype = question_bank::get_qtype($question->qtype, false);
|
||||
$namestr = $qtype->local_name();
|
||||
$icon = $this->pix_icon('icon', $namestr, $qtype->plugin_name(), ['class' => 'icon activityicon']);
|
||||
$namestr = get_string('randomquestion', 'quiz');
|
||||
$icon = $this->pix_icon('random', $namestr, 'mod_quiz', ['class' => 'icon activityicon']);
|
||||
|
||||
$editicon = $this->pix_icon('t/edit', $configuretitle, 'moodle', ['title' => '']);
|
||||
$qbankurlparams = [
|
||||
|
||||
@@ -166,6 +166,7 @@ class qbank_helper {
|
||||
foreach ($slotdata as $slot) {
|
||||
// Ensure the right id is the id.
|
||||
$slot->id = $slot->slotid;
|
||||
$slot->random = false;
|
||||
|
||||
if ($slot->filtercondition) {
|
||||
// Unpack the information about a random question.
|
||||
@@ -175,7 +176,8 @@ class qbank_helper {
|
||||
|
||||
$slot->category = $slot->filtercondition['filter']['category']['values'][0] ?? 0;
|
||||
|
||||
$slot->qtype = 'random';
|
||||
$slot->qtype = null; // Random slots don’t have an associated question.
|
||||
$slot->random = true;
|
||||
$slot->name = get_string('random', 'quiz');
|
||||
$slot->length = 1;
|
||||
} else if ($slot->qtype === null) {
|
||||
@@ -254,7 +256,7 @@ class qbank_helper {
|
||||
$slotdata = reset($slotdata);
|
||||
|
||||
// Non-random question.
|
||||
if ($slotdata->qtype != 'random') {
|
||||
if (!$slotdata->random) {
|
||||
return $slotdata->questionid;
|
||||
}
|
||||
|
||||
|
||||
@@ -594,7 +594,7 @@ class quiz_settings {
|
||||
// Skip questions where all versions are draft.
|
||||
continue;
|
||||
}
|
||||
if ($questiondata->qtype === 'random' && $includepotential) {
|
||||
if ($questiondata->random && $includepotential) {
|
||||
$filtercondition = $questiondata->filtercondition;
|
||||
if (!empty($filtercondition)) {
|
||||
$filter = $filtercondition['filter'];
|
||||
|
||||
@@ -263,7 +263,7 @@ class structure {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (in_array($this->get_question_type_for_slot($slotnumber), ['random', 'missingtype'])) {
|
||||
if ($this->slotsinorder[$slotnumber]->random || $this->get_question_type_for_slot($slotnumber) === 'missingtype') {
|
||||
return \question_engine::can_questions_finish_during_the_attempt(
|
||||
$this->quizobj->get_quiz()->preferredbehaviour);
|
||||
}
|
||||
@@ -1787,7 +1787,7 @@ class structure {
|
||||
// Find the random slots.
|
||||
$allslots = $this->get_slots();
|
||||
foreach ($allslots as $key => $slot) {
|
||||
if ($slot->qtype != 'random') {
|
||||
if (!$slot->random) {
|
||||
unset($allslots[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -948,6 +948,7 @@ $string['requiresubnet'] = 'Require network address';
|
||||
$string['requiresubnet_help'] = 'Quiz access may be restricted to particular subnets on the LAN or Internet by specifying a comma-separated list of partial or full IP address numbers. This can be useful for an invigilated (proctored) quiz, to ensure that only people in a certain location can access the quiz.';
|
||||
$string['response'] = 'Response';
|
||||
$string['responses'] = 'Responses';
|
||||
$string['restorenotag'] = 'A tag matching \'{$a->tagname}\' could not be found when restoring the random question to quiz slot ID {$a->slotid}. The tag was removed from the question\'s filter conditions.';
|
||||
$string['results'] = 'Results';
|
||||
$string['returnattempt'] = 'Return to attempt';
|
||||
$string['reuseifpossible'] = 'reuse previously removed';
|
||||
|
||||
@@ -182,7 +182,7 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
|
||||
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->random) {
|
||||
$randomfound = true;
|
||||
continue;
|
||||
}
|
||||
@@ -204,7 +204,7 @@ function quiz_start_new_attempt($quizobj, $quba, $attempt, $attemptnumber, $time
|
||||
|
||||
foreach ($quizobj->get_questions(null, false) as $questiondata) {
|
||||
$slot += 1;
|
||||
if ($questiondata->qtype != 'random') {
|
||||
if (!$questiondata->random) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1039,7 +1039,7 @@ function quiz_attempt_state_name($state) {
|
||||
*/
|
||||
function quiz_question_action_icons($quiz, $cmid, $question, $returnurl, $variant = null) {
|
||||
$html = '';
|
||||
if ($question->qtype !== 'random') {
|
||||
if (!$question->random) {
|
||||
$html = quiz_question_preview_button($quiz, $question, false, $variant);
|
||||
}
|
||||
$html .= quiz_question_edit_button($cmid, $question, $returnurl);
|
||||
@@ -1747,15 +1747,7 @@ function quiz_add_quiz_question($questionid, $quiz, $page = 0, $maxmark = null)
|
||||
$quiz->cmid = $cm->id;
|
||||
}
|
||||
|
||||
// Make sue the question is not of the "random" type.
|
||||
$questiontype = $DB->get_field('question', 'qtype', ['id' => $questionid]);
|
||||
if ($questiontype == 'random') {
|
||||
throw new coding_exception(
|
||||
'Adding "random" questions via quiz_add_quiz_question() is deprecated. '.
|
||||
'Please use mod_quiz\structure::add_random_questions().'
|
||||
);
|
||||
}
|
||||
|
||||
// If the question type is invalid, we cannot add it to the quiz. It shouldn't be possible to get to this
|
||||
// point without fiddling with the DOM so we can just throw an exception.
|
||||
if (!\question_bank::is_qtype_installed($questiontype)) {
|
||||
|
||||
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -111,6 +111,7 @@ function quiz_report_get_significant_questions($quiz) {
|
||||
$slotreport->slot = $slot->slot;
|
||||
$slotreport->id = $slot->questionid;
|
||||
$slotreport->qtype = $slot->qtype;
|
||||
$slotreport->random = $slot->random;
|
||||
$slotreport->length = $slot->length;
|
||||
$slotreport->number = $number;
|
||||
$slotreport->displaynumber = $slot->displaynumber ?? $number;
|
||||
|
||||
@@ -54,7 +54,6 @@ final class responses_from_steps_walkthrough_test extends \mod_quiz\tests\attemp
|
||||
*/
|
||||
public function test_walkthrough_from_csv($quizsettings, $csvdata): void {
|
||||
$this->resetAfterTest(true);
|
||||
question_bank::get_qtype('random')->clear_caches_before_testing();
|
||||
|
||||
$this->create_quiz($quizsettings, $csvdata['questions']);
|
||||
|
||||
|
||||
@@ -891,7 +891,7 @@ class quiz_statistics_report extends report_base {
|
||||
$questiondata = [];
|
||||
foreach ($questions as $qs => $question) {
|
||||
$displaynumber = $question->displaynumber;
|
||||
if ($question->qtype === 'random') {
|
||||
if ($question->random) {
|
||||
$question->id = 0;
|
||||
$question->name = get_string('random', 'quiz');
|
||||
$question->questiontext = get_string('random', 'quiz');
|
||||
@@ -909,6 +909,7 @@ class quiz_statistics_report extends report_base {
|
||||
$q->slot = $question->slot;
|
||||
$q->number = $displaynumber;
|
||||
$q->parenttype = null;
|
||||
$q->random = false;
|
||||
$questiondata[$question->slot] = $q;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ class quiz_statistics_table extends flexible_table {
|
||||
$number = $number . '.'.$questionstat->subqdisplayorder;
|
||||
}
|
||||
|
||||
if ($questionstat->question->qtype != 'random' && !is_null($questionstat->variant)) {
|
||||
if (!$questionstat->question->random && !is_null($questionstat->variant)) {
|
||||
$number = $number . '.'.$questionstat->variant;
|
||||
}
|
||||
|
||||
@@ -263,7 +263,7 @@ class quiz_statistics_table extends flexible_table {
|
||||
// Question in a slot, we are not on a page showing structural analysis of one slot,
|
||||
// we don't want linking on those pages.
|
||||
$number = $questionstat->question->number;
|
||||
$israndomquestion = $questionstat->question->qtype == 'random';
|
||||
$israndomquestion = $questionstat->question->random;
|
||||
$url = new moodle_url($baseurl, ['slot' => $questionstat->slot]);
|
||||
|
||||
if ($this->is_calculated_question_summary($questionstat)) {
|
||||
|
||||
@@ -25,13 +25,12 @@ Feature: Statistics calculations with random questions
|
||||
| questioncategory | qtype | template | name | questiontext |
|
||||
| Test questions | essay | plain | Test question 1 | |
|
||||
| Test questions | essay | plain | Test question 2 | |
|
||||
| Test questions | random | | Random (Test questions) | 0 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Quiz 1 | C1 | quiz1 |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| Random (Test questions) | 1 |
|
||||
| question | page | randomcategory |
|
||||
| Random (Test questions) | 1 | Test questions |
|
||||
And user "student" has attempted "Quiz 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | Here is my wonderful essay |
|
||||
@@ -43,13 +42,12 @@ Feature: Statistics calculations with random questions
|
||||
| questioncategory | qtype | template | name | questiontext |
|
||||
| Test questions | multichoice | one_of_four | Test question 1 | |
|
||||
| Test questions | multichoice | one_of_four | Test question 2 | |
|
||||
| Test questions | random | | Random (Test questions) | 0 |
|
||||
And the following "activities" exist:
|
||||
| activity | name | course | idnumber |
|
||||
| quiz | Quiz 1 | C1 | quiz1 |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| Random (Test questions) | 1 |
|
||||
| question | page | randomcategory |
|
||||
| Random (Test questions) | 1 | Test questions |
|
||||
And user "student" has attempted "Quiz 1" with responses:
|
||||
| slot | response |
|
||||
| 1 | One |
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
slot,id,number,category,parent,name,questiontext,questiontextformat,image,generalfeedback,defaultgrade,penalty,qtype,length,stamp,version,hidden,timecreated,timemodified,createdby,modifiedby,maxmark
|
||||
1,1,1,5,0,questionessay-11,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+WeUaUK,localhost+080922073527+qxLjv1,0,1222068927,0,2,NULL,1
|
||||
2,2,2,5,0,questionessay-17,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+pvCseX,localhost+080922073527+mzy6tY,0,1222068927,0,2,NULL,1
|
||||
3,3,3,5,0,questionessay-6,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+Cr3gDO,localhost+080922073527+hnxfTy,0,1222068927,0,2,NULL,1
|
||||
4,4,4,5,0,questionessay-8,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+sSq9ln,localhost+080922073527+PGyab3,0,1222068927,0,2,NULL,1
|
||||
5,5,5,5,0,questionmatch-10,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+oG1i2f,localhost+080922073527+S1UxZy,0,1222068927,0,2,NULL,0
|
||||
6,6,6,5,0,questionmatch-16,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+vMFHyY,localhost+080922073527+4GZIyQ,0,1222068927,0,2,NULL,0
|
||||
7,7,7,5,0,questionmatch-18,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+Xkxqn1,localhost+080922073527+xbU6U7,0,1222068927,0,2,NULL,0
|
||||
8,8,8,5,0,questionmultianswer-12,This question consists of some text with an answer embedded right here {#1} and right after that you will have to deal with this short answer {#2} and finally we have a floating point number {#3}. Note that addresses like www.moodle.org and smileys :-) all work as normal: a) How good is this? {#4} b) What grade would you give it? {#5} Good luck!,0,,General feedback,8,0.1,multianswer,1,localhost+080922073527+0zKgpF,localhost+080922073527+r1gsde,0,1222068927,0,2,NULL,8
|
||||
9,14,9,5,0,questionmultichoice-13,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+AjIjeV,localhost+080922073527+UhtTLR,0,1222068927,0,2,NULL,1
|
||||
10,15,10,5,0,questionmultichoice-14,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+IrAqRl,localhost+080922073527+xRfta8,0,1222068927,0,2,NULL,1
|
||||
11,16,11,5,0,questionmultichoice-3,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+DMGirU,localhost+080922073527+689V8k,0,1222068927,0,2,NULL,1
|
||||
12,17,12,5,0,questionmultichoice-5,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+wileZw,localhost+080922073527+zGcaDa,0,1222068927,0,2,NULL,1
|
||||
13,25,13,5,0,Random Short-Answer Matching,"<p>For each of the following questions, select the matching answer from the menu.</p>",1,,,1,0.1,randomsamatch,1,localhost+080922073724+qF803I,localhost+080922075820+zbZtaD,0,1222069044,1222070300,2,2,1
|
||||
14,22,14,5,0,Is Thai difficult?,Is Thai difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+2FLtCU,localhost+080922073655+fgUeOj,0,1222069015,0,2,NULL,1
|
||||
15,23,15,5,0,Is Thai grammar difficult?,Is Thai grammar difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+LYSD32,localhost+080922073655+WgRYk4,0,1222069015,0,2,NULL,1
|
||||
16,24,16,5,0,Is Thai pronunciation difficult?,Is Thai pronunciation difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+5p1w22,localhost+080922073655+g5jrXa,0,1222069015,0,2,NULL,1
|
||||
17,20,17,5,0,Who's buried in Grant's tomb?,Who's buried in Grant's tomb?,0,,,1,0.1,shortanswer,1,localhost+080922073655+PTDcDZ,localhost+080922073655+aghyfu,0,1222069015,0,2,NULL,1
|
||||
18,21,18,5,0,Who's buried in Jamie's tomb?,Who's buried in Jamie's tomb?,0,,,1,0.1,shortanswer,1,localhost+080922073655+Xvy1ns,localhost+080922073655+Mx0Izs,0,1222069015,0,2,NULL,1
|
||||
19,18,19,5,0,questiontruefalse-7,This question is really stupid,0,,Well done,1,1,truefalse,1,localhost+080922073527+9bzTef,localhost+080922073527+hjcQR1,0,1222068927,0,2,NULL,1
|
||||
20,19,20,5,0,questiontruefalse-9,This question is really stupid,0,,Well done,1,1,truefalse,1,localhost+080922073527+TI0yD4,localhost+080922073527+iXIulQ,0,1222068927,0,2,NULL,1
|
||||
slot,id,number,category,parent,name,questiontext,questiontextformat,image,generalfeedback,defaultgrade,penalty,qtype,length,stamp,version,hidden,timecreated,timemodified,createdby,modifiedby,maxmark,random
|
||||
1,1,1,5,0,questionessay-11,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+WeUaUK,localhost+080922073527+qxLjv1,0,1222068927,0,2,NULL,1,0
|
||||
2,2,2,5,0,questionessay-17,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+pvCseX,localhost+080922073527+mzy6tY,0,1222068927,0,2,NULL,1,0
|
||||
3,3,3,5,0,questionessay-6,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+Cr3gDO,localhost+080922073527+hnxfTy,0,1222068927,0,2,NULL,1,0
|
||||
4,4,4,5,0,questionessay-8,What is the purpose of life?,0,,General feedback,1,0,essay,1,localhost+080922073527+sSq9ln,localhost+080922073527+PGyab3,0,1222068927,0,2,NULL,1,0
|
||||
5,5,5,5,0,questionmatch-10,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+oG1i2f,localhost+080922073527+S1UxZy,0,1222068927,0,2,NULL,0,0
|
||||
6,6,6,5,0,questionmatch-16,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+vMFHyY,localhost+080922073527+4GZIyQ,0,1222068927,0,2,NULL,0,0
|
||||
7,7,7,5,0,questionmatch-18,"test question, generated by script",0,,Well done,1,0.1,match,1,localhost+080922073527+Xkxqn1,localhost+080922073527+xbU6U7,0,1222068927,0,2,NULL,0,0
|
||||
8,8,8,5,0,questionmultianswer-12,This question consists of some text with an answer embedded right here {#1} and right after that you will have to deal with this short answer {#2} and finally we have a floating point number {#3}. Note that addresses like www.moodle.org and smileys :-) all work as normal: a) How good is this? {#4} b) What grade would you give it? {#5} Good luck!,0,,General feedback,8,0.1,multianswer,1,localhost+080922073527+0zKgpF,localhost+080922073527+r1gsde,0,1222068927,0,2,NULL,8,0
|
||||
9,14,9,5,0,questionmultichoice-13,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+AjIjeV,localhost+080922073527+UhtTLR,0,1222068927,0,2,NULL,1,0
|
||||
10,15,10,5,0,questionmultichoice-14,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+IrAqRl,localhost+080922073527+xRfta8,0,1222068927,0,2,NULL,1,0
|
||||
11,16,11,5,0,questionmultichoice-3,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+DMGirU,localhost+080922073527+689V8k,0,1222068927,0,2,NULL,1,0
|
||||
12,17,12,5,0,questionmultichoice-5,How old is the sun?,0,,General feedback,1,0.1,multichoice,1,localhost+080922073527+wileZw,localhost+080922073527+zGcaDa,0,1222068927,0,2,NULL,1,0
|
||||
13,25,13,5,0,Random Short-Answer Matching,"<p>For each of the following questions, select the matching answer from the menu.</p>",1,,,1,0.1,randomsamatch,1,localhost+080922073724+qF803I,localhost+080922075820+zbZtaD,0,1222069044,1222070300,2,2,1,0
|
||||
14,22,14,5,0,Is Thai difficult?,Is Thai difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+2FLtCU,localhost+080922073655+fgUeOj,0,1222069015,0,2,NULL,1,0
|
||||
15,23,15,5,0,Is Thai grammar difficult?,Is Thai grammar difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+LYSD32,localhost+080922073655+WgRYk4,0,1222069015,0,2,NULL,1,0
|
||||
16,24,16,5,0,Is Thai pronunciation difficult?,Is Thai pronunciation difficult?,0,,,1,0.1,shortanswer,1,localhost+080922073655+5p1w22,localhost+080922073655+g5jrXa,0,1222069015,0,2,NULL,1,0
|
||||
17,20,17,5,0,Who's buried in Grant's tomb?,Who's buried in Grant's tomb?,0,,,1,0.1,shortanswer,1,localhost+080922073655+PTDcDZ,localhost+080922073655+aghyfu,0,1222069015,0,2,NULL,1,0
|
||||
18,21,18,5,0,Who's buried in Jamie's tomb?,Who's buried in Jamie's tomb?,0,,,1,0.1,shortanswer,1,localhost+080922073655+Xvy1ns,localhost+080922073655+Mx0Izs,0,1222069015,0,2,NULL,1,0
|
||||
19,18,19,5,0,questiontruefalse-7,This question is really stupid,0,,Well done,1,1,truefalse,1,localhost+080922073527+9bzTef,localhost+080922073527+hjcQR1,0,1222068927,0,2,NULL,1,0
|
||||
20,19,20,5,0,questiontruefalse-9,This question is really stupid,0,,Well done,1,1,truefalse,1,localhost+080922073527+TI0yD4,localhost+080922073527+iXIulQ,0,1222068927,0,2,NULL,1,0
|
||||
|
||||
|
@@ -534,7 +534,6 @@ final class attempt_walkthrough_test extends \advanced_testcase {
|
||||
global $SITE;
|
||||
|
||||
$this->resetAfterTest(true);
|
||||
question_bank::get_qtype('random')->clear_caches_before_testing();
|
||||
|
||||
$this->setAdminUser();
|
||||
|
||||
|
||||
@@ -247,9 +247,6 @@ final class repeated_restore_test extends advanced_testcase {
|
||||
global $CFG;
|
||||
$generators = [];
|
||||
foreach (\core\plugin_manager::instance()->get_plugins_of_type('qtype') as $qtype) {
|
||||
if ($qtype->name == 'random') {
|
||||
continue;
|
||||
}
|
||||
$helperpath = "{$CFG->dirroot}/question/type/{$qtype->name}/tests/helper.php";
|
||||
if (!file_exists($helperpath)) {
|
||||
continue;
|
||||
|
||||
@@ -76,8 +76,10 @@ final class restore_39_test extends advanced_testcase {
|
||||
foreach ($references as $reference) {
|
||||
$filtercondition = json_decode($reference->filtercondition);
|
||||
// Confirm the questionscontextid is set correctly, which is from filter question category id.
|
||||
$this->assertEquals($reference->questionscontextid,
|
||||
$qcats[$filtercondition->questioncategoryid]->contextid);
|
||||
$this->assertEquals(
|
||||
$reference->questionscontextid,
|
||||
$qcats[$filtercondition->filter->category->values[0]]->contextid,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,14 +191,16 @@ class behat_mod_quiz extends behat_question_base {
|
||||
* Put the specified questions on the specified pages of a given quiz.
|
||||
*
|
||||
* The first row should be column names:
|
||||
* | question | page | maxmark | requireprevious |
|
||||
* | question | page | maxmark | requireprevious | randomcategory | includesubcategories |
|
||||
* The first two of those are required. The others are optional.
|
||||
*
|
||||
* question needs to uniquely match a question name.
|
||||
* question needs to uniquely match a question name, otherwise you must include randomcategory.
|
||||
* page is a page number. Must start at 1, and on each following
|
||||
* row should be the same as the previous, or one more.
|
||||
* maxmark What the question is marked out of. Defaults to question.defaultmark.
|
||||
* requireprevious The question can only be attempted after the previous one was completed.
|
||||
* randomcategory To add a random question, pass a non-existant name for "question", then a valid category name here.
|
||||
* includesubcategories Option for the random question category filter, if randomcategory is specified.
|
||||
*
|
||||
* Then there should be a number of rows of data, one for each question you want to add.
|
||||
*
|
||||
@@ -251,7 +253,7 @@ class behat_mod_quiz extends behat_question_base {
|
||||
JOIN {question_versions} qv ON qv.questionid = q.id
|
||||
JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
|
||||
WHERE q.name = :name';
|
||||
$question = $DB->get_record_sql($sql, ['name' => $questiondata['question']], MUST_EXIST);
|
||||
$question = $DB->get_record_sql($sql, ['name' => $questiondata['question']]);
|
||||
|
||||
// Page number.
|
||||
$page = clean_param($questiondata['page'], PARAM_INT);
|
||||
@@ -280,7 +282,14 @@ class behat_mod_quiz extends behat_question_base {
|
||||
}
|
||||
}
|
||||
|
||||
if ($question->qtype == 'random') {
|
||||
if (!$question) {
|
||||
if (!array_key_exists('randomcategory', $questiondata) && $questiondata['randomcategory'] !== '') {
|
||||
throw new ExpectationException(
|
||||
'To add a random question, you must include a category name in the randomcategory column.',
|
||||
$this->getSession()->getDriver(),
|
||||
);
|
||||
}
|
||||
$categoryid = $DB->get_field('question_categories', 'id', ['name' => $questiondata['randomcategory']], MUST_EXIST);
|
||||
if (!array_key_exists('includingsubcategories', $questiondata) || $questiondata['includingsubcategories'] === '') {
|
||||
$includingsubcategories = false;
|
||||
} else {
|
||||
@@ -290,7 +299,7 @@ class behat_mod_quiz extends behat_question_base {
|
||||
$filter = [
|
||||
'category' => [
|
||||
'jointype' => \qbank_managecategories\category_condition::JOINTYPE_DEFAULT,
|
||||
'values' => [$question->category],
|
||||
'values' => [$categoryid],
|
||||
'filteroptions' => ['includesubcategories' => $includingsubcategories],
|
||||
],
|
||||
];
|
||||
|
||||
@@ -61,11 +61,10 @@ Feature: Edit quizzes where some questions require the previous one to have been
|
||||
And the following "questions" exist:
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | TF1 | First question |
|
||||
| Test questions | random | Random (Test questions) | 0 |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page | requireprevious |
|
||||
| Random (Test questions) | 1 | 0 |
|
||||
| TF1 | 1 | 1 |
|
||||
| question | page | requireprevious | randomcategory |
|
||||
| Random (Test questions) | 1 | 0 | Test questions |
|
||||
| TF1 | 1 | 1 | |
|
||||
And I am on the "Quiz 1" "mod_quiz > Edit" page
|
||||
Then "This question cannot be attempted until the previous question has been completed." "link" should be visible
|
||||
|
||||
@@ -118,12 +117,11 @@ Feature: Edit quizzes where some questions require the previous one to have been
|
||||
| questioncategory | qtype | name | questiontext |
|
||||
| Test questions | truefalse | TF1 | First question |
|
||||
| Test questions | truefalse | TF2 | Second question |
|
||||
| Test questions | random | Random (Test questions) | 0 |
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page | requireprevious |
|
||||
| Random (Test questions) | 1 | 0 |
|
||||
| TF1 | 1 | 1 |
|
||||
| TF2 | 1 | 1 |
|
||||
| question | page | requireprevious | randomcategory |
|
||||
| Random (Test questions) | 1 | 0 | Test questions |
|
||||
| TF1 | 1 | 1 | |
|
||||
| TF2 | 1 | 1 | |
|
||||
And I am on the "Quiz 1" "mod_quiz > Edit" page
|
||||
Then "be attempted" "link" in the "TF1" "list_item" should not be visible
|
||||
Then "be attempted" "link" in the "TF2" "list_item" should not be visible
|
||||
|
||||
@@ -31,15 +31,14 @@ Feature: Teacher can build quiz in a single activity format course
|
||||
| Test questions | multichoice | one_of_four | MCQ2 |
|
||||
| Qbank questions 1 | truefalse | | TFQ1 |
|
||||
| Qbank questions 2 | truefalse | | TFQ2 |
|
||||
| Test questions | random | | Random (Test questions) |
|
||||
# Add questions from different question categories and question banks to the quiz.
|
||||
# Add at least 1 random question.
|
||||
And quiz "Quiz 1" contains the following questions:
|
||||
| question | page |
|
||||
| MCQ1 | 1 |
|
||||
| TFQ1 | 1 |
|
||||
| TFQ2 | 2 |
|
||||
| Random (Test questions) | 3 |
|
||||
| question | page | randomcategory |
|
||||
| MCQ1 | 1 | |
|
||||
| TFQ1 | 1 | |
|
||||
| TFQ2 | 2 | |
|
||||
| Random (Test questions) | 3 | Test questions |
|
||||
|
||||
@javascript
|
||||
Scenario: Student can preview and answer quiz in a single activity format course
|
||||
|
||||
Binary file not shown.
@@ -116,15 +116,11 @@ final class lib_test extends \advanced_testcase {
|
||||
quiz_add_quiz_question($standardq->id, $quiz);
|
||||
$this->add_random_questions($quiz->id, 0, $cat->id, 1);
|
||||
|
||||
// Get the random question.
|
||||
$randomq = $DB->get_record('question', ['qtype' => 'random']);
|
||||
// Confirm we have a question set reference for the random question.
|
||||
$this->assertEquals(1, $DB->count_records('question_set_references', ['usingcontextid' => $context->id]));
|
||||
|
||||
quiz_delete_instance($quiz->id);
|
||||
|
||||
// Check that the random question was deleted.
|
||||
if ($randomq) {
|
||||
$this->assertEquals(0, $DB->count_records('question', ['id' => $randomq->id]));
|
||||
}
|
||||
// Check that the standard question was not deleted.
|
||||
$this->assertEquals(1, $DB->count_records('question', ['id' => $standardq->id]));
|
||||
|
||||
|
||||
@@ -617,13 +617,166 @@ final class quiz_question_restore_test extends \advanced_testcase {
|
||||
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
|
||||
$filterconditions = json_decode($setreference->filtercondition);
|
||||
$tags = [];
|
||||
foreach ($filterconditions->tags as $tagstring) {
|
||||
$tag = explode(',', $tagstring);
|
||||
$tags[] = $tag[1];
|
||||
foreach ($filterconditions->filter->qtagids->values as $tagid) {
|
||||
$tags[] = \core_tag_tag::get($tagid, 'id, name')->name;
|
||||
}
|
||||
$this->assertEquals([], array_diff($randomtags[$slot->slot], $tags));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pre 4.0 quiz restore for random question tags, where one of the tags in the random question condition is
|
||||
* not in the backup but does already exist on the site.
|
||||
*
|
||||
* @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
|
||||
*/
|
||||
public function test_pre_4_quiz_restore_for_random_question_tags_with_tag_not_in_backup(): void {
|
||||
global $DB, $USER;
|
||||
$this->resetAfterTest();
|
||||
$collection = \core_tag_collection::create((object) ['name' => 'question']);
|
||||
$missingtags = \core_tag_tag::create_if_missing($collection->id, ['name' => 'second question']);
|
||||
$missingtag = reset($missingtags);
|
||||
$randomtags = [
|
||||
'1' => ['first question' => null, 'one' => null, 'number one' => null],
|
||||
'2' => ['first question' => null, 'one' => null, 'number one' => null],
|
||||
'3' => ['one' => null, 'number one' => null, 'second question' => $missingtag->id],
|
||||
];
|
||||
$backupid = 'abc';
|
||||
$backuppath = make_backup_temp_directory($backupid);
|
||||
get_file_packer('application/vnd.moodle.backup')
|
||||
->extract_to_pathname(
|
||||
__DIR__ . "/fixtures/moodle_311_quiz_missing_tag.mbz",
|
||||
$backuppath,
|
||||
);
|
||||
|
||||
// Do the restore to new course with default settings.
|
||||
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
|
||||
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
|
||||
|
||||
$rc = new \restore_controller(
|
||||
$backupid,
|
||||
$newcourseid,
|
||||
\backup::INTERACTIVE_NO,
|
||||
\backup::MODE_GENERAL,
|
||||
$USER->id,
|
||||
\backup::TARGET_NEW_COURSE,
|
||||
);
|
||||
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Get the information about the resulting course and check that it is set up correctly.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
|
||||
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
|
||||
// Count the questions in quiz qbank.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
|
||||
$this->assertEquals(2, $this->question_count($context->id));
|
||||
|
||||
// Are the correct slots returned?
|
||||
$slots = $structure->get_slots();
|
||||
$this->assertCount(3, $slots);
|
||||
|
||||
// Check if the tags match with the actual restored data.
|
||||
foreach ($slots as $slot) {
|
||||
$setreference = $DB->get_record(
|
||||
'question_set_references',
|
||||
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot'],
|
||||
);
|
||||
$filterconditions = json_decode($setreference->filtercondition);
|
||||
$this->assertCount(count($randomtags[$slot->slot]), $filterconditions->filter->qtagids->values);
|
||||
foreach ($filterconditions->filter->qtagids->values as $tagid) {
|
||||
$name = \core_tag_tag::get($tagid, 'id, name')->name;
|
||||
$this->assertTrue(array_key_exists($name, $randomtags[$slot->slot]));
|
||||
// Check that tags in a filter, which were not otherwise present in the backup, were matched with the tag already
|
||||
// on the site.
|
||||
$id = $randomtags[$slot->slot][$name];
|
||||
if (!is_null($id)) {
|
||||
$this->assertEquals($id, $tagid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test pre 4.0 quiz restore for random question tags, where one of the tags in the random question condition is
|
||||
* not in the backup or the target site.
|
||||
*
|
||||
* @covers \restore_quiz_activity_structure_step::process_quiz_question_legacy_instance
|
||||
*/
|
||||
public function test_pre_4_quiz_restore_for_random_question_tags_with_tag_not_in_backup_or_site(): void {
|
||||
global $DB, $USER;
|
||||
$this->resetAfterTest();
|
||||
$randomtags = [
|
||||
'1' => ['first question', 'one', 'number one'],
|
||||
'2' => ['first question', 'one', 'number one'],
|
||||
'3' => ['one', 'number one'],
|
||||
];
|
||||
$backupid = 'abc';
|
||||
$backuppath = make_backup_temp_directory($backupid);
|
||||
get_file_packer('application/vnd.moodle.backup')
|
||||
->extract_to_pathname(
|
||||
__DIR__ . "/fixtures/moodle_311_quiz_missing_tag.mbz",
|
||||
$backuppath,
|
||||
);
|
||||
|
||||
// Do the restore to new course with default settings.
|
||||
$categoryid = $DB->get_field_sql("SELECT MIN(id) FROM {course_categories}");
|
||||
$newcourseid = \restore_dbops::create_new_course('Test fullname', 'Test shortname', $categoryid);
|
||||
$rc = new \restore_controller(
|
||||
$backupid,
|
||||
$newcourseid,
|
||||
\backup::INTERACTIVE_NO,
|
||||
\backup::MODE_GENERAL,
|
||||
$USER->id,
|
||||
\backup::TARGET_NEW_COURSE,
|
||||
);
|
||||
|
||||
$this->assertTrue($rc->execute_precheck());
|
||||
$rc->execute_plan();
|
||||
$rc->destroy();
|
||||
|
||||
// Get the information about the resulting course and check that it is set up correctly.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
$quiz = array_values($modinfo->get_instances_of('quiz'))[0];
|
||||
$quizobj = \mod_quiz\quiz_settings::create($quiz->instance);
|
||||
$structure = \mod_quiz\structure::create_for_quiz($quizobj);
|
||||
|
||||
// Count the questions in quiz qbank.
|
||||
$context = \context_module::instance(get_coursemodule_from_instance("quiz", $quizobj->get_quizid(), $newcourseid)->id);
|
||||
$this->assertEquals(2, $this->question_count($context->id));
|
||||
|
||||
// Are the correct slots returned?
|
||||
$slots = $structure->get_slots();
|
||||
$this->assertCount(3, $slots);
|
||||
|
||||
// Check if the tags match with the actual restored data.
|
||||
foreach ($slots as $slot) {
|
||||
$setreference = $DB->get_record(
|
||||
'question_set_references',
|
||||
['itemid' => $slot->id, 'component' => 'mod_quiz', 'questionarea' => 'slot'],
|
||||
);
|
||||
$filterconditions = json_decode($setreference->filtercondition);
|
||||
$tags = [];
|
||||
foreach ($filterconditions->filter->qtagids->values as $tagid) {
|
||||
$tags[] = \core_tag_tag::get($tagid, 'id, name')->name;
|
||||
}
|
||||
$this->assertEquals([], array_diff($randomtags[$slot->slot], $tags));
|
||||
}
|
||||
$log = $DB->get_fieldset(
|
||||
'backup_logs',
|
||||
'message',
|
||||
['backupid' => $rc->get_restoreid(), 'loglevel' => \backup::LOG_WARNING],
|
||||
);
|
||||
$slotid = end($slots)->id;
|
||||
$this->assertContains(
|
||||
"A tag matching 'second question' could not be found when restoring the random question to quiz slot "
|
||||
. "ID {$slotid}. The tag was removed from the question's filter conditions.",
|
||||
$log,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -88,9 +88,7 @@ final class tags_test extends \advanced_testcase {
|
||||
$this->assertNotFalse($tag3);
|
||||
|
||||
$slottags = $this->get_tags_for_slot($question->slotid);
|
||||
$slottags = reset($slottags);
|
||||
$slottags = explode(',', $slottags);
|
||||
$this->assertEquals("{$tag2->id},{$tag2->name}", "{$slottags[0]},{$slottags[1]}");
|
||||
$this->assertEquals($tag2->id, $slottags[0]);
|
||||
|
||||
// Course context question cats get restored to a default qbank module instance.
|
||||
$modinfo = get_fast_modinfo($newcourseid);
|
||||
@@ -103,7 +101,7 @@ final class tags_test extends \advanced_testcase {
|
||||
$randomincludingsubcategories = $DB->get_record('question_set_references',
|
||||
['itemid' => reset($slots)->id, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
|
||||
$filtercondition = json_decode($randomincludingsubcategories->filtercondition);
|
||||
$this->assertEquals(0, $filtercondition->includingsubcategories);
|
||||
$this->assertEquals(0, $filtercondition->filter->category->filteroptions->includesubcategories);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -118,8 +116,8 @@ final class tags_test extends \advanced_testcase {
|
||||
['itemid' => $slotid, 'component' => 'mod_quiz', 'questionarea' => 'slot']);
|
||||
if (isset($referencedata->filtercondition)) {
|
||||
$filtercondition = json_decode($referencedata->filtercondition);
|
||||
if (isset($filtercondition->tags)) {
|
||||
return $filtercondition->tags;
|
||||
if (isset($filtercondition->filter->qtagids)) {
|
||||
return $filtercondition->filter->qtagids->values;
|
||||
}
|
||||
}
|
||||
return [];
|
||||
|
||||
@@ -119,10 +119,11 @@ final class helper_test extends manage_category_test_base {
|
||||
MUST_EXIST
|
||||
);
|
||||
|
||||
// The following 2 lines have to be after the add_random_questions() call above.
|
||||
// Otherwise, add_random_questions() will have to be "smart" and use them instead of creating a new "random" question.
|
||||
$q1b = $this->create_question_in_a_category('random', $qcat1->id);
|
||||
$q2c = $this->create_question_in_a_category('random', $qcat2->id);
|
||||
// Create a couple of questions then edit their qtype so they look like legacy random questions.
|
||||
$q1b = $this->create_question_in_a_category('description', $qcat1->id);
|
||||
$DB->set_field('question', 'qtype', 'random', ['id' => $q1b->id]);
|
||||
$q2c = $this->create_question_in_a_category('description', $qcat2->id);
|
||||
$DB->set_field('question', 'qtype', 'random', ['id' => $q2c->id]);
|
||||
|
||||
$contexts = new \core_question\local\bank\question_edit_contexts(\context_module::instance($quiz->cmid));
|
||||
$manager = new category_manager();
|
||||
|
||||
@@ -467,6 +467,7 @@ class all_calculated_for_qubaid_condition {
|
||||
protected function make_new_subq_stat_for($displaynumber, $slot, $subqid, $variant = null) {
|
||||
$slotstat = fullclone($this->for_subq($subqid, $variant));
|
||||
$slotstat->question->number = $this->for_slot($slot)->question->number;
|
||||
$slotstat->question->random = false;
|
||||
$slotstat->subqdisplayorder = $displaynumber;
|
||||
return $slotstat;
|
||||
}
|
||||
|
||||
@@ -208,7 +208,7 @@ class calculated {
|
||||
* @return null|string represents the pool of questions from which this question draws if it is random, or null if not.
|
||||
*/
|
||||
public function random_selector_string() {
|
||||
if ($this->question->qtype == 'random') {
|
||||
if ($this->question->random) {
|
||||
return $this->question->category .'/'. $this->question->questiontext;
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -67,7 +67,7 @@ class calculator {
|
||||
* Constructor.
|
||||
*
|
||||
* @param object[] questions to analyze, keyed by slot, also analyses sub questions for random questions.
|
||||
* we expect some extra fields - slot, maxmark and number on the full question data objects.
|
||||
* We expect some extra fields - slot, random, maxmark and number on the full question data objects.
|
||||
* @param \core\progress\base|null $progress the element to send progress messages to, default is {@link \core\progress\none}.
|
||||
*/
|
||||
public function __construct($questions, $progress = null) {
|
||||
@@ -194,7 +194,7 @@ class calculator {
|
||||
$this->initial_question_walker($this->stats->for_slot($slot));
|
||||
|
||||
// The rest of this loop is to finish working out where randomly selected question stats should be displayed.
|
||||
if ($this->stats->for_slot($slot)->question->qtype == 'random') {
|
||||
if ($this->stats->for_slot($slot)->question->random) {
|
||||
$randomselectorstring = $this->stats->for_slot($slot)->random_selector_string();
|
||||
if ($nextslot && ($randomselectorstring == $this->stats->for_slot($nextslot)->random_selector_string())) {
|
||||
continue; // Next loop iteration.
|
||||
@@ -210,10 +210,9 @@ class calculator {
|
||||
$this->progress->start_progress('', count($lateststeps), 1);
|
||||
foreach ($lateststeps as $step) {
|
||||
$this->progress->increment_progress();
|
||||
$israndomquestion = ($this->stats->for_slot($step->slot)->question->qtype == 'random');
|
||||
$this->secondary_steps_walker($step, $this->stats->for_slot($step->slot), $summarks);
|
||||
|
||||
if ($israndomquestion) {
|
||||
if ($this->stats->for_slot($step->slot)->question->random) {
|
||||
$this->secondary_steps_walker($step, $this->stats->for_subq($step->questionid), $summarks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -971,10 +971,6 @@ class qformat_default {
|
||||
$question->status = 1;
|
||||
}
|
||||
|
||||
// do not export random questions
|
||||
if ($question->qtype == 'random') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check if we need to record category change
|
||||
if ($this->cattofile) {
|
||||
|
||||
@@ -366,6 +366,12 @@ class question_type {
|
||||
public function save_question($question, $form) {
|
||||
global $USER, $DB;
|
||||
|
||||
if ($question->qtype === 'random') {
|
||||
throw new \core\exception\coding_exception(
|
||||
'You cannot create a question with qtype "random". You must create a question set reference instead.',
|
||||
);
|
||||
}
|
||||
|
||||
// The actual update/insert done with multiple DB access, so we do it in a transaction.
|
||||
$transaction = $DB->start_delegated_transaction ();
|
||||
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* @package moodlecore
|
||||
* @subpackage backup-moodle2
|
||||
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* restore plugin class that provides the necessary information
|
||||
* needed to restore one random qtype plugin
|
||||
*
|
||||
* @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class restore_qtype_random_plugin extends restore_qtype_plugin {
|
||||
|
||||
/**
|
||||
* Define the plugin structure.
|
||||
*
|
||||
* @return array Array of {@link restore_path_elements}.
|
||||
*/
|
||||
protected function define_question_plugin_structure() {
|
||||
$paths = array();
|
||||
|
||||
// We have to specify a path here if we want after_execute_question to be called.
|
||||
$elename = 'donothing';
|
||||
$elepath = $this->get_pathfor('/');
|
||||
|
||||
$paths[] = new restore_path_element($elename, $elepath);
|
||||
|
||||
return $paths; // And we return the interesting paths.
|
||||
}
|
||||
|
||||
/**
|
||||
* Required function to process path. Should never be called.
|
||||
*
|
||||
* @param object $data Data elements.
|
||||
*/
|
||||
public function process_donothing($data) {
|
||||
// Intentionally blank.
|
||||
}
|
||||
|
||||
/**
|
||||
* Given one question_states record, return the answer
|
||||
* recoded pointing to all the restored stuff for random questions
|
||||
*
|
||||
* answer format is randomxx-yy, with xx being question->id and
|
||||
* yy the actual response to the question. We'll delegate the recode
|
||||
* to the corresponding qtype
|
||||
*
|
||||
* also, some old states can contain, simply, one question->id,
|
||||
* support them, just in case
|
||||
*/
|
||||
public function recode_legacy_state_answer($state) {
|
||||
global $DB;
|
||||
|
||||
$answer = $state->answer;
|
||||
$result = '';
|
||||
// Randomxx-yy answer format.
|
||||
if (preg_match('~^random([0-9]+)-(.*)$~', $answer, $matches)) {
|
||||
$questionid = $matches[1];
|
||||
$subanswer = $matches[2];
|
||||
$newquestionid = $this->get_mappingid('question', $questionid);
|
||||
$questionqtype = $DB->get_field('question', 'qtype', array('id' => $newquestionid));
|
||||
// Delegate subanswer recode to proper qtype, faking one question_states record.
|
||||
$substate = new stdClass();
|
||||
$substate->question = $newquestionid;
|
||||
$substate->answer = $subanswer;
|
||||
$newanswer = $this->step->restore_recode_legacy_answer($substate, $questionqtype);
|
||||
$result = 'random' . $newquestionid . '-' . $newanswer;
|
||||
|
||||
// Simple question id format.
|
||||
} else {
|
||||
$newquestionid = $this->get_mappingid('question', $answer);
|
||||
$result = $newquestionid;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* After restoring, make sure questiontext is set properly.
|
||||
*/
|
||||
public function after_execute_question() {
|
||||
global $DB;
|
||||
|
||||
// For random questions, questiontext should only ever be '0' or '1'.
|
||||
// In the past there were sometimes junk values like ''. If there
|
||||
// were any in the restore, fix them up.
|
||||
//
|
||||
// Note, we cannot just do this in one DB query, because MySQL is useless.
|
||||
// The expected case is that the SELECT returns 0 rows, so loading all the
|
||||
// ids should not be a problem.
|
||||
$problemquestions = $DB->get_records_sql_menu("
|
||||
SELECT q.id, 1
|
||||
FROM {question} q
|
||||
JOIN {backup_ids_temp} bi ON q.id = bi.newitemid
|
||||
WHERE q.qtype = 'random'
|
||||
AND " . $DB->sql_compare_text('q.questiontext') . " = ?
|
||||
AND bi.backupid = ?
|
||||
AND bi.itemname = 'question_created'
|
||||
", array('', $this->get_restoreid()));
|
||||
|
||||
if (!$problemquestions) {
|
||||
return; // Nothing to do.
|
||||
}
|
||||
|
||||
list($idtest, $params) = $DB->get_in_or_equal(array_keys($problemquestions));
|
||||
$DB->set_field_select('question', 'questiontext', '0', "id $idtest", $params);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Privacy Subsystem implementation for qtype_random.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace qtype_random\privacy;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
/**
|
||||
* Privacy Subsystem for qtype_random implementing null_provider.
|
||||
*
|
||||
* @copyright 2018 Andrew Nicols <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class provider implements \core_privacy\local\metadata\null_provider {
|
||||
|
||||
/**
|
||||
* Get the language string identifier with the component's language
|
||||
* file to explain why this plugin stores no data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function get_reason(): string {
|
||||
return 'privacy:metadata';
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Random question type upgrade code.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @copyright 2014 Eric Merrill
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
/**
|
||||
* Upgrade code for the random question type.
|
||||
* @param int $oldversion the version we are upgrading from.
|
||||
*/
|
||||
function xmldb_qtype_random_upgrade($oldversion) {
|
||||
// Automatically generated Moodle v4.4.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Automatically generated Moodle v4.5.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Automatically generated Moodle v5.0.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
// Automatically generated Moodle v5.1.0 release upgrade line.
|
||||
// Put any upgrade step following this.
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Defines the editing form for the random question type.
|
||||
*
|
||||
* @package qtype
|
||||
* @subpackage random
|
||||
* @copyright 2007 Jamie Pratt [email protected]
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* random editing form definition.
|
||||
*
|
||||
* @copyright 2007 Jamie Pratt [email protected]
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_random_edit_form extends question_edit_form {
|
||||
/**
|
||||
* Build the form definition.
|
||||
*
|
||||
* This adds all the form files that the default question type supports.
|
||||
* If your question type does not support all these fields, then you can
|
||||
* override this method and remove the ones you don't want with $mform->removeElement().
|
||||
*/
|
||||
protected function definition() {
|
||||
$mform = $this->_form;
|
||||
|
||||
// Standard fields at the start of the form.
|
||||
$mform->addElement('header', 'generalheader', get_string("general", 'form'));
|
||||
|
||||
$mform->addElement('questioncategory', 'category', get_string('category', 'question'),
|
||||
array('contexts' => $this->contexts->having_cap('moodle/question:useall'), 'top' => true));
|
||||
|
||||
$mform->addElement('advcheckbox', 'questiontext[text]',
|
||||
get_string('includingsubcategories', 'qtype_random'), null, null, array(0, 1));
|
||||
|
||||
$tops = question_get_top_categories_for_contexts(array_column($this->contexts->all(), 'id'));
|
||||
$mform->hideIf('questiontext[text]', 'category', 'in', $tops);
|
||||
|
||||
$mform->addElement('hidden', 'qtype');
|
||||
$mform->setType('qtype', PARAM_ALPHA);
|
||||
|
||||
$this->add_hidden_fields();
|
||||
|
||||
$buttonarray = array();
|
||||
$buttonarray[] = $mform->createElement('submit', 'submitbutton', get_string('savechanges'));
|
||||
$buttonarray[] = $mform->createElement('cancel');
|
||||
$mform->addGroup($buttonarray, 'buttonar', '', array(' '), false);
|
||||
$mform->closeHeaderBefore('buttonar');
|
||||
}
|
||||
|
||||
public function set_data($question) {
|
||||
$question->questiontext = array('text' => $question->questiontext);
|
||||
// We don't want the complex stuff in the base class to run.
|
||||
moodleform::set_data($question);
|
||||
}
|
||||
|
||||
public function validation($fromform, $files) {
|
||||
// Validation of category is not relevant for this question type.
|
||||
|
||||
return array();
|
||||
}
|
||||
|
||||
public function qtype() {
|
||||
return 'random';
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Strings for component 'qtype_random', language 'en', branch 'MOODLE_20_STABLE'
|
||||
*
|
||||
* @package qtype
|
||||
* @subpackage random
|
||||
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
$string['configselectmanualquestions'] = 'Can the random question type select a manually graded question when it is making its random choice of a question from a category?';
|
||||
$string['includingsubcategories'] = 'Including subcategories';
|
||||
$string['pluginname'] = 'Random';
|
||||
$string['pluginname_help'] = 'A random question is not a question type as such, but is a way of inserting a randomly-chosen question from a specified category into an activity.';
|
||||
$string['pluginnameediting'] = 'Editing a random question';
|
||||
$string['privacy:metadata'] = 'The Random question type plugin does not store any personal data.';
|
||||
$string['randomqname'] = 'Random ({$a})';
|
||||
$string['randomqnamefromtop'] = 'Faulty random question! Please delete this question.';
|
||||
$string['randomqnamefromtoptags'] = 'Faulty random question! Please delete this question.';
|
||||
$string['randomqnametags'] = 'Random ({$a->category}, tags: {$a->tags})';
|
||||
$string['randomqplusname'] = 'Random ({$a} and subcategories)';
|
||||
$string['randomqplusnamecourse'] = 'Random (Any category in this course)';
|
||||
$string['randomqplusnamecoursecat'] = 'Random (Any category inside course category {$a})';
|
||||
$string['randomqplusnamecoursecattags'] = 'Random (Any category inside course category {$a->category}, tags: {$a->tags})';
|
||||
$string['randomqplusnamecoursetags'] = 'Random (Any category in this course, tags: {$a->tags})';
|
||||
$string['randomqplusnamemodule'] = 'Random (Any category of this quiz)';
|
||||
$string['randomqplusnamemoduletags'] = 'Random (Any category of this quiz, tags: {$a->tags})';
|
||||
$string['randomqplusnamesystem'] = 'Random (Any system-level category)';
|
||||
$string['randomqplusnamesystemtags'] = 'Random (Any system-level category, tags: {$a->tags})';
|
||||
$string['randomqplusnametags'] = 'Random ({$a->category} and subcategories, tags: {$a->tags})';
|
||||
$string['selectedby'] = '{$a->questionname} selected by {$a->randomname}';
|
||||
$string['selectmanualquestions'] = 'Random questions can use manually graded questions';
|
||||
$string['taskunusedrandomscleanup'] = 'Remove unused random questions';
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,321 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Question type class for the random question type.
|
||||
*
|
||||
* @package qtype
|
||||
* @subpackage random
|
||||
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once($CFG->dirroot . '/question/type/questiontypebase.php');
|
||||
|
||||
|
||||
/**
|
||||
* The random question type.
|
||||
*
|
||||
* This question type does not have a question definition class, nor any
|
||||
* renderers. When you load a question of this type, it actually loads a
|
||||
* question chosen randomly from a particular category in the question bank.
|
||||
*
|
||||
* @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_random extends question_type {
|
||||
/** @var string comma-separated list of qytpe names not to select, can be used in SQL. */
|
||||
protected $excludedqtypes = null;
|
||||
|
||||
/** @var string comma-separated list of manually graded qytpe names, can be used in SQL. */
|
||||
protected $manualqtypes = null;
|
||||
|
||||
/**
|
||||
* Cache of availabe question ids from a particular category.
|
||||
* @var array two-dimensional array. The first key is a category id, the
|
||||
* second key is wether subcategories should be included.
|
||||
*/
|
||||
private $availablequestionsbycategory = array();
|
||||
|
||||
public function menu_name() {
|
||||
// Don't include this question type in the 'add new question' menu.
|
||||
return false;
|
||||
}
|
||||
|
||||
public function is_manual_graded() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function is_usable_by_random() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function is_question_manual_graded($question, $otherquestionsinuse) {
|
||||
global $DB;
|
||||
// We take our best shot at working whether a particular question is manually
|
||||
// graded follows: We look to see if any of the questions that this random
|
||||
// question might select if of a manually graded type. If a category contains
|
||||
// a mixture of manual and non-manual questions, and if all the attempts so
|
||||
// far selected non-manual ones, this will give the wrong answer, but we
|
||||
// don't care. Even so, this is an expensive calculation!
|
||||
$this->init_qtype_lists();
|
||||
if (!$this->manualqtypes) {
|
||||
return false;
|
||||
}
|
||||
if ($question->questiontext) {
|
||||
$categorylist = question_categorylist($question->category);
|
||||
} else {
|
||||
$categorylist = array($question->category);
|
||||
}
|
||||
list($qcsql, $qcparams) = $DB->get_in_or_equal($categorylist);
|
||||
// TODO use in_or_equal for $otherquestionsinuse and $this->manualqtypes.
|
||||
|
||||
$readystatus = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
$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
|
||||
WHERE qbe.questioncategoryid {$qcsql}
|
||||
AND q.parent = 0
|
||||
AND qv.status = '$readystatus'
|
||||
AND q.id NOT IN ($otherquestionsinuse)
|
||||
AND q.qtype IN ($this->manualqtypes)";
|
||||
|
||||
return $DB->record_exists_sql($sql, $qcparams);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method needs to be called before the ->excludedqtypes and
|
||||
* ->manualqtypes fields can be used.
|
||||
*/
|
||||
protected function init_qtype_lists() {
|
||||
if (!is_null($this->excludedqtypes)) {
|
||||
return; // Already done.
|
||||
}
|
||||
$excludedqtypes = array();
|
||||
$manualqtypes = array();
|
||||
foreach (question_bank::get_all_qtypes() as $qtype) {
|
||||
$quotedname = "'" . $qtype->name() . "'";
|
||||
if (!$qtype->is_usable_by_random()) {
|
||||
$excludedqtypes[] = $quotedname;
|
||||
} else if ($qtype->is_manual_graded()) {
|
||||
$manualqtypes[] = $quotedname;
|
||||
}
|
||||
}
|
||||
$this->excludedqtypes = implode(',', $excludedqtypes);
|
||||
$this->manualqtypes = implode(',', $manualqtypes);
|
||||
}
|
||||
|
||||
public function get_question_options($question) {
|
||||
parent::get_question_options($question);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Random questions always get a question name that is Random (cateogryname).
|
||||
* This function is a centralised place to calculate that, given the category.
|
||||
* @param stdClass $category the category this question picks from. (->parent, ->name & ->contextid are used.)
|
||||
* @param bool $includesubcategories whether this question also picks from subcategories.
|
||||
* @param string[] $tagnames Name of tags this question picks from.
|
||||
* @return string the name this question should have.
|
||||
*/
|
||||
public function question_name($category, $includesubcategories, $tagnames = []) {
|
||||
$categoryname = '';
|
||||
if ($category->parent && $includesubcategories) {
|
||||
$stringid = 'randomqplusname';
|
||||
$categoryname = shorten_text($category->name, 100);
|
||||
} else if ($category->parent) {
|
||||
$stringid = 'randomqname';
|
||||
$categoryname = shorten_text($category->name, 100);
|
||||
} else if ($includesubcategories) {
|
||||
$context = context::instance_by_id($category->contextid);
|
||||
|
||||
switch ($context->contextlevel) {
|
||||
case CONTEXT_MODULE:
|
||||
$stringid = 'randomqplusnamemodule';
|
||||
break;
|
||||
case CONTEXT_COURSE:
|
||||
$stringid = 'randomqplusnamecourse';
|
||||
break;
|
||||
case CONTEXT_COURSECAT:
|
||||
$stringid = 'randomqplusnamecoursecat';
|
||||
$categoryname = shorten_text($context->get_context_name(false), 100);
|
||||
break;
|
||||
case CONTEXT_SYSTEM:
|
||||
$stringid = 'randomqplusnamesystem';
|
||||
break;
|
||||
default: // Impossible.
|
||||
}
|
||||
} else {
|
||||
// No question will ever be selected. So, let's warn the teacher.
|
||||
$stringid = 'randomqnamefromtop';
|
||||
}
|
||||
|
||||
if ($tagnames) {
|
||||
$stringid .= 'tags';
|
||||
$a = new stdClass();
|
||||
if ($categoryname) {
|
||||
$a->category = $categoryname;
|
||||
}
|
||||
$a->tags = implode(', ', array_map(function($tagname) {
|
||||
return explode(',', $tagname)[1];
|
||||
}, $tagnames));
|
||||
} else {
|
||||
$a = $categoryname ? : null;
|
||||
}
|
||||
|
||||
$name = get_string($stringid, 'qtype_random', $a);
|
||||
|
||||
return shorten_text($name, 255);
|
||||
}
|
||||
|
||||
protected function set_selected_question_name($question, $randomname) {
|
||||
$a = new stdClass();
|
||||
$a->randomname = $randomname;
|
||||
$a->questionname = $question->name;
|
||||
$question->name = get_string('selectedby', 'qtype_random', $a);
|
||||
}
|
||||
|
||||
public function save_question($question, $form) {
|
||||
global $DB;
|
||||
|
||||
$form->name = '';
|
||||
list($category) = explode(',', $form->category);
|
||||
|
||||
if (!$form->includesubcategories) {
|
||||
if ($DB->record_exists('question_categories', ['id' => $category, 'parent' => 0])) {
|
||||
// The chosen category is a top category.
|
||||
$form->includesubcategories = true;
|
||||
}
|
||||
}
|
||||
|
||||
$form->tags = array();
|
||||
|
||||
if (empty($form->fromtags)) {
|
||||
$form->fromtags = array();
|
||||
}
|
||||
|
||||
$form->questiontext = array(
|
||||
'text' => $form->includesubcategories ? '1' : '0',
|
||||
'format' => 0
|
||||
);
|
||||
|
||||
// Name is not a required field for random questions, but
|
||||
// parent::save_question Assumes that it is.
|
||||
return parent::save_question($question, $form);
|
||||
}
|
||||
|
||||
public function save_question_options($question) {
|
||||
global $DB;
|
||||
|
||||
// No options, as such, but we set the parent field to the question's
|
||||
// own id. Setting the parent field has the effect of hiding this
|
||||
// question in various places.
|
||||
$updateobject = new stdClass();
|
||||
$updateobject->id = $question->id;
|
||||
$updateobject->parent = $question->id;
|
||||
|
||||
// We also force the question name to be 'Random (categoryname)'.
|
||||
$category = $DB->get_record('question_categories',
|
||||
array('id' => $question->category), '*', MUST_EXIST);
|
||||
$updateobject->name = $this->question_name($category, $question->includesubcategories, $question->fromtags);
|
||||
return $DB->update_record('question', $updateobject);
|
||||
}
|
||||
|
||||
/**
|
||||
* During unit tests we need to be able to reset all caches so that each new test starts in a known state.
|
||||
* Intended for use only for testing. This is a stop gap until we start using the MUC caching api here.
|
||||
* You need to call this before every test that loads one or more random questions.
|
||||
*/
|
||||
public function clear_caches_before_testing() {
|
||||
$this->availablequestionsbycategory = array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the usable questions from a particular question category.
|
||||
*
|
||||
* @param int $categoryid the id of a question category.
|
||||
* @param bool whether to include questions from subcategories.
|
||||
* @param string $questionsinuse comma-separated list of question ids to
|
||||
* exclude from consideration.
|
||||
* @return array of question records.
|
||||
*/
|
||||
public function get_available_questions_from_category($categoryid, $subcategories) {
|
||||
if (isset($this->availablequestionsbycategory[$categoryid][$subcategories])) {
|
||||
return $this->availablequestionsbycategory[$categoryid][$subcategories];
|
||||
}
|
||||
|
||||
$this->init_qtype_lists();
|
||||
if ($subcategories) {
|
||||
$categoryids = question_categorylist($categoryid);
|
||||
} else {
|
||||
$categoryids = array($categoryid);
|
||||
}
|
||||
|
||||
$questionids = question_bank::get_finder()->get_questions_from_categories(
|
||||
$categoryids, 'qtype NOT IN (' . $this->excludedqtypes . ')');
|
||||
$this->availablequestionsbycategory[$categoryid][$subcategories] = $questionids;
|
||||
return $questionids;
|
||||
}
|
||||
|
||||
public function make_question($questiondata) {
|
||||
return $this->choose_other_question($questiondata, array());
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the definition of another question picked randomly by this question.
|
||||
* @param object $questiondata the data defining a random question.
|
||||
* @param array $excludedquestions of question ids. We will no pick any question whose id is in this list.
|
||||
* @param bool $allowshuffle if false, then any shuffle option on the selected quetsion is disabled.
|
||||
* @param null|integer $forcequestionid if not null then force the picking of question with id $forcequestionid.
|
||||
* @throws coding_exception
|
||||
* @return question_definition|null the definition of the question that was
|
||||
* selected, or null if no suitable question could be found.
|
||||
*/
|
||||
public function choose_other_question($questiondata, $excludedquestions, $allowshuffle = true, $forcequestionid = null) {
|
||||
$available = $this->get_available_questions_from_category($questiondata->category,
|
||||
!empty($questiondata->questiontext));
|
||||
shuffle($available);
|
||||
|
||||
if ($forcequestionid !== null) {
|
||||
$forcedquestionkey = array_search($forcequestionid, $available);
|
||||
if ($forcedquestionkey !== false) {
|
||||
unset($available[$forcedquestionkey]);
|
||||
array_unshift($available, $forcequestionid);
|
||||
} else {
|
||||
throw new coding_exception('thisquestionidisnotavailable', $forcequestionid);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($available as $questionid) {
|
||||
if (in_array($questionid, $excludedquestions)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$question = question_bank::load_question($questionid, $allowshuffle);
|
||||
$this->set_selected_question_name($question, $questiondata->name);
|
||||
return $question;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_random_guess_score($questiondata) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Test helpers for the random question type.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @copyright 2013 The Open University
|
||||
* @author Jamie Pratt <[email protected]>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* Test helper class for the random question type.
|
||||
*
|
||||
* @copyright 2013 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class qtype_random_test_helper extends question_test_helper {
|
||||
public function get_test_questions() {
|
||||
return array('basic');
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the question form data for a random question which selects just from
|
||||
* it's own category and not from sub categories. Category id is not set.
|
||||
* @return stdClass
|
||||
*/
|
||||
public function get_random_question_form_data_basic() {
|
||||
$form = new stdClass();
|
||||
$form->questiontext = array('text' => '');
|
||||
$form->includesubcategories = '0';
|
||||
$form->status = \core_question\local\bank\question_version_status::QUESTION_STATUS_READY;
|
||||
return $form;
|
||||
}
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
<?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 qtype_random;
|
||||
|
||||
use qtype_random;
|
||||
use question_bank;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
|
||||
require_once($CFG->dirroot . '/question/type/random/questiontype.php');
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for the random question type class.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @copyright 2010 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class question_type_test extends \advanced_testcase {
|
||||
protected $qtype;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->qtype = new qtype_random();
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
$this->qtype = null;
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_name(): void {
|
||||
$this->assertEquals($this->qtype->name(), 'random');
|
||||
}
|
||||
|
||||
public function test_can_analyse_responses(): void {
|
||||
$this->assertFalse($this->qtype->can_analyse_responses());
|
||||
}
|
||||
|
||||
public function test_get_random_guess_score(): void {
|
||||
$this->assertNull($this->qtype->get_random_guess_score(null));
|
||||
}
|
||||
|
||||
public function test_load_question(): void {
|
||||
$this->resetAfterTest();
|
||||
|
||||
$syscontext = \context_system::instance();
|
||||
/** @var core_question_generator $generator */
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$category = $generator->create_question_category(['contextid' => $syscontext->id]);
|
||||
|
||||
$fromform = \test_question_maker::get_question_form_data('random');
|
||||
$fromform->category = $category->id . ',' . $syscontext->id;
|
||||
|
||||
$question = new \stdClass();
|
||||
$question->category = $category->id;
|
||||
$question->qtype = 'random';
|
||||
$question->createdby = 0;
|
||||
|
||||
$this->qtype->save_question($question, $fromform);
|
||||
$questiondata = question_bank::load_question_data($question->id);
|
||||
|
||||
$this->assertEquals(['id', 'category', 'parent', 'name', 'questiontext', 'questiontextformat',
|
||||
'generalfeedback', 'generalfeedbackformat', 'defaultmark', 'penalty', 'qtype',
|
||||
'length', 'stamp', 'timecreated', 'timemodified', 'createdby', 'modifiedby', 'idnumber', 'contextid',
|
||||
'status', 'versionid', 'version', 'questionbankentryid', 'categoryobject', 'options', 'hints'],
|
||||
array_keys(get_object_vars($questiondata)));
|
||||
$this->assertEquals($category->id, $questiondata->category);
|
||||
|
||||
// Random questions are not real questions. This is signaled by parent
|
||||
// being non-zero - and in fact equal to question id.
|
||||
$this->assertEquals($questiondata->id, $questiondata->parent);
|
||||
$this->assertEquals('Random (' . $category->name . ')', $questiondata->name);
|
||||
$this->assertEquals(0, $questiondata->questiontext); // Used to store 'Select from subcategories'.
|
||||
$this->assertEquals('random', $questiondata->qtype);
|
||||
$this->assertEquals(1, $questiondata->length);
|
||||
$this->assertEquals(\core_question\local\bank\question_version_status::QUESTION_STATUS_READY, $questiondata->status);
|
||||
$this->assertEquals($category->contextid, $questiondata->contextid);
|
||||
|
||||
// Options - not used.
|
||||
$this->assertEquals(['answers'], array_keys(get_object_vars($questiondata->options)));
|
||||
$this->assertEquals([], $questiondata->options->answers);
|
||||
|
||||
// Hints - not used.
|
||||
$this->assertEquals([], $questiondata->hints);
|
||||
}
|
||||
|
||||
public function test_get_possible_responses(): void {
|
||||
$this->assertEquals(array(), $this->qtype->get_possible_responses(null));
|
||||
}
|
||||
|
||||
public function test_question_creation(): void {
|
||||
$this->resetAfterTest();
|
||||
question_bank::get_qtype('random')->clear_caches_before_testing();
|
||||
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('core_question');
|
||||
$cat = $generator->create_question_category();
|
||||
$question1 = $generator->create_question('shortanswer', null, array('category' => $cat->id));
|
||||
$question2 = $generator->create_question('numerical', null, array('category' => $cat->id));
|
||||
|
||||
$randomquestion = $generator->create_question('random', null, array('category' => $cat->id));
|
||||
|
||||
$expectedids = array($question1->id, $question2->id);
|
||||
$actualids = question_bank::get_qtype('random')->get_available_questions_from_category($cat->id, 0);
|
||||
sort($expectedids);
|
||||
sort($actualids);
|
||||
$this->assertEquals($expectedids, $actualids);
|
||||
|
||||
$q = question_bank::load_question($randomquestion->id);
|
||||
|
||||
$this->assertContainsEquals($q->id, array($question1->id, $question2->id));
|
||||
}
|
||||
}
|
||||
@@ -1,974 +0,0 @@
|
||||
<?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 qtype_random;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->dirroot . '/question/engine/upgrade/tests/helper.php');
|
||||
|
||||
|
||||
/**
|
||||
* Testing the upgrade of random question attempts.
|
||||
*
|
||||
* @package qtype_random
|
||||
* @copyright 2009 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
final class upgrade_old_attempt_data_test extends \question_attempt_upgrader_test_base {
|
||||
|
||||
public function test_random_deferredfeedback_history620(): void {
|
||||
$quiz = (object) array(
|
||||
'id' => '2177',
|
||||
'course' => '5174',
|
||||
'name' => 'iCMA 41',
|
||||
'intro' => '<p>Welcome. This is the first K101 Interactive Computer Marked Assignment (iCMA).</p>
|
||||
<p>After the open date, begin by clicking <b>Start attempt</b> and a pop-up box will appear. Click OK and this will take you to the questions.</p>
|
||||
<p> </p>',
|
||||
'introformat' => FORMAT_HTML,
|
||||
'questiondecimalpoints' => '-1',
|
||||
'showuserpicture' => '1',
|
||||
'showblocks' => '1',
|
||||
'timeopen' => '1236556800',
|
||||
'timeclose' => '1238713140',
|
||||
'preferredbehaviour' => 'deferredfeedback',
|
||||
'attempts' => '1',
|
||||
'attemptonlast' => '0',
|
||||
'grademethod' => '1',
|
||||
'decimalpoints' => '0',
|
||||
'review' => '67268673',
|
||||
'questionsperpage' => '1',
|
||||
'shufflequestions' => '0',
|
||||
'shuffleanswers' => '0',
|
||||
'sumgrades' => '5',
|
||||
'grade' => '100',
|
||||
'timecreated' => '0',
|
||||
'timemodified' => '1236269160',
|
||||
'password' => '',
|
||||
'subnet' => '',
|
||||
'popup' => '0',
|
||||
'delay1' => '0',
|
||||
'delay2' => '0',
|
||||
'timelimit' => '0',
|
||||
);
|
||||
$attempt = (object) array(
|
||||
'id' => '125687',
|
||||
'uniqueid' => '125688',
|
||||
'quiz' => '2177',
|
||||
'userid' => '509191',
|
||||
'attempt' => '1',
|
||||
'sumgrades' => '5',
|
||||
'timestart' => '1236590532',
|
||||
'timefinish' => '1238184306',
|
||||
'timemodified' => '1238182560',
|
||||
'layout' => '28698,0,34245,0,34248,0,35005,0,35009,0,35013,0',
|
||||
'preview' => '0',
|
||||
);
|
||||
$question = (object) array(
|
||||
'id' => '34248',
|
||||
'category' => '3762',
|
||||
'parent' => '34248',
|
||||
'name' => 'Random Question (Q2)',
|
||||
'questiontext' => '1',
|
||||
'questiontextformat' => '0',
|
||||
'defaultmark' => '1',
|
||||
'penalty' => '0',
|
||||
'qtype' => 'random',
|
||||
'length' => '1',
|
||||
'stamp' => 'learn.open.ac.uk+090218115858+6H1FBE',
|
||||
'version' => 'learn.open.ac.uk+090218115858+REDwJ6',
|
||||
'hidden' => '0',
|
||||
'generalfeedback' => '',
|
||||
'generalfeedbackformat' => '1',
|
||||
'timecreated' => '1234958338',
|
||||
'timemodified' => '1234958338',
|
||||
'createdby' => '219095',
|
||||
'modifiedby' => '219095',
|
||||
'unlimited' => '0',
|
||||
'maxmark' => '1',
|
||||
);
|
||||
$qsession = (object) array(
|
||||
'id' => '1512603',
|
||||
'attemptid' => '125688',
|
||||
'questionid' => '34248',
|
||||
'newest' => '4010008',
|
||||
'newgraded' => '4010008',
|
||||
'sumpenalty' => '0.1',
|
||||
'manualcomment' => '',
|
||||
'manualcommentformat' => '1',
|
||||
'flagged' => '1',
|
||||
);
|
||||
$qstates = array(
|
||||
3820084 => (object) array(
|
||||
'attempt' => '125688',
|
||||
'question' => '34248',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '0',
|
||||
'answer' => 'random34999-103135,103136,103137:',
|
||||
'timestamp' => '1236590532',
|
||||
'event' => '0',
|
||||
'grade' => '0',
|
||||
'raw_grade' => '0',
|
||||
'penalty' => '0',
|
||||
'id' => 3820084,
|
||||
),
|
||||
4009916 => (object) array(
|
||||
'attempt' => '125688',
|
||||
'question' => '34248',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '1',
|
||||
'answer' => 'random34999-103135,103136,103137:103137',
|
||||
'timestamp' => '1238182723',
|
||||
'event' => '2',
|
||||
'grade' => '0',
|
||||
'raw_grade' => '1',
|
||||
'penalty' => '0.1',
|
||||
'id' => 4009916,
|
||||
),
|
||||
4010008 => (object) array(
|
||||
'attempt' => '125688',
|
||||
'question' => '34248',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '2',
|
||||
'answer' => 'random34999-103135,103136,103137:103137',
|
||||
'timestamp' => '1238182723',
|
||||
'event' => '6',
|
||||
'grade' => '1',
|
||||
'raw_grade' => '1',
|
||||
'penalty' => '0.1',
|
||||
'id' => 4010008,
|
||||
),
|
||||
);
|
||||
$realquestion = (object) array(
|
||||
'id' => '34999',
|
||||
'category' => '3762',
|
||||
'parent' => '0',
|
||||
'name' => 'K101 iCMA41 Qs 2-v2',
|
||||
'questiontext' => '<p>(Notice in the panel at the left that the question you have just done is now greyed out. Remember that you can <b>revisit any questions</b> by clicking on the <b>numbered boxes</b>. Click Q1 now to try this out and then click Q2 to return here.)</p>
|
||||
<p><img style="margin-left: 0px; margin-right: 0px;" src="http://learn.open.ac.uk/file.php/5174/K101images/icma41/K101CMA41-Q1-v0.gif" alt="Table 1 showing Number of households receiving home care (England)" title="Table 1 showing Number of households receiving home care (England)" width="512" height="284" /></p>
|
||||
<p><b>(<a target="_blank" href="http://learn.open.ac.uk/file.php/5174/iCMA-PDFs-resizableimages/k101-iCMA41-Q2-v2-ResizableFigure.pdf" title="PDF document containing iCMA image - to enable enlargement">Link to resizable Table 1</a>) </b></p>
|
||||
<p>Which was the first year when more than a quarter of a million households received care from independent agencies?</p>',
|
||||
'questiontextformat' => '1',
|
||||
'defaultmark' => '1',
|
||||
'penalty' => '0.1',
|
||||
'qtype' => 'multichoice',
|
||||
'length' => '1',
|
||||
'stamp' => 'learn.open.ac.uk+090227173002+mbdE0X',
|
||||
'version' => 'learn.open.ac.uk+090304190917+xAB5Nf',
|
||||
'hidden' => '0',
|
||||
'generalfeedback' => '',
|
||||
'generalfeedbackformat' => '1',
|
||||
'timecreated' => '1235755802',
|
||||
'timemodified' => '1236193757',
|
||||
'createdby' => '25299',
|
||||
'modifiedby' => '25299',
|
||||
'unlimited' => '0',
|
||||
'options' => (object) array(
|
||||
'id' => '15211',
|
||||
'question' => '34999',
|
||||
'layout' => '0',
|
||||
'answers' => array(
|
||||
103135 => (object) array(
|
||||
'question' => '34999',
|
||||
'answer' => '2001',
|
||||
'fraction' => '0',
|
||||
'feedback' => '',
|
||||
'id' => 103135,
|
||||
),
|
||||
103136 => (object) array(
|
||||
'question' => '34999',
|
||||
'answer' => '2003',
|
||||
'fraction' => '0',
|
||||
'feedback' => '',
|
||||
'id' => 103136,
|
||||
),
|
||||
103137 => (object) array(
|
||||
'question' => '34999',
|
||||
'answer' => '2005',
|
||||
'fraction' => '1',
|
||||
'feedback' => '',
|
||||
'id' => 103137,
|
||||
),
|
||||
),
|
||||
'single' => '1',
|
||||
'shuffleanswers' => '1',
|
||||
'correctfeedback' => 'Your answer is correct. Well done.',
|
||||
'partiallycorrectfeedback' => '',
|
||||
'incorrectfeedback' => 'Your answer is incorrect. The correct answer is: 2005.',
|
||||
'answernumbering' => 'abc',
|
||||
),
|
||||
);
|
||||
$this->loader->put_question_in_cache($realquestion);
|
||||
|
||||
$qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates);
|
||||
|
||||
$expectedqa = (object) array(
|
||||
'behaviour' => 'deferredfeedback',
|
||||
'questionid' => 34999,
|
||||
'variant' => 1,
|
||||
'maxmark' => 1,
|
||||
'minfraction' => 0,
|
||||
'maxfraction' => 1,
|
||||
'flagged' => 0,
|
||||
'questionsummary' => "(Notice in the panel at the left that the question you have just done is now greyed out. Remember that you can REVISIT ANY QUESTIONS by clicking on the NUMBERED BOXES. Click Q1 now to try this out and then click Q2 to return here.) \n\n[Table 1 showing Number of households receiving home care (England)] \n\n(LINK TO RESIZABLE TABLE 1) \n\nWhich was the first year when more than a quarter of a million households received care from independent agencies?",
|
||||
'rightanswer' => '2005',
|
||||
'responsesummary' => '2005',
|
||||
'timemodified' => 1238182723,
|
||||
'steps' => array(
|
||||
0 => (object) array(
|
||||
'sequencenumber' => 0,
|
||||
'state' => 'todo',
|
||||
'fraction' => null,
|
||||
'timecreated' => 1236590532,
|
||||
'userid' => 509191,
|
||||
'data' => array('_order' => '103135,103136,103137'),
|
||||
),
|
||||
1 => (object) array(
|
||||
'sequencenumber' => 1,
|
||||
'state' => 'complete',
|
||||
'fraction' => null,
|
||||
'timecreated' => 1238182723,
|
||||
'userid' => 509191,
|
||||
'data' => array('answer' => 2),
|
||||
),
|
||||
2 => (object) array(
|
||||
'sequencenumber' => 2,
|
||||
'state' => 'gradedright',
|
||||
'fraction' => 1,
|
||||
'timecreated' => 1238182723,
|
||||
'userid' => 509191,
|
||||
'data' => array('answer' => 2, '-finish' => 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$this->compare_qas($expectedqa, $qa);
|
||||
}
|
||||
|
||||
public function test_random_deferredfeedback_qsession4225582(): void {
|
||||
$quiz = (object) array(
|
||||
'id' => '4410',
|
||||
'course' => '5444',
|
||||
'name' => 'Block 3, Session 5 Quiz',
|
||||
'intro' => 'Pour chacune des questions suivantes vous avez trois réponses possibles. Cochez la bonne. (Cliquez sur « Check » pour vérifier votre réponse.)',
|
||||
'introformat' => FORMAT_HTML,
|
||||
'questiondecimalpoints' => '-1',
|
||||
'showuserpicture' => '1',
|
||||
'showblocks' => '1',
|
||||
'timeopen' => '0',
|
||||
'timeclose' => '0',
|
||||
'optionflags' => '0',
|
||||
'attempts' => '0',
|
||||
'attemptonlast' => '0',
|
||||
'grademethod' => '1',
|
||||
'decimalpoints' => '2',
|
||||
'review' => '71727591',
|
||||
'questionsperpage' => '3',
|
||||
'shufflequestions' => '1',
|
||||
'shuffleanswers' => '1',
|
||||
'sumgrades' => '15',
|
||||
'grade' => '10',
|
||||
'timecreated' => '0',
|
||||
'timemodified' => '1269429396',
|
||||
'password' => '',
|
||||
'subnet' => '',
|
||||
'popup' => '0',
|
||||
'delay1' => '0',
|
||||
'delay2' => '0',
|
||||
'timelimit' => '0',
|
||||
'preferredbehaviour' => 'deferredfeedback',
|
||||
);
|
||||
$attempt = (object) array(
|
||||
'id' => '418702',
|
||||
'uniqueid' => '418703',
|
||||
'quiz' => '4410',
|
||||
'userid' => '62892',
|
||||
'attempt' => '2',
|
||||
'sumgrades' => '9',
|
||||
'timestart' => '1274178725',
|
||||
'timefinish' => '1274179255',
|
||||
'timemodified' => '1274178763',
|
||||
'layout' => '101989,101985,101994,0,101988,101986,101992,0,101997,101990,101991,0,101984,101995,101987,0,101983,101993,101996,0',
|
||||
'preview' => '0',
|
||||
);
|
||||
$question = (object) array(
|
||||
'id' => '101984',
|
||||
'category' => '9148',
|
||||
'parent' => '101984',
|
||||
'name' => 'Random Question (Block 3, Session 5 Quiz)',
|
||||
'questiontext' => '1',
|
||||
'questiontextformat' => '0',
|
||||
'image' => '',
|
||||
'defaultmark' => '1',
|
||||
'penalty' => '0',
|
||||
'qtype' => 'random',
|
||||
'length' => '1',
|
||||
'stamp' => 'learn.open.ac.uk+100416133531+H1yive',
|
||||
'version' => 'learn.open.ac.uk+100416133531+KrpbHq',
|
||||
'hidden' => '0',
|
||||
'generalfeedback' => '',
|
||||
'generalfeedbackformat' => '1',
|
||||
'timecreated' => '1271424931',
|
||||
'timemodified' => '1271424931',
|
||||
'createdby' => '220574',
|
||||
'modifiedby' => '220574',
|
||||
'unlimited' => '0',
|
||||
'maxmark' => '1',
|
||||
);
|
||||
$qsession = (object) array(
|
||||
'id' => '4225582',
|
||||
'attemptid' => '418703',
|
||||
'questionid' => '101984',
|
||||
'newest' => '10786541',
|
||||
'newgraded' => '10786541',
|
||||
'sumpenalty' => '0.33',
|
||||
'manualcomment' => '',
|
||||
'manualcommentformat' => '1',
|
||||
'flagged' => '1',
|
||||
);
|
||||
$qstates = array(
|
||||
10786382 => (object) array(
|
||||
'attempt' => '418703',
|
||||
'question' => '101984',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '0',
|
||||
'answer' => 'random98355-341067,341066,341068:',
|
||||
'timestamp' => '1274178725',
|
||||
'event' => '0',
|
||||
'grade' => '0',
|
||||
'raw_grade' => '0',
|
||||
'penalty' => '0',
|
||||
'id' => 10786382,
|
||||
),
|
||||
10786398 => (object) array(
|
||||
'attempt' => '418703',
|
||||
'question' => '101984',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '1',
|
||||
'answer' => 'random98355-341067,341066,341068:341061',
|
||||
'timestamp' => '1274178725',
|
||||
'event' => '2',
|
||||
'grade' => '0',
|
||||
'raw_grade' => '0',
|
||||
'penalty' => '0.33',
|
||||
'id' => 10786398,
|
||||
),
|
||||
10786515 => (object) array(
|
||||
'attempt' => '418703',
|
||||
'question' => '101984',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '2',
|
||||
'answer' => 'random98355-341067,341066,341068:341067',
|
||||
'timestamp' => '1274179164',
|
||||
'event' => '2',
|
||||
'grade' => '0',
|
||||
'raw_grade' => '1',
|
||||
'penalty' => '0.33',
|
||||
'id' => 10786515,
|
||||
),
|
||||
10786541 => (object) array(
|
||||
'attempt' => '418703',
|
||||
'question' => '101984',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '3',
|
||||
'answer' => 'random98355-341067,341066,341068:341067',
|
||||
'timestamp' => '1274179164',
|
||||
'event' => '6',
|
||||
'grade' => '1',
|
||||
'raw_grade' => '1',
|
||||
'penalty' => '0.33',
|
||||
'id' => 10786541,
|
||||
),
|
||||
);
|
||||
$realquestion = (object) array(
|
||||
'id' => '98355',
|
||||
'category' => '9148',
|
||||
'parent' => '0',
|
||||
'name' => '20',
|
||||
'questiontext' => 'En France, le covoiturage est une pratique qui a été au départ préconisé par « les écolos et les verts », mais depuis, elle :',
|
||||
'questiontextformat' => '1',
|
||||
'defaultmark' => '1',
|
||||
'penalty' => '0.33',
|
||||
'qtype' => 'multichoice',
|
||||
'length' => '1',
|
||||
'stamp' => 'learn.open.ac.uk+100324131532+CExeWh',
|
||||
'version' => 'learn.open.ac.uk+100326162411+qEo2Bg',
|
||||
'hidden' => '0',
|
||||
'generalfeedback' => '',
|
||||
'generalfeedbackformat' => '1',
|
||||
'timecreated' => '1269436532',
|
||||
'timemodified' => '1269620651',
|
||||
'createdby' => '220574',
|
||||
'modifiedby' => '220574',
|
||||
'unlimited' => '0',
|
||||
'options' => (object) array(
|
||||
'id' => '37042',
|
||||
'question' => '98355',
|
||||
'layout' => '0',
|
||||
'answers' => array(
|
||||
341066 => (object) array(
|
||||
'question' => '98355',
|
||||
'answer' => 'a été soutenue par les partis de droite.',
|
||||
'fraction' => '0',
|
||||
'feedback' => '',
|
||||
'id' => 341066,
|
||||
),
|
||||
341067 => (object) array(
|
||||
'question' => '98355',
|
||||
'answer' => 'n’est plus uniquement un positionnement écologiste.',
|
||||
'fraction' => '1',
|
||||
'feedback' => '',
|
||||
'id' => 341067,
|
||||
),
|
||||
341068 => (object) array(
|
||||
'question' => '98355',
|
||||
'answer' => 'est une idée de gauche, du centre, de droite, d’extrême gauche ou d’extrême droite.',
|
||||
'fraction' => '0',
|
||||
'feedback' => '',
|
||||
'id' => 341068,
|
||||
),
|
||||
),
|
||||
'single' => '1',
|
||||
'shuffleanswers' => '1',
|
||||
'correctfeedback' => 'Vrai.',
|
||||
'partiallycorrectfeedback' => '',
|
||||
'incorrectfeedback' => 'Faux.',
|
||||
'answernumbering' => 'abc',
|
||||
),
|
||||
'hints' => false,
|
||||
'maxmark' => '1',
|
||||
);
|
||||
$this->loader->put_question_in_cache($realquestion);
|
||||
|
||||
$qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates);
|
||||
|
||||
$expectedqa = (object) array(
|
||||
'behaviour' => 'deferredfeedback',
|
||||
'questionid' => 98355,
|
||||
'variant' => 1,
|
||||
'maxmark' => 1,
|
||||
'minfraction' => 0,
|
||||
'maxfraction' => 1,
|
||||
'flagged' => 0,
|
||||
'questionsummary' => 'En France, le covoiturage est une pratique qui a été au départ préconisé par « les écolos et les verts », mais depuis, elle :',
|
||||
'rightanswer' => 'n’est plus uniquement un positionnement écologiste.',
|
||||
'responsesummary' => 'n’est plus uniquement un positionnement écologiste.',
|
||||
'timemodified' => 1274179164,
|
||||
'steps' => array(
|
||||
0 => (object) array(
|
||||
'sequencenumber' => 0,
|
||||
'state' => 'todo',
|
||||
'fraction' => null,
|
||||
'timecreated' => 1274178725,
|
||||
'userid' => 62892,
|
||||
'data' => array('_order' => '341067,341066,341068'),
|
||||
),
|
||||
1 => (object) array(
|
||||
'sequencenumber' => 1,
|
||||
'state' => 'complete',
|
||||
'fraction' => null,
|
||||
'timecreated' => 1274178725,
|
||||
'userid' => 62892,
|
||||
'data' => array('answer' => '-1'),
|
||||
),
|
||||
2 => (object) array(
|
||||
'sequencenumber' => 2,
|
||||
'state' => 'complete',
|
||||
'fraction' => null,
|
||||
'timecreated' => 1274179164,
|
||||
'userid' => 62892,
|
||||
'data' => array('answer' => '0'),
|
||||
),
|
||||
3 => (object) array(
|
||||
'sequencenumber' => 3,
|
||||
'state' => 'gradedright',
|
||||
'fraction' => 1,
|
||||
'timecreated' => 1274179164,
|
||||
'userid' => 62892,
|
||||
'data' => array('answer' => '0', '-finish' => '1'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$this->compare_qas($expectedqa, $qa);
|
||||
}
|
||||
|
||||
public function test_random_deferredfeedback_qsession3481928(): void {
|
||||
$quiz = (object) array(
|
||||
'id' => '2624',
|
||||
'course' => '5233',
|
||||
'name' => 'iCMA 42',
|
||||
'intro' => '',
|
||||
'introformat' => FORMAT_HTML,
|
||||
'questiondecimalpoints' => '-1',
|
||||
'showuserpicture' => '1',
|
||||
'showblocks' => '1',
|
||||
'timeopen' => '1267056000',
|
||||
'timeclose' => '1271890740',
|
||||
'optionflags' => '0',
|
||||
'attempts' => '1',
|
||||
'attemptonlast' => '0',
|
||||
'grademethod' => '1',
|
||||
'decimalpoints' => '2',
|
||||
'review' => '67268673',
|
||||
'questionsperpage' => '1',
|
||||
'shufflequestions' => '0',
|
||||
'shuffleanswers' => '1',
|
||||
'sumgrades' => '18',
|
||||
'grade' => '18',
|
||||
'timecreated' => '0',
|
||||
'timemodified' => '1271852647',
|
||||
'password' => '',
|
||||
'subnet' => '',
|
||||
'popup' => '0',
|
||||
'delay1' => '0',
|
||||
'delay2' => '0',
|
||||
'timelimit' => '0',
|
||||
'preferredbehaviour' => 'deferredfeedback',
|
||||
);
|
||||
$attempt = (object) array(
|
||||
'id' => '331814',
|
||||
'uniqueid' => '331815',
|
||||
'quiz' => '2624',
|
||||
'userid' => '239341',
|
||||
'attempt' => '1',
|
||||
'sumgrades' => '9.13333',
|
||||
'timestart' => '1267605659',
|
||||
'timefinish' => '1270202969',
|
||||
'timemodified' => '1269508052',
|
||||
'layout' => '68646,0,81245,0,81246,0,81247,0,81248,0,81249,0,81250,0,82795,0,82797,0,82798,0,82799,0,82800,0,82801,0,82802,0,82803,0,82804,0,82805,0,82806,0,82807,0',
|
||||
'preview' => '0',
|
||||
);
|
||||
$question = (object) array(
|
||||
'id' => '81247',
|
||||
'category' => '6882',
|
||||
'parent' => '81247',
|
||||
'name' => 'Random Question (42.02.03.GraphConvert)',
|
||||
'questiontext' => '1',
|
||||
'questiontextformat' => '0',
|
||||
'image' => '',
|
||||
'defaultmark' => '1',
|
||||
'penalty' => '0',
|
||||
'qtype' => 'random',
|
||||
'length' => '1',
|
||||
'stamp' => 'learn.open.ac.uk+091215084951+AYsG5O',
|
||||
'version' => 'learn.open.ac.uk+091215084951+VSqp4u',
|
||||
'hidden' => '0',
|
||||
'generalfeedback' => '',
|
||||
'generalfeedbackformat' => '1',
|
||||
'timecreated' => '1260866991',
|
||||
'timemodified' => '1260866991',
|
||||
'createdby' => '27595',
|
||||
'modifiedby' => '27595',
|
||||
'unlimited' => '0',
|
||||
'maxmark' => '1',
|
||||
);
|
||||
$qsession = (object) array(
|
||||
'id' => '3481928',
|
||||
'attemptid' => '331815',
|
||||
'questionid' => '81247',
|
||||
'newest' => '9646306',
|
||||
'newgraded' => '9646306',
|
||||
'sumpenalty' => '0.333333',
|
||||
'manualcomment' => '',
|
||||
'manualcommentformat' => '1',
|
||||
'flagged' => '1',
|
||||
);
|
||||
$qstates = array(
|
||||
8809801 => (object) array(
|
||||
'attempt' => '331815',
|
||||
'question' => '81247',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '0',
|
||||
'answer' => 'random83248-',
|
||||
'timestamp' => '1267605659',
|
||||
'event' => '0',
|
||||
'grade' => '0',
|
||||
'raw_grade' => '0',
|
||||
'penalty' => '0',
|
||||
'id' => 8809801,
|
||||
),
|
||||
9081885 => (object) array(
|
||||
'attempt' => '331815',
|
||||
'question' => '81247',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '1',
|
||||
'answer' => '64',
|
||||
'timestamp' => '1268384692',
|
||||
'event' => '2',
|
||||
'grade' => '0',
|
||||
'raw_grade' => '1',
|
||||
'penalty' => '0.333333',
|
||||
'id' => 9081885,
|
||||
),
|
||||
9107452 => (object) array(
|
||||
'attempt' => '331815',
|
||||
'question' => '81247',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '1',
|
||||
'answer' => 'random83248-64',
|
||||
'timestamp' => '1268471437',
|
||||
'event' => '2',
|
||||
'grade' => '0',
|
||||
'raw_grade' => '1',
|
||||
'penalty' => '0.333333',
|
||||
'id' => 9107452,
|
||||
),
|
||||
9646306 => (object) array(
|
||||
'attempt' => '331815',
|
||||
'question' => '81247',
|
||||
'originalquestion' => '0',
|
||||
'seq_number' => '2',
|
||||
'answer' => 'random83248-64',
|
||||
'timestamp' => '1268471437',
|
||||
'event' => '6',
|
||||
'grade' => '1',
|
||||
'raw_grade' => '1',
|
||||
'penalty' => '0.333333',
|
||||
'id' => 9646306,
|
||||
),
|
||||
);
|
||||
$realquestion = (object) array(
|
||||
'id' => '83248',
|
||||
'category' => '6882',
|
||||
'parent' => '0',
|
||||
'name' => '42.02.03.GraphConvert.E',
|
||||
'questiontext' => '<p> The figure below shows a graph for converting between miles and kilometres. </p><p><img src="http://learnacct.open.ac.uk/file.php/5233/quizImages/u2/figs/42.02.07.GraphConvert.png" alt=" The graph has a horizontal axis, labelled miles, with a scale marked from 0 to 50 in steps of 5, and a vertical axis, labelled kilometres, with a scale from 0 to 80 in steps of 10. The straight line of the graph starts at the origin and slopes up to the right, and appears to pass through the point corresponding to 25 miles and 40 kilometres, and through the point corresponding to 50 miles and 80 kilometres. "></img> </p><p>By taking a reading from the graph, estimate how many kilometres are equivalent to 40 miles. </p><p>Round your answer to the nearest kilometre and type it in the box below. </p><p>(Your answer should be a single number, without units.) </p>',
|
||||
'questiontextformat' => '0',
|
||||
'defaultmark' => '3',
|
||||
'penalty' => '0.333333',
|
||||
'qtype' => 'numerical',
|
||||
'length' => '1',
|
||||
'stamp' => 'learn.open.ac.uk+100108135957+SZhkKF',
|
||||
'version' => 'learn.open.ac.uk+100108135957+ej40PM',
|
||||
'hidden' => '0',
|
||||
'generalfeedback' => '<p> Conversion graph for miles and kilometres. </p><p><img src="http://learnacct.open.ac.uk/file.php/5233/quizImages/u2/figs/42.02.07.GraphConvert.E.png" alt=" The figure is the same as in the question, with some two additional lines drawn. So, the graph has a horizontal axis, labelled miles, with a scale marked from 0 to 50 in steps of 5, and a vertical axis, labelled kilometres, with a scale from 0 to 80 in steps of 10. The straight line of the graph starts at the origin and slopes up to the right, and appears to pass through the point corresponding to 25 miles and 40 kilometres, and through the point corresponding to 50 miles and 80 kilometres. The first additional line is vertical line drawn upwards from the horizontal axis from the point corresponding to 40 miles to the point where it meets the sloping line of the graph. From that point a horizontal line is drawn to meet the vertical axis at the point corresponding to 64 kilometres approximately."></img> </p><p>So 40 miles is approximately 64 km. </p><p>See Unit 2, Subsection 2.3. </p>',
|
||||
'generalfeedbackformat' => '1',
|
||||
'timecreated' => '1262959197',
|
||||
'timemodified' => '0',
|
||||
'createdby' => '123783',
|
||||
'modifiedby' => null,
|
||||
'unlimited' => '0',
|
||||
'options' => (object) array(
|
||||
'answers' => array(
|
||||
278332 => (object) array(
|
||||
'question' => '83248',
|
||||
'answer' => '64',
|
||||
'fraction' => '1',
|
||||
'feedback' => 'Your answer is correct.',
|
||||
'tolerance' => '1',
|
||||
'id' => 278332,
|
||||
),
|
||||
278333 => (object) array(
|
||||
'question' => '83248',
|
||||
'answer' => '64',
|
||||
'fraction' => '0',
|
||||
'feedback' => 'Your answer is incorrect. It is close, but not quite accurate enough.',
|
||||
'tolerance' => '5',
|
||||
'id' => 278333,
|
||||
),
|
||||
278334 => (object) array(
|
||||
'question' => '83248',
|
||||
'answer' => '25',
|
||||
'fraction' => '0',
|
||||
'feedback' => 'Your answer is incorrect. Remember you are converting from miles to kilometres.',
|
||||
'tolerance' => '5',
|
||||
'id' => 278334,
|
||||
),
|
||||
278335 => (object) array(
|
||||
'question' => '83248',
|
||||
'answer' => '*',
|
||||
'fraction' => '0',
|
||||
'feedback' => 'Your answer is incorrect.',
|
||||
'tolerance' => '0',
|
||||
'id' => 278335,
|
||||
),
|
||||
),
|
||||
'units' => array(
|
||||
0 => (object) array(
|
||||
'question' => '83248',
|
||||
'multiplier' => 1,
|
||||
'unit' => 'km',
|
||||
'id' => 2030,
|
||||
),
|
||||
1 => (object) array(
|
||||
'question' => '83248',
|
||||
'multiplier' => 1,
|
||||
'unit' => 'kilometres',
|
||||
'id' => 2031,
|
||||
),
|
||||
),
|
||||
),
|
||||
'hints' => array(
|
||||
44315 => (object) array(
|
||||
'questionid' => '83248',
|
||||
'hint' => 'See Unit 2, Subsection 2.3.',
|
||||
'rest' => null,
|
||||
'id' => 44315,
|
||||
),
|
||||
44316 => (object) array(
|
||||
'questionid' => '83248',
|
||||
'hint' => '<p> Find the measurement on the horizontal axis, draw a line vertically up to meet the conversion line and read off the corresponding value on the vertical axis. </p><p>See Unit 2, Subsection 2.3. </p>',
|
||||
'rest' => null,
|
||||
'id' => 44316,
|
||||
),
|
||||
),
|
||||
'maxmark' => '1',
|
||||
);
|
||||
$this->loader->put_question_in_cache($realquestion);
|
||||
|
||||
$qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates);
|
||||
|
||||
$expectedqa = (object) array(
|
||||
'behaviour' => 'deferredfeedback',
|
||||
'questionid' => 83248,
|
||||
'variant' => 1,
|
||||
'maxmark' => 1,
|
||||
'minfraction' => 0,
|
||||
'maxfraction' => 1,
|
||||
'flagged' => 0,
|
||||
'questionsummary' => "The figure below shows a graph for converting between miles and kilometres. \n\n[ The graph has a horizontal axis, labelled miles, with a scale marked from 0 to 50 in steps of 5, and a vertical axis, labelled kilometres, with a scale from 0 to 80 in steps of 10. The straight line of the graph starts at the origin and slopes up to the right, and appears to pass through the point corresponding to 25 miles and 40 kilometres, and through the point corresponding to 50 miles and 80 kilometres. ] \n\nBy taking a reading from the graph, estimate how many kilometres are equivalent to 40 miles. \n\nRound your answer to the nearest kilometre and type it in the box below. \n\n(Your answer should be a single number, without units.)",
|
||||
'rightanswer' => '64 km',
|
||||
'responsesummary' => '64',
|
||||
'timemodified' => 1268471437,
|
||||
'steps' => array(
|
||||
0 => (object) array(
|
||||
'sequencenumber' => 0,
|
||||
'state' => 'todo',
|
||||
'fraction' => null,
|
||||
'timecreated' => 1267605659,
|
||||
'userid' => 239341,
|
||||
'data' => array('_separators' => '.$,'),
|
||||
),
|
||||
1 => (object) array(
|
||||
'sequencenumber' => 1,
|
||||
'state' => 'complete',
|
||||
'fraction' => null,
|
||||
'timecreated' => 1268471437,
|
||||
'userid' => 239341,
|
||||
'data' => array('answer' => '64'),
|
||||
),
|
||||
2 => (object) array(
|
||||
'sequencenumber' => 2,
|
||||
'state' => 'gradedright',
|
||||
'fraction' => 1,
|
||||
'timecreated' => 1268471437,
|
||||
'userid' => 239341,
|
||||
'data' => array('answer' => '64', '-finish' => '1'),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$this->compare_qas($expectedqa, $qa);
|
||||
}
|
||||
|
||||
public function test_numerical_deferredfeedback_qsession55(): void {
|
||||
$quiz = (object) array(
|
||||
'id' => '2',
|
||||
'course' => '2',
|
||||
'name' => 'Numerical quiz',
|
||||
'intro' => '',
|
||||
'introformat' => '1',
|
||||
'timeopen' => '0',
|
||||
'timeclose' => '0',
|
||||
'attempts' => '0',
|
||||
'attemptonlast' => '0',
|
||||
'grademethod' => '1',
|
||||
'decimalpoints' => '2',
|
||||
'questiondecimalpoints' => '-1',
|
||||
'review' => '4459503',
|
||||
'questionsperpage' => '1',
|
||||
'shufflequestions' => '0',
|
||||
'shuffleanswers' => '1',
|
||||
'sumgrades' => '5.00000',
|
||||
'grade' => '10.00000',
|
||||
'timecreated' => '0',
|
||||
'timemodified' => '1305273177',
|
||||
'timelimit' => '0',
|
||||
'password' => '',
|
||||
'subnet' => '',
|
||||
'popup' => '0',
|
||||
'delay1' => '0',
|
||||
'delay2' => '0',
|
||||
'showuserpicture' => '0',
|
||||
'showblocks' => '0',
|
||||
'preferredbehaviour' => 'deferredfeedback',
|
||||
);
|
||||
$attempt = (object) array(
|
||||
'id' => '7',
|
||||
'uniqueid' => '7',
|
||||
'quiz' => '2',
|
||||
'userid' => '3',
|
||||
'attempt' => '1',
|
||||
'sumgrades' => '5.00000',
|
||||
'timestart' => '1305273645',
|
||||
'timefinish' => '1305273672',
|
||||
'timemodified' => '1305273672',
|
||||
'layout' => '6,12,13,15,14,0',
|
||||
'preview' => '0',
|
||||
);
|
||||
$question = (object) array(
|
||||
'id' => '14',
|
||||
'category' => '2',
|
||||
'parent' => '0',
|
||||
'name' => 'MC units',
|
||||
'questiontext' => '<p>What is twice 1.5 m?</p>',
|
||||
'questiontextformat' => '1',
|
||||
'generalfeedback' => '',
|
||||
'generalfeedbackformat' => '1',
|
||||
'penalty' => '0.1000000',
|
||||
'qtype' => 'numerical',
|
||||
'length' => '1',
|
||||
'stamp' => 'tjh238.vledev2.open.ac.uk+110513075703+anXKfw',
|
||||
'version' => 'tjh238.vledev2.open.ac.uk+110513075807+HiXe4P',
|
||||
'hidden' => '0',
|
||||
'timecreated' => '1305273423',
|
||||
'timemodified' => '1305273487',
|
||||
'createdby' => '2',
|
||||
'modifiedby' => '2',
|
||||
'maxmark' => '1.0000000',
|
||||
'options' => (object) array(
|
||||
'answers' => array(
|
||||
21 => (object) array(
|
||||
'id' => '21',
|
||||
'question' => '14',
|
||||
'answer' => '3',
|
||||
'answerformat' => '0',
|
||||
'fraction' => '1.0000000',
|
||||
'feedback' => '',
|
||||
'feedbackformat' => '1',
|
||||
'tolerance' => '',
|
||||
),
|
||||
),
|
||||
'units' => array(
|
||||
0 => (object) array(
|
||||
'id' => '5',
|
||||
'question' => '14',
|
||||
'multiplier' => 1,
|
||||
'unit' => 'm',
|
||||
),
|
||||
1 => (object) array(
|
||||
'id' => '6',
|
||||
'question' => '14',
|
||||
'multiplier' => 100,
|
||||
'unit' => 'cm',
|
||||
),
|
||||
),
|
||||
'unitgradingtype' => '2',
|
||||
'unitpenalty' => '0.5000000',
|
||||
'showunits' => '1',
|
||||
'unitsleft' => '0',
|
||||
'instructions' => '<p>Write an answer like 3 m.</p>',
|
||||
'instructionsformat' => '1',
|
||||
),
|
||||
'defaultmark' => '1.0000000',
|
||||
);
|
||||
$qsession = (object) array(
|
||||
'id' => '55',
|
||||
'attemptid' => '7',
|
||||
'questionid' => '14',
|
||||
'newest' => '155',
|
||||
'newgraded' => '155',
|
||||
'sumpenalty' => '0.1000000',
|
||||
'manualcomment' => '',
|
||||
'manualcommentformat' => '1',
|
||||
'flagged' => '0',
|
||||
);
|
||||
$qstates = array(
|
||||
145 => (object) array(
|
||||
'id' => '145',
|
||||
'attempt' => '7',
|
||||
'question' => '14',
|
||||
'seq_number' => '0',
|
||||
'answer' => '|||||',
|
||||
'timestamp' => '1305273645',
|
||||
'event' => '0',
|
||||
'grade' => '0.0000000',
|
||||
'raw_grade' => '0.0000000',
|
||||
'penalty' => '0.0000000',
|
||||
),
|
||||
150 => (object) array(
|
||||
'id' => '150',
|
||||
'attempt' => '7',
|
||||
'question' => '14',
|
||||
'seq_number' => '1',
|
||||
'answer' => '300|||||cm',
|
||||
'timestamp' => '1305273666',
|
||||
'event' => '2',
|
||||
'grade' => '0.0000000',
|
||||
'raw_grade' => '1.0000000',
|
||||
'penalty' => '0.1000000',
|
||||
),
|
||||
155 => (object) array(
|
||||
'id' => '155',
|
||||
'attempt' => '7',
|
||||
'question' => '14',
|
||||
'seq_number' => '2',
|
||||
'answer' => '300|||||cm',
|
||||
'timestamp' => '1305273666',
|
||||
'event' => '6',
|
||||
'grade' => '1.0000000',
|
||||
'raw_grade' => '1.0000000',
|
||||
'penalty' => '0.1000000',
|
||||
),
|
||||
);
|
||||
|
||||
$qa = $this->updater->convert_question_attempt($quiz, $attempt, $question, $qsession, $qstates);
|
||||
|
||||
$expectedqa = (object) array(
|
||||
'behaviour' => 'deferredfeedback',
|
||||
'questionid' => 14,
|
||||
'variant' => 1,
|
||||
'maxmark' => 1.0000000,
|
||||
'minfraction' => 0,
|
||||
'maxfraction' => 1,
|
||||
'flagged' => 0,
|
||||
'questionsummary' => 'What is twice 1.5 m?',
|
||||
'rightanswer' => '3 m',
|
||||
'responsesummary' => '300 cm',
|
||||
'timemodified' => 1305273666,
|
||||
'steps' => array(
|
||||
0 => (object) array(
|
||||
'sequencenumber' => 0,
|
||||
'state' => 'todo',
|
||||
'fraction' => null,
|
||||
'timecreated' => 1305273645,
|
||||
'userid' => 3,
|
||||
'data' => array('_separators' => '.$,'),
|
||||
),
|
||||
1 => (object) array(
|
||||
'sequencenumber' => 1,
|
||||
'state' => 'complete',
|
||||
'fraction' => null,
|
||||
'timecreated' => 1305273666,
|
||||
'userid' => 3,
|
||||
'data' => array('answer' => '300', 'unit' => 'cm'),
|
||||
),
|
||||
2 => (object) array(
|
||||
'sequencenumber' => 2,
|
||||
'state' => 'gradedright',
|
||||
'fraction' => 1,
|
||||
'timecreated' => 1305273666,
|
||||
'userid' => 3,
|
||||
'data' => array('answer' => '300', 'unit' => 'cm', '-finish' => 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$this->compare_qas($expectedqa, $qa);
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* Version information for the random question type.
|
||||
*
|
||||
* @package qtype
|
||||
* @subpackage random
|
||||
* @copyright 2005 Mark Nielsen
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$plugin->component = 'qtype_random';
|
||||
$plugin->version = 2025100601;
|
||||
|
||||
$plugin->requires = 2025092600;
|
||||
|
||||
$plugin->maturity = MATURITY_STABLE;
|
||||
@@ -1,45 +0,0 @@
|
||||
<?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/>.
|
||||
|
||||
/**
|
||||
* This file contains datbase upgrade code that is called from lib/db/upgrade.php,
|
||||
* and also check methods that can be used for pre-install checks via
|
||||
* admin/environment.php and lib/environmentlib.php.
|
||||
*
|
||||
* @package moodlecore
|
||||
* @subpackage questionbank
|
||||
* @copyright 2007 The Open University
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
|
||||
/**
|
||||
* In Moodle, all random questions should have question.parent set to be the same
|
||||
* as question.id. One effect of MDL-5482 is that this will not be true for questions that
|
||||
* were backed up then restored. The probably does not cause many problems, except occasionally,
|
||||
* if the bogus question.parent happens to point to a multianswer question type, or when you
|
||||
* try to do a subsequent backup. Anyway, these question.parent values should be fixed, and
|
||||
* that is what this update does.
|
||||
*/
|
||||
function question_fix_random_question_parents() {
|
||||
global $CFG, $DB;
|
||||
$DB->execute("UPDATE {question} SET parent = id WHERE qtype = 'random' AND parent <> id");
|
||||
|
||||
return true;
|
||||
}
|
||||
+1
-1
@@ -29,7 +29,7 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2026010900.01; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
$version = 2026010900.02; // YYYYMMDD = weekly release date of this DEV branch.
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '5.2dev (Build: 20260109)'; // Human-friendly version name
|
||||
|
||||
Reference in New Issue
Block a user