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
/**
18
 * Completion Progress block.
19
 *
20
 * @package    block_completion_progress
21
 * @copyright  2016 Michael de Raadt
22
 * @copyright  2021 Jonathon Fowler <fowlerj@usq.edu.au>
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
 
26
namespace block_completion_progress;
27
 
28
use stdClass;
29
use completion_info;
30
use context_course;
31
use coding_exception;
32
 
33
/**
34
 * Completion Progress.
35
 *
36
 * @package    block_completion_progress
37
 * @copyright  2016 Michael de Raadt
38
 * @copyright  2021 Jonathon Fowler <fowlerj@usq.edu.au>
39
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40
 */
41
class completion_progress implements \renderable {
42
    /**
43
     * Sort activities by course order.
44
     */
45
    const ORDERBY_COURSE = 'orderbycourse';
46
 
47
    /**
48
     * Sort activities by expected time order.
49
     */
50
    const ORDERBY_TIME = 'orderbytime';
51
 
52
    /**
53
     * The course.
54
     * @var object
55
     */
56
    protected $course;
57
 
58
    /**
59
     * The course context.
60
     * @var context_course
61
     */
62
    protected $context;
63
 
64
    /**
65
     * Completion info for the course.
66
     * @var completion_info
67
     */
68
    protected $completioninfo;
69
 
70
    /**
71
     * The user.
72
     * @var object
73
     */
74
    protected $user;
75
 
76
    /**
77
     * Block instance record.
78
     * @var stdClass
79
     */
80
    protected $blockinstance;
81
 
82
    /**
83
     * Block instance config.
84
     * @var stdClass
85
     */
86
    protected $blockconfig;
87
 
88
    /**
89
     * List of activities.
90
     * @var array cmid => obj
91
     */
92
    protected $activities = null;
93
 
94
    /**
95
     * List of visible activities.
96
     * @var array cmid => obj
97
     */
98
    protected $visibleactivities = null;
99
 
100
    /**
101
     * List of grade exclusions.
102
     * @var array of: [module-instance-userid, ...]
103
     */
104
    protected $exclusions = null;
105
 
106
    /**
107
     * List of submissions.
108
     * @var array of arrays: userid => [cmid => obj]
109
     */
110
    protected $submissions = null;
111
 
112
    /**
113
     * List of computed completions.
114
     * @var array of arrays: userid => [cmid => state]
115
     */
116
    protected $completions = null;
117
 
118
    /**
119
     * Whether exclusions have been loaded for all course users already.
120
     * @var boolean
121
     */
122
    protected $exclusionsforall = false;
123
 
124
    /**
125
     * Whether submissions have been loaded for all course users already.
126
     * @var boolean
127
     */
128
    protected $submissionsforall = false;
129
 
130
    /**
131
     * Whether completions have been loaded for all course users already.
132
     * @var boolean
133
     */
134
    protected $completionsforall = false;
135
 
136
    /**
137
     * Simple bar mode (for overview).
138
     * @var boolean
139
     */
140
    protected $simplebar = false;
141
 
142
    /**
143
     * Constructor.
144
     * @param object|int $courseorid
145
     */
146
    public function __construct($courseorid) {
147
        global $CFG;
148
 
149
        require_once($CFG->libdir.'/completionlib.php');
150
 
151
        if (is_object($courseorid)) {
152
            $this->course = $courseorid;
153
        } else {
154
            $this->course = get_course($courseorid);
155
        }
156
        $this->context = context_course::instance($this->course->id);
157
        $this->completioninfo = new completion_info($this->course);
158
    }
159
 
160
    /**
161
     * Specialise for a specific user.
162
     * @param stdClass $user containing minimum of core_user\fields::for_name()
163
     * @return self
164
     */
165
    public function for_user(stdClass $user): self {
166
        $this->user = $user;
167
 
168
        $this->load_exclusions();
169
        $this->load_submissions();
170
        $this->load_completions();
171
        $this->filter_visible_activities();
172
 
173
        return $this;
174
    }
175
 
176
    /**
177
     * Specialise for overview page use.
178
     * @return self
179
     */
180
    public function for_overview() {
181
        if ($this->user) {
182
            throw new coding_exception('cannot re-specialise for overview');
183
        }
184
        $this->user = null;
185
        $this->simplebar = true;
186
 
187
        $this->load_exclusions();
188
        $this->load_submissions();
189
        $this->load_completions();
190
 
191
        return $this;
192
    }
193
 
194
    /**
195
     * Specialise for a particular block instance.
196
     * @param stdClass $instance Instance record.
197
     * @param boolean $selectedonly Whether to filter by configured selected items.
198
     * @return self
199
     */
200
    public function for_block_instance(stdClass $instance, $selectedonly = true): self {
201
        if ($this->blockinstance) {
202
            throw new coding_exception('cannot re-specialise for a different block instance');
203
        }
204
        $this->blockinstance = $instance;
205
        $this->blockconfig = (object)(array)unserialize(base64_decode($instance->configdata ?? ''));
206
 
207
        $this->load_activities($selectedonly);
208
        $this->filter_visible_activities();
209
 
210
        return $this;
211
    }
212
 
213
    /**
214
     * Return the course object.
215
     * @return object
216
     */
217
    public function get_course(): stdClass {
218
        return $this->course;
219
    }
220
 
221
    /**
222
     * Return the course object.
223
     * @return context_course
224
     */
225
    public function get_context(): context_course {
226
        return $this->context;
227
    }
228
 
229
    /**
230
     * Return the completion info object.
231
     * @return completion_info
232
     */
233
    public function get_completion_info(): completion_info {
234
        return $this->completioninfo;
235
    }
236
 
237
    /**
238
     * Return the user.
239
     * @return stdClass
240
     */
241
    public function get_user(): ?stdClass {
242
        return $this->user;
243
    }
244
 
245
    /**
246
     * Return the simple bar mode.
247
     * @return boolean
248
     */
249
    public function is_simple_bar(): bool {
250
        return $this->simplebar;
251
    }
252
 
253
    /**
254
     * Check whether any activities are available.
255
     * @return boolean
256
     */
257
    public function has_activities(): bool {
258
        if ($this->activities === null) {
259
            throw new coding_exception('activities not loaded until for_block_instance() is called');
260
        }
261
        return !empty($this->activities);
262
    }
263
 
264
    /**
265
     * Return the activities in presentation order.
266
     * @param string|null $orderoverride
267
     * @return array
268
     */
269
    public function get_activities($orderoverride = null): array {
270
        if ($this->activities === null) {
271
            throw new coding_exception('activities not loaded until for_block_instance() is called');
272
        }
273
        $order = $orderoverride ?? $this->blockconfig->orderby ?? self::ORDERBY_COURSE;
274
        usort($this->activities, [$this, 'sorter_' . $order]);
275
        return $this->activities;
276
    }
277
 
278
    /**
279
     * Check whether any visible activities are available.
280
     * @return boolean
281
     */
282
    public function has_visible_activities(): bool {
283
        if ($this->visibleactivities === null) {
284
            throw new coding_exception('visible activities not computed until for_block_instance() is called');
285
        }
286
        return !empty($this->visibleactivities);
287
    }
288
 
289
    /**
290
     * Return the activities visible to the user in presentation order.
291
     * @param string|null $orderoverride
292
     * @return array
293
     */
294
    public function get_visible_activities($orderoverride = null): array {
295
        if ($this->visibleactivities === null) {
296
            throw new coding_exception('visible activities not computed until for_block_instance() is called');
297
        }
298
        $order = $orderoverride ?? $this->blockconfig->orderby ?? self::ORDERBY_COURSE;
299
        usort($this->visibleactivities, [$this, 'sorter_' . $order]);
300
        return $this->visibleactivities;
301
    }
302
 
303
    /**
304
     * Return the exclusions.
305
     * @return array of modname-modinstance-userid formatted items
306
     */
307
    public function get_exclusions(): array {
308
        return $this->exclusions;
309
    }
310
 
311
    /**
312
     * Get block instance.
313
     * @return stdClass|null
314
     */
315
    public function get_block_instance(): ?stdClass {
316
        return $this->blockinstance;
317
    }
318
 
319
    /**
320
     * Get block configuration.
321
     * @return stdClass
322
     */
323
    public function get_block_config(): stdClass {
324
        return $this->blockconfig;
325
    }
326
 
327
    /**
328
     * Get user activity submissions.
329
     * @return array cmid => info
330
     */
331
    public function get_submissions(): array {
332
        if ($this->submissions === null) {
333
            throw new coding_exception('submissions not computed until for_user() or for_overview() is called');
334
        }
335
        if ($this->user) {
336
            return $this->submissions[$this->user->id] ?? [];
337
        } else {
338
            throw new coding_exception('unimplemented');
339
        }
340
    }
341
 
342
    /**
343
     * Get user activity completion states.
344
     * @return array cmid => status
345
     */
346
    public function get_completions() {
347
        if ($this->completions === null) {
348
            throw new coding_exception('completions not computed until for_user() or for_overview() is called');
349
        }
350
        if ($this->user) {
351
            // Filter to visible activities and fill in gaps.
352
            $completions = $this->completions[$this->user->id] ?? [];
353
            $ret = [];
354
            foreach ($this->visibleactivities as $activity) {
355
                $ret[$activity->id] = $completions[$activity->id] ?? COMPLETION_INCOMPLETE;
356
            }
357
            return $ret;
358
        } else {
359
            throw new coding_exception('unimplemented');
360
        }
361
    }
362
 
363
    /**
364
     * Calculates an overall percentage of progress.
365
     * @return integer  Progress value as a percentage
366
     */
367
    public function get_percentage(): ?int {
368
        $completions = $this->get_completions();
369
        if (count($completions) == 0) {
370
            return null;
371
        }
372
 
373
        $completecount = 0;
374
        foreach ($completions as $complete) {
375
            if ($complete == COMPLETION_COMPLETE || $complete == COMPLETION_COMPLETE_PASS) {
376
                $completecount++;
377
            }
378
        }
379
 
380
        return (int)round(100 * $completecount / count($this->visibleactivities));
381
    }
382
 
383
    /**
384
     * Used to compare two activity entries based on order on course page.
385
     *
386
     * @param array $a
387
     * @param array $b
388
     * @return integer
389
     */
390
    private function sorter_orderbycourse($a, $b): int {
391
        if ($a->section != $b->section) {
392
            return $a->section <=> $b->section;
393
        } else {
394
            return $a->position <=> $b->position;
395
        }
396
    }
397
 
398
    /**
399
     * Used to compare two activity entries based their expected completion times
400
     *
401
     * @param array $a
402
     * @param array $b
403
     * @return integer
404
     */
405
    private function sorter_orderbytime($a, $b): int {
406
        if ($a->expected != 0 && $b->expected != 0 && $a->expected != $b->expected) {
407
            return $a->expected <=> $b->expected;
408
        } else if ($a->expected != 0 && $b->expected == 0) {
409
            return -1;
410
        } else if ($a->expected == 0 && $b->expected != 0) {
411
            return 1;
412
        } else {
413
            return $this->sorter_orderbycourse($a, $b);
414
        }
415
    }
416
 
417
 
418
    /**
419
     * Loads activities with completion set in current course.
420
     *
421
     * @param boolean $selectedonly Whether to filter by configured selected items.
422
     * @return array
423
     */
424
    protected function load_activities($selectedonly) {
425
        $modinfo = get_fast_modinfo($this->course, -1);
426
        $sections = $modinfo->get_sections();
427
        $selectedonly = $selectedonly && ($this->blockconfig->activitiesincluded ?? '') === 'selectedactivities';
428
        $selectedcms = $this->blockconfig->selectactivities ?? [];
429
 
430
        $this->activities = [];
431
 
432
        foreach ($modinfo->instances as $module => $cms) {
433
            $modulename = get_string('pluginname', $module);
434
            foreach ($cms as $cm) {
435
                if ($cm->completion == COMPLETION_TRACKING_NONE) {
436
                    continue;
437
                }
438
                if ($selectedonly && !in_array($module.'-'.$cm->instance, $selectedcms)) {
439
                    continue;
440
                }
441
 
442
                $this->activities[$cm->id] = (object)[
443
                    'type'       => $module,
444
                    'modulename' => $modulename,
445
                    'id'         => $cm->id,
446
                    'instance'   => $cm->instance,
447
                    'name'       => $cm->get_formatted_name(),
448
                    'expected'   => $cm->completionexpected,
449
                    'section'    => $cm->sectionnum,
450
                    'position'   => array_search($cm->id, $sections[$cm->sectionnum]),
451
                    'url'        => $cm->url instanceof \moodle_url ? $cm->url->out() : '',
452
                    'onclick'    => $cm->onclick,
453
                    'context'    => $cm->context,
454
                    'icon'       => $cm->get_icon_url(),
455
                    'available'  => $cm->available,
456
                ];
457
            }
458
        }
459
    }
460
 
461
    /**
462
     * Filter down the activities to those a user can see.
463
     */
464
    protected function filter_visible_activities() {
465
        global $CFG, $USER;
466
 
467
        if (!$this->user || $this->activities === null) {
468
            return;
469
        }
470
 
471
        $this->visibleactivities = [];
472
        $modinfo = get_fast_modinfo($this->course, $this->user->id);
473
        $canviewhidden = has_capability('moodle/course:viewhiddenactivities', $this->context, $this->user);
474
 
475
        // Keep only activities that are visible.
476
        foreach ($this->activities as $key => $activity) {
477
            $cm = $modinfo->cms[$activity->id];
478
 
479
            // Check visibility in course.
480
            if (!$cm->visible && !$canviewhidden) {
481
                continue;
482
            }
483
 
484
            // Check availability, allowing for visible, but not accessible items.
485
            if (!empty($CFG->enableavailability)) {
486
                if ($canviewhidden) {
487
                    $activity->available = true;
488
                } else {
489
                    if (isset($cm->available) && !$cm->available && empty($cm->availableinfo)) {
490
                        continue;
491
                    }
492
                    $activity->available = $cm->available;
493
                }
494
            }
495
 
496
            // Check for exclusions.
497
            if (in_array($activity->type.'-'.$activity->instance.'-'.$this->user->id, $this->exclusions)) {
498
                continue;
499
            }
500
 
501
            // Save the visible event.
502
            $this->visibleactivities[$key] = $activity;
503
        }
504
    }
505
 
506
    /**
507
     * Finds gradebook exclusions for students in the course.
508
     */
509
    protected function load_exclusions() {
510
        global $DB;
511
 
512
        if ($this->exclusionsforall) {
513
            // Already loaded.
514
            return;
515
        }
516
 
517
        $query = "SELECT g.id, i.itemmodule, i.iteminstance, g.userid
518
                   FROM {grade_grades} g, {grade_items} i
519
                  WHERE i.courseid = :courseid
520
                    AND i.id = g.itemid
521
                    AND g.excluded <> 0";
522
        $params = ['courseid' => $this->course->id];
523
        if ($this->user) {
524
            $query .= " AND g.userid = :userid";
525
            $params['userid'] = $this->user->id;
526
        } else {
527
            // Avoid refetching this info if specialising for user later.
528
            $this->exclusionsforall = true;
529
        }
530
 
531
        $this->exclusions = [];
532
        foreach ($DB->get_records_sql($query, $params) as $rec) {
533
            $this->exclusions[] = $rec->itemmodule . '-' . $rec->iteminstance . '-' . $rec->userid;
534
        }
535
    }
536
 
537
    /**
538
     * Loads completion information for enrolled users in the course.
539
     */
540
    protected function load_completions() {
541
        global $DB;
542
 
543
        if ($this->completionsforall) {
544
            // Already loaded.
545
            return;
546
        }
547
 
548
        // Somewhat faster than lots of calls to completion_info::get_data($cm, true, $userid)
549
        // where its cache can't be used because the userid is different.
550
        $enrolsql = get_enrolled_join($this->context, 'u.id', false);
551
        $query = "SELECT DISTINCT " . $DB->sql_concat('cm.id', "'-'", 'u.id') . " AS id,
552
                        u.id AS userid, cm.id AS cmid,
553
                        COALESCE(cmc.completionstate, :incomplete) AS completionstate
554
                    FROM {user} u {$enrolsql->joins}
555
              CROSS JOIN {course_modules} cm
556
               LEFT JOIN {course_modules_completion} cmc ON cmc.coursemoduleid = cm.id AND cmc.userid = u.id
557
                   WHERE {$enrolsql->wheres}
558
                     AND cm.course = :courseid
559
                     AND cm.completion <> :none";
560
        $params = $enrolsql->params + [
561
            'courseid' => $this->course->id,
562
            'incomplete' => COMPLETION_INCOMPLETE,
563
            'none' => COMPLETION_TRACKING_NONE,
564
        ];
565
        if ($this->user) {
566
            $query .= " AND u.id = :userid";
567
            $params['userid'] = $this->user->id;
568
        } else {
569
            // Avoid refetching this info if specialising for user later.
570
            $this->completionsforall = true;
571
        }
572
 
573
        $rset = $DB->get_recordset_sql($query, $params);
574
        $this->completions = [];
575
        foreach ($rset as $compl) {
576
            $submission = $this->submissions[$compl->userid][$compl->cmid] ?? null;
577
 
578
            if ($compl->completionstate == COMPLETION_INCOMPLETE && $submission) {
579
                $this->completions[$compl->userid][$compl->cmid] = 'submitted';
580
            } else if ($compl->completionstate == COMPLETION_COMPLETE_FAIL && $submission
581
                    && !$submission->graded) {
582
                $this->completions[$compl->userid][$compl->cmid] = 'submitted';
583
            } else {
584
                $this->completions[$compl->userid][$compl->cmid] = $compl->completionstate;
585
            }
586
        }
587
        $rset->close();
588
    }
589
 
590
    /**
591
     * Find submissions for students in the course.
592
     */
593
    protected function load_submissions() {
594
        global $DB, $CFG;
595
 
596
        if ($this->submissionsforall) {
597
            // Already loaded.
598
            return;
599
        }
600
 
601
        require_once($CFG->dirroot . '/mod/quiz/lib.php');
602
 
603
        $params = [
604
            'courseid' => $this->course->id,
605
        ];
606
 
607
        if ($this->user) {
608
            $assignwhere = 'AND s.userid = :userid';
609
            $workshopwhere = 'AND s.authorid = :userid';
610
            $quizwhere = 'AND qa.userid = :userid';
611
 
612
            $params += [
613
              'userid' => $this->user->id,
614
            ];
615
        } else {
616
            $assignwhere = '';
617
            $workshopwhere = '';
618
            $quizwhere = '';
619
 
620
            // Avoid refetching this info if specialising for user later.
621
            $this->submissionsforall = true;
622
        }
623
 
624
        // Queries to deliver instance IDs of activities with submissions by user.
625
        $queries = array (
626
            [
627
                // Assignments with individual submission, or groups requiring a submission per user,
628
                // or ungrouped users in a group submission situation.
629
                'module' => 'assign',
630
                'query' => "SELECT ". $DB->sql_concat('s.userid', "'-'", 'c.id') ." AS id,
631
                             s.userid, c.id AS cmid,
632
                             MAX(CASE WHEN ag.grade IS NULL OR ag.grade = -1 THEN 0 ELSE 1 END) AS graded
633
                          FROM {assign_submission} s
634
                            INNER JOIN {assign} a ON s.assignment = a.id
635
                            INNER JOIN {course_modules} c ON c.instance = a.id
636
                            INNER JOIN {modules} m ON m.name = 'assign' AND m.id = c.module
637
                            LEFT JOIN {assign_grades} ag ON ag.assignment = s.assignment
638
                                  AND ag.attemptnumber = s.attemptnumber
639
                                  AND ag.userid = s.userid
640
                          WHERE s.latest = 1
641
                            AND s.status = 'submitted'
642
                            AND a.course = :courseid
643
                            AND (
644
                                a.teamsubmission = 0 OR
645
                                (a.teamsubmission <> 0 AND a.requireallteammemberssubmit <> 0 AND s.groupid = 0) OR
646
                                (a.teamsubmission <> 0 AND a.preventsubmissionnotingroup = 0 AND s.groupid = 0)
647
                            )
648
                            $assignwhere
649
                        GROUP BY s.userid, c.id",
650
                'params' => [ ],
651
            ],
652
 
653
            [
654
                // Assignments with groups requiring only one submission per group.
655
                'module' => 'assign',
656
                'query' => "SELECT ". $DB->sql_concat('s.userid', "'-'", 'c.id') ." AS id,
657
                             s.userid, c.id AS cmid,
658
                             MAX(CASE WHEN ag.grade IS NULL OR ag.grade = -1 THEN 0 ELSE 1 END) AS graded
659
                          FROM {assign_submission} gs
660
                            INNER JOIN {assign} a ON gs.assignment = a.id
661
                            INNER JOIN {course_modules} c ON c.instance = a.id
662
                            INNER JOIN {modules} m ON m.name = 'assign' AND m.id = c.module
663
                            INNER JOIN {groups_members} s ON s.groupid = gs.groupid
664
                            LEFT JOIN {assign_grades} ag ON ag.assignment = gs.assignment
665
                                  AND ag.attemptnumber = gs.attemptnumber
666
                                  AND ag.userid = s.userid
667
                          WHERE gs.latest = 1
668
                            AND gs.status = 'submitted'
669
                            AND gs.userid = 0
670
                            AND a.course = :courseid
671
                            AND (a.teamsubmission <> 0 AND a.requireallteammemberssubmit = 0)
672
                            $assignwhere
673
                        GROUP BY s.userid, c.id",
674
                'params' => [ ],
675
            ],
676
 
677
            [
678
                'module' => 'workshop',
679
                'query' => "SELECT ". $DB->sql_concat('s.authorid', "'-'", 'c.id') ." AS id,
680
                               s.authorid AS userid, c.id AS cmid,
681
                               1 AS graded
682
                             FROM {workshop_submissions} s, {workshop} w, {modules} m, {course_modules} c
683
                            WHERE s.workshopid = w.id
684
                              AND w.course = :courseid
685
                              AND m.name = 'workshop'
686
                              AND m.id = c.module
687
                              AND c.instance = w.id
688
                              $workshopwhere
689
                          GROUP BY s.authorid, c.id",
690
                'params' => [ ],
691
            ],
692
 
693
            [
694
                // Quizzes with 'first' and 'last attempt' grading methods.
695
                'module' => 'quiz',
696
                'query' => "SELECT ". $DB->sql_concat('qa.userid', "'-'", 'c.id') ." AS id,
697
                           qa.userid, c.id AS cmid,
698
                           (CASE WHEN qa.sumgrades IS NULL THEN 0 ELSE 1 END) AS graded
699
                         FROM {quiz_attempts} qa
700
                           INNER JOIN {quiz} q ON q.id = qa.quiz
701
                           INNER JOIN {course_modules} c ON c.instance = q.id
702
                           INNER JOIN {modules} m ON m.name = 'quiz' AND m.id = c.module
703
                        WHERE qa.state = 'finished'
704
                          AND q.course = :courseid
705
                          AND qa.attempt = (
706
                            SELECT CASE WHEN q.grademethod = :gmfirst THEN MIN(qa1.attempt)
707
                                        WHEN q.grademethod = :gmlast THEN MAX(qa1.attempt) END
708
                            FROM {quiz_attempts} qa1
709
                            WHERE qa1.quiz = qa.quiz
710
                              AND qa1.userid = qa.userid
711
                              AND qa1.state = 'finished'
712
                          )
713
                          $quizwhere",
714
                'params' => [
715
                    'gmfirst' => QUIZ_ATTEMPTFIRST,
716
                    'gmlast' => QUIZ_ATTEMPTLAST,
717
                ],
718
            ],
719
            [
720
                // Quizzes with 'maximum' and 'average' grading methods.
721
                'module' => 'quiz',
722
                'query' => "SELECT ". $DB->sql_concat('qa.userid', "'-'", 'c.id') ." AS id,
723
                           qa.userid, c.id AS cmid,
724
                           MIN(CASE WHEN qa.sumgrades IS NULL THEN 0 ELSE 1 END) AS graded
725
                         FROM {quiz_attempts} qa
726
                           INNER JOIN {quiz} q ON q.id = qa.quiz
727
                           INNER JOIN {course_modules} c ON c.instance = q.id
728
                           INNER JOIN {modules} m ON m.name = 'quiz' AND m.id = c.module
729
                        WHERE (q.grademethod = :gmmax OR q.grademethod = :gmavg)
730
                          AND qa.state = 'finished'
731
                          AND q.course = :courseid
732
                          $quizwhere
733
                       GROUP BY qa.userid, c.id",
734
                'params' => [
735
                    'gmmax' => QUIZ_GRADEHIGHEST,
736
                    'gmavg' => QUIZ_GRADEAVERAGE,
737
                ],
738
            ],
739
        );
740
 
741
        $this->submissions = [];
742
        foreach ($queries as $spec) {
743
            $results = $DB->get_records_sql($spec['query'], $params + $spec['params']);
744
            foreach ($results as $obj) {
745
                unset($obj->id);
746
                $this->submissions[$obj->userid][$obj->cmid] = $obj;
747
            }
748
        }
749
    }
750
 
751
}