Merge branch 'MDL-77321-401' of https://github.com/paulholden/moodle into MOODLE_401_...
[moodle.git] / course / lib.php
blobb6745221c70367cec8386d0b65f18735acaa3ff1
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.
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 * Library of useful functions
20 * @copyright 1999 Martin Dougiamas http://dougiamas.com
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
22 * @package core_course
25 defined('MOODLE_INTERNAL') || die;
27 use core_courseformat\base as course_format;
29 require_once($CFG->libdir.'/completionlib.php');
30 require_once($CFG->libdir.'/filelib.php');
31 require_once($CFG->libdir.'/datalib.php');
32 require_once($CFG->dirroot.'/course/format/lib.php');
34 define('COURSE_MAX_LOGS_PER_PAGE', 1000); // Records.
35 define('COURSE_MAX_RECENT_PERIOD', 172800); // Two days, in seconds.
37 /**
38 * Number of courses to display when summaries are included.
39 * @var int
40 * @deprecated since 2.4, use $CFG->courseswithsummarieslimit instead.
42 define('COURSE_MAX_SUMMARIES_PER_PAGE', 10);
44 // Max courses in log dropdown before switching to optional.
45 define('COURSE_MAX_COURSES_PER_DROPDOWN', 1000);
46 // Max users in log dropdown before switching to optional.
47 define('COURSE_MAX_USERS_PER_DROPDOWN', 1000);
48 define('FRONTPAGENEWS', '0');
49 define('FRONTPAGECATEGORYNAMES', '2');
50 define('FRONTPAGECATEGORYCOMBO', '4');
51 define('FRONTPAGEENROLLEDCOURSELIST', '5');
52 define('FRONTPAGEALLCOURSELIST', '6');
53 define('FRONTPAGECOURSESEARCH', '7');
54 // Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage.
55 define('EXCELROWS', 65535);
56 define('FIRSTUSEDEXCELROW', 3);
58 define('MOD_CLASS_ACTIVITY', 0);
59 define('MOD_CLASS_RESOURCE', 1);
61 define('COURSE_TIMELINE_ALLINCLUDINGHIDDEN', 'allincludinghidden');
62 define('COURSE_TIMELINE_ALL', 'all');
63 define('COURSE_TIMELINE_PAST', 'past');
64 define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
65 define('COURSE_TIMELINE_FUTURE', 'future');
66 define('COURSE_TIMELINE_SEARCH', 'search');
67 define('COURSE_FAVOURITES', 'favourites');
68 define('COURSE_TIMELINE_HIDDEN', 'hidden');
69 define('COURSE_CUSTOMFIELD', 'customfield');
70 define('COURSE_DB_QUERY_LIMIT', 1000);
71 /** Searching for all courses that have no value for the specified custom field. */
72 define('COURSE_CUSTOMFIELD_EMPTY', -1);
74 // Course activity chooser footer default display option.
75 define('COURSE_CHOOSER_FOOTER_NONE', 'hidden');
77 // Download course content options.
78 define('DOWNLOAD_COURSE_CONTENT_DISABLED', 0);
79 define('DOWNLOAD_COURSE_CONTENT_ENABLED', 1);
80 define('DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT', 2);
82 function make_log_url($module, $url) {
83 switch ($module) {
84 case 'course':
85 if (strpos($url, 'report/') === 0) {
86 // there is only one report type, course reports are deprecated
87 $url = "/$url";
88 break;
90 case 'file':
91 case 'login':
92 case 'lib':
93 case 'admin':
94 case 'category':
95 case 'mnet course':
96 if (strpos($url, '../') === 0) {
97 $url = ltrim($url, '.');
98 } else {
99 $url = "/course/$url";
101 break;
102 case 'calendar':
103 $url = "/calendar/$url";
104 break;
105 case 'user':
106 case 'blog':
107 $url = "/$module/$url";
108 break;
109 case 'upload':
110 $url = $url;
111 break;
112 case 'coursetags':
113 $url = '/'.$url;
114 break;
115 case 'library':
116 case '':
117 $url = '/';
118 break;
119 case 'message':
120 $url = "/message/$url";
121 break;
122 case 'notes':
123 $url = "/notes/$url";
124 break;
125 case 'tag':
126 $url = "/tag/$url";
127 break;
128 case 'role':
129 $url = '/'.$url;
130 break;
131 case 'grade':
132 $url = "/grade/$url";
133 break;
134 default:
135 $url = "/mod/$module/$url";
136 break;
139 //now let's sanitise urls - there might be some ugly nasties:-(
140 $parts = explode('?', $url);
141 $script = array_shift($parts);
142 if (strpos($script, 'http') === 0) {
143 $script = clean_param($script, PARAM_URL);
144 } else {
145 $script = clean_param($script, PARAM_PATH);
148 $query = '';
149 if ($parts) {
150 $query = implode('', $parts);
151 $query = str_replace('&amp;', '&', $query); // both & and &amp; are stored in db :-|
152 $parts = explode('&', $query);
153 $eq = urlencode('=');
154 foreach ($parts as $key=>$part) {
155 $part = urlencode(urldecode($part));
156 $part = str_replace($eq, '=', $part);
157 $parts[$key] = $part;
159 $query = '?'.implode('&amp;', $parts);
162 return $script.$query;
166 function build_mnet_logs_array($hostid, $course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='',
167 $modname="", $modid=0, $modaction="", $groupid=0) {
168 global $CFG, $DB;
170 // It is assumed that $date is the GMT time of midnight for that day,
171 // and so the next 86400 seconds worth of logs are printed.
173 /// Setup for group handling.
175 // TODO: I don't understand group/context/etc. enough to be able to do
176 // something interesting with it here
177 // What is the context of a remote course?
179 /// If the group mode is separate, and this user does not have editing privileges,
180 /// then only the user's group can be viewed.
181 //if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) {
182 // $groupid = get_current_group($course->id);
184 /// If this course doesn't have groups, no groupid can be specified.
185 //else if (!$course->groupmode) {
186 // $groupid = 0;
189 $groupid = 0;
191 $joins = array();
192 $where = '';
194 $qry = "SELECT l.*, u.firstname, u.lastname, u.picture
195 FROM {mnet_log} l
196 LEFT JOIN {user} u ON l.userid = u.id
197 WHERE ";
198 $params = array();
200 $where .= "l.hostid = :hostid";
201 $params['hostid'] = $hostid;
203 // TODO: Is 1 really a magic number referring to the sitename?
204 if ($course != SITEID || $modid != 0) {
205 $where .= " AND l.course=:courseid";
206 $params['courseid'] = $course;
209 if ($modname) {
210 $where .= " AND l.module = :modname";
211 $params['modname'] = $modname;
214 if ('site_errors' === $modid) {
215 $where .= " AND ( l.action='error' OR l.action='infected' )";
216 } else if ($modid) {
217 //TODO: This assumes that modids are the same across sites... probably
218 //not true
219 $where .= " AND l.cmid = :modid";
220 $params['modid'] = $modid;
223 if ($modaction) {
224 $firstletter = substr($modaction, 0, 1);
225 if ($firstletter == '-') {
226 $where .= " AND ".$DB->sql_like('l.action', ':modaction', false, true, true);
227 $params['modaction'] = '%'.substr($modaction, 1).'%';
228 } else {
229 $where .= " AND ".$DB->sql_like('l.action', ':modaction', false);
230 $params['modaction'] = '%'.$modaction.'%';
234 if ($user) {
235 $where .= " AND l.userid = :user";
236 $params['user'] = $user;
239 if ($date) {
240 $enddate = $date + 86400;
241 $where .= " AND l.time > :date AND l.time < :enddate";
242 $params['date'] = $date;
243 $params['enddate'] = $enddate;
246 $result = array();
247 $result['totalcount'] = $DB->count_records_sql("SELECT COUNT('x') FROM {mnet_log} l WHERE $where", $params);
248 if(!empty($result['totalcount'])) {
249 $where .= " ORDER BY $order";
250 $result['logs'] = $DB->get_records_sql("$qry $where", $params, $limitfrom, $limitnum);
251 } else {
252 $result['logs'] = array();
254 return $result;
258 * Checks the integrity of the course data.
260 * In summary - compares course_sections.sequence and course_modules.section.
262 * More detailed, checks that:
263 * - course_sections.sequence contains each module id not more than once in the course
264 * - for each moduleid from course_sections.sequence the field course_modules.section
265 * refers to the same section id (this means course_sections.sequence is more
266 * important if they are different)
267 * - ($fullcheck only) each module in the course is present in one of
268 * course_sections.sequence
269 * - ($fullcheck only) removes non-existing course modules from section sequences
271 * If there are any mismatches, the changes are made and records are updated in DB.
273 * Course cache is NOT rebuilt if there are any errors!
275 * This function is used each time when course cache is being rebuilt with $fullcheck = false
276 * and in CLI script admin/cli/fix_course_sequence.php with $fullcheck = true
278 * @param int $courseid id of the course
279 * @param array $rawmods result of funciton {@link get_course_mods()} - containst
280 * the list of enabled course modules in the course. Retrieved from DB if not specified.
281 * Argument ignored in cashe of $fullcheck, the list is retrieved form DB anyway.
282 * @param array $sections records from course_sections table for this course.
283 * Retrieved from DB if not specified
284 * @param bool $fullcheck Will add orphaned modules to their sections and remove non-existing
285 * course modules from sequences. Only to be used in site maintenance mode when we are
286 * sure that another user is not in the middle of the process of moving/removing a module.
287 * @param bool $checkonly Only performs the check without updating DB, outputs all errors as debug messages.
288 * @return array array of messages with found problems. Empty output means everything is ok
290 function course_integrity_check($courseid, $rawmods = null, $sections = null, $fullcheck = false, $checkonly = false) {
291 global $DB;
292 $messages = array();
293 if ($sections === null) {
294 $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section', 'id,section,sequence');
296 if ($fullcheck) {
297 // Retrieve all records from course_modules regardless of module type visibility.
298 $rawmods = $DB->get_records('course_modules', array('course' => $courseid), 'id', 'id,section');
300 if ($rawmods === null) {
301 $rawmods = get_course_mods($courseid);
303 if (!$fullcheck && (empty($sections) || empty($rawmods))) {
304 // If either of the arrays is empty, no modules are displayed anyway.
305 return true;
307 $debuggingprefix = 'Failed integrity check for course ['.$courseid.']. ';
309 // First make sure that each module id appears in section sequences only once.
310 // If it appears in several section sequences the last section wins.
311 // If it appears twice in one section sequence, the first occurence wins.
312 $modsection = array();
313 foreach ($sections as $sectionid => $section) {
314 $sections[$sectionid]->newsequence = $section->sequence;
315 if (!empty($section->sequence)) {
316 $sequence = explode(",", $section->sequence);
317 $sequenceunique = array_unique($sequence);
318 if (count($sequenceunique) != count($sequence)) {
319 // Some course module id appears in this section sequence more than once.
320 ksort($sequenceunique); // Preserve initial order of modules.
321 $sequence = array_values($sequenceunique);
322 $sections[$sectionid]->newsequence = join(',', $sequence);
323 $messages[] = $debuggingprefix.'Sequence for course section ['.
324 $sectionid.'] is "'.$sections[$sectionid]->sequence.'", must be "'.$sections[$sectionid]->newsequence.'"';
326 foreach ($sequence as $cmid) {
327 if (array_key_exists($cmid, $modsection) && isset($rawmods[$cmid])) {
328 // Some course module id appears to be in more than one section's sequences.
329 $wrongsectionid = $modsection[$cmid];
330 $sections[$wrongsectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$wrongsectionid]->newsequence. ','), ',');
331 $messages[] = $debuggingprefix.'Course module ['.$cmid.'] must be removed from sequence of section ['.
332 $wrongsectionid.'] because it is also present in sequence of section ['.$sectionid.']';
334 $modsection[$cmid] = $sectionid;
339 // Add orphaned modules to their sections if they exist or to section 0 otherwise.
340 if ($fullcheck) {
341 foreach ($rawmods as $cmid => $mod) {
342 if (!isset($modsection[$cmid])) {
343 // This is a module that is not mentioned in course_section.sequence at all.
344 // Add it to the section $mod->section or to the last available section.
345 if ($mod->section && isset($sections[$mod->section])) {
346 $modsection[$cmid] = $mod->section;
347 } else {
348 $firstsection = reset($sections);
349 $modsection[$cmid] = $firstsection->id;
351 $sections[$modsection[$cmid]]->newsequence = trim($sections[$modsection[$cmid]]->newsequence.','.$cmid, ',');
352 $messages[] = $debuggingprefix.'Course module ['.$cmid.'] is missing from sequence of section ['.
353 $modsection[$cmid].']';
356 foreach ($modsection as $cmid => $sectionid) {
357 if (!isset($rawmods[$cmid])) {
358 // Section $sectionid refers to module id that does not exist.
359 $sections[$sectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$sectionid]->newsequence.','), ',');
360 $messages[] = $debuggingprefix.'Course module ['.$cmid.
361 '] does not exist but is present in the sequence of section ['.$sectionid.']';
366 // Update changed sections.
367 if (!$checkonly && !empty($messages)) {
368 foreach ($sections as $sectionid => $section) {
369 if ($section->newsequence !== $section->sequence) {
370 $DB->update_record('course_sections', array('id' => $sectionid, 'sequence' => $section->newsequence));
375 // Now make sure that all modules point to the correct sections.
376 foreach ($rawmods as $cmid => $mod) {
377 if (isset($modsection[$cmid]) && $modsection[$cmid] != $mod->section) {
378 if (!$checkonly) {
379 $DB->update_record('course_modules', array('id' => $cmid, 'section' => $modsection[$cmid]));
381 $messages[] = $debuggingprefix.'Course module ['.$cmid.
382 '] points to section ['.$mod->section.'] instead of ['.$modsection[$cmid].']';
386 return $messages;
390 * Returns an array where the key is the module name (component name without 'mod_')
391 * and the value is a lang_string object with a human-readable string.
393 * @param bool $plural If true, the function returns the plural forms of the names.
394 * @param bool $resetcache If true, the static cache will be reset
395 * @return lang_string[] Localised human-readable names of all used modules.
397 function get_module_types_names($plural = false, $resetcache = false) {
398 static $modnames = null;
399 global $DB, $CFG;
400 if ($modnames === null || $resetcache) {
401 $modnames = array(0 => array(), 1 => array());
402 if ($allmods = $DB->get_records("modules")) {
403 foreach ($allmods as $mod) {
404 if (file_exists("$CFG->dirroot/mod/$mod->name/lib.php") && $mod->visible) {
405 $modnames[0][$mod->name] = get_string("modulename", "$mod->name", null, true);
406 $modnames[1][$mod->name] = get_string("modulenameplural", "$mod->name", null, true);
411 return $modnames[(int)$plural];
415 * Set highlighted section. Only one section can be highlighted at the time.
417 * @param int $courseid course id
418 * @param int $marker highlight section with this number, 0 means remove higlightin
419 * @return void
421 function course_set_marker($courseid, $marker) {
422 global $DB, $COURSE;
423 $DB->set_field("course", "marker", $marker, array('id' => $courseid));
424 if ($COURSE && $COURSE->id == $courseid) {
425 $COURSE->marker = $marker;
427 core_courseformat\base::reset_course_cache($courseid);
428 course_modinfo::clear_instance_cache($courseid);
432 * For a given course section, marks it visible or hidden,
433 * and does the same for every activity in that section
435 * @param int $courseid course id
436 * @param int $sectionnumber The section number to adjust
437 * @param int $visibility The new visibility
438 * @return array A list of resources which were hidden in the section
440 function set_section_visible($courseid, $sectionnumber, $visibility) {
441 global $DB;
443 $resourcestotoggle = array();
444 if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) {
445 course_update_section($courseid, $section, array('visible' => $visibility));
447 // Determine which modules are visible for AJAX update
448 $modules = !empty($section->sequence) ? explode(',', $section->sequence) : array();
449 if (!empty($modules)) {
450 list($insql, $params) = $DB->get_in_or_equal($modules);
451 $select = 'id ' . $insql . ' AND visible = ?';
452 array_push($params, $visibility);
453 if (!$visibility) {
454 $select .= ' AND visibleold = 1';
456 $resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params);
459 return $resourcestotoggle;
463 * Return the course category context for the category with id $categoryid, except
464 * that if $categoryid is 0, return the system context.
466 * @param integer $categoryid a category id or 0.
467 * @return context the corresponding context
469 function get_category_or_system_context($categoryid) {
470 if ($categoryid) {
471 return context_coursecat::instance($categoryid, IGNORE_MISSING);
472 } else {
473 return context_system::instance();
478 * Print the buttons relating to course requests.
480 * @param context $context current page context.
481 * @deprecated since Moodle 4.0
482 * @todo Final deprecation MDL-73976
484 function print_course_request_buttons($context) {
485 global $CFG, $DB, $OUTPUT;
486 debugging("print_course_request_buttons() is deprecated. " .
487 "This is replaced with the category_action_bar tertiary navigation.", DEBUG_DEVELOPER);
488 if (empty($CFG->enablecourserequests)) {
489 return;
491 if (course_request::can_request($context)) {
492 // Print a button to request a new course.
493 $params = [];
494 if ($context instanceof context_coursecat) {
495 $params['category'] = $context->instanceid;
497 echo $OUTPUT->single_button(new moodle_url('/course/request.php', $params),
498 get_string('requestcourse'), 'get');
500 /// Print a button to manage pending requests
501 if (has_capability('moodle/site:approvecourse', $context)) {
502 $disabled = !$DB->record_exists('course_request', array());
503 echo $OUTPUT->single_button(new moodle_url('/course/pending.php'), get_string('coursespending'), 'get', array('disabled' => $disabled));
508 * Does the user have permission to edit things in this category?
510 * @param integer $categoryid The id of the category we are showing, or 0 for system context.
511 * @return boolean has_any_capability(array(...), ...); in the appropriate context.
513 function can_edit_in_category($categoryid = 0) {
514 $context = get_category_or_system_context($categoryid);
515 return has_any_capability(array('moodle/category:manage', 'moodle/course:create'), $context);
518 /// MODULE FUNCTIONS /////////////////////////////////////////////////////////////////
520 function add_course_module($mod) {
521 global $DB;
523 $mod->added = time();
524 unset($mod->id);
526 $cmid = $DB->insert_record("course_modules", $mod);
527 rebuild_course_cache($mod->course, true);
528 return $cmid;
532 * Creates a course section and adds it to the specified position
534 * @param int|stdClass $courseorid course id or course object
535 * @param int $position position to add to, 0 means to the end. If position is greater than
536 * number of existing secitons, the section is added to the end. This will become sectionnum of the
537 * new section. All existing sections at this or bigger position will be shifted down.
538 * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist
539 * @return stdClass created section object
541 function course_create_section($courseorid, $position = 0, $skipcheck = false) {
542 global $DB;
543 $courseid = is_object($courseorid) ? $courseorid->id : $courseorid;
545 // Find the last sectionnum among existing sections.
546 if ($skipcheck) {
547 $lastsection = $position - 1;
548 } else {
549 $lastsection = (int)$DB->get_field_sql('SELECT max(section) from {course_sections} WHERE course = ?', [$courseid]);
552 // First add section to the end.
553 $cw = new stdClass();
554 $cw->course = $courseid;
555 $cw->section = $lastsection + 1;
556 $cw->summary = '';
557 $cw->summaryformat = FORMAT_HTML;
558 $cw->sequence = '';
559 $cw->name = null;
560 $cw->visible = 1;
561 $cw->availability = null;
562 $cw->timemodified = time();
563 $cw->id = $DB->insert_record("course_sections", $cw);
565 // Now move it to the specified position.
566 if ($position > 0 && $position <= $lastsection) {
567 $course = is_object($courseorid) ? $courseorid : get_course($courseorid);
568 move_section_to($course, $cw->section, $position, true);
569 $cw->section = $position;
572 core\event\course_section_created::create_from_section($cw)->trigger();
574 rebuild_course_cache($courseid, true);
575 return $cw;
579 * Creates missing course section(s) and rebuilds course cache
581 * @param int|stdClass $courseorid course id or course object
582 * @param int|array $sections list of relative section numbers to create
583 * @return bool if there were any sections created
585 function course_create_sections_if_missing($courseorid, $sections) {
586 if (!is_array($sections)) {
587 $sections = array($sections);
589 $existing = array_keys(get_fast_modinfo($courseorid)->get_section_info_all());
590 if ($newsections = array_diff($sections, $existing)) {
591 foreach ($newsections as $sectionnum) {
592 course_create_section($courseorid, $sectionnum, true);
594 return true;
596 return false;
600 * Adds an existing module to the section
602 * Updates both tables {course_sections} and {course_modules}
604 * Note: This function does not use modinfo PROVIDED that the section you are
605 * adding the module to already exists. If the section does not exist, it will
606 * build modinfo if necessary and create the section.
608 * @param int|stdClass $courseorid course id or course object
609 * @param int $cmid id of the module already existing in course_modules table
610 * @param int $sectionnum relative number of the section (field course_sections.section)
611 * If section does not exist it will be created
612 * @param int|stdClass $beforemod id or object with field id corresponding to the module
613 * before which the module needs to be included. Null for inserting in the
614 * end of the section
615 * @return int The course_sections ID where the module is inserted
617 function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod = null) {
618 global $DB, $COURSE;
619 if (is_object($beforemod)) {
620 $beforemod = $beforemod->id;
622 if (is_object($courseorid)) {
623 $courseid = $courseorid->id;
624 } else {
625 $courseid = $courseorid;
627 // Do not try to use modinfo here, there is no guarantee it is valid!
628 $section = $DB->get_record('course_sections',
629 array('course' => $courseid, 'section' => $sectionnum), '*', IGNORE_MISSING);
630 if (!$section) {
631 // This function call requires modinfo.
632 course_create_sections_if_missing($courseorid, $sectionnum);
633 $section = $DB->get_record('course_sections',
634 array('course' => $courseid, 'section' => $sectionnum), '*', MUST_EXIST);
637 $modarray = explode(",", trim($section->sequence));
638 if (empty($section->sequence)) {
639 $newsequence = "$cmid";
640 } else if ($beforemod && ($key = array_keys($modarray, $beforemod))) {
641 $insertarray = array($cmid, $beforemod);
642 array_splice($modarray, $key[0], 1, $insertarray);
643 $newsequence = implode(",", $modarray);
644 } else {
645 $newsequence = "$section->sequence,$cmid";
647 $DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id));
648 $DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid));
649 rebuild_course_cache($courseid, true);
650 return $section->id; // Return course_sections ID that was used.
654 * Change the group mode of a course module.
656 * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
657 * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
659 * @param int $id course module ID.
660 * @param int $groupmode the new groupmode value.
661 * @return bool True if the $groupmode was updated.
663 function set_coursemodule_groupmode($id, $groupmode) {
664 global $DB;
665 $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST);
666 if ($cm->groupmode != $groupmode) {
667 $DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id));
668 \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
669 rebuild_course_cache($cm->course, false, true);
671 return ($cm->groupmode != $groupmode);
674 function set_coursemodule_idnumber($id, $idnumber) {
675 global $DB;
676 $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST);
677 if ($cm->idnumber != $idnumber) {
678 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
679 \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
680 rebuild_course_cache($cm->course, false, true);
682 return ($cm->idnumber != $idnumber);
686 * Set downloadcontent value to course module.
688 * @param int $id The id of the module.
689 * @param bool $downloadcontent Whether the module can be downloaded when download course content is enabled.
690 * @return bool True if downloadcontent has been updated, false otherwise.
692 function set_downloadcontent(int $id, bool $downloadcontent): bool {
693 global $DB;
694 $cm = $DB->get_record('course_modules', ['id' => $id], 'id, course, downloadcontent', MUST_EXIST);
695 if ($cm->downloadcontent != $downloadcontent) {
696 $DB->set_field('course_modules', 'downloadcontent', $downloadcontent, ['id' => $cm->id]);
697 rebuild_course_cache($cm->course, true);
699 return ($cm->downloadcontent != $downloadcontent);
703 * Set the visibility of a module and inherent properties.
705 * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
706 * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
708 * From 2.4 the parameter $prevstateoverrides has been removed, the logic it triggered
709 * has been moved to {@link set_section_visible()} which was the only place from which
710 * the parameter was used.
712 * @param int $id of the module
713 * @param int $visible state of the module
714 * @param int $visibleoncoursepage state of the module on the course page
715 * @return bool false when the module was not found, true otherwise
717 function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) {
718 global $DB, $CFG;
719 require_once($CFG->libdir.'/gradelib.php');
720 require_once($CFG->dirroot.'/calendar/lib.php');
722 if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) {
723 return false;
726 // Create events and propagate visibility to associated grade items if the value has changed.
727 // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades.
728 if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) {
729 return true;
732 if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) {
733 return false;
735 if (($cm->visible != $visible) &&
736 ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) {
737 foreach($events as $event) {
738 if ($visible) {
739 $event = new calendar_event($event);
740 $event->toggle_visibility(true);
741 } else {
742 $event = new calendar_event($event);
743 $event->toggle_visibility(false);
748 // Updating visible and visibleold to keep them in sync. Only changing a section visibility will
749 // affect visibleold to allow for an original visibility restore. See set_section_visible().
750 $cminfo = new stdClass();
751 $cminfo->id = $id;
752 $cminfo->visible = $visible;
753 $cminfo->visibleoncoursepage = $visibleoncoursepage;
754 $cminfo->visibleold = $visible;
755 $DB->update_record('course_modules', $cminfo);
757 // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there.
758 // Note that this must be done after updating the row in course_modules, in case
759 // the modules grade_item_update function needs to access $cm->visible.
760 if ($cm->visible != $visible &&
761 plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) &&
762 component_callback_exists('mod_' . $modulename, 'grade_item_update')) {
763 $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
764 component_callback('mod_' . $modulename, 'grade_item_update', array($instance));
765 } else if ($cm->visible != $visible) {
766 $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course));
767 if ($grade_items) {
768 foreach ($grade_items as $grade_item) {
769 $grade_item->set_hidden(!$visible);
774 \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
775 rebuild_course_cache($cm->course, false, true);
776 return true;
780 * Changes the course module name
782 * @param int $id course module id
783 * @param string $name new value for a name
784 * @return bool whether a change was made
786 function set_coursemodule_name($id, $name) {
787 global $CFG, $DB;
788 require_once($CFG->libdir . '/gradelib.php');
790 $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST);
792 $module = new \stdClass();
793 $module->id = $cm->instance;
795 // Escape strings as they would be by mform.
796 if (!empty($CFG->formatstringstriptags)) {
797 $module->name = clean_param($name, PARAM_TEXT);
798 } else {
799 $module->name = clean_param($name, PARAM_CLEANHTML);
801 if ($module->name === $cm->name || strval($module->name) === '') {
802 return false;
804 if (\core_text::strlen($module->name) > 255) {
805 throw new \moodle_exception('maximumchars', 'moodle', '', 255);
808 $module->timemodified = time();
809 $DB->update_record($cm->modname, $module);
810 $cm->name = $module->name;
811 \core\event\course_module_updated::create_from_cm($cm)->trigger();
812 \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
813 rebuild_course_cache($cm->course, false, true);
815 // Attempt to update the grade item if relevant.
816 $grademodule = $DB->get_record($cm->modname, array('id' => $cm->instance));
817 $grademodule->cmidnumber = $cm->idnumber;
818 $grademodule->modname = $cm->modname;
819 grade_update_mod_grades($grademodule);
821 // Update calendar events with the new name.
822 course_module_update_calendar_events($cm->modname, $grademodule, $cm);
824 return true;
828 * This function will handle the whole deletion process of a module. This includes calling
829 * the modules delete_instance function, deleting files, events, grades, conditional data,
830 * the data in the course_module and course_sections table and adding a module deletion
831 * event to the DB.
833 * @param int $cmid the course module id
834 * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook.
835 * @throws moodle_exception
836 * @since Moodle 2.5
838 function course_delete_module($cmid, $async = false) {
839 // Check the 'course_module_background_deletion_recommended' hook first.
840 // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
841 // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
842 // It's up to plugins to handle things like whether or not they are enabled.
843 if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
844 foreach ($pluginsfunction as $plugintype => $plugins) {
845 foreach ($plugins as $pluginfunction) {
846 if ($pluginfunction()) {
847 return course_module_flag_for_async_deletion($cmid);
853 global $CFG, $DB;
855 require_once($CFG->libdir.'/gradelib.php');
856 require_once($CFG->libdir.'/questionlib.php');
857 require_once($CFG->dirroot.'/blog/lib.php');
858 require_once($CFG->dirroot.'/calendar/lib.php');
860 // Get the course module.
861 if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
862 return true;
865 // Get the module context.
866 $modcontext = context_module::instance($cm->id);
868 // Get the course module name.
869 $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
871 // Get the file location of the delete_instance function for this module.
872 $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
874 // Include the file required to call the delete_instance function for this module.
875 if (file_exists($modlib)) {
876 require_once($modlib);
877 } else {
878 throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null,
879 "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
882 $deleteinstancefunction = $modulename . '_delete_instance';
884 // Ensure the delete_instance function exists for this module.
885 if (!function_exists($deleteinstancefunction)) {
886 throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
887 "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
890 // Allow plugins to use this course module before we completely delete it.
891 if ($pluginsfunction = get_plugins_with_function('pre_course_module_delete')) {
892 foreach ($pluginsfunction as $plugintype => $plugins) {
893 foreach ($plugins as $pluginfunction) {
894 $pluginfunction($cm);
899 // Call the delete_instance function, if it returns false throw an exception.
900 if (!$deleteinstancefunction($cm->instance)) {
901 throw new moodle_exception('cannotdeletemoduleinstance', '', '', null,
902 "Cannot delete the module $modulename (instance).");
905 question_delete_activity($cm);
907 // Remove all module files in case modules forget to do that.
908 $fs = get_file_storage();
909 $fs->delete_area_files($modcontext->id);
911 // Delete events from calendar.
912 if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) {
913 $coursecontext = context_course::instance($cm->course);
914 foreach($events as $event) {
915 $event->context = $coursecontext;
916 $calendarevent = calendar_event::load($event);
917 $calendarevent->delete();
921 // Delete grade items, outcome items and grades attached to modules.
922 if ($grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $modulename,
923 'iteminstance' => $cm->instance, 'courseid' => $cm->course))) {
924 foreach ($grade_items as $grade_item) {
925 $grade_item->delete('moddelete');
929 // Delete associated blogs and blog tag instances.
930 blog_remove_associations_for_module($modcontext->id);
932 // Delete completion and availability data; it is better to do this even if the
933 // features are not turned on, in case they were turned on previously (these will be
934 // very quick on an empty table).
935 $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id));
936 $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]);
937 $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id,
938 'course' => $cm->course,
939 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY));
941 // Delete all tag instances associated with the instance of this module.
942 core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id);
943 core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
945 // Notify the competency subsystem.
946 \core_competency\api::hook_course_module_deleted($cm);
948 // Delete the context.
949 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
951 // Delete the module from the course_modules table.
952 $DB->delete_records('course_modules', array('id' => $cm->id));
954 // Delete module from that section.
955 if (!delete_mod_from_section($cm->id, $cm->section)) {
956 throw new moodle_exception('cannotdeletemodulefromsection', '', '', null,
957 "Cannot delete the module $modulename (instance) from section.");
960 // Trigger event for course module delete action.
961 $event = \core\event\course_module_deleted::create(array(
962 'courseid' => $cm->course,
963 'context' => $modcontext,
964 'objectid' => $cm->id,
965 'other' => array(
966 'modulename' => $modulename,
967 'instanceid' => $cm->instance,
970 $event->add_record_snapshot('course_modules', $cm);
971 $event->trigger();
972 \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
973 rebuild_course_cache($cm->course, false, true);
977 * Schedule a course module for deletion in the background using an adhoc task.
979 * This method should not be called directly. Instead, please use course_delete_module($cmid, true), to denote async deletion.
980 * The real deletion of the module is handled by the task, which calls 'course_delete_module($cmid)'.
982 * @param int $cmid the course module id.
983 * @return bool whether the module was successfully scheduled for deletion.
984 * @throws \moodle_exception
986 function course_module_flag_for_async_deletion($cmid) {
987 global $CFG, $DB, $USER;
988 require_once($CFG->libdir.'/gradelib.php');
989 require_once($CFG->libdir.'/questionlib.php');
990 require_once($CFG->dirroot.'/blog/lib.php');
991 require_once($CFG->dirroot.'/calendar/lib.php');
993 // Get the course module.
994 if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
995 return true;
998 // We need to be reasonably certain the deletion is going to succeed before we background the process.
999 // Make the necessary delete_instance checks, etc. before proceeding further. Throw exceptions if required.
1001 // Get the course module name.
1002 $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
1004 // Get the file location of the delete_instance function for this module.
1005 $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
1007 // Include the file required to call the delete_instance function for this module.
1008 if (file_exists($modlib)) {
1009 require_once($modlib);
1010 } else {
1011 throw new \moodle_exception('cannotdeletemodulemissinglib', '', '', null,
1012 "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
1015 $deleteinstancefunction = $modulename . '_delete_instance';
1017 // Ensure the delete_instance function exists for this module.
1018 if (!function_exists($deleteinstancefunction)) {
1019 throw new \moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
1020 "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
1023 // We are going to defer the deletion as we can't be sure how long the module's pre_delete code will run for.
1024 $cm->deletioninprogress = '1';
1025 $DB->update_record('course_modules', $cm);
1027 // Create an adhoc task for the deletion of the course module. The task takes an array of course modules for removal.
1028 $removaltask = new \core_course\task\course_delete_modules();
1029 $removaltask->set_custom_data(array(
1030 'cms' => array($cm),
1031 'userid' => $USER->id,
1032 'realuserid' => \core\session\manager::get_realuser()->id
1035 // Queue the task for the next run.
1036 \core\task\manager::queue_adhoc_task($removaltask);
1038 // Reset the course cache to hide the module.
1039 rebuild_course_cache($cm->course, true);
1043 * Checks whether the given course has any course modules scheduled for adhoc deletion.
1045 * @param int $courseid the id of the course.
1046 * @param bool $onlygradable whether to check only gradable modules or all modules.
1047 * @return bool true if the course contains any modules pending deletion, false otherwise.
1049 function course_modules_pending_deletion(int $courseid, bool $onlygradable = false) : bool {
1050 if (empty($courseid)) {
1051 return false;
1054 if ($onlygradable) {
1055 // Fetch modules with grade items.
1056 if (!$coursegradeitems = grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid])) {
1057 // Return early when there is none.
1058 return false;
1062 $modinfo = get_fast_modinfo($courseid);
1063 foreach ($modinfo->get_cms() as $module) {
1064 if ($module->deletioninprogress == '1') {
1065 if ($onlygradable) {
1066 // Check if the module being deleted is in the list of course modules with grade items.
1067 foreach ($coursegradeitems as $coursegradeitem) {
1068 if ($coursegradeitem->itemmodule == $module->modname && $coursegradeitem->iteminstance == $module->instance) {
1069 // The module being deleted is within the gradable modules.
1070 return true;
1073 } else {
1074 return true;
1078 return false;
1082 * Checks whether the course module, as defined by modulename and instanceid, is scheduled for deletion within the given course.
1084 * @param int $courseid the course id.
1085 * @param string $modulename the module name. E.g. 'assign', 'book', etc.
1086 * @param int $instanceid the module instance id.
1087 * @return bool true if the course module is pending deletion, false otherwise.
1089 function course_module_instance_pending_deletion($courseid, $modulename, $instanceid) {
1090 if (empty($courseid) || empty($modulename) || empty($instanceid)) {
1091 return false;
1093 $modinfo = get_fast_modinfo($courseid);
1094 $instances = $modinfo->get_instances_of($modulename);
1095 return isset($instances[$instanceid]) && $instances[$instanceid]->deletioninprogress;
1098 function delete_mod_from_section($modid, $sectionid) {
1099 global $DB;
1101 if ($section = $DB->get_record("course_sections", array("id"=>$sectionid)) ) {
1103 $modarray = explode(",", $section->sequence);
1105 if ($key = array_keys ($modarray, $modid)) {
1106 array_splice($modarray, $key[0], 1);
1107 $newsequence = implode(",", $modarray);
1108 $DB->set_field("course_sections", "sequence", $newsequence, array("id"=>$section->id));
1109 rebuild_course_cache($section->course, true);
1110 return true;
1111 } else {
1112 return false;
1116 return false;
1120 * This function updates the calendar events from the information stored in the module table and the course
1121 * module table.
1123 * @param string $modulename Module name
1124 * @param stdClass $instance Module object. Either the $instance or the $cm must be supplied.
1125 * @param stdClass $cm Course module object. Either the $instance or the $cm must be supplied.
1126 * @return bool Returns true if calendar events are updated.
1127 * @since Moodle 3.3.4
1129 function course_module_update_calendar_events($modulename, $instance = null, $cm = null) {
1130 global $DB;
1132 if (isset($instance) || isset($cm)) {
1134 if (!isset($instance)) {
1135 $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
1137 if (!isset($cm)) {
1138 $cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course);
1140 if (!empty($cm)) {
1141 course_module_calendar_event_update_process($instance, $cm);
1143 return true;
1145 return false;
1149 * Update all instances through out the site or in a course.
1151 * @param string $modulename Module type to update.
1152 * @param integer $courseid Course id to update events. 0 for the whole site.
1153 * @return bool Returns True if the update was successful.
1154 * @since Moodle 3.3.4
1156 function course_module_bulk_update_calendar_events($modulename, $courseid = 0) {
1157 global $DB;
1159 $instances = null;
1160 if ($courseid) {
1161 if (!$instances = $DB->get_records($modulename, array('course' => $courseid))) {
1162 return false;
1164 } else {
1165 if (!$instances = $DB->get_records($modulename)) {
1166 return false;
1170 foreach ($instances as $instance) {
1171 if ($cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course)) {
1172 course_module_calendar_event_update_process($instance, $cm);
1175 return true;
1179 * Calendar events for a module instance are updated.
1181 * @param stdClass $instance Module instance object.
1182 * @param stdClass $cm Course Module object.
1183 * @since Moodle 3.3.4
1185 function course_module_calendar_event_update_process($instance, $cm) {
1186 // We need to call *_refresh_events() first because some modules delete 'old' events at the end of the code which
1187 // will remove the completion events.
1188 $refresheventsfunction = $cm->modname . '_refresh_events';
1189 if (function_exists($refresheventsfunction)) {
1190 call_user_func($refresheventsfunction, $cm->course, $instance, $cm);
1192 $completionexpected = (!empty($cm->completionexpected)) ? $cm->completionexpected : null;
1193 \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $instance, $completionexpected);
1197 * Moves a section within a course, from a position to another.
1198 * Be very careful: $section and $destination refer to section number,
1199 * not id!.
1201 * @param object $course
1202 * @param int $section Section number (not id!!!)
1203 * @param int $destination
1204 * @param bool $ignorenumsections
1205 * @return boolean Result
1207 function move_section_to($course, $section, $destination, $ignorenumsections = false) {
1208 /// Moves a whole course section up and down within the course
1209 global $USER, $DB;
1211 if (!$destination && $destination != 0) {
1212 return true;
1215 // compartibility with course formats using field 'numsections'
1216 $courseformatoptions = course_get_format($course)->get_format_options();
1217 if ((!$ignorenumsections && array_key_exists('numsections', $courseformatoptions) &&
1218 ($destination > $courseformatoptions['numsections'])) || ($destination < 1)) {
1219 return false;
1222 // Get all sections for this course and re-order them (2 of them should now share the same section number)
1223 if (!$sections = $DB->get_records_menu('course_sections', array('course' => $course->id),
1224 'section ASC, id ASC', 'id, section')) {
1225 return false;
1228 $movedsections = reorder_sections($sections, $section, $destination);
1230 // Update all sections. Do this in 2 steps to avoid breaking database
1231 // uniqueness constraint
1232 $transaction = $DB->start_delegated_transaction();
1233 foreach ($movedsections as $id => $position) {
1234 if ((int) $sections[$id] !== $position) {
1235 $DB->set_field('course_sections', 'section', -$position, ['id' => $id]);
1236 // Invalidate the section cache by given section id.
1237 course_modinfo::purge_course_section_cache_by_id($course->id, $id);
1240 foreach ($movedsections as $id => $position) {
1241 if ((int) $sections[$id] !== $position) {
1242 $DB->set_field('course_sections', 'section', $position, ['id' => $id]);
1243 // Invalidate the section cache by given section id.
1244 course_modinfo::purge_course_section_cache_by_id($course->id, $id);
1248 // If we move the highlighted section itself, then just highlight the destination.
1249 // Adjust the higlighted section location if we move something over it either direction.
1250 if ($section == $course->marker) {
1251 course_set_marker($course->id, $destination);
1252 } else if ($section > $course->marker && $course->marker >= $destination) {
1253 course_set_marker($course->id, $course->marker+1);
1254 } else if ($section < $course->marker && $course->marker <= $destination) {
1255 course_set_marker($course->id, $course->marker-1);
1258 $transaction->allow_commit();
1259 rebuild_course_cache($course->id, true, true);
1260 return true;
1264 * This method will delete a course section and may delete all modules inside it.
1266 * No permissions are checked here, use {@link course_can_delete_section()} to
1267 * check if section can actually be deleted.
1269 * @param int|stdClass $course
1270 * @param int|stdClass|section_info $section
1271 * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
1272 * @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook.
1273 * @return bool whether section was deleted
1275 function course_delete_section($course, $section, $forcedeleteifnotempty = true, $async = false) {
1276 global $DB;
1278 // Prepare variables.
1279 $courseid = (is_object($course)) ? $course->id : (int)$course;
1280 $sectionnum = (is_object($section)) ? $section->section : (int)$section;
1281 $section = $DB->get_record('course_sections', array('course' => $courseid, 'section' => $sectionnum));
1282 if (!$section) {
1283 // No section exists, can't proceed.
1284 return false;
1287 // Check the 'course_module_background_deletion_recommended' hook first.
1288 // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
1289 // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
1290 // It's up to plugins to handle things like whether or not they are enabled.
1291 if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
1292 foreach ($pluginsfunction as $plugintype => $plugins) {
1293 foreach ($plugins as $pluginfunction) {
1294 if ($pluginfunction()) {
1295 return course_delete_section_async($section, $forcedeleteifnotempty);
1301 $format = course_get_format($course);
1302 $sectionname = $format->get_section_name($section);
1304 // Delete section.
1305 $result = $format->delete_section($section, $forcedeleteifnotempty);
1307 // Trigger an event for course section deletion.
1308 if ($result) {
1309 $context = context_course::instance($courseid);
1310 $event = \core\event\course_section_deleted::create(
1311 array(
1312 'objectid' => $section->id,
1313 'courseid' => $courseid,
1314 'context' => $context,
1315 'other' => array(
1316 'sectionnum' => $section->section,
1317 'sectionname' => $sectionname,
1321 $event->add_record_snapshot('course_sections', $section);
1322 $event->trigger();
1324 return $result;
1328 * Course section deletion, using an adhoc task for deletion of the modules it contains.
1329 * 1. Schedule all modules within the section for adhoc removal.
1330 * 2. Move all modules to course section 0.
1331 * 3. Delete the resulting empty section.
1333 * @param \stdClass $section the section to schedule for deletion.
1334 * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules.
1335 * @return bool true if the section was scheduled for deletion, false otherwise.
1337 function course_delete_section_async($section, $forcedeleteifnotempty = true) {
1338 global $DB, $USER;
1340 // Objects only, and only valid ones.
1341 if (!is_object($section) || empty($section->id)) {
1342 return false;
1345 // Does the object currently exist in the DB for removal (check for stale objects).
1346 $section = $DB->get_record('course_sections', array('id' => $section->id));
1347 if (!$section || !$section->section) {
1348 // No section exists, or the section is 0. Can't proceed.
1349 return false;
1352 // Check whether the section can be removed.
1353 if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
1354 return false;
1357 $format = course_get_format($section->course);
1358 $sectionname = $format->get_section_name($section);
1360 // Flag those modules having no existing deletion flag. Some modules may have been scheduled for deletion manually, and we don't
1361 // want to create additional adhoc deletion tasks for these. Moving them to section 0 will suffice.
1362 $affectedmods = $DB->get_records_select('course_modules', 'course = ? AND section = ? AND deletioninprogress <> ?',
1363 [$section->course, $section->id, 1], '', 'id');
1364 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $section->course, 'section' => $section->id]);
1366 // Move all modules to section 0.
1367 $modules = $DB->get_records('course_modules', ['section' => $section->id], '');
1368 $sectionzero = $DB->get_record('course_sections', ['course' => $section->course, 'section' => '0']);
1369 foreach ($modules as $mod) {
1370 moveto_module($mod, $sectionzero);
1373 // Create and queue an adhoc task for the deletion of the modules.
1374 $removaltask = new \core_course\task\course_delete_modules();
1375 $data = array(
1376 'cms' => $affectedmods,
1377 'userid' => $USER->id,
1378 'realuserid' => \core\session\manager::get_realuser()->id
1380 $removaltask->set_custom_data($data);
1381 \core\task\manager::queue_adhoc_task($removaltask);
1383 // Delete the now empty section, passing in only the section number, which forces the function to fetch a new object.
1384 // The refresh is needed because the section->sequence is now stale.
1385 $result = $format->delete_section($section->section, $forcedeleteifnotempty);
1387 // Trigger an event for course section deletion.
1388 if ($result) {
1389 $context = \context_course::instance($section->course);
1390 $event = \core\event\course_section_deleted::create(
1391 array(
1392 'objectid' => $section->id,
1393 'courseid' => $section->course,
1394 'context' => $context,
1395 'other' => array(
1396 'sectionnum' => $section->section,
1397 'sectionname' => $sectionname,
1401 $event->add_record_snapshot('course_sections', $section);
1402 $event->trigger();
1404 rebuild_course_cache($section->course, true);
1406 return $result;
1410 * Updates the course section
1412 * This function does not check permissions or clean values - this has to be done prior to calling it.
1414 * @param int|stdClass $course
1415 * @param stdClass $section record from course_sections table - it will be updated with the new values
1416 * @param array|stdClass $data
1418 function course_update_section($course, $section, $data) {
1419 global $DB;
1421 $courseid = (is_object($course)) ? $course->id : (int)$course;
1423 // Some fields can not be updated using this method.
1424 $data = array_diff_key((array)$data, array('id', 'course', 'section', 'sequence'));
1425 $changevisibility = (array_key_exists('visible', $data) && (bool)$data['visible'] != (bool)$section->visible);
1426 if (array_key_exists('name', $data) && \core_text::strlen($data['name']) > 255) {
1427 throw new moodle_exception('maximumchars', 'moodle', '', 255);
1430 // Update record in the DB and course format options.
1431 $data['id'] = $section->id;
1432 $data['timemodified'] = time();
1433 $DB->update_record('course_sections', $data);
1434 // Invalidate the section cache by given section id.
1435 course_modinfo::purge_course_section_cache_by_id($courseid, $section->id);
1436 rebuild_course_cache($courseid, false, true);
1437 course_get_format($courseid)->update_section_format_options($data);
1439 // Update fields of the $section object.
1440 foreach ($data as $key => $value) {
1441 if (property_exists($section, $key)) {
1442 $section->$key = $value;
1446 // Trigger an event for course section update.
1447 $event = \core\event\course_section_updated::create(
1448 array(
1449 'objectid' => $section->id,
1450 'courseid' => $courseid,
1451 'context' => context_course::instance($courseid),
1452 'other' => array('sectionnum' => $section->section)
1455 $event->trigger();
1457 // If section visibility was changed, hide the modules in this section too.
1458 if ($changevisibility && !empty($section->sequence)) {
1459 $modules = explode(',', $section->sequence);
1460 foreach ($modules as $moduleid) {
1461 if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) {
1462 if ($data['visible']) {
1463 // As we unhide the section, we use the previously saved visibility stored in visibleold.
1464 set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage);
1465 } else {
1466 // We hide the section, so we hide the module but we store the original state in visibleold.
1467 set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage);
1468 $DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]);
1469 \course_modinfo::purge_course_module_cache($cm->course, $cm->id);
1471 \core\event\course_module_updated::create_from_cm($cm)->trigger();
1474 rebuild_course_cache($courseid, false, true);
1479 * Checks if the current user can delete a section (if course format allows it and user has proper permissions).
1481 * @param int|stdClass $course
1482 * @param int|stdClass|section_info $section
1483 * @return bool
1485 function course_can_delete_section($course, $section) {
1486 if (is_object($section)) {
1487 $section = $section->section;
1489 if (!$section) {
1490 // Not possible to delete 0-section.
1491 return false;
1493 // Course format should allow to delete sections.
1494 if (!course_get_format($course)->can_delete_section($section)) {
1495 return false;
1497 // Make sure user has capability to update course and move sections.
1498 $context = context_course::instance(is_object($course) ? $course->id : $course);
1499 if (!has_all_capabilities(array('moodle/course:movesections', 'moodle/course:update'), $context)) {
1500 return false;
1502 // Make sure user has capability to delete each activity in this section.
1503 $modinfo = get_fast_modinfo($course);
1504 if (!empty($modinfo->sections[$section])) {
1505 foreach ($modinfo->sections[$section] as $cmid) {
1506 if (!has_capability('moodle/course:manageactivities', context_module::instance($cmid))) {
1507 return false;
1511 return true;
1515 * Reordering algorithm for course sections. Given an array of section->section indexed by section->id,
1516 * an original position number and a target position number, rebuilds the array so that the
1517 * move is made without any duplication of section positions.
1518 * Note: The target_position is the position AFTER WHICH the moved section will be inserted. If you want to
1519 * insert a section before the first one, you must give 0 as the target (section 0 can never be moved).
1521 * @param array $sections
1522 * @param int $origin_position
1523 * @param int $target_position
1524 * @return array
1526 function reorder_sections($sections, $origin_position, $target_position) {
1527 if (!is_array($sections)) {
1528 return false;
1531 // We can't move section position 0
1532 if ($origin_position < 1) {
1533 echo "We can't move section position 0";
1534 return false;
1537 // Locate origin section in sections array
1538 if (!$origin_key = array_search($origin_position, $sections)) {
1539 echo "searched position not in sections array";
1540 return false; // searched position not in sections array
1543 // Extract origin section
1544 $origin_section = $sections[$origin_key];
1545 unset($sections[$origin_key]);
1547 // Find offset of target position (stupid PHP's array_splice requires offset instead of key index!)
1548 $found = false;
1549 $append_array = array();
1550 foreach ($sections as $id => $position) {
1551 if ($found) {
1552 $append_array[$id] = $position;
1553 unset($sections[$id]);
1555 if ($position == $target_position) {
1556 if ($target_position < $origin_position) {
1557 $append_array[$id] = $position;
1558 unset($sections[$id]);
1560 $found = true;
1564 // Append moved section
1565 $sections[$origin_key] = $origin_section;
1567 // Append rest of array (if applicable)
1568 if (!empty($append_array)) {
1569 foreach ($append_array as $id => $position) {
1570 $sections[$id] = $position;
1574 // Renumber positions
1575 $position = 0;
1576 foreach ($sections as $id => $p) {
1577 $sections[$id] = $position;
1578 $position++;
1581 return $sections;
1586 * Move the module object $mod to the specified $section
1587 * If $beforemod exists then that is the module
1588 * before which $modid should be inserted
1590 * @param stdClass|cm_info $mod
1591 * @param stdClass|section_info $section
1592 * @param int|stdClass $beforemod id or object with field id corresponding to the module
1593 * before which the module needs to be included. Null for inserting in the
1594 * end of the section
1595 * @return int new value for module visibility (0 or 1)
1597 function moveto_module($mod, $section, $beforemod=NULL) {
1598 global $OUTPUT, $DB;
1600 // Current module visibility state - return value of this function.
1601 $modvisible = $mod->visible;
1603 // Remove original module from original section.
1604 if (! delete_mod_from_section($mod->id, $mod->section)) {
1605 echo $OUTPUT->notification("Could not delete module from existing section");
1608 // Add the module into the new section.
1609 course_add_cm_to_section($section->course, $mod->id, $section->section, $beforemod);
1611 // If moving to a hidden section then hide module.
1612 if ($mod->section != $section->id) {
1613 if (!$section->visible && $mod->visible) {
1614 // Module was visible but must become hidden after moving to hidden section.
1615 $modvisible = 0;
1616 set_coursemodule_visible($mod->id, 0);
1617 // Set visibleold to 1 so module will be visible when section is made visible.
1618 $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id));
1620 if ($section->visible && !$mod->visible) {
1621 // Hidden module was moved to the visible section, restore the module visibility from visibleold.
1622 set_coursemodule_visible($mod->id, $mod->visibleold);
1623 $modvisible = $mod->visibleold;
1627 return $modvisible;
1631 * Returns the list of all editing actions that current user can perform on the module
1633 * @param cm_info $mod The module to produce editing buttons for
1634 * @param int $indent The current indenting (default -1 means no move left-right actions)
1635 * @param int $sr The section to link back to (used for creating the links)
1636 * @return array array of action_link or pix_icon objects
1638 function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
1639 global $COURSE, $SITE, $CFG;
1641 static $str;
1643 $coursecontext = context_course::instance($mod->course);
1644 $modcontext = context_module::instance($mod->id);
1645 $courseformat = course_get_format($mod->get_course());
1646 $usecomponents = $courseformat->supports_components();
1648 $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign');
1649 $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport');
1651 // No permission to edit anything.
1652 if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) {
1653 return array();
1656 $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
1658 if (!isset($str)) {
1659 $str = get_strings(array('delete', 'move', 'moveright', 'moveleft',
1660 'editsettings', 'duplicate', 'modhide', 'makeavailable', 'makeunavailable', 'modshow'), 'moodle');
1661 $str->assign = get_string('assignroles', 'role');
1662 $str->groupsnone = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsnone"));
1663 $str->groupsseparate = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsseparate"));
1664 $str->groupsvisible = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsvisible"));
1667 $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
1669 if ($sr !== null) {
1670 $baseurl->param('sr', $sr);
1672 $actions = array();
1674 // Update.
1675 if ($hasmanageactivities) {
1676 $actions['update'] = new action_menu_link_secondary(
1677 new moodle_url($baseurl, array('update' => $mod->id)),
1678 new pix_icon('t/edit', '', 'moodle', array('class' => 'iconsmall')),
1679 $str->editsettings,
1680 array('class' => 'editing_update', 'data-action' => 'update')
1684 // Move (only for component compatible formats).
1685 if ($usecomponents) {
1686 $actions['move'] = new action_menu_link_secondary(
1687 new moodle_url($baseurl, [
1688 'sesskey' => sesskey(),
1689 'copy' => $mod->id,
1691 new pix_icon('i/dragdrop', '', 'moodle', ['class' => 'iconsmall']),
1692 $str->move,
1694 'class' => 'editing_movecm',
1695 'data-action' => 'moveCm',
1696 'data-id' => $mod->id,
1701 // Indent.
1702 if ($hasmanageactivities && $indent >= 0) {
1703 $indentlimits = new stdClass();
1704 $indentlimits->min = 0;
1705 $indentlimits->max = 16;
1706 if (right_to_left()) { // Exchange arrows on RTL
1707 $rightarrow = 't/left';
1708 $leftarrow = 't/right';
1709 } else {
1710 $rightarrow = 't/right';
1711 $leftarrow = 't/left';
1714 if ($indent >= $indentlimits->max) {
1715 $enabledclass = 'hidden';
1716 } else {
1717 $enabledclass = '';
1719 $actions['moveright'] = new action_menu_link_secondary(
1720 new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
1721 new pix_icon($rightarrow, '', 'moodle', array('class' => 'iconsmall')),
1722 $str->moveright,
1723 array('class' => 'editing_moveright ' . $enabledclass, 'data-action' => 'moveright',
1724 'data-keepopen' => true, 'data-sectionreturn' => $sr)
1727 if ($indent <= $indentlimits->min) {
1728 $enabledclass = 'hidden';
1729 } else {
1730 $enabledclass = '';
1732 $actions['moveleft'] = new action_menu_link_secondary(
1733 new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
1734 new pix_icon($leftarrow, '', 'moodle', array('class' => 'iconsmall')),
1735 $str->moveleft,
1736 array('class' => 'editing_moveleft ' . $enabledclass, 'data-action' => 'moveleft',
1737 'data-keepopen' => true, 'data-sectionreturn' => $sr)
1742 // Hide/Show/Available/Unavailable.
1743 if (has_capability('moodle/course:activityvisibility', $modcontext)) {
1744 $allowstealth = !empty($CFG->allowstealth) && $courseformat->allow_stealth_module_visibility($mod, $mod->get_section_info());
1746 $sectionvisible = $mod->get_section_info()->visible;
1747 // The module on the course page may be in one of the following states:
1748 // - Available and displayed on the course page ($displayedoncoursepage);
1749 // - Not available and not displayed on the course page ($unavailable);
1750 // - Available but not displayed on the course page ($stealth) - this can also be a visible activity in a hidden section.
1751 $displayedoncoursepage = $mod->visible && $mod->visibleoncoursepage && $sectionvisible;
1752 $unavailable = !$mod->visible;
1753 $stealth = $mod->visible && (!$mod->visibleoncoursepage || !$sectionvisible);
1754 if ($displayedoncoursepage) {
1755 $actions['hide'] = new action_menu_link_secondary(
1756 new moodle_url($baseurl, array('hide' => $mod->id)),
1757 new pix_icon('t/hide', '', 'moodle', array('class' => 'iconsmall')),
1758 $str->modhide,
1760 'class' => 'editing_hide',
1761 'data-action' => ($usecomponents) ? 'cmHide' : 'hide',
1762 'data-id' => $mod->id,
1765 } else if (!$displayedoncoursepage && $sectionvisible) {
1766 // Offer to "show" only if the section is visible.
1767 $actions['show'] = new action_menu_link_secondary(
1768 new moodle_url($baseurl, array('show' => $mod->id)),
1769 new pix_icon('t/show', '', 'moodle', array('class' => 'iconsmall')),
1770 $str->modshow,
1772 'class' => 'editing_show',
1773 'data-action' => ($usecomponents) ? 'cmShow' : 'show',
1774 'data-id' => $mod->id,
1779 if ($stealth) {
1780 // When making the "stealth" module unavailable we perform the same action as hiding the visible module.
1781 $actions['hide'] = new action_menu_link_secondary(
1782 new moodle_url($baseurl, array('hide' => $mod->id)),
1783 new pix_icon('t/unblock', '', 'moodle', array('class' => 'iconsmall')),
1784 $str->makeunavailable,
1786 'class' => 'editing_makeunavailable',
1787 'data-action' => ($usecomponents) ? 'cmHide' : 'hide',
1788 'data-sectionreturn' => $sr,
1789 'data-id' => $mod->id,
1792 } else if ($unavailable && (!$sectionvisible || $allowstealth) && $mod->has_view()) {
1793 // Allow to make visually hidden module available in gradebook and other reports by making it a "stealth" module.
1794 // When the section is hidden it is an equivalent of "showing" the module.
1795 // Activities without the link (i.e. labels) can not be made available but hidden on course page.
1796 $action = $sectionvisible ? 'stealth' : 'show';
1797 if ($usecomponents) {
1798 $action = 'cm' . ucfirst($action);
1800 $actions[$action] = new action_menu_link_secondary(
1801 new moodle_url($baseurl, array('stealth' => $mod->id)),
1802 new pix_icon('t/block', '', 'moodle', array('class' => 'iconsmall')),
1803 $str->makeavailable,
1805 'class' => 'editing_makeavailable',
1806 'data-action' => $action,
1807 'data-sectionreturn' => $sr,
1808 'data-id' => $mod->id,
1814 // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php)
1815 if (has_all_capabilities($dupecaps, $coursecontext) &&
1816 plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) &&
1817 course_allowed_module($mod->get_course(), $mod->modname)) {
1818 $actions['duplicate'] = new action_menu_link_secondary(
1819 new moodle_url($baseurl, array('duplicate' => $mod->id)),
1820 new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')),
1821 $str->duplicate,
1822 array('class' => 'editing_duplicate', 'data-action' => 'duplicate', 'data-sectionreturn' => $sr)
1826 // Assign.
1827 if (has_capability('moodle/role:assign', $modcontext)){
1828 $actions['assign'] = new action_menu_link_secondary(
1829 new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
1830 new pix_icon('t/assignroles', '', 'moodle', array('class' => 'iconsmall')),
1831 $str->assign,
1832 array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr)
1836 // Delete.
1837 if ($hasmanageactivities) {
1838 $actions['delete'] = new action_menu_link_secondary(
1839 new moodle_url($baseurl, array('delete' => $mod->id)),
1840 new pix_icon('t/delete', '', 'moodle', array('class' => 'iconsmall')),
1841 $str->delete,
1842 array('class' => 'editing_delete', 'data-action' => 'delete', 'data-sectionreturn' => $sr)
1846 return $actions;
1850 * Returns the move action.
1852 * @param cm_info $mod The module to produce a move button for
1853 * @param int $sr The section to link back to (used for creating the links)
1854 * @return The markup for the move action, or an empty string if not available.
1856 function course_get_cm_move(cm_info $mod, $sr = null) {
1857 global $OUTPUT;
1859 static $str;
1860 static $baseurl;
1862 $modcontext = context_module::instance($mod->id);
1863 $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
1865 if (!isset($str)) {
1866 $str = get_strings(array('move'));
1869 if (!isset($baseurl)) {
1870 $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
1872 if ($sr !== null) {
1873 $baseurl->param('sr', $sr);
1877 if ($hasmanageactivities) {
1878 $pixicon = 'i/dragdrop';
1880 if (!course_ajax_enabled($mod->get_course())) {
1881 // Override for course frontpage until we get drag/drop working there.
1882 $pixicon = 't/move';
1885 $attributes = [
1886 'class' => 'editing_move',
1887 'data-action' => 'move',
1888 'data-sectionreturn' => $sr,
1889 'title' => $str->move,
1890 'aria-label' => $str->move,
1892 return html_writer::link(
1893 new moodle_url($baseurl, ['copy' => $mod->id]),
1894 $OUTPUT->pix_icon($pixicon, '', 'moodle', ['class' => 'iconsmall']),
1895 $attributes
1898 return '';
1902 * given a course object with shortname & fullname, this function will
1903 * truncate the the number of chars allowed and add ... if it was too long
1905 function course_format_name ($course,$max=100) {
1907 $context = context_course::instance($course->id);
1908 $shortname = format_string($course->shortname, true, array('context' => $context));
1909 $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
1910 $str = $shortname.': '. $fullname;
1911 if (core_text::strlen($str) <= $max) {
1912 return $str;
1914 else {
1915 return core_text::substr($str,0,$max-3).'...';
1920 * Is the user allowed to add this type of module to this course?
1921 * @param object $course the course settings. Only $course->id is used.
1922 * @param string $modname the module name. E.g. 'forum' or 'quiz'.
1923 * @param \stdClass $user the user to check, defaults to the global user if not provided.
1924 * @return bool whether the current user is allowed to add this type of module to this course.
1926 function course_allowed_module($course, $modname, \stdClass $user = null) {
1927 global $USER;
1928 $user = $user ?? $USER;
1929 if (is_numeric($modname)) {
1930 throw new coding_exception('Function course_allowed_module no longer
1931 supports numeric module ids. Please update your code to pass the module name.');
1934 $capability = 'mod/' . $modname . ':addinstance';
1935 if (!get_capability_info($capability)) {
1936 // Debug warning that the capability does not exist, but no more than once per page.
1937 static $warned = array();
1938 $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
1939 if (!isset($warned[$modname]) && $archetype !== MOD_ARCHETYPE_SYSTEM) {
1940 debugging('The module ' . $modname . ' does not define the standard capability ' .
1941 $capability , DEBUG_DEVELOPER);
1942 $warned[$modname] = 1;
1945 // If the capability does not exist, the module can always be added.
1946 return true;
1949 $coursecontext = context_course::instance($course->id);
1950 return has_capability($capability, $coursecontext, $user);
1954 * Efficiently moves many courses around while maintaining
1955 * sortorder in order.
1957 * @param array $courseids is an array of course ids
1958 * @param int $categoryid
1959 * @return bool success
1961 function move_courses($courseids, $categoryid) {
1962 global $DB;
1964 if (empty($courseids)) {
1965 // Nothing to do.
1966 return false;
1969 if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) {
1970 return false;
1973 $courseids = array_reverse($courseids);
1974 $newparent = context_coursecat::instance($category->id);
1975 $i = 1;
1977 list($where, $params) = $DB->get_in_or_equal($courseids);
1978 $dbcourses = $DB->get_records_select('course', 'id ' . $where, $params, '', 'id, category, shortname, fullname');
1979 foreach ($dbcourses as $dbcourse) {
1980 $course = new stdClass();
1981 $course->id = $dbcourse->id;
1982 $course->timemodified = time();
1983 $course->category = $category->id;
1984 $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++;
1985 if ($category->visible == 0) {
1986 // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get
1987 // to previous state if somebody unhides the category.
1988 $course->visible = 0;
1991 $DB->update_record('course', $course);
1993 // Update context, so it can be passed to event.
1994 $context = context_course::instance($course->id);
1995 $context->update_moved($newparent);
1997 // Trigger a course updated event.
1998 $event = \core\event\course_updated::create(array(
1999 'objectid' => $course->id,
2000 'context' => context_course::instance($course->id),
2001 'other' => array('shortname' => $dbcourse->shortname,
2002 'fullname' => $dbcourse->fullname,
2003 'updatedfields' => array('category' => $category->id))
2005 $event->set_legacy_logdata(array($course->id, 'course', 'move', 'edit.php?id=' . $course->id, $course->id));
2006 $event->trigger();
2008 fix_course_sortorder();
2009 cache_helper::purge_by_event('changesincourse');
2011 return true;
2015 * Returns the display name of the given section that the course prefers
2017 * Implementation of this function is provided by course format
2018 * @see core_courseformat\base::get_section_name()
2020 * @param int|stdClass $courseorid The course to get the section name for (object or just course id)
2021 * @param int|stdClass $section Section object from database or just field course_sections.section
2022 * @return string Display name that the course format prefers, e.g. "Week 2"
2024 function get_section_name($courseorid, $section) {
2025 return course_get_format($courseorid)->get_section_name($section);
2029 * Tells if current course format uses sections
2031 * @param string $format Course format ID e.g. 'weeks' $course->format
2032 * @return bool
2034 function course_format_uses_sections($format) {
2035 $course = new stdClass();
2036 $course->format = $format;
2037 return course_get_format($course)->uses_sections();
2041 * Returns the information about the ajax support in the given source format
2043 * The returned object's property (boolean)capable indicates that
2044 * the course format supports Moodle course ajax features.
2046 * @param string $format
2047 * @return stdClass
2049 function course_format_ajax_support($format) {
2050 $course = new stdClass();
2051 $course->format = $format;
2052 return course_get_format($course)->supports_ajax();
2056 * Can the current user delete this course?
2057 * Course creators have exception,
2058 * 1 day after the creation they can sill delete the course.
2059 * @param int $courseid
2060 * @return boolean
2062 function can_delete_course($courseid) {
2063 global $USER;
2065 $context = context_course::instance($courseid);
2067 if (has_capability('moodle/course:delete', $context)) {
2068 return true;
2071 // hack: now try to find out if creator created this course recently (1 day)
2072 if (!has_capability('moodle/course:create', $context)) {
2073 return false;
2076 $since = time() - 60*60*24;
2077 $course = get_course($courseid);
2079 if ($course->timecreated < $since) {
2080 return false; // Return if the course was not created in last 24 hours.
2083 $logmanger = get_log_manager();
2084 $readers = $logmanger->get_readers('\core\log\sql_reader');
2085 $reader = reset($readers);
2087 if (empty($reader)) {
2088 return false; // No log reader found.
2091 // A proper reader.
2092 $select = "userid = :userid AND courseid = :courseid AND eventname = :eventname AND timecreated > :since";
2093 $params = array('userid' => $USER->id, 'since' => $since, 'courseid' => $course->id, 'eventname' => '\core\event\course_created');
2095 return (bool)$reader->get_events_select_count($select, $params);
2099 * Save the Your name for 'Some role' strings.
2101 * @param integer $courseid the id of this course.
2102 * @param array $data the data that came from the course settings form.
2104 function save_local_role_names($courseid, $data) {
2105 global $DB;
2106 $context = context_course::instance($courseid);
2108 foreach ($data as $fieldname => $value) {
2109 if (strpos($fieldname, 'role_') !== 0) {
2110 continue;
2112 list($ignored, $roleid) = explode('_', $fieldname);
2114 // make up our mind whether we want to delete, update or insert
2115 if (!$value) {
2116 $DB->delete_records('role_names', array('contextid' => $context->id, 'roleid' => $roleid));
2118 } else if ($rolename = $DB->get_record('role_names', array('contextid' => $context->id, 'roleid' => $roleid))) {
2119 $rolename->name = $value;
2120 $DB->update_record('role_names', $rolename);
2122 } else {
2123 $rolename = new stdClass;
2124 $rolename->contextid = $context->id;
2125 $rolename->roleid = $roleid;
2126 $rolename->name = $value;
2127 $DB->insert_record('role_names', $rolename);
2129 // This will ensure the course contacts cache is purged..
2130 core_course_category::role_assignment_changed($roleid, $context);
2135 * Returns options to use in course overviewfiles filemanager
2137 * @param null|stdClass|core_course_list_element|int $course either object that has 'id' property or just the course id;
2138 * may be empty if course does not exist yet (course create form)
2139 * @return array|null array of options such as maxfiles, maxbytes, accepted_types, etc.
2140 * or null if overviewfiles are disabled
2142 function course_overviewfiles_options($course) {
2143 global $CFG;
2144 if (empty($CFG->courseoverviewfileslimit)) {
2145 return null;
2148 // Create accepted file types based on config value, falling back to default all.
2149 $acceptedtypes = (new \core_form\filetypes_util)->normalize_file_types($CFG->courseoverviewfilesext);
2150 if (in_array('*', $acceptedtypes) || empty($acceptedtypes)) {
2151 $acceptedtypes = '*';
2154 $options = array(
2155 'maxfiles' => $CFG->courseoverviewfileslimit,
2156 'maxbytes' => $CFG->maxbytes,
2157 'subdirs' => 0,
2158 'accepted_types' => $acceptedtypes
2160 if (!empty($course->id)) {
2161 $options['context'] = context_course::instance($course->id);
2162 } else if (is_int($course) && $course > 0) {
2163 $options['context'] = context_course::instance($course);
2165 return $options;
2169 * Create a course and either return a $course object
2171 * Please note this functions does not verify any access control,
2172 * the calling code is responsible for all validation (usually it is the form definition).
2174 * @param array $editoroptions course description editor options
2175 * @param object $data - all the data needed for an entry in the 'course' table
2176 * @return object new course instance
2178 function create_course($data, $editoroptions = NULL) {
2179 global $DB, $CFG;
2181 //check the categoryid - must be given for all new courses
2182 $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST);
2184 // Check if the shortname already exists.
2185 if (!empty($data->shortname)) {
2186 if ($DB->record_exists('course', array('shortname' => $data->shortname))) {
2187 throw new moodle_exception('shortnametaken', '', '', $data->shortname);
2191 // Check if the idnumber already exists.
2192 if (!empty($data->idnumber)) {
2193 if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) {
2194 throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
2198 if (empty($CFG->enablecourserelativedates)) {
2199 // Make sure we're not setting the relative dates mode when the setting is disabled.
2200 unset($data->relativedatesmode);
2203 if ($errorcode = course_validate_dates((array)$data)) {
2204 throw new moodle_exception($errorcode);
2207 // Check if timecreated is given.
2208 $data->timecreated = !empty($data->timecreated) ? $data->timecreated : time();
2209 $data->timemodified = $data->timecreated;
2211 // place at beginning of any category
2212 $data->sortorder = 0;
2214 if ($editoroptions) {
2215 // summary text is updated later, we need context to store the files first
2216 $data->summary = '';
2217 $data->summary_format = FORMAT_HTML;
2220 // Get default completion settings as a fallback in case the enablecompletion field is not set.
2221 $courseconfig = get_config('moodlecourse');
2222 $defaultcompletion = !empty($CFG->enablecompletion) ? $courseconfig->enablecompletion : COMPLETION_DISABLED;
2223 $enablecompletion = $data->enablecompletion ?? $defaultcompletion;
2224 // Unset showcompletionconditions when completion tracking is not enabled for the course.
2225 if ($enablecompletion == COMPLETION_DISABLED) {
2226 unset($data->showcompletionconditions);
2227 } else if (!isset($data->showcompletionconditions)) {
2228 // Show completion conditions should have a default value when completion is enabled. Set it to the site defaults.
2229 // This scenario can happen when a course is created through data generators or through a web service.
2230 $data->showcompletionconditions = $courseconfig->showcompletionconditions;
2233 if (!isset($data->visible)) {
2234 // data not from form, add missing visibility info
2235 $data->visible = $category->visible;
2237 $data->visibleold = $data->visible;
2239 $newcourseid = $DB->insert_record('course', $data);
2240 $context = context_course::instance($newcourseid, MUST_EXIST);
2242 if ($editoroptions) {
2243 // Save the files used in the summary editor and store
2244 $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
2245 $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid));
2246 $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid));
2248 if ($overviewfilesoptions = course_overviewfiles_options($newcourseid)) {
2249 // Save the course overviewfiles
2250 $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2253 // update course format options
2254 course_get_format($newcourseid)->update_course_format_options($data);
2256 $course = course_get_format($newcourseid)->get_course();
2258 fix_course_sortorder();
2259 // purge appropriate caches in case fix_course_sortorder() did not change anything
2260 cache_helper::purge_by_event('changesincourse');
2262 // Trigger a course created event.
2263 $event = \core\event\course_created::create(array(
2264 'objectid' => $course->id,
2265 'context' => context_course::instance($course->id),
2266 'other' => array('shortname' => $course->shortname,
2267 'fullname' => $course->fullname)
2270 $event->trigger();
2272 // Setup the blocks
2273 blocks_add_default_course_blocks($course);
2275 // Create default section and initial sections if specified (unless they've already been created earlier).
2276 // We do not want to call course_create_sections_if_missing() because to avoid creating course cache.
2277 $numsections = isset($data->numsections) ? $data->numsections : 0;
2278 $existingsections = $DB->get_fieldset_sql('SELECT section from {course_sections} WHERE course = ?', [$newcourseid]);
2279 $newsections = array_diff(range(0, $numsections), $existingsections);
2280 foreach ($newsections as $sectionnum) {
2281 course_create_section($newcourseid, $sectionnum, true);
2284 // Save any custom role names.
2285 save_local_role_names($course->id, (array)$data);
2287 // set up enrolments
2288 enrol_course_updated(true, $course, $data);
2290 // Update course tags.
2291 if (isset($data->tags)) {
2292 core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
2295 // Save custom fields if there are any of them in the form.
2296 $handler = core_course\customfield\course_handler::create();
2297 // Make sure to set the handler's parent context first.
2298 $coursecatcontext = context_coursecat::instance($category->id);
2299 $handler->set_parent_context($coursecatcontext);
2300 // Save the custom field data.
2301 $data->id = $course->id;
2302 $handler->instance_form_save($data, true);
2304 return $course;
2308 * Update a course.
2310 * Please note this functions does not verify any access control,
2311 * the calling code is responsible for all validation (usually it is the form definition).
2313 * @param object $data - all the data needed for an entry in the 'course' table
2314 * @param array $editoroptions course description editor options
2315 * @return void
2317 function update_course($data, $editoroptions = NULL) {
2318 global $DB, $CFG;
2320 // Prevent changes on front page course.
2321 if ($data->id == SITEID) {
2322 throw new moodle_exception('invalidcourse', 'error');
2325 $oldcourse = course_get_format($data->id)->get_course();
2326 $context = context_course::instance($oldcourse->id);
2328 // Make sure we're not changing whatever the course's relativedatesmode setting is.
2329 unset($data->relativedatesmode);
2331 // Capture the updated fields for the log data.
2332 $updatedfields = [];
2333 foreach (get_object_vars($oldcourse) as $field => $value) {
2334 if ($field == 'summary_editor') {
2335 if (($data->$field)['text'] !== $value['text']) {
2336 // The summary might be very long, we don't wan't to fill up the log record with the full text.
2337 $updatedfields[$field] = '(updated)';
2339 } else if ($field == 'tags' && isset($data->tags)) {
2340 // Tags might not have the same array keys, just check the values.
2341 if (array_values($data->$field) !== array_values($value)) {
2342 $updatedfields[$field] = $data->$field;
2344 } else {
2345 if (isset($data->$field) && $data->$field != $value) {
2346 $updatedfields[$field] = $data->$field;
2351 $data->timemodified = time();
2353 if ($editoroptions) {
2354 $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
2356 if ($overviewfilesoptions = course_overviewfiles_options($data->id)) {
2357 $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2360 // Check we don't have a duplicate shortname.
2361 if (!empty($data->shortname) && $oldcourse->shortname != $data->shortname) {
2362 if ($DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data->shortname, $data->id))) {
2363 throw new moodle_exception('shortnametaken', '', '', $data->shortname);
2367 // Check we don't have a duplicate idnumber.
2368 if (!empty($data->idnumber) && $oldcourse->idnumber != $data->idnumber) {
2369 if ($DB->record_exists_sql('SELECT id from {course} WHERE idnumber = ? AND id <> ?', array($data->idnumber, $data->id))) {
2370 throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
2374 if ($errorcode = course_validate_dates((array)$data)) {
2375 throw new moodle_exception($errorcode);
2378 if (!isset($data->category) or empty($data->category)) {
2379 // prevent nulls and 0 in category field
2380 unset($data->category);
2382 $changesincoursecat = $movecat = (isset($data->category) and $oldcourse->category != $data->category);
2384 if (!isset($data->visible)) {
2385 // data not from form, add missing visibility info
2386 $data->visible = $oldcourse->visible;
2389 if ($data->visible != $oldcourse->visible) {
2390 // reset the visibleold flag when manually hiding/unhiding course
2391 $data->visibleold = $data->visible;
2392 $changesincoursecat = true;
2393 } else {
2394 if ($movecat) {
2395 $newcategory = $DB->get_record('course_categories', array('id'=>$data->category));
2396 if (empty($newcategory->visible)) {
2397 // make sure when moving into hidden category the course is hidden automatically
2398 $data->visible = 0;
2403 // Set newsitems to 0 if format does not support announcements.
2404 if (isset($data->format)) {
2405 $newcourseformat = course_get_format((object)['format' => $data->format]);
2406 if (!$newcourseformat->supports_news()) {
2407 $data->newsitems = 0;
2411 // Set showcompletionconditions to null when completion tracking has been disabled for the course.
2412 if (isset($data->enablecompletion) && $data->enablecompletion == COMPLETION_DISABLED) {
2413 $data->showcompletionconditions = null;
2416 // Update custom fields if there are any of them in the form.
2417 $handler = core_course\customfield\course_handler::create();
2418 $handler->instance_form_save($data);
2420 // Update with the new data
2421 $DB->update_record('course', $data);
2422 // make sure the modinfo cache is reset
2423 rebuild_course_cache($data->id);
2425 // Purge course image cache in case if course image has been updated.
2426 \cache::make('core', 'course_image')->delete($data->id);
2428 // update course format options with full course data
2429 course_get_format($data->id)->update_course_format_options($data, $oldcourse);
2431 $course = $DB->get_record('course', array('id'=>$data->id));
2433 if ($movecat) {
2434 $newparent = context_coursecat::instance($course->category);
2435 $context->update_moved($newparent);
2437 $fixcoursesortorder = $movecat || (isset($data->sortorder) && ($oldcourse->sortorder != $data->sortorder));
2438 if ($fixcoursesortorder) {
2439 fix_course_sortorder();
2442 // purge appropriate caches in case fix_course_sortorder() did not change anything
2443 cache_helper::purge_by_event('changesincourse');
2444 if ($changesincoursecat) {
2445 cache_helper::purge_by_event('changesincoursecat');
2448 // Test for and remove blocks which aren't appropriate anymore
2449 blocks_remove_inappropriate($course);
2451 // Save any custom role names.
2452 save_local_role_names($course->id, $data);
2454 // update enrol settings
2455 enrol_course_updated(false, $course, $data);
2457 // Update course tags.
2458 if (isset($data->tags)) {
2459 core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
2462 // Trigger a course updated event.
2463 $event = \core\event\course_updated::create(array(
2464 'objectid' => $course->id,
2465 'context' => context_course::instance($course->id),
2466 'other' => array('shortname' => $course->shortname,
2467 'fullname' => $course->fullname,
2468 'updatedfields' => $updatedfields)
2471 $event->set_legacy_logdata(array($course->id, 'course', 'update', 'edit.php?id=' . $course->id, $course->id));
2472 $event->trigger();
2474 if ($oldcourse->format !== $course->format) {
2475 // Remove all options stored for the previous format
2476 // We assume that new course format migrated everything it needed watching trigger
2477 // 'course_updated' and in method format_XXX::update_course_format_options()
2478 $DB->delete_records('course_format_options',
2479 array('courseid' => $course->id, 'format' => $oldcourse->format));
2484 * Calculate the average number of enrolled participants per course.
2486 * This is intended for statistics purposes during the site registration. Only visible courses are taken into account.
2487 * Front page enrolments are excluded.
2489 * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions.
2490 * @param int $lastloginsince If specified, count only users who logged in after this timestamp.
2491 * @return float
2493 function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float {
2494 global $DB;
2496 $params = [];
2498 $sql = "SELECT DISTINCT ue.userid, e.courseid
2499 FROM {user_enrolments} ue
2500 JOIN {enrol} e ON e.id = ue.enrolid
2501 JOIN {course} c ON c.id = e.courseid ";
2503 if ($onlyactive || $lastloginsince) {
2504 $sql .= "JOIN {user} u ON u.id = ue.userid ";
2507 $sql .= "WHERE e.courseid <> " . SITEID . " AND c.visible = 1 ";
2509 if ($onlyactive) {
2510 $sql .= "AND ue.status = :active
2511 AND e.status = :enabled
2512 AND ue.timestart < :now1
2513 AND (ue.timeend = 0 OR ue.timeend > :now2) ";
2515 // Same as in the enrollib - the rounding should help caching in the database.
2516 $now = round(time(), -2);
2518 $params += [
2519 'active' => ENROL_USER_ACTIVE,
2520 'enabled' => ENROL_INSTANCE_ENABLED,
2521 'now1' => $now,
2522 'now2' => $now,
2526 if ($lastloginsince) {
2527 $sql .= "AND u.lastlogin > :lastlogin ";
2528 $params['lastlogin'] = $lastloginsince;
2531 $sql = "SELECT COUNT(*)
2532 FROM ($sql) total";
2534 $enrolmenttotal = $DB->count_records_sql($sql, $params);
2536 // Get the number of visible courses (exclude the front page).
2537 $coursetotal = $DB->count_records('course', ['visible' => 1]);
2538 $coursetotal = $coursetotal - 1;
2540 if (empty($coursetotal)) {
2541 $participantaverage = 0;
2543 } else {
2544 $participantaverage = $enrolmenttotal / $coursetotal;
2547 return $participantaverage;
2551 * Average number of course modules
2552 * @return integer
2554 function average_number_of_courses_modules() {
2555 global $DB, $SITE;
2557 //count total of visible course module (except front page)
2558 $sql = 'SELECT COUNT(*) FROM (
2559 SELECT cm.course, cm.module
2560 FROM {course} c, {course_modules} cm
2561 WHERE c.id = cm.course
2562 AND c.id <> :siteid
2563 AND cm.visible = 1
2564 AND c.visible = 1) total';
2565 $params = array('siteid' => $SITE->id);
2566 $moduletotal = $DB->count_records_sql($sql, $params);
2569 //count total of visible courses (minus front page)
2570 $coursetotal = $DB->count_records('course', array('visible' => 1));
2571 $coursetotal = $coursetotal - 1 ;
2573 //average of course module
2574 if (empty($coursetotal)) {
2575 $coursemoduleaverage = 0;
2576 } else {
2577 $coursemoduleaverage = $moduletotal / $coursetotal;
2580 return $coursemoduleaverage;
2584 * This class pertains to course requests and contains methods associated with
2585 * create, approving, and removing course requests.
2587 * Please note we do not allow embedded images here because there is no context
2588 * to store them with proper access control.
2590 * @copyright 2009 Sam Hemelryk
2591 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2592 * @since Moodle 2.0
2594 * @property-read int $id
2595 * @property-read string $fullname
2596 * @property-read string $shortname
2597 * @property-read string $summary
2598 * @property-read int $summaryformat
2599 * @property-read int $summarytrust
2600 * @property-read string $reason
2601 * @property-read int $requester
2603 class course_request {
2606 * This is the stdClass that stores the properties for the course request
2607 * and is externally accessed through the __get magic method
2608 * @var stdClass
2610 protected $properties;
2613 * An array of options for the summary editor used by course request forms.
2614 * This is initially set by {@link summary_editor_options()}
2615 * @var array
2616 * @static
2618 protected static $summaryeditoroptions;
2621 * Static function to prepare the summary editor for working with a course
2622 * request.
2624 * @static
2625 * @param null|stdClass $data Optional, an object containing the default values
2626 * for the form, these may be modified when preparing the
2627 * editor so this should be called before creating the form
2628 * @return stdClass An object that can be used to set the default values for
2629 * an mforms form
2631 public static function prepare($data=null) {
2632 if ($data === null) {
2633 $data = new stdClass;
2635 $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options());
2636 return $data;
2640 * Static function to create a new course request when passed an array of properties
2641 * for it.
2643 * This function also handles saving any files that may have been used in the editor
2645 * @static
2646 * @param stdClass $data
2647 * @return course_request The newly created course request
2649 public static function create($data) {
2650 global $USER, $DB, $CFG;
2651 $data->requester = $USER->id;
2653 // Setting the default category if none set.
2654 if (empty($data->category) || !empty($CFG->lockrequestcategory)) {
2655 $data->category = $CFG->defaultrequestcategory;
2658 // Summary is a required field so copy the text over
2659 $data->summary = $data->summary_editor['text'];
2660 $data->summaryformat = $data->summary_editor['format'];
2662 $data->id = $DB->insert_record('course_request', $data);
2664 // Create a new course_request object and return it
2665 $request = new course_request($data);
2667 // Notify the admin if required.
2668 if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) {
2670 $a = new stdClass;
2671 $a->link = "$CFG->wwwroot/course/pending.php";
2672 $a->user = fullname($USER);
2673 $subject = get_string('courserequest');
2674 $message = get_string('courserequestnotifyemail', 'admin', $a);
2675 foreach ($users as $user) {
2676 $request->notify($user, $USER, 'courserequested', $subject, $message);
2680 return $request;
2684 * Returns an array of options to use with a summary editor
2686 * @uses course_request::$summaryeditoroptions
2687 * @return array An array of options to use with the editor
2689 public static function summary_editor_options() {
2690 global $CFG;
2691 if (self::$summaryeditoroptions === null) {
2692 self::$summaryeditoroptions = array('maxfiles' => 0, 'maxbytes'=>0);
2694 return self::$summaryeditoroptions;
2698 * Loads the properties for this course request object. Id is required and if
2699 * only id is provided then we load the rest of the properties from the database
2701 * @param stdClass|int $properties Either an object containing properties
2702 * or the course_request id to load
2704 public function __construct($properties) {
2705 global $DB;
2706 if (empty($properties->id)) {
2707 if (empty($properties)) {
2708 throw new coding_exception('You must provide a course request id when creating a course_request object');
2710 $id = $properties;
2711 $properties = new stdClass;
2712 $properties->id = (int)$id;
2713 unset($id);
2715 if (empty($properties->requester)) {
2716 if (!($this->properties = $DB->get_record('course_request', array('id' => $properties->id)))) {
2717 throw new \moodle_exception('unknowncourserequest');
2719 } else {
2720 $this->properties = $properties;
2722 $this->properties->collision = null;
2726 * Returns the requested property
2728 * @param string $key
2729 * @return mixed
2731 public function __get($key) {
2732 return $this->properties->$key;
2736 * Override this to ensure empty($request->blah) calls return a reliable answer...
2738 * This is required because we define the __get method
2740 * @param mixed $key
2741 * @return bool True is it not empty, false otherwise
2743 public function __isset($key) {
2744 return (!empty($this->properties->$key));
2748 * Returns the user who requested this course
2750 * Uses a static var to cache the results and cut down the number of db queries
2752 * @staticvar array $requesters An array of cached users
2753 * @return stdClass The user who requested the course
2755 public function get_requester() {
2756 global $DB;
2757 static $requesters= array();
2758 if (!array_key_exists($this->properties->requester, $requesters)) {
2759 $requesters[$this->properties->requester] = $DB->get_record('user', array('id'=>$this->properties->requester));
2761 return $requesters[$this->properties->requester];
2765 * Checks that the shortname used by the course does not conflict with any other
2766 * courses that exist
2768 * @param string|null $shortnamemark The string to append to the requests shortname
2769 * should a conflict be found
2770 * @return bool true is there is a conflict, false otherwise
2772 public function check_shortname_collision($shortnamemark = '[*]') {
2773 global $DB;
2775 if ($this->properties->collision !== null) {
2776 return $this->properties->collision;
2779 if (empty($this->properties->shortname)) {
2780 debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER);
2781 $this->properties->collision = false;
2782 } else if ($DB->record_exists('course', array('shortname' => $this->properties->shortname))) {
2783 if (!empty($shortnamemark)) {
2784 $this->properties->shortname .= ' '.$shortnamemark;
2786 $this->properties->collision = true;
2787 } else {
2788 $this->properties->collision = false;
2790 return $this->properties->collision;
2794 * Checks user capability to approve a requested course
2796 * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is
2797 * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'.
2799 * @return bool
2801 public function can_approve() {
2802 global $CFG;
2803 $category = null;
2804 if ($this->properties->category) {
2805 $category = core_course_category::get($this->properties->category, IGNORE_MISSING);
2806 } else if ($CFG->defaultrequestcategory) {
2807 $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING);
2809 if ($category) {
2810 return has_capability('moodle/site:approvecourse', $category->get_context());
2813 // We can not determine the context where the course should be created. The approver should have
2814 // both capabilities to approve courses and change course category in the system context.
2815 return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance());
2819 * Returns the category where this course request should be created
2821 * Note that we don't check here that user has a capability to view
2822 * hidden categories if he has capabilities 'moodle/site:approvecourse' and
2823 * 'moodle/course:changecategory'
2825 * @return core_course_category
2827 public function get_category() {
2828 global $CFG;
2829 if ($this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING))) {
2830 return $category;
2831 } else if ($CFG->defaultrequestcategory &&
2832 ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING))) {
2833 return $category;
2834 } else {
2835 return core_course_category::get_default();
2840 * This function approves the request turning it into a course
2842 * This function converts the course request into a course, at the same time
2843 * transferring any files used in the summary to the new course and then removing
2844 * the course request and the files associated with it.
2846 * @return int The id of the course that was created from this request
2848 public function approve() {
2849 global $CFG, $DB, $USER;
2851 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2853 $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST);
2855 $courseconfig = get_config('moodlecourse');
2857 // Transfer appropriate settings
2858 $data = clone($this->properties);
2859 unset($data->id);
2860 unset($data->reason);
2861 unset($data->requester);
2863 // Set category
2864 $category = $this->get_category();
2865 $data->category = $category->id;
2866 // Set misc settings
2867 $data->requested = 1;
2869 // Apply course default settings
2870 $data->format = $courseconfig->format;
2871 $data->newsitems = $courseconfig->newsitems;
2872 $data->showgrades = $courseconfig->showgrades;
2873 $data->showreports = $courseconfig->showreports;
2874 $data->maxbytes = $courseconfig->maxbytes;
2875 $data->groupmode = $courseconfig->groupmode;
2876 $data->groupmodeforce = $courseconfig->groupmodeforce;
2877 $data->visible = $courseconfig->visible;
2878 $data->visibleold = $data->visible;
2879 $data->lang = $courseconfig->lang;
2880 $data->enablecompletion = $courseconfig->enablecompletion;
2881 $data->numsections = $courseconfig->numsections;
2882 $data->startdate = usergetmidnight(time());
2883 if ($courseconfig->courseenddateenabled) {
2884 $data->enddate = usergetmidnight(time()) + $courseconfig->courseduration;
2887 list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname);
2889 $course = create_course($data);
2890 $context = context_course::instance($course->id, MUST_EXIST);
2892 // add enrol instances
2893 if (!$DB->record_exists('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) {
2894 if ($manual = enrol_get_plugin('manual')) {
2895 $manual->add_default_instance($course);
2899 // enrol the requester as teacher if necessary
2900 if (!empty($CFG->creatornewroleid) and !is_viewing($context, $user, 'moodle/role:assign') and !is_enrolled($context, $user, 'moodle/role:assign')) {
2901 enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid);
2904 $this->delete();
2906 $a = new stdClass();
2907 $a->name = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
2908 $a->url = $CFG->wwwroot.'/course/view.php?id=' . $course->id;
2909 $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a), $course->id);
2911 return $course->id;
2915 * Reject a course request
2917 * This function rejects a course request, emailing the requesting user the
2918 * provided notice and then removing the request from the database
2920 * @param string $notice The message to display to the user
2922 public function reject($notice) {
2923 global $USER, $DB;
2924 $user = $DB->get_record('user', array('id' => $this->properties->requester), '*', MUST_EXIST);
2925 $this->notify($user, $USER, 'courserequestrejected', get_string('courserejectsubject'), get_string('courserejectemail', 'moodle', $notice));
2926 $this->delete();
2930 * Deletes the course request and any associated files
2932 public function delete() {
2933 global $DB;
2934 $DB->delete_records('course_request', array('id' => $this->properties->id));
2938 * Send a message from one user to another using events_trigger
2940 * @param object $touser
2941 * @param object $fromuser
2942 * @param string $name
2943 * @param string $subject
2944 * @param string $message
2945 * @param int|null $courseid
2947 protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) {
2948 $eventdata = new \core\message\message();
2949 $eventdata->courseid = empty($courseid) ? SITEID : $courseid;
2950 $eventdata->component = 'moodle';
2951 $eventdata->name = $name;
2952 $eventdata->userfrom = $fromuser;
2953 $eventdata->userto = $touser;
2954 $eventdata->subject = $subject;
2955 $eventdata->fullmessage = $message;
2956 $eventdata->fullmessageformat = FORMAT_PLAIN;
2957 $eventdata->fullmessagehtml = '';
2958 $eventdata->smallmessage = '';
2959 $eventdata->notification = 1;
2960 message_send($eventdata);
2964 * Checks if current user can request a course in this context
2966 * @param context $context
2967 * @return bool
2969 public static function can_request(context $context) {
2970 global $CFG;
2971 if (empty($CFG->enablecourserequests)) {
2972 return false;
2974 if (has_capability('moodle/course:create', $context)) {
2975 return false;
2978 if ($context instanceof context_system) {
2979 $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING);
2980 return $defaultcontext &&
2981 has_capability('moodle/course:request', $defaultcontext);
2982 } else if ($context instanceof context_coursecat) {
2983 if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) {
2984 return has_capability('moodle/course:request', $context);
2987 return false;
2992 * Return a list of page types
2993 * @param string $pagetype current page type
2994 * @param context $parentcontext Block's parent context
2995 * @param context $currentcontext Current context of block
2996 * @return array array of page types
2998 function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
2999 if ($pagetype === 'course-index' || $pagetype === 'course-index-category') {
3000 // For courses and categories browsing pages (/course/index.php) add option to show on ANY category page
3001 $pagetypes = array('*' => get_string('page-x', 'pagetype'),
3002 'course-index-*' => get_string('page-course-index-x', 'pagetype'),
3004 } else if ($currentcontext && (!($coursecontext = $currentcontext->get_course_context(false)) || $coursecontext->instanceid == SITEID)) {
3005 // We know for sure that despite pagetype starts with 'course-' this is not a page in course context (i.e. /course/search.php, etc.)
3006 $pagetypes = array('*' => get_string('page-x', 'pagetype'));
3007 } else {
3008 // Otherwise consider it a page inside a course even if $currentcontext is null
3009 $pagetypes = array('*' => get_string('page-x', 'pagetype'),
3010 'course-*' => get_string('page-course-x', 'pagetype'),
3011 'course-view-*' => get_string('page-course-view-x', 'pagetype')
3014 return $pagetypes;
3018 * Determine whether course ajax should be enabled for the specified course
3020 * @param stdClass $course The course to test against
3021 * @return boolean Whether course ajax is enabled or note
3023 function course_ajax_enabled($course) {
3024 global $CFG, $PAGE, $SITE;
3026 // The user must be editing for AJAX to be included
3027 if (!$PAGE->user_is_editing()) {
3028 return false;
3031 // Check that the theme suports
3032 if (!$PAGE->theme->enablecourseajax) {
3033 return false;
3036 // Check that the course format supports ajax functionality
3037 // The site 'format' doesn't have information on course format support
3038 if ($SITE->id !== $course->id) {
3039 $courseformatajaxsupport = course_format_ajax_support($course->format);
3040 if (!$courseformatajaxsupport->capable) {
3041 return false;
3045 // All conditions have been met so course ajax should be enabled
3046 return true;
3050 * Include the relevant javascript and language strings for the resource
3051 * toolbox YUI module
3053 * @param integer $id The ID of the course being applied to
3054 * @param array $usedmodules An array containing the names of the modules in use on the page
3055 * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site
3056 * @param stdClass $config An object containing configuration parameters for ajax modules including:
3057 * * resourceurl The URL to post changes to for resource changes
3058 * * sectionurl The URL to post changes to for section changes
3059 * * pageparams Additional parameters to pass through in the post
3060 * @return bool
3062 function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) {
3063 global $CFG, $PAGE, $SITE;
3065 // Init the course editor module to support UI components.
3066 $format = course_get_format($course);
3067 include_course_editor($format);
3069 // Ensure that ajax should be included
3070 if (!course_ajax_enabled($course)) {
3071 return false;
3074 // Component based formats don't use YUI drag and drop anymore.
3075 if (!$format->supports_components() && course_format_uses_sections($course->format)) {
3077 if (!$config) {
3078 $config = new stdClass();
3081 // The URL to use for resource changes.
3082 if (!isset($config->resourceurl)) {
3083 $config->resourceurl = '/course/rest.php';
3086 // The URL to use for section changes.
3087 if (!isset($config->sectionurl)) {
3088 $config->sectionurl = '/course/rest.php';
3091 // Any additional parameters which need to be included on page submission.
3092 if (!isset($config->pageparams)) {
3093 $config->pageparams = array();
3096 $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop',
3097 array(array(
3098 'courseid' => $course->id,
3099 'ajaxurl' => $config->sectionurl,
3100 'config' => $config,
3101 )), null, true);
3103 $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_resource_dragdrop',
3104 array(array(
3105 'courseid' => $course->id,
3106 'ajaxurl' => $config->resourceurl,
3107 'config' => $config,
3108 )), null, true);
3111 // Require various strings for the command toolbox
3112 $PAGE->requires->strings_for_js(array(
3113 'moveleft',
3114 'deletechecktype',
3115 'deletechecktypename',
3116 'edittitle',
3117 'edittitleinstructions',
3118 'show',
3119 'hide',
3120 'highlight',
3121 'highlightoff',
3122 'groupsnone',
3123 'groupsvisible',
3124 'groupsseparate',
3125 'clicktochangeinbrackets',
3126 'markthistopic',
3127 'markedthistopic',
3128 'movesection',
3129 'movecoursemodule',
3130 'movecoursesection',
3131 'movecontent',
3132 'tocontent',
3133 'emptydragdropregion',
3134 'afterresource',
3135 'aftersection',
3136 'totopofsection',
3137 ), 'moodle');
3139 // Include section-specific strings for formats which support sections.
3140 if (course_format_uses_sections($course->format)) {
3141 $PAGE->requires->strings_for_js(array(
3142 'showfromothers',
3143 'hidefromothers',
3144 ), 'format_' . $course->format);
3147 // For confirming resource deletion we need the name of the module in question
3148 foreach ($usedmodules as $module => $modname) {
3149 $PAGE->requires->string_for_js('pluginname', $module);
3152 // Load drag and drop upload AJAX.
3153 require_once($CFG->dirroot.'/course/dnduploadlib.php');
3154 dndupload_add_to_course($course, $enabledmodules);
3156 $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format));
3158 return true;
3162 * Include and configure the course editor modules.
3164 * @param course_format $format the course format instance.
3166 function include_course_editor(course_format $format) {
3167 global $PAGE, $SITE;
3169 $course = $format->get_course();
3171 if ($SITE->id === $course->id) {
3172 return;
3175 $statekey = course_format::session_cache($course);
3177 // Edition mode and some format specs must be passed to the init method.
3178 $setup = (object)[
3179 'editing' => $format->show_editor(),
3180 'supportscomponents' => $format->supports_components(),
3181 'statekey' => $statekey,
3183 // All the new editor elements will be loaded after the course is presented and
3184 // the initial course state will be generated using core_course_get_state webservice.
3185 $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]);
3189 * Returns the sorted list of available course formats, filtered by enabled if necessary
3191 * @param bool $enabledonly return only formats that are enabled
3192 * @return array array of sorted format names
3194 function get_sorted_course_formats($enabledonly = false) {
3195 global $CFG;
3196 $formats = core_component::get_plugin_list('format');
3198 if (!empty($CFG->format_plugins_sortorder)) {
3199 $order = explode(',', $CFG->format_plugins_sortorder);
3200 $order = array_merge(array_intersect($order, array_keys($formats)),
3201 array_diff(array_keys($formats), $order));
3202 } else {
3203 $order = array_keys($formats);
3205 if (!$enabledonly) {
3206 return $order;
3208 $sortedformats = array();
3209 foreach ($order as $formatname) {
3210 if (!get_config('format_'.$formatname, 'disabled')) {
3211 $sortedformats[] = $formatname;
3214 return $sortedformats;
3218 * The URL to use for the specified course (with section)
3220 * @param int|stdClass $courseorid The course to get the section name for (either object or just course id)
3221 * @param int|stdClass $section Section object from database or just field course_sections.section
3222 * if omitted the course view page is returned
3223 * @param array $options options for view URL. At the moment core uses:
3224 * 'navigation' (bool) if true and section has no separate page, the function returns null
3225 * 'sr' (int) used by multipage formats to specify to which section to return
3226 * @return moodle_url The url of course
3228 function course_get_url($courseorid, $section = null, $options = array()) {
3229 return course_get_format($courseorid)->get_view_url($section, $options);
3233 * Create a module.
3235 * It includes:
3236 * - capability checks and other checks
3237 * - create the module from the module info
3239 * @param object $module
3240 * @return object the created module info
3241 * @throws moodle_exception if user is not allowed to perform the action or module is not allowed in this course
3243 function create_module($moduleinfo) {
3244 global $DB, $CFG;
3246 require_once($CFG->dirroot . '/course/modlib.php');
3248 // Check manadatory attributs.
3249 $mandatoryfields = array('modulename', 'course', 'section', 'visible');
3250 if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_MOD_INTRO, true)) {
3251 $mandatoryfields[] = 'introeditor';
3253 foreach($mandatoryfields as $mandatoryfield) {
3254 if (!isset($moduleinfo->{$mandatoryfield})) {
3255 throw new moodle_exception('createmodulemissingattribut', '', '', $mandatoryfield);
3259 // Some additional checks (capability / existing instances).
3260 $course = $DB->get_record('course', array('id'=>$moduleinfo->course), '*', MUST_EXIST);
3261 list($module, $context, $cw) = can_add_moduleinfo($course, $moduleinfo->modulename, $moduleinfo->section);
3263 // Add the module.
3264 $moduleinfo->module = $module->id;
3265 $moduleinfo = add_moduleinfo($moduleinfo, $course, null);
3267 return $moduleinfo;
3271 * Update a module.
3273 * It includes:
3274 * - capability and other checks
3275 * - update the module
3277 * @param object $module
3278 * @return object the updated module info
3279 * @throws moodle_exception if current user is not allowed to update the module
3281 function update_module($moduleinfo) {
3282 global $DB, $CFG;
3284 require_once($CFG->dirroot . '/course/modlib.php');
3286 // Check the course module exists.
3287 $cm = get_coursemodule_from_id('', $moduleinfo->coursemodule, 0, false, MUST_EXIST);
3289 // Check the course exists.
3290 $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
3292 // Some checks (capaibility / existing instances).
3293 list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm);
3295 // Retrieve few information needed by update_moduleinfo.
3296 $moduleinfo->modulename = $cm->modname;
3297 if (!isset($moduleinfo->scale)) {
3298 $moduleinfo->scale = 0;
3300 $moduleinfo->type = 'mod';
3302 // Update the module.
3303 list($cm, $moduleinfo) = update_moduleinfo($cm, $moduleinfo, $course, null);
3305 return $moduleinfo;
3309 * Duplicate a module on the course for ajax.
3311 * @see mod_duplicate_module()
3312 * @param object $course The course
3313 * @param object $cm The course module to duplicate
3314 * @param int $sr The section to link back to (used for creating the links)
3315 * @throws moodle_exception if the plugin doesn't support duplication
3316 * @return Object containing:
3317 * - fullcontent: The HTML markup for the created CM
3318 * - cmid: The CMID of the newly created CM
3319 * - redirect: Whether to trigger a redirect following this change
3321 function mod_duplicate_activity($course, $cm, $sr = null) {
3322 global $PAGE;
3324 $newcm = duplicate_module($course, $cm);
3326 $resp = new stdClass();
3327 if ($newcm) {
3329 $format = course_get_format($course);
3330 $renderer = $format->get_renderer($PAGE);
3331 $modinfo = $format->get_modinfo();
3332 $section = $modinfo->get_section_info($newcm->sectionnum);
3334 // Get the new element html content.
3335 $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $newcm);
3337 $resp->cmid = $newcm->id;
3338 } else {
3339 // Trigger a redirect.
3340 $resp->redirect = true;
3342 return $resp;
3346 * Api to duplicate a module.
3348 * @param object $course course object.
3349 * @param object $cm course module object to be duplicated.
3350 * @since Moodle 2.8
3352 * @throws Exception
3353 * @throws coding_exception
3354 * @throws moodle_exception
3355 * @throws restore_controller_exception
3357 * @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm.
3359 function duplicate_module($course, $cm) {
3360 global $CFG, $DB, $USER;
3361 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
3362 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
3363 require_once($CFG->libdir . '/filelib.php');
3365 $a = new stdClass();
3366 $a->modtype = get_string('modulename', $cm->modname);
3367 $a->modname = format_string($cm->name);
3369 if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) {
3370 throw new moodle_exception('duplicatenosupport', 'error', '', $a);
3373 // Backup the activity.
3375 $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE,
3376 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id);
3378 $backupid = $bc->get_backupid();
3379 $backupbasepath = $bc->get_plan()->get_basepath();
3381 $bc->execute_plan();
3383 $bc->destroy();
3385 // Restore the backup immediately.
3387 $rc = new restore_controller($backupid, $course->id,
3388 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING);
3390 // Make sure that the restore_general_groups setting is always enabled when duplicating an activity.
3391 $plan = $rc->get_plan();
3392 $groupsetting = $plan->get_setting('groups');
3393 if (empty($groupsetting->get_value())) {
3394 $groupsetting->set_value(true);
3397 $cmcontext = context_module::instance($cm->id);
3398 if (!$rc->execute_precheck()) {
3399 $precheckresults = $rc->get_precheck_results();
3400 if (is_array($precheckresults) && !empty($precheckresults['errors'])) {
3401 if (empty($CFG->keeptempdirectoriesonbackup)) {
3402 fulldelete($backupbasepath);
3407 $rc->execute_plan();
3409 // Now a bit hacky part follows - we try to get the cmid of the newly
3410 // restored copy of the module.
3411 $newcmid = null;
3412 $tasks = $rc->get_plan()->get_tasks();
3413 foreach ($tasks as $task) {
3414 if (is_subclass_of($task, 'restore_activity_task')) {
3415 if ($task->get_old_contextid() == $cmcontext->id) {
3416 $newcmid = $task->get_moduleid();
3417 break;
3422 $rc->destroy();
3424 if (empty($CFG->keeptempdirectoriesonbackup)) {
3425 fulldelete($backupbasepath);
3428 // If we know the cmid of the new course module, let us move it
3429 // right below the original one. otherwise it will stay at the
3430 // end of the section.
3431 if ($newcmid) {
3432 // Proceed with activity renaming before everything else. We don't use APIs here to avoid
3433 // triggering a lot of create/update duplicated events.
3434 $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course);
3435 // Add ' (copy)' to duplicates. Note we don't cleanup or validate lengths here. It comes
3436 // from original name that was valid, so the copy should be too.
3437 $newname = get_string('duplicatedmodule', 'moodle', $newcm->name);
3438 $DB->set_field($cm->modname, 'name', $newname, ['id' => $newcm->instance]);
3440 $section = $DB->get_record('course_sections', array('id' => $cm->section, 'course' => $cm->course));
3441 $modarray = explode(",", trim($section->sequence));
3442 $cmindex = array_search($cm->id, $modarray);
3443 if ($cmindex !== false && $cmindex < count($modarray) - 1) {
3444 moveto_module($newcm, $section, $modarray[$cmindex + 1]);
3447 // Update calendar events with the duplicated module.
3448 // The following line is to be removed in MDL-58906.
3449 course_module_update_calendar_events($newcm->modname, null, $newcm);
3451 // Trigger course module created event. We can trigger the event only if we know the newcmid.
3452 $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid);
3453 $event = \core\event\course_module_created::create_from_cm($newcm);
3454 $event->trigger();
3457 return isset($newcm) ? $newcm : null;
3461 * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3462 * Sorts by descending order of time.
3464 * @param stdClass $a First object
3465 * @param stdClass $b Second object
3466 * @return int 0,1,-1 representing the order
3468 function compare_activities_by_time_desc($a, $b) {
3469 // Make sure the activities actually have a timestamp property.
3470 if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3471 return 0;
3473 // We treat instances without timestamp as if they have a timestamp of 0.
3474 if ((!property_exists($a, 'timestamp')) && (property_exists($b,'timestamp'))) {
3475 return 1;
3477 if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3478 return -1;
3480 if ($a->timestamp == $b->timestamp) {
3481 return 0;
3483 return ($a->timestamp > $b->timestamp) ? -1 : 1;
3487 * Compare two objects to find out their correct order based on timestamp (to be used by usort).
3488 * Sorts by ascending order of time.
3490 * @param stdClass $a First object
3491 * @param stdClass $b Second object
3492 * @return int 0,1,-1 representing the order
3494 function compare_activities_by_time_asc($a, $b) {
3495 // Make sure the activities actually have a timestamp property.
3496 if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3497 return 0;
3499 // We treat instances without timestamp as if they have a timestamp of 0.
3500 if ((!property_exists($a, 'timestamp')) && (property_exists($b, 'timestamp'))) {
3501 return -1;
3503 if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) {
3504 return 1;
3506 if ($a->timestamp == $b->timestamp) {
3507 return 0;
3509 return ($a->timestamp < $b->timestamp) ? -1 : 1;
3513 * Changes the visibility of a course.
3515 * @param int $courseid The course to change.
3516 * @param bool $show True to make it visible, false otherwise.
3517 * @return bool
3519 function course_change_visibility($courseid, $show = true) {
3520 $course = new stdClass;
3521 $course->id = $courseid;
3522 $course->visible = ($show) ? '1' : '0';
3523 $course->visibleold = $course->visible;
3524 update_course($course);
3525 return true;
3529 * Changes the course sortorder by one, moving it up or down one in respect to sort order.
3531 * @param stdClass|core_course_list_element $course
3532 * @param bool $up If set to true the course will be moved up one. Otherwise down one.
3533 * @return bool
3535 function course_change_sortorder_by_one($course, $up) {
3536 global $DB;
3537 $params = array($course->sortorder, $course->category);
3538 if ($up) {
3539 $select = 'sortorder < ? AND category = ?';
3540 $sort = 'sortorder DESC';
3541 } else {
3542 $select = 'sortorder > ? AND category = ?';
3543 $sort = 'sortorder ASC';
3545 fix_course_sortorder();
3546 $swapcourse = $DB->get_records_select('course', $select, $params, $sort, '*', 0, 1);
3547 if ($swapcourse) {
3548 $swapcourse = reset($swapcourse);
3549 $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $course->id));
3550 $DB->set_field('course', 'sortorder', $course->sortorder, array('id' => $swapcourse->id));
3551 // Finally reorder courses.
3552 fix_course_sortorder();
3553 cache_helper::purge_by_event('changesincourse');
3554 return true;
3556 return false;
3560 * Changes the sort order of courses in a category so that the first course appears after the second.
3562 * @param int|stdClass $courseorid The course to focus on.
3563 * @param int $moveaftercourseid The course to shifter after or 0 if you want it to be the first course in the category.
3564 * @return bool
3566 function course_change_sortorder_after_course($courseorid, $moveaftercourseid) {
3567 global $DB;
3569 if (!is_object($courseorid)) {
3570 $course = get_course($courseorid);
3571 } else {
3572 $course = $courseorid;
3575 if ((int)$moveaftercourseid === 0) {
3576 // We've moving the course to the start of the queue.
3577 $sql = 'SELECT sortorder
3578 FROM {course}
3579 WHERE category = :categoryid
3580 ORDER BY sortorder';
3581 $params = array(
3582 'categoryid' => $course->category
3584 $sortorder = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE);
3586 $sql = 'UPDATE {course}
3587 SET sortorder = sortorder + 1
3588 WHERE category = :categoryid
3589 AND id <> :id';
3590 $params = array(
3591 'categoryid' => $course->category,
3592 'id' => $course->id,
3594 $DB->execute($sql, $params);
3595 $DB->set_field('course', 'sortorder', $sortorder, array('id' => $course->id));
3596 } else if ($course->id === $moveaftercourseid) {
3597 // They're the same - moronic.
3598 debugging("Invalid move after course given.", DEBUG_DEVELOPER);
3599 return false;
3600 } else {
3601 // Moving this course after the given course. It could be before it could be after.
3602 $moveaftercourse = get_course($moveaftercourseid);
3603 if ($course->category !== $moveaftercourse->category) {
3604 debugging("Cannot re-order courses. The given courses do not belong to the same category.", DEBUG_DEVELOPER);
3605 return false;
3607 // Increment all courses in the same category that are ordered after the moveafter course.
3608 // This makes a space for the course we're moving.
3609 $sql = 'UPDATE {course}
3610 SET sortorder = sortorder + 1
3611 WHERE category = :categoryid
3612 AND sortorder > :sortorder';
3613 $params = array(
3614 'categoryid' => $moveaftercourse->category,
3615 'sortorder' => $moveaftercourse->sortorder
3617 $DB->execute($sql, $params);
3618 $DB->set_field('course', 'sortorder', $moveaftercourse->sortorder + 1, array('id' => $course->id));
3620 fix_course_sortorder();
3621 cache_helper::purge_by_event('changesincourse');
3622 return true;
3626 * Trigger course viewed event. This API function is used when course view actions happens,
3627 * usually in course/view.php but also in external functions.
3629 * @param stdClass $context course context object
3630 * @param int $sectionnumber section number
3631 * @since Moodle 2.9
3633 function course_view($context, $sectionnumber = 0) {
3635 $eventdata = array('context' => $context);
3637 if (!empty($sectionnumber)) {
3638 $eventdata['other']['coursesectionnumber'] = $sectionnumber;
3641 $event = \core\event\course_viewed::create($eventdata);
3642 $event->trigger();
3644 user_accesstime_log($context->instanceid);
3648 * Returns courses tagged with a specified tag.
3650 * @param core_tag_tag $tag
3651 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3652 * are displayed on the page and the per-page limit may be bigger
3653 * @param int $fromctx context id where the link was displayed, may be used by callbacks
3654 * to display items in the same context first
3655 * @param int $ctx context id where to search for records
3656 * @param bool $rec search in subcontexts as well
3657 * @param int $page 0-based number of page being displayed
3658 * @return \core_tag\output\tagindex
3660 function course_get_tagged_courses($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) {
3661 global $CFG, $PAGE;
3663 $perpage = $exclusivemode ? $CFG->coursesperpage : 5;
3664 $displayoptions = array(
3665 'limit' => $perpage,
3666 'offset' => $page * $perpage,
3667 'viewmoreurl' => null,
3670 $courserenderer = $PAGE->get_renderer('core', 'course');
3671 $totalcount = core_course_category::search_courses_count(array('tagid' => $tag->id, 'ctx' => $ctx, 'rec' => $rec));
3672 $content = $courserenderer->tagged_courses($tag->id, $exclusivemode, $ctx, $rec, $displayoptions);
3673 $totalpages = ceil($totalcount / $perpage);
3675 return new core_tag\output\tagindex($tag, 'core', 'course', $content,
3676 $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages);
3680 * Implements callback inplace_editable() allowing to edit values in-place
3682 * @param string $itemtype
3683 * @param int $itemid
3684 * @param mixed $newvalue
3685 * @return \core\output\inplace_editable
3687 function core_course_inplace_editable($itemtype, $itemid, $newvalue) {
3688 if ($itemtype === 'activityname') {
3689 return \core_courseformat\output\local\content\cm\title::update($itemid, $newvalue);
3694 * This function calculates the minimum and maximum cutoff values for the timestart of
3695 * the given event.
3697 * It will return an array with two values, the first being the minimum cutoff value and
3698 * the second being the maximum cutoff value. Either or both values can be null, which
3699 * indicates there is no minimum or maximum, respectively.
3701 * If a cutoff is required then the function must return an array containing the cutoff
3702 * timestamp and error string to display to the user if the cutoff value is violated.
3704 * A minimum and maximum cutoff return value will look like:
3706 * [1505704373, 'The date must be after this date'],
3707 * [1506741172, 'The date must be before this date']
3710 * @param calendar_event $event The calendar event to get the time range for
3711 * @param stdClass $course The course object to get the range from
3712 * @return array Returns an array with min and max date.
3714 function core_course_core_calendar_get_valid_event_timestart_range(\calendar_event $event, $course) {
3715 $mindate = null;
3716 $maxdate = null;
3718 if ($course->startdate) {
3719 $mindate = [
3720 $course->startdate,
3721 get_string('errorbeforecoursestart', 'calendar')
3725 return [$mindate, $maxdate];
3729 * Render the message drawer to be included in the top of the body of each page.
3731 * @return string HTML
3733 function core_course_drawer(): string {
3734 global $PAGE;
3736 // Only add course index on non-site course pages.
3737 if (!$PAGE->course || $PAGE->course->id == SITEID) {
3738 return '';
3741 // Show course index to users can access the course only.
3742 if (!can_access_course($PAGE->course)) {
3743 return '';
3746 $format = course_get_format($PAGE->course);
3747 $renderer = $format->get_renderer($PAGE);
3748 if (method_exists($renderer, 'course_index_drawer')) {
3749 return $renderer->course_index_drawer($format);
3752 return '';
3756 * Returns course modules tagged with a specified tag ready for output on tag/index.php page
3758 * This is a callback used by the tag area core/course_modules to search for course modules
3759 * tagged with a specific tag.
3761 * @param core_tag_tag $tag
3762 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag
3763 * are displayed on the page and the per-page limit may be bigger
3764 * @param int $fromcontextid context id where the link was displayed, may be used by callbacks
3765 * to display items in the same context first
3766 * @param int $contextid context id where to search for records
3767 * @param bool $recursivecontext search in subcontexts as well
3768 * @param int $page 0-based number of page being displayed
3769 * @return \core_tag\output\tagindex
3771 function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcontextid = 0, $contextid = 0,
3772 $recursivecontext = 1, $page = 0) {
3773 global $OUTPUT;
3774 $perpage = $exclusivemode ? 20 : 5;
3776 // Build select query.
3777 $ctxselect = context_helper::get_preload_record_columns_sql('ctx');
3778 $query = "SELECT cm.id AS cmid, c.id AS courseid, $ctxselect
3779 FROM {course_modules} cm
3780 JOIN {tag_instance} tt ON cm.id = tt.itemid
3781 JOIN {course} c ON cm.course = c.id
3782 JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel
3783 WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component
3784 AND cm.deletioninprogress = 0
3785 AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%";
3787 $params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core',
3788 'coursemodulecontextlevel' => CONTEXT_MODULE);
3789 if ($contextid) {
3790 $context = context::instance_by_id($contextid);
3791 $query .= $recursivecontext ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid';
3792 $params['contextid'] = $context->id;
3793 $params['path'] = $context->path.'/%';
3796 $query .= ' ORDER BY';
3797 if ($fromcontextid) {
3798 // In order-clause specify that modules from inside "fromctx" context should be returned first.
3799 $fromcontext = context::instance_by_id($fromcontextid);
3800 $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),';
3801 $params['fromcontextid'] = $fromcontext->id;
3802 $params['frompath'] = $fromcontext->path.'/%';
3804 $query .= ' c.sortorder, cm.id';
3805 $totalpages = $page + 1;
3807 // Use core_tag_index_builder to build and filter the list of items.
3808 // Request one item more than we need so we know if next page exists.
3809 $builder = new core_tag_index_builder('core', 'course_modules', $query, $params, $page * $perpage, $perpage + 1);
3810 while ($item = $builder->has_item_that_needs_access_check()) {
3811 context_helper::preload_from_record($item);
3812 $courseid = $item->courseid;
3813 if (!$builder->can_access_course($courseid)) {
3814 $builder->set_accessible($item, false);
3815 continue;
3817 $modinfo = get_fast_modinfo($builder->get_course($courseid));
3818 // Set accessibility of this item and all other items in the same course.
3819 $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) {
3820 if ($taggeditem->courseid == $courseid) {
3821 $cm = $modinfo->get_cm($taggeditem->cmid);
3822 $builder->set_accessible($taggeditem, $cm->uservisible);
3827 $items = $builder->get_items();
3828 if (count($items) > $perpage) {
3829 $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists.
3830 array_pop($items);
3833 // Build the display contents.
3834 if ($items) {
3835 $tagfeed = new core_tag\output\tagfeed();
3836 foreach ($items as $item) {
3837 context_helper::preload_from_record($item);
3838 $course = $builder->get_course($item->courseid);
3839 $modinfo = get_fast_modinfo($course);
3840 $cm = $modinfo->get_cm($item->cmid);
3841 $courseurl = course_get_url($item->courseid, $cm->sectionnum);
3842 $cmname = $cm->get_formatted_name();
3843 if (!$exclusivemode) {
3844 $cmname = shorten_text($cmname, 100);
3846 $cmname = html_writer::link($cm->url?:$courseurl, $cmname);
3847 $coursename = format_string($course->fullname, true,
3848 array('context' => context_course::instance($item->courseid)));
3849 $coursename = html_writer::link($courseurl, $coursename);
3850 $icon = html_writer::empty_tag('img', array('src' => $cm->get_icon_url()));
3851 $tagfeed->add($icon, $cmname, $coursename);
3854 $content = $OUTPUT->render_from_template('core_tag/tagfeed',
3855 $tagfeed->export_for_template($OUTPUT));
3857 return new core_tag\output\tagindex($tag, 'core', 'course_modules', $content,
3858 $exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages);
3863 * Return an object with the list of navigation options in a course that are avaialable or not for the current user.
3864 * This function also handles the frontpage course.
3866 * @param stdClass $context context object (it can be a course context or the system context for frontpage settings)
3867 * @param stdClass $course the course where the settings are being rendered
3868 * @return stdClass the navigation options in a course and their availability status
3869 * @since Moodle 3.2
3871 function course_get_user_navigation_options($context, $course = null) {
3872 global $CFG, $USER;
3874 $isloggedin = isloggedin();
3875 $isguestuser = isguestuser();
3876 $isfrontpage = $context->contextlevel == CONTEXT_SYSTEM;
3878 if ($isfrontpage) {
3879 $sitecontext = $context;
3880 } else {
3881 $sitecontext = context_system::instance();
3884 // Sets defaults for all options.
3885 $options = (object) [
3886 'badges' => false,
3887 'blogs' => false,
3888 'competencies' => false,
3889 'grades' => false,
3890 'notes' => false,
3891 'participants' => false,
3892 'search' => false,
3893 'tags' => false,
3896 $options->blogs = !empty($CFG->enableblogs) &&
3897 ($CFG->bloglevel == BLOG_GLOBAL_LEVEL ||
3898 ($CFG->bloglevel == BLOG_SITE_LEVEL and ($isloggedin and !$isguestuser)))
3899 && has_capability('moodle/blog:view', $sitecontext);
3901 $options->notes = !empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $context);
3903 // Frontpage settings?
3904 if ($isfrontpage) {
3905 // We are on the front page, so make sure we use the proper capability (site:viewparticipants).
3906 $options->participants = course_can_view_participants($sitecontext);
3907 $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext);
3908 $options->tags = !empty($CFG->usetags) && $isloggedin;
3909 $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext);
3910 } else {
3911 // We are in a course, so make sure we use the proper capability (course:viewparticipants).
3912 $options->participants = course_can_view_participants($context);
3914 // Only display badges if they are enabled and the current user can manage them or if they can view them and have,
3915 // at least, one available badge.
3916 if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges)) {
3917 $canmanage = has_any_capability([
3918 'moodle/badges:createbadge',
3919 'moodle/badges:awardbadge',
3920 'moodle/badges:configurecriteria',
3921 'moodle/badges:configuremessages',
3922 'moodle/badges:configuredetails',
3923 'moodle/badges:deletebadge',
3925 $context
3927 $totalbadges = [];
3928 $canview = false;
3929 if (!$canmanage) {
3930 // This only needs to be calculated if the user can't manage badges (to improve performance).
3931 $canview = has_capability('moodle/badges:viewbadges', $context);
3932 if ($canview) {
3933 require_once($CFG->dirroot.'/lib/badgeslib.php');
3934 if (is_null($course)) {
3935 $totalbadges = count(badges_get_badges(BADGE_TYPE_SITE, 0, '', '', 0, 0, $USER->id));
3936 } else {
3937 $totalbadges = count(badges_get_badges(BADGE_TYPE_COURSE, $course->id, '', '', 0, 0, $USER->id));
3942 $options->badges = ($canmanage || ($canview && $totalbadges > 0));
3944 // Add view grade report is permitted.
3945 $grades = false;
3947 if (has_capability('moodle/grade:viewall', $context)) {
3948 $grades = true;
3949 } else if (!empty($course->showgrades)) {
3950 $reports = core_component::get_plugin_list('gradereport');
3951 if (is_array($reports) && count($reports) > 0) { // Get all installed reports.
3952 arsort($reports); // User is last, we want to test it first.
3953 foreach ($reports as $plugin => $plugindir) {
3954 if (has_capability('gradereport/'.$plugin.':view', $context)) {
3955 // Stop when the first visible plugin is found.
3956 $grades = true;
3957 break;
3962 $options->grades = $grades;
3965 if (\core_competency\api::is_enabled()) {
3966 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage');
3967 $options->competencies = has_any_capability($capabilities, $context);
3969 return $options;
3973 * Return an object with the list of administration options in a course that are available or not for the current user.
3974 * This function also handles the frontpage settings.
3976 * @param stdClass $course course object (for frontpage it should be a clone of $SITE)
3977 * @param stdClass $context context object (course context)
3978 * @return stdClass the administration options in a course and their availability status
3979 * @since Moodle 3.2
3981 function course_get_user_administration_options($course, $context) {
3982 global $CFG;
3983 $isfrontpage = $course->id == SITEID;
3984 $completionenabled = $CFG->enablecompletion && $course->enablecompletion;
3985 $hascompletionoptions = count(core_completion\manager::get_available_completion_options($course->id)) > 0;
3986 $options = new stdClass;
3987 $options->update = has_capability('moodle/course:update', $context);
3988 $options->editcompletion = $CFG->enablecompletion && $course->enablecompletion &&
3989 ($options->update || $hascompletionoptions);
3990 $options->filters = has_capability('moodle/filter:manage', $context) &&
3991 count(filter_get_available_in_context($context)) > 0;
3992 $options->reports = has_capability('moodle/site:viewreports', $context);
3993 $options->backup = has_capability('moodle/backup:backupcourse', $context);
3994 $options->restore = has_capability('moodle/restore:restorecourse', $context);
3995 $options->copy = \core_course\management\helper::can_copy_course($course->id);
3996 $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context));
3998 if (!$isfrontpage) {
3999 $options->tags = has_capability('moodle/course:tag', $context);
4000 $options->gradebook = has_capability('moodle/grade:manage', $context);
4001 $options->outcomes = !empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $context);
4002 $options->badges = !empty($CFG->enablebadges);
4003 $options->import = has_capability('moodle/restore:restoretargetimport', $context);
4004 $options->reset = has_capability('moodle/course:reset', $context);
4005 $options->roles = has_capability('moodle/role:switchroles', $context);
4006 } else {
4007 // Set default options to false.
4008 $listofoptions = array('tags', 'gradebook', 'outcomes', 'badges', 'import', 'publish', 'reset', 'roles', 'grades');
4010 foreach ($listofoptions as $option) {
4011 $options->$option = false;
4015 return $options;
4019 * Validates course start and end dates.
4021 * Checks that the end course date is not greater than the start course date.
4023 * $coursedata['startdate'] or $coursedata['enddate'] may not be set, it depends on the form and user input.
4025 * @param array $coursedata May contain startdate and enddate timestamps, depends on the user input.
4026 * @return mixed False if everything alright, error codes otherwise.
4028 function course_validate_dates($coursedata) {
4030 // If both start and end dates are set end date should be later than the start date.
4031 if (!empty($coursedata['startdate']) && !empty($coursedata['enddate']) &&
4032 ($coursedata['enddate'] < $coursedata['startdate'])) {
4033 return 'enddatebeforestartdate';
4036 // If start date is not set end date can not be set.
4037 if (empty($coursedata['startdate']) && !empty($coursedata['enddate'])) {
4038 return 'nostartdatenoenddate';
4041 return false;
4045 * Check for course updates in the given context level instances (only modules supported right Now)
4047 * @param stdClass $course course object
4048 * @param array $tocheck instances to check for updates
4049 * @param array $filter check only for updates in these areas
4050 * @return array list of warnings and instances with updates information
4051 * @since Moodle 3.2
4053 function course_check_updates($course, $tocheck, $filter = array()) {
4054 global $CFG, $DB;
4056 $instances = array();
4057 $warnings = array();
4058 $modulescallbacksupport = array();
4059 $modinfo = get_fast_modinfo($course);
4061 $supportedplugins = get_plugin_list_with_function('mod', 'check_updates_since');
4063 // Check instances.
4064 foreach ($tocheck as $instance) {
4065 if ($instance['contextlevel'] == 'module') {
4066 // Check module visibility.
4067 try {
4068 $cm = $modinfo->get_cm($instance['id']);
4069 } catch (Exception $e) {
4070 $warnings[] = array(
4071 'item' => 'module',
4072 'itemid' => $instance['id'],
4073 'warningcode' => 'cmidnotincourse',
4074 'message' => 'This module id does not belong to this course.'
4076 continue;
4079 if (!$cm->uservisible) {
4080 $warnings[] = array(
4081 'item' => 'module',
4082 'itemid' => $instance['id'],
4083 'warningcode' => 'nonuservisible',
4084 'message' => 'You don\'t have access to this module.'
4086 continue;
4088 if (empty($supportedplugins['mod_' . $cm->modname])) {
4089 $warnings[] = array(
4090 'item' => 'module',
4091 'itemid' => $instance['id'],
4092 'warningcode' => 'missingcallback',
4093 'message' => 'This module does not implement the check_updates_since callback: ' . $instance['contextlevel'],
4095 continue;
4097 // Retrieve the module instance.
4098 $instances[] = array(
4099 'contextlevel' => $instance['contextlevel'],
4100 'id' => $instance['id'],
4101 'updates' => call_user_func($cm->modname . '_check_updates_since', $cm, $instance['since'], $filter)
4104 } else {
4105 $warnings[] = array(
4106 'item' => 'contextlevel',
4107 'itemid' => $instance['id'],
4108 'warningcode' => 'contextlevelnotsupported',
4109 'message' => 'Context level not yet supported ' . $instance['contextlevel'],
4113 return array($instances, $warnings);
4117 * This function classifies a course as past, in progress or future.
4119 * This function may incur a DB hit to calculate course completion.
4120 * @param stdClass $course Course record
4121 * @param stdClass $user User record (optional - defaults to $USER).
4122 * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required).
4123 * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST)
4125 function course_classify_for_timeline($course, $user = null, $completioninfo = null) {
4126 global $USER;
4128 if ($user == null) {
4129 $user = $USER;
4132 if ($completioninfo == null) {
4133 $completioninfo = new completion_info($course);
4136 // Let plugins override data for timeline classification.
4137 $pluginsfunction = get_plugins_with_function('extend_course_classify_for_timeline', 'lib.php');
4138 foreach ($pluginsfunction as $plugintype => $plugins) {
4139 foreach ($plugins as $pluginfunction) {
4140 $pluginfunction($course, $user, $completioninfo);
4144 $today = time();
4145 // End date past.
4146 if (!empty($course->enddate) && (course_classify_end_date($course) < $today)) {
4147 return COURSE_TIMELINE_PAST;
4150 // Course was completed.
4151 if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) {
4152 return COURSE_TIMELINE_PAST;
4155 // Start date not reached.
4156 if (!empty($course->startdate) && (course_classify_start_date($course) > $today)) {
4157 return COURSE_TIMELINE_FUTURE;
4160 // Everything else is in progress.
4161 return COURSE_TIMELINE_INPROGRESS;
4165 * This function calculates the end date to use for display classification purposes,
4166 * incorporating the grace period, if any.
4168 * @param stdClass $course The course record.
4169 * @return int The new enddate.
4171 function course_classify_end_date($course) {
4172 global $CFG;
4173 $coursegraceperiodafter = (empty($CFG->coursegraceperiodafter)) ? 0 : $CFG->coursegraceperiodafter;
4174 $enddate = (new \DateTimeImmutable())->setTimestamp($course->enddate)->modify("+{$coursegraceperiodafter} days");
4175 return $enddate->getTimestamp();
4179 * This function calculates the start date to use for display classification purposes,
4180 * incorporating the grace period, if any.
4182 * @param stdClass $course The course record.
4183 * @return int The new startdate.
4185 function course_classify_start_date($course) {
4186 global $CFG;
4187 $coursegraceperiodbefore = (empty($CFG->coursegraceperiodbefore)) ? 0 : $CFG->coursegraceperiodbefore;
4188 $startdate = (new \DateTimeImmutable())->setTimestamp($course->startdate)->modify("-{$coursegraceperiodbefore} days");
4189 return $startdate->getTimestamp();
4193 * Group a list of courses into either past, future, or in progress.
4195 * The return value will be an array indexed by the COURSE_TIMELINE_* constants
4196 * with each value being an array of courses in that group.
4197 * E.g.
4199 * COURSE_TIMELINE_PAST => [... list of past courses ...],
4200 * COURSE_TIMELINE_FUTURE => [],
4201 * COURSE_TIMELINE_INPROGRESS => []
4204 * @param array $courses List of courses to be grouped.
4205 * @return array
4207 function course_classify_courses_for_timeline(array $courses) {
4208 return array_reduce($courses, function($carry, $course) {
4209 $classification = course_classify_for_timeline($course);
4210 array_push($carry[$classification], $course);
4212 return $carry;
4213 }, [
4214 COURSE_TIMELINE_PAST => [],
4215 COURSE_TIMELINE_FUTURE => [],
4216 COURSE_TIMELINE_INPROGRESS => []
4221 * Get the list of enrolled courses for the current user.
4223 * This function returns a Generator. The courses will be loaded from the database
4224 * in chunks rather than a single query.
4226 * @param int $limit Restrict result set to this amount
4227 * @param int $offset Skip this number of records from the start of the result set
4228 * @param string|null $sort SQL string for sorting
4229 * @param string|null $fields SQL string for fields to be returned
4230 * @param int $dbquerylimit The number of records to load per DB request
4231 * @param array $includecourses courses ids to be restricted
4232 * @param array $hiddencourses courses ids to be excluded
4233 * @return Generator
4235 function course_get_enrolled_courses_for_logged_in_user(
4236 int $limit = 0,
4237 int $offset = 0,
4238 string $sort = null,
4239 string $fields = null,
4240 int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4241 array $includecourses = [],
4242 array $hiddencourses = []
4243 ) : Generator {
4245 $haslimit = !empty($limit);
4246 $recordsloaded = 0;
4247 $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4249 while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $includecourses, false, $offset, $hiddencourses)) {
4250 yield from $courses;
4252 $recordsloaded += $querylimit;
4254 if (count($courses) < $querylimit) {
4255 break;
4257 if ($haslimit && $recordsloaded >= $limit) {
4258 break;
4261 $offset += $querylimit;
4266 * Get the list of enrolled courses the current user searched for.
4268 * This function returns a Generator. The courses will be loaded from the database
4269 * in chunks rather than a single query.
4271 * @param int $limit Restrict result set to this amount
4272 * @param int $offset Skip this number of records from the start of the result set
4273 * @param string|null $sort SQL string for sorting
4274 * @param string|null $fields SQL string for fields to be returned
4275 * @param int $dbquerylimit The number of records to load per DB request
4276 * @param array $searchcriteria contains search criteria
4277 * @param array $options display options, same as in get_courses() except 'recursive' is ignored -
4278 * search is always category-independent
4279 * @return Generator
4281 function course_get_enrolled_courses_for_logged_in_user_from_search(
4282 int $limit = 0,
4283 int $offset = 0,
4284 string $sort = null,
4285 string $fields = null,
4286 int $dbquerylimit = COURSE_DB_QUERY_LIMIT,
4287 array $searchcriteria = [],
4288 array $options = []
4289 ) : Generator {
4291 $haslimit = !empty($limit);
4292 $recordsloaded = 0;
4293 $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit;
4294 $ids = core_course_category::search_courses($searchcriteria, $options);
4296 // If no courses were found matching the criteria return back.
4297 if (empty($ids)) {
4298 return;
4301 while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $ids, false, $offset)) {
4302 yield from $courses;
4304 $recordsloaded += $querylimit;
4306 if (count($courses) < $querylimit) {
4307 break;
4309 if ($haslimit && $recordsloaded >= $limit) {
4310 break;
4313 $offset += $querylimit;
4318 * Search the given $courses for any that match the given $classification up to the specified
4319 * $limit.
4321 * This function will return the subset of courses that match the classification as well as the
4322 * number of courses it had to process to build that subset.
4324 * It is recommended that for larger sets of courses this function is given a Generator that loads
4325 * the courses from the database in chunks.
4327 * @param array|Traversable $courses List of courses to process
4328 * @param string $classification One of the COURSE_TIMELINE_* constants
4329 * @param int $limit Limit the number of results to this amount
4330 * @return array First value is the filtered courses, second value is the number of courses processed
4332 function course_filter_courses_by_timeline_classification(
4333 $courses,
4334 string $classification,
4335 int $limit = 0
4336 ) : array {
4338 if (!in_array($classification,
4339 [COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS,
4340 COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN, COURSE_TIMELINE_SEARCH])) {
4341 $message = 'Classification must be one of COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, '
4342 . 'COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_SEARCH or COURSE_TIMELINE_FUTURE';
4343 throw new moodle_exception($message);
4346 $filteredcourses = [];
4347 $numberofcoursesprocessed = 0;
4348 $filtermatches = 0;
4350 foreach ($courses as $course) {
4351 $numberofcoursesprocessed++;
4352 $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0);
4354 // Added as of MDL-63457 toggle viewability for each user.
4355 if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN || ($classification == COURSE_TIMELINE_HIDDEN && $pref) ||
4356 $classification == COURSE_TIMELINE_SEARCH||
4357 (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) {
4358 $filteredcourses[] = $course;
4359 $filtermatches++;
4362 if ($limit && $filtermatches >= $limit) {
4363 // We've found the number of requested courses. No need to continue searching.
4364 break;
4368 // Return the number of filtered courses as well as the number of courses that were searched
4369 // in order to find the matching courses. This allows the calling code to do some kind of
4370 // pagination.
4371 return [$filteredcourses, $numberofcoursesprocessed];
4375 * Search the given $courses for any that match the given $classification up to the specified
4376 * $limit.
4378 * This function will return the subset of courses that are favourites as well as the
4379 * number of courses it had to process to build that subset.
4381 * It is recommended that for larger sets of courses this function is given a Generator that loads
4382 * the courses from the database in chunks.
4384 * @param array|Traversable $courses List of courses to process
4385 * @param array $favouritecourseids Array of favourite courses.
4386 * @param int $limit Limit the number of results to this amount
4387 * @return array First value is the filtered courses, second value is the number of courses processed
4389 function course_filter_courses_by_favourites(
4390 $courses,
4391 $favouritecourseids,
4392 int $limit = 0
4393 ) : array {
4395 $filteredcourses = [];
4396 $numberofcoursesprocessed = 0;
4397 $filtermatches = 0;
4399 foreach ($courses as $course) {
4400 $numberofcoursesprocessed++;
4402 if (in_array($course->id, $favouritecourseids)) {
4403 $filteredcourses[] = $course;
4404 $filtermatches++;
4407 if ($limit && $filtermatches >= $limit) {
4408 // We've found the number of requested courses. No need to continue searching.
4409 break;
4413 // Return the number of filtered courses as well as the number of courses that were searched
4414 // in order to find the matching courses. This allows the calling code to do some kind of
4415 // pagination.
4416 return [$filteredcourses, $numberofcoursesprocessed];
4420 * Search the given $courses for any that have a $customfieldname value that matches the given
4421 * $customfieldvalue, up to the specified $limit.
4423 * This function will return the subset of courses that matches the value as well as the
4424 * number of courses it had to process to build that subset.
4426 * It is recommended that for larger sets of courses this function is given a Generator that loads
4427 * the courses from the database in chunks.
4429 * @param array|Traversable $courses List of courses to process
4430 * @param string $customfieldname the shortname of the custom field to match against
4431 * @param string $customfieldvalue the value this custom field needs to match
4432 * @param int $limit Limit the number of results to this amount
4433 * @return array First value is the filtered courses, second value is the number of courses processed
4435 function course_filter_courses_by_customfield(
4436 $courses,
4437 $customfieldname,
4438 $customfieldvalue,
4439 int $limit = 0
4440 ) : array {
4441 global $DB;
4443 if (!$courses) {
4444 return [[], 0];
4447 // Prepare the list of courses to search through.
4448 $coursesbyid = [];
4449 foreach ($courses as $course) {
4450 $coursesbyid[$course->id] = $course;
4452 if (!$coursesbyid) {
4453 return [[], 0];
4455 list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED);
4457 // Get the id of the custom field.
4458 $sql = "
4459 SELECT f.id
4460 FROM {customfield_field} f
4461 JOIN {customfield_category} cat ON cat.id = f.categoryid
4462 WHERE f.shortname = ?
4463 AND cat.component = 'core_course'
4464 AND cat.area = 'course'
4466 $fieldid = $DB->get_field_sql($sql, [$customfieldname]);
4467 if (!$fieldid) {
4468 return [[], 0];
4471 // Get a list of courseids that match that custom field value.
4472 if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) {
4473 $comparevalue = $DB->sql_compare_text('cd.value');
4474 $sql = "
4475 SELECT c.id
4476 FROM {course} c
4477 LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid
4478 WHERE c.id $csql
4479 AND (cd.value IS NULL OR $comparevalue = '' OR $comparevalue = '0')
4481 $params['fieldid'] = $fieldid;
4482 $matchcourseids = $DB->get_fieldset_sql($sql, $params);
4483 } else {
4484 $comparevalue = $DB->sql_compare_text('value');
4485 $select = "fieldid = :fieldid AND $comparevalue = :customfieldvalue AND instanceid $csql";
4486 $params['fieldid'] = $fieldid;
4487 $params['customfieldvalue'] = $customfieldvalue;
4488 $matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params);
4491 // Prepare the list of courses to return.
4492 $filteredcourses = [];
4493 $numberofcoursesprocessed = 0;
4494 $filtermatches = 0;
4496 foreach ($coursesbyid as $course) {
4497 $numberofcoursesprocessed++;
4499 if (in_array($course->id, $matchcourseids)) {
4500 $filteredcourses[] = $course;
4501 $filtermatches++;
4504 if ($limit && $filtermatches >= $limit) {
4505 // We've found the number of requested courses. No need to continue searching.
4506 break;
4510 // Return the number of filtered courses as well as the number of courses that were searched
4511 // in order to find the matching courses. This allows the calling code to do some kind of
4512 // pagination.
4513 return [$filteredcourses, $numberofcoursesprocessed];
4517 * Check module updates since a given time.
4518 * This function checks for updates in the module config, file areas, completion, grades, comments and ratings.
4520 * @param cm_info $cm course module data
4521 * @param int $from the time to check
4522 * @param array $fileareas additional file ares to check
4523 * @param array $filter if we need to filter and return only selected updates
4524 * @return stdClass object with the different updates
4525 * @since Moodle 3.2
4527 function course_check_module_updates_since($cm, $from, $fileareas = array(), $filter = array()) {
4528 global $DB, $CFG, $USER;
4530 $context = $cm->context;
4531 $mod = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST);
4533 $updates = new stdClass();
4534 $course = get_course($cm->course);
4535 $component = 'mod_' . $cm->modname;
4537 // Check changes in the module configuration.
4538 if (isset($mod->timemodified) and (empty($filter) or in_array('configuration', $filter))) {
4539 $updates->configuration = (object) array('updated' => false);
4540 if ($updates->configuration->updated = $mod->timemodified > $from) {
4541 $updates->configuration->timeupdated = $mod->timemodified;
4545 // Check for updates in files.
4546 if (plugin_supports('mod', $cm->modname, FEATURE_MOD_INTRO)) {
4547 $fileareas[] = 'intro';
4549 if (!empty($fileareas) and (empty($filter) or in_array('fileareas', $filter))) {
4550 $fs = get_file_storage();
4551 $files = $fs->get_area_files($context->id, $component, $fileareas, false, "filearea, timemodified DESC", false, $from);
4552 foreach ($fileareas as $filearea) {
4553 $updates->{$filearea . 'files'} = (object) array('updated' => false);
4555 foreach ($files as $file) {
4556 $updates->{$file->get_filearea() . 'files'}->updated = true;
4557 $updates->{$file->get_filearea() . 'files'}->itemids[] = $file->get_id();
4561 // Check completion.
4562 $supportcompletion = plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES);
4563 $supportcompletion = $supportcompletion or plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_TRACKS_VIEWS);
4564 if ($supportcompletion and (empty($filter) or in_array('completion', $filter))) {
4565 $updates->completion = (object) array('updated' => false);
4566 $completion = new completion_info($course);
4567 // Use wholecourse to cache all the modules the first time.
4568 $completiondata = $completion->get_data($cm, true);
4569 if ($updates->completion->updated = !empty($completiondata->timemodified) && $completiondata->timemodified > $from) {
4570 $updates->completion->timemodified = $completiondata->timemodified;
4574 // Check grades.
4575 $supportgrades = plugin_supports('mod', $cm->modname, FEATURE_GRADE_HAS_GRADE);
4576 $supportgrades = $supportgrades or plugin_supports('mod', $cm->modname, FEATURE_GRADE_OUTCOMES);
4577 if ($supportgrades and (empty($filter) or (in_array('gradeitems', $filter) or in_array('outcomes', $filter)))) {
4578 require_once($CFG->libdir . '/gradelib.php');
4579 $grades = grade_get_grades($course->id, 'mod', $cm->modname, $mod->id, $USER->id);
4581 if (empty($filter) or in_array('gradeitems', $filter)) {
4582 $updates->gradeitems = (object) array('updated' => false);
4583 foreach ($grades->items as $gradeitem) {
4584 foreach ($gradeitem->grades as $grade) {
4585 if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4586 $updates->gradeitems->updated = true;
4587 $updates->gradeitems->itemids[] = $gradeitem->id;
4593 if (empty($filter) or in_array('outcomes', $filter)) {
4594 $updates->outcomes = (object) array('updated' => false);
4595 foreach ($grades->outcomes as $outcome) {
4596 foreach ($outcome->grades as $grade) {
4597 if ($grade->datesubmitted > $from or $grade->dategraded > $from) {
4598 $updates->outcomes->updated = true;
4599 $updates->outcomes->itemids[] = $outcome->id;
4606 // Check comments.
4607 if (plugin_supports('mod', $cm->modname, FEATURE_COMMENT) and (empty($filter) or in_array('comments', $filter))) {
4608 $updates->comments = (object) array('updated' => false);
4609 require_once($CFG->dirroot . '/comment/lib.php');
4610 require_once($CFG->dirroot . '/comment/locallib.php');
4611 $manager = new comment_manager();
4612 $comments = $manager->get_component_comments_since($course, $context, $component, $from, $cm);
4613 if (!empty($comments)) {
4614 $updates->comments->updated = true;
4615 $updates->comments->itemids = array_keys($comments);
4619 // Check ratings.
4620 if (plugin_supports('mod', $cm->modname, FEATURE_RATE) and (empty($filter) or in_array('ratings', $filter))) {
4621 $updates->ratings = (object) array('updated' => false);
4622 require_once($CFG->dirroot . '/rating/lib.php');
4623 $manager = new rating_manager();
4624 $ratings = $manager->get_component_ratings_since($context, $component, $from);
4625 if (!empty($ratings)) {
4626 $updates->ratings->updated = true;
4627 $updates->ratings->itemids = array_keys($ratings);
4631 return $updates;
4635 * Returns true if the user can view the participant page, false otherwise,
4637 * @param context $context The context we are checking.
4638 * @return bool
4640 function course_can_view_participants($context) {
4641 $viewparticipantscap = 'moodle/course:viewparticipants';
4642 if ($context->contextlevel == CONTEXT_SYSTEM) {
4643 $viewparticipantscap = 'moodle/site:viewparticipants';
4646 return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context);
4650 * Checks if a user can view the participant page, if not throws an exception.
4652 * @param context $context The context we are checking.
4653 * @throws required_capability_exception
4655 function course_require_view_participants($context) {
4656 if (!course_can_view_participants($context)) {
4657 $viewparticipantscap = 'moodle/course:viewparticipants';
4658 if ($context->contextlevel == CONTEXT_SYSTEM) {
4659 $viewparticipantscap = 'moodle/site:viewparticipants';
4661 throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', '');
4666 * Return whether the user can download from the specified backup file area in the given context.
4668 * @param string $filearea the backup file area. E.g. 'course', 'backup' or 'automated'.
4669 * @param \context $context
4670 * @param stdClass $user the user object. If not provided, the current user will be checked.
4671 * @return bool true if the user is allowed to download in the context, false otherwise.
4673 function can_download_from_backup_filearea($filearea, \context $context, stdClass $user = null) {
4674 $candownload = false;
4675 switch ($filearea) {
4676 case 'course':
4677 case 'backup':
4678 $candownload = has_capability('moodle/backup:downloadfile', $context, $user);
4679 break;
4680 case 'automated':
4681 // Given the automated backups may contain userinfo, we restrict access such that only users who are able to
4682 // restore with userinfo are able to download the file. Users can't create these backups, so checking 'backup:userinfo'
4683 // doesn't make sense here.
4684 $candownload = has_capability('moodle/backup:downloadfile', $context, $user) &&
4685 has_capability('moodle/restore:userinfo', $context, $user);
4686 break;
4687 default:
4688 break;
4691 return $candownload;
4695 * Get a list of hidden courses
4697 * @param int|object|null $user User override to get the filter from. Defaults to current user
4698 * @return array $ids List of hidden courses
4699 * @throws coding_exception
4701 function get_hidden_courses_on_timeline($user = null) {
4702 global $USER;
4704 if (empty($user)) {
4705 $user = $USER->id;
4708 $preferences = get_user_preferences(null, null, $user);
4709 $ids = [];
4710 foreach ($preferences as $key => $value) {
4711 if (preg_match('/block_myoverview_hidden_course_(\d)+/', $key)) {
4712 $id = preg_split('/block_myoverview_hidden_course_/', $key);
4713 $ids[] = $id[1];
4717 return $ids;
4721 * Returns a list of the most recently courses accessed by a user
4723 * @param int $userid User id from which the courses will be obtained
4724 * @param int $limit Restrict result set to this amount
4725 * @param int $offset Skip this number of records from the start of the result set
4726 * @param string|null $sort SQL string for sorting
4727 * @return array
4729 function course_get_recent_courses(int $userid = null, int $limit = 0, int $offset = 0, string $sort = null) {
4731 global $CFG, $USER, $DB;
4733 if (empty($userid)) {
4734 $userid = $USER->id;
4737 $basefields = [
4738 'id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category',
4739 'shortname', 'fullname', 'timeaccess', 'component', 'visible',
4740 'showactivitydates', 'showcompletionconditions',
4743 if (empty($sort)) {
4744 $sort = 'timeaccess DESC';
4745 } else {
4746 // The SQL string for sorting can define sorting by multiple columns.
4747 $rawsorts = explode(',', $sort);
4748 $sorts = array();
4749 // Validate and trim the sort parameters in the SQL string for sorting.
4750 foreach ($rawsorts as $rawsort) {
4751 $sort = trim($rawsort);
4752 $sortparams = explode(' ', $sort);
4753 // A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess').
4754 if (count($sortparams) > 2) {
4755 throw new invalid_parameter_exception(
4756 'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].');
4758 $sortfield = trim($sortparams[0]);
4759 // Validate the value which defines the field to sort by.
4760 if (!in_array($sortfield, $basefields)) {
4761 throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' .
4762 implode(', ', $basefields) . '.');
4764 $sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : '';
4765 // Validate the value which defines the sort direction (if present).
4766 $allowedsortdirections = ['asc', 'desc'];
4767 if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) {
4768 throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' .
4769 implode(', ', $allowedsortdirections) . '.');
4771 $sorts[] = $sort;
4773 $sort = implode(',', $sorts);
4776 $ctxfields = context_helper::get_preload_record_columns_sql('ctx');
4778 $coursefields = 'c.' . join(',', $basefields);
4780 // Ask the favourites service to give us the join SQL for favourited courses,
4781 // so we can include favourite information in the query.
4782 $usercontext = \context_user::instance($userid);
4783 $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
4784 list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_course', 'courses', 'fav', 'ul.courseid');
4786 $sql = "SELECT $coursefields, $ctxfields
4787 FROM {course} c
4788 JOIN {context} ctx
4789 ON ctx.contextlevel = :contextlevel
4790 AND ctx.instanceid = c.id
4791 JOIN {user_lastaccess} ul
4792 ON ul.courseid = c.id
4793 $favsql
4794 LEFT JOIN {enrol} eg ON eg.courseid = c.id AND eg.status = :statusenrolg AND eg.enrol = :guestenrol
4795 WHERE ul.userid = :userid
4796 AND c.visible = :visible
4797 AND (eg.id IS NOT NULL
4798 OR EXISTS (SELECT e.id
4799 FROM {enrol} e
4800 JOIN {user_enrolments} ue ON ue.enrolid = e.id
4801 WHERE e.courseid = c.id
4802 AND e.status = :statusenrol
4803 AND ue.status = :status
4804 AND ue.userid = :userid2
4805 AND ue.timestart < :now1
4806 AND (ue.timeend = 0 OR ue.timeend > :now2)
4808 ORDER BY $sort";
4810 $now = round(time(), -2); // Improves db caching.
4811 $params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE,
4812 'statusenrol' => ENROL_INSTANCE_ENABLED, 'guestenrol' => 'guest', 'now1' => $now, 'now2' => $now,
4813 'userid2' => $userid, 'statusenrolg' => ENROL_INSTANCE_ENABLED] + $favparams;
4815 $recentcourses = $DB->get_records_sql($sql, $params, $offset, $limit);
4817 // Filter courses if last access field is hidden.
4818 $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields));
4820 if ($userid != $USER->id && isset($hiddenfields['lastaccess'])) {
4821 $recentcourses = array_filter($recentcourses, function($course) {
4822 context_helper::preload_from_record($course);
4823 $context = context_course::instance($course->id, IGNORE_MISSING);
4824 // If last access was a hidden field, a user requesting info about another user would need permission to view hidden
4825 // fields.
4826 return has_capability('moodle/course:viewhiddenuserfields', $context);
4830 return $recentcourses;
4834 * Calculate the course start date and offset for the given user ids.
4836 * If the course is a fixed date course then the course start date will be returned.
4837 * If the course is a relative date course then the course date will be calculated and
4838 * and offset provided.
4840 * The dates are returned as an array with the index being the user id. The array
4841 * contains the start date and start offset values for the user.
4843 * If the user is not enrolled in the course then the course start date will be returned.
4845 * If we have a course which starts on 1563244000 and 2 users, id 123 and 456, where the
4846 * former is enrolled in the course at 1563244693 and the latter is not enrolled then the
4847 * return value would look like:
4849 * '123' => [
4850 * 'start' => 1563244693,
4851 * 'startoffset' => 693
4852 * ],
4853 * '456' => [
4854 * 'start' => 1563244000,
4855 * 'startoffset' => 0
4859 * @param stdClass $course The course to fetch dates for.
4860 * @param array $userids The list of user ids to get dates for.
4861 * @return array
4863 function course_get_course_dates_for_user_ids(stdClass $course, array $userids): array {
4864 if (empty($course->relativedatesmode)) {
4865 // This course isn't set to relative dates so we can early return with the course
4866 // start date.
4867 return array_reduce($userids, function($carry, $userid) use ($course) {
4868 $carry[$userid] = [
4869 'start' => $course->startdate,
4870 'startoffset' => 0
4872 return $carry;
4873 }, []);
4876 // We're dealing with a relative dates course now so we need to calculate some dates.
4877 $cache = cache::make('core', 'course_user_dates');
4878 $dates = [];
4879 $uncacheduserids = [];
4881 // Try fetching the values from the cache so that we don't need to do a DB request.
4882 foreach ($userids as $userid) {
4883 $cachekey = "{$course->id}_{$userid}";
4884 $cachedvalue = $cache->get($cachekey);
4886 if ($cachedvalue === false) {
4887 // Looks like we haven't seen this user for this course before so we'll have
4888 // to fetch it.
4889 $uncacheduserids[] = $userid;
4890 } else {
4891 [$start, $startoffset] = $cachedvalue;
4892 $dates[$userid] = [
4893 'start' => $start,
4894 'startoffset' => $startoffset
4899 if (!empty($uncacheduserids)) {
4900 // Load the enrolments for any users we haven't seen yet. Set the "onlyactive" param
4901 // to false because it filters out users with enrolment start times in the future which
4902 // we don't want.
4903 $enrolments = enrol_get_course_users($course->id, false, $uncacheduserids);
4905 foreach ($uncacheduserids as $userid) {
4906 // Find the user enrolment that has the earliest start date.
4907 $enrolment = array_reduce(array_values($enrolments), function($carry, $enrolment) use ($userid) {
4908 // Only consider enrolments for this user if the user enrolment is active and the
4909 // enrolment method is enabled.
4910 if (
4911 $enrolment->uestatus == ENROL_USER_ACTIVE &&
4912 $enrolment->estatus == ENROL_INSTANCE_ENABLED &&
4913 $enrolment->id == $userid
4915 if (is_null($carry)) {
4916 // Haven't found an enrolment yet for this user so use the one we just found.
4917 $carry = $enrolment;
4918 } else {
4919 // We've already found an enrolment for this user so let's use which ever one
4920 // has the earliest start time.
4921 $carry = $carry->uetimestart < $enrolment->uetimestart ? $carry : $enrolment;
4925 return $carry;
4926 }, null);
4928 if ($enrolment) {
4929 // The course is in relative dates mode so we calculate the student's start
4930 // date based on their enrolment start date.
4931 $start = $course->startdate > $enrolment->uetimestart ? $course->startdate : $enrolment->uetimestart;
4932 $startoffset = $start - $course->startdate;
4933 } else {
4934 // The user is not enrolled in the course so default back to the course start date.
4935 $start = $course->startdate;
4936 $startoffset = 0;
4939 $dates[$userid] = [
4940 'start' => $start,
4941 'startoffset' => $startoffset
4944 $cachekey = "{$course->id}_{$userid}";
4945 $cache->set($cachekey, [$start, $startoffset]);
4949 return $dates;
4953 * Calculate the course start date and offset for the given user id.
4955 * If the course is a fixed date course then the course start date will be returned.
4956 * If the course is a relative date course then the course date will be calculated and
4957 * and offset provided.
4959 * The return array contains the start date and start offset values for the user.
4961 * If the user is not enrolled in the course then the course start date will be returned.
4963 * If we have a course which starts on 1563244000. If a user's enrolment starts on 1563244693
4964 * then the return would be:
4966 * 'start' => 1563244693,
4967 * 'startoffset' => 693
4970 * If the use was not enrolled then the return would be:
4972 * 'start' => 1563244000,
4973 * 'startoffset' => 0
4976 * @param stdClass $course The course to fetch dates for.
4977 * @param int $userid The user id to get dates for.
4978 * @return array
4980 function course_get_course_dates_for_user_id(stdClass $course, int $userid): array {
4981 return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid];
4985 * Renders the course copy form for the modal on the course management screen.
4987 * @param array $args
4988 * @return string $o Form HTML.
4990 function course_output_fragment_new_base_form($args) {
4992 $serialiseddata = json_decode($args['jsonformdata'], true);
4993 $formdata = [];
4994 if (!empty($serialiseddata)) {
4995 parse_str($serialiseddata, $formdata);
4998 $context = context_course::instance($args['courseid']);
4999 $copycaps = \core_course\management\helper::get_course_copy_capabilities();
5000 require_all_capabilities($copycaps, $context);
5002 $course = get_course($args['courseid']);
5003 $mform = new \core_backup\output\copy_form(
5004 null,
5005 array('course' => $course, 'returnto' => '', 'returnurl' => ''),
5006 'post', '', ['class' => 'ignoredirty'], true, $formdata);
5008 if (!empty($serialiseddata)) {
5009 // If we were passed non-empty form data we want the mform to call validation functions and show errors.
5010 $mform->is_validated();
5013 ob_start();
5014 $mform->display();
5015 $o = ob_get_contents();
5016 ob_end_clean();
5018 return $o;