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 Topic
19 *
20 * @since     Moodle 2.0
21 * @package   format_topics
22 * @copyright 2009 Sam Hemelryk
23 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26defined('MOODLE_INTERNAL') || die();
27require_once($CFG->dirroot. '/course/format/lib.php');
28
29/**
30 * Main class for the Topics course format
31 *
32 * @package    format_topics
33 * @copyright  2012 Marina Glancy
34 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35 */
36class format_topics extends format_base {
37
38    /**
39     * Returns true if this course format uses sections
40     *
41     * @return bool
42     */
43    public function uses_sections() {
44        return true;
45    }
46
47    /**
48     * Returns the display name of the given section that the course prefers.
49     *
50     * Use section name is specified by user. Otherwise use default ("Topic #")
51     *
52     * @param int|stdClass $section Section object from database or just field section.section
53     * @return string Display name that the course format prefers, e.g. "Topic 2"
54     */
55    public function get_section_name($section) {
56        $section = $this->get_section($section);
57        if ((string)$section->name !== '') {
58            return format_string($section->name, true,
59                    array('context' => context_course::instance($this->courseid)));
60        } else {
61            return $this->get_default_section_name($section);
62        }
63    }
64
65    /**
66     * Returns the default section name for the topics course format.
67     *
68     * If the section number is 0, it will use the string with key = section0name from the course format's lang file.
69     * If the section number is not 0, the base implementation of format_base::get_default_section_name which uses
70     * the string with the key = 'sectionname' from the course format's lang file + the section number will be used.
71     *
72     * @param stdClass $section Section object from database or just field course_sections section
73     * @return string The default value for the section name.
74     */
75    public function get_default_section_name($section) {
76        if ($section->section == 0) {
77            // Return the general section.
78            return get_string('section0name', 'format_topics');
79        } else {
80            // Use format_base::get_default_section_name implementation which
81            // will display the section name in "Topic n" format.
82            return parent::get_default_section_name($section);
83        }
84    }
85
86    /**
87     * The URL to use for the specified course (with section)
88     *
89     * @param int|stdClass $section Section object from database or just field course_sections.section
90     *     if omitted the course view page is returned
91     * @param array $options options for view URL. At the moment core uses:
92     *     'navigation' (bool) if true and section has no separate page, the function returns null
93     *     'sr' (int) used by multipage formats to specify to which section to return
94     * @return null|moodle_url
95     */
96    public function get_view_url($section, $options = array()) {
97        global $CFG;
98        $course = $this->get_course();
99        $url = new moodle_url('/course/view.php', array('id' => $course->id));
100
101        $sr = null;
102        if (array_key_exists('sr', $options)) {
103            $sr = $options['sr'];
104        }
105        if (is_object($section)) {
106            $sectionno = $section->section;
107        } else {
108            $sectionno = $section;
109        }
110        if ($sectionno !== null) {
111            if ($sr !== null) {
112                if ($sr) {
113                    $usercoursedisplay = COURSE_DISPLAY_MULTIPAGE;
114                    $sectionno = $sr;
115                } else {
116                    $usercoursedisplay = COURSE_DISPLAY_SINGLEPAGE;
117                }
118            } else {
119                $usercoursedisplay = $course->coursedisplay;
120            }
121            if ($sectionno != 0 && $usercoursedisplay == COURSE_DISPLAY_MULTIPAGE) {
122                $url->param('section', $sectionno);
123            } else {
124                if (empty($CFG->linkcoursesections) && !empty($options['navigation'])) {
125                    return null;
126                }
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     *
136     * The returned object's property (boolean)capable indicates that
137     * the course format supports Moodle course ajax features.
138     *
139     * @return stdClass
140     */
141    public function supports_ajax() {
142        $ajaxsupport = new stdClass();
143        $ajaxsupport->capable = true;
144        return $ajaxsupport;
145    }
146
147    /**
148     * Loads all of the course sections into the navigation
149     *
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;
155        // if section is specified in course/view.php, make sure it is expanded in navigation
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        }
163
164        // check if there are callbacks to extend course navigation
165        parent::extend_course_navigation($navigation, $node);
166
167        // We want to remove the general section if it is empty.
168        $modinfo = get_fast_modinfo($this->get_course());
169        $sections = $modinfo->get_sections();
170        if (!isset($sections[0])) {
171            // The general section is empty to find the navigation node for it we need to get its ID.
172            $section = $modinfo->get_section_info(0);
173            $generalsection = $node->get($section->id, navigation_node::TYPE_SECTION);
174            if ($generalsection) {
175                // We found the node - now remove it.
176                $generalsection->remove();
177            }
178        }
179    }
180
181    /**
182     * Custom action after section has been moved in AJAX mode
183     *
184     * Used in course/rest.php
185     *
186     * @return array This will be passed in ajax respose
187     */
188    function ajax_section_move() {
189        global $PAGE;
190        $titles = array();
191        $course = $this->get_course();
192        $modinfo = get_fast_modinfo($course);
193        $renderer = $this->get_renderer($PAGE);
194        if ($renderer && ($sections = $modinfo->get_section_info_all())) {
195            foreach ($sections as $number => $section) {
196                $titles[$number] = $renderer->section_title($section, $course);
197            }
198        }
199        return array('sectiontitles' => $titles, 'action' => 'move');
200    }
201
202    /**
203     * Returns the list of blocks to be automatically added for the newly created course
204     *
205     * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
206     *     each of values is an array of block names (for left and right side columns)
207     */
208    public function get_default_blocks() {
209        return array(
210            BLOCK_POS_LEFT => array(),
211            BLOCK_POS_RIGHT => array()
212        );
213    }
214
215    /**
216     * Definitions of the additional options that this course format uses for course
217     *
218     * Topics format uses the following options:
219     * - coursedisplay
220     * - hiddensections
221     *
222     * @param bool $foreditform
223     * @return array of options
224     */
225    public function course_format_options($foreditform = false) {
226        static $courseformatoptions = false;
227        if ($courseformatoptions === false) {
228            $courseconfig = get_config('moodlecourse');
229            $courseformatoptions = array(
230                'hiddensections' => array(
231                    'default' => $courseconfig->hiddensections,
232                    'type' => PARAM_INT,
233                ),
234                'coursedisplay' => array(
235                    'default' => $courseconfig->coursedisplay,
236                    'type' => PARAM_INT,
237                ),
238            );
239        }
240        if ($foreditform && !isset($courseformatoptions['coursedisplay']['label'])) {
241            $courseformatoptionsedit = array(
242                'hiddensections' => array(
243                    'label' => new lang_string('hiddensections'),
244                    'help' => 'hiddensections',
245                    'help_component' => 'moodle',
246                    'element_type' => 'select',
247                    'element_attributes' => array(
248                        array(
249                            0 => new lang_string('hiddensectionscollapsed'),
250                            1 => new lang_string('hiddensectionsinvisible')
251                        )
252                    ),
253                ),
254                'coursedisplay' => array(
255                    'label' => new lang_string('coursedisplay'),
256                    'element_type' => 'select',
257                    'element_attributes' => array(
258                        array(
259                            COURSE_DISPLAY_SINGLEPAGE => new lang_string('coursedisplay_single'),
260                            COURSE_DISPLAY_MULTIPAGE => new lang_string('coursedisplay_multi')
261                        )
262                    ),
263                    'help' => 'coursedisplay',
264                    'help_component' => 'moodle',
265                )
266            );
267            $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
268        }
269        return $courseformatoptions;
270    }
271
272    /**
273     * Adds format options elements to the course/section edit form.
274     *
275     * This function is called from {@link course_edit_form::definition_after_data()}.
276     *
277     * @param MoodleQuickForm $mform form the elements are added to.
278     * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form.
279     * @return array array of references to the added form elements.
280     */
281    public function create_edit_form_elements(&$mform, $forsection = false) {
282        global $COURSE;
283        $elements = parent::create_edit_form_elements($mform, $forsection);
284
285        if (!$forsection && (empty($COURSE->id) || $COURSE->id == SITEID)) {
286            // Add "numsections" element to the create course form - it will force new course to be prepopulated
287            // with empty sections.
288            // The "Number of sections" option is no longer available when editing course, instead teachers should
289            // delete and add sections when needed.
290            $courseconfig = get_config('moodlecourse');
291            $max = (int)$courseconfig->maxsections;
292            $element = $mform->addElement('select', 'numsections', get_string('numberweeks'), range(0, $max ?: 52));
293            $mform->setType('numsections', PARAM_INT);
294            if (is_null($mform->getElementValue('numsections'))) {
295                $mform->setDefault('numsections', $courseconfig->numsections);
296            }
297            array_unshift($elements, $element);
298        }
299
300        return $elements;
301    }
302
303    /**
304     * Updates format options for a course
305     *
306     * In case if course format was changed to 'topics', we try to copy options
307     * 'coursedisplay' and 'hiddensections' from the previous format.
308     *
309     * @param stdClass|array $data return value from {@link moodleform::get_data()} or array with data
310     * @param stdClass $oldcourse if this function is called from {@link update_course()}
311     *     this object contains information about the course before update
312     * @return bool whether there were any changes to the options values
313     */
314    public function update_course_format_options($data, $oldcourse = null) {
315        $data = (array)$data;
316        if ($oldcourse !== null) {
317            $oldcourse = (array)$oldcourse;
318            $options = $this->course_format_options();
319            foreach ($options as $key => $unused) {
320                if (!array_key_exists($key, $data)) {
321                    if (array_key_exists($key, $oldcourse)) {
322                        $data[$key] = $oldcourse[$key];
323                    }
324                }
325            }
326        }
327        return $this->update_format_options($data);
328    }
329
330    /**
331     * Whether this format allows to delete sections
332     *
333     * Do not call this function directly, instead use {@link course_can_delete_section()}
334     *
335     * @param int|stdClass|section_info $section
336     * @return bool
337     */
338    public function can_delete_section($section) {
339        return true;
340    }
341
342    /**
343     * Prepares the templateable object to display section name
344     *
345     * @param \section_info|\stdClass $section
346     * @param bool $linkifneeded
347     * @param bool $editable
348     * @param null|lang_string|string $edithint
349     * @param null|lang_string|string $editlabel
350     * @return \core\output\inplace_editable
351     */
352    public function inplace_editable_render_section_name($section, $linkifneeded = true,
353                                                         $editable = null, $edithint = null, $editlabel = null) {
354        if (empty($edithint)) {
355            $edithint = new lang_string('editsectionname', 'format_topics');
356        }
357        if (empty($editlabel)) {
358            $title = get_section_name($section->course, $section);
359            $editlabel = new lang_string('newsectionname', 'format_topics', $title);
360        }
361        return parent::inplace_editable_render_section_name($section, $linkifneeded, $editable, $edithint, $editlabel);
362    }
363
364    /**
365     * Indicates whether the course format supports the creation of a news forum.
366     *
367     * @return bool
368     */
369    public function supports_news() {
370        return true;
371    }
372
373    /**
374     * Returns whether this course format allows the activity to
375     * have "triple visibility state" - visible always, hidden on course page but available, hidden.
376     *
377     * @param stdClass|cm_info $cm course module (may be null if we are displaying a form for adding a module)
378     * @param stdClass|section_info $section section where this module is located or will be added to
379     * @return bool
380     */
381    public function allow_stealth_module_visibility($cm, $section) {
382        // Allow the third visibility state inside visible sections or in section 0.
383        return !$section->section || $section->visible;
384    }
385
386    public function section_action($section, $action, $sr) {
387        global $PAGE;
388
389        if ($section->section && ($action === 'setmarker' || $action === 'removemarker')) {
390            // Format 'topics' allows to set and remove markers in addition to common section actions.
391            require_capability('moodle/course:setcurrentsection', context_course::instance($this->courseid));
392            course_set_marker($this->courseid, ($action === 'setmarker') ? $section->section : 0);
393            return null;
394        }
395
396        // For show/hide actions call the parent method and return the new content for .section_availability element.
397        $rv = parent::section_action($section, $action, $sr);
398        $renderer = $PAGE->get_renderer('format_topics');
399        $rv['section_availability'] = $renderer->section_availability($this->get_section($section));
400        return $rv;
401    }
402
403    /**
404     * Return the plugin configs for external functions.
405     *
406     * @return array the list of configuration settings
407     * @since Moodle 3.5
408     */
409    public function get_config_for_external() {
410        // Return everything (nothing to hide).
411        return $this->get_format_options();
412    }
413}
414
415/**
416 * Implements callback inplace_editable() allowing to edit values in-place
417 *
418 * @param string $itemtype
419 * @param int $itemid
420 * @param mixed $newvalue
421 * @return \core\output\inplace_editable
422 */
423function format_topics_inplace_editable($itemtype, $itemid, $newvalue) {
424    global $DB, $CFG;
425    require_once($CFG->dirroot . '/course/lib.php');
426    if ($itemtype === 'sectionname' || $itemtype === 'sectionnamenl') {
427        $section = $DB->get_record_sql(
428            'SELECT s.* FROM {course_sections} s JOIN {course} c ON s.course = c.id WHERE s.id = ? AND c.format = ?',
429            array($itemid, 'topics'), MUST_EXIST);
430        return course_get_format($section->course)->inplace_editable_update_section_name($section, $itemtype, $newvalue);
431    }
432}
433