weekly release 4.0dev
[moodle.git] / course / lib.php
CommitLineData
d9cb06dc 1<?php
d9cb06dc 2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Library of useful functions
19 *
20 * @copyright 1999 Martin Dougiamas http://dougiamas.com
21 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
5dc361e1 22 * @package core_course
d9cb06dc 23 */
f9903ed0 24
df997f84
PS
25defined('MOODLE_INTERNAL') || die;
26
830c3eb9
FR
27use core_courseformat\base as course_format;
28
4e781c7b 29require_once($CFG->libdir.'/completionlib.php');
8bdc9cac 30require_once($CFG->libdir.'/filelib.php');
160e77f9 31require_once($CFG->libdir.'/datalib.php');
ee7084e9 32require_once($CFG->dirroot.'/course/format/lib.php');
f9903ed0 33
5dc361e1
SH
34define('COURSE_MAX_LOGS_PER_PAGE', 1000); // Records.
35define('COURSE_MAX_RECENT_PERIOD', 172800); // Two days, in seconds.
3b7bfbb5
FM
36
37/**
38 * Number of courses to display when summaries are included.
39 * @var int
40 * @deprecated since 2.4, use $CFG->courseswithsummarieslimit instead.
41 */
42define('COURSE_MAX_SUMMARIES_PER_PAGE', 10);
43
5dc361e1
SH
44// Max courses in log dropdown before switching to optional.
45define('COURSE_MAX_COURSES_PER_DROPDOWN', 1000);
46// Max users in log dropdown before switching to optional.
47define('COURSE_MAX_USERS_PER_DROPDOWN', 1000);
48define('FRONTPAGENEWS', '0');
5dc361e1 49define('FRONTPAGECATEGORYNAMES', '2');
5dc361e1 50define('FRONTPAGECATEGORYCOMBO', '4');
0fd26350 51define('FRONTPAGEENROLLEDCOURSELIST', '5');
5dc361e1
SH
52define('FRONTPAGEALLCOURSELIST', '6');
53define('FRONTPAGECOURSESEARCH', '7');
54// Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage.
6f24e48e 55define('EXCELROWS', 65535);
56define('FIRSTUSEDEXCELROW', 3);
60fdc714 57
89bfeee0 58define('MOD_CLASS_ACTIVITY', 0);
59define('MOD_CLASS_RESOURCE', 1);
60
cd4abbc7 61define('COURSE_TIMELINE_ALLINCLUDINGHIDDEN', 'allincludinghidden');
6481a21f 62define('COURSE_TIMELINE_ALL', 'all');
d07373f2
DW
63define('COURSE_TIMELINE_PAST', 'past');
64define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
65define('COURSE_TIMELINE_FUTURE', 'future');
ccd62d28 66define('COURSE_TIMELINE_SEARCH', 'search');
3cfff885 67define('COURSE_FAVOURITES', 'favourites');
e6f03948 68define('COURSE_TIMELINE_HIDDEN', 'hidden');
8d166d77 69define('COURSE_CUSTOMFIELD', 'customfield');
2c1d19fd 70define('COURSE_DB_QUERY_LIMIT', 1000);
8d166d77
DS
71/** Searching for all courses that have no value for the specified custom field. */
72define('COURSE_CUSTOMFIELD_EMPTY', -1);
d07373f2 73
b4121975
MM
74// Course activity chooser footer default display option.
75define('COURSE_CHOOSER_FOOTER_NONE', 'hidden');
76
11456d71
MH
77// Download course content options.
78define('DOWNLOAD_COURSE_CONTENT_DISABLED', 0);
79define('DOWNLOAD_COURSE_CONTENT_ENABLED', 1);
80define('DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT', 2);
81
600149be 82function make_log_url($module, $url) {
83 switch ($module) {
bd7be234 84 case 'course':
d48f6068 85 if (strpos($url, 'report/') === 0) {
3ce3bb88 86 // there is only one report type, course reports are deprecated
d48f6068
PS
87 $url = "/$url";
88 break;
89 }
bd7be234 90 case 'file':
91 case 'login':
92 case 'lib':
93 case 'admin':
95d1d037 94 case 'category':
bd5d0ce5 95 case 'mnet course':
11003188 96 if (strpos($url, '../') === 0) {
97 $url = ltrim($url, '.');
98 } else {
99 $url = "/course/$url";
100 }
bd5d0ce5 101 break;
e662454a
AA
102 case 'calendar':
103 $url = "/calendar/$url";
104 break;
01d5d399 105 case 'user':
bd7be234 106 case 'blog':
11003188 107 $url = "/$module/$url";
600149be 108 break;
bd7be234 109 case 'upload':
11003188 110 $url = $url;
c80b7585 111 break;
38fb8190 112 case 'coursetags':
11003188 113 $url = '/'.$url;
38fb8190 114 break;
bd7be234 115 case 'library':
116 case '':
11003188 117 $url = '/';
de2dfe68 118 break;
4597d533 119 case 'message':
11003188 120 $url = "/message/$url";
121 break;
122 case 'notes':
123 $url = "/notes/$url";
4597d533 124 break;
b89e4ad8 125 case 'tag':
126 $url = "/tag/$url";
127 break;
dbcf271b 128 case 'role':
129 $url = '/'.$url;
130 break;
afd2efc0
AD
131 case 'grade':
132 $url = "/grade/$url";
133 break;
600149be 134 default:
11003188 135 $url = "/mod/$module/$url";
600149be 136 break;
137 }
11003188 138
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);
146 }
147
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;
158 }
159 $query = '?'.implode('&amp;', $parts);
160 }
161
162 return $script.$query;
600149be 163}
164
92890025 165
c215b32b 166function build_mnet_logs_array($hostid, $course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='',
167 $modname="", $modid=0, $modaction="", $groupid=0) {
cb6fec1f 168 global $CFG, $DB;
c215b32b 169
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.
172
173 /// Setup for group handling.
238c0dd9 174
175 // TODO: I don't understand group/context/etc. enough to be able to do
c215b32b 176 // something interesting with it here
177 // What is the context of a remote course?
238c0dd9 178
c215b32b 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.
9a5e297b 181 //if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) {
c215b32b 182 // $groupid = get_current_group($course->id);
183 //}
184 /// If this course doesn't have groups, no groupid can be specified.
185 //else if (!$course->groupmode) {
186 // $groupid = 0;
187 //}
cb6fec1f 188
c215b32b 189 $groupid = 0;
190
191 $joins = array();
92701024 192 $where = '';
c215b32b 193
cb6fec1f 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();
199
200 $where .= "l.hostid = :hostid";
201 $params['hostid'] = $hostid;
c215b32b 202
203 // TODO: Is 1 really a magic number referring to the sitename?
cb6fec1f 204 if ($course != SITEID || $modid != 0) {
205 $where .= " AND l.course=:courseid";
206 $params['courseid'] = $course;
c215b32b 207 }
208
209 if ($modname) {
cb6fec1f 210 $where .= " AND l.module = :modname";
211 $params['modname'] = $modname;
c215b32b 212 }
213
214 if ('site_errors' === $modid) {
cb6fec1f 215 $where .= " AND ( l.action='error' OR l.action='infected' )";
c215b32b 216 } else if ($modid) {
238c0dd9 217 //TODO: This assumes that modids are the same across sites... probably
c215b32b 218 //not true
cb6fec1f 219 $where .= " AND l.cmid = :modid";
220 $params['modid'] = $modid;
c215b32b 221 }
222
223 if ($modaction) {
224 $firstletter = substr($modaction, 0, 1);
8086b083 225 if ($firstletter == '-') {
47586394 226 $where .= " AND ".$DB->sql_like('l.action', ':modaction', false, true, true);
8086b083
PS
227 $params['modaction'] = '%'.substr($modaction, 1).'%';
228 } else {
229 $where .= " AND ".$DB->sql_like('l.action', ':modaction', false);
230 $params['modaction'] = '%'.$modaction.'%';
c215b32b 231 }
232 }
233
234 if ($user) {
cb6fec1f 235 $where .= " AND l.userid = :user";
236 $params['user'] = $user;
c215b32b 237 }
238
239 if ($date) {
240 $enddate = $date + 86400;
cb6fec1f 241 $where .= " AND l.time > :date AND l.time < :enddate";
242 $params['date'] = $date;
243 $params['enddate'] = $enddate;
c215b32b 244 }
245
246 $result = array();
cb6fec1f 247 $result['totalcount'] = $DB->count_records_sql("SELECT COUNT('x') FROM {mnet_log} l WHERE $where", $params);
c215b32b 248 if(!empty($result['totalcount'])) {
cb6fec1f 249 $where .= " ORDER BY $order";
250 $result['logs'] = $DB->get_records_sql("$qry $where", $params, $limitfrom, $limitnum);
c215b32b 251 } else {
252 $result['logs'] = array();
253 }
254 return $result;
255}
256
749ce98e
DW
257/**
258 * Checks the integrity of the course data.
259 *
260 * In summary - compares course_sections.sequence and course_modules.section.
261 *
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
270 *
271 * If there are any mismatches, the changes are made and records are updated in DB.
272 *
273 * Course cache is NOT rebuilt if there are any errors!
274 *
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
277 *
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
289 */
290function 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');
295 }
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');
299 }
300 if ($rawmods === null) {
301 $rawmods = get_course_mods($courseid);
302 }
303 if (!$fullcheck && (empty($sections) || empty($rawmods))) {
304 // If either of the arrays is empty, no modules are displayed anyway.
305 return true;
306 }
307 $debuggingprefix = 'Failed integrity check for course ['.$courseid.']. ';
308
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.'"';
325 }
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.']';
333 }
334 $modsection[$cmid] = $sectionid;
335 }
336 }
337 }
338
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;
350 }
351 $sections[$modsection[$cmid]]->newsequence = trim($sections[$modsection[$cmid]]->newsequence.','.$cmid, ',');
352 $messages[] = $debuggingprefix.'Course module ['.$cmid.'] is missing from sequence of section ['.
e2e9cb6a 353 $modsection[$cmid].']';
749ce98e
DW
354 }
355 }
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.']';
362 }
363 }
364 }
365
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));
371 }
372 }
373 }
374
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]));
380 }
381 $messages[] = $debuggingprefix.'Course module ['.$cmid.
382 '] points to section ['.$mod->section.'] instead of ['.$modsection[$cmid].']';
383 }
384 }
385
386 return $messages;
387}
388
cb6fec1f 389/**
390 * For a given course, returns an array of course activity objects
391 * Each item in the array contains he following properties:
392 */
d897cae4 393function get_array_of_activities($courseid) {
d897cae4 394// cm - course module id
395// mod - name of the module (eg forum)
396// section - the number of the section (eg week or topic)
397// name - the name of the instance
5867bfb5 398// visible - is the instance visible or not
13534ef7 399// groupingid - grouping id
86aa7ccf 400// extra - contains extra string to include in any link
cb6fec1f 401 global $CFG, $DB;
8dddba42 402
a0c30e1b 403 $course = $DB->get_record('course', array('id'=>$courseid));
404
405 if (empty($course)) {
406 throw new moodle_exception('courseidnotfound');
407 }
408
d897cae4 409 $mod = array();
410
a0c30e1b 411 $rawmods = get_course_mods($courseid);
412 if (empty($rawmods)) {
dd97c328 413 return $mod; // always return array
d897cae4 414 }
8341055e 415 $courseformat = course_get_format($course);
d897cae4 416
8341055e
MG
417 if ($sections = $DB->get_records('course_sections', array('course' => $courseid),
418 'section ASC', 'id,section,sequence,visible')) {
749ce98e
DW
419 // First check and correct obvious mismatches between course_sections.sequence and course_modules.section.
420 if ($errormessages = course_integrity_check($courseid, $rawmods, $sections)) {
421 debugging(join('<br>', $errormessages));
422 $rawmods = get_course_mods($courseid);
8341055e
MG
423 $sections = $DB->get_records('course_sections', array('course' => $courseid),
424 'section ASC', 'id,section,sequence,visible');
749ce98e
DW
425 }
426 // Build array of activities.
d897cae4 427 foreach ($sections as $section) {
74666583 428 if (!empty($section->sequence)) {
d897cae4 429 $sequence = explode(",", $section->sequence);
430 foreach ($sequence as $seq) {
7af6281f 431 if (empty($rawmods[$seq])) {
432 continue;
433 }
8341055e
MG
434 // Adjust visibleoncoursepage, value in DB may not respect format availability.
435 $rawmods[$seq]->visibleoncoursepage = (!$rawmods[$seq]->visible
436 || $rawmods[$seq]->visibleoncoursepage
437 || empty($CFG->allowstealth)
438 || !$courseformat->allow_stealth_module_visibility($rawmods[$seq], $section)) ? 1 : 0;
439
440 // Create an object that will be cached.
b85b25eb 441 $mod[$seq] = new stdClass();
dd97c328 442 $mod[$seq]->id = $rawmods[$seq]->instance;
443 $mod[$seq]->cm = $rawmods[$seq]->id;
444 $mod[$seq]->mod = $rawmods[$seq]->modname;
adaeccb6 445
446 // Oh dear. Inconsistent names left here for backward compatibility.
dd97c328 447 $mod[$seq]->section = $section->section;
adaeccb6 448 $mod[$seq]->sectionid = $rawmods[$seq]->section;
449
450 $mod[$seq]->module = $rawmods[$seq]->module;
451 $mod[$seq]->added = $rawmods[$seq]->added;
452 $mod[$seq]->score = $rawmods[$seq]->score;
66b250fd 453 $mod[$seq]->idnumber = $rawmods[$seq]->idnumber;
dd97c328 454 $mod[$seq]->visible = $rawmods[$seq]->visible;
8341055e 455 $mod[$seq]->visibleoncoursepage = $rawmods[$seq]->visibleoncoursepage;
adaeccb6 456 $mod[$seq]->visibleold = $rawmods[$seq]->visibleold;
dd97c328 457 $mod[$seq]->groupmode = $rawmods[$seq]->groupmode;
458 $mod[$seq]->groupingid = $rawmods[$seq]->groupingid;
82bd6a5e 459 $mod[$seq]->indent = $rawmods[$seq]->indent;
460 $mod[$seq]->completion = $rawmods[$seq]->completion;
dd97c328 461 $mod[$seq]->extra = "";
adaeccb6 462 $mod[$seq]->completiongradeitemnumber =
463 $rawmods[$seq]->completiongradeitemnumber;
464 $mod[$seq]->completionview = $rawmods[$seq]->completionview;
465 $mod[$seq]->completionexpected = $rawmods[$seq]->completionexpected;
8c40662e 466 $mod[$seq]->showdescription = $rawmods[$seq]->showdescription;
8d1f33e1 467 $mod[$seq]->availability = $rawmods[$seq]->availability;
3869d774 468 $mod[$seq]->deletioninprogress = $rawmods[$seq]->deletioninprogress;
8dddba42 469
470 $modname = $mod[$seq]->mod;
471 $functionname = $modname."_get_coursemodule_info";
472
3a37b3f8 473 if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
474 continue;
475 }
476
8dddba42 477 include_once("$CFG->dirroot/mod/$modname/lib.php");
478
b3a89232 479 if ($hasfunction = function_exists($functionname)) {
9d361034 480 if ($info = $functionname($rawmods[$seq])) {
9d361034 481 if (!empty($info->icon)) {
482 $mod[$seq]->icon = $info->icon;
483 }
9a9012dc
PS
484 if (!empty($info->iconcomponent)) {
485 $mod[$seq]->iconcomponent = $info->iconcomponent;
486 }
1ea543df 487 if (!empty($info->name)) {
9a9012dc 488 $mod[$seq]->name = $info->name;
1ea543df 489 }
0d8b6a69 490 if ($info instanceof cached_cm_info) {
491 // When using cached_cm_info you can include three new fields
492 // that aren't available for legacy code
493 if (!empty($info->content)) {
494 $mod[$seq]->content = $info->content;
495 }
496 if (!empty($info->extraclasses)) {
497 $mod[$seq]->extraclasses = $info->extraclasses;
498 }
c443a1cd 499 if (!empty($info->iconurl)) {
4fcdb012
MG
500 // Convert URL to string as it's easier to store. Also serialized object contains \0 byte and can not be written to Postgres DB.
501 $url = new moodle_url($info->iconurl);
502 $mod[$seq]->iconurl = $url->out(false);
c443a1cd 503 }
0d8b6a69 504 if (!empty($info->onclick)) {
505 $mod[$seq]->onclick = $info->onclick;
506 }
507 if (!empty($info->customdata)) {
508 $mod[$seq]->customdata = $info->customdata;
509 }
510 } else {
511 // When using a stdclass, the (horrible) deprecated ->extra field
512 // is available for BC
513 if (!empty($info->extra)) {
514 $mod[$seq]->extra = $info->extra;
515 }
516 }
c9f6251e 517 }
518 }
b3a89232 519 // When there is no modname_get_coursemodule_info function,
520 // but showdescriptions is enabled, then we use the 'intro'
521 // and 'introformat' fields in the module table
522 if (!$hasfunction && $rawmods[$seq]->showdescription) {
523 if ($modvalues = $DB->get_record($rawmods[$seq]->modname,
524 array('id' => $rawmods[$seq]->instance), 'name, intro, introformat')) {
525 // Set content from intro and introformat. Filters are disabled
526 // because we filter it with format_text at display time
527 $mod[$seq]->content = format_module_intro($rawmods[$seq]->modname,
528 $modvalues, $rawmods[$seq]->id, false);
529
530 // To save making another query just below, put name in here
531 $mod[$seq]->name = $modvalues->name;
532 }
533 }
1ea543df 534 if (!isset($mod[$seq]->name)) {
9a9012dc 535 $mod[$seq]->name = $DB->get_field($rawmods[$seq]->modname, "name", array("id"=>$rawmods[$seq]->instance));
1ea543df 536 }
0d8b6a69 537
061e6b28 538 // Minimise the database size by unsetting default options when they are
539 // 'empty'. This list corresponds to code in the cm_info constructor.
540 foreach (array('idnumber', 'groupmode', 'groupingid',
541 'indent', 'completion', 'extra', 'extraclasses', 'iconurl', 'onclick', 'content',
542 'icon', 'iconcomponent', 'customdata', 'availability', 'completionview',
3869d774 543 'completionexpected', 'score', 'showdescription', 'deletioninprogress') as $property) {
0d8b6a69 544 if (property_exists($mod[$seq], $property) &&
545 empty($mod[$seq]->{$property})) {
546 unset($mod[$seq]->{$property});
547 }
548 }
adaeccb6 549 // Special case: this value is usually set to null, but may be 0
550 if (property_exists($mod[$seq], 'completiongradeitemnumber') &&
551 is_null($mod[$seq]->completiongradeitemnumber)) {
552 unset($mod[$seq]->completiongradeitemnumber);
553 }
d897cae4 554 }
555 }
556 }
557 }
558 return $mod;
559}
560
cb6fec1f 561/**
d57aa283
MG
562 * Returns the localised human-readable names of all used modules
563 *
564 * @param bool $plural if true returns the plural forms of the names
565 * @return array where key is the module name (component name without 'mod_') and
566 * the value is the human-readable string. Array sorted alphabetically by value
cb6fec1f 567 */
d57aa283
MG
568function get_module_types_names($plural = false) {
569 static $modnames = null;
570 global $DB, $CFG;
571 if ($modnames === null) {
572 $modnames = array(0 => array(), 1 => array());
573 if ($allmods = $DB->get_records("modules")) {
574 foreach ($allmods as $mod) {
575 if (file_exists("$CFG->dirroot/mod/$mod->name/lib.php") && $mod->visible) {
d91aa1e7
MJ
576 $modnames[0][$mod->name] = get_string("modulename", "$mod->name", null, true);
577 $modnames[1][$mod->name] = get_string("modulenameplural", "$mod->name", null, true);
d57aa283 578 }
13534ef7 579 }
c7da6f7a 580 }
7468bf01 581 }
d57aa283 582 return $modnames[(int)$plural];
7468bf01 583}
584
93d46f48
RK
585/**
586 * Set highlighted section. Only one section can be highlighted at the time.
587 *
588 * @param int $courseid course id
589 * @param int $marker highlight section with this number, 0 means remove higlightin
590 * @return void
591 */
592function course_set_marker($courseid, $marker) {
8341055e 593 global $DB, $COURSE;
93d46f48 594 $DB->set_field("course", "marker", $marker, array('id' => $courseid));
8341055e
MG
595 if ($COURSE && $COURSE->id == $courseid) {
596 $COURSE->marker = $marker;
597 }
21d93554 598 core_courseformat\base::reset_course_cache($courseid);
8341055e 599 course_modinfo::clear_instance_cache($courseid);
93d46f48
RK
600}
601
cb6fec1f 602/**
7e85563d 603 * For a given course section, marks it visible or hidden,
cb6fec1f 604 * and does the same for every activity in that section
ebaa29d1
ARN
605 *
606 * @param int $courseid course id
607 * @param int $sectionnumber The section number to adjust
608 * @param int $visibility The new visibility
609 * @return array A list of resources which were hidden in the section
cb6fec1f 610 */
7d99d695 611function set_section_visible($courseid, $sectionnumber, $visibility) {
cb6fec1f 612 global $DB;
7d99d695 613
ebaa29d1 614 $resourcestotoggle = array();
cb6fec1f 615 if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) {
f26481c2 616 course_update_section($courseid, $section, array('visible' => $visibility));
ebaa29d1
ARN
617
618 // Determine which modules are visible for AJAX update
f26481c2 619 $modules = !empty($section->sequence) ? explode(',', $section->sequence) : array();
ebaa29d1
ARN
620 if (!empty($modules)) {
621 list($insql, $params) = $DB->get_in_or_equal($modules);
622 $select = 'id ' . $insql . ' AND visible = ?';
623 array_push($params, $visibility);
624 if (!$visibility) {
625 $select .= ' AND visibleold = 1';
626 }
627 $resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params);
628 }
7d99d695 629 }
ebaa29d1 630 return $resourcestotoggle;
7d99d695 631}
ba2e5d73 632
8ed5dd63 633/**
634 * Return the course category context for the category with id $categoryid, except
635 * that if $categoryid is 0, return the system context.
636 *
637 * @param integer $categoryid a category id or 0.
5dc361e1 638 * @return context the corresponding context
8ed5dd63 639 */
640function get_category_or_system_context($categoryid) {
641 if ($categoryid) {
4658aec5 642 return context_coursecat::instance($categoryid, IGNORE_MISSING);
8ed5dd63 643 } else {
9a5e297b 644 return context_system::instance();
8ed5dd63 645 }
646}
647
77eddcd5 648/**
649 * Print the buttons relating to course requests.
650 *
3e15abe5 651 * @param context $context current page context.
77eddcd5 652 */
8e57a6df 653function print_course_request_buttons($context) {
b4531207 654 global $CFG, $DB, $OUTPUT;
77eddcd5 655 if (empty($CFG->enablecourserequests)) {
656 return;
657 }
3e15abe5
MG
658 if (course_request::can_request($context)) {
659 // Print a button to request a new course.
660 $params = [];
661 if ($context instanceof context_coursecat) {
662 $params['category'] = $context->instanceid;
663 }
664 echo $OUTPUT->single_button(new moodle_url('/course/request.php', $params),
665 get_string('requestcourse'), 'get');
77eddcd5 666 }
667 /// Print a button to manage pending requests
beff3806 668 if (has_capability('moodle/site:approvecourse', $context)) {
5c2ed7e2 669 $disabled = !$DB->record_exists('course_request', array());
8e57a6df 670 echo $OUTPUT->single_button(new moodle_url('/course/pending.php'), get_string('coursespending'), 'get', array('disabled' => $disabled));
77eddcd5 671 }
672}
673
5048e034 674/**
675 * Does the user have permission to edit things in this category?
676 *
677 * @param integer $categoryid The id of the category we are showing, or 0 for system context.
678 * @return boolean has_any_capability(array(...), ...); in the appropriate context.
679 */
680function can_edit_in_category($categoryid = 0) {
681 $context = get_category_or_system_context($categoryid);
682 return has_any_capability(array('moodle/category:manage', 'moodle/course:create'), $context);
683}
684
11b0c469 685/// MODULE FUNCTIONS /////////////////////////////////////////////////////////////////
686
687function add_course_module($mod) {
cb6fec1f 688 global $DB;
11b0c469 689
e5dfd0f3 690 $mod->added = time();
53f4ad2c 691 unset($mod->id);
11b0c469 692
38b19bbc
MG
693 $cmid = $DB->insert_record("course_modules", $mod);
694 rebuild_course_cache($mod->course, true);
695 return $cmid;
11b0c469 696}
697
89b909f6
MG
698/**
699 * Creates a course section and adds it to the specified position
700 *
701 * @param int|stdClass $courseorid course id or course object
702 * @param int $position position to add to, 0 means to the end. If position is greater than
703 * number of existing secitons, the section is added to the end. This will become sectionnum of the
704 * new section. All existing sections at this or bigger position will be shifted down.
705 * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist
706 * @return stdClass created section object
707 */
708function course_create_section($courseorid, $position = 0, $skipcheck = false) {
709 global $DB;
710 $courseid = is_object($courseorid) ? $courseorid->id : $courseorid;
711
712 // Find the last sectionnum among existing sections.
713 if ($skipcheck) {
714 $lastsection = $position - 1;
715 } else {
716 $lastsection = (int)$DB->get_field_sql('SELECT max(section) from {course_sections} WHERE course = ?', [$courseid]);
717 }
718
719 // First add section to the end.
720 $cw = new stdClass();
721 $cw->course = $courseid;
722 $cw->section = $lastsection + 1;
723 $cw->summary = '';
724 $cw->summaryformat = FORMAT_HTML;
725 $cw->sequence = '';
726 $cw->name = null;
727 $cw->visible = 1;
728 $cw->availability = null;
4ddf7c60 729 $cw->timemodified = time();
89b909f6
MG
730 $cw->id = $DB->insert_record("course_sections", $cw);
731
732 // Now move it to the specified position.
733 if ($position > 0 && $position <= $lastsection) {
734 $course = is_object($courseorid) ? $courseorid : get_course($courseorid);
735 move_section_to($course, $cw->section, $position, true);
736 $cw->section = $position;
737 }
738
739 core\event\course_section_created::create_from_section($cw)->trigger();
740
741 rebuild_course_cache($courseid, true);
742 return $cw;
743}
744
97928ddf 745/**
4ede27b2 746 * Creates missing course section(s) and rebuilds course cache
99e9f9a6 747 *
b46be6ad 748 * @param int|stdClass $courseorid course id or course object
4ede27b2
MG
749 * @param int|array $sections list of relative section numbers to create
750 * @return bool if there were any sections created
97928ddf 751 */
b46be6ad 752function course_create_sections_if_missing($courseorid, $sections) {
4ede27b2
MG
753 if (!is_array($sections)) {
754 $sections = array($sections);
755 }
b46be6ad 756 $existing = array_keys(get_fast_modinfo($courseorid)->get_section_info_all());
89b909f6
MG
757 if ($newsections = array_diff($sections, $existing)) {
758 foreach ($newsections as $sectionnum) {
759 course_create_section($courseorid, $sectionnum, true);
4ede27b2 760 }
89b909f6 761 return true;
4ede27b2 762 }
89b909f6 763 return false;
97928ddf 764}
52f14061 765
ece966f0 766/**
722e6ba9 767 * Adds an existing module to the section
ece966f0 768 *
722e6ba9
MG
769 * Updates both tables {course_sections} and {course_modules}
770 *
5523c344 771 * Note: This function does not use modinfo PROVIDED that the section you are
772 * adding the module to already exists. If the section does not exist, it will
773 * build modinfo if necessary and create the section.
774 *
722e6ba9 775 * @param int|stdClass $courseorid course id or course object
46453565 776 * @param int $cmid id of the module already existing in course_modules table
722e6ba9
MG
777 * @param int $sectionnum relative number of the section (field course_sections.section)
778 * If section does not exist it will be created
779 * @param int|stdClass $beforemod id or object with field id corresponding to the module
780 * before which the module needs to be included. Null for inserting in the
781 * end of the section
782 * @return int The course_sections ID where the module is inserted
ece966f0 783 */
46453565 784function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod = null) {
722e6ba9
MG
785 global $DB, $COURSE;
786 if (is_object($beforemod)) {
787 $beforemod = $beforemod->id;
788 }
46453565
PS
789 if (is_object($courseorid)) {
790 $courseid = $courseorid->id;
791 } else {
792 $courseid = $courseorid;
793 }
46453565 794 // Do not try to use modinfo here, there is no guarantee it is valid!
5523c344 795 $section = $DB->get_record('course_sections',
796 array('course' => $courseid, 'section' => $sectionnum), '*', IGNORE_MISSING);
797 if (!$section) {
798 // This function call requires modinfo.
799 course_create_sections_if_missing($courseorid, $sectionnum);
800 $section = $DB->get_record('course_sections',
801 array('course' => $courseid, 'section' => $sectionnum), '*', MUST_EXIST);
802 }
803
722e6ba9 804 $modarray = explode(",", trim($section->sequence));
281d730e 805 if (empty($section->sequence)) {
46453565 806 $newsequence = "$cmid";
722e6ba9 807 } else if ($beforemod && ($key = array_keys($modarray, $beforemod))) {
46453565 808 $insertarray = array($cmid, $beforemod);
722e6ba9
MG
809 array_splice($modarray, $key[0], 1, $insertarray);
810 $newsequence = implode(",", $modarray);
811 } else {
46453565 812 $newsequence = "$section->sequence,$cmid";
722e6ba9
MG
813 }
814 $DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id));
46453565 815 $DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid));
b46be6ad
MG
816 if (is_object($courseorid)) {
817 rebuild_course_cache($courseorid->id, true);
818 } else {
819 rebuild_course_cache($courseorid, true);
820 }
722e6ba9 821 return $section->id; // Return course_sections ID that was used.
11b0c469 822}
823
9e533215
SL
824/**
825 * Change the group mode of a course module.
826 *
827 * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
828 * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
829 *
830 * @param int $id course module ID.
831 * @param int $groupmode the new groupmode value.
832 * @return bool True if the $groupmode was updated.
833 */
48e535bc 834function set_coursemodule_groupmode($id, $groupmode) {
cb6fec1f 835 global $DB;
38b19bbc
MG
836 $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST);
837 if ($cm->groupmode != $groupmode) {
838 $DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id));
839 rebuild_course_cache($cm->course, true);
840 }
841 return ($cm->groupmode != $groupmode);
3d575e6f 842}
843
177d4abf 844function set_coursemodule_idnumber($id, $idnumber) {
cb6fec1f 845 global $DB;
38b19bbc
MG
846 $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST);
847 if ($cm->idnumber != $idnumber) {
848 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
849 rebuild_course_cache($cm->course, true);
850 }
851 return ($cm->idnumber != $idnumber);
177d4abf 852}
4e781c7b 853
02f66c42 854/**
00a34234
FM
855 * Set the visibility of a module and inherent properties.
856 *
9e533215
SL
857 * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
858 * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
859 *
00a34234
FM
860 * From 2.4 the parameter $prevstateoverrides has been removed, the logic it triggered
861 * has been moved to {@link set_section_visible()} which was the only place from which
862 * the parameter was used.
863 *
864 * @param int $id of the module
865 * @param int $visible state of the module
8341055e 866 * @param int $visibleoncoursepage state of the module on the course page
00a34234
FM
867 * @return bool false when the module was not found, true otherwise
868 */
8341055e 869function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) {
f5e2602a 870 global $DB, $CFG;
0f078024 871 require_once($CFG->libdir.'/gradelib.php');
c5ceaeaf 872 require_once($CFG->dirroot.'/calendar/lib.php');
0f078024 873
cb6fec1f 874 if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) {
978abb42 875 return false;
876 }
40fcc261
AD
877
878 // Create events and propagate visibility to associated grade items if the value has changed.
879 // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades.
8341055e 880 if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) {
40fcc261
AD
881 return true;
882 }
883
cb6fec1f 884 if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) {
978abb42 885 return false;
886 }
8341055e
MG
887 if (($cm->visible != $visible) &&
888 ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) {
dcd338ff 889 foreach($events as $event) {
48e535bc 890 if ($visible) {
379cd2d4
EL
891 $event = new calendar_event($event);
892 $event->toggle_visibility(true);
48e535bc 893 } else {
379cd2d4
EL
894 $event = new calendar_event($event);
895 $event->toggle_visibility(false);
48e535bc 896 }
dcd338ff 897 }
898 }
f5e2602a 899
00a34234
FM
900 // Updating visible and visibleold to keep them in sync. Only changing a section visibility will
901 // affect visibleold to allow for an original visibility restore. See set_section_visible().
902 $cminfo = new stdClass();
903 $cminfo->id = $id;
904 $cminfo->visible = $visible;
8341055e 905 $cminfo->visibleoncoursepage = $visibleoncoursepage;
00a34234
FM
906 $cminfo->visibleold = $visible;
907 $DB->update_record('course_modules', $cminfo);
908
1c73df9e
TH
909 // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there.
910 // Note that this must be done after updating the row in course_modules, in case
911 // the modules grade_item_update function needs to access $cm->visible.
8341055e
MG
912 if ($cm->visible != $visible &&
913 plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) &&
1c73df9e
TH
914 component_callback_exists('mod_' . $modulename, 'grade_item_update')) {
915 $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
916 component_callback('mod_' . $modulename, 'grade_item_update', array($instance));
8341055e 917 } else if ($cm->visible != $visible) {
1c73df9e
TH
918 $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course));
919 if ($grade_items) {
920 foreach ($grade_items as $grade_item) {
921 $grade_item->set_hidden(!$visible);
922 }
923 }
924 }
925
38b19bbc
MG
926 rebuild_course_cache($cm->course, true);
927 return true;
1acfbce5 928}
929
f59f89b4
MG
930/**
931 * Changes the course module name
932 *
933 * @param int $id course module id
934 * @param string $name new value for a name
935 * @return bool whether a change was made
936 */
937function set_coursemodule_name($id, $name) {
938 global $CFG, $DB;
939 require_once($CFG->libdir . '/gradelib.php');
940
941 $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST);
942
943 $module = new \stdClass();
944 $module->id = $cm->instance;
945
946 // Escape strings as they would be by mform.
947 if (!empty($CFG->formatstringstriptags)) {
948 $module->name = clean_param($name, PARAM_TEXT);
949 } else {
950 $module->name = clean_param($name, PARAM_CLEANHTML);
951 }
952 if ($module->name === $cm->name || strval($module->name) === '') {
953 return false;
954 }
955 if (\core_text::strlen($module->name) > 255) {
956 throw new \moodle_exception('maximumchars', 'moodle', '', 255);
957 }
958
959 $module->timemodified = time();
960 $DB->update_record($cm->modname, $module);
961 $cm->name = $module->name;
962 \core\event\course_module_updated::create_from_cm($cm)->trigger();
963 rebuild_course_cache($cm->course, true);
964
965 // Attempt to update the grade item if relevant.
966 $grademodule = $DB->get_record($cm->modname, array('id' => $cm->instance));
967 $grademodule->cmidnumber = $cm->idnumber;
968 $grademodule->modname = $cm->modname;
969 grade_update_mod_grades($grademodule);
970
9851111f 971 // Update calendar events with the new name.
a85e191c 972 course_module_update_calendar_events($cm->modname, $grademodule, $cm);
9851111f 973
f59f89b4
MG
974 return true;
975}
976
cb6fec1f 977/**
7f7144fd 978 * This function will handle the whole deletion process of a module. This includes calling
a347aee3
MN
979 * the modules delete_instance function, deleting files, events, grades, conditional data,
980 * the data in the course_module and course_sections table and adding a module deletion
981 * event to the DB.
290130b3 982 *
a347aee3 983 * @param int $cmid the course module id
3869d774
JD
984 * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook.
985 * @throws moodle_exception
5bcfd504 986 * @since Moodle 2.5
290130b3 987 */
3869d774
JD
988function course_delete_module($cmid, $async = false) {
989 // Check the 'course_module_background_deletion_recommended' hook first.
990 // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
991 // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
992 // It's up to plugins to handle things like whether or not they are enabled.
993 if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
994 foreach ($pluginsfunction as $plugintype => $plugins) {
995 foreach ($plugins as $pluginfunction) {
996 if ($pluginfunction()) {
997 return course_module_flag_for_async_deletion($cmid);
998 }
999 }
1000 }
1001 }
1002
63deb5c3 1003 global $CFG, $DB;
a347aee3 1004
f615fbab 1005 require_once($CFG->libdir.'/gradelib.php');
7f7144fd 1006 require_once($CFG->libdir.'/questionlib.php');
cae83708 1007 require_once($CFG->dirroot.'/blog/lib.php');
c5ceaeaf 1008 require_once($CFG->dirroot.'/calendar/lib.php');
f615fbab 1009
a347aee3
MN
1010 // Get the course module.
1011 if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
290130b3 1012 return true;
1013 }
a347aee3
MN
1014
1015 // Get the module context.
1016 $modcontext = context_module::instance($cm->id);
1017
1018 // Get the course module name.
1019 $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
1020
1021 // Get the file location of the delete_instance function for this module.
1022 $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
1023
1024 // Include the file required to call the delete_instance function for this module.
1025 if (file_exists($modlib)) {
1026 require_once($modlib);
1027 } else {
9a9cb741
MN
1028 throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null,
1029 "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
a347aee3
MN
1030 }
1031
9a9cb741 1032 $deleteinstancefunction = $modulename . '_delete_instance';
a347aee3 1033
9a9cb741
MN
1034 // Ensure the delete_instance function exists for this module.
1035 if (!function_exists($deleteinstancefunction)) {
1036 throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
1037 "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
1038 }
1039
ea88fd4e
MN
1040 // Allow plugins to use this course module before we completely delete it.
1041 if ($pluginsfunction = get_plugins_with_function('pre_course_module_delete')) {
1042 foreach ($pluginsfunction as $plugintype => $plugins) {
1043 foreach ($plugins as $pluginfunction) {
1044 $pluginfunction($cm);
1045 }
1046 }
1047 }
1048
4a45b711 1049 question_delete_activity($cm);
7f7144fd 1050
9a9cb741 1051 // Call the delete_instance function, if it returns false throw an exception.
a347aee3 1052 if (!$deleteinstancefunction($cm->instance)) {
9a9cb741
MN
1053 throw new moodle_exception('cannotdeletemoduleinstance', '', '', null,
1054 "Cannot delete the module $modulename (instance).");
a347aee3
MN
1055 }
1056
1057 // Remove all module files in case modules forget to do that.
1058 $fs = get_file_storage();
1059 $fs->delete_area_files($modcontext->id);
1060
1061 // Delete events from calendar.
1062 if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) {
70b62308 1063 $coursecontext = context_course::instance($cm->course);
dcd338ff 1064 foreach($events as $event) {
70b62308
MG
1065 $event->context = $coursecontext;
1066 $calendarevent = calendar_event::load($event);
c5ceaeaf 1067 $calendarevent->delete();
dcd338ff 1068 }
1069 }
a347aee3
MN
1070
1071 // Delete grade items, outcome items and grades attached to modules.
1072 if ($grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $modulename,
1073 'iteminstance' => $cm->instance, 'courseid' => $cm->course))) {
f615fbab 1074 foreach ($grade_items as $grade_item) {
1075 $grade_item->delete('moddelete');
1076 }
f615fbab 1077 }
a347aee3 1078
c69a43f2
MG
1079 // Delete associated blogs and blog tag instances.
1080 blog_remove_associations_for_module($modcontext->id);
1081
46e12372
SM
1082 // Delete completion and availability data; it is better to do this even if the
1083 // features are not turned on, in case they were turned on previously (these will be
a347aee3 1084 // very quick on an empty table).
46e12372 1085 $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id));
ede323e2 1086 $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id,
e9f7c4a4 1087 'course' => $cm->course,
ede323e2 1088 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY));
06b3a6b2 1089
d9994071 1090 // Delete all tag instances associated with the instance of this module.
74fa9f76 1091 core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id);
dffcf46f 1092 core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
cc033d48 1093
56537316
FM
1094 // Notify the competency subsystem.
1095 \core_competency\api::hook_course_module_deleted($cm);
1096
a347aee3 1097 // Delete the context.
c592eea2 1098 context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
a347aee3
MN
1099
1100 // Delete the module from the course_modules table.
1101 $DB->delete_records('course_modules', array('id' => $cm->id));
1102
1103 // Delete module from that section.
1104 if (!delete_mod_from_section($cm->id, $cm->section)) {
9a9cb741
MN
1105 throw new moodle_exception('cannotdeletemodulefromsection', '', '', null,
1106 "Cannot delete the module $modulename (instance) from section.");
a347aee3
MN
1107 }
1108
63deb5c3
AA
1109 // Trigger event for course module delete action.
1110 $event = \core\event\course_module_deleted::create(array(
1111 'courseid' => $cm->course,
1112 'context' => $modcontext,
1113 'objectid' => $cm->id,
1114 'other' => array(
1115 'modulename' => $modulename,
1116 'instanceid' => $cm->instance,
1117 )
1118 ));
1119 $event->add_record_snapshot('course_modules', $cm);
1120 $event->trigger();
38b19bbc 1121 rebuild_course_cache($cm->course, true);
11b0c469 1122}
1123
3869d774
JD
1124/**
1125 * Schedule a course module for deletion in the background using an adhoc task.
1126 *
1127 * This method should not be called directly. Instead, please use course_delete_module($cmid, true), to denote async deletion.
1128 * The real deletion of the module is handled by the task, which calls 'course_delete_module($cmid)'.
1129 *
1130 * @param int $cmid the course module id.
1131 * @return bool whether the module was successfully scheduled for deletion.
1132 * @throws \moodle_exception
1133 */
1134function course_module_flag_for_async_deletion($cmid) {
44eb1490 1135 global $CFG, $DB, $USER;
3869d774
JD
1136 require_once($CFG->libdir.'/gradelib.php');
1137 require_once($CFG->libdir.'/questionlib.php');
1138 require_once($CFG->dirroot.'/blog/lib.php');
1139 require_once($CFG->dirroot.'/calendar/lib.php');
1140
1141 // Get the course module.
1142 if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
1143 return true;
1144 }
1145
1146 // We need to be reasonably certain the deletion is going to succeed before we background the process.
1147 // Make the necessary delete_instance checks, etc. before proceeding further. Throw exceptions if required.
1148
1149 // Get the course module name.
1150 $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
1151
1152 // Get the file location of the delete_instance function for this module.
1153 $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
1154
1155 // Include the file required to call the delete_instance function for this module.
1156 if (file_exists($modlib)) {
1157 require_once($modlib);
1158 } else {
1159 throw new \moodle_exception('cannotdeletemodulemissinglib', '', '', null,
1160 "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
1161 }
1162
1163 $deleteinstancefunction = $modulename . '_delete_instance';
1164
1165 // Ensure the delete_instance function exists for this module.
1166 if (!function_exists($deleteinstancefunction)) {
1167 throw new \moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
1168 "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
1169 }
1170
1171 // We are going to defer the deletion as we can't be sure how long the module's pre_delete code will run for.
1172 $cm->deletioninprogress = '1';
1173 $DB->update_record('course_modules', $cm);
1174
1175 // Create an adhoc task for the deletion of the course module. The task takes an array of course modules for removal.
1176 $removaltask = new \core_course\task\course_delete_modules();
44eb1490
DM
1177 $removaltask->set_custom_data(array(
1178 'cms' => array($cm),
1179 'userid' => $USER->id,
1180 'realuserid' => \core\session\manager::get_realuser()->id
1181 ));
3869d774
JD
1182
1183 // Queue the task for the next run.
1184 \core\task\manager::queue_adhoc_task($removaltask);
1185
1186 // Reset the course cache to hide the module.
1187 rebuild_course_cache($cm->course, true);
1188}
1189
1190/**
1191 * Checks whether the given course has any course modules scheduled for adhoc deletion.
1192 *
1193 * @param int $courseid the id of the course.
36272110 1194 * @param bool $onlygradable whether to check only gradable modules or all modules.
3869d774
JD
1195 * @return bool true if the course contains any modules pending deletion, false otherwise.
1196 */
36272110 1197function course_modules_pending_deletion(int $courseid, bool $onlygradable = false) : bool {
3869d774
JD
1198 if (empty($courseid)) {
1199 return false;
1200 }
36272110
SR
1201
1202 if ($onlygradable) {
1203 // Fetch modules with grade items.
1204 if (!$coursegradeitems = grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid])) {
1205 // Return early when there is none.
1206 return false;
1207 }
1208 }
1209
3869d774
JD
1210 $modinfo = get_fast_modinfo($courseid);
1211 foreach ($modinfo->get_cms() as $module) {
1212 if ($module->deletioninprogress == '1') {
36272110
SR
1213 if ($onlygradable) {
1214 // Check if the module being deleted is in the list of course modules with grade items.
1215 foreach ($coursegradeitems as $coursegradeitem) {
1216 if ($coursegradeitem->itemmodule == $module->modname && $coursegradeitem->iteminstance == $module->instance) {
1217 // The module being deleted is within the gradable modules.
1218 return true;
1219 }
1220 }
1221 } else {
1222 return true;
1223 }
3869d774
JD
1224 }
1225 }
1226 return false;
1227}
1228
1229/**
1230 * Checks whether the course module, as defined by modulename and instanceid, is scheduled for deletion within the given course.
1231 *
1232 * @param int $courseid the course id.
1233 * @param string $modulename the module name. E.g. 'assign', 'book', etc.
1234 * @param int $instanceid the module instance id.
1235 * @return bool true if the course module is pending deletion, false otherwise.
1236 */
1237function course_module_instance_pending_deletion($courseid, $modulename, $instanceid) {
1238 if (empty($courseid) || empty($modulename) || empty($instanceid)) {
1239 return false;
1240 }
1241 $modinfo = get_fast_modinfo($courseid);
1242 $instances = $modinfo->get_instances_of($modulename);
1243 return isset($instances[$instanceid]) && $instances[$instanceid]->deletioninprogress;
1244}
1245
722e6ba9 1246function delete_mod_from_section($modid, $sectionid) {
cb6fec1f 1247 global $DB;
11b0c469 1248
722e6ba9 1249 if ($section = $DB->get_record("course_sections", array("id"=>$sectionid)) ) {
11b0c469 1250
e5dfd0f3 1251 $modarray = explode(",", $section->sequence);
11b0c469 1252
722e6ba9 1253 if ($key = array_keys ($modarray, $modid)) {
11b0c469 1254 array_splice($modarray, $key[0], 1);
1255 $newsequence = implode(",", $modarray);
38b19bbc
MG
1256 $DB->set_field("course_sections", "sequence", $newsequence, array("id"=>$section->id));
1257 rebuild_course_cache($section->course, true);
1258 return true;
11b0c469 1259 } else {
1260 return false;
1261 }
89adb174 1262
11b0c469 1263 }
7977cffd 1264 return false;
11b0c469 1265}
1266
e3658a6a
AG
1267/**
1268 * This function updates the calendar events from the information stored in the module table and the course
1269 * module table.
1270 *
1271 * @param string $modulename Module name
1272 * @param stdClass $instance Module object. Either the $instance or the $cm must be supplied.
1273 * @param stdClass $cm Course module object. Either the $instance or the $cm must be supplied.
1274 * @return bool Returns true if calendar events are updated.
1275 * @since Moodle 3.3.4
1276 */
1277function course_module_update_calendar_events($modulename, $instance = null, $cm = null) {
1278 global $DB;
1279
1280 if (isset($instance) || isset($cm)) {
1281
1282 if (!isset($instance)) {
1283 $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
1284 }
1285 if (!isset($cm)) {
1286 $cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course);
1287 }
a66de3ca
DW
1288 if (!empty($cm)) {
1289 course_module_calendar_event_update_process($instance, $cm);
1290 }
e3658a6a
AG
1291 return true;
1292 }
1293 return false;
1294}
1295
1296/**
1297 * Update all instances through out the site or in a course.
1298 *
1299 * @param string $modulename Module type to update.
1300 * @param integer $courseid Course id to update events. 0 for the whole site.
1301 * @return bool Returns True if the update was successful.
1302 * @since Moodle 3.3.4
1303 */
1304function course_module_bulk_update_calendar_events($modulename, $courseid = 0) {
1305 global $DB;
1306
1307 $instances = null;
1308 if ($courseid) {
1309 if (!$instances = $DB->get_records($modulename, array('course' => $courseid))) {
1310 return false;
1311 }
1312 } else {
1313 if (!$instances = $DB->get_records($modulename)) {
1314 return false;
1315 }
1316 }
1317
1318 foreach ($instances as $instance) {
a66de3ca
DW
1319 if ($cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course)) {
1320 course_module_calendar_event_update_process($instance, $cm);
1321 }
e3658a6a
AG
1322 }
1323 return true;
1324}
1325
1326/**
1327 * Calendar events for a module instance are updated.
1328 *
1329 * @param stdClass $instance Module instance object.
1330 * @param stdClass $cm Course Module object.
1331 * @since Moodle 3.3.4
1332 */
1333function course_module_calendar_event_update_process($instance, $cm) {
1334 // We need to call *_refresh_events() first because some modules delete 'old' events at the end of the code which
1335 // will remove the completion events.
1336 $refresheventsfunction = $cm->modname . '_refresh_events';
1337 if (function_exists($refresheventsfunction)) {
1338 call_user_func($refresheventsfunction, $cm->course, $instance, $cm);
1339 }
1340 $completionexpected = (!empty($cm->completionexpected)) ? $cm->completionexpected : null;
1341 \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $instance, $completionexpected);
1342}
1343
3440ec12 1344/**
1345 * Moves a section within a course, from a position to another.
1346 * Be very careful: $section and $destination refer to section number,
1347 * not id!.
1348 *
1349 * @param object $course
1350 * @param int $section Section number (not id!!!)
1351 * @param int $destination
ca9cae84 1352 * @param bool $ignorenumsections
3440ec12 1353 * @return boolean Result
1354 */
ca9cae84 1355function move_section_to($course, $section, $destination, $ignorenumsections = false) {
3440ec12 1356/// Moves a whole course section up and down within the course
1357 global $USER, $DB;
1358
ca255392 1359 if (!$destination && $destination != 0) {
3440ec12 1360 return true;
1361 }
1362
850acb35
MG
1363 // compartibility with course formats using field 'numsections'
1364 $courseformatoptions = course_get_format($course)->get_format_options();
ca9cae84 1365 if ((!$ignorenumsections && array_key_exists('numsections', $courseformatoptions) &&
850acb35 1366 ($destination > $courseformatoptions['numsections'])) || ($destination < 1)) {
3440ec12 1367 return false;
1368 }
1369
1370 // Get all sections for this course and re-order them (2 of them should now share the same section number)
1371 if (!$sections = $DB->get_records_menu('course_sections', array('course' => $course->id),
1372 'section ASC, id ASC', 'id, section')) {
1373 return false;
1374 }
1375
cf76b335 1376 $movedsections = reorder_sections($sections, $section, $destination);
3440ec12 1377
cf76b335 1378 // Update all sections. Do this in 2 steps to avoid breaking database
1379 // uniqueness constraint
1380 $transaction = $DB->start_delegated_transaction();
1381 foreach ($movedsections as $id => $position) {
1382 if ($sections[$id] !== $position) {
1383 $DB->set_field('course_sections', 'section', -$position, array('id' => $id));
1384 }
1385 }
e7e0f8d2
DP
1386 foreach ($movedsections as $id => $position) {
1387 if ($sections[$id] !== $position) {
1388 $DB->set_field('course_sections', 'section', $position, array('id' => $id));
1389 }
3440ec12 1390 }
1391
15e2552f
RK
1392 // If we move the highlighted section itself, then just highlight the destination.
1393 // Adjust the higlighted section location if we move something over it either direction.
1394 if ($section == $course->marker) {
e7b6e6b9 1395 course_set_marker($course->id, $destination);
2365213f 1396 } elseif ($section > $course->marker && $course->marker >= $destination) {
e7b6e6b9 1397 course_set_marker($course->id, $course->marker+1);
2365213f 1398 } elseif ($section < $course->marker && $course->marker <= $destination) {
e7b6e6b9 1399 course_set_marker($course->id, $course->marker-1);
15e2552f
RK
1400 }
1401
cf76b335 1402 $transaction->allow_commit();
38b19bbc 1403 rebuild_course_cache($course->id, true);
3440ec12 1404 return true;
1405}
1406
ca9cae84
MG
1407/**
1408 * This method will delete a course section and may delete all modules inside it.
1409 *
1410 * No permissions are checked here, use {@link course_can_delete_section()} to
1411 * check if section can actually be deleted.
1412 *
1413 * @param int|stdClass $course
1414 * @param int|stdClass|section_info $section
1415 * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it.
3869d774 1416 * @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook.
ca9cae84
MG
1417 * @return bool whether section was deleted
1418 */
3869d774 1419function course_delete_section($course, $section, $forcedeleteifnotempty = true, $async = false) {
fa29c0c3
RK
1420 global $DB;
1421
1422 // Prepare variables.
1423 $courseid = (is_object($course)) ? $course->id : (int)$course;
1424 $sectionnum = (is_object($section)) ? $section->section : (int)$section;
1425 $section = $DB->get_record('course_sections', array('course' => $courseid, 'section' => $sectionnum));
1426 if (!$section) {
1427 // No section exists, can't proceed.
1428 return false;
1429 }
3869d774
JD
1430
1431 // Check the 'course_module_background_deletion_recommended' hook first.
1432 // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
1433 // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
1434 // It's up to plugins to handle things like whether or not they are enabled.
1435 if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
1436 foreach ($pluginsfunction as $plugintype => $plugins) {
1437 foreach ($plugins as $pluginfunction) {
1438 if ($pluginfunction()) {
1439 return course_delete_section_async($section, $forcedeleteifnotempty);
1440 }
1441 }
1442 }
1443 }
1444
fa29c0c3
RK
1445 $format = course_get_format($course);
1446 $sectionname = $format->get_section_name($section);
1447
1448 // Delete section.
1449 $result = $format->delete_section($section, $forcedeleteifnotempty);
1450
1451 // Trigger an event for course section deletion.
1452 if ($result) {
1453 $context = context_course::instance($courseid);
1454 $event = \core\event\course_section_deleted::create(
3869d774
JD
1455 array(
1456 'objectid' => $section->id,
1457 'courseid' => $courseid,
1458 'context' => $context,
1459 'other' => array(
1460 'sectionnum' => $section->section,
1461 'sectionname' => $sectionname,
fa29c0c3 1462 )
3869d774
JD
1463 )
1464 );
fa29c0c3
RK
1465 $event->add_record_snapshot('course_sections', $section);
1466 $event->trigger();
1467 }
1468 return $result;
ca9cae84
MG
1469}
1470
3869d774
JD
1471/**
1472 * Course section deletion, using an adhoc task for deletion of the modules it contains.
1473 * 1. Schedule all modules within the section for adhoc removal.
1474 * 2. Move all modules to course section 0.
1475 * 3. Delete the resulting empty section.
1476 *
1477 * @param \stdClass $section the section to schedule for deletion.
1478 * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules.
1479 * @return bool true if the section was scheduled for deletion, false otherwise.
1480 */
1481function course_delete_section_async($section, $forcedeleteifnotempty = true) {
44eb1490 1482 global $DB, $USER;
3869d774
JD
1483
1484 // Objects only, and only valid ones.
1485 if (!is_object($section) || empty($section->id)) {
1486 return false;
1487 }
1488
1489 // Does the object currently exist in the DB for removal (check for stale objects).
1490 $section = $DB->get_record('course_sections', array('id' => $section->id));
1491 if (!$section || !$section->section) {
1492 // No section exists, or the section is 0. Can't proceed.
1493 return false;
1494 }
1495
1496 // Check whether the section can be removed.
1497 if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) {
1498 return false;
1499 }
1500
1501 $format = course_get_format($section->course);
1502 $sectionname = $format->get_section_name($section);
1503
1504 // Flag those modules having no existing deletion flag. Some modules may have been scheduled for deletion manually, and we don't
1505 // want to create additional adhoc deletion tasks for these. Moving them to section 0 will suffice.
1506 $affectedmods = $DB->get_records_select('course_modules', 'course = ? AND section = ? AND deletioninprogress <> ?',
1507 [$section->course, $section->id, 1], '', 'id');
1508 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $section->course, 'section' => $section->id]);
1509
1510 // Move all modules to section 0.
1511 $modules = $DB->get_records('course_modules', ['section' => $section->id], '');
1512 $sectionzero = $DB->get_record('course_sections', ['course' => $section->course, 'section' => '0']);
1513 foreach ($modules as $mod) {
1514 moveto_module($mod, $sectionzero);
1515 }
1516
1517 // Create and queue an adhoc task for the deletion of the modules.
1518 $removaltask = new \core_course\task\course_delete_modules();
1519 $data = array(
44eb1490
DM
1520 'cms' => $affectedmods,
1521 'userid' => $USER->id,
1522 'realuserid' => \core\session\manager::get_realuser()->id
3869d774
JD
1523 );
1524 $removaltask->set_custom_data($data);
1525 \core\task\manager::queue_adhoc_task($removaltask);
1526
1527 // Delete the now empty section, passing in only the section number, which forces the function to fetch a new object.
1528 // The refresh is needed because the section->sequence is now stale.
1529 $result = $format->delete_section($section->section, $forcedeleteifnotempty);
1530
1531 // Trigger an event for course section deletion.
1532 if ($result) {
1533 $context = \context_course::instance($section->course);
1534 $event = \core\event\course_section_deleted::create(
1535 array(
1536 'objectid' => $section->id,
1537 'courseid' => $section->course,
1538 'context' => $context,
1539 'other' => array(
1540 'sectionnum' => $section->section,
1541 'sectionname' => $sectionname,
1542 )
1543 )
1544 );
1545 $event->add_record_snapshot('course_sections', $section);
1546 $event->trigger();
1547 }
1548 rebuild_course_cache($section->course, true);
1549
1550 return $result;
1551}
1552
f26481c2
MG
1553/**
1554 * Updates the course section
1555 *
1556 * This function does not check permissions or clean values - this has to be done prior to calling it.
1557 *
1558 * @param int|stdClass $course
1559 * @param stdClass $section record from course_sections table - it will be updated with the new values
1560 * @param array|stdClass $data
1561 */
1562function course_update_section($course, $section, $data) {
1563 global $DB;
1564
1565 $courseid = (is_object($course)) ? $course->id : (int)$course;
1566
1567 // Some fields can not be updated using this method.
1568 $data = array_diff_key((array)$data, array('id', 'course', 'section', 'sequence'));
1569 $changevisibility = (array_key_exists('visible', $data) && (bool)$data['visible'] != (bool)$section->visible);
1570 if (array_key_exists('name', $data) && \core_text::strlen($data['name']) > 255) {
1571 throw new moodle_exception('maximumchars', 'moodle', '', 255);
1572 }
1573
1574 // Update record in the DB and course format options.
1575 $data['id'] = $section->id;
4ddf7c60 1576 $data['timemodified'] = time();
f26481c2
MG
1577 $DB->update_record('course_sections', $data);
1578 rebuild_course_cache($courseid, true);
1579 course_get_format($courseid)->update_section_format_options($data);
1580
1581 // Update fields of the $section object.
1582 foreach ($data as $key => $value) {
1583 if (property_exists($section, $key)) {
1584 $section->$key = $value;
1585 }
1586 }
1587
1588 // Trigger an event for course section update.
1589 $event = \core\event\course_section_updated::create(
1590 array(
1591 'objectid' => $section->id,
1592 'courseid' => $courseid,
1593 'context' => context_course::instance($courseid),
1594 'other' => array('sectionnum' => $section->section)
1595 )
1596 );
1597 $event->trigger();
1598
1599 // If section visibility was changed, hide the modules in this section too.
1600 if ($changevisibility && !empty($section->sequence)) {
1601 $modules = explode(',', $section->sequence);
1602 foreach ($modules as $moduleid) {
1603 if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) {
1604 if ($data['visible']) {
1605 // As we unhide the section, we use the previously saved visibility stored in visibleold.
8341055e 1606 set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage);
f26481c2
MG
1607 } else {
1608 // We hide the section, so we hide the module but we store the original state in visibleold.
8341055e 1609 set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage);
f26481c2
MG
1610 $DB->set_field('course_modules', 'visibleold', $cm->visible, array('id' => $moduleid));
1611 }
1612 \core\event\course_module_updated::create_from_cm($cm)->trigger();
1613 }
1614 }
1615 }
1616}
1617
ca9cae84
MG
1618/**
1619 * Checks if the current user can delete a section (if course format allows it and user has proper permissions).
1620 *
1621 * @param int|stdClass $course
1622 * @param int|stdClass|section_info $section
1623 * @return bool
1624 */
1625function course_can_delete_section($course, $section) {
1626 if (is_object($section)) {
1627 $section = $section->section;
1628 }
1629 if (!$section) {
1630 // Not possible to delete 0-section.
1631 return false;
1632 }
1633 // Course format should allow to delete sections.
1634 if (!course_get_format($course)->can_delete_section($section)) {
1635 return false;
1636 }
1637 // Make sure user has capability to update course and move sections.
1638 $context = context_course::instance(is_object($course) ? $course->id : $course);
1639 if (!has_all_capabilities(array('moodle/course:movesections', 'moodle/course:update'), $context)) {
1640 return false;
1641 }
1642 // Make sure user has capability to delete each activity in this section.
1643 $modinfo = get_fast_modinfo($course);
1644 if (!empty($modinfo->sections[$section])) {
1645 foreach ($modinfo->sections[$section] as $cmid) {
1646 if (!has_capability('moodle/course:manageactivities', context_module::instance($cmid))) {
1647 return false;
1648 }
1649 }
1650 }
1651 return true;
1652}
1653
3440ec12 1654/**
1655 * Reordering algorithm for course sections. Given an array of section->section indexed by section->id,
1656 * an original position number and a target position number, rebuilds the array so that the
1657 * move is made without any duplication of section positions.
1658 * Note: The target_position is the position AFTER WHICH the moved section will be inserted. If you want to
1659 * insert a section before the first one, you must give 0 as the target (section 0 can never be moved).
1660 *
1661 * @param array $sections
1662 * @param int $origin_position
1663 * @param int $target_position
1664 * @return array
1665 */
1666function reorder_sections($sections, $origin_position, $target_position) {
1667 if (!is_array($sections)) {
1668 return false;
1669 }
1670
1671 // We can't move section position 0
1672 if ($origin_position < 1) {
1673 echo "We can't move section position 0";
1674 return false;
1675 }
1676
1677 // Locate origin section in sections array
1678 if (!$origin_key = array_search($origin_position, $sections)) {
1679 echo "searched position not in sections array";
1680 return false; // searched position not in sections array
1681 }
1682
1683 // Extract origin section
1684 $origin_section = $sections[$origin_key];
1685 unset($sections[$origin_key]);
1686
1687 // Find offset of target position (stupid PHP's array_splice requires offset instead of key index!)
1688 $found = false;
1689 $append_array = array();
1690 foreach ($sections as $id => $position) {
1691 if ($found) {
1692 $append_array[$id] = $position;
1693 unset($sections[$id]);
1694 }
1695 if ($position == $target_position) {
eb01aa2c
RT
1696 if ($target_position < $origin_position) {
1697 $append_array[$id] = $position;
1698 unset($sections[$id]);
1699 }
3440ec12 1700 $found = true;
1701 }
1702 }
1703
1704 // Append moved section
1705 $sections[$origin_key] = $origin_section;
1706
1707 // Append rest of array (if applicable)
1708 if (!empty($append_array)) {
1709 foreach ($append_array as $id => $position) {
1710 $sections[$id] = $position;
1711 }
1712 }
1713
1714 // Renumber positions
1715 $position = 0;
1716 foreach ($sections as $id => $p) {
1717 $sections[$id] = $position;
1718 $position++;
1719 }
1720
1721 return $sections;
1722
1723}
1724
cb6fec1f 1725/**
1726 * Move the module object $mod to the specified $section
1727 * If $beforemod exists then that is the module
1728 * before which $modid should be inserted
d55f05ef
MG
1729 *
1730 * @param stdClass|cm_info $mod
1731 * @param stdClass|section_info $section
1732 * @param int|stdClass $beforemod id or object with field id corresponding to the module
1733 * before which the module needs to be included. Null for inserting in the
1734 * end of the section
1735 * @return int new value for module visibility (0 or 1)
cb6fec1f 1736 */
7977cffd 1737function moveto_module($mod, $section, $beforemod=NULL) {
a83dd077 1738 global $OUTPUT, $DB;
7977cffd 1739
d55f05ef
MG
1740 // Current module visibility state - return value of this function.
1741 $modvisible = $mod->visible;
1742
1743 // Remove original module from original section.
7977cffd 1744 if (! delete_mod_from_section($mod->id, $mod->section)) {
e6db3026 1745 echo $OUTPUT->notification("Could not delete module from existing section");
7977cffd 1746 }
1747
d55f05ef 1748 // If moving to a hidden section then hide module.
bb1592c8
CF
1749 if ($mod->section != $section->id) {
1750 if (!$section->visible && $mod->visible) {
d55f05ef
MG
1751 // Module was visible but must become hidden after moving to hidden section.
1752 $modvisible = 0;
bb1592c8
CF
1753 set_coursemodule_visible($mod->id, 0);
1754 // Set visibleold to 1 so module will be visible when section is made visible.
1755 $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id));
1756 }
1757 if ($section->visible && !$mod->visible) {
d55f05ef 1758 // Hidden module was moved to the visible section, restore the module visibility from visibleold.
bb1592c8 1759 set_coursemodule_visible($mod->id, $mod->visibleold);
d55f05ef 1760 $modvisible = $mod->visibleold;
bb1592c8 1761 }
7977cffd 1762 }
1763
d55f05ef 1764 // Add the module into the new section.
722e6ba9 1765 course_add_cm_to_section($section->course, $mod->id, $section->section, $beforemod);
d55f05ef 1766 return $modvisible;
7977cffd 1767}
1768
f558b291
MG
1769/**
1770 * Returns the list of all editing actions that current user can perform on the module
1771 *
1772 * @param cm_info $mod The module to produce editing buttons for
1773 * @param int $indent The current indenting (default -1 means no move left-right actions)
1774 * @param int $sr The section to link back to (used for creating the links)
1775 * @return array array of action_link or pix_icon objects
1776 */
1777function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) {
8341055e 1778 global $COURSE, $SITE, $CFG;
94361e02 1779
3d575e6f 1780 static $str;
1781
9a5e297b
AA
1782 $coursecontext = context_course::instance($mod->course);
1783 $modcontext = context_module::instance($mod->id);
8341055e 1784 $courseformat = course_get_format($mod->get_course());
7749e187 1785
af189935
PS
1786 $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign');
1787 $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport');
1788
cf69a00a 1789 // No permission to edit anything.
af189935 1790 if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) {
f558b291 1791 return array();
217a8ee9 1792 }
1793
af189935
PS
1794 $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
1795
3d575e6f 1796 if (!isset($str)) {
f558b291 1797 $str = get_strings(array('delete', 'move', 'moveright', 'moveleft',
125c4c4c 1798 'editsettings', 'duplicate', 'modhide', 'makeavailable', 'makeunavailable', 'modshow'), 'moodle');
f558b291 1799 $str->assign = get_string('assignroles', 'role');
6dc5908e
ARN
1800 $str->groupsnone = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsnone"));
1801 $str->groupsseparate = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsseparate"));
1802 $str->groupsvisible = get_string('clicktochangeinbrackets', 'moodle', get_string("groupsvisible"));
1acfbce5 1803 }
94361e02 1804
af189935 1805 $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
24e1eae4 1806
f558b291
MG
1807 if ($sr !== null) {
1808 $baseurl->param('sr', $sr);
7749e187
SH
1809 }
1810 $actions = array();
1811
a83d83e4
AN
1812 // Update.
1813 if ($hasmanageactivities) {
1814 $actions['update'] = new action_menu_link_secondary(
7a9a07d2 1815 new moodle_url($baseurl, array('update' => $mod->id)),
cce54c47 1816 new pix_icon('t/edit', '', 'moodle', array('class' => 'iconsmall')),
a83d83e4
AN
1817 $str->editsettings,
1818 array('class' => 'editing_update', 'data-action' => 'update')
7a9a07d2
ARN
1819 );
1820 }
1821
cf69a00a 1822 // Indent.
639d9904 1823 if ($hasmanageactivities && $indent >= 0) {
a83d83e4
AN
1824 $indentlimits = new stdClass();
1825 $indentlimits->min = 0;
1826 $indentlimits->max = 16;
7749e187
SH
1827 if (right_to_left()) { // Exchange arrows on RTL
1828 $rightarrow = 't/left';
1829 $leftarrow = 't/right';
1830 } else {
1831 $rightarrow = 't/right';
1832 $leftarrow = 't/left';
1833 }
1834
a83d83e4
AN
1835 if ($indent >= $indentlimits->max) {
1836 $enabledclass = 'hidden';
1837 } else {
1838 $enabledclass = '';
1839 }
1840 $actions['moveright'] = new action_menu_link_secondary(
1841 new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '1')),
cce54c47 1842 new pix_icon($rightarrow, '', 'moodle', array('class' => 'iconsmall')),
a83d83e4 1843 $str->moveright,
8341055e
MG
1844 array('class' => 'editing_moveright ' . $enabledclass, 'data-action' => 'moveright',
1845 'data-keepopen' => true, 'data-sectionreturn' => $sr)
a83d83e4
AN
1846 );
1847
1848 if ($indent <= $indentlimits->min) {
1849 $enabledclass = 'hidden';
1850 } else {
1851 $enabledclass = '';
7749e187 1852 }
3665af78 1853 $actions['moveleft'] = new action_menu_link_secondary(
e282c679 1854 new moodle_url($baseurl, array('id' => $mod->id, 'indent' => '-1')),
cce54c47 1855 new pix_icon($leftarrow, '', 'moodle', array('class' => 'iconsmall')),
e282c679 1856 $str->moveleft,
8341055e
MG
1857 array('class' => 'editing_moveleft ' . $enabledclass, 'data-action' => 'moveleft',
1858 'data-keepopen' => true, 'data-sectionreturn' => $sr)
e282c679 1859 );
a83d83e4
AN
1860
1861 }
1862
8341055e 1863 // Hide/Show/Available/Unavailable.
a83d83e4 1864 if (has_capability('moodle/course:activityvisibility', $modcontext)) {
8341055e
MG
1865 $allowstealth = !empty($CFG->allowstealth) && $courseformat->allow_stealth_module_visibility($mod, $mod->get_section_info());
1866
1867 $sectionvisible = $mod->get_section_info()->visible;
1868 // The module on the course page may be in one of the following states:
1869 // - Available and displayed on the course page ($displayedoncoursepage);
1870 // - Not available and not displayed on the course page ($unavailable);
1871 // - Available but not displayed on the course page ($stealth) - this can also be a visible activity in a hidden section.
1872 $displayedoncoursepage = $mod->visible && $mod->visibleoncoursepage && $sectionvisible;
1873 $unavailable = !$mod->visible;
1874 $stealth = $mod->visible && (!$mod->visibleoncoursepage || !$sectionvisible);
1875 if ($displayedoncoursepage) {
a83d83e4
AN
1876 $actions['hide'] = new action_menu_link_secondary(
1877 new moodle_url($baseurl, array('hide' => $mod->id)),
cce54c47 1878 new pix_icon('t/hide', '', 'moodle', array('class' => 'iconsmall')),
125c4c4c 1879 $str->modhide,
a83d83e4
AN
1880 array('class' => 'editing_hide', 'data-action' => 'hide')
1881 );
8341055e
MG
1882 } else if (!$displayedoncoursepage && $sectionvisible) {
1883 // Offer to "show" only if the section is visible.
a83d83e4
AN
1884 $actions['show'] = new action_menu_link_secondary(
1885 new moodle_url($baseurl, array('show' => $mod->id)),
cce54c47 1886 new pix_icon('t/show', '', 'moodle', array('class' => 'iconsmall')),
125c4c4c 1887 $str->modshow,
a83d83e4
AN
1888 array('class' => 'editing_show', 'data-action' => 'show')
1889 );
7749e187 1890 }
8341055e
MG
1891
1892 if ($stealth) {
1893 // When making the "stealth" module unavailable we perform the same action as hiding the visible module.
1894 $actions['hide'] = new action_menu_link_secondary(
1895 new moodle_url($baseurl, array('hide' => $mod->id)),
cce54c47 1896 new pix_icon('t/unblock', '', 'moodle', array('class' => 'iconsmall')),
8341055e
MG
1897 $str->makeunavailable,
1898 array('class' => 'editing_makeunavailable', 'data-action' => 'hide', 'data-sectionreturn' => $sr)
1899 );
1900 } else if ($unavailable && (!$sectionvisible || $allowstealth) && $mod->has_view()) {
1901 // Allow to make visually hidden module available in gradebook and other reports by making it a "stealth" module.
1902 // When the section is hidden it is an equivalent of "showing" the module.
1903 // Activities without the link (i.e. labels) can not be made available but hidden on course page.
1904 $action = $sectionvisible ? 'stealth' : 'show';
1905 $actions[$action] = new action_menu_link_secondary(
1906 new moodle_url($baseurl, array($action => $mod->id)),
cce54c47 1907 new pix_icon('t/block', '', 'moodle', array('class' => 'iconsmall')),
8341055e
MG
1908 $str->makeavailable,
1909 array('class' => 'editing_makeavailable', 'data-action' => $action, 'data-sectionreturn' => $sr)
1910 );
1911 }
dc0dc7d5 1912 }
7749e187 1913
8a1a951f 1914 // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php)
03c39daa 1915 if (has_all_capabilities($dupecaps, $coursecontext) &&
8341055e
MG
1916 plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) &&
1917 course_allowed_module($mod->get_course(), $mod->modname)) {
3665af78 1918 $actions['duplicate'] = new action_menu_link_secondary(
8645a28f 1919 new moodle_url($baseurl, array('duplicate' => $mod->id)),
cce54c47 1920 new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')),
e282c679 1921 $str->duplicate,
8341055e 1922 array('class' => 'editing_duplicate', 'data-action' => 'duplicate', 'data-sectionreturn' => $sr)
8645a28f
EL
1923 );
1924 }
7749e187 1925
cf69a00a 1926 // Groupmode.
9ac099a1 1927 if ($hasmanageactivities && !$mod->coursegroupmodeforce) {
ea191cf1 1928 if (plugin_supports('mod', $mod->modname, FEATURE_GROUPS, false)) {
9ac099a1
AN
1929 if ($mod->effectivegroupmode == SEPARATEGROUPS) {
1930 $nextgroupmode = VISIBLEGROUPS;
1931 $grouptitle = $str->groupsseparate;
1932 $actionname = 'groupsseparate';
8341055e 1933 $nextactionname = 'groupsvisible';
6272fce6 1934 $groupimage = 'i/groups';
9ac099a1
AN
1935 } else if ($mod->effectivegroupmode == VISIBLEGROUPS) {
1936 $nextgroupmode = NOGROUPS;
1937 $grouptitle = $str->groupsvisible;
1938 $actionname = 'groupsvisible';
8341055e 1939 $nextactionname = 'groupsnone';
6272fce6 1940 $groupimage = 'i/groupv';
9ac099a1
AN
1941 } else {
1942 $nextgroupmode = SEPARATEGROUPS;
1943 $grouptitle = $str->groupsnone;
1944 $actionname = 'groupsnone';
8341055e 1945 $nextactionname = 'groupsseparate';
6272fce6 1946 $groupimage = 'i/groupn';
9ac099a1
AN
1947 }
1948
1949 $actions[$actionname] = new action_menu_link_primary(
1950 new moodle_url($baseurl, array('id' => $mod->id, 'groupmode' => $nextgroupmode)),
cce54c47 1951 new pix_icon($groupimage, '', 'moodle', array('class' => 'iconsmall')),
9ac099a1 1952 $grouptitle,
8341055e
MG
1953 array('class' => 'editing_'. $actionname, 'data-action' => $nextactionname,
1954 'aria-live' => 'assertive', 'data-sectionreturn' => $sr)
9ac099a1 1955 );
32d03b7b 1956 } else {
9ac099a1 1957 $actions['nogroupsupport'] = new action_menu_filler();
32d03b7b 1958 }
7977cffd 1959 }
1960
cf69a00a 1961 // Assign.
af189935 1962 if (has_capability('moodle/role:assign', $modcontext)){
3665af78 1963 $actions['assign'] = new action_menu_link_secondary(
f558b291 1964 new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)),
cce54c47 1965 new pix_icon('t/assignroles', '', 'moodle', array('class' => 'iconsmall')),
e282c679 1966 $str->assign,
8341055e 1967 array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr)
7749e187
SH
1968 );
1969 }
1970
a83d83e4
AN
1971 // Delete.
1972 if ($hasmanageactivities) {
1973 $actions['delete'] = new action_menu_link_secondary(
1974 new moodle_url($baseurl, array('delete' => $mod->id)),
cce54c47 1975 new pix_icon('t/delete', '', 'moodle', array('class' => 'iconsmall')),
a83d83e4 1976 $str->delete,
8341055e 1977 array('class' => 'editing_delete', 'data-action' => 'delete', 'data-sectionreturn' => $sr)
a83d83e4
AN
1978 );
1979 }
1980
f558b291 1981 return $actions;
90845098 1982}
1983
4657ba81
AN
1984/**
1985 * Returns the move action.
1986 *
1987 * @param cm_info $mod The module to produce a move button for
1988 * @param int $sr The section to link back to (used for creating the links)
1989 * @return The markup for the move action, or an empty string if not available.
1990 */
1991function course_get_cm_move(cm_info $mod, $sr = null) {
1992 global $OUTPUT;
1993
1994 static $str;
1995 static $baseurl;
1996
1997 $modcontext = context_module::instance($mod->id);
1998 $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext);
1999
2000 if (!isset($str)) {
2001 $str = get_strings(array('move'));
2002 }
2003
2004 if (!isset($baseurl)) {
2005 $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey()));
2006
2007 if ($sr !== null) {
2008 $baseurl->param('sr', $sr);
2009 }
2010 }
2011
2012 if ($hasmanageactivities) {
2013 $pixicon = 'i/dragdrop';
2014
69353dce 2015 if (!course_ajax_enabled($mod->get_course())) {
4657ba81
AN
2016 // Override for course frontpage until we get drag/drop working there.
2017 $pixicon = 't/move';
2018 }
2019
2896cb83
SR
2020 $attributes = [
2021 'class' => 'editing_move',
2022 'data-action' => 'move',
2023 'data-sectionreturn' => $sr,
2024 'title' => $str->move,
2025 'aria-label' => $str->move,
2026 ];
4657ba81 2027 return html_writer::link(
2896cb83
SR
2028 new moodle_url($baseurl, ['copy' => $mod->id]),
2029 $OUTPUT->pix_icon($pixicon, '', 'moodle', ['class' => 'iconsmall']),
2030 $attributes
4657ba81
AN
2031 );
2032 }
2033 return '';
2034}
2035
b61efafb 2036/**
264867fd 2037 * given a course object with shortname & fullname, this function will
b61efafb 2038 * truncate the the number of chars allowed and add ... if it was too long
2039 */
2040function course_format_name ($course,$max=100) {
264867fd 2041
9a5e297b 2042 $context = context_course::instance($course->id);
8ebbb06a 2043 $shortname = format_string($course->shortname, true, array('context' => $context));
9a5e297b 2044 $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
d1e36240 2045 $str = $shortname.': '. $fullname;
2f1e464a 2046 if (core_text::strlen($str) <= $max) {
b61efafb 2047 return $str;
2048 }
2049 else {
2f1e464a 2050 return core_text::substr($str,0,$max-3).'...';
b61efafb 2051 }
2052}
2053
0705ff84 2054/**
9665ecd2
TH
2055 * Is the user allowed to add this type of module to this course?
2056 * @param object $course the course settings. Only $course->id is used.
2057 * @param string $modname the module name. E.g. 'forum' or 'quiz'.
82101863 2058 * @param \stdClass $user the user to check, defaults to the global user if not provided.
9665ecd2 2059 * @return bool whether the current user is allowed to add this type of module to this course.
0705ff84 2060 */
82101863
JD
2061function course_allowed_module($course, $modname, \stdClass $user = null) {
2062 global $USER;
2063 $user = $user ?? $USER;
9665ecd2
TH
2064 if (is_numeric($modname)) {
2065 throw new coding_exception('Function course_allowed_module no longer
2066 supports numeric module ids. Please update your code to pass the module name.');
0705ff84 2067 }
264867fd 2068
9665ecd2
TH
2069 $capability = 'mod/' . $modname . ':addinstance';
2070 if (!get_capability_info($capability)) {
2071 // Debug warning that the capability does not exist, but no more than once per page.
2072 static $warned = array();
2073 $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER);
2074 if (!isset($warned[$modname]) && $archetype !== MOD_ARCHETYPE_SYSTEM) {
2075 debugging('The module ' . $modname . ' does not define the standard capability ' .
2076 $capability , DEBUG_DEVELOPER);
2077 $warned[$modname] = 1;
2078 }
ddb0a19f 2079
9665ecd2
TH
2080 // If the capability does not exist, the module can always be added.
2081 return true;
0705ff84 2082 }
238c0dd9 2083
9665ecd2 2084 $coursecontext = context_course::instance($course->id);
82101863 2085 return has_capability($capability, $coursecontext, $user);
0705ff84 2086}
2087
cb6fec1f 2088/**
2089 * Efficiently moves many courses around while maintaining
2090 * sortorder in order.
2091 *
e92c39ca
PS
2092 * @param array $courseids is an array of course ids
2093 * @param int $categoryid
df997f84 2094 * @return bool success
cb6fec1f 2095 */
2096function move_courses($courseids, $categoryid) {
ed54cb41 2097 global $DB;
861efb19 2098
df997f84 2099 if (empty($courseids)) {
ed54cb41 2100 // Nothing to do.
5dc361e1 2101 return false;
df997f84 2102 }
264867fd 2103
ed54cb41 2104 if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) {
df997f84
PS
2105 return false;
2106 }
19f601d1 2107
df997f84 2108 $courseids = array_reverse($courseids);
9a5e297b 2109 $newparent = context_coursecat::instance($category->id);
df997f84 2110 $i = 1;
0cbe8111 2111
aec8fe2f 2112 list($where, $params) = $DB->get_in_or_equal($courseids);
3a11e2d2 2113 $dbcourses = $DB->get_records_select('course', 'id ' . $where, $params, '', 'id, category, shortname, fullname');
30fe1181
MG
2114 foreach ($dbcourses as $dbcourse) {
2115 $course = new stdClass();
2116 $course->id = $dbcourse->id;
bc6efcb4 2117 $course->timemodified = time();
30fe1181 2118 $course->category = $category->id;
10db3a0b 2119 $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++;
30fe1181
MG
2120 if ($category->visible == 0) {
2121 // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get
2122 // to previous state if somebody unhides the category.
2123 $course->visible = 0;
2124 }
ed54cb41 2125
30fe1181 2126 $DB->update_record('course', $course);
ed54cb41 2127
3a11e2d2 2128 // Update context, so it can be passed to event.
30fe1181 2129 $context = context_course::instance($course->id);
3a11e2d2 2130 $context->update_moved($newparent);
30fe1181
MG
2131
2132 // Trigger a course updated event.
2133 $event = \core\event\course_updated::create(array(
2134 'objectid' => $course->id,
3a11e2d2 2135 'context' => context_course::instance($course->id),
30fe1181 2136 'other' => array('shortname' => $dbcourse->shortname,
288d6ebb
MJ
2137 'fullname' => $dbcourse->fullname,
2138 'updatedfields' => array('category' => $category->id))
30fe1181 2139 ));
30fe1181
MG
2140 $event->set_legacy_logdata(array($course->id, 'course', 'move', 'edit.php?id=' . $course->id, $course->id));
2141 $event->trigger();
0cbe8111 2142 }
df997f84 2143 fix_course_sortorder();
eabbfa82 2144 cache_helper::purge_by_event('changesincourse');
df997f84 2145
861efb19 2146 return true;
2147}
2148
ae628043 2149/**
ee7084e9 2150 * Returns the display name of the given section that the course prefers
7487c856 2151 *
ee7084e9 2152 * Implementation of this function is provided by course format
21d93554 2153 * @see core_courseformat\base::get_section_name()
7487c856 2154 *
ee7084e9
MG
2155 * @param int|stdClass $courseorid The course to get the section name for (object or just course id)
2156 * @param int|stdClass $section Section object from database or just field course_sections.section
2157 * @return string Display name that the course format prefers, e.g. "Week 2"
ae628043 2158 */
ee7084e9
MG
2159function get_section_name($courseorid, $section) {
2160 return course_get_format($courseorid)->get_section_name($section);
7487c856
SH
2161}
2162
2163/**
ee7084e9 2164 * Tells if current course format uses sections
1b048629 2165 *
7487c856 2166 * @param string $format Course format ID e.g. 'weeks' $course->format
ee7084e9 2167 * @return bool
7487c856 2168 */
7487c856 2169function course_format_uses_sections($format) {
ee7084e9
MG
2170 $course = new stdClass();
2171 $course->format = $format;
2172 return course_get_format($course)->uses_sections();
ae628043 2173}
2174
c0b5d925
DM
2175/**
2176 * Returns the information about the ajax support in the given source format
2177 *
2178 * The returned object's property (boolean)capable indicates that
2179 * the course format supports Moodle course ajax features.
c0b5d925
DM
2180 *
2181 * @param string $format
2182 * @return stdClass
2183 */
2184function course_format_ajax_support($format) {
ee7084e9
MG
2185 $course = new stdClass();
2186 $course->format = $format;
2187 return course_get_format($course)->supports_ajax();
c0b5d925
DM
2188}
2189
2585a68d 2190/**
2191 * Can the current user delete this course?
8b449a39 2192 * Course creators have exception,
2193 * 1 day after the creation they can sill delete the course.
2585a68d 2194 * @param int $courseid
2195 * @return boolean
2585a68d 2196 */
2197function can_delete_course($courseid) {
536c0865 2198 global $USER;
2585a68d 2199
9a5e297b 2200 $context = context_course::instance($courseid);
2585a68d 2201
8b449a39 2202 if (has_capability('moodle/course:delete', $context)) {
2203 return true;
2204 }
2205
2206 // hack: now try to find out if creator created this course recently (1 day)
2207 if (!has_capability('moodle/course:create', $context)) {
2208 return false;
2209 }
2210
2211 $since = time() - 60*60*24;
536c0865
AA
2212 $course = get_course($courseid);
2213
2214 if ($course->timecreated < $since) {
2215 return false; // Return if the course was not created in last 24 hours.
2216 }
2217
2218 $logmanger = get_log_manager();
59aebbed 2219 $readers = $logmanger->get_readers('\core\log\sql_reader');
536c0865
AA
2220 $reader = reset($readers);
2221
2222 if (empty($reader)) {
2223 return false; // No log reader found.
2224 }
8b449a39 2225
536c0865
AA
2226 // A proper reader.
2227 $select = "userid = :userid AND courseid = :courseid AND eventname = :eventname AND timecreated > :since";
2228 $params = array('userid' => $USER->id, 'since' => $since, 'courseid' => $course->id, 'eventname' => '\core\event\course_created');
8b449a39 2229
536c0865 2230 return (bool)$reader->get_events_select_count($select, $params);
2585a68d 2231}
2232
bef12c99 2233/**
2234 * Save the Your name for 'Some role' strings.
2235 *
2236 * @param integer $courseid the id of this course.
2237 * @param array $data the data that came from the course settings form.
2238 */
2239function save_local_role_names($courseid, $data) {
2240 global $DB;
9a5e297b 2241 $context = context_course::instance($courseid);
bef12c99 2242
2243 foreach ($data as $fieldname => $value) {
df997f84 2244 if (strpos($fieldname, 'role_') !== 0) {
bef12c99 2245 continue;
2246 }
2247 list($ignored, $roleid) = explode('_', $fieldname);
2248
2249 // make up our mind whether we want to delete, update or insert
2250 if (!$value) {
2251 $DB->delete_records('role_names', array('contextid' => $context->id, 'roleid' => $roleid));
2252
2253 } else if ($rolename = $DB->get_record('role_names', array('contextid' => $context->id, 'roleid' => $roleid))) {
2254 $rolename->name = $value;
2255 $DB->update_record('role_names', $rolename);
2256
2257 } else {
2258 $rolename = new stdClass;
2259 $rolename->contextid = $context->id;
2260 $rolename->roleid = $roleid;
2261 $rolename->name = $value;
2262 $DB->insert_record('role_names', $rolename);
2263 }
29d4cc65 2264 // This will ensure the course contacts cache is purged..
442f12f8 2265 core_course_category::role_assignment_changed($roleid, $context);
bef12c99 2266 }
2267}
2585a68d 2268
d1f8c1bd
MG
2269/**
2270 * Returns options to use in course overviewfiles filemanager
2271 *
442f12f8 2272 * @param null|stdClass|core_course_list_element|int $course either object that has 'id' property or just the course id;
d1f8c1bd
MG
2273 * may be empty if course does not exist yet (course create form)
2274 * @return array|null array of options such as maxfiles, maxbytes, accepted_types, etc.
2275 * or null if overviewfiles are disabled
2276 */
2277function course_overviewfiles_options($course) {
2278 global $CFG;
2279 if (empty($CFG->courseoverviewfileslimit)) {
2280 return null;
2281 }
7f636497
PH
2282
2283 // Create accepted file types based on config value, falling back to default all.
2284 $acceptedtypes = (new \core_form\filetypes_util)->normalize_file_types($CFG->courseoverviewfilesext);
2285 if (in_array('*', $acceptedtypes) || empty($acceptedtypes)) {
2286 $acceptedtypes = '*';
d1f8c1bd 2287 }
7f636497 2288
d1f8c1bd
MG
2289 $options = array(
2290 'maxfiles' => $CFG->courseoverviewfileslimit,
2291 'maxbytes' => $CFG->maxbytes,
2292 'subdirs' => 0,
7f636497 2293 'accepted_types' => $acceptedtypes
d1f8c1bd
MG
2294 );
2295 if (!empty($course->id)) {
2296 $options['context'] = context_course::instance($course->id);
2297 } else if (is_int($course) && $course > 0) {
2298 $options['context'] = context_course::instance($course);
2299 }
2300 return $options;
2301}
2302
c3df0901 2303/**
df997f84
PS
2304 * Create a course and either return a $course object
2305 *
2306 * Please note this functions does not verify any access control,
2307 * the calling code is responsible for all validation (usually it is the form definition).
bfefa87e 2308 *
df997f84 2309 * @param array $editoroptions course description editor options
bfefa87e 2310 * @param object $data - all the data needed for an entry in the 'course' table
df997f84 2311 * @return object new course instance
bfefa87e 2312 */
df997f84 2313function create_course($data, $editoroptions = NULL) {
0d1e5456 2314 global $DB, $CFG;
bfefa87e 2315
df997f84
PS
2316 //check the categoryid - must be given for all new courses
2317 $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST);
71b77297 2318
9930e426 2319 // Check if the shortname already exists.
df997f84
PS
2320 if (!empty($data->shortname)) {
2321 if ($DB->record_exists('course', array('shortname' => $data->shortname))) {
9930e426 2322 throw new moodle_exception('shortnametaken', '', '', $data->shortname);
71b77297 2323 }
2324 }
2325
9930e426 2326 // Check if the idnumber already exists.
df997f84
PS
2327 if (!empty($data->idnumber)) {
2328 if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) {
9930e426 2329 throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
71b77297 2330 }
2331 }
2f48819b 2332
f96623cb
JP
2333 if (empty($CFG->enablecourserelativedates)) {
2334 // Make sure we're not setting the relative dates mode when the setting is disabled.
2335 unset($data->relativedatesmode);
2336 }
2337
8643c576
DM
2338 if ($errorcode = course_validate_dates((array)$data)) {
2339 throw new moodle_exception($errorcode);
2340 }
2341
9a4231e9
S
2342 // Check if timecreated is given.
2343 $data->timecreated = !empty($data->timecreated) ? $data->timecreated : time();
df997f84 2344 $data->timemodified = $data->timecreated;
71b77297 2345
df997f84
PS
2346 // place at beginning of any category
2347 $data->sortorder = 0;
00f270bc 2348
df997f84
PS
2349 if ($editoroptions) {
2350 // summary text is updated later, we need context to store the files first
2351 $data->summary = '';
2352 $data->summary_format = FORMAT_HTML;
2353 }
2354
fca42002
JP
2355 // Get default completion settings as a fallback in case the enablecompletion field is not set.
2356 $courseconfig = get_config('moodlecourse');
2357 $defaultcompletion = !empty($CFG->enablecompletion) ? $courseconfig->enablecompletion : COMPLETION_DISABLED;
2358 $enablecompletion = $data->enablecompletion ?? $defaultcompletion;
2359 // Unset showcompletionconditions when completion tracking is not enabled for the course.
2360 if ($enablecompletion == COMPLETION_DISABLED) {
2361 unset($data->showcompletionconditions);
2362 } else if (!isset($data->showcompletionconditions)) {
2363 // Show completion conditions should have a default value when completion is enabled. Set it to the site defaults.
2364 // This scenario can happen when a course is created through data generators or through a web service.
2365 $data->showcompletionconditions = $courseconfig->showcompletionconditions;
2366 }
2367
db1218a9
PS
2368 if (!isset($data->visible)) {
2369 // data not from form, add missing visibility info
54a01598 2370 $data->visible = $category->visible;
bfefa87e 2371 }
db1218a9 2372 $data->visibleold = $data->visible;
2585a68d 2373
df997f84 2374 $newcourseid = $DB->insert_record('course', $data);
9a5e297b 2375 $context = context_course::instance($newcourseid, MUST_EXIST);
bfefa87e 2376
df997f84
PS
2377 if ($editoroptions) {
2378 // Save the files used in the summary editor and store
64f93798 2379 $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
df997f84
PS
2380 $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid));
2381 $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid));
2382 }
d1f8c1bd
MG
2383 if ($overviewfilesoptions = course_overviewfiles_options($newcourseid)) {
2384 // Save the course overviewfiles
2385 $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2386 }
bfefa87e 2387
fc79ede5
MG
2388 // update course format options
2389 course_get_format($newcourseid)->update_course_format_options($data);
2390
2391 $course = course_get_format($newcourseid)->get_course();
bfefa87e 2392
df997f84 2393 fix_course_sortorder();
eabbfa82
MG
2394 // purge appropriate caches in case fix_course_sortorder() did not change anything
2395 cache_helper::purge_by_event('changesincourse');
bfefa87e 2396
2542cb54
MG
2397 // Trigger a course created event.
2398 $event = \core\event\course_created::create(array(
2399 'objectid' => $course->id,
2400 'context' => context_course::instance($course->id),
2401 'other' => array('shortname' => $course->shortname,
2402 'fullname' => $course->fullname)
2403 ));
16076f1e 2404
2542cb54
MG
2405 $event->trigger();
2406
2407 // Setup the blocks
2408 blocks_add_default_course_blocks($course);
2409
89b909f6
MG
2410 // Create default section and initial sections if specified (unless they've already been created earlier).
2411 // We do not want to call course_create_sections_if_missing() because to avoid creating course cache.
2412 $numsections = isset($data->numsections) ? $data->numsections : 0;
2413 $existingsections = $DB->get_fieldset_sql('SELECT section from {course_sections} WHERE course = ?', [$newcourseid]);
2414 $newsections = array_diff(range(0, $numsections), $existingsections);
2415 foreach ($newsections as $sectionnum) {
2416 course_create_section($newcourseid, $sectionnum, true);
2417 }
2542cb54 2418
df997f84 2419 // Save any custom role names.
e92c39ca 2420 save_local_role_names($course->id, (array)$data);
bfefa87e 2421
df997f84
PS
2422 // set up enrolments
2423 enrol_course_updated(true, $course, $data);
bef12c99 2424
0d1e5456 2425 // Update course tags.
74fa9f76
MG
2426 if (isset($data->tags)) {
2427 core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
0d1e5456
MG
2428 }
2429
7a0162f1
DM
2430 // Save custom fields if there are any of them in the form.
2431 $handler = core_course\customfield\course_handler::create();
2f61fe97
JP
2432 // Make sure to set the handler's parent context first.
2433 $coursecatcontext = context_coursecat::instance($category->id);
2434 $handler->set_parent_context($coursecatcontext);
2435 // Save the custom field data.
7a0162f1
DM
2436 $data->id = $course->id;
2437 $handler->instance_form_save($data, true);
2438
df997f84 2439 return $course;
bfefa87e 2440}
2441
c3df0901 2442/**
df997f84
PS
2443 * Update a course.
2444 *
2445 * Please note this functions does not verify any access control,
2446 * the calling code is responsible for all validation (usually it is the form definition).
bfefa87e 2447 *
2448 * @param object $data - all the data needed for an entry in the 'course' table
df997f84
PS
2449 * @param array $editoroptions course description editor options
2450 * @return void
bfefa87e 2451 */
df997f84 2452function update_course($data, $editoroptions = NULL) {
0d1e5456 2453 global $DB, $CFG;
bfefa87e 2454
12b94016
SL
2455 // Prevent changes on front page course.
2456 if ($data->id == SITEID) {
2457 throw new moodle_exception('invalidcourse', 'error');
2458 }
2459
fc79ede5 2460 $oldcourse = course_get_format($data->id)->get_course();
9a5e297b 2461 $context = context_course::instance($oldcourse->id);
bfefa87e 2462
f96623cb
JP
2463 // Make sure we're not changing whatever the course's relativedatesmode setting is.
2464 unset($data->relativedatesmode);
2465
288d6ebb
MJ
2466 // Capture the updated fields for the log data.
2467 $updatedfields = [];
2468 foreach (get_object_vars($oldcourse) as $field => $value) {
2469 if ($field == 'summary_editor') {
2470 if (($data->$field)['text'] !== $value['text']) {
2471 // The summary might be very long, we don't wan't to fill up the log record with the full text.
2472 $updatedfields[$field] = '(updated)';
2473 }
34cd2286 2474 } else if ($field == 'tags' && isset($data->tags)) {
288d6ebb
MJ
2475 // Tags might not have the same array keys, just check the values.
2476 if (array_values($data->$field) !== array_values($value)) {
2477 $updatedfields[$field] = $data->$field;
2478 }
2479 } else {
2480 if (isset($data->$field) && $data->$field != $value) {
2481 $updatedfields[$field] = $data->$field;
2482 }
2483 }
2484 }
2485
2486 $data->timemodified = time();
2487
df997f84 2488 if ($editoroptions) {
64f93798 2489 $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0);
71b77297 2490 }
d1f8c1bd
MG
2491 if ($overviewfilesoptions = course_overviewfiles_options($data->id)) {
2492 $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0);
2493 }
71b77297 2494
5536a561
FD
2495 // Check we don't have a duplicate shortname.
2496 if (!empty($data->shortname) && $oldcourse->shortname != $data->shortname) {
e1301685 2497 if ($DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data->shortname, $data->id))) {
5536a561
FD
2498 throw new moodle_exception('shortnametaken', '', '', $data->shortname);
2499 }
2500 }
2501
2502 // Check we don't have a duplicate idnumber.
2503 if (!empty($data->idnumber) && $oldcourse->idnumber != $data->idnumber) {
e1301685 2504 if ($DB->record_exists_sql('SELECT id from {course} WHERE idnumber = ? AND id <> ?', array($data->idnumber, $data->id))) {
5536a561
FD
2505 throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber);
2506 }
2507 }
2508
8643c576
DM
2509 if ($errorcode = course_validate_dates((array)$data)) {
2510 throw new moodle_exception($errorcode);
2511 }
2512
df997f84
PS
2513 if (!isset($data->category) or empty($data->category)) {
2514 // prevent nulls and 0 in category field
bfefa87e 2515 unset($data->category);
2516 }
b80a50f7 2517 $changesincoursecat = $movecat = (isset($data->category) and $oldcourse->category != $data->category);
bfefa87e 2518
db1218a9
PS
2519 if (!isset($data->visible)) {
2520 // data not from form, add missing visibility info
df997f84
PS
2521 $data->visible = $oldcourse->visible;
2522 }
bfefa87e 2523
df997f84
PS
2524 if ($data->visible != $oldcourse->visible) {
2525 // reset the visibleold flag when manually hiding/unhiding course
2526 $data->visibleold = $data->visible;
b55248d5 2527 $changesincoursecat = true;
df997f84 2528 } else {
a372aab5 2529 if ($movecat) {
df997f84
PS
2530 $newcategory = $DB->get_record('course_categories', array('id'=>$data->category));
2531 if (empty($newcategory->visible)) {
2532 // make sure when moving into hidden category the course is hidden automatically
2533 $data->visible = 0;
2534 }
a372aab5 2535 }
df997f84 2536 }
a372aab5 2537
019186b7
AT
2538 // Set newsitems to 0 if format does not support announcements.
2539 if (isset($data->format)) {
2540 $newcourseformat = course_get_format((object)['format' => $data->format]);
745f79da 2541 if (!$newcourseformat->supports_news()) {
019186b7
AT
2542 $data->newsitems = 0;
2543 }
2544 }
2545
fca42002
JP
2546 // Set showcompletionconditions to null when completion tracking has been disabled for the course.
2547 if (isset($data->enablecompletion) && $data->enablecompletion == COMPLETION_DISABLED) {
2548 $data->showcompletionconditions = null;
2549 }
2550
7a0162f1
DM
2551 // Update custom fields if there are any of them in the form.
2552 $handler = core_course\customfield\course_handler::create();
2553 $handler->instance_form_save($data);
2554
df997f84 2555 // Update with the new data
487caf6b 2556 $DB->update_record('course', $data);
38b19bbc
MG
2557 // make sure the modinfo cache is reset
2558 rebuild_course_cache($data->id);
bfefa87e 2559
2affc87d
DM
2560 // Purge course image cache in case if course image has been updated.
2561 \cache::make('core', 'course_image')->delete($data->id);
2562
fc79ede5
MG
2563 // update course format options with full course data
2564 course_get_format($data->id)->update_course_format_options($data, $oldcourse);
2565
df997f84 2566 $course = $DB->get_record('course', array('id'=>$data->id));
bfefa87e 2567
df997f84 2568 if ($movecat) {
9a5e297b 2569 $newparent = context_coursecat::instance($course->category);
2c5b0eb7 2570 $context->update_moved($newparent);
df997f84 2571 }
3a11e2d2
RT
2572 $fixcoursesortorder = $movecat || (isset($data->sortorder) && ($oldcourse->sortorder != $data->sortorder));
2573 if ($fixcoursesortorder) {
24f824b9
MG
2574 fix_course_sortorder();
2575 }
2576
eabbfa82
MG
2577 // purge appropriate caches in case fix_course_sortorder() did not change anything
2578 cache_helper::purge_by_event('changesincourse');
b55248d5 2579 if ($changesincoursecat) {
b80a50f7
MG
2580 cache_helper::purge_by_event('changesincoursecat');
2581 }
bfefa87e 2582
df997f84
PS
2583 // Test for and remove blocks which aren't appropriate anymore
2584 blocks_remove_inappropriate($course);
bfefa87e 2585
df997f84
PS
2586 // Save any custom role names.
2587 save_local_role_names($course->id, $data);
2588
2589 // update enrol settings
2590 enrol_course_updated(false, $course, $data);
2591
0d1e5456 2592 // Update course tags.
74fa9f76
MG
2593 if (isset($data->tags)) {
2594 core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags);
0d1e5456
MG
2595 }
2596
53a8e678
MN
2597 // Trigger a course updated event.
2598 $event = \core\event\course_updated::create(array(
2599 'objectid' => $course->id,
3a11e2d2 2600 'context' => context_course::instance($course->id),
53a8e678 2601 'other' => array('shortname' => $course->shortname,
288d6ebb
MJ
2602 'fullname' => $course->fullname,
2603 'updatedfields' => $updatedfields)
53a8e678 2604 ));
3a11e2d2 2605
2bf2f359 2606 $event->set_legacy_logdata(array($course->id, 'course', 'update', 'edit.php?id=' . $course->id, $course->id));
53a8e678 2607 $event->trigger();
fc79ede5
MG
2608
2609 if ($oldcourse->format !== $course->format) {
2610 // Remove all options stored for the previous format
2611 // We assume that new course format migrated everything it needed watching trigger
2612 // 'course_updated' and in method format_XXX::update_course_format_options()
2613 $DB->delete_records('course_format_options',
2614 array('courseid' => $course->id, 'format' => $oldcourse->format));
2615 }
bfefa87e 2616}
2585a68d 2617
07ab0c80 2618/**
c268c1bd
DM
2619 * Calculate the average number of enrolled participants per course.
2620 *
2621 * This is intended for statistics purposes during the site registration. Only visible courses are taken into account.
2622 * Front page enrolments are excluded.
2623 *
2624 * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions.
2625 * @param int $lastloginsince If specified, count only users who logged in after this timestamp.
2626 * @return float
07ab0c80 2627 */
c268c1bd
DM
2628function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float {
2629 global $DB;
fcb4decd 2630
c268c1bd
DM
2631 $params = [
2632 'siteid' => SITEID,
2633 ];
2634
2635 $sql = "SELECT DISTINCT ue.userid, e.courseid
2636 FROM {user_enrolments} ue
2637 JOIN {enrol} e ON e.id = ue.enrolid
2638 JOIN {course} c ON c.id = e.courseid ";
2639
2640 if ($onlyactive || $lastloginsince) {
2641 $sql .= "JOIN {user} u ON u.id = ue.userid ";
2642 }
fcb4decd 2643
c268c1bd
DM
2644 $sql .= "WHERE e.courseid <> :siteid
2645 AND c.visible = 1 ";
fcb4decd 2646
c268c1bd
DM
2647 if ($onlyactive) {
2648 $sql .= "AND ue.status = :active
2649 AND e.status = :enabled
2650 AND ue.timestart < :now1
2651 AND (ue.timeend = 0 OR ue.timeend > :now2) ";
2652
2653 // Same as in the enrollib - the rounding should help caching in the database.
2654 $now = round(time(), -2);
2655
2656 $params += [
2657 'active' => ENROL_USER_ACTIVE,
2658 'enabled' => ENROL_INSTANCE_ENABLED,
2659 'now1' => $now,
2660 'now2' => $now,
2661 ];
2662 }
2663
2664 if ($lastloginsince) {
2665 $sql .= "AND u.lastlogin > :lastlogin ";
2666 $params['lastlogin'] = $lastloginsince;
2667 }
2668
2669 $sql = "SELECT COUNT(*)
2670 FROM ($sql) total";
2671
2672 $enrolmenttotal = $DB->count_records_sql($sql, $params);
2673
2674 // Get the number of visible courses (exclude the front page).
2675 $coursetotal = $DB->count_records('course', ['visible' => 1]);
2676 $coursetotal = $coursetotal - 1;
fcb4decd 2677
fcb4decd 2678 if (empty($coursetotal)) {
2679 $participantaverage = 0;
c268c1bd 2680
fcb4decd 2681 } else {
2682 $participantaverage = $enrolmenttotal / $coursetotal;
2683 }
2684
2685 return $participantaverage;
07ab0c80 2686}
2687
2688/**
fcb4decd 2689 * Average number of course modules
07ab0c80 2690 * @return integer
2691 */
2692function average_number_of_courses_modules() {
fcb4decd 2693 global $DB, $SITE;
2694
2695 //count total of visible course module (except front page)
2696 $sql = 'SELECT COUNT(*) FROM (
2697 SELECT cm.course, cm.module
2698 FROM {course} c, {course_modules} cm
516c5eca 2699 WHERE c.id = cm.course
fcb4decd 2700 AND c.id <> :siteid
2701 AND cm.visible = 1
9f247093 2702 AND c.visible = 1) total';
fcb4decd 2703 $params = array('siteid' => $SITE->id);
2704 $moduletotal = $DB->count_records_sql($sql, $params);
2705
2706
2707 //count total of visible courses (minus front page)
2708 $coursetotal = $DB->count_records('course', array('visible' => 1));
2709 $coursetotal = $coursetotal - 1 ;
2710
2711 //average of course module
2712 if (empty($coursetotal)) {
2713 $coursemoduleaverage = 0;
2714 } else {
2715 $coursemoduleaverage = $moduletotal / $coursetotal;
2716 }
2717
2718 return $coursemoduleaverage;
07ab0c80 2719}
2720
8bdc9cac
SH
2721/**
2722 * This class pertains to course requests and contains methods associated with
2723 * create, approving, and removing course requests.
2724 *
64f93798
PS
2725 * Please note we do not allow embedded images here because there is no context
2726 * to store them with proper access control.
2727 *
8bdc9cac
SH
2728 * @copyright 2009 Sam Hemelryk
2729 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2730 * @since Moodle 2.0
2731 *
2732 * @property-read int $id
2733 * @property-read string $fullname
2734 * @property-read string $shortname
2735 * @property-read string $summary
2736 * @property-read int $summaryformat
2737 * @property-read int $summarytrust
2738 * @property-read string $reason
2739 * @property-read int $requester
8bdc9cac
SH
2740 */
2741class course_request {
2742
2743 /**
2744 * This is the stdClass that stores the properties for the course request
7e85563d 2745 * and is externally accessed through the __get magic method
8bdc9cac
SH
2746 * @var stdClass
2747 */
2748 protected $properties;
2749
2750 /**
2751 * An array of options for the summary editor used by course request forms.
2752 * This is initially set by {@link summary_editor_options()}
2753 * @var array
2754 * @static
2755 */
2756 protected static $summaryeditoroptions;
2757
8bdc9cac
SH
2758 /**
2759 * Static function to prepare the summary editor for working with a course
2760 * request.
2761 *
2762 * @static
2763 * @param null|stdClass $data Optional, an object containing the default values
2764 * for the form, these may be modified when preparing the
2765 * editor so this should be called before creating the form
2766 * @return stdClass An object that can be used to set the default values for
2767 * an mforms form
2768 */
2769 public static function prepare($data=null) {
2770 if ($data === null) {
2771 $data = new stdClass;
2772 }
64f93798 2773 $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options());
8bdc9cac
SH
2774 return $data;
2775 }
2776
2777 /**
2778 * Static function to create a new course request when passed an array of properties
2779 * for it.
2780 *
2781 * This function also handles saving any files that may have been used in the editor
2782 *
2783 * @static
2784 * @param stdClass $data
2785 * @return course_request The newly created course request
2786 */
2787 public static function create($data) {
2788 global $USER, $DB, $CFG;
2789 $data->requester = $USER->id;
64f93798 2790
d347f304 2791 // Setting the default category if none set.
959e4f0e 2792 if (empty($data->category) || !empty($CFG->lockrequestcategory)) {
59b9a140
FM
2793 $data->category = $CFG->defaultrequestcategory;
2794 }
2795
64f93798
PS
2796 // Summary is a required field so copy the text over
2797 $data->summary = $data->summary_editor['text'];
2798 $data->summaryformat = $data->summary_editor['format'];
2799
8bdc9cac 2800 $data->id = $DB->insert_record('course_request', $data);
64f93798 2801
8bdc9cac
SH
2802 // Create a new course_request object and return it
2803 $request = new course_request($data);
2804
2805 // Notify the admin if required.
adf176d7 2806 if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) {
aa6c1ced 2807
8bdc9cac
SH
2808 $a = new stdClass;
2809 $a->link = "$CFG->wwwroot/course/pending.php";
2810 $a->user = fullname($USER);
2811 $subject = get_string('courserequest');
2812 $message = get_string('courserequestnotifyemail', 'admin', $a);
2813 foreach ($users as $user) {
2a63b636 2814 $request->notify($user, $USER, 'courserequested', $subject, $message);
8bdc9cac
SH
2815 }
2816 }
2817
2818 return $request;
2819 }
2820
2821 /**
2822 * Returns an array of options to use with a summary editor
2823 *
2824 * @uses course_request::$summaryeditoroptions
2825 * @return array An array of options to use with the editor
2826 */
2827 public static function summary_editor_options() {
2828 global $CFG;
2829 if (self::$summaryeditoroptions === null) {
64f93798 2830 self::$summaryeditoroptions = array('maxfiles' => 0, 'maxbytes'=>0);
8bdc9cac
SH
2831 }
2832 return self::$summaryeditoroptions;
2833 }
2834
8bdc9cac
SH
2835 /**
2836 * Loads the properties for this course request object. Id is required and if
2837 * only id is provided then we load the rest of the properties from the database
2838 *
2839 * @param stdClass|int $properties Either an object containing properties
2840 * or the course_request id to load
2841 */
2842 public function __construct($properties) {
2843 global $DB;
2844 if (empty($properties->id)) {
2845 if (empty($properties)) {
2846 throw new coding_exception('You must provide a course request id when creating a course_request object');
2847 }
2848 $id = $properties;
2849 $properties = new stdClass;
2850 $properties->id = (int)$id;
2851 unset($id);
2852 }
2853 if (empty($properties->requester)) {
2854 if (!($this->properties = $DB->get_record('course_request', array('id' => $properties->id)))) {
2855 print_error('unknowncourserequest');
2856 }
2857 } else {
2858 $this->properties = $properties;
2859 }
2860 $this->properties->collision = null;
2861 }
2862
2863 /**
2864 * Returns the requested property
2865 *
2866 * @param string $key
2867 * @return mixed
2868 */
2869 public function __get($key) {
8bdc9cac
SH
2870 return $this->properties->$key;
2871 }
2872
2873 /**
2874 * Override this to ensure empty($request->blah) calls return a reliable answer...
2875 *
2876 * This is required because we define the __get method
2877 *
2878 * @param mixed $key
2879 * @return bool True is it not empty, false otherwise
2880 */
2881 public function __isset($key) {
2882 return (!empty($this->properties->$key));
2883 }
2884
2885 /**
2886 * Returns the user who requested this course
2887 *
2888 * Uses a static var to cache the results and cut down the number of db queries
2889 *
2890 * @staticvar array $requesters An array of cached users
2891 * @return stdClass The user who requested the course
2892 */
2893 public function get_requester() {
2894 global $DB;
2895 static $requesters= array();
2896 if (!array_key_exists($this->properties->requester, $requesters)) {
2897 $requesters[$this->properties->requester] = $DB->get_record('user', array('id'=>$this->properties->requester));
2898 }
2899 return $requesters[$this->properties->requester];
2900 }
2901
2902 /**
2903 * Checks that the shortname used by the course does not conflict with any other
2904 * courses that exist
2905 *
2906 * @param string|null $shortnamemark The string to append to the requests shortname
2907 * should a conflict be found
2908 * @return bool true is there is a conflict, false otherwise
2909 */
2910 public function check_shortname_collision($shortnamemark = '[*]') {
2911 global $DB;
2912
2913 if ($this->properties->collision !== null) {
2914 return $this->properties->collision;
2915 }
2916
2917 if (empty($this->properties->shortname)) {
2918 debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER);
2919 $this->properties->collision = false;
2920 } else if ($DB->record_exists('course', array('shortname' => $this->properties->shortname))) {
2921 if (!empty($shortnamemark)) {
2922 $this->properties->shortname .= ' '.$shortnamemark;
2923 }
2924 $this->properties->collision = true;
2925 } else {
2926 $this->properties->collision = false;
2927 }
2928 return $this->properties->collision;
2929 }
2930
3e15abe5
MG
2931 /**
2932 * Checks user capability to approve a requested course
2933 *
2934 * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is
2935 * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'.
2936 *
2937 * @return bool
2938 */
2939 public function can_approve() {
2940 global $CFG;
2941 $category = null;
2942 if ($this->properties->category) {
2943 $category = core_course_category::get($this->properties->category, IGNORE_MISSING);
2944 } else if ($CFG->defaultrequestcategory) {
2945 $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING);
2946 }
2947 if ($category) {
2948 return has_capability('moodle/site:approvecourse', $category->get_context());
2949 }
2950
2951 // We can not determine the context where the course should be created. The approver should have
2952 // both capabilities to approve courses and change course category in the system context.
2953 return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance());
2954 }
2955
2d8a275b
MG
2956 /**
2957 * Returns the category where this course request should be created
2958 *
2959 * Note that we don't check here that user has a capability to view
2960 * hidden categories if he has capabilities 'moodle/site:approvecourse' and
2961 * 'moodle/course:changecategory'
2962 *
442f12f8 2963 * @return core_course_category
2d8a275b
MG
2964 */
2965 public function get_category() {
2966 global $CFG;
3e15abe5
MG
2967 if ($this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING))) {
2968 return $category;
2969 } else if ($CFG->defaultrequestcategory &&
2970 ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING))) {
2971 return $category;
2972 } else {
2973 return core_course_category::get_default();
2d8a275b 2974 }
2d8a275b
MG
2975 }
2976
8bdc9cac
SH
2977 /**
2978 * This function approves the request turning it into a course
2979 *
2980 * This function converts the course request into a course, at the same time
7e85563d 2981 * transferring any files used in the summary to the new course and then removing
8bdc9cac
SH
2982 * the course request and the files associated with it.
2983 *
2984 * @return int The id of the course that was created from this request
2985 */
2986 public function approve() {
2987 global $CFG, $DB, $USER;
70740405 2988
9f9d7bbe
AG
2989 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2990
70740405
PS
2991 $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST);
2992
8bdc9cac
SH
2993 $courseconfig = get_config('moodlecourse');
2994
2995 // Transfer appropriate settings
70740405
PS
2996 $data = clone($this->properties);
2997 unset($data->id);
2998 unset($data->reason);
2999 unset($data->requester);
8bdc9cac
SH
3000
3001 // Set category
2d8a275b 3002 $category = $this->get_category();
70740405 3003 $data->category = $category->id;
8bdc9cac 3004 // Set misc settings
70740405 3005 $data->requested = 1;
8bdc9cac
SH
3006
3007 // Apply course default settings
70740405 3008 $data->format = $courseconfig->format;
70740405
PS
3009 $data->newsitems = $courseconfig->newsitems;
3010 $data->showgrades = $courseconfig->showgrades;
3011 $data->showreports = $courseconfig->showreports;
3012 $data->maxbytes = $courseconfig->maxbytes;
3013 $data->groupmode = $courseconfig->groupmode;
3014 $data->groupmodeforce = $courseconfig->groupmodeforce;
3015 $data->visible = $courseconfig->visible;
3016 $data->visibleold = $data->visible;
3017 $data->lang = $courseconfig->lang;
f6b9b6b7 3018 $data->enablecompletion = $courseconfig->enablecompletion;
ca66f965 3019 $data->numsections = $courseconfig->numsections;
6363c9ad
FK
3020 $data->startdate = usergetmidnight(time());
3021 if ($courseconfig->courseenddateenabled) {
3022 $data->enddate = usergetmidnight(time()) + $courseconfig->courseduration;
3023 }
8bdc9cac 3024
9f9d7bbe
AG
3025 list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname);
3026
70740405 3027 $course = create_course($data);
1f364c87 3028 $context = context_course::instance($course->id, MUST_EXIST);
8bdc9cac 3029
70740405
PS
3030 // add enrol instances
3031 if (!$DB->record_exists('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) {
3032 if ($manual = enrol_get_plugin('manual')) {
3033 $manual->add_default_instance($course);
3034 }
8bdc9cac 3035 }
70740405
PS
3036
3037 // enrol the requester as teacher if necessary
3038 if (!empty($CFG->creatornewroleid) and !is_viewing($context, $user, 'moodle/role:assign') and !is_enrolled($context, $user, 'moodle/role:assign')) {
3039 enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid);
3040 }
3041
3042 $this->delete();
3043
fbaea88f 3044 $a = new stdClass();
9a5e297b 3045 $a->name = format_string($course->fullname, true, array('context' => context_course::instance($course->id)));
70740405 3046 $a->url = $CFG->wwwroot.'/course/view.php?id=' . $course->id;
880fc15b 3047 $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a), $course->id);
70740405
PS
3048
3049 return $course->id;
8bdc9cac
SH
3050 }
3051
3052 /**
3053 * Reject a course request
3054 *
3055 * This function rejects a course request, emailing the requesting user the
3056 * provided notice and then removing the request from the database
3057 *
3058 * @param string $notice The message to display to the user
3059 */
3060 public function reject($notice) {
70740405
PS
3061 global $USER, $DB;
3062 $user = $DB->get_record('user', array('id' => $this->properties->requester), '*', MUST_EXIST);
8bdc9cac
SH
3063 $this->notify($user, $USER, 'courserequestrejected', get_string('courserejectsubject'), get_string('courserejectemail', 'moodle', $notice));
3064 $this->delete();
3065 }
3066
3067 /**
3068 * Deletes the course request and any associated files
3069 */
3070 public function delete() {
3071 global $DB;
3072 $DB->delete_records('course_request', array('id' => $this->properties->id));
8bdc9cac
SH
3073 }
3074
3075 /**
3076 * Send a message from one user to another using events_trigger
3077 *
3078 * @param object $touser
3079 * @param object $fromuser
3080 * @param string $name
3081 * @param string $subject
3082 * @param string $message
880fc15b 3083 * @param int|null $courseid
8bdc9cac 3084 */
0eada7a5 3085 protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) {
cc350fd9 3086 $eventdata = new \core\message\message();
880fc15b 3087 $eventdata->courseid = empty($courseid) ? SITEID : $courseid;
a1708181 3088 $eventdata->component = 'moodle';
8bdc9cac
SH
3089 $eventdata->name = $name;
3090 $eventdata->userfrom = $fromuser;
3091 $eventdata->userto = $touser;
3092 $eventdata->subject = $subject;
3093 $eventdata->fullmessage = $message;
3094 $eventdata->fullmessageformat = FORMAT_PLAIN;
3095 $eventdata->fullmessagehtml = '';
3096 $eventdata->smallmessage = '';
a1708181 3097 $eventdata->notification = 1;
7c7d3afa 3098 message_send($eventdata);
8bdc9cac 3099 }
3e15abe5
MG
3100
3101 /**
3102 * Checks if current user can request a course in this context
3103 *
3104 * @param context $context
3105 * @return bool
3106 */
3107 public static function can_request(context $context) {
3108 global $CFG;
3109 if (empty($CFG->enablecourserequests)) {
3110 return false;
3111 }
3112 if (has_capability('moodle/course:create', $context)) {
3113 return false;
3114 }
3115
3116 if ($context instanceof context_system) {
3117 $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING);
3118 return $defaultcontext &&
3119 has_capability('moodle/course:request', $defaultcontext);
3120 } else if ($context instanceof context_coursecat) {
959e4f0e 3121 if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) {
3e15abe5
MG
3122 return has_capability('moodle/course:request', $context);
3123 }
3124 }
3125 return false;
3126 }
5c34c4ba 3127}
b1627a92
DC
3128
3129/**
3130 * Return a list of page types
3131 * @param string $pagetype current page type
9dd85edf
MG
3132 * @param context $parentcontext Block's parent context
3133 * @param context $currentcontext Current context of block
3134 * @return array array of page types
b1627a92 3135 */
b38e2e28 3136function course_page_type_list($pagetype, $parentcontext, $currentcontext) {
9dd85edf
MG
3137 if ($pagetype === 'course-index' || $pagetype === 'course-index-category') {
3138 // For courses and categories browsing pages (/course/index.php) add option to show on ANY category page
3139 $pagetypes = array('*' => get_string('page-x', 'pagetype'),
3140 'course-index-*' => get_string('page-course-index-x', 'pagetype'),
3141 );
3142 } else if ($currentcontext && (!($coursecontext = $currentcontext->get_course_context(false)) || $coursecontext->instanceid == SITEID)) {
3143 // We know for sure that despite pagetype starts with 'course-' this is not a page in course context (i.e. /course/search.php, etc.)
3144 $pagetypes = array('*' => get_string('page-x', 'pagetype'));
3145 } else {
3146 // Otherwise consider it a page inside a course even if $currentcontext is null
3147 $pagetypes = array('*' => get_string('page-x', 'pagetype'),
3148 'course-*' => get_string('page-course-x', 'pagetype'),
3149 'course-view-*' => get_string('page-course-view-x', 'pagetype')
3150 );
b1627a92 3151 }
9dd85edf 3152 return $pagetypes;
af27c69e 3153}
ebaa29d1 3154
5720019d
ARN
3155/**
3156 * Determine whether course ajax should be enabled for the specified course
3157 *
924c34df 3158 * @param stdClass $course The course to test against
5720019d
ARN
3159 * @return boolean Whether course ajax is enabled or note
3160 */
3161function course_ajax_enabled($course) {
3162 global $CFG, $PAGE, $SITE;
3163
5720019d
ARN
3164 // The user must be editing for AJAX to be included
3165 if (!$PAGE->user_is_editing()) {
3166 return false;
3167 }
3168
3169 // Check that the theme suports
3170 if (!$PAGE->theme->enablecourseajax) {
3171 return false;
3172 }
3173
3174 // Check that the course format supports ajax functionality
3175 // The site 'format' doesn't have information on course format support
3176 if ($SITE->id !== $course->id) {
3177 $courseformatajaxsupport = course_format_ajax_support($course->format);
3178 if (!$courseformatajaxsupport->capable) {
3179 return false;
3180 }
3181 }
3182
3183 // All conditions have been met so course ajax should be enabled
3184 return true;
3185}
3186
ebaa29d1
ARN
3187/**
3188 * Include the relevant javascript and language strings for the resource
3189 * toolbox YUI module
3190 *
3191 * @param integer $id The ID of the course being applied to
7b061512
SH
3192 * @param array $usedmodules An array containing the names of the modules in use on the page
3193 * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site
924c34df 3194 * @param stdClass $config An object containing configuration parameters for ajax modules including:
ebaa29d1
ARN
3195 * * resourceurl The URL to post changes to for resource changes
3196 * * sectionurl The URL to post changes to for section changes
3197 * * pageparams Additional parameters to pass through in the post
7b061512 3198 * @return bool
ebaa29d1 3199 */
7b061512 3200function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) {
52973369 3201 global $CFG, $PAGE, $SITE;
ebaa29d1 3202
23499e64
FR
3203 // Init the course editor module to support UI components.
3204 $format = course_get_format($course);
3205 include_course_editor($format);
3206
ebaa29d1 3207 // Ensure that ajax should be included
5720019d
ARN
3208 if (!course_ajax_enabled($course)) {
3209 return false;
ebaa29d1
ARN
3210 }
3211
23499e64
FR
3212 // Component based formats don't use YUI drag and drop anymore.
3213 if (!$format->supports_components() && course_format_uses_sections($course->format)) {
ebaa29d1 3214
23499e64
FR
3215 if (!$config) {
3216 $config = new stdClass();
3217 }
ebaa29d1 3218
23499e64
FR
3219 // The URL to use for resource changes
3220 if (!isset($config->resourceurl)) {
3221 $config->resourceurl = '/course/rest.php';
3222 }
ebaa29d1 3223
23499e64
FR
3224 // The URL to use for section changes
3225 if (!isset($config->sectionurl)) {
3226 $config->sectionurl = '/course/rest.php';
3227 }
3228
3229 // Any additional parameters which need to be included on page submission
3230 if (!isset($config->pageparams)) {
3231 $config->pageparams = array();
3232 }
ebaa29d1 3233
c77582fe 3234 $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop',
15e2552f
RK
3235 array(array(
3236 'courseid' => $course->id,
56838156 3237 'ajaxurl' => $config->sectionurl,
15e2552f
RK
3238 'config' => $config,
3239 )), null, true);
3240
c77582fe 3241 $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_resource_dragdrop',
15e2552f
RK
3242 array(array(
3243 'courseid' => $course->id,
56838156 3244 'ajaxurl' => $config->resourceurl,
15e2552f
RK
3245 'config' => $config,
3246 )), null, true);
56838156
RK
3247 }
3248
ebaa29d1
ARN
3249 // Require various strings for the command toolbox
3250 $PAGE->requires->strings_for_js(array(
3251 'moveleft',
3252 'deletechecktype',
3253 'deletechecktypename',
7a9a07d2
ARN
3254 'edittitle',
3255 'edittitleinstructions',
ebaa29d1
ARN
3256 'show',
3257 'hide',
60cf0742
S
3258 'highlight',
3259 'highlightoff',
ebaa29d1
ARN
3260 'groupsnone',
3261 'groupsvisible',
3262 'groupsseparate',
3263 'clicktochangeinbrackets',
3264 'markthistopic',
3265 'markedthistopic',
15e2552f 3266 'movesection',
34bcc6a9
AN
3267 'movecoursemodule',
3268 'movecoursesection',
dd66b6ab 3269 'movecontent',
bbb483b2 3270 'tocontent',
34bcc6a9
AN
3271 'emptydragdropregion',
3272 'afterresource',
3273 'aftersection',
3274 'totopofsection',
ebaa29d1
ARN
3275 ), 'moodle');
3276
47281f7e
AN
3277 // Include section-specific strings for formats which support sections.
3278 if (course_format_uses_sections($course->format)) {
ebaa29d1
ARN
3279 $PAGE->requires->strings_for_js(array(
3280 'showfromothers',
3281 'hidefromothers',
3282 ), 'format_' . $course->format);
3283 }
3284
3285 // For confirming resource deletion we need the name of the module in question
7b061512 3286 foreach ($usedmodules as $module => $modname) {
ebaa29d1
ARN
3287 $PAGE->requires->string_for_js('pluginname', $module);
3288 }
3aaa1843 3289
32528f94 3290 // Load drag and drop upload AJAX.
52973369 3291 require_once($CFG->dirroot.'/course/dnduploadlib.php');
7b061512
SH
3292 dndupload_add_to_course($course, $enabledmodules);
3293
8341055e
MG
3294 $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format));
3295
5720019d 3296 return true;
ebaa29d1 3297}
5218b9bb 3298
830c3eb9
FR
3299/**
3300 * Include and configure the course editor modules.
3301 *
3302 * @param course_format $format the course format instance.
3303 */
3304function include_course_editor(course_format $format) {
3305 global $PAGE, $SITE;
3306
3307 $course = $format->get_course();
3308
3309 if ($SITE->id === $course->id) {
3310 return;
3311 }
3312
3313 // Edition mode and some format specs must be passed to the init method.
3314 $setup = (object)[
3315 'editing' => $format->show_editor(),
7f750dc0 3316 'supportscomponents' => $format->supports_components(),
830c3eb9
FR
3317 ];
3318 // All the new editor elements will be loaded after the course is presented and
3319 // the initial course state will be generated using core_course_get_state webservice.
3320 $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]);
3321}
3322
3776335c
MG
3323/**
3324 * Returns the sorted list of available course formats, filtered by enabled if necessary
3325 *
3326 * @param bool $enabledonly return only formats that are enabled
3327 * @return array array of sorted format names
3328 */
3329function get_sorted_course_formats($enabledonly = false) {
3330 global $CFG;
bd3b3bba 3331 $formats = core_component::get_plugin_list('format');
3776335c
MG
3332
3333 if (!empty($CFG->format_plugins_sortorder)) {
3334 $order = explode(',', $CFG->format_plugins_sortorder);
3335 $order = array_merge(array_intersect($order, array_keys($formats)),
3336 array_diff(array_keys($formats), $order));
3337 } else {
3338 $order = array_keys($formats);
3339 }
3340 if (!$enabledonly) {
3341 return $order;
3342 }
3343 $sortedformats = array();
3344 foreach ($order as $formatname) {
3345 if (!get_config('format_'.$formatname, 'disabled')) {
3346 $sortedformats[] = $formatname;
3347 }
3348 }
3349 return $sortedformats;
3350}
3351
5218b9bb
DP
3352/**
3353 * The URL to use for the specified course (with section)
3354 *
ee7084e9
MG
3355 * @param int|stdClass $courseorid The course to get the section name for (either object or just course id)
3356 * @param int|stdClass $section Section object from database or just field course_sections.section
923451c5
MG
3357 * if omitted the course view page is returned
3358 * @param array $options options for view URL. At the moment core uses:
3359 * 'navigation' (bool) if true and section has no separate page, the function returns null
3360 * 'sr' (int) used by multipage formats to specify to which section to return
5218b9bb
DP
3361 * @return moodle_url The url of course
3362 */
ee7084e9
MG
3363function course_get_url($courseorid, $section = null, $options = array()) {
3364 return course_get_format($courseorid)->get_view_url($section, $options);
5218b9bb 3365}
dd5d933f 3366
dd5d933f
JM
3367/**
3368 * Create a module.
7cb0ea2c 3369 *
dd5d933f
JM
3370 * It includes:
3371 * - capability checks and other checks
3372 * - create the module from the module info
3373 *
3374 * @param object $module
3375 * @return object the created module info
b4b75872 3376 * @throws moodle_exception if user is not allowed to perform the action or module is not allowed in this course
dd5d933f
JM
3377 */