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 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [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  /**
  18   * Contains class core_course_list_element
  19   *
  20   * @package    core
  21   * @subpackage course
  22   * @copyright  2018 Marina Glancy
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  defined('MOODLE_INTERNAL') || die();
  27  
  28  /**
  29   * Class to store information about one course in a list of courses
  30   *
  31   * Not all information may be retrieved when object is created but
  32   * it will be retrieved on demand when appropriate property or method is
  33   * called.
  34   *
  35   * Instances of this class are usually returned by functions
  36   * {@link core_course_category::search_courses()}
  37   * and
  38   * {@link core_course_category::get_courses()}
  39   *
  40   * @property-read int $id
  41   * @property-read int $category Category ID
  42   * @property-read int $sortorder
  43   * @property-read string $fullname
  44   * @property-read string $shortname
  45   * @property-read string $idnumber
  46   * @property-read string $summary Course summary. Field is present if core_course_category::get_courses()
  47   *     was called with option 'summary'. Otherwise will be retrieved from DB on first request
  48   * @property-read int $summaryformat Summary format. Field is present if core_course_category::get_courses()
  49   *     was called with option 'summary'. Otherwise will be retrieved from DB on first request
  50   * @property-read string $format Course format. Retrieved from DB on first request
  51   * @property-read int $showgrades Retrieved from DB on first request
  52   * @property-read int $newsitems Retrieved from DB on first request
  53   * @property-read int $startdate
  54   * @property-read int $enddate
  55   * @property-read int $marker Retrieved from DB on first request
  56   * @property-read int $maxbytes Retrieved from DB on first request
  57   * @property-read int $legacyfiles Retrieved from DB on first request
  58   * @property-read int $showreports Retrieved from DB on first request
  59   * @property-read int $visible
  60   * @property-read int $visibleold Retrieved from DB on first request
  61   * @property-read int $groupmode Retrieved from DB on first request
  62   * @property-read int $groupmodeforce Retrieved from DB on first request
  63   * @property-read int $defaultgroupingid Retrieved from DB on first request
  64   * @property-read string $lang Retrieved from DB on first request
  65   * @property-read string $theme Retrieved from DB on first request
  66   * @property-read int $timecreated Retrieved from DB on first request
  67   * @property-read int $timemodified Retrieved from DB on first request
  68   * @property-read int $requested Retrieved from DB on first request
  69   * @property-read int $enablecompletion Retrieved from DB on first request
  70   * @property-read int $completionnotify Retrieved from DB on first request
  71   * @property-read int $cacherev
  72   *
  73   * @package    core
  74   * @subpackage course
  75   * @copyright  2013 Marina Glancy
  76   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  77   */
  78  class core_course_list_element implements IteratorAggregate {
  79  
  80      /** @var stdClass record retrieved from DB, may have additional calculated property such as managers and hassummary */
  81      protected $record;
  82  
  83      /** @var array array of course contacts - stores result of call to get_course_contacts() */
  84      protected $coursecontacts;
  85  
  86      /** @var bool true if the current user can access the course, false otherwise. */
  87      protected $canaccess = null;
  88  
  89      /**
  90       * Creates an instance of the class from record
  91       *
  92       * @param stdClass $record except fields from course table it may contain
  93       *     field hassummary indicating that summary field is not empty.
  94       *     Also it is recommended to have context fields here ready for
  95       *     context preloading
  96       */
  97      public function __construct(stdClass $record) {
  98          context_helper::preload_from_record($record);
  99          $this->record = new stdClass();
 100          foreach ($record as $key => $value) {
 101              $this->record->$key = $value;
 102          }
 103      }
 104  
 105      /**
 106       * Indicates if the course has non-empty summary field
 107       *
 108       * @return bool
 109       */
 110      public function has_summary() {
 111          if (isset($this->record->hassummary)) {
 112              return !empty($this->record->hassummary);
 113          }
 114          if (!isset($this->record->summary)) {
 115              // We need to retrieve summary.
 116              $this->__get('summary');
 117          }
 118          return !empty($this->record->summary);
 119      }
 120  
 121      /**
 122       * Indicates if the course have course contacts to display
 123       *
 124       * @return bool
 125       */
 126      public function has_course_contacts() {
 127          if (!isset($this->record->managers)) {
 128              $courses = array($this->id => &$this->record);
 129              core_course_category::preload_course_contacts($courses);
 130          }
 131          return !empty($this->record->managers);
 132      }
 133  
 134      /**
 135       * Returns list of course contacts (usually teachers) to display in course link
 136       *
 137       * Roles to display are set up in $CFG->coursecontact
 138       *
 139       * The result is the list of users where user id is the key and the value
 140       * is an array with elements:
 141       *  - 'user' - object containing basic user information
 142       *  - 'role' - object containing basic role information (id, name, shortname, coursealias)
 143       *  - 'rolename' => role_get_name($role, $context, ROLENAME_ALIAS)
 144       *  - 'username' => fullname($user, $canviewfullnames)
 145       *
 146       * @return array
 147       */
 148      public function get_course_contacts() {
 149          global $CFG;
 150          if (empty($CFG->coursecontact)) {
 151              // No roles are configured to be displayed as course contacts.
 152              return array();
 153          }
 154  
 155          if (!$this->has_course_contacts()) {
 156              // No course contacts exist.
 157              return array();
 158          }
 159  
 160          if ($this->coursecontacts === null) {
 161              $this->coursecontacts = array();
 162  
 163              $context = context_course::instance($this->id);
 164  
 165              $canviewfullnames = has_capability('moodle/site:viewfullnames', $context);
 166  
 167              $displayall = get_config('core', 'coursecontactduplicates');
 168  
 169              foreach ($this->record->managers as $ruser) {
 170                  $processed = array_key_exists($ruser->id, $this->coursecontacts);
 171                  if (!$displayall && $processed) {
 172                      continue;
 173                  }
 174  
 175                  $role = (object)[
 176                          'id'          => $ruser->roleid,
 177                          'name'        => $ruser->rolename,
 178                          'shortname'   => $ruser->roleshortname,
 179                          'coursealias' => $ruser->rolecoursealias,
 180                  ];
 181                  $role->displayname = role_get_name($role, $context, ROLENAME_ALIAS);
 182  
 183                  if (!$processed) {
 184                      $user = username_load_fields_from_object((object)[], $ruser, null, ['id', 'username']);
 185                      $this->coursecontacts[$ruser->id] = [
 186                              'user'     => $user,
 187                              'username' => fullname($user, $canviewfullnames),
 188  
 189                              // List of all roles.
 190                              'roles'    => [],
 191  
 192                              // Primary role of this user.
 193                              'role'     => $role,
 194                              'rolename' => $role->displayname,
 195                      ];
 196                  }
 197                  $this->coursecontacts[$ruser->id]['roles'][$ruser->roleid] = $role;
 198              }
 199          }
 200          return $this->coursecontacts;
 201      }
 202  
 203      /**
 204       * Returns custom fields data for this course
 205       *
 206       * @return \core_customfield\data_controller[]
 207       */
 208      public function get_custom_fields() : array {
 209          if (!isset($this->record->customfields)) {
 210              $this->record->customfields = \core_course\customfield\course_handler::create()->get_instance_data($this->id);
 211          }
 212          return $this->record->customfields;
 213      }
 214  
 215      /**
 216       * Does this course have custom fields
 217       *
 218       * @return bool
 219       */
 220      public function has_custom_fields() : bool {
 221          $customfields = $this->get_custom_fields();
 222          return !empty($customfields);
 223      }
 224  
 225      /**
 226       * Checks if course has any associated overview files
 227       *
 228       * @return bool
 229       */
 230      public function has_course_overviewfiles() {
 231          global $CFG;
 232          if (empty($CFG->courseoverviewfileslimit)) {
 233              return false;
 234          }
 235          $fs = get_file_storage();
 236          $context = context_course::instance($this->id);
 237          return !$fs->is_area_empty($context->id, 'course', 'overviewfiles');
 238      }
 239  
 240      /**
 241       * Returns all course overview files
 242       *
 243       * @return array array of stored_file objects
 244       */
 245      public function get_course_overviewfiles() {
 246          global $CFG;
 247          if (empty($CFG->courseoverviewfileslimit)) {
 248              return array();
 249          }
 250          require_once($CFG->libdir. '/filestorage/file_storage.php');
 251          require_once($CFG->dirroot. '/course/lib.php');
 252          $fs = get_file_storage();
 253          $context = context_course::instance($this->id);
 254          $files = $fs->get_area_files($context->id, 'course', 'overviewfiles', false, 'filename', false);
 255          if (count($files)) {
 256              $overviewfilesoptions = course_overviewfiles_options($this->id);
 257              $acceptedtypes = $overviewfilesoptions['accepted_types'];
 258              if ($acceptedtypes !== '*') {
 259                  // Filter only files with allowed extensions.
 260                  require_once($CFG->libdir. '/filelib.php');
 261                  foreach ($files as $key => $file) {
 262                      if (!file_extension_in_typegroup($file->get_filename(), $acceptedtypes)) {
 263                          unset($files[$key]);
 264                      }
 265                  }
 266              }
 267              if (count($files) > $CFG->courseoverviewfileslimit) {
 268                  // Return no more than $CFG->courseoverviewfileslimit files.
 269                  $files = array_slice($files, 0, $CFG->courseoverviewfileslimit, true);
 270              }
 271          }
 272          return $files;
 273      }
 274  
 275      /**
 276       * Magic method to check if property is set
 277       *
 278       * @param string $name
 279       * @return bool
 280       */
 281      public function __isset($name) {
 282          return isset($this->record->$name);
 283      }
 284  
 285      /**
 286       * Magic method to get a course property
 287       *
 288       * Returns any field from table course (retrieves it from DB if it was not retrieved before)
 289       *
 290       * @param string $name
 291       * @return mixed
 292       */
 293      public function __get($name) {
 294          global $DB;
 295          if (property_exists($this->record, $name)) {
 296              return $this->record->$name;
 297          } else if ($name === 'summary' || $name === 'summaryformat') {
 298              // Retrieve fields summary and summaryformat together because they are most likely to be used together.
 299              $record = $DB->get_record('course', array('id' => $this->record->id), 'summary, summaryformat', MUST_EXIST);
 300              $this->record->summary = $record->summary;
 301              $this->record->summaryformat = $record->summaryformat;
 302              return $this->record->$name;
 303          } else if (array_key_exists($name, $DB->get_columns('course'))) {
 304              // Another field from table 'course' that was not retrieved.
 305              $this->record->$name = $DB->get_field('course', $name, array('id' => $this->record->id), MUST_EXIST);
 306              return $this->record->$name;
 307          }
 308          debugging('Invalid course property accessed! '.$name);
 309          return null;
 310      }
 311  
 312      /**
 313       * All properties are read only, sorry.
 314       *
 315       * @param string $name
 316       */
 317      public function __unset($name) {
 318          debugging('Can not unset '.get_class($this).' instance properties!');
 319      }
 320  
 321      /**
 322       * Magic setter method, we do not want anybody to modify properties from the outside
 323       *
 324       * @param string $name
 325       * @param mixed $value
 326       */
 327      public function __set($name, $value) {
 328          debugging('Can not change '.get_class($this).' instance properties!');
 329      }
 330  
 331      /**
 332       * Create an iterator because magic vars can't be seen by 'foreach'.
 333       * Exclude context fields
 334       *
 335       * Implementing method from interface IteratorAggregate
 336       *
 337       * @return ArrayIterator
 338       */
 339      public function getIterator(): Traversable {
 340          $ret = array('id' => $this->record->id);
 341          foreach ($this->record as $property => $value) {
 342              $ret[$property] = $value;
 343          }
 344          return new ArrayIterator($ret);
 345      }
 346  
 347      /**
 348       * Returns the name of this course as it should be displayed within a list.
 349       * @return string
 350       */
 351      public function get_formatted_name() {
 352          return format_string(
 353              get_course_display_name_for_list($this),
 354              true,
 355              ['context' => $this->get_context()]
 356          );
 357      }
 358  
 359      /**
 360       * Returns the formatted fullname for this course.
 361       * @return string
 362       */
 363      public function get_formatted_fullname() {
 364          return format_string(
 365              $this->__get('fullname'),
 366              true,
 367              ['context' => $this->get_context()]
 368          );
 369      }
 370  
 371      /**
 372       * Returns the formatted shortname for this course.
 373       * @return string
 374       */
 375      public function get_formatted_shortname() {
 376          return format_string(
 377              $this->__get('shortname'),
 378              true,
 379              ['context' => $this->get_context()]
 380          );
 381      }
 382  
 383      /**
 384       * Returns true if the current user can access this course.
 385       * @return bool
 386       */
 387      public function can_access() {
 388          if ($this->canaccess === null) {
 389              $this->canaccess = can_access_course($this->record);
 390          }
 391          return $this->canaccess;
 392      }
 393  
 394      /**
 395       * Returns true if the user can edit this courses settings.
 396       *
 397       * Note: this function does not check that the current user can access the course.
 398       * To do that please call require_login with the course, or if not possible call
 399       * {@link core_course_list_element::can_access()}
 400       *
 401       * @return bool
 402       */
 403      public function can_edit() {
 404          return has_capability('moodle/course:update', $this->get_context());
 405      }
 406  
 407      /**
 408       * Returns true if the user can change the visibility of this course.
 409       *
 410       * Note: this function does not check that the current user can access the course.
 411       * To do that please call require_login with the course, or if not possible call
 412       * {@link core_course_list_element::can_access()}
 413       *
 414       * @return bool
 415       */
 416      public function can_change_visibility() {
 417          // You must be able to both hide a course and view the hidden course.
 418          return has_all_capabilities(array('moodle/course:visibility', 'moodle/course:viewhiddencourses'),
 419              $this->get_context());
 420      }
 421  
 422      /**
 423       * Returns the context for this course.
 424       * @return context_course
 425       */
 426      public function get_context() {
 427          return context_course::instance($this->__get('id'));
 428      }
 429  
 430      /**
 431       * Returns true if the current user can review enrolments for this course.
 432       *
 433       * Note: this function does not check that the current user can access the course.
 434       * To do that please call require_login with the course, or if not possible call
 435       * {@link core_course_list_element::can_access()}
 436       *
 437       * @return bool
 438       */
 439      public function can_review_enrolments() {
 440          return has_capability('moodle/course:enrolreview', $this->get_context());
 441      }
 442  
 443      /**
 444       * Returns true if the current user can delete this course.
 445       *
 446       * Note: this function does not check that the current user can access the course.
 447       * To do that please call require_login with the course, or if not possible call
 448       * {@link core_course_list_element::can_access()}
 449       *
 450       * @return bool
 451       */
 452      public function can_delete() {
 453          return can_delete_course($this->id);
 454      }
 455  
 456      /**
 457       * Returns true if the current user can backup this course.
 458       *
 459       * Note: this function does not check that the current user can access the course.
 460       * To do that please call require_login with the course, or if not possible call
 461       * {@link core_course_list_element::can_access()}
 462       *
 463       * @return bool
 464       */
 465      public function can_backup() {
 466          return has_capability('moodle/backup:backupcourse', $this->get_context());
 467      }
 468  
 469      /**
 470       * Returns true if the current user can restore this course.
 471       *
 472       * Note: this function does not check that the current user can access the course.
 473       * To do that please call require_login with the course, or if not possible call
 474       * {@link core_course_list_element::can_access()}
 475       *
 476       * @return bool
 477       */
 478      public function can_restore() {
 479          return has_capability('moodle/restore:restorecourse', $this->get_context());
 480      }
 481  }