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