Proyectos de Subversion Moodle

Rev

Rev 11 | Mostrar el archivo completo | | | Autoría | Ultima modificación | Ver Log |

Rev 11 Rev 1441
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
}