Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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
declare(strict_types=1);
18
 
19
namespace core_reportbuilder\local\entities;
20
 
21
use context_helper;
22
use context_system;
23
use context_user;
24
use core\context;
25
use core_component;
1441 ariadna 26
use core_date;
27
use core_user;
1 efrain 28
use html_writer;
29
use lang_string;
30
use moodle_url;
31
use stdClass;
32
use theme_config;
33
use core_user\fields;
34
use core_reportbuilder\local\filters\boolean_select;
35
use core_reportbuilder\local\filters\date;
36
use core_reportbuilder\local\filters\select;
37
use core_reportbuilder\local\filters\text;
38
use core_reportbuilder\local\filters\user as user_filter;
39
use core_reportbuilder\local\helpers\user_profile_fields;
40
use core_reportbuilder\local\helpers\format;
41
use core_reportbuilder\local\report\column;
42
use core_reportbuilder\local\report\filter;
43
 
44
/**
45
 * User entity class implementation.
46
 *
47
 * This entity defines all the user columns and filters to be used in any report.
48
 *
49
 * @package    core_reportbuilder
50
 * @copyright  2020 Sara Arjona <sara@moodle.com> based on Marina Glancy code.
51
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
52
 */
53
class user extends base {
54
 
55
    /**
56
     * Database tables that this entity uses
57
     *
58
     * @return string[]
59
     */
60
    protected function get_default_tables(): array {
61
        return [
62
            'user',
63
            'context',
64
            'tag_instance',
65
            'tag',
66
        ];
67
    }
68
 
69
    /**
70
     * The default title for this entity
71
     *
72
     * @return lang_string
73
     */
74
    protected function get_default_entity_title(): lang_string {
75
        return new lang_string('entityuser', 'core_reportbuilder');
76
    }
77
 
78
    /**
1441 ariadna 79
     * Initialise the entity
1 efrain 80
     *
81
     * @return base
82
     */
83
    public function initialise(): base {
1441 ariadna 84
        $tablealias = $this->get_table_alias('user');
1 efrain 85
 
1441 ariadna 86
        $userprofilefields = (new user_profile_fields(
87
            "{$tablealias}.id",
88
            $this->get_entity_name(),
89
        ))
90
            ->add_joins($this->get_joins());
91
 
1 efrain 92
        $columns = array_merge($this->get_all_columns(), $userprofilefields->get_columns());
93
        foreach ($columns as $column) {
94
            $this->add_column($column);
95
        }
96
 
1441 ariadna 97
        // All the filters defined by the entity can also be used as conditions.
1 efrain 98
        $filters = array_merge($this->get_all_filters(), $userprofilefields->get_filters());
99
        foreach ($filters as $filter) {
1441 ariadna 100
            $this
101
                ->add_condition($filter)
102
                ->add_filter($filter);
1 efrain 103
        }
104
 
105
        return $this;
106
    }
107
 
108
    /**
109
     * Returns column that corresponds to the given identity field, profile field identifiers will be converted to those
110
     * used by the {@see user_profile_fields} helper
111
     *
112
     * @param string $identityfield Field from the user table, or a custom profile field
113
     * @return column
114
     */
115
    public function get_identity_column(string $identityfield): column {
116
        if (preg_match(fields::PROFILE_FIELD_REGEX, $identityfield, $matches)) {
117
            $identityfield = 'profilefield_' . $matches[1];
118
        }
119
 
120
        return $this->get_column($identityfield);
121
    }
122
 
123
    /**
124
     * Returns columns that correspond to the site configured identity fields
125
     *
126
     * @param context $context
127
     * @param string[] $excluding
128
     * @return column[]
129
     */
130
    public function get_identity_columns(context $context, array $excluding = []): array {
131
        $identityfields = fields::for_identity($context)->excluding(...$excluding)->get_required_fields();
132
 
133
        return array_map([$this, 'get_identity_column'], $identityfields);
134
    }
135
 
136
    /**
137
     * Returns filter that corresponds to the given identity field, profile field identifiers will be converted to those
138
     * used by the {@see user_profile_fields} helper
139
     *
140
     * @param string $identityfield Field from the user table, or a custom profile field
141
     * @return filter
142
     */
143
    public function get_identity_filter(string $identityfield): filter {
144
        if (preg_match(fields::PROFILE_FIELD_REGEX, $identityfield, $matches)) {
145
            $identityfield = 'profilefield_' . $matches[1];
146
        }
147
 
148
        return $this->get_filter($identityfield);
149
    }
150
 
151
    /**
152
     * Returns filters that correspond to the site configured identity fields
153
     *
154
     * @param context $context
155
     * @param string[] $excluding
156
     * @return filter[]
157
     */
158
    public function get_identity_filters(context $context, array $excluding = []): array {
159
        $identityfields = fields::for_identity($context)->excluding(...$excluding)->get_required_fields();
160
 
161
        return array_map([$this, 'get_identity_filter'], $identityfields);
162
    }
163
 
164
    /**
165
     * Return joins necessary for retrieving tags
166
     *
167
     * @return string[]
168
     */
169
    public function get_tag_joins(): array {
170
        return $this->get_tag_joins_for_entity('core', 'user', $this->get_table_alias('user') . '.id');
171
    }
172
 
173
    /**
174
     * Returns list of all available columns
175
     *
176
     * These are all the columns available to use in any report that uses this entity.
177
     *
178
     * @return column[]
179
     */
180
    protected function get_all_columns(): array {
181
        global $DB;
182
 
183
        $usertablealias = $this->get_table_alias('user');
184
        $contexttablealias = $this->get_table_alias('context');
185
 
186
        $fullnameselect = self::get_name_fields_select($usertablealias);
187
        $fullnamesort = explode(', ', $fullnameselect);
188
 
189
        $userpictureselect = fields::for_userpic()->get_sql($usertablealias, false, '', '', false)->selects;
190
        $viewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
191
 
192
        // Fullname column.
193
        $columns[] = (new column(
194
            'fullname',
195
            new lang_string('fullname'),
196
            $this->get_entity_name()
197
        ))
198
            ->add_joins($this->get_joins())
199
            ->add_fields($fullnameselect)
1441 ariadna 200
            ->set_is_sortable(true, $fullnamesort)
201
            ->add_callback(static function($value, stdClass $row) use ($viewfullnames): string {
202
 
203
                // Ensure we have at least one field present.
204
                if (count(array_filter((array) $row, fn($field) => $field !== null)) === 0) {
1 efrain 205
                    return '';
206
                }
207
 
208
                // Ensure we populate all required name properties.
209
                $namefields = fields::get_name_fields();
210
                foreach ($namefields as $namefield) {
211
                    $row->{$namefield} = $row->{$namefield} ?? '';
212
                }
213
 
214
                return fullname($row, $viewfullnames);
215
            });
216
 
217
        // Formatted fullname columns (with link, picture or both).
218
        $fullnamefields = [
219
            'fullnamewithlink' => new lang_string('userfullnamewithlink', 'core_reportbuilder'),
220
            'fullnamewithpicture' => new lang_string('userfullnamewithpicture', 'core_reportbuilder'),
221
            'fullnamewithpicturelink' => new lang_string('userfullnamewithpicturelink', 'core_reportbuilder'),
222
        ];
223
        foreach ($fullnamefields as $fullnamefield => $fullnamelang) {
224
            $column = (new column(
225
                $fullnamefield,
226
                $fullnamelang,
227
                $this->get_entity_name()
228
            ))
229
                ->add_joins($this->get_joins())
230
                ->add_fields($fullnameselect)
231
                ->add_field("{$usertablealias}.id")
1441 ariadna 232
                ->set_is_sortable(true, $fullnamesort)
233
                ->add_callback(static function($value, stdClass $row) use ($fullnamefield, $viewfullnames): string {
1 efrain 234
                    global $OUTPUT;
235
 
1441 ariadna 236
                    // Ensure we have at least one field present.
237
                    if (count(array_filter((array) $row, fn($field) => $field !== null)) === 0) {
1 efrain 238
                        return '';
239
                    }
240
 
241
                    // Ensure we populate all required name properties.
242
                    $namefields = fields::get_name_fields();
243
                    foreach ($namefields as $namefield) {
244
                        $row->{$namefield} = $row->{$namefield} ?? '';
245
                    }
246
 
247
                    if ($fullnamefield === 'fullnamewithlink') {
248
                        return html_writer::link(new moodle_url('/user/profile.php', ['id' => $row->id]),
249
                            fullname($row, $viewfullnames));
250
                    }
251
                    if ($fullnamefield === 'fullnamewithpicture') {
252
                        return $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) .
253
                            fullname($row, $viewfullnames);
254
                    }
255
                    if ($fullnamefield === 'fullnamewithpicturelink') {
256
                        return html_writer::link(new moodle_url('/user/profile.php', ['id' => $row->id]),
257
                            $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) .
258
                            fullname($row, $viewfullnames));
259
                    }
260
 
1441 ariadna 261
                    return (string) $value;
1 efrain 262
                });
263
 
264
            // Picture fields need some more data.
265
            if (strpos($fullnamefield, 'picture') !== false) {
266
                $column->add_fields($userpictureselect);
267
            }
268
 
269
            $columns[] = $column;
270
        }
271
 
272
        // Picture column.
273
        $columns[] = (new column(
274
            'picture',
1441 ariadna 275
            new lang_string('picture'),
1 efrain 276
            $this->get_entity_name()
277
        ))
278
            ->add_joins($this->get_joins())
279
            ->add_fields($userpictureselect)
1441 ariadna 280
            ->add_callback(static function($value, stdClass $row): string {
1 efrain 281
                global $OUTPUT;
282
 
283
                return !empty($row->id) ? $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) : '';
284
            });
285
 
286
        // Add all other user fields.
287
        $userfields = $this->get_user_fields();
288
        foreach ($userfields as $userfield => $userfieldlang) {
289
            $columntype = $this->get_user_field_type($userfield);
290
 
291
            $column = (new column(
292
                $userfield,
293
                $userfieldlang,
294
                $this->get_entity_name()
295
            ))
296
                ->add_joins($this->get_joins())
297
                ->set_type($columntype)
1441 ariadna 298
                ->add_field("{$usertablealias}.{$userfield}")
299
                ->set_is_sortable(true)
1 efrain 300
                ->add_callback([$this, 'format'], $userfield);
301
 
302
            // Join on the context table so that we can use it for formatting these columns later.
303
            if ($userfield === 'description') {
304
                $column
305
                    ->add_join("LEFT JOIN {context} {$contexttablealias}
306
                           ON {$contexttablealias}.contextlevel = " . CONTEXT_USER . "
307
                          AND {$contexttablealias}.instanceid = {$usertablealias}.id")
308
                    ->add_fields("{$usertablealias}.descriptionformat, {$usertablealias}.id")
309
                    ->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias));
310
            }
311
 
312
            $columns[] = $column;
313
        }
314
 
315
        return $columns;
316
    }
317
 
318
    /**
319
     * Formats the user field for display.
320
     *
321
     * @param mixed $value Current field value.
322
     * @param stdClass $row Complete row.
323
     * @param string $fieldname Name of the field to format.
324
     * @return string
325
     */
326
    public function format($value, stdClass $row, string $fieldname): string {
327
        global $CFG;
328
 
329
        if ($this->get_user_field_type($fieldname) === column::TYPE_BOOLEAN) {
330
            return format::boolean_as_text($value);
331
        }
332
 
333
        if ($this->get_user_field_type($fieldname) === column::TYPE_TIMESTAMP) {
334
            return format::userdate($value, $row);
335
        }
336
 
337
        // If the column has corresponding filter, determine the value from its options.
338
        $options = $this->get_options_for($fieldname);
1441 ariadna 339
        if ($options !== null && $value !== null && array_key_exists($value, $options)) {
1 efrain 340
            return $options[$value];
341
        }
342
 
343
        if ($fieldname === 'description') {
344
            if (empty($row->id)) {
345
                return '';
346
            }
347
 
348
            require_once("{$CFG->libdir}/filelib.php");
349
 
350
            context_helper::preload_from_record($row);
351
            $context = context_user::instance($row->id);
352
 
353
            $description = file_rewrite_pluginfile_urls($value, 'pluginfile.php', $context->id, 'user', 'profile', null);
354
            return format_text($description, $row->descriptionformat, ['context' => $context->id]);
355
        }
356
 
357
        return s($value);
358
    }
359
 
360
    /**
361
     * Returns a SQL statement to select all user fields necessary for fullname() function
362
     *
363
     * Note the implementation here is similar to {@see fields::get_sql_fullname} but without concatenation
364
     *
365
     * @param string $usertablealias
366
     * @return string
367
     */
368
    public static function get_name_fields_select(string $usertablealias = 'u'): string {
369
 
370
        $namefields = fields::get_name_fields(true);
371
 
372
        $viewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
1441 ariadna 373
        $dummyfullname = core_user::get_dummy_fullname(null, ['override' => $viewfullnames]);
1 efrain 374
 
375
        // Extract any name fields from the fullname format in the order that they appear.
376
        $matchednames = array_values(order_in_string($namefields, $dummyfullname));
377
 
378
        $userfields = array_map(static function(string $userfield) use ($usertablealias): string {
379
            if (!empty($usertablealias)) {
380
                $userfield = "{$usertablealias}.{$userfield}";
381
            }
382
 
383
            return $userfield;
384
        }, $matchednames);
385
 
386
        return implode(', ', $userfields);
387
    }
388
 
389
    /**
390
     * User fields
391
     *
392
     * @return lang_string[]
393
     */
394
    protected function get_user_fields(): array {
395
        return [
396
            'firstname' => new lang_string('firstname'),
397
            'lastname' => new lang_string('lastname'),
398
            'email' => new lang_string('email'),
399
            'city' => new lang_string('city'),
400
            'country' => new lang_string('country'),
1441 ariadna 401
            'lang' => new lang_string('language'),
402
            'timezone' => new lang_string('timezone'),
1 efrain 403
            'theme' => new lang_string('theme'),
404
            'description' => new lang_string('description'),
405
            'firstnamephonetic' => new lang_string('firstnamephonetic'),
406
            'lastnamephonetic' => new lang_string('lastnamephonetic'),
407
            'middlename' => new lang_string('middlename'),
408
            'alternatename' => new lang_string('alternatename'),
409
            'idnumber' => new lang_string('idnumber'),
410
            'institution' => new lang_string('institution'),
411
            'department' => new lang_string('department'),
412
            'phone1' => new lang_string('phone1'),
413
            'phone2' => new lang_string('phone2'),
414
            'address' => new lang_string('address'),
415
            'lastaccess' => new lang_string('lastaccess'),
416
            'suspended' => new lang_string('suspended'),
417
            'confirmed' => new lang_string('confirmed', 'admin'),
418
            'username' => new lang_string('username'),
419
            'auth' => new lang_string('authentication', 'moodle'),
420
            'moodlenetprofile' => new lang_string('moodlenetprofile', 'user'),
421
            'timecreated' => new lang_string('timecreated', 'core_reportbuilder'),
422
            'timemodified' => new lang_string('timemodified', 'core_reportbuilder'),
423
            'lastip' => new lang_string('lastip'),
424
        ];
425
    }
426
 
427
    /**
428
     * Return appropriate column type for given user field
429
     *
430
     * @param string $userfield
431
     * @return int
432
     */
433
    protected function get_user_field_type(string $userfield): int {
434
        switch ($userfield) {
435
            case 'description':
436
                $fieldtype = column::TYPE_LONGTEXT;
437
                break;
438
            case 'confirmed':
439
            case 'suspended':
440
                $fieldtype = column::TYPE_BOOLEAN;
441
                break;
442
            case 'lastaccess':
443
            case 'timecreated':
444
            case 'timemodified':
445
                $fieldtype = column::TYPE_TIMESTAMP;
446
                break;
447
            default:
448
                $fieldtype = column::TYPE_TEXT;
449
                break;
450
        }
451
 
452
        return $fieldtype;
453
    }
454
 
455
    /**
456
     * Return list of all available filters
457
     *
458
     * @return filter[]
459
     */
460
    protected function get_all_filters(): array {
461
        $tablealias = $this->get_table_alias('user');
462
 
463
        // Fullname filter.
464
        $canviewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
465
        [$fullnamesql, $fullnameparams] = fields::get_sql_fullname($tablealias, $canviewfullnames);
466
        $filters[] = (new filter(
467
            text::class,
468
            'fullname',
469
            new lang_string('fullname'),
470
            $this->get_entity_name(),
471
            $fullnamesql,
472
            $fullnameparams
473
        ))
474
            ->add_joins($this->get_joins());
475
 
1441 ariadna 476
        // Picture filter.
477
        $filters[] = (new filter(
478
            boolean_select::class,
479
            'picture',
480
            new lang_string('picture'),
481
            $this->get_entity_name(),
482
            "CASE WHEN {$tablealias}.picture > 0 THEN 1 ELSE 0 END",
483
        ))
484
            ->add_joins($this->get_joins());
485
 
1 efrain 486
        // User fields filters.
487
        $fields = $this->get_user_fields();
488
        foreach ($fields as $field => $name) {
489
            $optionscallback = [static::class, 'get_options_for_' . $field];
490
            if (is_callable($optionscallback)) {
491
                $classname = select::class;
492
            } else if ($this->get_user_field_type($field) === column::TYPE_BOOLEAN) {
493
                $classname = boolean_select::class;
494
            } else if ($this->get_user_field_type($field) === column::TYPE_TIMESTAMP) {
495
                $classname = date::class;
496
            } else {
497
                $classname = text::class;
498
            }
499
 
500
            $filter = (new filter(
501
                $classname,
502
                $field,
503
                $name,
504
                $this->get_entity_name(),
1441 ariadna 505
                "{$tablealias}.{$field}"
1 efrain 506
            ))
507
                ->add_joins($this->get_joins());
508
 
509
            // Populate filter options by callback, if available.
510
            if (is_callable($optionscallback)) {
511
                $filter->set_options_callback($optionscallback);
512
            }
513
 
514
            $filters[] = $filter;
515
        }
516
 
517
        // User select filter.
518
        $filters[] = (new filter(
519
            user_filter::class,
520
            'userselect',
521
            new lang_string('userselect', 'core_reportbuilder'),
522
            $this->get_entity_name(),
523
            "{$tablealias}.id"
524
        ))
525
            ->add_joins($this->get_joins());
526
 
527
        return $filters;
528
    }
529
 
530
    /**
531
     * Gets list of options if the filter supports it
532
     *
533
     * @param string $fieldname
534
     * @return null|array
535
     */
536
    protected function get_options_for(string $fieldname): ?array {
537
        static $cached = [];
538
        if (!array_key_exists($fieldname, $cached)) {
539
            $callable = [static::class, 'get_options_for_' . $fieldname];
540
            if (is_callable($callable)) {
541
                $cached[$fieldname] = $callable();
542
            } else {
543
                $cached[$fieldname] = null;
544
            }
545
        }
546
        return $cached[$fieldname];
547
    }
548
 
549
    /**
550
     * List of options for the field auth
551
     *
552
     * @return string[]
553
     */
554
    public static function get_options_for_auth(): array {
555
        $authlist = array_keys(core_component::get_plugin_list('auth'));
556
 
557
        return array_map(
558
            fn(string $auth) => get_auth_plugin($auth)->get_title(),
559
            array_combine($authlist, $authlist),
560
        );
561
    }
562
 
563
    /**
564
     * List of options for the field country.
565
     *
566
     * @return string[]
567
     */
568
    public static function get_options_for_country(): array {
569
        return get_string_manager()->get_list_of_countries();
570
    }
571
 
572
    /**
1441 ariadna 573
     * List of options for the field lang.
574
     *
575
     * @return string[]
576
     */
577
    public static function get_options_for_lang(): array {
578
        return get_string_manager()->get_list_of_translations();
579
    }
580
 
581
    /**
582
     * List of options for the field timezone.
583
     *
584
     * @return string[]
585
     */
586
    public static function get_options_for_timezone(): array {
587
        return core_date::get_list_of_timezones(null, true);
588
    }
589
 
590
    /**
1 efrain 591
     * List of options for the field theme.
592
     *
593
     * @return string[]
594
     */
595
    public static function get_options_for_theme(): array {
596
        return array_map(
597
            fn(theme_config $theme) => $theme->get_theme_name(),
598
            get_list_of_themes(),
599
        );
600
    }
601
}