MDL-67585 core_course: Use the content_item_service to build the picker
[moodle.git] / course / format / singleactivity / lib.php
CommitLineData
28c61e0a
MG
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 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 */
24
25defined('MOODLE_INTERNAL') || die();
26require_once($CFG->dirroot. '/course/format/lib.php');
27
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 */
35class format_singleactivity extends format_base {
36 /** @var cm_info the current activity. Use get_activity() to retrieve it. */
37 private $activity = false;
38
208397c1
AN
39 /** @var int The category ID guessed from the form data. */
40 private $categoryid = false;
41
28c61e0a
MG
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 }
65
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);
f67b5c99 75 if (has_capability('moodle/course:viewhiddensections', $context)) {
28c61e0a
MG
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) {
45ad1693 85 if (has_capability('moodle/course:viewhiddenactivities', context_module::instance($cmid))) {
f67b5c99
AN
86 $this->navigation_add_activity($orphanednode, $modinfo->cms[$cmid]);
87 }
28c61e0a
MG
88 }
89 }
90 }
91 }
92
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 }
73ee2fda 107 $action = $cm->url;
28c61e0a
MG
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 }
126
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 }
140
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;
208397c1
AN
151
152 $fetchtypes = $courseformatoptions === false;
153 $fetchtypes = $fetchtypes || ($foreditform && !isset($courseformatoptions['activitytype']['label']));
154
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 }
174
28c61e0a
MG
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 );
208397c1 183
b4c92fb7 184 if (!empty($availabletypes) && !isset($availabletypes[$config->activitytype])) {
208397c1
AN
185 $courseformatoptions['activitytype']['default'] = array_keys($availabletypes)[0];
186 }
28c61e0a 187 }
208397c1 188
28c61e0a 189 if ($foreditform && !isset($courseformatoptions['activitytype']['label'])) {
28c61e0a
MG
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 }
203
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;
208397c1
AN
217
218 if (!$this->course && $submitvalues = $mform->getSubmitValues()) {
219 $this->categoryid = $submitvalues['category'];
220 }
221
28c61e0a
MG
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 }
233
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);
253
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 }
267
268 // Make sure the current activity is in the 0-section.
02804470 269 $changed = false;
28c61e0a
MG
270 if ($activity && $activity->sectionnum != 0) {
271 moveto_module($activity, $modinfo->get_section_info(0));
02804470
TH
272 $changed = true;
273 }
274 if ($activity && !$activity->visible) {
275 set_coursemodule_visible($activity->id, 1);
276 $changed = true;
277 }
278 if ($changed) {
28c61e0a
MG
279 // Cache was reset so get modinfo again.
280 $modinfo = get_fast_modinfo($this->courseid);
281 }
282
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 }
310
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();
640ef177 318 $availabletypes = $this->get_supported_activities();
28c61e0a
MG
319 if (!empty($options['activitytype']) &&
320 array_key_exists($options['activitytype'], $availabletypes)) {
321 return $options['activitytype'];
322 } else {
323 return null;
324 }
325 }
326
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 }
338
640ef177
FM
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 }
355
6619945b
MG
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 }
378
379 /**
9ca0420e 380 * Checks if the activity type has multiple items in the activity chooser.
4bebed40 381 * This may happen as a result of defining callback modulename_get_shortcuts().
6619945b
MG
382 *
383 * @return bool|null (null if the check is not possible)
384 */
385 public function activity_has_subtypes() {
2f040002 386 global $PAGE, $USER;
6619945b
MG
387 if (!($modname = $this->get_activitytype())) {
388 return null;
389 }
2f040002
JD
390 $contentitemservice = new \core_course\local\service\content_item_service($PAGE->get_renderer('course'));
391 $metadata = $contentitemservice->get_content_items_for_user_in_course($USER, $this->get_course());
9ca0420e
MG
392 foreach ($metadata as $key => $moduledata) {
393 if (preg_match('/^'.$modname.':/', $key)) {
394 return true;
395 }
396 }
397 return false;
6619945b
MG
398 }
399
28c61e0a
MG
400 /**
401 * Allows course format to execute code on moodle_page::set_course()
402 *
403 * This function is executed before the output starts.
404 *
405 * If everything is configured correctly, user is redirected from the
406 * default course view page to the activity view page.
407 *
408 * "Section 1" is the administrative page to manage orphaned activities
409 *
410 * If user is on course view page and there is no module added to the course
411 * and the user has 'moodle/course:manageactivities' capability, redirect to create module
412 * form.
413 *
414 * @param moodle_page $page instance of page calling set_course
415 */
416 public function page_set_course(moodle_page $page) {
417 global $PAGE;
418 $page->add_body_class('format-'. $this->get_format());
419 if ($PAGE == $page && $page->has_set_url() &&
420 $page->url->compare(new moodle_url('/course/view.php'), URL_MATCH_BASE)) {
421 $edit = optional_param('edit', -1, PARAM_BOOL);
422 if (($edit == 0 || $edit == 1) && confirm_sesskey()) {
423 // This is a request to turn editing mode on or off, do not redirect here, /course/view.php will do redirection.
424 return;
425 }
426 $cm = $this->get_activity();
427 $cursection = optional_param('section', null, PARAM_INT);
428 if (!empty($cursection) && has_capability('moodle/course:viewhiddensections',
429 context_course::instance($this->courseid))) {
430 // Display orphaned activities (course view page, section 1).
431 return;
432 }
433 if (!$this->get_activitytype()) {
434 if (has_capability('moodle/course:update', context_course::instance($this->courseid))) {
435 // Teacher is redirected to edit course page.
436 $url = new moodle_url('/course/edit.php', array('id' => $this->courseid));
437 redirect($url, get_string('erroractivitytype', 'format_singleactivity'));
438 } else {
439 // Student sees an empty course page.
440 return;
441 }
442 }
443 if ($cm === null) {
6619945b
MG
444 if ($this->can_add_activity()) {
445 // This is a user who has capability to create an activity.
446 if ($this->activity_has_subtypes()) {
9ca0420e 447 // Activity has multiple items in the activity chooser, it can not be added automatically.
6619945b
MG
448 if (optional_param('addactivity', 0, PARAM_INT)) {
449 return;
450 } else {
451 $url = new moodle_url('/course/view.php', array('id' => $this->courseid, 'addactivity' => 1));
452 redirect($url);
453 }
454 }
455 // Redirect to the add activity form.
456 $url = new moodle_url('/course/mod.php', array('id' => $this->courseid,
457 'section' => 0, 'sesskey' => sesskey(), 'add' => $this->get_activitytype()));
28c61e0a
MG
458 redirect($url);
459 } else {
460 // Student views an empty course page.
461 return;
462 }
73ee2fda 463 } else if (!$cm->uservisible || !$cm->url) {
28c61e0a
MG
464 // Activity is set but not visible to current user or does not have url.
465 // Display course page (either empty or with availability restriction info).
466 return;
467 } else {
468 // Everything is set up and accessible, redirect to the activity page!
73ee2fda 469 redirect($cm->url);
28c61e0a
MG
470 }
471 }
472 }
473
474 /**
475 * Allows course format to execute code on moodle_page::set_cm()
476 *
477 * If we are inside the main module for this course, remove extra node level
478 * from navigation: substitute course node with activity node, move all children
479 *
480 * @param moodle_page $page instance of page calling set_cm
481 */
482 public function page_set_cm(moodle_page $page) {
483 global $PAGE;
484 parent::page_set_cm($page);
485 if ($PAGE == $page && ($cm = $this->get_activity()) &&
486 $cm->uservisible &&
487 ($cm->id === $page->cm->id) &&
488 ($activitynode = $page->navigation->find($cm->id, navigation_node::TYPE_ACTIVITY)) &&
489 ($node = $page->navigation->find($page->course->id, navigation_node::TYPE_COURSE))) {
490 // Substitute course node with activity node, move all children.
491 $node->action = $activitynode->action;
492 $node->type = $activitynode->type;
493 $node->id = $activitynode->id;
494 $node->key = $activitynode->key;
495 $node->isactive = $node->isactive || $activitynode->isactive;
496 $node->icon = null;
497 if ($activitynode->children->count()) {
498 foreach ($activitynode->children as &$child) {
499 $child->remove();
500 $node->add_node($child);
501 }
502 } else {
503 $node->search_for_active_node();
504 }
505 $activitynode->remove();
506 }
507 }
d9203fb7
FM
508
509 /**
510 * Returns true if the course has a front page.
511 *
512 * @return boolean false
513 */
514 public function has_view_page() {
515 return false;
516 }
517
ef184ad6
JL
518 /**
519 * Return the plugin configs for external functions.
520 *
521 * @return array the list of configuration settings
522 * @since Moodle 3.5
523 */
524 public function get_config_for_external() {
525 // Return everything (nothing to hide).
526 return $this->get_format_options();
527 }
28c61e0a 528}