See Release Notes
Long Term Support Release
Differences Between: [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 the base definition class for any course format plugin. 19 * 20 * @package core_courseformat 21 * @copyright 2020 Ferran Recio <ferran@moodle.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace core_courseformat; 26 27 use navigation_node; 28 use moodle_page; 29 use core_component; 30 use course_modinfo; 31 use html_writer; 32 use section_info; 33 use context_course; 34 use editsection_form; 35 use moodle_exception; 36 use coding_exception; 37 use moodle_url; 38 use lang_string; 39 use completion_info; 40 use external_api; 41 use stdClass; 42 use cache; 43 use core_courseformat\output\legacy_renderer; 44 45 /** 46 * Base class for course formats 47 * 48 * Each course format must declare class 49 * class format_FORMATNAME extends core_courseformat\base {} 50 * in file lib.php 51 * 52 * For each course just one instance of this class is created and it will always be returned by 53 * course_get_format($courseorid). Format may store it's specific course-dependent options in 54 * variables of this class. 55 * 56 * In rare cases instance of child class may be created just for format without course id 57 * i.e. to check if format supports AJAX. 58 * 59 * Also course formats may extend class section_info and overwrite 60 * course_format::build_section_cache() to return more information about sections. 61 * 62 * If you are upgrading from Moodle 2.3 start with copying the class format_legacy and renaming 63 * it to format_FORMATNAME, then move the code from your callback functions into 64 * appropriate functions of the class. 65 * 66 * @package core_courseformat 67 * @copyright 2012 Marina Glancy 68 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 69 */ 70 abstract class base { 71 /** @var int Id of the course in this instance (maybe 0) */ 72 protected $courseid; 73 /** @var string format used for this course. Please note that it can be different from 74 * course.format field if course referes to non-existing of disabled format */ 75 protected $format; 76 /** @var stdClass course data for course object, please use course_format::get_course() */ 77 protected $course = false; 78 /** @var array caches format options, please use course_format::get_format_options() */ 79 protected $formatoptions = array(); 80 /** @var int the section number in single section format, zero for multiple section formats. */ 81 protected $singlesection = 0; 82 /** @var course_modinfo the current course modinfo, please use course_format::get_modinfo() */ 83 private $modinfo = null; 84 /** @var array cached instances */ 85 private static $instances = array(); 86 /** @var array plugin name => class name. */ 87 private static $classesforformat = array('site' => 'site'); 88 89 /** 90 * Creates a new instance of class 91 * 92 * Please use course_get_format($courseorid) to get an instance of the format class 93 * 94 * @param string $format 95 * @param int $courseid 96 * @return course_format 97 */ 98 protected function __construct($format, $courseid) { 99 $this->format = $format; 100 $this->courseid = $courseid; 101 } 102 103 /** 104 * Validates that course format exists and enabled and returns either itself or default format 105 * 106 * @param string $format 107 * @return string 108 */ 109 protected static final function get_format_or_default($format) { 110 global $CFG; 111 require_once($CFG->dirroot . '/course/lib.php'); 112 113 if (array_key_exists($format, self::$classesforformat)) { 114 return self::$classesforformat[$format]; 115 } 116 117 $plugins = get_sorted_course_formats(); 118 foreach ($plugins as $plugin) { 119 self::$classesforformat[$plugin] = $plugin; 120 } 121 122 if (array_key_exists($format, self::$classesforformat)) { 123 return self::$classesforformat[$format]; 124 } 125 126 if (PHPUNIT_TEST && class_exists('format_' . $format)) { 127 // Allow unittests to use non-existing course formats. 128 return $format; 129 } 130 131 // Else return default format. 132 $defaultformat = get_config('moodlecourse', 'format'); 133 if (!in_array($defaultformat, $plugins)) { 134 // When default format is not set correctly, use the first available format. 135 $defaultformat = reset($plugins); 136 } 137 debugging('Format plugin format_'.$format.' is not found. Using default format_'.$defaultformat, DEBUG_DEVELOPER); 138 139 self::$classesforformat[$format] = $defaultformat; 140 return $defaultformat; 141 } 142 143 /** 144 * Get class name for the format. 145 * 146 * If course format xxx does not declare class format_xxx, format_legacy will be returned. 147 * This function also includes lib.php file from corresponding format plugin 148 * 149 * @param string $format 150 * @return string 151 */ 152 protected static final function get_class_name($format) { 153 global $CFG; 154 static $classnames = array('site' => 'format_site'); 155 if (!isset($classnames[$format])) { 156 $plugins = core_component::get_plugin_list('format'); 157 $usedformat = self::get_format_or_default($format); 158 if (isset($plugins[$usedformat]) && file_exists($plugins[$usedformat].'/lib.php')) { 159 require_once($plugins[$usedformat].'/lib.php'); 160 } 161 $classnames[$format] = 'format_'. $usedformat; 162 if (!class_exists($classnames[$format])) { 163 require_once($CFG->dirroot.'/course/format/formatlegacy.php'); 164 $classnames[$format] = 'format_legacy'; 165 } 166 } 167 return $classnames[$format]; 168 } 169 170 /** 171 * Returns an instance of the class 172 * 173 * @todo MDL-35727 use MUC for caching of instances, limit the number of cached instances 174 * 175 * @param int|stdClass $courseorid either course id or 176 * an object that has the property 'format' and may contain property 'id' 177 * @return course_format 178 */ 179 public static final function instance($courseorid) { 180 global $DB; 181 if (!is_object($courseorid)) { 182 $courseid = (int)$courseorid; 183 if ($courseid && isset(self::$instances[$courseid]) && count(self::$instances[$courseid]) == 1) { 184 $formats = array_keys(self::$instances[$courseid]); 185 $format = reset($formats); 186 } else { 187 $format = $DB->get_field('course', 'format', array('id' => $courseid), MUST_EXIST); 188 } 189 } else { 190 $format = $courseorid->format; 191 if (isset($courseorid->id)) { 192 $courseid = clean_param($courseorid->id, PARAM_INT); 193 } else { 194 $courseid = 0; 195 } 196 } 197 // Validate that format exists and enabled, use default otherwise. 198 $format = self::get_format_or_default($format); 199 if (!isset(self::$instances[$courseid][$format])) { 200 $classname = self::get_class_name($format); 201 self::$instances[$courseid][$format] = new $classname($format, $courseid); 202 } 203 return self::$instances[$courseid][$format]; 204 } 205 206 /** 207 * Resets cache for the course (or all caches) 208 * 209 * To be called from rebuild_course_cache() 210 * 211 * @param int $courseid 212 */ 213 public static final function reset_course_cache($courseid = 0) { 214 if ($courseid) { 215 if (isset(self::$instances[$courseid])) { 216 foreach (self::$instances[$courseid] as $format => $object) { 217 // In case somebody keeps the reference to course format object. 218 self::$instances[$courseid][$format]->course = false; 219 self::$instances[$courseid][$format]->formatoptions = array(); 220 self::$instances[$courseid][$format]->modinfo = null; 221 } 222 unset(self::$instances[$courseid]); 223 } 224 } else { 225 self::$instances = array(); 226 } 227 } 228 /** 229 * Reset the current user for all courses. 230 * 231 * The course format cache resets every time the course cache resets but 232 * also when the user changes their language, all course editors 233 * 234 * @return void 235 */ 236 public static function session_cache_reset_all(): void { 237 $statecache = cache::make('core', 'courseeditorstate'); 238 $statecache->purge(); 239 } 240 241 /** 242 * Reset the current user course format cache. 243 * 244 * The course format cache resets every time the course cache resets but 245 * also when the user changes their course format preference, complete 246 * an activity... 247 * 248 * @param stdClass $course the course object 249 * @return string the new statekey 250 */ 251 public static function session_cache_reset(stdClass $course): string { 252 $statecache = cache::make('core', 'courseeditorstate'); 253 $newkey = $course->cacherev . '_' . time(); 254 $statecache->set($course->id, $newkey); 255 return $newkey; 256 } 257 258 /** 259 * Return the current user course format cache key. 260 * 261 * The course format session cache can be used to cache the 262 * user course representation. The statekey will be reset when the 263 * the course state changes. For example when the course is edited, 264 * the user completes an activity or simply some course preference 265 * like collapsing a section happens. 266 * 267 * @param stdClass $course the course object 268 * @return string the current statekey 269 */ 270 public static function session_cache(stdClass $course): string { 271 $statecache = cache::make('core', 'courseeditorstate'); 272 $statekey = $statecache->get($course->id); 273 // Validate the statekey code. 274 if (preg_match('/^[0-9]+_[0-9]+$/', $statekey)) { 275 list($cacherev) = explode('_', $statekey); 276 if ($cacherev == $course->cacherev) { 277 return $statekey; 278 } 279 } 280 return self::session_cache_reset($course); 281 } 282 283 /** 284 * Returns the format name used by this course 285 * 286 * @return string 287 */ 288 public final function get_format() { 289 return $this->format; 290 } 291 292 /** 293 * Returns id of the course (0 if course is not specified) 294 * 295 * @return int 296 */ 297 public final function get_courseid() { 298 return $this->courseid; 299 } 300 301 /** 302 * Returns a record from course database table plus additional fields 303 * that course format defines 304 * 305 * @return stdClass 306 */ 307 public function get_course() { 308 global $DB; 309 if (!$this->courseid) { 310 return null; 311 } 312 if ($this->course === false) { 313 $this->course = get_course($this->courseid); 314 $options = $this->get_format_options(); 315 $dbcoursecolumns = null; 316 foreach ($options as $optionname => $optionvalue) { 317 if (isset($this->course->$optionname)) { 318 // Course format options must not have the same names as existing columns in db table "course". 319 if (!isset($dbcoursecolumns)) { 320 $dbcoursecolumns = $DB->get_columns('course'); 321 } 322 if (isset($dbcoursecolumns[$optionname])) { 323 debugging('The option name '.$optionname.' in course format '.$this->format. 324 ' is invalid because the field with the same name exists in {course} table', 325 DEBUG_DEVELOPER); 326 continue; 327 } 328 } 329 $this->course->$optionname = $optionvalue; 330 } 331 } 332 return $this->course; 333 } 334 335 /** 336 * Get the course display value for the current course. 337 * 338 * Formats extending topics or weeks will use coursedisplay as this setting name 339 * so they don't need to override the method. However, if the format uses a different 340 * display logic it must override this method to ensure the core renderers know 341 * if a COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE is being used. 342 * 343 * @return int The current value (COURSE_DISPLAY_MULTIPAGE or COURSE_DISPLAY_SINGLEPAGE) 344 */ 345 public function get_course_display(): int { 346 return $this->get_course()->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; 347 } 348 349 /** 350 * Return the current course modinfo. 351 * 352 * This method is used mainly by the output components to avoid unnecesary get_fast_modinfo calls. 353 * 354 * @return course_modinfo 355 */ 356 public function get_modinfo(): course_modinfo { 357 global $USER; 358 if ($this->modinfo === null) { 359 $this->modinfo = course_modinfo::instance($this->courseid, $USER->id); 360 } 361 return $this->modinfo; 362 } 363 364 /** 365 * Method used in the rendered and during backup instead of legacy 'numsections' 366 * 367 * Default renderer will treat sections with sectionnumber greater that the value returned by this 368 * method as "orphaned" and not display them on the course page unless in editing mode. 369 * Backup will store this value as 'numsections'. 370 * 371 * This method ensures that 3rd party course format plugins that still use 'numsections' continue to 372 * work but at the same time we no longer expect formats to have 'numsections' property. 373 * 374 * @return int The last section number, or -1 if sections are entirely missing 375 */ 376 public function get_last_section_number() { 377 $course = $this->get_course(); 378 if (isset($course->numsections)) { 379 return $course->numsections; 380 } 381 $modinfo = get_fast_modinfo($course); 382 $sections = $modinfo->get_section_info_all(); 383 384 // Sections seem to be missing entirely. Avoid subsequent errors and return early. 385 if (count($sections) === 0) { 386 return -1; 387 } 388 389 return (int)max(array_keys($sections)); 390 } 391 392 /** 393 * Method used to get the maximum number of sections for this course format. 394 * @return int 395 */ 396 public function get_max_sections() { 397 $maxsections = get_config('moodlecourse', 'maxsections'); 398 if (!isset($maxsections) || !is_numeric($maxsections)) { 399 $maxsections = 52; 400 } 401 return $maxsections; 402 } 403 404 /** 405 * Returns true if the course has a front page. 406 * 407 * This function is called to determine if the course has a view page, whether or not 408 * it contains a listing of activities. It can be useful to set this to false when the course 409 * format has only one activity and ignores the course page. Or if there are multiple 410 * activities but no page to see the centralised information. 411 * 412 * Initially this was created to know if forms should add a button to return to the course page. 413 * So if 'Return to course' does not make sense in your format your should probably return false. 414 * 415 * @return boolean 416 * @since Moodle 2.6 417 */ 418 public function has_view_page() { 419 return true; 420 } 421 422 /** 423 * Generate the title for this section page. 424 * 425 * @return string the page title 426 */ 427 public function page_title(): string { 428 global $PAGE; 429 return $PAGE->title; 430 } 431 432 /** 433 * Returns true if this course format uses sections 434 * 435 * This function may be called without specifying the course id 436 * i.e. in course_format_uses_sections() 437 * 438 * Developers, note that if course format does use sections there should be defined a language 439 * string with the name 'sectionname' defining what the section relates to in the format, i.e. 440 * $string['sectionname'] = 'Topic'; 441 * or 442 * $string['sectionname'] = 'Week'; 443 * 444 * @return bool 445 */ 446 public function uses_sections() { 447 return false; 448 } 449 450 /** 451 * Returns true if this course format uses course index 452 * 453 * This function may be called without specifying the course id 454 * i.e. in course_index_drawer() 455 * 456 * @return bool 457 */ 458 public function uses_course_index() { 459 return false; 460 } 461 462 /** 463 * Returns true if this course format uses activity indentation. 464 * 465 * @return bool if the course format uses indentation. 466 */ 467 public function uses_indentation(): bool { 468 return true; 469 } 470 471 /** 472 * Returns a list of sections used in the course 473 * 474 * This is a shortcut to get_fast_modinfo()->get_section_info_all() 475 * @see get_fast_modinfo() 476 * @see course_modinfo::get_section_info_all() 477 * 478 * @return array of section_info objects 479 */ 480 public final function get_sections() { 481 if ($course = $this->get_course()) { 482 $modinfo = get_fast_modinfo($course); 483 return $modinfo->get_section_info_all(); 484 } 485 return array(); 486 } 487 488 /** 489 * Returns information about section used in course 490 * 491 * @param int|stdClass $section either section number (field course_section.section) or row from course_section table 492 * @param int $strictness 493 * @return section_info 494 */ 495 public final function get_section($section, $strictness = IGNORE_MISSING) { 496 if (is_object($section)) { 497 $sectionnum = $section->section; 498 } else { 499 $sectionnum = $section; 500 } 501 $sections = $this->get_sections(); 502 if (array_key_exists($sectionnum, $sections)) { 503 return $sections[$sectionnum]; 504 } 505 if ($strictness == MUST_EXIST) { 506 throw new moodle_exception('sectionnotexist'); 507 } 508 return null; 509 } 510 511 /** 512 * Returns the display name of the given section that the course prefers. 513 * 514 * @param int|stdClass $section Section object from database or just field course_sections.section 515 * @return Display name that the course format prefers, e.g. "Topic 2" 516 */ 517 public function get_section_name($section) { 518 if (is_object($section)) { 519 $sectionnum = $section->section; 520 } else { 521 $sectionnum = $section; 522 } 523 524 if (get_string_manager()->string_exists('sectionname', 'format_' . $this->format)) { 525 return get_string('sectionname', 'format_' . $this->format) . ' ' . $sectionnum; 526 } 527 528 // Return an empty string if there's no available section name string for the given format. 529 return ''; 530 } 531 532 /** 533 * Returns the default section using course_format's implementation of get_section_name. 534 * 535 * @param int|stdClass $section Section object from database or just field course_sections section 536 * @return string The default value for the section name based on the given course format. 537 */ 538 public function get_default_section_name($section) { 539 return self::get_section_name($section); 540 } 541 542 /** 543 * Returns the name for the highlighted section. 544 * 545 * @return string The name for the highlighted section based on the given course format. 546 */ 547 public function get_section_highlighted_name(): string { 548 return get_string('highlighted'); 549 } 550 551 /** 552 * Set if the current format instance will show multiple sections or an individual one. 553 * 554 * Some formats has the hability to swith from one section to multiple sections per page, 555 * this method replaces the old print_multiple_section_page and print_single_section_page. 556 * 557 * @param int $singlesection zero for all sections or a section number 558 */ 559 public function set_section_number(int $singlesection): void { 560 $this->singlesection = $singlesection; 561 } 562 563 /** 564 * Set if the current format instance will show multiple sections or an individual one. 565 * 566 * Some formats has the hability to swith from one section to multiple sections per page, 567 * output components will use this method to know if the current display is a single or 568 * multiple sections. 569 * 570 * @return int zero for all sections or the sectin number 571 */ 572 public function get_section_number(): int { 573 return $this->singlesection; 574 } 575 576 /** 577 * Return the format section preferences. 578 * 579 * @return array of preferences indexed by sectionid 580 */ 581 public function get_sections_preferences(): array { 582 global $USER; 583 584 $result = []; 585 586 $course = $this->get_course(); 587 $coursesectionscache = cache::make('core', 'coursesectionspreferences'); 588 589 $coursesections = $coursesectionscache->get($course->id); 590 if ($coursesections) { 591 return $coursesections; 592 } 593 594 $sectionpreferences = $this->get_sections_preferences_by_preference(); 595 596 foreach ($sectionpreferences as $preference => $sectionids) { 597 if (!empty($sectionids) && is_array($sectionids)) { 598 foreach ($sectionids as $sectionid) { 599 if (!isset($result[$sectionid])) { 600 $result[$sectionid] = new stdClass(); 601 } 602 $result[$sectionid]->$preference = true; 603 } 604 } 605 } 606 607 $coursesectionscache->set($course->id, $result); 608 return $result; 609 } 610 611 /** 612 * Return the format section preferences. 613 * 614 * @return array of preferences indexed by preference name 615 */ 616 public function get_sections_preferences_by_preference(): array { 617 global $USER; 618 $course = $this->get_course(); 619 try { 620 $sectionpreferences = (array) json_decode( 621 get_user_preferences("coursesectionspreferences_{$course->id}", '', $USER->id) 622 ); 623 if (empty($sectionpreferences)) { 624 $sectionpreferences = []; 625 } 626 } catch (\Throwable $e) { 627 $sectionpreferences = []; 628 } 629 return $sectionpreferences; 630 } 631 632 /** 633 * Return the format section preferences. 634 * 635 * @param string $preferencename preference name 636 * @param int[] $sectionids affected section ids 637 * 638 */ 639 public function set_sections_preference(string $preferencename, array $sectionids) { 640 global $USER; 641 $course = $this->get_course(); 642 $sectionpreferences = $this->get_sections_preferences_by_preference(); 643 $sectionpreferences[$preferencename] = $sectionids; 644 set_user_preference('coursesectionspreferences_' . $course->id, json_encode($sectionpreferences), $USER->id); 645 // Invalidate section preferences cache. 646 $coursesectionscache = cache::make('core', 'coursesectionspreferences'); 647 $coursesectionscache->delete($course->id); 648 } 649 650 /** 651 * Returns the information about the ajax support in the given source format 652 * 653 * The returned object's property (boolean)capable indicates that 654 * the course format supports Moodle course ajax features. 655 * 656 * @return stdClass 657 */ 658 public function supports_ajax() { 659 // No support by default. 660 $ajaxsupport = new stdClass(); 661 $ajaxsupport->capable = false; 662 return $ajaxsupport; 663 } 664 665 /** 666 * Returns true if this course format is compatible with content components. 667 * 668 * Using components means the content elements can watch the frontend course state and 669 * react to the changes. Formats with component compatibility can have more interactions 670 * without refreshing the page, like having drag and drop from the course index to reorder 671 * sections and activities. 672 * 673 * @return bool if the format is compatible with components. 674 */ 675 public function supports_components() { 676 return false; 677 } 678 679 680 /** 681 * Custom action after section has been moved in AJAX mode 682 * 683 * Used in course/rest.php 684 * 685 * @return array This will be passed in ajax respose 686 */ 687 public function ajax_section_move() { 688 return null; 689 } 690 691 /** 692 * The URL to use for the specified course (with section) 693 * 694 * Please note that course view page /course/view.php?id=COURSEID is hardcoded in many 695 * places in core and contributed modules. If course format wants to change the location 696 * of the view script, it is not enough to change just this function. Do not forget 697 * to add proper redirection. 698 * 699 * @param int|stdClass $section Section object from database or just field course_sections.section 700 * if null the course view page is returned 701 * @param array $options options for view URL. At the moment core uses: 702 * 'navigation' (bool) if true and section has no separate page, the function returns null 703 * 'sr' (int) used by multipage formats to specify to which section to return 704 * @return null|moodle_url 705 */ 706 public function get_view_url($section, $options = array()) { 707 global $CFG; 708 $course = $this->get_course(); 709 $url = new moodle_url('/course/view.php', array('id' => $course->id)); 710 711 if (array_key_exists('sr', $options)) { 712 $sectionno = $options['sr']; 713 } else if (is_object($section)) { 714 $sectionno = $section->section; 715 } else { 716 $sectionno = $section; 717 } 718 if (empty($CFG->linkcoursesections) && !empty($options['navigation']) && $sectionno !== null) { 719 // By default assume that sections are never displayed on separate pages. 720 return null; 721 } 722 if ($this->uses_sections() && $sectionno !== null) { 723 $url->set_anchor('section-'.$sectionno); 724 } 725 return $url; 726 } 727 728 /** 729 * Loads all of the course sections into the navigation 730 * 731 * This method is called from global_navigation::load_course_sections() 732 * 733 * By default the method global_navigation::load_generic_course_sections() is called 734 * 735 * When overwriting please note that navigationlib relies on using the correct values for 736 * arguments $type and $key in navigation_node::add() 737 * 738 * Example of code creating a section node: 739 * $sectionnode = $node->add($sectionname, $url, navigation_node::TYPE_SECTION, null, $section->id); 740 * $sectionnode->nodetype = navigation_node::NODETYPE_BRANCH; 741 * 742 * Example of code creating an activity node: 743 * $activitynode = $sectionnode->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $activity->id, $icon); 744 * if (global_navigation::module_extends_navigation($activity->modname)) { 745 * $activitynode->nodetype = navigation_node::NODETYPE_BRANCH; 746 * } else { 747 * $activitynode->nodetype = navigation_node::NODETYPE_LEAF; 748 * } 749 * 750 * Also note that if $navigation->includesectionnum is not null, the section with this relative 751 * number needs is expected to be loaded 752 * 753 * @param global_navigation $navigation 754 * @param navigation_node $node The course node within the navigation 755 */ 756 public function extend_course_navigation($navigation, navigation_node $node) { 757 if ($course = $this->get_course()) { 758 $navigation->load_generic_course_sections($course, $node); 759 } 760 return array(); 761 } 762 763 /** 764 * Returns the list of blocks to be automatically added for the newly created course 765 * 766 * @see blocks_add_default_course_blocks() 767 * 768 * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT 769 * each of values is an array of block names (for left and right side columns) 770 */ 771 public function get_default_blocks() { 772 global $CFG; 773 if (isset($CFG->defaultblocks)) { 774 return blocks_parse_default_blocks_list($CFG->defaultblocks); 775 } 776 $blocknames = array( 777 BLOCK_POS_LEFT => array(), 778 BLOCK_POS_RIGHT => array() 779 ); 780 return $blocknames; 781 } 782 783 /** 784 * Returns the localised name of this course format plugin 785 * 786 * @return lang_string 787 */ 788 public final function get_format_name() { 789 return new lang_string('pluginname', 'format_'.$this->get_format()); 790 } 791 792 /** 793 * Definitions of the additional options that this course format uses for course 794 * 795 * This function may be called often, it should be as fast as possible. 796 * Avoid using get_string() method, use "new lang_string()" instead 797 * It is not recommended to use dynamic or course-dependant expressions here 798 * This function may be also called when course does not exist yet. 799 * 800 * Option names must be different from fields in the {course} talbe or any form elements on 801 * course edit form, it may even make sence to use special prefix for them. 802 * 803 * Each option must have the option name as a key and the array of properties as a value: 804 * 'default' - default value for this option (assumed null if not specified) 805 * 'type' - type of the option value (PARAM_INT, PARAM_RAW, etc.) 806 * 807 * Additional properties used by default implementation of 808 * course_format::create_edit_form_elements() (calls this method with $foreditform = true) 809 * 'label' - localised human-readable label for the edit form 810 * 'element_type' - type of the form element, default 'text' 811 * 'element_attributes' - additional attributes for the form element, these are 4th and further 812 * arguments in the moodleform::addElement() method 813 * 'help' - string for help button. Note that if 'help' value is 'myoption' then the string with 814 * the name 'myoption_help' must exist in the language file 815 * 'help_component' - language component to look for help string, by default this the component 816 * for this course format 817 * 818 * This is an interface for creating simple form elements. If format plugin wants to use other 819 * methods such as disableIf, it can be done by overriding create_edit_form_elements(). 820 * 821 * Course format options can be accessed as: 822 * $this->get_course()->OPTIONNAME (inside the format class) 823 * course_get_format($course)->get_course()->OPTIONNAME (outside of format class) 824 * 825 * All course options are returned by calling: 826 * $this->get_format_options(); 827 * 828 * @param bool $foreditform 829 * @return array of options 830 */ 831 public function course_format_options($foreditform = false) { 832 return array(); 833 } 834 835 /** 836 * Definitions of the additional options that this course format uses for section 837 * 838 * See course_format::course_format_options() for return array definition. 839 * 840 * Additionally section format options may have property 'cache' set to true 841 * if this option needs to be cached in get_fast_modinfo(). The 'cache' property 842 * is recommended to be set only for fields used in course_format::get_section_name(), 843 * course_format::extend_course_navigation() and course_format::get_view_url() 844 * 845 * For better performance cached options are recommended to have 'cachedefault' property 846 * Unlike 'default', 'cachedefault' should be static and not access get_config(). 847 * 848 * Regardless of value of 'cache' all options are accessed in the code as 849 * $sectioninfo->OPTIONNAME 850 * where $sectioninfo is instance of section_info, returned by 851 * get_fast_modinfo($course)->get_section_info($sectionnum) 852 * or get_fast_modinfo($course)->get_section_info_all() 853 * 854 * All format options for particular section are returned by calling: 855 * $this->get_format_options($section); 856 * 857 * @param bool $foreditform 858 * @return array 859 */ 860 public function section_format_options($foreditform = false) { 861 return array(); 862 } 863 864 /** 865 * Returns the format options stored for this course or course section 866 * 867 * When overriding please note that this function is called from rebuild_course_cache() 868 * and section_info object, therefore using of get_fast_modinfo() and/or any function that 869 * accesses it may lead to recursion. 870 * 871 * @param null|int|stdClass|section_info $section if null the course format options will be returned 872 * otherwise options for specified section will be returned. This can be either 873 * section object or relative section number (field course_sections.section) 874 * @return array 875 */ 876 public function get_format_options($section = null) { 877 global $DB; 878 if ($section === null) { 879 $options = $this->course_format_options(); 880 } else { 881 $options = $this->section_format_options(); 882 } 883 if (empty($options)) { 884 // There are no option for course/sections anyway, no need to go further. 885 return array(); 886 } 887 if ($section === null) { 888 // Course format options will be returned. 889 $sectionid = 0; 890 } else if ($this->courseid && isset($section->id)) { 891 // Course section format options will be returned. 892 $sectionid = $section->id; 893 } else if ($this->courseid && is_int($section) && 894 ($sectionobj = $DB->get_record('course_sections', 895 array('section' => $section, 'course' => $this->courseid), 'id'))) { 896 // Course section format options will be returned. 897 $sectionid = $sectionobj->id; 898 } else { 899 // Non-existing (yet) section was passed as an argument 900 // default format options for course section will be returned. 901 $sectionid = -1; 902 } 903 if (!array_key_exists($sectionid, $this->formatoptions)) { 904 $this->formatoptions[$sectionid] = array(); 905 // First fill with default values. 906 foreach ($options as $optionname => $optionparams) { 907 $this->formatoptions[$sectionid][$optionname] = null; 908 if (array_key_exists('default', $optionparams)) { 909 $this->formatoptions[$sectionid][$optionname] = $optionparams['default']; 910 } 911 } 912 if ($this->courseid && $sectionid !== -1) { 913 // Overwrite the default options values with those stored in course_format_options table 914 // nothing can be stored if we are interested in generic course ($this->courseid == 0) 915 // or generic section ($sectionid === 0). 916 $records = $DB->get_records('course_format_options', 917 array('courseid' => $this->courseid, 918 'format' => $this->format, 919 'sectionid' => $sectionid 920 ), '', 'id,name,value'); 921 $indexedrecords = []; 922 foreach ($records as $record) { 923 $indexedrecords[$record->name] = $record->value; 924 } 925 foreach ($options as $optionname => $option) { 926 contract_value($this->formatoptions[$sectionid], $indexedrecords, $option, $optionname); 927 } 928 } 929 } 930 return $this->formatoptions[$sectionid]; 931 } 932 933 /** 934 * Adds format options elements to the course/section edit form 935 * 936 * This function is called from course_edit_form::definition_after_data() 937 * 938 * @param MoodleQuickForm $mform form the elements are added to 939 * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form 940 * @return array array of references to the added form elements 941 */ 942 public function create_edit_form_elements(&$mform, $forsection = false) { 943 $elements = array(); 944 if ($forsection) { 945 $options = $this->section_format_options(true); 946 } else { 947 $options = $this->course_format_options(true); 948 } 949 foreach ($options as $optionname => $option) { 950 if (!isset($option['element_type'])) { 951 $option['element_type'] = 'text'; 952 } 953 $args = array($option['element_type'], $optionname, $option['label']); 954 if (!empty($option['element_attributes'])) { 955 $args = array_merge($args, $option['element_attributes']); 956 } 957 $elements[] = call_user_func_array(array($mform, 'addElement'), $args); 958 if (isset($option['help'])) { 959 $helpcomponent = 'format_'. $this->get_format(); 960 if (isset($option['help_component'])) { 961 $helpcomponent = $option['help_component']; 962 } 963 $mform->addHelpButton($optionname, $option['help'], $helpcomponent); 964 } 965 if (isset($option['type'])) { 966 $mform->setType($optionname, $option['type']); 967 } 968 if (isset($option['default']) && !array_key_exists($optionname, $mform->_defaultValues)) { 969 // Set defaults for the elements in the form. 970 // Since we call this method after set_data() make sure that we don't override what was already set. 971 $mform->setDefault($optionname, $option['default']); 972 } 973 } 974 975 if (!$forsection && empty($this->courseid)) { 976 // Check if course end date form field should be enabled by default. 977 // If a default date is provided to the form element, it is magically enabled by default in the 978 // MoodleQuickForm_date_time_selector class, otherwise it's disabled by default. 979 if (get_config('moodlecourse', 'courseenddateenabled')) { 980 // At this stage (this is called from definition_after_data) course data is already set as default. 981 // We can not overwrite what is in the database. 982 $mform->setDefault('enddate', $this->get_default_course_enddate($mform)); 983 } 984 } 985 986 return $elements; 987 } 988 989 /** 990 * Override if you need to perform some extra validation of the format options 991 * 992 * @param array $data array of ("fieldname"=>value) of submitted data 993 * @param array $files array of uploaded files "element_name"=>tmp_file_path 994 * @param array $errors errors already discovered in edit form validation 995 * @return array of "element_name"=>"error_description" if there are errors, 996 * or an empty array if everything is OK. 997 * Do not repeat errors from $errors param here 998 */ 999 public function edit_form_validation($data, $files, $errors) { 1000 return array(); 1001 } 1002 1003 /** 1004 * Prepares values of course or section format options before storing them in DB 1005 * 1006 * If an option has invalid value it is not returned 1007 * 1008 * @param array $rawdata associative array of the proposed course/section format options 1009 * @param int|null $sectionid null if it is course format option 1010 * @return array array of options that have valid values 1011 */ 1012 protected function validate_format_options(array $rawdata, int $sectionid = null) : array { 1013 if (!$sectionid) { 1014 $allformatoptions = $this->course_format_options(true); 1015 } else { 1016 $allformatoptions = $this->section_format_options(true); 1017 } 1018 $data = array_intersect_key($rawdata, $allformatoptions); 1019 foreach ($data as $key => $value) { 1020 $option = $allformatoptions[$key] + ['type' => PARAM_RAW, 'element_type' => null, 'element_attributes' => [[]]]; 1021 expand_value($data, $data, $option, $key); 1022 if ($option['element_type'] === 'select' && !array_key_exists($data[$key], $option['element_attributes'][0])) { 1023 // Value invalid for select element, skip. 1024 unset($data[$key]); 1025 } 1026 } 1027 return $data; 1028 } 1029 1030 /** 1031 * Validates format options for the course 1032 * 1033 * @param array $data data to insert/update 1034 * @return array array of options that have valid values 1035 */ 1036 public function validate_course_format_options(array $data) : array { 1037 return $this->validate_format_options($data); 1038 } 1039 1040 /** 1041 * Updates format options for a course or section 1042 * 1043 * If $data does not contain property with the option name, the option will not be updated 1044 * 1045 * @param stdClass|array $data return value from moodleform::get_data() or array with data 1046 * @param null|int $sectionid null if these are options for course or section id (course_sections.id) 1047 * if these are options for section 1048 * @return bool whether there were any changes to the options values 1049 */ 1050 protected function update_format_options($data, $sectionid = null) { 1051 global $DB; 1052 $data = $this->validate_format_options((array)$data, $sectionid); 1053 if (!$sectionid) { 1054 $allformatoptions = $this->course_format_options(); 1055 $sectionid = 0; 1056 } else { 1057 $allformatoptions = $this->section_format_options(); 1058 } 1059 if (empty($allformatoptions)) { 1060 // Nothing to update anyway. 1061 return false; 1062 } 1063 $defaultoptions = array(); 1064 $cached = array(); 1065 foreach ($allformatoptions as $key => $option) { 1066 $defaultoptions[$key] = null; 1067 if (array_key_exists('default', $option)) { 1068 $defaultoptions[$key] = $option['default']; 1069 } 1070 expand_value($defaultoptions, $defaultoptions, $option, $key); 1071 $cached[$key] = ($sectionid === 0 || !empty($option['cache'])); 1072 } 1073 $records = $DB->get_records('course_format_options', 1074 array('courseid' => $this->courseid, 1075 'format' => $this->format, 1076 'sectionid' => $sectionid 1077 ), '', 'name,id,value'); 1078 $changed = $needrebuild = false; 1079 foreach ($defaultoptions as $key => $value) { 1080 if (isset($records[$key])) { 1081 if (array_key_exists($key, $data) && $records[$key]->value != $data[$key]) { 1082 $DB->set_field('course_format_options', 'value', 1083 $data[$key], array('id' => $records[$key]->id)); 1084 $changed = true; 1085 $needrebuild = $needrebuild || $cached[$key]; 1086 } 1087 } else { 1088 if (array_key_exists($key, $data) && $data[$key] !== $value) { 1089 $newvalue = $data[$key]; 1090 $changed = true; 1091 $needrebuild = $needrebuild || $cached[$key]; 1092 } else { 1093 $newvalue = $value; 1094 // We still insert entry in DB but there are no changes from user point of 1095 // view and no need to call rebuild_course_cache(). 1096 } 1097 $DB->insert_record('course_format_options', array( 1098 'courseid' => $this->courseid, 1099 'format' => $this->format, 1100 'sectionid' => $sectionid, 1101 'name' => $key, 1102 'value' => $newvalue 1103 )); 1104 } 1105 } 1106 if ($needrebuild) { 1107 if ($sectionid) { 1108 // Invalidate the section cache by given section id. 1109 course_modinfo::purge_course_section_cache_by_id($this->courseid, $sectionid); 1110 // Partial rebuild sections that have been invalidated. 1111 rebuild_course_cache($this->courseid, true, true); 1112 } else { 1113 // Full rebuild if sectionid is null. 1114 rebuild_course_cache($this->courseid); 1115 } 1116 } 1117 if ($changed) { 1118 // Reset internal caches. 1119 if (!$sectionid) { 1120 $this->course = false; 1121 } 1122 unset($this->formatoptions[$sectionid]); 1123 } 1124 return $changed; 1125 } 1126 1127 /** 1128 * Updates format options for a course 1129 * 1130 * If $data does not contain property with the option name, the option will not be updated 1131 * 1132 * @param stdClass|array $data return value from moodleform::get_data() or array with data 1133 * @param stdClass $oldcourse if this function is called from update_course() 1134 * this object contains information about the course before update 1135 * @return bool whether there were any changes to the options values 1136 */ 1137 public function update_course_format_options($data, $oldcourse = null) { 1138 return $this->update_format_options($data); 1139 } 1140 1141 /** 1142 * Updates format options for a section 1143 * 1144 * Section id is expected in $data->id (or $data['id']) 1145 * If $data does not contain property with the option name, the option will not be updated 1146 * 1147 * @param stdClass|array $data return value from moodleform::get_data() or array with data 1148 * @return bool whether there were any changes to the options values 1149 */ 1150 public function update_section_format_options($data) { 1151 $data = (array)$data; 1152 return $this->update_format_options($data, $data['id']); 1153 } 1154 1155 /** 1156 * Return an instance of moodleform to edit a specified section 1157 * 1158 * Default implementation returns instance of editsection_form that automatically adds 1159 * additional fields defined in course_format::section_format_options() 1160 * 1161 * Format plugins may extend editsection_form if they want to have custom edit section form. 1162 * 1163 * @param mixed $action the action attribute for the form. If empty defaults to auto detect the 1164 * current url. If a moodle_url object then outputs params as hidden variables. 1165 * @param array $customdata the array with custom data to be passed to the form 1166 * /course/editsection.php passes section_info object in 'cs' field 1167 * for filling availability fields 1168 * @return moodleform 1169 */ 1170 public function editsection_form($action, $customdata = array()) { 1171 global $CFG; 1172 require_once($CFG->dirroot. '/course/editsection_form.php'); 1173 if (!array_key_exists('course', $customdata)) { 1174 $customdata['course'] = $this->get_course(); 1175 } 1176 return new editsection_form($action, $customdata); 1177 } 1178 1179 /** 1180 * Allows course format to execute code on moodle_page::set_course() 1181 * 1182 * @param moodle_page $page instance of page calling set_course 1183 */ 1184 public function page_set_course(moodle_page $page) { 1185 } 1186 1187 /** 1188 * Allows course format to execute code on moodle_page::set_cm() 1189 * 1190 * Current module can be accessed as $page->cm (returns instance of cm_info) 1191 * 1192 * @param moodle_page $page instance of page calling set_cm 1193 */ 1194 public function page_set_cm(moodle_page $page) { 1195 } 1196 1197 /** 1198 * Course-specific information to be output on any course page (usually above navigation bar) 1199 * 1200 * Example of usage: 1201 * define 1202 * class format_FORMATNAME_XXX implements renderable {} 1203 * 1204 * create format renderer in course/format/FORMATNAME/renderer.php, define rendering function: 1205 * class format_FORMATNAME_renderer extends plugin_renderer_base { 1206 * protected function render_format_FORMATNAME_XXX(format_FORMATNAME_XXX $xxx) { 1207 * return html_writer::tag('div', 'This is my header/footer'); 1208 * } 1209 * } 1210 * 1211 * Return instance of format_FORMATNAME_XXX in this function, the appropriate method from 1212 * plugin renderer will be called 1213 * 1214 * @return null|renderable null for no output or object with data for plugin renderer 1215 */ 1216 public function course_header() { 1217 return null; 1218 } 1219 1220 /** 1221 * Course-specific information to be output on any course page (usually in the beginning of 1222 * standard footer) 1223 * 1224 * See course_format::course_header() for usage 1225 * 1226 * @return null|renderable null for no output or object with data for plugin renderer 1227 */ 1228 public function course_footer() { 1229 return null; 1230 } 1231 1232 /** 1233 * Course-specific information to be output immediately above content on any course page 1234 * 1235 * See course_format::course_header() for usage 1236 * 1237 * @return null|renderable null for no output or object with data for plugin renderer 1238 */ 1239 public function course_content_header() { 1240 return null; 1241 } 1242 1243 /** 1244 * Course-specific information to be output immediately below content on any course page 1245 * 1246 * See course_format::course_header() for usage 1247 * 1248 * @return null|renderable null for no output or object with data for plugin renderer 1249 */ 1250 public function course_content_footer() { 1251 return null; 1252 } 1253 1254 /** 1255 * Returns instance of page renderer used by this plugin 1256 * 1257 * @param moodle_page $page 1258 * @return renderer_base 1259 */ 1260 public function get_renderer(moodle_page $page) { 1261 try { 1262 $renderer = $page->get_renderer('format_'. $this->get_format()); 1263 } catch (moodle_exception $e) { 1264 $formatname = $this->get_format(); 1265 $expectedrenderername = 'format_'. $this->get_format() . '\output\renderer'; 1266 debugging( 1267 "The '{$formatname}' course format does not define the {$expectedrenderername} renderer class. This is required since Moodle 4.0.", 1268 DEBUG_DEVELOPER 1269 ); 1270 $renderer = new legacy_renderer($page, null); 1271 } 1272 1273 return $renderer; 1274 } 1275 1276 /** 1277 * Returns instance of output component used by this plugin 1278 * 1279 * @throws coding_exception if the format class does not extends the original core one. 1280 * @param string $outputname the element to render (section, activity...) 1281 * @return string the output component classname 1282 */ 1283 public function get_output_classname(string $outputname): string { 1284 // The core output class. 1285 $baseclass = "core_courseformat\\output\\local\\$outputname"; 1286 1287 // Look in this format and any parent formats before we get to the base one. 1288 $classes = array_merge([get_class($this)], class_parents($this)); 1289 foreach ($classes as $component) { 1290 if ($component === self::class) { 1291 break; 1292 } 1293 1294 // Because course formats are in the root namespace, there is no need to process the 1295 // class name - it is already a Frankenstyle component name beginning 'format_'. 1296 1297 // Check if there is a specific class in this format. 1298 $outputclass = "$component\\output\\courseformat\\$outputname"; 1299 if (class_exists($outputclass)) { 1300 // Check that the outputclass is a subclass of the base class. 1301 if (!is_subclass_of($outputclass, $baseclass)) { 1302 throw new coding_exception("The \"$outputclass\" must extend \"$baseclass\""); 1303 } 1304 return $outputclass; 1305 } 1306 } 1307 1308 return $baseclass; 1309 } 1310 1311 /** 1312 * Returns true if the specified section is current 1313 * 1314 * By default we analyze $course->marker 1315 * 1316 * @param int|stdClass|section_info $section 1317 * @return bool 1318 */ 1319 public function is_section_current($section) { 1320 if (is_object($section)) { 1321 $sectionnum = $section->section; 1322 } else { 1323 $sectionnum = $section; 1324 } 1325 return ($sectionnum && ($course = $this->get_course()) && $course->marker == $sectionnum); 1326 } 1327 1328 /** 1329 * Returns if an specific section is visible to the current user. 1330 * 1331 * Formats can overrride this method to implement any special section logic. 1332 * 1333 * @param section_info $section the section modinfo 1334 * @return bool; 1335 */ 1336 public function is_section_visible(section_info $section): bool { 1337 // Previous to Moodle 4.0 thas logic was hardcoded. To prevent errors in the contrib plugins 1338 // the default logic is the same required for topics and weeks format and still uses 1339 // a "hiddensections" format setting. 1340 $course = $this->get_course(); 1341 $hidesections = $course->hiddensections ?? true; 1342 // Show the section if the user is permitted to access it, OR if it's not available 1343 // but there is some available info text which explains the reason & should display, 1344 // OR it is hidden but the course has a setting to display hidden sections as unavailable. 1345 return $section->uservisible || 1346 ($section->visible && !$section->available && !empty($section->availableinfo)) || 1347 (!$section->visible && !$hidesections); 1348 } 1349 1350 /** 1351 * return true if the course editor must be displayed. 1352 * 1353 * @param array|null $capabilities array of capabilities a user needs to have to see edit controls in general. 1354 * If null or not specified, the user needs to have 'moodle/course:manageactivities'. 1355 * @return bool true if edit controls must be displayed 1356 */ 1357 public function show_editor(?array $capabilities = ['moodle/course:manageactivities']): bool { 1358 global $PAGE; 1359 $course = $this->get_course(); 1360 $coursecontext = context_course::instance($course->id); 1361 if ($capabilities === null) { 1362 $capabilities = ['moodle/course:manageactivities']; 1363 } 1364 return $PAGE->user_is_editing() && has_all_capabilities($capabilities, $coursecontext); 1365 } 1366 1367 /** 1368 * Allows to specify for modinfo that section is not available even when it is visible and conditionally available. 1369 * 1370 * Note: affected user can be retrieved as: $section->modinfo->userid 1371 * 1372 * Course format plugins can override the method to change the properties $available and $availableinfo that were 1373 * calculated by conditional availability. 1374 * To make section unavailable set: 1375 * $available = false; 1376 * To make unavailable section completely hidden set: 1377 * $availableinfo = ''; 1378 * To make unavailable section visible with availability message set: 1379 * $availableinfo = get_string('sectionhidden', 'format_xxx'); 1380 * 1381 * @param section_info $section 1382 * @param bool $available the 'available' propery of the section_info as it was evaluated by conditional availability. 1383 * Can be changed by the method but 'false' can not be overridden by 'true'. 1384 * @param string $availableinfo the 'availableinfo' propery of the section_info as it was evaluated by conditional availability. 1385 * Can be changed by the method 1386 */ 1387 public function section_get_available_hook(section_info $section, &$available, &$availableinfo) { 1388 } 1389 1390 /** 1391 * Whether this format allows to delete sections 1392 * 1393 * If format supports deleting sections it is also recommended to define language string 1394 * 'deletesection' inside the format. 1395 * 1396 * Do not call this function directly, instead use course_can_delete_section() 1397 * 1398 * @param int|stdClass|section_info $section 1399 * @return bool 1400 */ 1401 public function can_delete_section($section) { 1402 return false; 1403 } 1404 1405 /** 1406 * Deletes a section 1407 * 1408 * Do not call this function directly, instead call course_delete_section() 1409 * 1410 * @param int|stdClass|section_info $section 1411 * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it. 1412 * @return bool whether section was deleted 1413 */ 1414 public function delete_section($section, $forcedeleteifnotempty = false) { 1415 global $DB; 1416 if (!$this->uses_sections()) { 1417 // Not possible to delete section if sections are not used. 1418 return false; 1419 } 1420 if (!is_object($section)) { 1421 $section = $DB->get_record('course_sections', array('course' => $this->get_courseid(), 'section' => $section), 1422 'id,section,sequence,summary'); 1423 } 1424 if (!$section || !$section->section) { 1425 // Not possible to delete 0-section. 1426 return false; 1427 } 1428 1429 if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) { 1430 return false; 1431 } 1432 1433 $course = $this->get_course(); 1434 1435 // Remove the marker if it points to this section. 1436 if ($section->section == $course->marker) { 1437 course_set_marker($course->id, 0); 1438 } 1439 1440 $lastsection = $DB->get_field_sql('SELECT max(section) from {course_sections} 1441 WHERE course = ?', array($course->id)); 1442 1443 // Find out if we need to descrease the 'numsections' property later. 1444 $courseformathasnumsections = array_key_exists('numsections', 1445 $this->get_format_options()); 1446 $decreasenumsections = $courseformathasnumsections && ($section->section <= $course->numsections); 1447 1448 // Move the section to the end. 1449 move_section_to($course, $section->section, $lastsection, true); 1450 1451 // Delete all modules from the section. 1452 foreach (preg_split('/,/', $section->sequence, -1, PREG_SPLIT_NO_EMPTY) as $cmid) { 1453 course_delete_module($cmid); 1454 } 1455 1456 // Delete section and it's format options. 1457 $DB->delete_records('course_format_options', array('sectionid' => $section->id)); 1458 $DB->delete_records('course_sections', array('id' => $section->id)); 1459 // Invalidate the section cache by given section id. 1460 course_modinfo::purge_course_section_cache_by_id($course->id, $section->id); 1461 // Partial rebuild section cache that has been purged. 1462 rebuild_course_cache($course->id, true, true); 1463 1464 // Delete section summary files. 1465 $context = \context_course::instance($course->id); 1466 $fs = get_file_storage(); 1467 $fs->delete_area_files($context->id, 'course', 'section', $section->id); 1468 1469 // Descrease 'numsections' if needed. 1470 if ($decreasenumsections) { 1471 $this->update_course_format_options(array('numsections' => $course->numsections - 1)); 1472 } 1473 1474 return true; 1475 } 1476 1477 /** 1478 * Prepares the templateable object to display section name 1479 * 1480 * @param \section_info|\stdClass $section 1481 * @param bool $linkifneeded 1482 * @param bool $editable 1483 * @param null|lang_string|string $edithint 1484 * @param null|lang_string|string $editlabel 1485 * @return \core\output\inplace_editable 1486 */ 1487 public function inplace_editable_render_section_name($section, $linkifneeded = true, 1488 $editable = null, $edithint = null, $editlabel = null) { 1489 global $USER, $CFG; 1490 require_once($CFG->dirroot.'/course/lib.php'); 1491 1492 if ($editable === null) { 1493 $editable = !empty($USER->editing) && has_capability('moodle/course:update', 1494 context_course::instance($section->course)); 1495 } 1496 1497 $displayvalue = $title = get_section_name($section->course, $section); 1498 if ($linkifneeded) { 1499 // Display link under the section name if the course format setting is to display one section per page. 1500 $url = course_get_url($section->course, $section->section, array('navigation' => true)); 1501 if ($url) { 1502 $displayvalue = html_writer::link($url, $title); 1503 } 1504 $itemtype = 'sectionname'; 1505 } else { 1506 // If $linkifneeded==false, we never display the link (this is used when rendering the section header). 1507 // Itemtype 'sectionnamenl' (nl=no link) will tell the callback that link should not be rendered - 1508 // there is no other way callback can know where we display the section name. 1509 $itemtype = 'sectionnamenl'; 1510 } 1511 if (empty($edithint)) { 1512 $edithint = new lang_string('editsectionname'); 1513 } 1514 if (empty($editlabel)) { 1515 $editlabel = new lang_string('newsectionname', '', $title); 1516 } 1517 1518 return new \core\output\inplace_editable('format_' . $this->format, $itemtype, $section->id, $editable, 1519 $displayvalue, $section->name, $edithint, $editlabel); 1520 } 1521 1522 /** 1523 * Updates the value in the database and modifies this object respectively. 1524 * 1525 * ALWAYS check user permissions before performing an update! Throw exceptions if permissions are not sufficient 1526 * or value is not legit. 1527 * 1528 * @param stdClass $section 1529 * @param string $itemtype 1530 * @param mixed $newvalue 1531 * @return \core\output\inplace_editable 1532 */ 1533 public function inplace_editable_update_section_name($section, $itemtype, $newvalue) { 1534 if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') { 1535 $context = context_course::instance($section->course); 1536 external_api::validate_context($context); 1537 require_capability('moodle/course:update', $context); 1538 1539 $newtitle = clean_param($newvalue, PARAM_TEXT); 1540 if (strval($section->name) !== strval($newtitle)) { 1541 course_update_section($section->course, $section, array('name' => $newtitle)); 1542 } 1543 return $this->inplace_editable_render_section_name($section, ($itemtype === 'sectionname'), true); 1544 } 1545 } 1546 1547 1548 /** 1549 * Returns the default end date value based on the start date. 1550 * 1551 * This is the default implementation for course formats, it is based on 1552 * moodlecourse/courseduration setting. Course formats like format_weeks for 1553 * example can overwrite this method and return a value based on their internal options. 1554 * 1555 * @param moodleform $mform 1556 * @param array $fieldnames The form - field names mapping. 1557 * @return int 1558 */ 1559 public function get_default_course_enddate($mform, $fieldnames = array()) { 1560 1561 if (empty($fieldnames)) { 1562 $fieldnames = array('startdate' => 'startdate'); 1563 } 1564 1565 $startdate = $this->get_form_start_date($mform, $fieldnames); 1566 $courseduration = intval(get_config('moodlecourse', 'courseduration')); 1567 if (!$courseduration) { 1568 // Default, it should be already set during upgrade though. 1569 $courseduration = YEARSECS; 1570 } 1571 1572 return $startdate + $courseduration; 1573 } 1574 1575 /** 1576 * Indicates whether the course format supports the creation of the Announcements forum. 1577 * 1578 * For course format plugin developers, please override this to return true if you want the Announcements forum 1579 * to be created upon course creation. 1580 * 1581 * @return bool 1582 */ 1583 public function supports_news() { 1584 // For backwards compatibility, check if default blocks include the news_items block. 1585 $defaultblocks = $this->get_default_blocks(); 1586 foreach ($defaultblocks as $blocks) { 1587 if (in_array('news_items', $blocks)) { 1588 return true; 1589 } 1590 } 1591 // Return false by default. 1592 return false; 1593 } 1594 1595 /** 1596 * Get the start date value from the course settings page form. 1597 * 1598 * @param moodleform $mform 1599 * @param array $fieldnames The form - field names mapping. 1600 * @return int 1601 */ 1602 protected function get_form_start_date($mform, $fieldnames) { 1603 $startdate = $mform->getElementValue($fieldnames['startdate']); 1604 return $mform->getElement($fieldnames['startdate'])->exportValue($startdate); 1605 } 1606 1607 /** 1608 * Returns whether this course format allows the activity to 1609 * have "triple visibility state" - visible always, hidden on course page but available, hidden. 1610 * 1611 * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) 1612 * @param stdClass|section_info $section section where this module is located or will be added to 1613 * @return bool 1614 */ 1615 public function allow_stealth_module_visibility($cm, $section) { 1616 return false; 1617 } 1618 1619 /** 1620 * Callback used in WS core_course_edit_section when teacher performs an AJAX action on a section (show/hide) 1621 * 1622 * Access to the course is already validated in the WS but the callback has to make sure 1623 * that particular action is allowed by checking capabilities 1624 * 1625 * Course formats should register 1626 * 1627 * @param stdClass|section_info $section 1628 * @param string $action 1629 * @param int $sr the section return 1630 * @return null|array|stdClass any data for the Javascript post-processor (must be json-encodeable) 1631 */ 1632 public function section_action($section, $action, $sr) { 1633 global $PAGE; 1634 if (!$this->uses_sections() || !$section->section) { 1635 // No section actions are allowed if course format does not support sections. 1636 // No actions are allowed on the 0-section by default (overwrite in course format if needed). 1637 throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action)); 1638 } 1639 1640 $course = $this->get_course(); 1641 $coursecontext = context_course::instance($course->id); 1642 $modinfo = $this->get_modinfo(); 1643 $renderer = $this->get_renderer($PAGE); 1644 1645 if (!($section instanceof section_info)) { 1646 $section = $modinfo->get_section_info($section->section); 1647 } 1648 1649 if ($sr) { 1650 $this->set_section_number($sr); 1651 } 1652 1653 switch($action) { 1654 case 'hide': 1655 case 'show': 1656 require_capability('moodle/course:sectionvisibility', $coursecontext); 1657 $visible = ($action === 'hide') ? 0 : 1; 1658 course_update_section($course, $section, array('visible' => $visible)); 1659 break; 1660 case 'refresh': 1661 return [ 1662 'content' => $renderer->course_section_updated($this, $section), 1663 ]; 1664 default: 1665 throw new moodle_exception('sectionactionnotsupported', 'core', null, s($action)); 1666 } 1667 1668 return ['modules' => $this->get_section_modules_updated($section)]; 1669 } 1670 1671 /** 1672 * Return an array with all section modules content. 1673 * 1674 * This method is used in section_action method to generate the updated modules content 1675 * after a modinfo change. 1676 * 1677 * @param section_info $section the section 1678 * @return string[] the full modules content. 1679 */ 1680 protected function get_section_modules_updated(section_info $section): array { 1681 global $PAGE; 1682 1683 $modules = []; 1684 1685 if (!$this->uses_sections() || !$section->section) { 1686 return $modules; 1687 } 1688 1689 // Load the cmlist output from the updated modinfo. 1690 $renderer = $this->get_renderer($PAGE); 1691 $modinfo = $this->get_modinfo(); 1692 $coursesections = $modinfo->sections; 1693 if (array_key_exists($section->section, $coursesections)) { 1694 foreach ($coursesections[$section->section] as $cmid) { 1695 $cm = $modinfo->get_cm($cmid); 1696 $modules[] = $renderer->course_section_updated_cm_item($this, $section, $cm); 1697 } 1698 } 1699 return $modules; 1700 } 1701 1702 /** 1703 * Return the plugin config settings for external functions, 1704 * in some cases the configs will need formatting or be returned only if the current user has some capabilities enabled. 1705 * 1706 * @return array the list of configs 1707 * @since Moodle 3.5 1708 */ 1709 public function get_config_for_external() { 1710 return array(); 1711 } 1712 1713 /** 1714 * Course deletion hook. 1715 * 1716 * Format plugins can override this method to clean any format specific data and dependencies. 1717 * 1718 */ 1719 public function delete_format_data() { 1720 global $DB; 1721 $course = $this->get_course(); 1722 // By default, formats store some most display specifics in a user preference. 1723 $DB->delete_records('user_preferences', ['name' => 'coursesectionspreferences_' . $course->id]); 1724 } 1725 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body