| Línea 17... |
Línea 17... |
| 17 |
namespace mod_quiz;
|
17 |
namespace mod_quiz;
|
| Línea 18... |
Línea 18... |
| 18 |
|
18 |
|
| 19 |
use coding_exception;
|
19 |
use coding_exception;
|
| 20 |
use context_module;
|
20 |
use context_module;
|
| - |
|
21 |
use core\output\inplace_editable;
|
| 21 |
use core\output\inplace_editable;
|
22 |
use core_question\local\bank\version_options;
|
| 22 |
use mod_quiz\event\quiz_grade_item_created;
|
23 |
use mod_quiz\event\quiz_grade_item_created;
|
| 23 |
use mod_quiz\event\quiz_grade_item_deleted;
|
24 |
use mod_quiz\event\quiz_grade_item_deleted;
|
| 24 |
use mod_quiz\event\quiz_grade_item_updated;
|
25 |
use mod_quiz\event\quiz_grade_item_updated;
|
| 25 |
use mod_quiz\event\slot_grade_item_updated;
|
26 |
use mod_quiz\event\slot_grade_item_updated;
|
| - |
|
27 |
use mod_quiz\event\slot_mark_updated;
|
| 26 |
use mod_quiz\event\slot_mark_updated;
|
28 |
use mod_quiz\event\slot_version_updated;
|
| 27 |
use mod_quiz\question\bank\qbank_helper;
|
29 |
use mod_quiz\question\bank\qbank_helper;
|
| 28 |
use mod_quiz\question\qubaids_for_quiz;
|
30 |
use mod_quiz\question\qubaids_for_quiz;
|
| Línea 29... |
Línea 31... |
| 29 |
use stdClass;
|
31 |
use stdClass;
|
| Línea 40... |
Línea 42... |
| 40 |
* @package mod_quiz
|
42 |
* @package mod_quiz
|
| 41 |
* @copyright 2014 The Open University
|
43 |
* @copyright 2014 The Open University
|
| 42 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
44 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
| 43 |
*/
|
45 |
*/
|
| 44 |
class structure {
|
46 |
class structure {
|
| - |
|
47 |
|
| - |
|
48 |
/**
|
| - |
|
49 |
* Placeholder string used when a question category is missing.
|
| - |
|
50 |
*/
|
| - |
|
51 |
const MISSING_QUESTION_CATEGORY_PLACEHOLDER = 'missing_question_category';
|
| - |
|
52 |
|
| 45 |
/** @var quiz_settings the quiz this is the structure of. */
|
53 |
/** @var quiz_settings the quiz this is the structure of. */
|
| 46 |
protected $quizobj = null;
|
54 |
protected $quizobj = null;
|
| Línea 47... |
Línea 55... |
| 47 |
|
55 |
|
| 48 |
/**
|
56 |
/**
|
| Línea 66... |
Línea 74... |
| 66 |
protected $canbeedited = null;
|
74 |
protected $canbeedited = null;
|
| Línea 67... |
Línea 75... |
| 67 |
|
75 |
|
| 68 |
/** @var bool caches the results of can_add_random_question. */
|
76 |
/** @var bool caches the results of can_add_random_question. */
|
| Línea -... |
Línea 77... |
| - |
|
77 |
protected $canaddrandom = null;
|
| - |
|
78 |
|
| - |
|
79 |
/** @var array the slotids => question categories array for all slots containing a random question. */
|
| - |
|
80 |
protected $randomslotcategories = null;
|
| - |
|
81 |
|
| - |
|
82 |
/** @var array the slotids => question tags array for all slots containing a random question. */
|
| - |
|
83 |
protected $randomslottags = null;
|
| - |
|
84 |
|
| - |
|
85 |
/** @var array an array of question banks course_modules records indexed by their associated contextid */
|
| 69 |
protected $canaddrandom = null;
|
86 |
protected array $questionsources = [];
|
| 70 |
|
87 |
|
| 71 |
/**
|
88 |
/**
|
| 72 |
* Create an instance of this class representing an empty quiz.
|
89 |
* Create an instance of this class representing an empty quiz.
|
| 73 |
*
|
90 |
*
|
| Línea 801... |
Línea 818... |
| 801 |
*/
|
818 |
*/
|
| 802 |
public function get_version_choices_for_slot(int $slotnumber): array {
|
819 |
public function get_version_choices_for_slot(int $slotnumber): array {
|
| 803 |
$slot = $this->get_slot_by_number($slotnumber);
|
820 |
$slot = $this->get_slot_by_number($slotnumber);
|
| Línea 804... |
Línea 821... |
| 804 |
|
821 |
|
| 805 |
// Get all the versions which exist.
|
822 |
// Get all the versions which exist.
|
| 806 |
$versions = qbank_helper::get_version_options($slot->questionid);
|
823 |
$versions = version_options::get_version_menu_options($slot->questionid);
|
| Línea 807... |
Línea 824... |
| 807 |
$latestversion = reset($versions);
|
824 |
$versioninfo = [];
|
| 808 |
|
- |
|
| 809 |
// Format the choices for display.
|
825 |
|
| 810 |
$versionoptions = [];
|
826 |
// Loop through them and set which one is selected.
|
| 811 |
foreach ($versions as $version) {
|
- |
|
| 812 |
$version->selected = $version->version === $slot->requestedversion;
|
827 |
foreach ($versions as $versionnumber => $version) {
|
| 813 |
|
- |
|
| 814 |
if ($version->version === $latestversion->version) {
|
828 |
$versioninfo[] = (object)[
|
| 815 |
$version->versionvalue = get_string('questionversionlatest', 'quiz', $version->version);
|
829 |
'version' => $versionnumber,
|
| 816 |
} else {
|
830 |
'versionvalue' => $version,
|
| 817 |
$version->versionvalue = get_string('questionversion', 'quiz', $version->version);
|
- |
|
| 818 |
}
|
- |
|
| 819 |
|
831 |
'selected' => ($versionnumber == $slot->requestedversion),
|
| Línea 820... |
Línea -... |
| 820 |
$versionoptions[] = $version;
|
- |
|
| 821 |
}
|
- |
|
| 822 |
|
- |
|
| 823 |
// Make a choice for 'Always latest'.
|
- |
|
| 824 |
$alwaysuselatest = new stdClass();
|
- |
|
| 825 |
$alwaysuselatest->versionid = 0;
|
- |
|
| 826 |
$alwaysuselatest->version = 0;
|
- |
|
| 827 |
$alwaysuselatest->versionvalue = get_string('alwayslatest', 'quiz');
|
- |
|
| 828 |
$alwaysuselatest->selected = $slot->requestedversion === null;
|
832 |
];
|
| 829 |
array_unshift($versionoptions, $alwaysuselatest);
|
833 |
}
|
| Línea 830... |
Línea 834... |
| 830 |
|
834 |
|
| 831 |
return $versionoptions;
|
835 |
return $versioninfo;
|
| 832 |
}
|
836 |
}
|
| Línea 1167... |
Línea 1171... |
| 1167 |
$transaction->allow_commit();
|
1171 |
$transaction->allow_commit();
|
| 1168 |
return true;
|
1172 |
return true;
|
| 1169 |
}
|
1173 |
}
|
| Línea 1170... |
Línea 1174... |
| 1170 |
|
1174 |
|
| - |
|
1175 |
/**
|
| - |
|
1176 |
* Update the question version for a given slot, if necessary.
|
| - |
|
1177 |
*
|
| - |
|
1178 |
* @param int $id ID of row from the quiz_slots table.
|
| - |
|
1179 |
* @param int|null $newversion The new question version for the slot.
|
| - |
|
1180 |
* A null value means 'Always latest'.
|
| - |
|
1181 |
* @return bool True if the version was updated, false if no update was required.
|
| - |
|
1182 |
* @throws coding_exception If the specified version does not exist.
|
| - |
|
1183 |
*/
|
| - |
|
1184 |
public function update_slot_version(int $id, ?int $newversion): bool {
|
| - |
|
1185 |
global $DB;
|
| - |
|
1186 |
|
| - |
|
1187 |
$slot = $this->get_slot_by_id($id);
|
| - |
|
1188 |
$context = $this->quizobj->get_context();
|
| - |
|
1189 |
$refparams = ['usingcontextid' => $context->id, 'component' => 'mod_quiz', 'questionarea' => 'slot', 'itemid' => $slot->id];
|
| - |
|
1190 |
$reference = $DB->get_record('question_references', $refparams, '*', MUST_EXIST);
|
| - |
|
1191 |
$oldversion = is_null($reference->version) ? null : (int) $reference->version;
|
| - |
|
1192 |
$reference->version = $newversion === 0 ? null : $newversion;
|
| - |
|
1193 |
$existsparams = ['questionbankentryid' => $reference->questionbankentryid, 'version' => $newversion];
|
| - |
|
1194 |
$versionexists = $DB->record_exists('question_versions', $existsparams);
|
| - |
|
1195 |
|
| - |
|
1196 |
// We are attempting to switch to an existing version.
|
| - |
|
1197 |
// Verify that the version we want to switch to exists.
|
| - |
|
1198 |
if (!is_null($newversion) && !$versionexists) {
|
| - |
|
1199 |
throw new coding_exception(
|
| - |
|
1200 |
'Version: ' . $newversion . ' ' .
|
| - |
|
1201 |
'does not exist for question bank entry: ' . $reference->questionbankentryid
|
| - |
|
1202 |
);
|
| - |
|
1203 |
}
|
| - |
|
1204 |
|
| - |
|
1205 |
if ($newversion === $oldversion) {
|
| - |
|
1206 |
return false;
|
| - |
|
1207 |
}
|
| - |
|
1208 |
|
| - |
|
1209 |
$transaction = $DB->start_delegated_transaction();
|
| - |
|
1210 |
$DB->update_record('question_references', $reference);
|
| - |
|
1211 |
slot_version_updated::create([
|
| - |
|
1212 |
'context' => $this->quizobj->get_context(),
|
| - |
|
1213 |
'objectid' => $slot->id,
|
| - |
|
1214 |
'other' => [
|
| - |
|
1215 |
'quizid' => $this->get_quizid(),
|
| - |
|
1216 |
'previousversion' => $oldversion,
|
| - |
|
1217 |
'newversion' => $reference->version,
|
| - |
|
1218 |
],
|
| - |
|
1219 |
])->trigger();
|
| - |
|
1220 |
$transaction->allow_commit();
|
| - |
|
1221 |
|
| - |
|
1222 |
return true;
|
| - |
|
1223 |
}
|
| - |
|
1224 |
|
| 1171 |
/**
|
1225 |
/**
|
| 1172 |
* Change which grade this slot contributes to, for quizzes with multiple grades.
|
1226 |
* Change which grade this slot contributes to, for quizzes with multiple grades.
|
| 1173 |
*
|
1227 |
*
|
| 1174 |
* It does not update 'sumgrades' in the quiz table. If this method returns true,
|
1228 |
* It does not update 'sumgrades' in the quiz table. If this method returns true,
|
| 1175 |
* it will be necessary to recompute all the quiz grades.
|
1229 |
* it will be necessary to recompute all the quiz grades.
|
| Línea 1648... |
Línea 1702... |
| 1648 |
$randomslot->set_quiz($this->get_quiz());
|
1702 |
$randomslot->set_quiz($this->get_quiz());
|
| 1649 |
$randomslot->set_filter_condition(json_encode($filtercondition));
|
1703 |
$randomslot->set_filter_condition(json_encode($filtercondition));
|
| 1650 |
$randomslot->insert($addonpage);
|
1704 |
$randomslot->insert($addonpage);
|
| 1651 |
}
|
1705 |
}
|
| 1652 |
}
|
1706 |
}
|
| - |
|
1707 |
|
| - |
|
1708 |
/**
|
| - |
|
1709 |
* Get a human-readable description of a random slot.
|
| - |
|
1710 |
*
|
| - |
|
1711 |
* @param int $slotid id of slot.
|
| - |
|
1712 |
* @return string that can be used to display the random slot.
|
| - |
|
1713 |
*/
|
| - |
|
1714 |
public function describe_random_slot(int $slotid): string {
|
| - |
|
1715 |
$this->ensure_random_slot_info_loaded();
|
| - |
|
1716 |
|
| - |
|
1717 |
if (!isset($this->randomslotcategories[$slotid])) {
|
| - |
|
1718 |
throw new coding_exception('Called describe_random_slot on slot id ' .
|
| - |
|
1719 |
$slotid . ' which is not a random slot.');
|
| - |
|
1720 |
}
|
| - |
|
1721 |
|
| - |
|
1722 |
// Build the random question name with categories and tags information and return.
|
| - |
|
1723 |
$a = new stdClass();
|
| - |
|
1724 |
$a->category = $this->randomslotcategories[$slotid];
|
| - |
|
1725 |
$stringid = 'randomqnamecat';
|
| - |
|
1726 |
|
| - |
|
1727 |
if (!empty($this->randomslottags[$slotid])) {
|
| - |
|
1728 |
$a->tags = $this->randomslottags[$slotid];
|
| - |
|
1729 |
$stringid = 'randomqnamecattags';
|
| - |
|
1730 |
}
|
| - |
|
1731 |
|
| - |
|
1732 |
return shorten_text(get_string($stringid, 'quiz', $a), 255);
|
| - |
|
1733 |
}
|
| - |
|
1734 |
|
| - |
|
1735 |
/**
|
| - |
|
1736 |
* Ensure that {@see load_random_slot_info()} has been called, so the data is available.
|
| - |
|
1737 |
*/
|
| - |
|
1738 |
protected function ensure_random_slot_info_loaded(): void {
|
| - |
|
1739 |
if ($this->randomslotcategories == null) {
|
| - |
|
1740 |
$this->load_random_slot_info();
|
| - |
|
1741 |
}
|
| - |
|
1742 |
}
|
| - |
|
1743 |
|
| - |
|
1744 |
/**
|
| - |
|
1745 |
* Load information about the question categories and tags for all random slots,
|
| - |
|
1746 |
*/
|
| - |
|
1747 |
protected function load_random_slot_info(): void {
|
| - |
|
1748 |
global $DB;
|
| - |
|
1749 |
|
| - |
|
1750 |
// Find the random slots.
|
| - |
|
1751 |
$allslots = $this->get_slots();
|
| - |
|
1752 |
foreach ($allslots as $key => $slot) {
|
| - |
|
1753 |
if ($slot->qtype != 'random') {
|
| - |
|
1754 |
unset($allslots[$key]);
|
| - |
|
1755 |
}
|
| - |
|
1756 |
}
|
| - |
|
1757 |
if (empty($allslots)) {
|
| - |
|
1758 |
// No random slots. Nothing to do.
|
| - |
|
1759 |
$this->randomslotcategories = [];
|
| - |
|
1760 |
$this->randomslottags = [];
|
| - |
|
1761 |
return;
|
| - |
|
1762 |
}
|
| - |
|
1763 |
|
| - |
|
1764 |
// Loop over all random slots to build arrays of the data we will need.
|
| - |
|
1765 |
$tagids = [];
|
| - |
|
1766 |
$questioncategoriesids = [];
|
| - |
|
1767 |
// An associative array of slotid. Example structure:
|
| - |
|
1768 |
// ['cat' => [values => catid, 'includesubcategories' => true, 'tag' => [tagid, tagid, ...]].
|
| - |
|
1769 |
$randomcategoriesandtags = [];
|
| - |
|
1770 |
foreach ($allslots as $slotid => $slot) {
|
| - |
|
1771 |
foreach ($slot->filtercondition as $name => $value) {
|
| - |
|
1772 |
if ($name !== 'filter') {
|
| - |
|
1773 |
continue;
|
| - |
|
1774 |
}
|
| - |
|
1775 |
|
| - |
|
1776 |
// Parse the filter condition.
|
| - |
|
1777 |
foreach ($value as $filteroption => $filtervalue) {
|
| - |
|
1778 |
if ($filteroption === 'category') {
|
| - |
|
1779 |
$randomcategoriesandtags[$slotid]['cat']['values'] = $questioncategoriesids[] = $filtervalue['values'][0];
|
| - |
|
1780 |
$randomcategoriesandtags[$slotid]['cat']['includesubcategories'] =
|
| - |
|
1781 |
$filtervalue['filteroptions']['includesubcategories'] ?? false;
|
| - |
|
1782 |
}
|
| - |
|
1783 |
|
| - |
|
1784 |
if ($filteroption === 'qtagids') {
|
| - |
|
1785 |
foreach ($filtervalue as $qtagidsoption => $qtagidsvalue) {
|
| - |
|
1786 |
if ($qtagidsoption !== 'values') {
|
| - |
|
1787 |
continue;
|
| - |
|
1788 |
}
|
| - |
|
1789 |
foreach ($qtagidsvalue as $qtagidsvaluevalue) {
|
| - |
|
1790 |
$randomcategoriesandtags[$slotid]['tag'][] = $qtagidsvaluevalue;
|
| - |
|
1791 |
$tagids[] = $qtagidsvaluevalue;
|
| - |
|
1792 |
}
|
| - |
|
1793 |
}
|
| - |
|
1794 |
}
|
| - |
|
1795 |
}
|
| - |
|
1796 |
}
|
| - |
|
1797 |
}
|
| - |
|
1798 |
|
| - |
|
1799 |
// Get names for all tags into a tagid => name array.
|
| - |
|
1800 |
$tags = \core_tag_tag::get_bulk($tagids, 'id, rawname');
|
| - |
|
1801 |
$tagnames = array_map(fn($tag) => $tag->get_display_name(), $tags);
|
| - |
|
1802 |
|
| - |
|
1803 |
// Get names for all question categories.
|
| - |
|
1804 |
$categories = $DB->get_records_list('question_categories', 'id', $questioncategoriesids,
|
| - |
|
1805 |
'id', 'id, name, contextid, parent');
|
| - |
|
1806 |
|
| - |
|
1807 |
// Now, put the data required for each slot into $this->randomslotcategories and $this->randomslottags.
|
| - |
|
1808 |
foreach ($randomcategoriesandtags as $slotid => $catandtags) {
|
| - |
|
1809 |
$qcategoryid = $catandtags['cat']['values'];
|
| - |
|
1810 |
if (!array_key_exists($qcategoryid, $categories)) {
|
| - |
|
1811 |
$this->randomslotcategories[$slotid] = self::MISSING_QUESTION_CATEGORY_PLACEHOLDER;
|
| - |
|
1812 |
continue;
|
| - |
|
1813 |
}
|
| - |
|
1814 |
$qcategory = $categories[$qcategoryid];
|
| - |
|
1815 |
$includesubcategories = $catandtags['cat']['includesubcategories'];
|
| - |
|
1816 |
$this->randomslotcategories[$slotid] = $this->get_used_category_description($qcategory, $includesubcategories);
|
| - |
|
1817 |
if (isset($catandtags['tag'])) {
|
| - |
|
1818 |
$slottagnames = [];
|
| - |
|
1819 |
foreach ($catandtags['tag'] as $tagid) {
|
| - |
|
1820 |
$slottagnames[] = $tagnames[$tagid];
|
| - |
|
1821 |
}
|
| - |
|
1822 |
$this->randomslottags[$slotid] = implode(', ', $slottagnames);
|
| - |
|
1823 |
}
|
| - |
|
1824 |
|
| - |
|
1825 |
}
|
| - |
|
1826 |
}
|
| - |
|
1827 |
|
| - |
|
1828 |
/**
|
| - |
|
1829 |
* Returns a description of the used question category, taking into account the context and whether subcategories are
|
| - |
|
1830 |
* included.
|
| - |
|
1831 |
*
|
| - |
|
1832 |
* @param stdClass $qcategory The question category object containing category details.
|
| - |
|
1833 |
* @param bool $includesubcategories Whether subcategories are included.
|
| - |
|
1834 |
* @return string The generated description based on the used category.
|
| - |
|
1835 |
* @throws coding_exception If the context level is unsupported.
|
| - |
|
1836 |
*/
|
| - |
|
1837 |
private function get_used_category_description(stdClass $qcategory, bool $includesubcategories): string {
|
| - |
|
1838 |
|
| - |
|
1839 |
$context = \context::instance_by_id($qcategory->contextid);
|
| - |
|
1840 |
|
| - |
|
1841 |
if ($context->contextlevel != CONTEXT_MODULE) {
|
| - |
|
1842 |
throw new coding_exception('Unsupported context.');
|
| - |
|
1843 |
}
|
| - |
|
1844 |
|
| - |
|
1845 |
if ($qcategory->name === 'top') { // This is a "top" question category.
|
| - |
|
1846 |
if (!$includesubcategories) {
|
| - |
|
1847 |
// Question categories labeled as "top" cannot directly contain questions. If the subcategories that may
|
| - |
|
1848 |
// hold questions are excluded, the generated random questions will be invalid. Thus, return a description
|
| - |
|
1849 |
// that informs the user about the issues associated with these types of generated random questions.
|
| - |
|
1850 |
return get_string('randomfaultynosubcat', 'mod_quiz');
|
| - |
|
1851 |
}
|
| - |
|
1852 |
return get_string('randommodulewithsubcat', 'mod_quiz');
|
| - |
|
1853 |
}
|
| - |
|
1854 |
// Otherwise, return the description of the used standard question category, also indicating whether subcategories
|
| - |
|
1855 |
// are included.
|
| - |
|
1856 |
return $includesubcategories ? get_string('randomcatwithsubcat', 'mod_quiz', $qcategory->name) :
|
| - |
|
1857 |
$qcategory->name;
|
| - |
|
1858 |
}
|
| - |
|
1859 |
|
| - |
|
1860 |
/**
|
| - |
|
1861 |
* Populate question_sources with cm records for later reference.
|
| - |
|
1862 |
*
|
| - |
|
1863 |
* @return void
|
| - |
|
1864 |
*/
|
| - |
|
1865 |
private function populate_question_sources(): void {
|
| - |
|
1866 |
global $DB;
|
| - |
|
1867 |
|
| - |
|
1868 |
$contextids = array_map(fn($question) => $question->contextid, $this->questions);
|
| - |
|
1869 |
[$insql, $inparams] = $DB->get_in_or_equal(array_unique($contextids));
|
| - |
|
1870 |
|
| - |
|
1871 |
$sql = "
|
| - |
|
1872 |
SELECT c.id as contextid, cm.id, cm.course
|
| - |
|
1873 |
FROM {context} c
|
| - |
|
1874 |
JOIN {course_modules} cm ON cm.id = c.instanceid AND c.contextlevel = ?
|
| - |
|
1875 |
WHERE c.id {$insql}
|
| - |
|
1876 |
";
|
| - |
|
1877 |
$params = array_merge([context_module::LEVEL], $inparams);
|
| - |
|
1878 |
|
| - |
|
1879 |
$this->questionsources = $DB->get_records_sql($sql, $params);
|
| - |
|
1880 |
}
|
| - |
|
1881 |
|
| - |
|
1882 |
/**
|
| - |
|
1883 |
* Get data on the question bank being used by the question in the slot.
|
| - |
|
1884 |
*
|
| - |
|
1885 |
* @param int $slot slot number
|
| - |
|
1886 |
* @return stdClass|null
|
| - |
|
1887 |
*/
|
| - |
|
1888 |
public function get_source_bank(int $slot): ?stdClass {
|
| - |
|
1889 |
$questionid = $this->slotsinorder[$slot]->questionid;
|
| - |
|
1890 |
|
| - |
|
1891 |
$this->questionsources[$this->questions[$questionid]->contextid] ?? $this->populate_question_sources();
|
| - |
|
1892 |
|
| - |
|
1893 |
// This shouldn't happen as all categories belong to a module context level but let's account for it.
|
| - |
|
1894 |
if (empty($this->questionsources[$this->questions[$questionid]->contextid])) {
|
| - |
|
1895 |
return null;
|
| - |
|
1896 |
}
|
| - |
|
1897 |
|
| - |
|
1898 |
$cminfo = \cm_info::create($this->questionsources[$this->questions[$questionid]->contextid]);
|
| - |
|
1899 |
|
| - |
|
1900 |
return (object) [
|
| - |
|
1901 |
'cminfo' => $cminfo,
|
| - |
|
1902 |
'issharedbank' => plugin_supports('mod', $cminfo->modname, FEATURE_PUBLISHES_QUESTIONS, false),
|
| - |
|
1903 |
];
|
| - |
|
1904 |
}
|
| 1653 |
}
|
1905 |
}
|