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