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