5db9dea89ea632ab6deea2dcf3696923f20ecfbe
[moodle.git] / course / format / singleactivity / lib.php
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/>.
17 /**
18  * This file contains main class for the course format singleactivity
19  *
20  * @package    format_singleactivity
21  * @copyright  2012 Marina Glancy
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 defined('MOODLE_INTERNAL') || die();
26 require_once($CFG->dirroot. '/course/format/lib.php');
28 /**
29  * Main class for the singleactivity course format
30  *
31  * @package    format_singleactivity
32  * @copyright  2012 Marina Glancy
33  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34  */
35 class format_singleactivity extends format_base {
36     /** @var cm_info the current activity. Use get_activity() to retrieve it. */
37     private $activity = false;
39     /** @var int The category ID guessed from the form data. */
40     private $categoryid = false;
42     /**
43      * The URL to use for the specified course
44      *
45      * @param int|stdClass $section Section object from database or just field course_sections.section
46      *     if null the course view page is returned
47      * @param array $options options for view URL. At the moment core uses:
48      *     'navigation' (bool) if true and section has no separate page, the function returns null
49      *     'sr' (int) used by multipage formats to specify to which section to return
50      * @return null|moodle_url
51      */
52     public function get_view_url($section, $options = array()) {
53         $sectionnum = $section;
54         if (is_object($sectionnum)) {
55             $sectionnum = $section->section;
56         }
57         if ($sectionnum == 1) {
58             return new moodle_url('/course/view.php', array('id' => $this->courseid, 'section' => 1));
59         }
60         if (!empty($options['navigation']) && $section !== null) {
61             return null;
62         }
63         return new moodle_url('/course/view.php', array('id' => $this->courseid));
64     }
66     /**
67      * Loads all of the course sections into the navigation
68      *
69      * @param global_navigation $navigation
70      * @param navigation_node $node The course node within the navigation
71      */
72     public function extend_course_navigation($navigation, navigation_node $node) {
73         // Display orphaned activities for the users who can see them.
74         $context = context_course::instance($this->courseid);
75         if (has_capability('moodle/course:viewhiddensections', $context)) {
76             $modinfo = get_fast_modinfo($this->courseid);
77             if (!empty($modinfo->sections[1])) {
78                 $section1 = $modinfo->get_section_info(1);
79                 // Show orphaned activities.
80                 $orphanednode = $node->add(get_string('orphaned', 'format_singleactivity'),
81                         $this->get_view_url(1), navigation_node::TYPE_SECTION, null, $section1->id);
82                 $orphanednode->nodetype = navigation_node::NODETYPE_BRANCH;
83                 $orphanednode->add_class('orphaned');
84                 foreach ($modinfo->sections[1] as $cmid) {
85                     if (has_capability('moodle/course:viewhiddenactivities', context_module::instance($cmid))) {
86                         $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
87                     }
88                 }
89             }
90         }
91     }
93     /**
94      * Adds a course module to the navigation node
95      *
96      * This is basically copied from function global_navigation::load_section_activities()
97      * because it is not accessible from outside.
98      *
99      * @param navigation_node $node
100      * @param cm_info $cm
101      * @return null|navigation_node
102      */
103     protected function navigation_add_activity(navigation_node $node, $cm) {
104         if (!$cm->uservisible) {
105             return null;
106         }
107         $action = $cm->url;
108         if (!$action) {
109             // Do not add to navigation activity without url (i.e. labels).
110             return null;
111         }
112         $activityname = format_string($cm->name, true, array('context' => context_module::instance($cm->id)));
113         if ($cm->icon) {
114             $icon = new pix_icon($cm->icon, $cm->modfullname, $cm->iconcomponent);
115         } else {
116             $icon = new pix_icon('icon', $cm->modfullname, $cm->modname);
117         }
118         $activitynode = $node->add($activityname, $action, navigation_node::TYPE_ACTIVITY, null, $cm->id, $icon);
119         if (global_navigation::module_extends_navigation($cm->modname)) {
120             $activitynode->nodetype = navigation_node::NODETYPE_BRANCH;
121         } else {
122             $activitynode->nodetype = navigation_node::NODETYPE_LEAF;
123         }
124         return $activitynode;
125     }
127     /**
128      * Returns the list of blocks to be automatically added for the newly created course
129      *
130      * @return array of default blocks, must contain two keys BLOCK_POS_LEFT and BLOCK_POS_RIGHT
131      *     each of values is an array of block names (for left and right side columns)
132      */
133     public function get_default_blocks() {
134         // No blocks for this format because course view page is not displayed anyway.
135         return array(
136             BLOCK_POS_LEFT => array(),
137             BLOCK_POS_RIGHT => array()
138         );
139     }
141     /**
142      * Definitions of the additional options that this course format uses for course
143      *
144      * Singleactivity course format uses one option 'activitytype'
145      *
146      * @param bool $foreditform
147      * @return array of options
148      */
149     public function course_format_options($foreditform = false) {
150         static $courseformatoptions = false;
152         $fetchtypes = $courseformatoptions === false;
153         $fetchtypes = $fetchtypes || ($foreditform && !isset($courseformatoptions['activitytype']['label']));
155         if ($fetchtypes) {
156             $availabletypes = $this->get_supported_activities();
157             if ($this->course) {
158                 // The course exists. Test against the course.
159                 $testcontext = context_course::instance($this->course->id);
160             } else if ($this->categoryid) {
161                 // The course does not exist yet, but we have a category ID that we can test against.
162                 $testcontext = context_coursecat::instance($this->categoryid);
163             } else {
164                 // The course does not exist, and we somehow do not have a category. Test capabilities against the system context.
165                 $testcontext = context_system::instance();
166             }
167             foreach (array_keys($availabletypes) as $activity) {
168                 $capability = "mod/{$activity}:addinstance";
169                 if (!has_capability($capability, $testcontext)) {
170                     unset($availabletypes[$activity]);
171                 }
172             }
173         }
175         if ($courseformatoptions === false) {
176             $config = get_config('format_singleactivity');
177             $courseformatoptions = array(
178                 'activitytype' => array(
179                     'default' => $config->activitytype,
180                     'type' => PARAM_TEXT,
181                 ),
182             );
184             if (!empty($availabletypes) && !isset($availabletypes[$config->activitytype])) {
185                 $courseformatoptions['activitytype']['default'] = array_keys($availabletypes)[0];
186             }
187         }
189         if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) {
190             $courseformatoptionsedit = array(
191                 'activitytype' => array(
192                     'label' => new lang_string('activitytype', 'format_singleactivity'),
193                     'help' => 'activitytype',
194                     'help_component' => 'format_singleactivity',
195                     'element_type' => 'select',
196                     'element_attributes' => array($availabletypes),
197                 ),
198             );
199             $courseformatoptions = array_merge_recursive($courseformatoptions, $courseformatoptionsedit);
200         }
201         return $courseformatoptions;
202     }
204     /**
205      * Adds format options elements to the course/section edit form
206      *
207      * This function is called from {@link course_edit_form::definition_after_data()}
208      *
209      * Format singleactivity adds a warning when format of the course is about to be changed.
210      *
211      * @param MoodleQuickForm $mform form the elements are added to
212      * @param bool $forsection 'true' if this is a section edit form, 'false' if this is course edit form
213      * @return array array of references to the added form elements
214      */
215     public function create_edit_form_elements(&$mform, $forsection = false) {
216         global $PAGE;
218         if (!$this->course && $submitvalues = $mform->getSubmitValues()) {
219             $this->categoryid = $submitvalues['category'];
220         }
222         $elements = parent::create_edit_form_elements($mform, $forsection);
223         if (!$forsection && ($course = $PAGE->course) && !empty($course->format) &&
224                 $course->format !== 'site' && $course->format !== 'singleactivity') {
225             // This is the existing course in other format, display a warning.
226             $element = $mform->addElement('static', '', '',
227                     html_writer::tag('span', get_string('warningchangeformat', 'format_singleactivity'),
228                             array('class' => 'error')));
229             array_unshift($elements, $element);
230         }
231         return $elements;
232     }
234     /**
235      * Make sure that current active activity is in section 0
236      *
237      * All other activities are moved to section 1 that will be displayed as 'Orphaned'.
238      * It may be needed after the course format was changed or activitytype in
239      * course settings has been changed.
240      *
241      * @return null|cm_info current activity
242      */
243     public function reorder_activities() {
244         course_create_sections_if_missing($this->courseid, array(0, 1));
245         foreach ($this->get_sections() as $sectionnum => $section) {
246             if (($sectionnum && $section->visible) ||
247                     (!$sectionnum && !$section->visible)) {
248                 // Make sure that 0 section is visible and all others are hidden.
249                 set_section_visible($this->courseid, $sectionnum, $sectionnum == 0);
250             }
251         }
252         $modinfo = get_fast_modinfo($this->courseid);
254         // Find the current activity (first activity with the specified type in all course activities).
255         $activitytype = $this->get_activitytype();
256         $activity = null;
257         if (!empty($activitytype)) {
258             foreach ($modinfo->sections as $sectionnum => $cmlist) {
259                 foreach ($cmlist as $cmid) {
260                     if ($modinfo->cms[$cmid]->modname === $activitytype) {
261                         $activity = $modinfo->cms[$cmid];
262                         break 2;
263                     }
264                 }
265             }
266         }
268         // Make sure the current activity is in the 0-section.
269         $changed = false;
270         if ($activity && $activity->sectionnum != 0) {
271             moveto_module($activity, $modinfo->get_section_info(0));
272             $changed = true;
273         }
274         if ($activity && !$activity->visible) {
275             set_coursemodule_visible($activity->id, 1);
276             $changed = true;
277         }
278         if ($changed) {
279             // Cache was reset so get modinfo again.
280             $modinfo = get_fast_modinfo($this->courseid);
281         }
283         // Move all other activities into section 1 (the order must be kept).
284         $hasvisibleactivities = false;
285         $firstorphanedcm = null;
286         foreach ($modinfo->sections as $sectionnum => $cmlist) {
287             if ($sectionnum && !empty($cmlist) && $firstorphanedcm === null) {
288                 $firstorphanedcm = reset($cmlist);
289             }
290             foreach ($cmlist as $cmid) {
291                 if ($sectionnum > 1) {
292                     moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1));
293                 } else if (!$hasvisibleactivities && $sectionnum == 1 && $modinfo->get_cm($cmid)->visible) {
294                     $hasvisibleactivities = true;
295                 }
296             }
297         }
298         if (!empty($modinfo->sections[0])) {
299             foreach ($modinfo->sections[0] as $cmid) {
300                 if (!$activity || $cmid != $activity->id) {
301                     moveto_module($modinfo->get_cm($cmid), $modinfo->get_section_info(1), $firstorphanedcm);
302                 }
303             }
304         }
305         if ($hasvisibleactivities) {
306             set_section_visible($this->courseid, 1, false);
307         }
308         return $activity;
309     }
311     /**
312      * Returns the name of activity type used for this course
313      *
314      * @return string|null
315      */
316     protected function get_activitytype() {
317         $options = $this->get_format_options();
318         $availabletypes = $this->get_supported_activities();
319         if (!empty($options['activitytype']) &&
320                 array_key_exists($options['activitytype'], $availabletypes)) {
321             return $options['activitytype'];
322         } else {
323             return null;
324         }
325     }
327     /**
328      * Returns the current activity if exists
329      *
330      * @return null|cm_info
331      */
332     protected function get_activity() {
333         if ($this->activity === false) {
334             $this->activity = $this->reorder_activities();
335         }
336         return $this->activity;
337     }
339     /**
340      * Get the activities supported by the format.
341      *
342      * Here we ignore the modules that do not have a page of their own, like the label.
343      *
344      * @return array array($module => $name of the module).
345      */
346     public static function get_supported_activities() {
347         $availabletypes = get_module_types_names();
348         foreach ($availabletypes as $module => $name) {
349             if (plugin_supports('mod', $module, FEATURE_NO_VIEW_LINK, false)) {
350                 unset($availabletypes[$module]);
351             }
352         }
353         return $availabletypes;
354     }
356     /**
357      * Checks if the current user can add the activity of the specified type to this course.
358      *
359      * @return bool
360      */
361     protected function can_add_activity() {
362         global $CFG;
363         if (!($modname = $this->get_activitytype())) {
364             return false;
365         }
366         if (!has_capability('moodle/course:manageactivities', context_course::instance($this->courseid))) {
367             return false;
368         }
369         if (!course_allowed_module($this->get_course(), $modname)) {
370             return false;
371         }
372         $libfile = "$CFG->dirroot/mod/$modname/lib.php";
373         if (!file_exists($libfile)) {
374             return null;
375         }
376         return true;
377     }
379     /**
380      * Checks if the activity type has multiple items in the activity chooser.
381      * This may happen as a result of defining callback modulename_get_shortcuts().
382      *
383      * @return bool|null (null if the check is not possible)
384      */
385     public function activity_has_subtypes() {
386         if (!($modname = $this->get_activitytype())) {
387             return null;
388         }
389         $metadata = get_module_metadata($this->get_course(), self::get_supported_activities());
390         foreach ($metadata as $key => $moduledata) {
391             if (preg_match('/^'.$modname.':/', $key)) {
392                 return true;
393             }
394         }
395         return false;
396     }
398     /**
399      * Allows course format to execute code on moodle_page::set_course()
400      *
401      * This function is executed before the output starts.
402      *
403      * If everything is configured correctly, user is redirected from the
404      * default course view page to the activity view page.
405      *
406      * "Section 1" is the administrative page to manage orphaned activities
407      *
408      * If user is on course view page and there is no module added to the course
409      * and the user has 'moodle/course:manageactivities' capability, redirect to create module
410      * form.
411      *
412      * @param moodle_page $page instance of page calling set_course
413      */
414     public function page_set_course(moodle_page $page) {
415         global $PAGE;
416         $page->add_body_class('format-'. $this->get_format());
417         if ($PAGE == $page && $page->has_set_url() &&
418                 $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
419             $edit = optional_param('edit', -1, PARAM_BOOL);
420             if (($edit == 0 || $edit == 1) && confirm_sesskey()) {
421                 // This is a request to turn editing mode on or off, do not redirect here, /course/view.php will do redirection.
422                 return;
423             }
424             $cm = $this->get_activity();
425             $cursection = optional_param('section', null, PARAM_INT);
426             if (!empty($cursection) && has_capability('moodle/course:viewhiddensections',
427                     context_course::instance($this->courseid))) {
428                 // Display orphaned activities (course view page, section 1).
429                 return;
430             }
431             if (!$this->get_activitytype()) {
432                 if (has_capability('moodle/course:update', context_course::instance($this->courseid))) {
433                     // Teacher is redirected to edit course page.
434                     $url = new moodle_url('/course/edit.php', array('id' => $this->courseid));
435                     redirect($url, get_string('erroractivitytype', 'format_singleactivity'));
436                 } else {
437                     // Student sees an empty course page.
438                     return;
439                 }
440             }
441             if ($cm === null) {
442                 if ($this->can_add_activity()) {
443                     // This is a user who has capability to create an activity.
444                     if ($this->activity_has_subtypes()) {
445                         // Activity has multiple items in the activity chooser, it can not be added automatically.
446                         if (optional_param('addactivity', 0, PARAM_INT)) {
447                             return;
448                         } else {
449                             $url = new moodle_url('/course/view.php', array('id' => $this->courseid, 'addactivity' => 1));
450                             redirect($url);
451                         }
452                     }
453                     // Redirect to the add activity form.
454                     $url = new moodle_url('/course/mod.php', array('id' => $this->courseid,
455                         'section' => 0, 'sesskey' => sesskey(), 'add' => $this->get_activitytype()));
456                     redirect($url);
457                 } else {
458                     // Student views an empty course page.
459                     return;
460                 }
461             } else if (!$cm->uservisible || !$cm->url) {
462                 // Activity is set but not visible to current user or does not have url.
463                 // Display course page (either empty or with availability restriction info).
464                 return;
465             } else {
466                 // Everything is set up and accessible, redirect to the activity page!
467                 redirect($cm->url);
468             }
469         }
470     }
472     /**
473      * Allows course format to execute code on moodle_page::set_cm()
474      *
475      * If we are inside the main module for this course, remove extra node level
476      * from navigation: substitute course node with activity node, move all children
477      *
478      * @param moodle_page $page instance of page calling set_cm
479      */
480     public function page_set_cm(moodle_page $page) {
481         global $PAGE;
482         parent::page_set_cm($page);
483         if ($PAGE == $page && ($cm = $this->get_activity()) &&
484                 $cm->uservisible &&
485                 ($cm->id === $page->cm->id) &&
486                 ($activitynode = $page->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY)) &&
487                 ($node = $page->navigation->find($page->course->id, navigation_node::TYPE_COURSE))) {
488             // Substitute course node with activity node, move all children.
489             $node->action = $activitynode->action;
490             $node->type = $activitynode->type;
491             $node->id = $activitynode->id;
492             $node->key = $activitynode->key;
493             $node->isactive = $node->isactive || $activitynode->isactive;
494             $node->icon = null;
495             if ($activitynode->children->count()) {
496                 foreach ($activitynode->children as &$child) {
497                     $child->remove();
498                     $node->add_node($child);
499                 }
500             } else {
501                 $node->search_for_active_node();
502             }
503             $activitynode->remove();
504         }
505     }
507     /**
508      * Returns true if the course has a front page.
509      *
510      * @return boolean false
511      */
512     public function has_view_page() {
513         return false;
514     }
516     /**
517      * Return the plugin configs for external functions.
518      *
519      * @return array the list of configuration settings
520      * @since Moodle 3.5
521      */
522     public function get_config_for_external() {
523         // Return everything (nothing to hide).
524         return $this->get_format_options();
525     }