Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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_course;
  22  use context_helper;
  23  use core_reportbuilder\local\filters\boolean_select;
  24  use core_reportbuilder\local\filters\course_selector;
  25  use core_reportbuilder\local\filters\date;
  26  use core_reportbuilder\local\filters\select;
  27  use core_reportbuilder\local\filters\text;
  28  use core_reportbuilder\local\helpers\custom_fields;
  29  use core_reportbuilder\local\helpers\format;
  30  use core_reportbuilder\local\report\column;
  31  use core_reportbuilder\local\report\filter;
  32  use html_writer;
  33  use lang_string;
  34  use stdClass;
  35  
  36  defined('MOODLE_INTERNAL') || die();
  37  
  38  global $CFG;
  39  require_once($CFG->dirroot . '/course/lib.php');
  40  
  41  /**
  42   * Course entity class implementation
  43   *
  44   * This entity defines all the course columns and filters to be used in any report.
  45   *
  46   * @package     core_reportbuilder
  47   * @copyright   2021 Sara Arjona <sara@moodle.com> based on Marina Glancy code.
  48   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  49   */
  50  class course extends base {
  51  
  52      /**
  53       * Database tables that this entity uses and their default aliases.
  54       *
  55       * @return array
  56       */
  57      protected function get_default_table_aliases(): array {
  58          return [
  59              'course' => 'c',
  60              'context' => 'cctx',
  61              'tag_instance' => 'cti',
  62              'tag' => 'ct',
  63          ];
  64      }
  65  
  66      /**
  67       * The default machine-readable name for this entity that will be used in the internal names of the columns/filters.
  68       *
  69       * @return string
  70       */
  71      protected function get_default_entity_name(): string {
  72          return 'course';
  73      }
  74  
  75      /**
  76       * The default title for this entity in the list of columns/filters in the report builder.
  77       *
  78       * @return lang_string
  79       */
  80      protected function get_default_entity_title(): lang_string {
  81          return new lang_string('entitycourse', 'core_reportbuilder');
  82      }
  83  
  84      /**
  85       * Get custom fields helper
  86       *
  87       * @return custom_fields
  88       */
  89      protected function get_custom_fields(): custom_fields {
  90          $customfields = new custom_fields($this->get_table_alias('course') . '.id', $this->get_entity_name(),
  91              'core_course', 'course');
  92          $customfields->add_joins($this->get_joins());
  93          return $customfields;
  94      }
  95  
  96      /**
  97       * Initialise the entity, adding all course and custom course fields
  98       *
  99       * @return base
 100       */
 101      public function initialise(): base {
 102          $customfields = $this->get_custom_fields();
 103  
 104          $columns = array_merge($this->get_all_columns(), $customfields->get_columns());
 105          foreach ($columns as $column) {
 106              $this->add_column($column);
 107          }
 108  
 109          $filters = array_merge($this->get_all_filters(), $customfields->get_filters());
 110          foreach ($filters as $filter) {
 111              $this
 112                  ->add_condition($filter)
 113                  ->add_filter($filter);
 114          }
 115  
 116          return $this;
 117      }
 118  
 119      /**
 120       * Return syntax for joining on the context table
 121       *
 122       * @return string
 123       */
 124      public function get_context_join(): string {
 125          $coursealias = $this->get_table_alias('course');
 126          $contextalias = $this->get_table_alias('context');
 127  
 128          return "LEFT JOIN {context} {$contextalias}
 129              ON {$contextalias}.contextlevel = " . CONTEXT_COURSE . "
 130             AND {$contextalias}.instanceid = {$coursealias}.id";
 131      }
 132  
 133      /**
 134       * Course fields.
 135       *
 136       * @return array
 137       */
 138      protected function get_course_fields(): array {
 139          return [
 140              'fullname' => new lang_string('fullnamecourse'),
 141              'shortname' => new lang_string('shortnamecourse'),
 142              'idnumber' => new lang_string('idnumbercourse'),
 143              'summary' => new lang_string('coursesummary'),
 144              'format' => new lang_string('format'),
 145              'startdate' => new lang_string('startdate'),
 146              'enddate' => new lang_string('enddate'),
 147              'visible' => new lang_string('coursevisibility'),
 148              'groupmode' => new lang_string('groupmode', 'group'),
 149              'groupmodeforce' => new lang_string('groupmodeforce', 'group'),
 150              'lang' => new lang_string('forcelanguage'),
 151              'calendartype' => new lang_string('forcecalendartype', 'calendar'),
 152              'theme' => new lang_string('forcetheme'),
 153              'enablecompletion' => new lang_string('enablecompletion', 'completion'),
 154              'downloadcontent' => new lang_string('downloadcoursecontent', 'course'),
 155          ];
 156      }
 157  
 158      /**
 159       * Check if this field is sortable
 160       *
 161       * @param string $fieldname
 162       * @return bool
 163       */
 164      protected function is_sortable(string $fieldname): bool {
 165          // Some columns can't be sorted, like longtext or images.
 166          $nonsortable = [
 167              'summary',
 168          ];
 169  
 170          return !in_array($fieldname, $nonsortable);
 171      }
 172  
 173      /**
 174       * Return appropriate column type for given user field
 175       *
 176       * @param string $coursefield
 177       * @return int
 178       */
 179      protected function get_course_field_type(string $coursefield): int {
 180          switch ($coursefield) {
 181              case 'downloadcontent':
 182              case 'enablecompletion':
 183              case 'groupmodeforce':
 184              case 'visible':
 185                  $fieldtype = column::TYPE_BOOLEAN;
 186                  break;
 187              case 'startdate':
 188              case 'enddate':
 189                  $fieldtype = column::TYPE_TIMESTAMP;
 190                  break;
 191              case 'summary':
 192                  $fieldtype = column::TYPE_LONGTEXT;
 193                  break;
 194              case 'groupmode':
 195                  $fieldtype = column::TYPE_INTEGER;
 196                  break;
 197              case 'calendartype':
 198              case 'idnumber':
 199              case 'format':
 200              case 'fullname':
 201              case 'lang':
 202              case 'shortname':
 203              case 'theme':
 204              default:
 205                  $fieldtype = column::TYPE_TEXT;
 206                  break;
 207          }
 208  
 209          return $fieldtype;
 210      }
 211  
 212      /**
 213       * Return joins necessary for retrieving tags
 214       *
 215       * @return string[]
 216       */
 217      public function get_tag_joins(): array {
 218          $course = $this->get_table_alias('course');
 219          $taginstance = $this->get_table_alias('tag_instance');
 220          $tag = $this->get_table_alias('tag');
 221  
 222          return [
 223              "LEFT JOIN {tag_instance} {$taginstance}
 224                      ON {$taginstance}.component = 'core'
 225                     AND {$taginstance}.itemtype = 'course'
 226                     AND {$taginstance}.itemid = {$course}.id",
 227              "LEFT JOIN {tag} {$tag}
 228                      ON {$tag}.id = {$taginstance}.tagid",
 229          ];
 230      }
 231  
 232      /**
 233       * Returns list of all available columns.
 234       *
 235       * These are all the columns available to use in any report that uses this entity.
 236       *
 237       * @return column[]
 238       */
 239      protected function get_all_columns(): array {
 240          global $DB;
 241  
 242          $coursefields = $this->get_course_fields();
 243          $tablealias = $this->get_table_alias('course');
 244          $contexttablealias = $this->get_table_alias('context');
 245  
 246          // Columns course full name with link, course short name with link and course id with link.
 247          $fields = [
 248              'coursefullnamewithlink' => 'fullname',
 249              'courseshortnamewithlink' => 'shortname',
 250              'courseidnumberewithlink' => 'idnumber',
 251          ];
 252          foreach ($fields as $key => $field) {
 253              $column = (new column(
 254                  $key,
 255                  new lang_string($key, 'core_reportbuilder'),
 256                  $this->get_entity_name()
 257              ))
 258                  ->add_joins($this->get_joins())
 259                  ->set_type(column::TYPE_TEXT)
 260                  ->add_fields("{$tablealias}.{$field} as $key, {$tablealias}.id")
 261                  ->set_is_sortable(true)
 262                  ->add_callback(static function(?string $value, stdClass $row): string {
 263                      if ($value === null) {
 264                          return '';
 265                      }
 266  
 267                      context_helper::preload_from_record($row);
 268  
 269                      return html_writer::link(course_get_url($row->id),
 270                          format_string($value, true, ['context' => context_course::instance($row->id)]));
 271                  });
 272  
 273              // Join on the context table so that we can use it for formatting these columns later.
 274              if ($key === 'coursefullnamewithlink') {
 275                  $column->add_join($this->get_context_join())
 276                      ->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias));
 277              }
 278  
 279              $columns[] = $column;
 280          }
 281  
 282          foreach ($coursefields as $coursefield => $coursefieldlang) {
 283              $columntype = $this->get_course_field_type($coursefield);
 284  
 285              $columnfieldsql = "{$tablealias}.{$coursefield}";
 286              if ($columntype === column::TYPE_LONGTEXT && $DB->get_dbfamily() === 'oracle') {
 287                  $columnfieldsql = $DB->sql_order_by_text($columnfieldsql, 1024);
 288              }
 289  
 290              $column = (new column(
 291                  $coursefield,
 292                  $coursefieldlang,
 293                  $this->get_entity_name()
 294              ))
 295                  ->add_joins($this->get_joins())
 296                  ->set_type($columntype)
 297                  ->add_field($columnfieldsql, $coursefield)
 298                  ->add_callback([$this, 'format'], $coursefield)
 299                  ->set_is_sortable($this->is_sortable($coursefield));
 300  
 301              // Join on the context table so that we can use it for formatting these columns later.
 302              if ($coursefield === 'summary' || $coursefield === 'shortname' || $coursefield === 'fullname') {
 303                  $column->add_join($this->get_context_join())
 304                      ->add_field("{$tablealias}.id", 'courseid')
 305                      ->add_fields(context_helper::get_preload_record_columns_sql($contexttablealias));
 306              }
 307  
 308              $columns[] = $column;
 309          }
 310  
 311          return $columns;
 312      }
 313  
 314      /**
 315       * Returns list of all available filters
 316       *
 317       * @return array
 318       */
 319      protected function get_all_filters(): array {
 320          global $DB;
 321  
 322          $filters = [];
 323          $tablealias = $this->get_table_alias('course');
 324  
 325          $fields = $this->get_course_fields();
 326          foreach ($fields as $field => $name) {
 327              $filterfieldsql = "{$tablealias}.{$field}";
 328              if ($this->get_course_field_type($field) === column::TYPE_LONGTEXT) {
 329                  $filterfieldsql = $DB->sql_cast_to_char($filterfieldsql);
 330              }
 331  
 332              $optionscallback = [static::class, 'get_options_for_' . $field];
 333              if (is_callable($optionscallback)) {
 334                  $filterclass = select::class;
 335              } else if ($this->get_course_field_type($field) === column::TYPE_BOOLEAN) {
 336                  $filterclass = boolean_select::class;
 337              } else if ($this->get_course_field_type($field) === column::TYPE_TIMESTAMP) {
 338                  $filterclass = date::class;
 339              } else {
 340                  $filterclass = text::class;
 341              }
 342  
 343              $filter = (new filter(
 344                  $filterclass,
 345                  $field,
 346                  $name,
 347                  $this->get_entity_name(),
 348                  $filterfieldsql
 349              ))
 350                  ->add_joins($this->get_joins());
 351  
 352              // Populate filter options by callback, if available.
 353              if (is_callable($optionscallback)) {
 354                  $filter->set_options_callback($optionscallback);
 355              }
 356  
 357              $filters[] = $filter;
 358          }
 359  
 360          // We add our own custom course selector filter.
 361          $filters[] = (new filter(
 362              course_selector::class,
 363              'courseselector',
 364              new lang_string('courseselect', 'core_reportbuilder'),
 365              $this->get_entity_name(),
 366              "{$tablealias}.id"
 367          ))
 368              ->add_joins($this->get_joins());
 369  
 370          return $filters;
 371      }
 372  
 373      /**
 374       * Gets list of options if the filter supports it
 375       *
 376       * @param string $fieldname
 377       * @return null|array
 378       */
 379      protected function get_options_for(string $fieldname): ?array {
 380          static $cached = [];
 381          if (!array_key_exists($fieldname, $cached)) {
 382              $callable = [static::class, 'get_options_for_' . $fieldname];
 383              if (is_callable($callable)) {
 384                  $cached[$fieldname] = $callable();
 385              } else {
 386                  $cached[$fieldname] = null;
 387              }
 388          }
 389          return $cached[$fieldname];
 390      }
 391  
 392      /**
 393       * List of options for the field groupmode.
 394       *
 395       * @return array
 396       */
 397      public static function get_options_for_groupmode(): array {
 398          return [
 399              NOGROUPS => get_string('groupsnone', 'group'),
 400              SEPARATEGROUPS => get_string('groupsseparate', 'group'),
 401              VISIBLEGROUPS => get_string('groupsvisible', 'group'),
 402          ];
 403      }
 404  
 405      /**
 406       * List of options for the field format.
 407       *
 408       * @return array
 409       */
 410      public static function get_options_for_format(): array {
 411          global $CFG;
 412          require_once($CFG->dirroot.'/course/lib.php');
 413  
 414          $options = [];
 415  
 416          $courseformats = get_sorted_course_formats(true);
 417          foreach ($courseformats as $courseformat) {
 418              $options[$courseformat] = get_string('pluginname', "format_{$courseformat}");
 419          }
 420  
 421          return $options;
 422      }
 423  
 424      /**
 425       * List of options for the field theme.
 426       *
 427       * @return array
 428       */
 429      public static function get_options_for_theme(): array {
 430          $options = [];
 431  
 432          $themeobjects = get_list_of_themes();
 433          foreach ($themeobjects as $key => $theme) {
 434              if (empty($theme->hidefromselector)) {
 435                  $options[$key] = get_string('pluginname', "theme_{$theme->name}");
 436              }
 437          }
 438  
 439          return $options;
 440      }
 441  
 442      /**
 443       * List of options for the field lang.
 444       *
 445       * @return array
 446       */
 447      public static function get_options_for_lang(): array {
 448          return get_string_manager()->get_list_of_translations();
 449      }
 450  
 451      /**
 452       * List of options for the field.
 453       *
 454       * @return array
 455       */
 456      public static function get_options_for_calendartype(): array {
 457          return \core_calendar\type_factory::get_list_of_calendar_types();
 458      }
 459  
 460      /**
 461       * Formats the course field for display.
 462       *
 463       * @param mixed $value Current field value.
 464       * @param stdClass $row Complete row.
 465       * @param string $fieldname Name of the field to format.
 466       * @return string
 467       */
 468      public function format($value, stdClass $row, string $fieldname): string {
 469          if ($this->get_course_field_type($fieldname) === column::TYPE_TIMESTAMP) {
 470              return format::userdate($value, $row);
 471          }
 472  
 473          $options = $this->get_options_for($fieldname);
 474          if ($options !== null && array_key_exists($value, $options)) {
 475              return $options[$value];
 476          }
 477  
 478          if ($this->get_course_field_type($fieldname) === column::TYPE_BOOLEAN) {
 479              return format::boolean_as_text($value);
 480          }
 481  
 482          if (in_array($fieldname, ['fullname', 'shortname'])) {
 483              if (!$row->courseid) {
 484                  return '';
 485              }
 486              context_helper::preload_from_record($row);
 487              $context = context_course::instance($row->courseid);
 488              return format_string($value, true, ['context' => $context->id, 'escape' => false]);
 489          }
 490  
 491          if (in_array($fieldname, ['summary'])) {
 492              if (!$row->courseid) {
 493                  return '';
 494              }
 495              context_helper::preload_from_record($row);
 496              $context = context_course::instance($row->courseid);
 497              $summary = file_rewrite_pluginfile_urls($row->summary, 'pluginfile.php', $context->id, 'course', 'summary', null);
 498              return format_text($summary);
 499          }
 500  
 501          return s($value);
 502      }
 503  }