MDL-68612 user: Participants filter row accessibility improvements
[moodle.git] / user / classes / output / participants_filter.php
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/>.
17 /**
18  * Class for rendering user filters on the course participants page.
19  *
20  * @package    core_user
21  * @copyright  2020 Michael Hawkins <michaelh@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 namespace core_user\output;
26 use context_course;
27 use renderable;
28 use renderer_base;
29 use stdClass;
30 use templatable;
32 /**
33  * Class for rendering user filters on the course participants page.
34  *
35  * @copyright  2020 Michael Hawkins <michaelh@moodle.com>
36  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37  */
38 class participants_filter implements renderable, templatable {
40     /** @var context_course $context The context where the filters are being rendered. */
41     protected $context;
43     /** @var string $tableregionid The table to be updated by this filter */
44     protected $tableregionid;
46     /** @var stdClass $course The course shown */
47     protected $course;
49     /**
50      * Participants filter constructor.
51      *
52      * @param context_course $context The context where the filters are being rendered.
53      * @param string $tableregionid The table to be updated by this filter
54      */
55     public function __construct(context_course $context, string $tableregionid) {
56         $this->context = $context;
57         $this->tableregionid = $tableregionid;
59         $this->course = get_course($context->instanceid);
60     }
62     /**
63      * Get data for all filter types.
64      *
65      * @return array
66      */
67     protected function get_filtertypes(): array {
68         $filtertypes = [];
70         $filtertypes[] = $this->get_keyword_filter();
72         if ($filtertype = $this->get_enrolmentstatus_filter()) {
73             $filtertypes[] = $filtertype;
74         }
76         if ($filtertype = $this->get_roles_filter()) {
77             $filtertypes[] = $filtertype;
78         }
80         if ($filtertype = $this->get_enrolments_filter()) {
81             $filtertypes[] = $filtertype;
82         }
84         if ($filtertype = $this->get_groups_filter()) {
85             $filtertypes[] = $filtertype;
86         }
88         if ($filtertype = $this->get_accesssince_filter()) {
89             $filtertypes[] = $filtertype;
90         }
92         return $filtertypes;
93     }
95     /**
96      * Get data for the enrolment status filter.
97      *
98      * @return stdClass|null
99      */
100     protected function get_enrolmentstatus_filter(): ?stdClass {
101         if (!has_capability('moodle/course:enrolreview', $this->context)) {
102             return null;
103         }
105         return $this->get_filter_object(
106             'status',
107             get_string('participationstatus', 'core_enrol'),
108             false,
109             true,
110             null,
111             [
112                 (object) [
113                     'value' => ENROL_USER_ACTIVE,
114                     'title' => get_string('active'),
115                 ],
116                 (object) [
117                     'value' => ENROL_USER_SUSPENDED,
118                     'title'  => get_string('inactive'),
119                 ],
120             ]
121         );
122     }
124     /**
125      * Get data for the roles filter.
126      *
127      * @return stdClass|null
128      */
129     protected function get_roles_filter(): ?stdClass {
130         $roles = [];
131         $roles += [-1 => get_string('noroles', 'role')];
132         $roles += get_viewable_roles($this->context);
134         if (has_capability('moodle/role:assign', $this->context)) {
135             $roles += get_assignable_roles($this->context, ROLENAME_ALIAS);
136         }
138         return $this->get_filter_object(
139             'roles',
140             get_string('roles', 'core_role'),
141             false,
142             true,
143             null,
144             array_map(function($id, $title) {
145                 return (object) [
146                     'value' => $id,
147                     'title' => $title,
148                 ];
149             }, array_keys($roles), array_values($roles))
150         );
151     }
153     /**
154      * Get data for the roles filter.
155      *
156      * @return stdClass|null
157      */
158     protected function get_enrolments_filter(): ?stdClass {
159         if (!has_capability('moodle/course:enrolreview', $this->context)) {
160             return null;
161         }
163         if ($this->course->id == SITEID) {
164             // No enrolment methods for the site.
165             return null;
166         }
168         $instances = enrol_get_instances($this->course->id, true);
169         $plugins = enrol_get_plugins(false);
171         return $this->get_filter_object(
172             'enrolments',
173             get_string('enrolmentinstances', 'core_enrol'),
174             false,
175             true,
176             null,
177             array_filter(array_map(function($instance) use ($plugins): ?stdClass {
178                 if (!array_key_exists($instance->enrol, $plugins)) {
179                     return null;
180                 }
182                 return (object) [
183                     'value' => $instance->id,
184                     'title' => $plugins[$instance->enrol]->get_instance_name($instance),
185                 ];
186             }, array_values($instances)))
187         );
188     }
190     /**
191      * Get data for the groups filter.
192      *
193      * @return stdClass|null
194      */
195     protected function get_groups_filter(): ?stdClass {
196         global $USER;
198         // Filter options for groups, if available.
199         $seeallgroups = has_capability('moodle/site:accessallgroups', $this->context);
200         $seeallgroups = $seeallgroups || ($this->course->groupmode != SEPARATEGROUPS);
201         if ($seeallgroups) {
202             $groups = [];
203             $groups += [USERSWITHOUTGROUP => (object) [
204                     'id' => USERSWITHOUTGROUP,
205                     'name' => get_string('nogroup', 'group'),
206                 ]];
207             $groups += groups_get_all_groups($this->course->id);
208         } else {
209             // Otherwise, just list the groups the user belongs to.
210             $groups = groups_get_all_groups($this->course->id, $USER->id);
211         }
213         // Return no data if no groups found (which includes if the only value is 'No group').
214         if (empty($groups) || (count($groups) === 1 && array_key_exists(-1, $groups))) {
215             return null;
216         }
218         return $this->get_filter_object(
219             'groups',
220             get_string('groups', 'core_group'),
221             false,
222             true,
223             null,
224             array_map(function($group) {
225                 return (object) [
226                     'value' => $group->id,
227                     'title' => $group->name,
228                 ];
229             }, array_values($groups))
230         );
231     }
233     /**
234      * Get data for the accesssince filter.
235      *
236      * @return stdClass|null
237      */
238     protected function get_accesssince_filter(): ?stdClass {
239         global $CFG, $DB;
241         $hiddenfields = [];
242         if (!has_capability('moodle/course:viewhiddenuserfields', $this->context)) {
243             $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
244         }
246         if (array_key_exists('lastaccess', $hiddenfields)) {
247             return null;
248         }
250         // Get minimum lastaccess for this course and display a dropbox to filter by lastaccess going back this far.
251         // We need to make it diferently for normal courses and site course.
252         if (!$this->course->id == SITEID) {
253             // Regular course.
254             $params = [
255                 'courseid' => $this->course->id,
256                 'timeaccess' => 0,
257             ];
258             $select = 'courseid = :courseid AND timeaccess != :timeaccess';
259             $minlastaccess = $DB->get_field_select('user_lastaccess', 'MIN(timeaccess)', $select, $params);
260             $lastaccess0exists = $DB->record_exists('user_lastaccess', $params);
261         } else {
262             // Front page.
263             $params = ['lastaccess' => 0];
264             $select = 'lastaccess != :lastaccess';
265             $minlastaccess = $DB->get_field_select('user', 'MIN(lastaccess)', $select, $params);
266             $lastaccess0exists = $DB->record_exists('user', $params);
267         }
269         $now = usergetmidnight(time());
270         $timeoptions = [];
271         $criteria = get_string('usersnoaccesssince');
273         $getoptions = function(int $count, string $singletype, string $type) use ($now, $minlastaccess): array {
274             $values = [];
275             for ($i = 1; $i <= $count; $i++) {
276                 $timestamp = strtotime("-{$i} {$type}", $now);
277                 if ($timestamp < $minlastaccess) {
278                     break;
279                 }
281                 if ($i === 1) {
282                     $title = get_string("num{$singletype}", 'moodle', $i);
283                 } else {
284                     $title = get_string("num{$type}", 'moodle', $i);
285                 }
287                 $values[] = [
288                     'value' => $timestamp,
289                     'title' => $title,
290                 ];
291             }
293             return $values;
294         };
296         $values = array_merge(
297             $getoptions(6, 'day', 'days'),
298             $getoptions(10, 'week', 'weeks'),
299             $getoptions(11, 'month', 'months'),
300             $getoptions(1, 'year', 'years')
301         );
303         if ($lastaccess0exists) {
304             $values[] = [
305                 'value' => time(),
306                 'title' => get_string('never', 'moodle'),
307             ];
308         }
310         if (count($values) <= 1) {
311             // Nothing to show.
312             return null;
313         }
315         return $this->get_filter_object(
316             'accesssince',
317             get_string('usersnoaccesssince'),
318             false,
319             false,
320             null,
321             $values
322         );
323     }
325     /**
326      * Get data for the keywords filter.
327      *
328      * @return stdClass|null
329      */
330     protected function get_keyword_filter(): ?stdClass {
331         return $this->get_filter_object(
332             'keywords',
333             get_string('filterbykeyword', 'core_user'),
334             true,
335             true,
336             'core_user/local/participantsfilter/filtertypes/keyword',
337             [],
338             true
339         );
340     }
342     /**
343      * Export the renderer data in a mustache template friendly format.
344      *
345      * @param renderer_base $output Unused.
346      * @return stdClass Data in a format compatible with a mustache template.
347      */
348     public function export_for_template(renderer_base $output): stdClass {
349         return (object) [
350             'tableregionid' => $this->tableregionid,
351             'courseid' => $this->context->instanceid,
352             'filtertypes' => $this->get_filtertypes(),
353             'rownumber' => 1,
354         ];
356         return $data;
357     }
359     /**
360      * Get a standardised filter object.
361      *
362      * @param string $name
363      * @param string $title
364      * @param bool $custom
365      * @param bool $multiple
366      * @param string|null $filterclass
367      * @param array $values
368      * @param bool $allowempty
369      * @return stdClass|null
370      */
371     protected function get_filter_object(
372         string $name,
373         string $title,
374         bool $custom,
375         bool $multiple,
376         ?string $filterclass,
377         array $values,
378         bool $allowempty = false
379     ): ?stdClass {
381         if (!$allowempty && empty($values)) {
382             // Do not show empty filters.
383             return null;
384         }
386         return (object) [
387             'name' => $name,
388             'title' => $title,
389             'allowcustom' => $custom,
390             'allowmultiple' => $multiple,
391             'filtertypeclass' => $filterclass,
392             'values' => $values,
393         ];
394     }