Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?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 mod_quiz\question\bank;use context_module;use core_question\local\bank\question_version_status;use core_question\local\bank\random_question_loader;use core_question\question_reference_manager;use qbank_tagquestion\tag_condition;use qubaid_condition;use stdClass;defined('MOODLE_INTERNAL') || die();require_once($CFG->dirroot . '/question/engine/bank.php');/*** Helper class for question bank and its associated data.** @package mod_quiz* @category question* @copyright 2021 Catalyst IT Australia Pty Ltd* @author Safat Shahin <safatshahin@catalyst-au.net>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class qbank_helper {/*** Get the available versions of a question where one of the version has the given question id.** @param int $questionid id of a question.* @return stdClass[] other versions of this question. Each object has fields versionid,* version and questionid. Array is returned most recent version first.*/public static function get_version_options(int $questionid): array {global $DB;return $DB->get_records_sql("SELECT allversions.id AS versionid,allversions.version,allversions.questionidFROM {question_versions} allversionsWHERE allversions.questionbankentryid = (SELECT givenversion.questionbankentryidFROM {question_versions} givenversionWHERE givenversion.questionid = ?)AND allversions.status <> ?ORDER BY allversions.version DESC", [$questionid, question_version_status::QUESTION_STATUS_DRAFT]);}/*** Get the information about which questions should be used to create a quiz attempt.** Each element in the returned array is indexed by slot.slot (slot number) an each object hass:* - All the field of the slot table.* - contextid for where the question(s) come from.* - category id for where the questions come from.* - For non-random questions, All the fields of the question table (but id is in questionid).* Also question version and question bankentryid.* - For random questions, filtercondition, which is also unpacked into category, randomrecurse,* randomtags, and note that these also have a ->name set and ->qtype set to 'random'.** @param int $quizid the id of the quiz to load the data for.* @param context_module $quizcontext the context of this quiz.* @param int|null $slotid optional, if passed only load the data for this one slot (if it is in this quiz).* @return array indexed by slot, with information about the content of each slot.*/public static function get_question_structure(int $quizid, context_module $quizcontext,int $slotid = null): array {global $DB;$params = ['draft' => question_version_status::QUESTION_STATUS_DRAFT,'quizcontextid' => $quizcontext->id,'quizcontextid2' => $quizcontext->id,'quizcontextid3' => $quizcontext->id,'quizid' => $quizid,'quizid2' => $quizid,];$slotidtest = '';$slotidtest2 = '';if ($slotid !== null) {$params['slotid'] = $slotid;$params['slotid2'] = $slotid;$slotidtest = ' AND slot.id = :slotid';$slotidtest2 = ' AND lslot.id = :slotid2';}// Load all the data about each slot.$slotdata = $DB->get_records_sql("SELECT slot.slot,slot.id AS slotid,slot.page,slot.displaynumber,slot.requireprevious,slot.maxmark,slot.quizgradeitemid,qsr.filtercondition,qsr.usingcontextid,qv.status,qv.id AS versionid,qv.version,qr.version AS requestedversion,qv.questionbankentryid,q.id AS questionid,q.*,qc.id AS category,COALESCE(qc.contextid, qsr.questionscontextid) AS contextidFROM {quiz_slots} slot-- case where a particular question has been added to the quiz.LEFT JOIN {question_references} qr ON qr.usingcontextid = :quizcontextid AND qr.component = 'mod_quiz'AND qr.questionarea = 'slot' AND qr.itemid = slot.idLEFT JOIN {question_bank_entries} qbe ON qbe.id = qr.questionbankentryid-- This way of getting the latest version for each slot is a bit more complicated-- than we would like, but the simpler SQL did not work in Oracle 11.2.-- (It did work fine in Oracle 19.x, so once we have updated our min supported-- version we could consider digging the old code out of git history from-- just before the commit that added this comment.-- For relevant question_bank_entries, this gets the latest non-draft slot number.LEFT JOIN (SELECT lv.questionbankentryid,MAX(CASE WHEN lv.status <> :draft THEN lv.version END) AS usableversion,MAX(lv.version) AS anyversionFROM {quiz_slots} lslotJOIN {question_references} lqr ON lqr.usingcontextid = :quizcontextid2 AND lqr.component = 'mod_quiz'AND lqr.questionarea = 'slot' AND lqr.itemid = lslot.idJOIN {question_versions} lv ON lv.questionbankentryid = lqr.questionbankentryidWHERE lslot.quizid = :quizid2$slotidtest2AND lqr.version IS NULLGROUP BY lv.questionbankentryid) latestversions ON latestversions.questionbankentryid = qr.questionbankentryidLEFT JOIN {question_versions} qv ON qv.questionbankentryid = qbe.id-- Either specified version, or latest usable version, or a draft version.AND qv.version = COALESCE(qr.version,latestversions.usableversion,latestversions.anyversion)LEFT JOIN {question_categories} qc ON qc.id = qbe.questioncategoryidLEFT JOIN {question} q ON q.id = qv.questionid-- Case where a random question has been added.LEFT JOIN {question_set_references} qsr ON qsr.usingcontextid = :quizcontextid3 AND qsr.component = 'mod_quiz'AND qsr.questionarea = 'slot' AND qsr.itemid = slot.idWHERE slot.quizid = :quizid$slotidtestORDER BY slot.slot", $params);// Unpack the random info from question_set_reference.foreach ($slotdata as $slot) {// Ensure the right id is the id.$slot->id = $slot->slotid;if ($slot->filtercondition) {// Unpack the information about a random question.$slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique.$filter = json_decode($slot->filtercondition, true);$slot->filtercondition = question_reference_manager::convert_legacy_set_reference_filter_condition($filter);$slot->category = $slot->filtercondition['filter']['category']['values'][0] ?? 0;$slot->qtype = 'random';$slot->name = get_string('random', 'quiz');$slot->length = 1;} else if ($slot->qtype === null) {// This question must have gone missing. Put in a placeholder.$slot->questionid = 's' . $slot->id; // Sometimes this is used as an array key, so needs to be unique.$slot->category = 0;$slot->qtype = 'missingtype';$slot->name = get_string('missingquestion', 'quiz');$slot->questiontext = ' ';$slot->questiontextformat = FORMAT_HTML;$slot->length = 1;} else if (!\question_bank::qtype_exists($slot->qtype)) {// Question of unknown type found in the database. Set to placeholder question types instead.$slot->qtype = 'missingtype';} else {$slot->_partiallyloaded = 1;}}return $slotdata;}/*** Get this list of random selection tag ids from one of the slots returned by get_question_structure.** @param stdClass $slotdata one of the array elements returned by get_question_structure.* @return array list of tag ids.*/public static function get_tag_ids_for_slot(stdClass $slotdata): array {$tagids = [];if (!isset($slotdata->filtercondition['filter'])) {return $tagids;}$filter = $slotdata->filtercondition['filter'];if (isset($filter['qtagids'])) {$tagids = $filter['qtagids']['values'];}return $tagids;}/*** Given a slot from the array returned by get_question_structure, describe the random question it represents.** @param stdClass $slotdata one of the array elements returned by get_question_structure.* @return string that can be used to display the random slot.*/public static function describe_random_question(stdClass $slotdata): string {$qtagids = self::get_tag_ids_for_slot($slotdata);if ($qtagids) {$tagnames = [];$tags = \core_tag_tag::get_bulk($qtagids, 'id, name');foreach ($tags as $tag) {$tagnames[] = $tag->name;}$description = get_string('randomqnametags', 'mod_quiz', implode(",", $tagnames));} else {$description = get_string('randomqname', 'mod_quiz');}return shorten_text($description, 255);}/*** Choose question for redo in a particular slot.** @param int $quizid the id of the quiz to load the data for.* @param context_module $quizcontext the context of this quiz.* @param int $slotid optional, if passed only load the data for this one slot (if it is in this quiz).* @param qubaid_condition $qubaids attempts to consider when avoiding picking repeats of random questions.* @return int the id of the question to use.*/public static function choose_question_for_redo(int $quizid, context_module $quizcontext,int $slotid, qubaid_condition $qubaids): int {$slotdata = self::get_question_structure($quizid, $quizcontext, $slotid);$slotdata = reset($slotdata);// Non-random question.if ($slotdata->qtype != 'random') {return $slotdata->questionid;}// Random question.$randomloader = new random_question_loader($qubaids, []);$fitlercondition = $slotdata->filtercondition;$filter = $fitlercondition['filter'] ?? [];$newqusetionid = $randomloader->get_next_filtered_question_id($filter);if ($newqusetionid === null) {throw new \moodle_exception('notenoughrandomquestions', 'quiz');}return $newqusetionid;}/*** Check all the questions in an attempt and return information about their versions.** Once a quiz attempt has been started, it continues to use the version of each question* it was started with. This checks the version used for each question, against the* quiz settings for that slot, and returns which version would be used if the quiz* attempt was being started now.** There are several cases for each slot:* - If this slot is currently set to use version 'Always latest' (which includes* random slots) and if there is now a newer version than the one in the attempt,* use that.* - If the slot is currently set to use a fixed version of the question, and that* is different from the version currently in the attempt, use that.* - Otherwise, use the same version.** This is used in places like the re-grade code.** The returned data probably contains a bit more information than is strictly needed,* (see the SQL for details) but returning a few extra ints is fast, and this could* prove invaluable when debugging. The key information is probably:* - questionattemptslot <-- array key* - questionattemptid* - currentversion* - currentquestionid* - newversion* - newquestionid** @param stdClass $attempt a quiz_attempt database row.* @param context_module $quizcontext the quiz context for the quiz the attempt belongs to.* @return array for each question_attempt in the quiz attempt, information about whether it is using* the latest version of the question. Array indexed by questionattemptslot.*/public static function get_version_information_for_questions_in_attempt(stdClass $attempt,context_module $quizcontext,): array {global $DB;return $DB->get_records_sql("SELECT qa.slot AS questionattemptslot,qa.id AS questionattemptid,slot.slot AS quizslot,slot.id AS quizslotid,qr.id AS questionreferenceid,currentqv.version AS currentversion,currentqv.questionid AS currentquestionid,newqv.version AS newversion,newqv.questionid AS newquestionid-- Start with the question currently used in the attempt.FROM {question_attempts} qaJOIN {question_versions} currentqv ON currentqv.questionid = qa.questionid-- Join in the question metadata which says if this is a qa from a 'Try another question like this one'.JOIN {question_attempt_steps} firststep ON firststep.questionattemptid = qa.idAND firststep.sequencenumber = 0LEFT JOIN {question_attempt_step_data} otherslotinfo ON otherslotinfo.attemptstepid = firststep.idAND otherslotinfo.name = :otherslotmetadataname-- Join in the quiz slot information, and hence for non-random slots, the questino_reference.JOIN {quiz_slots} slot ON slot.quizid = :quizidAND slot.slot = COALESCE({$DB->sql_cast_char2int('otherslotinfo.value', true)}, qa.slot)LEFT JOIN {question_references} qr ON qr.usingcontextid = :quizcontextidAND qr.component = 'mod_quiz'AND qr.questionarea = 'slot'AND qr.itemid = slot.id-- Finally, get the new version for this slot.JOIN {question_versions} newqv ON newqv.questionbankentryid = currentqv.questionbankentryidAND newqv.version = COALESCE(-- If the quiz setting say use a particular version, use that.qr.version,-- Otherwise, we need the latest non-draft version of the current questions.(SELECT MAX(version)FROM {question_versions}WHERE questionbankentryid = currentqv.questionbankentryid AND status <> :draft),-- Otherwise, there is not a suitable other version, so stick with the current one.currentqv.version)-- We want this for questions in the current attempt.WHERE qa.questionusageid = :questionusageid-- Order not essential, but fast and good for debugging.ORDER BY qa.slot", ['otherslotmetadataname' => ':_originalslot','quizid' => $attempt->quiz,'quizcontextid' => $quizcontext->id,'draft' => question_version_status::QUESTION_STATUS_DRAFT,'questionusageid' => $attempt->uniqueid,]);}}