Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
queue
100.00% covered (success)
100.00%
58 / 58
100.00% covered (success)
100.00%
4 / 4
15
100.00% covered (success)
100.00%
1 / 1
 for_user
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
10
 assign_counts
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 quiz_counts
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 item
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
16
17namespace block_gradingqueue\local;
18
19use moodle_url;
20use stdClass;
21
22/**
23 * Work waiting for grading across every course a user can grade in.
24 *
25 * Assignments: the latest submission is submitted and has no grade, or was changed after it was
26 * graded (the rule mod_assign uses for its own "needs grading" count), from students with an active
27 * enrolment. Quizzes: finished attempts with no total yet, which means a question such as an essay is
28 * waiting for manual grading.
29 *
30 * Counts are per activity. Group restrictions are applied on the grading page the link opens.
31 *
32 * @package    block_gradingqueue
33 * @copyright  2026 DevLMS <contact@devlms.com>
34 * @license    https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
36final class queue {
37    /**
38     * Activities with work waiting, oldest waiting first.
39     *
40     * @param int $userid
41     * @param int $courseid Only this course, or 0 for all.
42     * @return stdClass[] courseid, coursename, cmid, modname, name, url, count, oldest
43     */
44    public static function for_user(int $userid, int $courseid = 0): array {
45        $cms = ['assign' => [], 'quiz' => []];
46        foreach (enrol_get_users_courses($userid, true) as $course) {
47            if ($courseid && (int) $course->id !== $courseid) {
48                continue;
49            }
50            $modinfo = get_fast_modinfo($course, $userid);
51            foreach (['assign' => 'mod/assign:grade', 'quiz' => 'mod/quiz:grade'] as $modname => $capability) {
52                foreach ($modinfo->get_instances_of($modname) as $cm) {
53                    if ($cm->uservisible && has_capability($capability, $cm->context, $userid)) {
54                        $cms[$modname][$cm->instance] = $cm;
55                    }
56                }
57            }
58        }
59
60        $items = [];
61        foreach (self::assign_counts(array_keys($cms['assign'])) as $row) {
62            $cm = $cms['assign'][$row->instance];
63            $items[] = self::item(
64                $cm,
65                (int) $row->total,
66                (int) $row->oldest,
67                new moodle_url('/mod/assign/view.php', ['id' => $cm->id, 'action' => 'grading'])
68            );
69        }
70        foreach (self::quiz_counts(array_keys($cms['quiz'])) as $row) {
71            $cm = $cms['quiz'][$row->instance];
72            $items[] = self::item(
73                $cm,
74                (int) $row->total,
75                (int) $row->oldest,
76                new moodle_url('/mod/quiz/report.php', ['id' => $cm->id, 'mode' => 'grading'])
77            );
78        }
79        usort($items, fn($a, $b) => [$a->oldest, $a->cmid] <=> [$b->oldest, $b->cmid]);
80        return $items;
81    }
82
83    /**
84     * Submissions waiting per assignment.
85     *
86     * @param int[] $ids Assignment ids.
87     * @return stdClass[] instance, total, oldest
88     */
89    private static function assign_counts(array $ids): array {
90        global $DB;
91        if (!$ids) {
92            return [];
93        }
94        [$insql, $params] = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
95        $params += ['submitted' => 'submitted', 'active' => ENROL_USER_ACTIVE, 'enabled' => ENROL_INSTANCE_ENABLED];
96        return array_values($DB->get_records_sql(
97            "SELECT s.assignment AS instance, COUNT(s.id) AS total, MIN(s.timemodified) AS oldest
98               FROM {assign_submission} s
99               JOIN {assign} a ON a.id = s.assignment
100          LEFT JOIN {assign_grades} g ON g.assignment = s.assignment AND g.userid = s.userid
101                                     AND g.attemptnumber = s.attemptnumber
102              WHERE s.assignment $insql
103                AND s.latest = 1
104                AND s.userid > 0
105                AND s.status = :submitted
106                AND s.timemodified IS NOT NULL
107                AND (g.id IS NULL OR g.grade IS NULL OR s.timemodified >= g.timemodified OR (a.grade < 0 AND g.grade = -1))
108                AND EXISTS (SELECT 1
109                              FROM {user_enrolments} ue
110                              JOIN {enrol} e ON e.id = ue.enrolid
111                             WHERE e.courseid = a.course AND ue.userid = s.userid
112                               AND ue.status = :active AND e.status = :enabled)
113           GROUP BY s.assignment",
114            $params
115        ));
116    }
117
118    /**
119     * Finished attempts with no total yet, per quiz.
120     *
121     * @param int[] $ids Quiz ids.
122     * @return stdClass[] instance, total, oldest
123     */
124    private static function quiz_counts(array $ids): array {
125        global $DB;
126        if (!$ids) {
127            return [];
128        }
129        [$insql, $params] = $DB->get_in_or_equal($ids, SQL_PARAMS_NAMED);
130        $params['finished'] = 'finished';
131        return array_values($DB->get_records_sql(
132            "SELECT qa.quiz AS instance, COUNT(qa.id) AS total, MIN(qa.timefinish) AS oldest
133               FROM {quiz_attempts} qa
134              WHERE qa.quiz $insql
135                AND qa.state = :finished
136                AND qa.preview = 0
137                AND qa.sumgrades IS NULL
138           GROUP BY qa.quiz",
139            $params
140        ));
141    }
142
143    /**
144     * One row of the queue.
145     *
146     * @param \cm_info $cm
147     * @param int $count
148     * @param int $oldest
149     * @param moodle_url $url
150     * @return stdClass
151     */
152    private static function item(\cm_info $cm, int $count, int $oldest, moodle_url $url): stdClass {
153        return (object) [
154            'courseid' => (int) $cm->course,
155            'coursename' => format_string($cm->get_course()->fullname, true, ['context' => $cm->context->get_course_context()]),
156            'cmid' => (int) $cm->id,
157            'modname' => $cm->modname,
158            'name' => $cm->get_formatted_name(),
159            'url' => $url->out(false),
160            'count' => $count,
161            'oldest' => $oldest,
162        ];
163    }
164}