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]

   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   * This file contains main class for the course format singleactivity
  19   *
  20   * @package    format_singleactivity
  21   * @copyright  2012 Marina Glancy
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  require_once($CFG->dirroot. '/course/format/lib.php');
  27  
  28  /**
  29   * Main class for the singleactivity course format
  30   *
  31   * @package    format_singleactivity
  32   * @copyright  2012 Marina Glancy
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class format_singleactivity extends core_courseformat\base {
  36      /** @var cm_info the current activity. Use get_activity() to retrieve it. */
  37      private $activity = false;
  38  
  39      /** @var int The category ID guessed from the form data. */
  40      private $categoryid = false;
  41  
  42      /**
  43       * The URL to use for the specified course
  44       *
  45       * @param int|stdClass $section Section object from database or just field course_sections.section
  46       *     if null the course view page is returned
  47       * @param array $options options for view URL. At the moment core uses:
  48       *     'navigation' (bool) if true and section has no separate page, the function returns null
  49       *     'sr' (int) used by multipage formats to specify to which section to return
  50       * @return null|moodle_url
  51       */
  52      public function get_view_url($section, $options = array()) {
  53          $sectionnum = $section;
  54          if (is_object($sectionnum)) {
  55              $sectionnum = $section->section;
  56          }
  57          if ($sectionnum == 1) {
  58              return new moodle_url('/course/view.php', array('id' => $this->courseid, 'section' => 1));
  59          }
  60          if (!empty($options['navigation']) && $section !== null) {
  61              return null;
  62          }
  63          return new moodle_url('/course/view.php', array('id' => $this->courseid));
  64      }
  65  
  66      /**
  67       * Loads all of the course sections into the navigation
  68       *
  69       * @param global_navigation $navigation
  70       * @param navigation_node $node The course node within the navigation
  71       */
  72      public function extend_course_navigation($navigation, navigation_node $node) {
  73          // Display orphaned activities for the users who can see them.
  74          $context = context_course::instance($this->courseid);
  75          if (has_capability('moodle/course:viewhiddensections', $context)) {
  76              $modinfo = get_fast_modinfo($this->courseid);
  77              if (!empty($modinfo->sections[1])) {
  78                  $section1 = $modinfo->get_section_info(1);
  79                  // Show orphaned activities.
  80                  $orphanednode = $node->add(get_string('orphaned', 'format_singleactivity'),
  81                          $this->get_view_url(1), navigation_node::TYPE_SECTION, null, $section1->id);
  82                  $orphanednode->nodetype = navigation_node::NODETYPE_BRANCH;
  83                  $orphanednode->add_class('orphaned');
  84                  foreach ($modinfo->sections[1] as $cmid) {
  85                      if (has_capability('moodle/course:viewhiddenactivities', context_module::instance($cmid))) {
  86                          $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
  87                      }
  88                  }
  89              }
  90          }
  91      }
  92  
  93      /**
  94       * Adds a course module to the navigation node
  95       *
  96       * This is basically copied from function global_navigation::load_section_activities()
  97       * because it is not accessible from outside.
  98       *
  99       * @param navigation_node $node
 100       * @param cm_info $cm
 101       * @return null|navigation_node
 102       */
 103      protected function navigation_add_activity(navigation_node $node, $cm) {
 104          if (!$cm->uservisible) {
 105              return null;
 106          }
 107          $action = $cm->url;
 108          if (!$action) {
 109              // Do not add to navigation activity without url (i.e. labels).
 110              return null;
 111          }
 112          $activityname = format_string($cm->name, true, array('context' => context_module::instance($cm->id)));
 113          if ($cm->icon) {
 114              $icon = new pix_icon($cm->icon, $cm->modfullname, $cm->iconcomponent);
 115          } else {
 116              $icon = new pix_icon('monologo', $cm->modfullname, $cm->modname);
 117          }
 118          $activitynode = $node->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon);
 119          if (global_navigation::module_extends_navigation($cm->modname)) {
 120              $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
 121          } else {
 122              $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
 123          }
 124          return $activitynode;
 125      }
 126  
 127      /**
 128       * Returns the list of blocks to be automatically added for the newly created course
 129       *
 130       * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
 131       *     each of values is an array of block names (for left and right side columns)
 132       */
 133      public function get_default_blocks() {
 134          // No blocks for this format because course view page is not displayed anyway.
 135          return array(
 136              BLOCK_POS_LEFT => array(),
 137              BLOCK_POS_RIGHT => array()
 138          );
 139      }
 140  
 141      /**
 142       * Definitions of the additional options that this course format uses for course
 143       *
 144       * Singleactivity course format uses one option 'activitytype'
 145       *
 146       * @param bool $foreditform
 147       * @return array of options
 148       */
 149      public function course_format_options($foreditform = false) {
 150          static $courseformatoptions = false;
 151  
 152          $fetchtypes = $courseformatoptions === false;
 153          $fetchtypes = $fetchtypes || ($foreditform && !isset($courseformatoptions['activitytype']['label']));
 154  
 155          if ($fetchtypes) {
 156              $availabletypes = $this->get_supported_activities();
 157              if ($this->courseid) {
 158                  // The course exists. Test against the course.
 159                  $testcontext = context_course::instance($this->courseid);
 160              } else if ($this->categoryid) {
 161                  // The course does not exist yet, but we have a category ID that we can test against.
 162                  $testcontext = context_coursecat::instance($this->categoryid);
 163              } else {
 164                  // The course does not exist, and we somehow do not have a category. Test capabilities against the system context.
 165                  $testcontext = context_system::instance();
 166              }
 167              foreach (array_keys($availabletypes) as $activity) {
 168                  $capability = "mod/{$activity}:addinstance";
 169                  if (!has_capability($capability, $testcontext)) {
 170                      unset($availabletypes[$activity]);
 171                  }
 172              }
 173          }
 174  
 175          if ($courseformatoptions === false) {
 176              $config = get_config('format_singleactivity');
 177              $courseformatoptions = array(
 178                  'activitytype' => array(
 179                      'default' => $config->activitytype,
 180                      'type' => PARAM_TEXT,
 181                  ),
 182              );
 183  
 184              if (!empty($availabletypes) && !isset($availabletypes[$config->activitytype])) {
 185                  $courseformatoptions['activitytype']['default'] = array_keys($availabletypes)[0];
 186              }
 187          }
 188  
 189          if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) {
 190              $courseformatoptionsedit = array(
 191                  'activitytype' => array(
 192                      'label' => new lang_string('activitytype', 'format_singleactivity'),
 193                      'help' => 'activitytype',
 194                      'help_component' => 'format_singleactivity',
 195                      'element_type' => 'select',
 196                      'element_attributes' => array($availabletypes),
 197                  ),
 198              );
 199              $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
 200          }
 201          return $courseformatoptions;
 202      }
 203  
 204      /**
 205       * Adds format options elements to the course/section edit form
 206       *
 207       * This function is called from {@link course_edit_form::definition_after_data()}
 208       *
 209       * Format singleactivity adds a warning when format of the course is about to be changed.
 210       *
 211       * @param MoodleQuickForm $mform form the elements are added to
 212       * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
 213       * @return array array of references to the added form elements
 214       */
 215      public function create_edit_form_elements(&$mform, $forsection = false) {
 216          global $PAGE;
 217  
 218          if (!$this->course && $submitvalues = $mform->getSubmitValues()) {
 219              $this->categoryid = $submitvalues['category'];
 220          }
 221  
 222          $elements = parent::create_edit_form_elements($mform, $forsection);
 223          if (!$forsection && ($course = $PAGE->course) && !empty($course->format) &&
 224                  $course->format !== 'site' && $course->format !== 'singleactivity') {
 225              // This is the existing course in other format, display a warning.
 226              $element = $mform->addElement('static', '', '',
 227                      html_writer::tag('span', get_string('warningchangeformat', 'format_singleactivity'),
 228                              array('class' => 'error')));
 229              array_unshift($elements, $element);
 230          }
 231          return $elements;
 232      }
 233  
 234      /**
 235       * Make sure that current active activity is in section 0
 236       *
 237       * All other activities are moved to section 1 that will be displayed as 'Orphaned'.
 238       * It may be needed after the course format was changed or activitytype in
 239       * course settings has been changed.
 240       *
 241       * @return null|cm_info current activity
 242       */
 243      public function reorder_activities() {
 244          course_create_sections_if_missing($this->courseid, array(0, 1));
 245          foreach ($this->get_sections() as $sectionnum => $section) {
 246              if (($sectionnum && $section->visible) ||
 247                      (!$sectionnum && !$section->visible)) {
 248                  // Make sure that 0 section is visible and all others are hidden.
 249                  set_section_visible($this->courseid, $sectionnum, $sectionnum == 0);
 250              }
 251          }
 252          $modinfo = get_fast_modinfo($this->courseid);
 253  
 254          // Find the current activity (first activity with the specified type in all course activities).
 255          $activitytype = $this->get_activitytype();
 256          $activity = null;
 257          if (!empty($activitytype)) {
 258              foreach ($modinfo->sections as $sectionnum => $cmlist) {
 259                  foreach ($cmlist as $cmid) {
 260                      if ($modinfo->cms[$cmid]->modname === $activitytype) {
 261                          $activity = $modinfo->cms[$cmid];
 262                          break 2;
 263                      }
 264                  }
 265              }
 266          }
 267  
 268          // Make sure the current activity is in the 0-section.
 269          $changed = false;
 270          if ($activity && $activity->sectionnum != 0) {
 271              moveto_module($activity, $modinfo->get_section_info(0));
 272              $changed = true;
 273          }
 274          if ($activity && !$activity->visible) {
 275              set_coursemodule_visible($activity->id, 1);
 276              $changed = true;
 277          }
 278          if ($changed) {
 279              // Cache was reset so get modinfo again.
 280              $modinfo = get_fast_modinfo($this->courseid);
 281          }
 282  
 283          // Move all other activities into section 1 (the order must be kept).
 284          $hasvisibleactivities = false;
 285          $firstorphanedcm = null;
 286          foreach ($modinfo->sections as $sectionnum => $cmlist) {
 287              if ($sectionnum && !empty($cmlist) && $firstorphanedcm === null) {
 288                  $firstorphanedcm = reset($cmlist);
 289              }
 290              foreach ($cmlist as $cmid) {
 291                  if ($sectionnum > 1) {
 292                      moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1));
 293                  } else if (!$hasvisibleactivities && $sectionnum == 1 && $modinfo->get_cm($cmid)->visible) {
 294                      $hasvisibleactivities = true;
 295                  }
 296              }
 297          }
 298          if (!empty($modinfo->sections[0])) {
 299              foreach ($modinfo->sections[0] as $cmid) {
 300                  if (!$activity || $cmid != $activity->id) {
 301                      moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1), $firstorphanedcm);
 302                  }
 303              }
 304          }
 305          if ($hasvisibleactivities) {
 306              set_section_visible($this->courseid, 1, false);
 307          }
 308          return $activity;
 309      }
 310  
 311      /**
 312       * Returns the name of activity type used for this course
 313       *
 314       * @return string|null
 315       */
 316      protected function get_activitytype() {
 317          $options = $this->get_format_options();
 318          $availabletypes = $this->get_supported_activities();
 319          if (!empty($options['activitytype']) &&
 320                  array_key_exists($options['activitytype'], $availabletypes)) {
 321              return $options['activitytype'];
 322          } else {
 323              return null;
 324          }
 325      }
 326  
 327      /**
 328       * Returns the current activity if exists
 329       *
 330       * @return null|cm_info
 331       */
 332      protected function get_activity() {
 333          if ($this->activity === false) {
 334              $this->activity = $this->reorder_activities();
 335          }
 336          return $this->activity;
 337      }
 338  
 339      /**
 340       * Get the activities supported by the format.
 341       *
 342       * Here we ignore the modules that do not have a page of their own, like the label.
 343       *
 344       * @return array array($module => $name of the module).
 345       */
 346      public static function get_supported_activities() {
 347          $availabletypes = get_module_types_names();
 348          foreach ($availabletypes as $module => $name) {
 349              if (plugin_supports('mod', $module, FEATURE_NO_VIEW_LINK, false)) {
 350                  unset($availabletypes[$module]);
 351              }
 352          }
 353          return $availabletypes;
 354      }
 355  
 356      /**
 357       * Checks if the current user can add the activity of the specified type to this course.
 358       *
 359       * @return bool
 360       */
 361      protected function can_add_activity() {
 362          global $CFG;
 363          if (!($modname = $this->get_activitytype())) {
 364              return false;
 365          }
 366          if (!has_capability('moodle/course:manageactivities', context_course::instance($this->courseid))) {
 367              return false;
 368          }
 369          if (!course_allowed_module($this->get_course(), $modname)) {
 370              return false;
 371          }
 372          $libfile = "$CFG->dirroot/mod/$modname/lib.php";
 373          if (!file_exists($libfile)) {
 374              return null;
 375          }
 376          return true;
 377      }
 378  
 379      /**
 380       * Checks if the activity type has multiple items in the activity chooser.
 381       *
 382       * @return bool|null (null if the check is not possible)
 383       */
 384      public function activity_has_subtypes() {
 385          global $USER;
 386          if (!($modname = $this->get_activitytype())) {
 387              return null;
 388          }
 389          $contentitemservice = \core_course\local\factory\content_item_service_factory::get_content_item_service();
 390          $metadata = $contentitemservice->get_content_items_for_user_in_course($USER, $this->get_course());
 391  
 392          // If there are multiple items originating from this mod_xxx component, then it's deemed to have subtypes.
 393          // If there is only 1 item, but it's not a reference to the core content item for the module, then it's also deemed to
 394          // have subtypes.
 395          $count = 0;
 396          foreach ($metadata as $key => $moduledata) {
 397              if ('mod_'.$modname === $moduledata->componentname) {
 398                  $count ++;
 399              }
 400          }
 401          if ($count > 1) {
 402              return true;
 403          } else {
 404              // Get the single item.
 405              $itemmetadata = $metadata[array_search('mod_' . $modname, array_column($metadata, 'componentname'))];
 406              $urlbase = new \moodle_url('/course/mod.php', ['id' => $this->get_course()->id]);
 407              $referenceurl = new \moodle_url($urlbase, ['add' => $modname]);
 408              if ($referenceurl->out(false) != $itemmetadata->link) {
 409                  return true;
 410              }
 411          }
 412          return false;
 413      }
 414  
 415      /**
 416       * Allows course format to execute code on moodle_page::set_course()
 417       *
 418       * This function is executed before the output starts.
 419       *
 420       * If everything is configured correctly, user is redirected from the
 421       * default course view page to the activity view page.
 422       *
 423       * "Section 1" is the administrative page to manage orphaned activities
 424       *
 425       * If user is on course view page and there is no module added to the course
 426       * and the user has 'moodle/course:manageactivities' capability, redirect to create module
 427       * form.
 428       *
 429       * @param moodle_page $page instance of page calling set_course
 430       */
 431      public function page_set_course(moodle_page $page) {
 432          global $PAGE;
 433          $page->add_body_class('format-'. $this->get_format());
 434          if ($PAGE == $page && $page->has_set_url() &&
 435                  $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
 436              $edit = optional_param('edit', -1, PARAM_BOOL);
 437              if (($edit == 0 || $edit == 1) && confirm_sesskey()) {
 438                  // This is a request to turn editing mode on or off, do not redirect here, /course/view.php will do redirection.
 439                  return;
 440              }
 441              $cm = $this->get_activity();
 442              $cursection = optional_param('section', null, PARAM_INT);
 443              if (!empty($cursection) && has_capability('moodle/course:viewhiddensections',
 444                      context_course::instance($this->courseid))) {
 445                  // Display orphaned activities (course view page, section 1).
 446                  return;
 447              }
 448              if (!$this->get_activitytype()) {
 449                  if (has_capability('moodle/course:update', context_course::instance($this->courseid))) {
 450                      // Teacher is redirected to edit course page.
 451                      $url = new moodle_url('/course/edit.php', array('id' => $this->courseid));
 452                      redirect($url, get_string('erroractivitytype', 'format_singleactivity'));
 453                  } else {
 454                      // Student sees an empty course page.
 455                      return;
 456                  }
 457              }
 458              if ($cm === null) {
 459                  if ($this->can_add_activity()) {
 460                      // This is a user who has capability to create an activity.
 461                      if ($this->activity_has_subtypes()) {
 462                          // Activity has multiple items in the activity chooser, it can not be added automatically.
 463                          if (optional_param('addactivity', 0, PARAM_INT)) {
 464                              return;
 465                          } else {
 466                              $url = new moodle_url('/course/view.php', array('id' => $this->courseid, 'addactivity' => 1));
 467                              redirect($url);
 468                          }
 469                      }
 470                      // Redirect to the add activity form.
 471                      $url = new moodle_url('/course/mod.php', array('id' => $this->courseid,
 472                          'section' => 0, 'sesskey' => sesskey(), 'add' => $this->get_activitytype()));
 473                      redirect($url);
 474                  } else {
 475                      // Student views an empty course page.
 476                      return;
 477                  }
 478              } else if (!$cm->uservisible || !$cm->url) {
 479                  // Activity is set but not visible to current user or does not have url.
 480                  // Display course page (either empty or with availability restriction info).
 481                  return;
 482              } else {
 483                  // Everything is set up and accessible, redirect to the activity page!
 484                  redirect($cm->url);
 485              }
 486          }
 487      }
 488  
 489      /**
 490       * Allows course format to execute code on moodle_page::set_cm()
 491       *
 492       * If we are inside the main module for this course, remove extra node level
 493       * from navigation: substitute course node with activity node, move all children
 494       *
 495       * @param moodle_page $page instance of page calling set_cm
 496       */
 497      public function page_set_cm(moodle_page $page) {
 498          global $PAGE;
 499          parent::page_set_cm($page);
 500          if ($PAGE == $page && ($cm = $this->get_activity()) &&
 501                  $cm->uservisible &&
 502                  ($cm->id === $page->cm->id) &&
 503                  ($activitynode = $page->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY)) &&
 504                  ($node = $page->navigation->find($page->course->id, navigation_node::TYPE_COURSE))) {
 505              // Substitute course node with activity node, move all children.
 506              $node->action = $activitynode->action;
 507              $node->type = $activitynode->type;
 508              $node->id = $activitynode->id;
 509              $node->key = $activitynode->key;
 510              $node->isactive = $node->isactive || $activitynode->isactive;
 511              $node->icon = null;
 512              if ($activitynode->children->count()) {
 513                  foreach ($activitynode->children as &$child) {
 514                      $child->remove();
 515                      $node->add_node($child);
 516                  }
 517              } else {
 518                  $node->search_for_active_node();
 519              }
 520              $activitynode->remove();
 521          }
 522      }
 523  
 524      /**
 525       * Returns true if the course has a front page.
 526       *
 527       * @return boolean false
 528       */
 529      public function has_view_page() {
 530          return false;
 531      }
 532  
 533      /**
 534       * Return the plugin configs for external functions.
 535       *
 536       * @return array the list of configuration settings
 537       * @since Moodle 3.5
 538       */
 539      public function get_config_for_external() {
 540          // Return everything (nothing to hide).
 541          return $this->get_format_options();
 542      }
 543  }