See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * This file contains main class for the course format Weeks 19 * 20 * @since Moodle 2.0 21 * @package format_weeks 22 * @copyright 2009 Sam Hemelryk 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 require_once($CFG->dirroot. '/course/format/lib.php'); 28 require_once($CFG->dirroot. '/course/lib.php'); 29 30 /** 31 * Main class for the Weeks course format 32 * 33 * @package format_weeks 34 * @copyright 2012 Marina Glancy 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class format_weeks extends core_courseformat\base { 38 39 /** 40 * Returns true if this course format uses sections 41 * 42 * @return bool 43 */ 44 public function uses_sections() { 45 return true; 46 } 47 48 public function uses_course_index() { 49 return true; 50 } 51 52 public function uses_indentation(): bool { 53 return (get_config('format_weeks', 'indentation')) ? true : false; 54 } 55 56 /** 57 * Generate the title for this section page 58 * @return string the page title 59 */ 60 public function page_title(): string { 61 return get_string('weeklyoutline'); 62 } 63 64 /** 65 * Returns the display name of the given section that the course prefers. 66 * 67 * @param int|stdClass $section Section object from database or just field section.section 68 * @return string Display name that the course format prefers, e.g. "Topic 2" 69 */ 70 public function get_section_name($section) { 71 $section = $this->get_section($section); 72 if ((string)$section->name !== '') { 73 // Return the name the user set. 74 return format_string($section->name, true, array('context' => context_course::instance($this->courseid))); 75 } else { 76 return $this->get_default_section_name($section); 77 } 78 } 79 80 /** 81 * Returns the default section name for the weekly course format. 82 * 83 * If the section number is 0, it will use the string with key = section0name from the course format's lang file. 84 * Otherwise, the default format of "[start date] - [end date]" will be returned. 85 * 86 * @param stdClass $section Section object from database or just field course_sections section 87 * @return string The default value for the section name. 88 */ 89 public function get_default_section_name($section) { 90 if ($section->section == 0) { 91 // Return the general section. 92 return get_string('section0name', 'format_weeks'); 93 } else { 94 $dates = $this->get_section_dates($section); 95 96 // We subtract 24 hours for display purposes. 97 $dates->end = ($dates->end - 86400); 98 99 $dateformat = get_string('strftimedateshort'); 100 $weekday = userdate($dates->start, $dateformat); 101 $endweekday = userdate($dates->end, $dateformat); 102 return $weekday.' - '.$endweekday; 103 } 104 } 105 106 /** 107 * Returns the name for the highlighted section. 108 * 109 * @return string The name for the highlighted section based on the given course format. 110 */ 111 public function get_section_highlighted_name(): string { 112 return get_string('currentsection', 'format_weeks'); 113 } 114 115 /** 116 * The URL to use for the specified course (with section) 117 * 118 * @param int|stdClass $section Section object from database or just field course_sections.section 119 * if omitted the course view page is returned 120 * @param array $options options for view URL. At the moment core uses: 121 * 'navigation' (bool) if true and section has no separate page, the function returns null 122 * 'sr' (int) used by multipage formats to specify to which section to return 123 * @return null|moodle_url 124 */ 125 public function get_view_url($section, $options = array()) { 126 global $CFG; 127 $course = $this->get_course(); 128 $url = new moodle_url('/course/view.php', array('id' => $course->id)); 129 130 $sr = null; 131 if (array_key_exists('sr', $options)) { 132 $sr = $options['sr']; 133 } 134 if (is_object($section)) { 135 $sectionno = $section->section; 136 } else { 137 $sectionno = $section; 138 } 139 if ($sectionno !== null) { 140 if ($sr !== null) { 141 if ($sr) { 142 $usercoursedisplay = COURSE_DISPLAY_MULTIPAGE; 143 $sectionno = $sr; 144 } else { 145 $usercoursedisplay = COURSE_DISPLAY_SINGLEPAGE; 146 } 147 } else { 148 $usercoursedisplay = $course->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE; 149 } 150 if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) { 151 $url->param('section', $sectionno); 152 } else { 153 if (empty($CFG->linkcoursesections) && !empty($options['navigation'])) { 154 return null; 155 } 156 $url->set_anchor('section-'.$sectionno); 157 } 158 } 159 return $url; 160 } 161 162 /** 163 * Returns the information about the ajax support in the given source format 164 * 165 * The returned object's property (boolean)capable indicates that 166 * the course format supports Moodle course ajax features. 167 * 168 * @return stdClass 169 */ 170 public function supports_ajax() { 171 $ajaxsupport = new stdClass(); 172 $ajaxsupport->capable = true; 173 return $ajaxsupport; 174 } 175 176 public function supports_components() { 177 return true; 178 } 179 180 /** 181 * Loads all of the course sections into the navigation 182 * 183 * @param global_navigation $navigation 184 * @param navigation_node $node The course node within the navigation 185 */ 186 public function extend_course_navigation($navigation, navigation_node $node) { 187 global $PAGE; 188 // if section is specified in course/view.php, make sure it is expanded in navigation 189 if ($navigation->includesectionnum === false) { 190 $selectedsection = optional_param('section', null, PARAM_INT); 191 if ($selectedsection !== null && (!defined('AJAX_SCRIPT') || AJAX_SCRIPT == '0') && 192 $PAGE->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) { 193 $navigation->includesectionnum = $selectedsection; 194 } 195 } 196 parent::extend_course_navigation($navigation, $node); 197 198 // We want to remove the general section if it is empty. 199 $modinfo = get_fast_modinfo($this->get_course()); 200 $sections = $modinfo->get_sections(); 201 if (!isset($sections[0])) { 202 // The general section is empty to find the navigation node for it we need to get its ID. 203 $section = $modinfo->get_section_info(0); 204 $generalsection = $node->get($section->id, navigation_node::TYPE_SECTION); 205 if ($generalsection) { 206 // We found the node - now remove it. 207 $generalsection->remove(); 208 } 209 } 210 } 211 212 /** 213 * Custom action after section has been moved in AJAX mode 214 * 215 * Used in course/rest.php 216 * 217 * @return array This will be passed in ajax respose 218 */ 219 function ajax_section_move() { 220 global $PAGE; 221 $titles = array(); 222 $current = -1; 223 $course = $this->get_course(); 224 $modinfo = get_fast_modinfo($course); 225 $renderer = $this->get_renderer($PAGE); 226 if ($renderer && ($sections = $modinfo->get_section_info_all())) { 227 foreach ($sections as $number => $section) { 228 $titles[$number] = $renderer->section_title($section, $course); 229 if ($this->is_section_current($section)) { 230 $current = $number; 231 } 232 } 233 } 234 return array('sectiontitles' => $titles, 'current' => $current, 'action' => 'move'); 235 } 236 237 /** 238 * Returns the list of blocks to be automatically added for the newly created course 239 * 240 * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT 241 * each of values is an array of block names (for left and right side columns) 242 */ 243 public function get_default_blocks() { 244 return array( 245 BLOCK_POS_LEFT => array(), 246 BLOCK_POS_RIGHT => array() 247 ); 248 } 249 250 /** 251 * Definitions of the additional options that this course format uses for course 252 * 253 * Weeks format uses the following options: 254 * - coursedisplay 255 * - hiddensections 256 * - automaticenddate 257 * 258 * @param bool $foreditform 259 * @return array of options 260 */ 261 public function course_format_options($foreditform = false) { 262 static $courseformatoptions = false; 263 if ($courseformatoptions === false) { 264 $courseconfig = get_config('moodlecourse'); 265 $courseformatoptions = array( 266 'hiddensections' => array( 267 'default' => $courseconfig->hiddensections, 268 'type' => PARAM_INT, 269 ), 270 'coursedisplay' => array( 271 'default' => $courseconfig->coursedisplay ?? COURSE_DISPLAY_SINGLEPAGE, 272 'type' => PARAM_INT, 273 ), 274 'automaticenddate' => array( 275 'default' => 1, 276 'type' => PARAM_BOOL, 277 ), 278 ); 279 } 280 if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) { 281 $courseformatoptionsedit = array( 282 'hiddensections' => array( 283 'label' => new lang_string('hiddensections'), 284 'help' => 'hiddensections', 285 'help_component' => 'moodle', 286 'element_type' => 'select', 287 'element_attributes' => array( 288 array( 289 0 => new lang_string('hiddensectionscollapsed'), 290 1 => new lang_string('hiddensectionsinvisible') 291 ) 292 ), 293 ), 294 'coursedisplay' => array( 295 'label' => new lang_string('coursedisplay'), 296 'element_type' => 'select', 297 'element_attributes' => array( 298 array( 299 COURSE_DISPLAY_SINGLEPAGE => new lang_string('coursedisplay_single'), 300 COURSE_DISPLAY_MULTIPAGE => new lang_string('coursedisplay_multi') 301 ) 302 ), 303 'help' => 'coursedisplay', 304 'help_component' => 'moodle', 305 ), 306 'automaticenddate' => array( 307 'label' => new lang_string('automaticenddate', 'format_weeks'), 308 'help' => 'automaticenddate', 309 'help_component' => 'format_weeks', 310 'element_type' => 'advcheckbox', 311 ) 312 ); 313 $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit); 314 } 315 return $courseformatoptions; 316 } 317 318 /** 319 * Adds format options elements to the course/section edit form. 320 * 321 * This function is called from {@link course_edit_form::definition_after_data()}. 322 * 323 * @param MoodleQuickForm $mform form the elements are added to. 324 * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form. 325 * @return array array of references to the added form elements. 326 */ 327 public function create_edit_form_elements(&$mform, $forsection = false) { 328 global $COURSE; 329 $elements = parent::create_edit_form_elements($mform, $forsection); 330 331 if (!$forsection && (empty($COURSE->id) || $COURSE->id == SITEID)) { 332 // Add "numsections" element to the create course form - it will force new course to be prepopulated 333 // with empty sections. 334 // The "Number of sections" option is no longer available when editing course, instead teachers should 335 // delete and add sections when needed. 336 $courseconfig = get_config('moodlecourse'); 337 $max = (int)$courseconfig->maxsections; 338 $element = $mform->addElement('select', 'numsections', get_string('numberweeks'), range(0, $max ?: 52)); 339 $mform->setType('numsections', PARAM_INT); 340 if (is_null($mform->getElementValue('numsections'))) { 341 $mform->setDefault('numsections', $courseconfig->numsections); 342 } 343 array_unshift($elements, $element); 344 } 345 346 // Re-order things. 347 $mform->insertElementBefore($mform->removeElement('automaticenddate', false), 'idnumber'); 348 $mform->disabledIf('enddate', 'automaticenddate', 'checked'); 349 foreach ($elements as $key => $element) { 350 if ($element->getName() == 'automaticenddate') { 351 unset($elements[$key]); 352 } 353 } 354 355 return $elements; 356 } 357 358 /** 359 * Updates format options for a course 360 * 361 * In case if course format was changed to 'weeks', we try to copy options 362 * 'coursedisplay', 'numsections' and 'hiddensections' from the previous format. 363 * If previous course format did not have 'numsections' option, we populate it with the 364 * current number of sections 365 * 366 * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data 367 * @param stdClass $oldcourse if this function is called from {@link update_course()} 368 * this object contains information about the course before update 369 * @return bool whether there were any changes to the options values 370 */ 371 public function update_course_format_options($data, $oldcourse = null) { 372 global $DB; 373 $data = (array)$data; 374 if ($oldcourse !== null) { 375 $oldcourse = (array)$oldcourse; 376 $options = $this->course_format_options(); 377 foreach ($options as $key => $unused) { 378 if (!array_key_exists($key, $data)) { 379 if (array_key_exists($key, $oldcourse)) { 380 $data[$key] = $oldcourse[$key]; 381 } 382 } 383 } 384 } 385 return $this->update_format_options($data); 386 } 387 388 /** 389 * Return the start and end date of the passed section 390 * 391 * @param int|stdClass|section_info $section section to get the dates for 392 * @param int $startdate Force course start date, useful when the course is not yet created 393 * @return stdClass property start for startdate, property end for enddate 394 */ 395 public function get_section_dates($section, $startdate = false) { 396 global $USER; 397 398 if ($startdate === false) { 399 $course = $this->get_course(); 400 $userdates = course_get_course_dates_for_user_id($course, $USER->id); 401 $startdate = $userdates['start']; 402 } 403 404 if (is_object($section)) { 405 $sectionnum = $section->section; 406 } else { 407 $sectionnum = $section; 408 } 409 410 // Create a DateTime object for the start date. 411 $startdateobj = new DateTime("@$startdate"); 412 $startdateobj->setTimezone(core_date::get_user_timezone_object()); 413 414 // Calculate the interval for one week. 415 $oneweekinterval = new DateInterval('P7D'); 416 417 // Calculate the interval for the specified number of sections. 418 for ($i = 1; $i < $sectionnum; $i++) { 419 $startdateobj->add($oneweekinterval); 420 } 421 422 // Calculate the end date. 423 $enddateobj = clone $startdateobj; 424 $enddateobj->add($oneweekinterval); 425 426 $dates = new stdClass(); 427 $dates->start = $startdateobj->getTimestamp(); 428 $dates->end = $enddateobj->getTimestamp(); 429 430 return $dates; 431 } 432 433 /** 434 * Returns true if the specified week is current 435 * 436 * @param int|stdClass|section_info $section 437 * @return bool 438 */ 439 public function is_section_current($section) { 440 if (is_object($section)) { 441 $sectionnum = $section->section; 442 } else { 443 $sectionnum = $section; 444 } 445 if ($sectionnum < 1) { 446 return false; 447 } 448 $timenow = time(); 449 $dates = $this->get_section_dates($section); 450 return (($timenow >= $dates->start) && ($timenow < $dates->end)); 451 } 452 453 /** 454 * Whether this format allows to delete sections 455 * 456 * Do not call this function directly, instead use {@link course_can_delete_section()} 457 * 458 * @param int|stdClass|section_info $section 459 * @return bool 460 */ 461 public function can_delete_section($section) { 462 return true; 463 } 464 465 /** 466 * Prepares the templateable object to display section name 467 * 468 * @param \section_info|\stdClass $section 469 * @param bool $linkifneeded 470 * @param bool $editable 471 * @param null|lang_string|string $edithint 472 * @param null|lang_string|string $editlabel 473 * @return \core\output\inplace_editable 474 */ 475 public function inplace_editable_render_section_name($section, $linkifneeded = true, 476 $editable = null, $edithint = null, $editlabel = null) { 477 if (empty($edithint)) { 478 $edithint = new lang_string('editsectionname', 'format_weeks'); 479 } 480 if (empty($editlabel)) { 481 $title = get_section_name($section->course, $section); 482 $editlabel = new lang_string('newsectionname', 'format_weeks', $title); 483 } 484 return parent::inplace_editable_render_section_name($section, $linkifneeded, $editable, $edithint, $editlabel); 485 } 486 487 /** 488 * Returns the default end date for weeks course format. 489 * 490 * @param moodleform $mform 491 * @param array $fieldnames The form - field names mapping. 492 * @return int 493 */ 494 public function get_default_course_enddate($mform, $fieldnames = array()) { 495 496 if (empty($fieldnames['startdate'])) { 497 $fieldnames['startdate'] = 'startdate'; 498 } 499 500 if (empty($fieldnames['numsections'])) { 501 $fieldnames['numsections'] = 'numsections'; 502 } 503 504 $startdate = $this->get_form_start_date($mform, $fieldnames); 505 if ($mform->elementExists($fieldnames['numsections'])) { 506 $numsections = $mform->getElementValue($fieldnames['numsections']); 507 $numsections = $mform->getElement($fieldnames['numsections'])->exportValue($numsections); 508 } else if ($this->get_courseid()) { 509 // For existing courses get the number of sections. 510 $numsections = $this->get_last_section_number(); 511 } else { 512 // Fallback to the default value for new courses. 513 $numsections = get_config('moodlecourse', $fieldnames['numsections']); 514 } 515 516 // Final week's last day. 517 $dates = $this->get_section_dates(intval($numsections), $startdate); 518 return $dates->end; 519 } 520 521 /** 522 * Indicates whether the course format supports the creation of a news forum. 523 * 524 * @return bool 525 */ 526 public function supports_news() { 527 return true; 528 } 529 530 /** 531 * Returns whether this course format allows the activity to 532 * have "triple visibility state" - visible always, hidden on course page but available, hidden. 533 * 534 * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module) 535 * @param stdClass|section_info $section section where this module is located or will be added to 536 * @return bool 537 */ 538 public function allow_stealth_module_visibility($cm, $section) { 539 // Allow the third visibility state inside visible sections or in section 0. 540 return !$section->section || $section->visible; 541 } 542 543 public function section_action($section, $action, $sr) { 544 global $PAGE; 545 546 // Call the parent method and return the new content for .section_availability element. 547 $rv = parent::section_action($section, $action, $sr); 548 $renderer = $PAGE->get_renderer('format_weeks'); 549 550 if (!($section instanceof section_info)) { 551 $modinfo = course_modinfo::instance($this->courseid); 552 $section = $modinfo->get_section_info($section->section); 553 } 554 $elementclass = $this->get_output_classname('content\\section\\availability'); 555 $availability = new $elementclass($this, $section); 556 557 $rv['section_availability'] = $renderer->render($availability); 558 559 return $rv; 560 } 561 562 /** 563 * Updates the end date for a course in weeks format if option automaticenddate is set. 564 * 565 * This method is called from event observers and it can not use any modinfo or format caches because 566 * events are triggered before the caches are reset. 567 * 568 * @param int $courseid 569 */ 570 public static function update_end_date($courseid) { 571 global $DB, $COURSE; 572 573 // Use one DB query to retrieve necessary fields in course, value for automaticenddate and number of the last 574 // section. This query will also validate that the course is indeed in 'weeks' format. 575 $insql = "SELECT c.id, c.format, c.startdate, c.enddate, MAX(s.section) AS lastsection 576 FROM {course} c 577 JOIN {course_sections} s 578 ON s.course = c.id 579 WHERE c.format = :format 580 AND c.id = :courseid 581 GROUP BY c.id, c.format, c.startdate, c.enddate"; 582 $sql = "SELECT co.id, co.format, co.startdate, co.enddate, co.lastsection, fo.value AS automaticenddate 583 FROM ($insql) co 584 LEFT JOIN {course_format_options} fo 585 ON fo.courseid = co.id 586 AND fo.format = co.format 587 AND fo.name = :optionname 588 AND fo.sectionid = 0"; 589 $course = $DB->get_record_sql($sql, 590 ['optionname' => 'automaticenddate', 'format' => 'weeks', 'courseid' => $courseid]); 591 592 if (!$course) { 593 // Looks like it is a course in a different format, nothing to do here. 594 return; 595 } 596 597 // Create an instance of this class and mock the course object. 598 $format = new format_weeks('weeks', $courseid); 599 $format->course = $course; 600 601 // If automaticenddate is not specified take the default value. 602 if (!isset($course->automaticenddate)) { 603 $defaults = $format->course_format_options(); 604 $course->automaticenddate = $defaults['automaticenddate']['default']; 605 } 606 607 // Check that the course format for setting an automatic date is set. 608 if (!empty($course->automaticenddate)) { 609 // Get the final week's last day. 610 $dates = $format->get_section_dates((int)$course->lastsection); 611 612 // Set the course end date. 613 if ($course->enddate != $dates->end) { 614 $DB->set_field('course', 'enddate', $dates->end, array('id' => $course->id)); 615 if (isset($COURSE->id) && $COURSE->id == $courseid) { 616 $COURSE->enddate = $dates->end; 617 } 618 } 619 } 620 } 621 622 /** 623 * Return the plugin configs for external functions. 624 * 625 * @return array the list of configuration settings 626 * @since Moodle 3.5 627 */ 628 public function get_config_for_external() { 629 // Return everything (nothing to hide). 630 $formatoptions = $this->get_format_options(); 631 $formatoptions['indentation'] = get_config('format_weeks', 'indentation'); 632 return $formatoptions; 633 } 634 } 635 636 /** 637 * Implements callback inplace_editable() allowing to edit values in-place 638 * 639 * @param string $itemtype 640 * @param int $itemid 641 * @param mixed $newvalue 642 * @return \core\output\inplace_editable 643 */ 644 function format_weeks_inplace_editable($itemtype, $itemid, $newvalue) { 645 global $DB, $CFG; 646 require_once($CFG->dirroot . '/course/lib.php'); 647 if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') { 648 $section = $DB->get_record_sql( 649 'SELECT s.* FROM {course_sections} s JOIN {course} c ON s.course = c.id WHERE s.id = ? AND c.format = ?', 650 array($itemid, 'weeks'), MUST_EXIST); 651 return course_get_format($section->course)->inplace_editable_update_section_name($section, $itemtype, $newvalue); 652 } 653 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body