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