Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
namespace mod_quiz;
18
 
19
use cm_info;
20
use coding_exception;
21
use context;
22
use context_module;
23
use core_question\local\bank\question_version_status;
24
use mod_quiz\question\bank\qbank_helper;
25
use mod_quiz\question\display_options;
26
use moodle_exception;
27
use moodle_url;
28
use question_bank;
29
use stdClass;
30
 
31
/**
32
 * A class encapsulating the settings for a quiz.
33
 *
34
 * When this class is initialised, it may have the settings adjusted to account
35
 * for the overrides for a particular user. See the create methods.
36
 *
37
 * Initially, it only loads a minimal amount of information about each question - loading
38
 * extra information only when necessary or when asked. The class tracks which questions
39
 * are loaded.
40
 *
41
 * @package   mod_quiz
42
 * @copyright 2008 Tim Hunt
43
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
44
 */
45
class quiz_settings {
46
    /** @var stdClass the course settings from the database. */
47
    protected $course;
48
    /** @var cm_info the course_module settings from the database. */
49
    protected $cm;
50
    /** @var stdClass the quiz settings from the database. */
51
    protected $quiz;
52
    /** @var context the quiz context. */
53
    protected $context;
54
 
55
    /**
56
     * @var stdClass[] of questions augmented with slot information. For non-random
57
     *     questions, the array key is question id. For random quesions it is 's' . $slotid.
58
     *     probalby best to use ->questionid field of the object instead.
59
     */
60
    protected $questions = null;
61
    /** @var stdClass[] of quiz_section rows. */
62
    protected $sections = null;
63
    /** @var access_manager the access manager for this quiz. */
64
    protected $accessmanager = null;
65
    /** @var bool whether the current user has capability mod/quiz:preview. */
66
    protected $ispreviewuser = null;
67
 
68
    /** @var grade_calculator|null grade calculator for this quiz. */
69
    protected ?grade_calculator $gradecalculator = null;
70
 
71
    // Constructor =============================================================.
72
 
73
    /**
74
     * Constructor, assuming we already have the necessary data loaded.
75
     *
76
     * @param stdClass $quiz the row from the quiz table.
77
     * @param stdClass $cm the course_module object for this quiz.
78
     * @param stdClass $course the row from the course table for the course we belong to.
79
     * @param bool $getcontext intended for testing - stops the constructor getting the context.
80
     */
81
    public function __construct($quiz, $cm, $course, $getcontext = true) {
82
        $this->quiz = $quiz;
83
        $this->cm = $cm;
84
        $this->quiz->cmid = $this->cm->id;
85
        $this->course = $course;
86
        if ($getcontext && !empty($cm->id)) {
87
            $this->context = context_module::instance($cm->id);
88
        }
89
    }
90
 
91
    /**
92
     * Helper used by the other factory methods.
93
     *
94
     * @param stdClass $quiz
95
     * @param cm_info $cm
96
     * @param stdClass $course
97
     * @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
98
     * @return quiz_settings the new quiz settings object.
99
     */
100
    protected static function create_helper(stdClass $quiz, cm_info $cm, stdClass $course, ?int $userid): self {
101
        // Update quiz with override information.
102
        if ($userid) {
103
            $quiz = quiz_update_effective_access($quiz, $userid);
104
        }
105
 
106
        return new quiz_settings($quiz, $cm, $course);
107
    }
108
 
109
    /**
110
     * Static function to create a new quiz settings object from a quiz id, for a specific user.
111
     *
112
     * @param int $quizid the quiz id.
113
     * @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
114
     * @return quiz_settings the new quiz settings object.
115
     */
116
    public static function create(int $quizid, int $userid = null): self {
117
        $quiz = access_manager::load_quiz_and_settings($quizid);
118
        [$course, $cm] = get_course_and_cm_from_instance($quiz, 'quiz');
119
 
120
        return self::create_helper($quiz, $cm, $course, $userid);
121
    }
122
 
123
    /**
124
     * Static function to create a new quiz settings object from a cmid, for a specific user.
125
     *
126
     * @param int $cmid the course-module id.
127
     * @param int|null $userid the the userid (optional). If passed, relevant overrides are applied.
128
     * @return quiz_settings the new quiz settings object.
129
     */
130
    public static function create_for_cmid(int $cmid, int $userid = null): self {
131
        [$course, $cm] = get_course_and_cm_from_cmid($cmid, 'quiz');
132
        $quiz = access_manager::load_quiz_and_settings($cm->instance);
133
 
134
        return self::create_helper($quiz, $cm, $course, $userid);
135
    }
136
 
137
    /**
138
     * Create a {@see quiz_attempt} for an attempt at this quiz.
139
     *
140
     * @param stdClass $attemptdata row from the quiz_attempts table.
141
     * @return quiz_attempt the new quiz_attempt object.
142
     */
143
    public function create_attempt_object($attemptdata) {
144
        return new quiz_attempt($attemptdata, $this->quiz, $this->cm, $this->course);
145
    }
146
 
147
    // Functions for loading more data =========================================.
148
 
149
    /**
150
     * Load just basic information about all the questions in this quiz.
151
     */
152
    public function preload_questions() {
153
        $slots = qbank_helper::get_question_structure($this->quiz->id, $this->context);
154
        $this->questions = [];
155
        foreach ($slots as $slot) {
156
            $this->questions[$slot->questionid] = $slot;
157
        }
158
    }
159
 
160
    /**
161
     * Fully load some or all of the questions for this quiz. You must call
162
     * {@see preload_questions()} first.
163
     *
164
     * @param array|null $deprecated no longer supported (it was not used).
165
     */
166
    public function load_questions($deprecated = null) {
167
        if ($deprecated !== null) {
168
            debugging('The argument to quiz::load_questions is no longer supported. ' .
169
                    'All questions are always loaded.', DEBUG_DEVELOPER);
170
        }
171
        if ($this->questions === null) {
172
            throw new coding_exception('You must call preload_questions before calling load_questions.');
173
        }
174
 
175
        $questionstoprocess = [];
176
        foreach ($this->questions as $question) {
177
            if (is_number($question->questionid)) {
178
                $question->id = $question->questionid;
179
                $questionstoprocess[$question->questionid] = $question;
180
            }
181
        }
182
        get_question_options($questionstoprocess);
183
    }
184
 
185
    /**
186
     * Get an instance of the {@see \mod_quiz\structure} class for this quiz.
187
     *
188
     * @return structure describes the questions in the quiz.
189
     */
190
    public function get_structure() {
191
        return structure::create_for_quiz($this);
192
    }
193
 
194
    // Simple getters ==========================================================.
195
 
196
    /**
197
     * Get the id of the course this quiz belongs to.
198
     *
199
     * @return int the course id.
200
     */
201
    public function get_courseid() {
202
        return $this->course->id;
203
    }
204
 
205
    /**
206
     * Get the course settings object that this quiz belongs to.
207
     *
208
     * @return stdClass the row of the course table.
209
     */
210
    public function get_course() {
211
        return $this->course;
212
    }
213
 
214
    /**
215
     * Get this quiz's id (in the quiz table).
216
     *
217
     * @return int the quiz id.
218
     */
219
    public function get_quizid() {
220
        return $this->quiz->id;
221
    }
222
 
223
    /**
224
     * Get the quiz settings object.
225
     *
226
     * @return stdClass the row of the quiz table.
227
     */
228
    public function get_quiz() {
229
        return $this->quiz;
230
    }
231
 
232
    /**
233
     * Get the quiz name.
234
     *
235
     * @return string the name of this quiz.
236
     */
237
    public function get_quiz_name() {
238
        return $this->quiz->name;
239
    }
240
 
241
    /**
242
     * Get the navigation method in use.
243
     *
244
     * @return int QUIZ_NAVMETHOD_FREE or QUIZ_NAVMETHOD_SEQ.
245
     */
246
    public function get_navigation_method() {
247
        return $this->quiz->navmethod;
248
    }
249
 
250
    /**
251
     * How many attepts is the user allowed at this quiz?
252
     *
253
     * @return int the number of attempts allowed at this quiz (0 = infinite).
254
     */
255
    public function get_num_attempts_allowed() {
256
        return $this->quiz->attempts;
257
    }
258
 
259
    /**
260
     * Get the course-module id for this quiz.
261
     *
262
     * @return int the course_module id.
263
     */
264
    public function get_cmid() {
265
        return $this->cm->id;
266
    }
267
 
268
    /**
269
     * Get the course-module object for this quiz.
270
     *
271
     * @return cm_info the course_module object.
272
     */
273
    public function get_cm() {
274
        return $this->cm;
275
    }
276
 
277
    /**
278
     * Get the quiz context.
279
     *
280
     * @return context_module the module context for this quiz.
281
     */
282
    public function get_context() {
283
        return $this->context;
284
    }
285
 
286
    /**
287
     * Is the current user is someone who previews the quiz, rather than attempting it?
288
     *
289
     * @return bool true user is a preview user. False, if they can do real attempts.
290
     */
291
    public function is_preview_user() {
292
        if (is_null($this->ispreviewuser)) {
293
            $this->ispreviewuser = has_capability('mod/quiz:preview', $this->context);
294
        }
295
        return $this->ispreviewuser;
296
    }
297
 
298
    /**
299
     * Checks user enrollment in the current course.
300
     *
301
     * @param int $userid the id of the user to check.
302
     * @return bool whether the user is enrolled.
303
     */
304
    public function is_participant($userid) {
305
        return is_enrolled($this->get_context(), $userid, 'mod/quiz:attempt', $this->show_only_active_users());
306
    }
307
 
308
    /**
309
     * Check is only active users in course should be shown.
310
     *
311
     * @return bool true if only active users should be shown.
312
     */
313
    public function show_only_active_users() {
314
        return !has_capability('moodle/course:viewsuspendedusers', $this->get_context());
315
    }
316
 
317
    /**
318
     * Have any questions been added to this quiz yet?
319
     *
320
     * @return bool whether any questions have been added to this quiz.
321
     */
322
    public function has_questions() {
323
        if ($this->questions === null) {
324
            $this->preload_questions();
325
        }
326
        return !empty($this->questions);
327
    }
328
 
329
    /**
330
     * Get a particular question in this quiz, by its id.
331
     *
332
     * @param int $id the question id.
333
     * @return stdClass the question object with that id.
334
     */
335
    public function get_question($id) {
336
        return $this->questions[$id];
337
    }
338
 
339
    /**
340
     * Get some of the question in this quiz.
341
     *
342
     * @param array|null $questionids question ids of the questions to load. null for all.
343
     * @param bool $requirequestionfullyloaded Whether to require that a particular question is fully loaded.
344
     * @return stdClass[] the question data objects.
345
     */
346
    public function get_questions(?array $questionids = null, bool $requirequestionfullyloaded = true) {
347
        if (is_null($questionids)) {
348
            $questionids = array_keys($this->questions);
349
        }
350
        $questions = [];
351
        foreach ($questionids as $id) {
352
            if (!array_key_exists($id, $this->questions)) {
353
                throw new moodle_exception('cannotstartmissingquestion', 'quiz', $this->view_url());
354
            }
355
            $questions[$id] = $this->questions[$id];
356
            if ($requirequestionfullyloaded) {
357
                $this->ensure_question_loaded($id);
358
            }
359
        }
360
        return $questions;
361
    }
362
 
363
    /**
364
     * Get all the sections in this quiz.
365
     *
366
     * @return array 0, 1, 2, ... => quiz_sections row from the database.
367
     */
368
    public function get_sections() {
369
        global $DB;
370
        if ($this->sections === null) {
371
            $this->sections = array_values($DB->get_records('quiz_sections',
372
                    ['quizid' => $this->get_quizid()], 'firstslot'));
373
        }
374
        return $this->sections;
375
    }
376
 
377
    /**
378
     * Return access_manager and instance of the access_manager class
379
     * for this quiz at this time.
380
     *
381
     * @param int $timenow the current time as a unix timestamp.
382
     * @return access_manager an instance of the access_manager class
383
     *      for this quiz at this time.
384
     */
385
    public function get_access_manager($timenow) {
386
        if (is_null($this->accessmanager)) {
387
            $this->accessmanager = new access_manager($this, $timenow,
388
                    has_capability('mod/quiz:ignoretimelimits', $this->context, null, false));
389
        }
390
        return $this->accessmanager;
391
    }
392
 
393
    /**
394
     * Return the grade_calculator object for this quiz.
395
     *
396
     * @return grade_calculator
397
     */
398
    public function get_grade_calculator(): grade_calculator {
399
        if ($this->gradecalculator === null) {
400
            $this->gradecalculator = grade_calculator::create($this);
401
        }
402
 
403
        return $this->gradecalculator;
404
    }
405
 
406
    /**
407
     * Wrapper round the has_capability funciton that automatically passes in the quiz context.
408
     *
409
     * @param string $capability the name of the capability to check. For example mod/quiz:view.
410
     * @param int|null $userid A user id. By default (null) checks the permissions of the current user.
411
     * @param bool $doanything If false, ignore effect of admin role assignment.
412
     * @return boolean true if the user has this capability. Otherwise false.
413
     */
414
    public function has_capability($capability, $userid = null, $doanything = true) {
415
        return has_capability($capability, $this->context, $userid, $doanything);
416
    }
417
 
418
    /**
419
     * Wrapper round the require_capability function that automatically passes in the quiz context.
420
     *
421
     * @param string $capability the name of the capability to check. For example mod/quiz:view.
422
     * @param int|null $userid A user id. By default (null) checks the permissions of the current user.
423
     * @param bool $doanything If false, ignore effect of admin role assignment.
424
     */
425
    public function require_capability($capability, $userid = null, $doanything = true) {
426
        require_capability($capability, $this->context, $userid, $doanything);
427
    }
428
 
429
    // URLs related to this attempt ============================================.
430
 
431
    /**
432
     * Get the URL of this quiz's view.php page.
433
     *
434
     * @return moodle_url the URL of this quiz's view page.
435
     */
436
    public function view_url() {
437
        return new moodle_url('/mod/quiz/view.php', ['id' => $this->cm->id]);
438
    }
439
 
440
    /**
441
     * Get the URL of this quiz's edit questions page.
442
     *
443
     * @return moodle_url the URL of this quiz's edit page.
444
     */
445
    public function edit_url() {
446
        return new moodle_url('/mod/quiz/edit.php', ['cmid' => $this->cm->id]);
447
    }
448
 
449
    /**
450
     * Get the URL of a particular page within an attempt.
451
     *
452
     * @param int $attemptid the id of an attempt.
453
     * @param int $page optional page number to go to in the attempt.
454
     * @return moodle_url the URL of that attempt.
455
     */
456
    public function attempt_url($attemptid, $page = 0) {
457
        $params = ['attempt' => $attemptid, 'cmid' => $this->get_cmid()];
458
        if ($page) {
459
            $params['page'] = $page;
460
        }
461
        return new moodle_url('/mod/quiz/attempt.php', $params);
462
    }
463
 
464
    /**
465
     * Get the URL to start/continue an attempt.
466
     *
467
     * @param int $page page in the attempt to start on (optional).
468
     * @return moodle_url the URL of this quiz's edit page. Needs to be POSTed to with a cmid parameter.
469
     */
470
    public function start_attempt_url($page = 0) {
471
        $params = ['cmid' => $this->cm->id, 'sesskey' => sesskey()];
472
        if ($page) {
473
            $params['page'] = $page;
474
        }
475
        return new moodle_url('/mod/quiz/startattempt.php', $params);
476
    }
477
 
478
    /**
479
     * Get the URL to review a particular quiz attempt.
480
     *
481
     * @param int $attemptid the id of an attempt.
482
     * @return string the URL of the review of that attempt.
483
     */
484
    public function review_url($attemptid) {
485
        return new moodle_url('/mod/quiz/review.php', ['attempt' => $attemptid, 'cmid' => $this->get_cmid()]);
486
    }
487
 
488
    /**
489
     * Get the URL for the summary page for a particular attempt.
490
     *
491
     * @param int $attemptid the id of an attempt.
492
     * @return string the URL of the review of that attempt.
493
     */
494
    public function summary_url($attemptid) {
495
        return new moodle_url('/mod/quiz/summary.php', ['attempt' => $attemptid, 'cmid' => $this->get_cmid()]);
496
    }
497
 
498
    // Bits of content =========================================================.
499
 
500
    /**
501
     * If $reviewoptions->attempt is false, meaning that students can't review this
502
     * attempt at the moment, return an appropriate string explaining why.
503
     *
504
     * @param int $when One of the display_options::DURING,
505
     *      IMMEDIATELY_AFTER, LATER_WHILE_OPEN or AFTER_CLOSE constants.
506
     * @param bool $short if true, return a shorter string.
507
     * @param int|null $attemptsubmittime time this attempt was submitted. (Optional, but should be given.)
508
     * @return string an appropraite message.
509
     */
510
    public function cannot_review_message($when, $short = false, int $attemptsubmittime = null) {
511
 
512
        if ($attemptsubmittime === null) {
513
            debugging('It is recommended that you pass $attemptsubmittime to cannot_review_message', DEBUG_DEVELOPER);
514
            $attemptsubmittime = time(); // This will be approximately right, which is enough for the one place were it is used.
515
        }
516
 
517
        if ($short) {
518
            $langstrsuffix = 'short';
519
            $dateformat = get_string('strftimedatetimeshort', 'langconfig');
520
        } else {
521
            $langstrsuffix = '';
522
            $dateformat = '';
523
        }
524
 
525
        $reviewfrom = 0;
526
        switch ($when) {
527
            case display_options::DURING:
528
                return '';
529
 
530
            case display_options::IMMEDIATELY_AFTER:
531
                if ($this->quiz->reviewattempt & display_options::LATER_WHILE_OPEN) {
532
                    $reviewfrom = $attemptsubmittime + quiz_attempt::IMMEDIATELY_AFTER_PERIOD;
533
                    break;
534
                }
535
                // Fall through.
536
 
537
            case display_options::LATER_WHILE_OPEN:
538
                if ($this->quiz->timeclose && ($this->quiz->reviewattempt & display_options::AFTER_CLOSE)) {
539
                    $reviewfrom = $this->quiz->timeclose;
540
                    break;
541
                }
542
        }
543
 
544
        if ($reviewfrom) {
545
            return get_string('noreviewuntil' . $langstrsuffix, 'quiz',
546
                    userdate($reviewfrom, $dateformat));
547
        } else {
548
            return get_string('noreview' . $langstrsuffix, 'quiz');
549
        }
550
    }
551
 
552
    /**
553
     * Probably not used any more, but left for backwards compatibility.
554
     *
555
     * @param string $title the name of this particular quiz page.
556
     * @return string always returns ''.
557
     */
558
    public function navigation($title) {
559
        global $PAGE;
560
        $PAGE->navbar->add($title);
561
        return '';
562
    }
563
 
564
    // Private methods =========================================================.
565
 
566
    /**
567
     * Check that the definition of a particular question is loaded, and if not throw an exception.
568
     *
569
     * @param int $id a question id.
570
     */
571
    protected function ensure_question_loaded($id) {
572
        if (isset($this->questions[$id]->_partiallyloaded)) {
573
            throw new moodle_exception('questionnotloaded', 'quiz', $this->view_url(), $id);
574
        }
575
    }
576
 
577
    /**
578
     * Return all the question types used in this quiz.
579
     *
580
     * @param boolean $includepotential if the quiz include random questions,
581
     *      setting this flag to true will make the function to return all the
582
     *      possible question types in the random questions category.
583
     * @return array a sorted array including the different question types.
584
     * @since  Moodle 3.1
585
     */
586
    public function get_all_question_types_used($includepotential = false) {
587
        $questiontypes = [];
588
 
589
        // To control if we need to look in categories for questions.
590
        $qcategories = [];
591
 
592
        foreach ($this->get_questions(null, false) as $questiondata) {
593
            if ($questiondata->status == question_version_status::QUESTION_STATUS_DRAFT) {
594
                // Skip questions where all versions are draft.
595
                continue;
596
            }
597
            if ($questiondata->qtype === 'random' && $includepotential) {
598
                $filtercondition = $questiondata->filtercondition;
599
                if (!empty($filtercondition)) {
600
                    $filter = $filtercondition['filter'];
601
                    if (isset($filter['category'])) {
602
                        foreach ($filter['category']['values'] as $catid) {
603
                            $qcategories[$catid] = $filter['category']['filteroptions']['includesubcategories'];
604
                        }
605
                    }
606
                }
607
            } else {
608
                if (!in_array($questiondata->qtype, $questiontypes)) {
609
                    $questiontypes[] = $questiondata->qtype;
610
                }
611
            }
612
        }
613
 
614
        if (!empty($qcategories)) {
615
            // We have to look for all the question types in these categories.
616
            $categoriestolook = [];
617
            foreach ($qcategories as $cat => $includesubcats) {
618
                if ($includesubcats) {
619
                    $categoriestolook = array_merge($categoriestolook, question_categorylist($cat));
620
                } else {
621
                    $categoriestolook[] = $cat;
622
                }
623
            }
624
            $questiontypesincategories = question_bank::get_all_question_types_in_categories($categoriestolook);
625
            $questiontypes = array_merge($questiontypes, $questiontypesincategories);
626
        }
627
        $questiontypes = array_unique($questiontypes);
628
        sort($questiontypes);
629
 
630
        return $questiontypes;
631
    }
632
 
633
    /**
634
     * Returns an override manager instance with context and quiz loaded.
635
     *
636
     * @return \mod_quiz\local\override_manager
637
     */
638
    public function get_override_manager(): \mod_quiz\local\override_manager {
639
        return new \mod_quiz\local\override_manager(
640
            quiz: $this->quiz,
641
            context: $this->context
642
        );
643
    }
644
}