Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

   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   * Class containing data for my overview block.
  19   *
  20   * @package    block_myoverview
  21   * @copyright  2017 Ryan Wyllie <ryan@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace block_myoverview\output;
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  use core_competency\url;
  28  use renderable;
  29  use renderer_base;
  30  use templatable;
  31  use stdClass;
  32  
  33  require_once($CFG->dirroot . '/blocks/myoverview/lib.php');
  34  
  35  /**
  36   * Class containing data for my overview block.
  37   *
  38   * @copyright  2018 Bas Brands <bas@moodle.com>
  39   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  40   */
  41  class main implements renderable, templatable {
  42  
  43      /**
  44       * Store the grouping preference.
  45       *
  46       * @var string String matching the grouping constants defined in myoverview/lib.php
  47       */
  48      private $grouping;
  49  
  50      /**
  51       * Store the sort preference.
  52       *
  53       * @var string String matching the sort constants defined in myoverview/lib.php
  54       */
  55      private $sort;
  56  
  57      /**
  58       * Store the view preference.
  59       *
  60       * @var string String matching the view/display constants defined in myoverview/lib.php
  61       */
  62      private $view;
  63  
  64      /**
  65       * Store the paging preference.
  66       *
  67       * @var string String matching the paging constants defined in myoverview/lib.php
  68       */
  69      private $paging;
  70  
  71      /**
  72       * Store the display categories config setting.
  73       *
  74       * @var boolean
  75       */
  76      private $displaycategories;
  77  
  78      /**
  79       * Store the configuration values for the myoverview block.
  80       *
  81       * @var array Array of available layouts matching view/display constants defined in myoverview/lib.php
  82       */
  83      private $layouts;
  84  
  85      /**
  86       * Store a course grouping option setting
  87       *
  88       * @var boolean
  89       */
  90      private $displaygroupingallincludinghidden;
  91  
  92      /**
  93       * Store a course grouping option setting.
  94       *
  95       * @var boolean
  96       */
  97      private $displaygroupingall;
  98  
  99      /**
 100       * Store a course grouping option setting.
 101       *
 102       * @var boolean
 103       */
 104      private $displaygroupinginprogress;
 105  
 106      /**
 107       * Store a course grouping option setting.
 108       *
 109       * @var boolean
 110       */
 111      private $displaygroupingfuture;
 112  
 113      /**
 114       * Store a course grouping option setting.
 115       *
 116       * @var boolean
 117       */
 118      private $displaygroupingpast;
 119  
 120      /**
 121       * Store a course grouping option setting.
 122       *
 123       * @var boolean
 124       */
 125      private $displaygroupingfavourites;
 126  
 127      /**
 128       * Store a course grouping option setting.
 129       *
 130       * @var boolean
 131       */
 132      private $displaygroupinghidden;
 133  
 134      /**
 135       * Store a course grouping option setting.
 136       *
 137       * @var bool
 138       */
 139      private $displaygroupingcustomfield;
 140  
 141      /**
 142       * Store the custom field used by customfield grouping.
 143       *
 144       * @var string
 145       */
 146      private $customfiltergrouping;
 147  
 148      /**
 149       * Store the selected custom field value to group by.
 150       *
 151       * @var string
 152       */
 153      private $customfieldvalue;
 154  
 155      /** @var bool true if grouping selector should be shown, otherwise false. */
 156      protected $displaygroupingselector;
 157  
 158      /**
 159       * main constructor.
 160       * Initialize the user preferences
 161       *
 162       * @param string $grouping Grouping user preference
 163       * @param string $sort Sort user preference
 164       * @param string $view Display user preference
 165       * @param int $paging
 166       * @param string $customfieldvalue
 167       *
 168       * @throws \dml_exception
 169       */
 170      public function __construct($grouping, $sort, $view, $paging, $customfieldvalue = null) {
 171          global $CFG;
 172          // Get plugin config.
 173          $config = get_config('block_myoverview');
 174  
 175          // Build the course grouping option name to check if the given grouping is enabled afterwards.
 176          $groupingconfigname = 'displaygrouping'.$grouping;
 177          // Check the given grouping and remember it if it is enabled.
 178          if ($grouping && $config->$groupingconfigname == true) {
 179              $this->grouping = $grouping;
 180  
 181              // Otherwise fall back to another grouping in a reasonable order.
 182              // This is done to prevent one-time UI glitches in the case when a user has chosen a grouping option previously which
 183              // was then disabled by the admin in the meantime.
 184          } else {
 185              $this->grouping = $this->get_fallback_grouping($config);
 186          }
 187          unset ($groupingconfigname);
 188  
 189          // Remember which custom field value we were using, if grouping by custom field.
 190          $this->customfieldvalue = $customfieldvalue;
 191  
 192          // Check and remember the given sorting.
 193          if ($sort) {
 194              $this->sort = $sort;
 195          } else if ($CFG->courselistshortnames) {
 196              $this->sort = BLOCK_MYOVERVIEW_SORTING_SHORTNAME;
 197          } else {
 198              $this->sort = BLOCK_MYOVERVIEW_SORTING_TITLE;
 199          }
 200          // In case sorting remembered is shortname and display extended course names not checked,
 201          // we should revert sorting to title.
 202          if (!$CFG->courselistshortnames && $sort == BLOCK_MYOVERVIEW_SORTING_SHORTNAME) {
 203              $this->sort = BLOCK_MYOVERVIEW_SORTING_TITLE;
 204          }
 205  
 206          // Check and remember the given view.
 207          $this->view = $view ? $view : BLOCK_MYOVERVIEW_VIEW_CARD;
 208  
 209          // Check and remember the given page size, `null` indicates no page size set
 210          // while a `0` indicates a paging size of `All`.
 211          if (!is_null($paging) && $paging == BLOCK_MYOVERVIEW_PAGING_ALL) {
 212              $this->paging = BLOCK_MYOVERVIEW_PAGING_ALL;
 213          } else {
 214              $this->paging = $paging ? $paging : BLOCK_MYOVERVIEW_PAGING_12;
 215          }
 216  
 217          // Check and remember if the course categories should be shown or not.
 218          if (!$config->displaycategories) {
 219              $this->displaycategories = BLOCK_MYOVERVIEW_DISPLAY_CATEGORIES_OFF;
 220          } else {
 221              $this->displaycategories = BLOCK_MYOVERVIEW_DISPLAY_CATEGORIES_ON;
 222          }
 223  
 224          // Get and remember the available layouts.
 225          $this->set_available_layouts();
 226          $this->view = $view ? $view : reset($this->layouts);
 227  
 228          // Check and remember if the particular grouping options should be shown or not.
 229          $this->displaygroupingallincludinghidden = $config->displaygroupingallincludinghidden;
 230          $this->displaygroupingall = $config->displaygroupingall;
 231          $this->displaygroupinginprogress = $config->displaygroupinginprogress;
 232          $this->displaygroupingfuture = $config->displaygroupingfuture;
 233          $this->displaygroupingpast = $config->displaygroupingpast;
 234          $this->displaygroupingfavourites = $config->displaygroupingfavourites;
 235          $this->displaygroupinghidden = $config->displaygroupinghidden;
 236          $this->displaygroupingcustomfield = ($config->displaygroupingcustomfield && $config->customfiltergrouping);
 237          $this->customfiltergrouping = $config->customfiltergrouping;
 238  
 239          // Check and remember if the grouping selector should be shown at all or not.
 240          // It will be shown if more than 1 grouping option is enabled.
 241          $displaygroupingselectors = array($this->displaygroupingallincludinghidden,
 242                  $this->displaygroupingall,
 243                  $this->displaygroupinginprogress,
 244                  $this->displaygroupingfuture,
 245                  $this->displaygroupingpast,
 246                  $this->displaygroupingfavourites,
 247                  $this->displaygroupinghidden);
 248          $displaygroupingselectorscount = count(array_filter($displaygroupingselectors));
 249          if ($displaygroupingselectorscount > 1 || $this->displaygroupingcustomfield) {
 250              $this->displaygroupingselector = true;
 251          } else {
 252              $this->displaygroupingselector = false;
 253          }
 254          unset ($displaygroupingselectors, $displaygroupingselectorscount);
 255      }
 256      /**
 257       * Determine the most sensible fallback grouping to use (in cases where the stored selection
 258       * is no longer available).
 259       * @param object $config
 260       * @return string
 261       */
 262      private function get_fallback_grouping($config) {
 263          if ($config->displaygroupingall == true) {
 264              return BLOCK_MYOVERVIEW_GROUPING_ALL;
 265          }
 266          if ($config->displaygroupingallincludinghidden == true) {
 267              return BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
 268          }
 269          if ($config->displaygroupinginprogress == true) {
 270              return BLOCK_MYOVERVIEW_GROUPING_INPROGRESS;
 271          }
 272          if ($config->displaygroupingfuture == true) {
 273              return BLOCK_MYOVERVIEW_GROUPING_FUTURE;
 274          }
 275          if ($config->displaygroupingpast == true) {
 276              return BLOCK_MYOVERVIEW_GROUPING_PAST;
 277          }
 278          if ($config->displaygroupingfavourites == true) {
 279              return BLOCK_MYOVERVIEW_GROUPING_FAVOURITES;
 280          }
 281          if ($config->displaygroupinghidden == true) {
 282              return BLOCK_MYOVERVIEW_GROUPING_HIDDEN;
 283          }
 284          if ($config->displaygroupingcustomfield == true) {
 285              return BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD;
 286          }
 287          // In this case, no grouping option is enabled and the grouping is not needed at all.
 288          // But it's better not to leave $this->grouping unset for any unexpected case.
 289          return BLOCK_MYOVERVIEW_GROUPING_ALLINCLUDINGHIDDEN;
 290      }
 291  
 292      /**
 293       * Set the available layouts based on the config table settings,
 294       * if none are available, defaults to the cards view.
 295       *
 296       * @throws \dml_exception
 297       *
 298       */
 299      public function set_available_layouts() {
 300  
 301          if ($config = get_config('block_myoverview', 'layouts')) {
 302              $this->layouts = explode(',', $config);
 303          } else {
 304              $this->layouts = array(BLOCK_MYOVERVIEW_VIEW_CARD);
 305          }
 306      }
 307  
 308      /**
 309       * Get the user preferences as an array to figure out what has been selected.
 310       *
 311       * @return array $preferences Array with the pref as key and value set to true
 312       */
 313      public function get_preferences_as_booleans() {
 314          $preferences = [];
 315          $preferences[$this->sort] = true;
 316          $preferences[$this->grouping] = true;
 317          // Only use the user view/display preference if it is in available layouts.
 318          if (in_array($this->view, $this->layouts)) {
 319              $preferences[$this->view] = true;
 320          } else {
 321              $preferences[reset($this->layouts)] = true;
 322          }
 323  
 324          return $preferences;
 325      }
 326  
 327      /**
 328       * Format a layout into an object for export as a Context variable to template.
 329       *
 330       * @param string $layoutname
 331       *
 332       * @return \stdClass $layout an object representation of a layout
 333       * @throws \coding_exception
 334       */
 335      public function format_layout_for_export($layoutname) {
 336          $layout = new stdClass();
 337  
 338          $layout->id = $layoutname;
 339          $layout->name = get_string($layoutname, 'block_myoverview');
 340          $layout->active = $this->view == $layoutname ? true : false;
 341          $layout->arialabel = get_string('aria:' . $layoutname, 'block_myoverview');
 342  
 343          return $layout;
 344      }
 345  
 346      /**
 347       * Get the available layouts formatted for export.
 348       *
 349       * @return array an array of objects representing available layouts
 350       */
 351      public function get_formatted_available_layouts_for_export() {
 352  
 353          return array_map(array($this, 'format_layout_for_export'), $this->layouts);
 354  
 355      }
 356  
 357      /**
 358       * Get the list of values to add to the grouping dropdown
 359       * @return object[] containing name, value and active fields
 360       */
 361      public function get_customfield_values_for_export() {
 362          global $DB, $USER;
 363          if (!$this->displaygroupingcustomfield) {
 364              return [];
 365          }
 366          $fieldid = $DB->get_field('customfield_field', 'id', ['shortname' => $this->customfiltergrouping]);
 367          if (!$fieldid) {
 368              return [];
 369          }
 370          $courses = enrol_get_all_users_courses($USER->id, true);
 371          if (!$courses) {
 372              return [];
 373          }
 374          list($csql, $params) = $DB->get_in_or_equal(array_keys($courses), SQL_PARAMS_NAMED);
 375          $select = "instanceid $csql AND fieldid = :fieldid";
 376          $params['fieldid'] = $fieldid;
 377          $distinctablevalue = $DB->sql_compare_text('value');
 378          $values = $DB->get_records_select_menu('customfield_data', $select, $params, '',
 379              "DISTINCT $distinctablevalue, $distinctablevalue AS value2");
 380          \core_collator::asort($values, \core_collator::SORT_NATURAL);
 381          $values = array_filter($values);
 382          if (!$values) {
 383              return [];
 384          }
 385          $field = \core_customfield\field_controller::create($fieldid);
 386          $isvisible = $field->get_configdata_property('visibility') == \core_course\customfield\course_handler::VISIBLETOALL;
 387          // Only visible fields to everybody supporting course grouping will be displayed.
 388          if (!$field->supports_course_grouping() || !$isvisible) {
 389              return []; // The field shouldn't have been selectable in the global settings, but just skip it now.
 390          }
 391          $values = $field->course_grouping_format_values($values);
 392          $customfieldactive = ($this->grouping === BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD);
 393          $ret = [];
 394          foreach ($values as $value => $name) {
 395              $ret[] = (object)[
 396                  'name' => $name,
 397                  'value' => $value,
 398                  'active' => ($customfieldactive && ($this->customfieldvalue == $value)),
 399              ];
 400          }
 401          return $ret;
 402      }
 403  
 404      /**
 405       * Export this data so it can be used as the context for a mustache template.
 406       *
 407       * @param \renderer_base $output
 408       * @return array Context variables for the template
 409       * @throws \coding_exception
 410       *
 411       */
 412      public function export_for_template(renderer_base $output) {
 413          global $CFG, $USER;
 414  
 415          $nocoursesurl = $output->image_url('courses', 'block_myoverview')->out();
 416  
 417          $newcourseurl = '';
 418          $coursecat = \core_course_category::user_top();
 419          if ($coursecat && ($category = \core_course_category::get_nearest_editable_subcategory($coursecat, ['create']))) {
 420              $newcourseurl = new \moodle_url('/course/edit.php', ['category' => $category->id]);
 421          }
 422  
 423          $customfieldvalues = $this->get_customfield_values_for_export();
 424          $selectedcustomfield = '';
 425          if ($this->grouping == BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD) {
 426              foreach ($customfieldvalues as $field) {
 427                  if ($field->value == $this->customfieldvalue) {
 428                      $selectedcustomfield = $field->name;
 429                      break;
 430                  }
 431              }
 432              // If the selected custom field value has not been found (possibly because the field has
 433              // been changed in the settings) find a suitable fallback.
 434              if (!$selectedcustomfield) {
 435                  $this->grouping = $this->get_fallback_grouping(get_config('block_myoverview'));
 436                  if ($this->grouping == BLOCK_MYOVERVIEW_GROUPING_CUSTOMFIELD) {
 437                      // If the fallback grouping is still customfield, then select the first field.
 438                      $firstfield = reset($customfieldvalues);
 439                      if ($firstfield) {
 440                          $selectedcustomfield = $firstfield->name;
 441                          $this->customfieldvalue = $firstfield->value;
 442                      }
 443                  }
 444              }
 445          }
 446          $preferences = $this->get_preferences_as_booleans();
 447          $availablelayouts = $this->get_formatted_available_layouts_for_export();
 448          $sort = '';
 449          if ($this->sort == BLOCK_MYOVERVIEW_SORTING_SHORTNAME) {
 450              $sort = 'shortname';
 451          } else {
 452              $sort = $this->sort == BLOCK_MYOVERVIEW_SORTING_TITLE ? 'fullname' : 'ul.timeaccess desc';
 453          }
 454  
 455          $defaultvariables = [
 456              'totalcoursecount' => count(enrol_get_all_users_courses($USER->id, true)),
 457              'nocoursesimg' => $nocoursesurl,
 458              'newcourseurl' => $newcourseurl,
 459              'grouping' => $this->grouping,
 460              'sort' => $sort,
 461              // If the user preference display option is not available, default to first available layout.
 462              'view' => in_array($this->view, $this->layouts) ? $this->view : reset($this->layouts),
 463              'paging' => $this->paging,
 464              'layouts' => $availablelayouts,
 465              'displaycategories' => $this->displaycategories,
 466              'displaydropdown' => (count($availablelayouts) > 1) ? true : false,
 467              'displaygroupingallincludinghidden' => $this->displaygroupingallincludinghidden,
 468              'displaygroupingall' => $this->displaygroupingall,
 469              'displaygroupinginprogress' => $this->displaygroupinginprogress,
 470              'displaygroupingfuture' => $this->displaygroupingfuture,
 471              'displaygroupingpast' => $this->displaygroupingpast,
 472              'displaygroupingfavourites' => $this->displaygroupingfavourites,
 473              'displaygroupinghidden' => $this->displaygroupinghidden,
 474              'displaygroupingselector' => $this->displaygroupingselector,
 475              'displaygroupingcustomfield' => $this->displaygroupingcustomfield && $customfieldvalues,
 476              'customfieldname' => $this->customfiltergrouping,
 477              'customfieldvalue' => $this->customfieldvalue,
 478              'customfieldvalues' => $customfieldvalues,
 479              'selectedcustomfield' => $selectedcustomfield,
 480              'showsortbyshortname' => $CFG->courselistshortnames,
 481          ];
 482          return array_merge($defaultvariables, $preferences);
 483  
 484      }
 485  
 486      /**
 487       * Export this data so it can be used as the context for a mustache template.
 488       *
 489       * @param \renderer_base $output
 490       * @return array Context variables for the template
 491       * @throws \coding_exception
 492       *
 493       */
 494      public function export_for_zero_state_template(renderer_base $output) {
 495          global $CFG, $DB;
 496  
 497          $nocoursesimg = $output->image_url('courses', 'block_myoverview');
 498  
 499          $coursecat = \core_course_category::user_top();
 500          if ($coursecat) {
 501              $category = \core_course_category::get_nearest_editable_subcategory($coursecat, ['moodle/course:request']);
 502              if ($category && $category->can_request_course()) {
 503                  // Add Request a course button.
 504                  $button = new \single_button(
 505                      new \moodle_url('/course/request.php', ['category' => $category->id]),
 506                      get_string('requestcourse'),
 507                      'post',
 508                      \single_button::BUTTON_PRIMARY
 509                  );
 510                  return $this->generate_zero_state_data(
 511                      $nocoursesimg,
 512                      [$button->export_for_template($output)],
 513                      ['title' => 'zero_request_title', 'intro' => 'zero_request_intro']
 514                  );
 515              }
 516  
 517              $totalcourses = $DB->count_records_select('course', 'category > 0');
 518              if (!$totalcourses && ($category = \core_course_category::get_nearest_editable_subcategory($coursecat, ['create']))) {
 519                  // Add Quickstart guide and Create course buttons.
 520                  $quickstarturl = $CFG->coursecreationguide;
 521                  if ($quickstarturl) {
 522                      $quickstartbutton = new \single_button(
 523                          new \moodle_url($quickstarturl, ['lang' => current_language()]),
 524                          get_string('viewquickstart', 'block_myoverview'),
 525                      );
 526                      $buttons = [$quickstartbutton->export_for_template($output)];
 527                  }
 528  
 529                  $createbutton = new \single_button(
 530                      new \moodle_url('/course/edit.php', ['category' => $category->id]),
 531                      get_string('createcourse', 'block_myoverview'),
 532                      'post',
 533                      \single_button::BUTTON_PRIMARY
 534                  );
 535                  $buttons[] = $createbutton->export_for_template($output);
 536                  return $this->generate_zero_state_data(
 537                      $nocoursesimg,
 538                      $buttons,
 539                      ['title' => 'zero_nocourses_title', 'intro' => 'zero_nocourses_intro']
 540                  );
 541              }
 542  
 543              if ($categorytocreate = \core_course_category::get_nearest_editable_subcategory($coursecat, ['create'])) {
 544                  $createbutton = new \single_button(
 545                      new \moodle_url('/course/edit.php', ['category' => $categorytocreate->id]),
 546                      get_string('createcourse', 'block_myoverview'),
 547                      'post',
 548                      \single_button::BUTTON_PRIMARY
 549                  );
 550                  $buttons = [$createbutton->export_for_template($output)];
 551                  if ($categorytomanage = \core_course_category::get_nearest_editable_subcategory($coursecat, ['manage'])) {
 552                      // Add a Manage course button.
 553                      $managebutton = new \single_button(
 554                          new \moodle_url('/course/management.php', ['category' => $categorytomanage->id]),
 555                          get_string('managecourses')
 556                      );
 557                      $buttons[] = $managebutton->export_for_template($output);
 558                      return $this->generate_zero_state_data(
 559                          $nocoursesimg,
 560                          array_reverse($buttons),
 561                          ['title' => 'zero_default_title', 'intro' => 'zero_default_intro']
 562                      );
 563                  }
 564                  return $this->generate_zero_state_data(
 565                      $nocoursesimg,
 566                      $buttons,
 567                      ['title' => 'zero_default_title', 'intro' => 'zero_default_intro']
 568                  );
 569              }
 570          }
 571  
 572          return $this->generate_zero_state_data(
 573              $nocoursesimg,
 574              [],
 575              ['title' => 'zero_default_title', 'intro' => 'zero_default_intro']
 576          );
 577      }
 578  
 579      /**
 580       * Generate the state zero data.
 581       *
 582       * @param \moodle_url $imageurl The URL to the image to show
 583       * @param string[] $buttons Exported {@see \single_button} instances
 584       * @param array $strings Title and intro strings for the zero state if needed.
 585       * @return array Context variables for the template
 586       */
 587      private function generate_zero_state_data(\moodle_url $imageurl, array $buttons, array $strings) {
 588          global $CFG;
 589          // Documentation data.
 590          $dochref = new \moodle_url($CFG->docroot, ['lang' => current_language()]);
 591          $quickstart = new \moodle_url($CFG->coursecreationguide, ['lang' => current_language()]);
 592          $docparams = [
 593              'quickhref' => $quickstart->out(),
 594              'quicktitle' => get_string('viewquickstart', 'block_myoverview'),
 595              'quicktarget' => '_blank',
 596              'dochref' => $dochref->out(),
 597              'doctitle' => get_string('documentation'),
 598              'doctarget' => $CFG->doctonewwindow ? '_blank' : '_self',
 599          ];
 600          return [
 601              'nocoursesimg' => $imageurl->out(),
 602              'title' => ($strings['title']) ? get_string($strings['title'], 'block_myoverview') : '',
 603              'intro' => ($strings['intro']) ? get_string($strings['intro'], 'block_myoverview', $docparams) : '',
 604              'buttons' => $buttons,
 605          ];
 606      }
 607  }