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