Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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_system;
  22  use html_writer;
  23  use lang_string;
  24  use moodle_url;
  25  use stdClass;
  26  use core_user\fields;
  27  use core_reportbuilder\local\filters\boolean_select;
  28  use core_reportbuilder\local\filters\date;
  29  use core_reportbuilder\local\filters\select;
  30  use core_reportbuilder\local\filters\text;
  31  use core_reportbuilder\local\filters\user as user_filter;
  32  use core_reportbuilder\local\helpers\user_profile_fields;
  33  use core_reportbuilder\local\helpers\format;
  34  use core_reportbuilder\local\report\column;
  35  use core_reportbuilder\local\report\filter;
  36  
  37  /**
  38   * User entity class implementation.
  39   *
  40   * This entity defines all the user columns and filters to be used in any report.
  41   *
  42   * @package    core_reportbuilder
  43   * @copyright  2020 Sara Arjona <sara@moodle.com> based on Marina Glancy code.
  44   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   */
  46  class user extends base {
  47  
  48      /**
  49       * Database tables that this entity uses and their default aliases
  50       *
  51       * @return array
  52       */
  53      protected function get_default_table_aliases(): array {
  54          return ['user' => 'u'];
  55      }
  56  
  57      /**
  58       * The default title for this entity
  59       *
  60       * @return lang_string
  61       */
  62      protected function get_default_entity_title(): lang_string {
  63          return new lang_string('entityuser', 'core_reportbuilder');
  64      }
  65  
  66      /**
  67       * Initialise the entity, add all user fields and all 'visible' user profile fields
  68       *
  69       * @return base
  70       */
  71      public function initialise(): base {
  72          $userprofilefields = $this->get_user_profile_fields();
  73  
  74          $columns = array_merge($this->get_all_columns(), $userprofilefields->get_columns());
  75          foreach ($columns as $column) {
  76              $this->add_column($column);
  77          }
  78  
  79          $filters = array_merge($this->get_all_filters(), $userprofilefields->get_filters());
  80          foreach ($filters as $filter) {
  81              $this->add_filter($filter);
  82          }
  83  
  84          $conditions = array_merge($this->get_all_filters(), $userprofilefields->get_filters());
  85          foreach ($conditions as $condition) {
  86              $this->add_condition($condition);
  87          }
  88  
  89          return $this;
  90      }
  91  
  92      /**
  93       * Get user profile fields helper instance
  94       *
  95       * @return user_profile_fields
  96       */
  97      protected function get_user_profile_fields(): user_profile_fields {
  98          $userprofilefields = new user_profile_fields($this->get_table_alias('user') . '.id', $this->get_entity_name());
  99          $userprofilefields->add_joins($this->get_joins());
 100          return $userprofilefields;
 101      }
 102  
 103      /**
 104       * Returns list of all available columns
 105       *
 106       * These are all the columns available to use in any report that uses this entity.
 107       *
 108       * @return column[]
 109       */
 110      protected function get_all_columns(): array {
 111          global $DB;
 112  
 113          $usertablealias = $this->get_table_alias('user');
 114  
 115          $fullnameselect = self::get_name_fields_select($usertablealias);
 116          $fullnamesort = explode(', ', $fullnameselect);
 117  
 118          $userpictureselect = fields::for_userpic()->get_sql($usertablealias, false, '', '', false)->selects;
 119          $viewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
 120  
 121          // Fullname column.
 122          $columns[] = (new column(
 123              'fullname',
 124              new lang_string('fullname'),
 125              $this->get_entity_name()
 126          ))
 127              ->add_joins($this->get_joins())
 128              ->add_fields($fullnameselect)
 129              ->set_type(column::TYPE_TEXT)
 130              ->set_is_sortable($this->is_sortable('fullname'), $fullnamesort)
 131              ->add_callback(static function(?string $value, stdClass $row) use ($viewfullnames): string {
 132                  if ($value === null) {
 133                      return '';
 134                  }
 135  
 136                  // Ensure we populate all required name properties.
 137                  $namefields = fields::get_name_fields();
 138                  foreach ($namefields as $namefield) {
 139                      $row->{$namefield} = $row->{$namefield} ?? '';
 140                  }
 141  
 142                  return fullname($row, $viewfullnames);
 143              });
 144  
 145          // Formatted fullname columns (with link, picture or both).
 146          $fullnamefields = [
 147              'fullnamewithlink' => new lang_string('userfullnamewithlink', 'core_reportbuilder'),
 148              'fullnamewithpicture' => new lang_string('userfullnamewithpicture', 'core_reportbuilder'),
 149              'fullnamewithpicturelink' => new lang_string('userfullnamewithpicturelink', 'core_reportbuilder'),
 150          ];
 151          foreach ($fullnamefields as $fullnamefield => $fullnamelang) {
 152              $column = (new column(
 153                  $fullnamefield,
 154                  $fullnamelang,
 155                  $this->get_entity_name()
 156              ))
 157                  ->add_joins($this->get_joins())
 158                  ->add_fields($fullnameselect)
 159                  ->add_field("{$usertablealias}.id")
 160                  ->set_type(column::TYPE_TEXT)
 161                  ->set_is_sortable($this->is_sortable($fullnamefield), $fullnamesort)
 162                  ->add_callback(static function(?string $value, stdClass $row) use ($fullnamefield, $viewfullnames): string {
 163                      global $OUTPUT;
 164  
 165                      if ($value === null) {
 166                          return '';
 167                      }
 168  
 169                      // Ensure we populate all required name properties.
 170                      $namefields = fields::get_name_fields();
 171                      foreach ($namefields as $namefield) {
 172                          $row->{$namefield} = $row->{$namefield} ?? '';
 173                      }
 174  
 175                      if ($fullnamefield === 'fullnamewithlink') {
 176                          return html_writer::link(new moodle_url('/user/profile.php', ['id' => $row->id]),
 177                              fullname($row, $viewfullnames));
 178                      }
 179                      if ($fullnamefield === 'fullnamewithpicture') {
 180                          return $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) .
 181                              fullname($row, $viewfullnames);
 182                      }
 183                      if ($fullnamefield === 'fullnamewithpicturelink') {
 184                          return html_writer::link(new moodle_url('/user/profile.php', ['id' => $row->id]),
 185                              $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) .
 186                              fullname($row, $viewfullnames));
 187                      }
 188  
 189                      return $value;
 190                  });
 191  
 192              // Picture fields need some more data.
 193              if (strpos($fullnamefield, 'picture') !== false) {
 194                  $column->add_fields($userpictureselect);
 195              }
 196  
 197              $columns[] = $column;
 198          }
 199  
 200          // Picture column.
 201          $columns[] = (new column(
 202              'picture',
 203              new lang_string('userpicture', 'core_reportbuilder'),
 204              $this->get_entity_name()
 205          ))
 206              ->add_joins($this->get_joins())
 207              ->add_fields($userpictureselect)
 208              ->set_type(column::TYPE_INTEGER)
 209              ->set_is_sortable($this->is_sortable('picture'))
 210              // It doesn't make sense to offer integer aggregation methods for this column.
 211              ->set_disabled_aggregation(['avg', 'max', 'min', 'sum'])
 212              ->add_callback(static function ($value, stdClass $row): string {
 213                  global $OUTPUT;
 214  
 215                  return !empty($row->id) ? $OUTPUT->user_picture($row, ['link' => false, 'alttext' => false]) : '';
 216              });
 217  
 218          // Add all other user fields.
 219          $userfields = $this->get_user_fields();
 220          foreach ($userfields as $userfield => $userfieldlang) {
 221              $columntype = $this->get_user_field_type($userfield);
 222  
 223              $columnfieldsql = "{$usertablealias}.{$userfield}";
 224              if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') {
 225                  $columnfieldsql = $DB->sql_order_by_text($columnfieldsql, 1024);
 226              }
 227  
 228              $column = (new column(
 229                  $userfield,
 230                  $userfieldlang,
 231                  $this->get_entity_name()
 232              ))
 233                  ->add_joins($this->get_joins())
 234                  ->set_type($columntype)
 235                  ->add_field($columnfieldsql, $userfield)
 236                  ->set_is_sortable($this->is_sortable($userfield))
 237                  ->add_callback([$this, 'format'], $userfield);
 238  
 239              // Some columns also have specific format callbacks.
 240              if ($userfield === 'country') {
 241                  $column->add_callback(static function(string $country): string {
 242                      $countries = get_string_manager()->get_list_of_countries(true);
 243                      return $countries[$country] ?? '';
 244                  });
 245              }
 246  
 247              $columns[] = $column;
 248          }
 249  
 250          return $columns;
 251      }
 252  
 253      /**
 254       * Check if this field is sortable
 255       *
 256       * @param string $fieldname
 257       * @return bool
 258       */
 259      protected function is_sortable(string $fieldname): bool {
 260          // Some columns can't be sorted, like longtext or images.
 261          $nonsortable = [
 262              'picture',
 263          ];
 264  
 265          return !in_array($fieldname, $nonsortable);
 266      }
 267  
 268      /**
 269       * Formats the user field for display.
 270       *
 271       * @param mixed $value Current field value.
 272       * @param stdClass $row Complete row.
 273       * @param string $fieldname Name of the field to format.
 274       * @return string
 275       */
 276      public function format($value, stdClass $row, string $fieldname): string {
 277          if ($this->get_user_field_type($fieldname) === column::TYPE_BOOLEAN) {
 278              return format::boolean_as_text($value);
 279          }
 280  
 281          if ($this->get_user_field_type($fieldname) === column::TYPE_TIMESTAMP) {
 282              return format::userdate($value, $row);
 283          }
 284  
 285          return s($value);
 286      }
 287  
 288      /**
 289       * Returns a SQL statement to select all user fields necessary for fullname() function
 290       *
 291       * Note the implementation here is similar to {@see fields::get_sql_fullname} but without concatenation
 292       *
 293       * @param string $usertablealias
 294       * @return string
 295       */
 296      public static function get_name_fields_select(string $usertablealias = 'u'): string {
 297  
 298          $namefields = fields::get_name_fields(true);
 299  
 300          // Create a dummy user object containing all name fields.
 301          $dummyuser = (object) array_combine($namefields, $namefields);
 302          $dummyfullname = fullname($dummyuser, true);
 303  
 304          // Extract any name fields from the fullname format in the order that they appear.
 305          $matchednames = array_values(order_in_string($namefields, $dummyfullname));
 306  
 307          $userfields = array_map(static function(string $userfield) use ($usertablealias): string {
 308              if (!empty($usertablealias)) {
 309                  $userfield = "{$usertablealias}.{$userfield}";
 310              }
 311  
 312              return $userfield;
 313          }, $matchednames);
 314  
 315          return implode(', ', $userfields);
 316      }
 317  
 318      /**
 319       * User fields
 320       *
 321       * @return lang_string[]
 322       */
 323      protected function get_user_fields(): array {
 324          return [
 325              'firstname' => new lang_string('firstname'),
 326              'lastname' => new lang_string('lastname'),
 327              'email' => new lang_string('email'),
 328              'city' => new lang_string('city'),
 329              'country' => new lang_string('country'),
 330              'firstnamephonetic' => new lang_string('firstnamephonetic'),
 331              'lastnamephonetic' => new lang_string('lastnamephonetic'),
 332              'middlename' => new lang_string('middlename'),
 333              'alternatename' => new lang_string('alternatename'),
 334              'idnumber' => new lang_string('idnumber'),
 335              'institution' => new lang_string('institution'),
 336              'department' => new lang_string('department'),
 337              'phone1' => new lang_string('phone1'),
 338              'phone2' => new lang_string('phone2'),
 339              'address' => new lang_string('address'),
 340              'lastaccess' => new lang_string('lastaccess'),
 341              'suspended' => new lang_string('suspended'),
 342              'confirmed' => new lang_string('confirmed', 'admin'),
 343              'username' => new lang_string('username'),
 344              'moodlenetprofile' => new lang_string('moodlenetprofile', 'user'),
 345          ];
 346      }
 347  
 348      /**
 349       * Return appropriate column type for given user field
 350       *
 351       * @param string $userfield
 352       * @return int
 353       */
 354      protected function get_user_field_type(string $userfield): int {
 355          switch ($userfield) {
 356              case 'confirmed':
 357              case 'suspended':
 358                  $fieldtype = column::TYPE_BOOLEAN;
 359                  break;
 360              case 'lastaccess':
 361                  $fieldtype = column::TYPE_TIMESTAMP;
 362                  break;
 363              default:
 364                  $fieldtype = column::TYPE_TEXT;
 365                  break;
 366          }
 367  
 368          return $fieldtype;
 369      }
 370  
 371      /**
 372       * Return list of all available filters
 373       *
 374       * @return filter[]
 375       */
 376      protected function get_all_filters(): array {
 377          $filters = [];
 378          $tablealias = $this->get_table_alias('user');
 379  
 380          // Fullname filter.
 381          $canviewfullnames = has_capability('moodle/site:viewfullnames', context_system::instance());
 382          [$fullnamesql, $fullnameparams] = fields::get_sql_fullname($tablealias, $canviewfullnames);
 383          $filters[] = (new filter(
 384              text::class,
 385              'fullname',
 386              new lang_string('fullname'),
 387              $this->get_entity_name(),
 388              $fullnamesql,
 389              $fullnameparams
 390          ))
 391              ->add_joins($this->get_joins());
 392  
 393          // User fields filters.
 394          $fields = $this->get_user_fields();
 395          foreach ($fields as $field => $name) {
 396              $optionscallback = [static::class, 'get_options_for_' . $field];
 397              if (is_callable($optionscallback)) {
 398                  $classname = select::class;
 399              } else if ($this->get_user_field_type($field) === column::TYPE_BOOLEAN) {
 400                  $classname = boolean_select::class;
 401              } else if ($this->get_user_field_type($field) === column::TYPE_TIMESTAMP) {
 402                  $classname = date::class;
 403              } else {
 404                  $classname = text::class;
 405              }
 406  
 407              $filter = (new filter(
 408                  $classname,
 409                  $field,
 410                  $name,
 411                  $this->get_entity_name(),
 412                  $tablealias . '.' . $field
 413              ))
 414                  ->add_joins($this->get_joins());
 415  
 416              // Populate filter options by callback, if available.
 417              if (is_callable($optionscallback)) {
 418                  $filter->set_options_callback($optionscallback);
 419              }
 420  
 421              $filters[] = $filter;
 422          }
 423  
 424          // User select filter.
 425          $filters[] = (new filter(
 426              user_filter::class,
 427              'userselect',
 428              new lang_string('userselect', 'core_reportbuilder'),
 429              $this->get_entity_name(),
 430              "{$tablealias}.id"
 431          ))
 432              ->add_joins($this->get_joins());
 433  
 434          return $filters;
 435      }
 436  
 437      /**
 438       * List of options for the field country.
 439       *
 440       * @return string[]
 441       */
 442      public static function get_options_for_country(): array {
 443          return array_map('shorten_text', get_string_manager()->get_list_of_countries());
 444      }
 445  }