PageRenderTime 58ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/course/lib.php

https://bitbucket.org/moodle/moodle
PHP | 5132 lines | 3085 code | 601 blank | 1446 comment | 634 complexity | 3a0059cea7d8c7feec8b0a9ea43804a9 MD5 | raw file
Possible License(s): Apache-2.0, LGPL-2.1, BSD-3-Clause, MIT, GPL-3.0

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Library of useful functions
  18. *
  19. * @copyright 1999 Martin Dougiamas http://dougiamas.com
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. * @package core_course
  22. */
  23. defined('MOODLE_INTERNAL') || die;
  24. use core_courseformat\base as course_format;
  25. require_once($CFG->libdir.'/completionlib.php');
  26. require_once($CFG->libdir.'/filelib.php');
  27. require_once($CFG->libdir.'/datalib.php');
  28. require_once($CFG->dirroot.'/course/format/lib.php');
  29. define('COURSE_MAX_LOGS_PER_PAGE', 1000); // Records.
  30. define('COURSE_MAX_RECENT_PERIOD', 172800); // Two days, in seconds.
  31. /**
  32. * Number of courses to display when summaries are included.
  33. * @var int
  34. * @deprecated since 2.4, use $CFG->courseswithsummarieslimit instead.
  35. */
  36. define('COURSE_MAX_SUMMARIES_PER_PAGE', 10);
  37. // Max courses in log dropdown before switching to optional.
  38. define('COURSE_MAX_COURSES_PER_DROPDOWN', 1000);
  39. // Max users in log dropdown before switching to optional.
  40. define('COURSE_MAX_USERS_PER_DROPDOWN', 1000);
  41. define('FRONTPAGENEWS', '0');
  42. define('FRONTPAGECATEGORYNAMES', '2');
  43. define('FRONTPAGECATEGORYCOMBO', '4');
  44. define('FRONTPAGEENROLLEDCOURSELIST', '5');
  45. define('FRONTPAGEALLCOURSELIST', '6');
  46. define('FRONTPAGECOURSESEARCH', '7');
  47. // Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage.
  48. define('EXCELROWS', 65535);
  49. define('FIRSTUSEDEXCELROW', 3);
  50. define('MOD_CLASS_ACTIVITY', 0);
  51. define('MOD_CLASS_RESOURCE', 1);
  52. define('COURSE_TIMELINE_ALLINCLUDINGHIDDEN', 'allincludinghidden');
  53. define('COURSE_TIMELINE_ALL', 'all');
  54. define('COURSE_TIMELINE_PAST', 'past');
  55. define('COURSE_TIMELINE_INPROGRESS', 'inprogress');
  56. define('COURSE_TIMELINE_FUTURE', 'future');
  57. define('COURSE_TIMELINE_SEARCH', 'search');
  58. define('COURSE_FAVOURITES', 'favourites');
  59. define('COURSE_TIMELINE_HIDDEN', 'hidden');
  60. define('COURSE_CUSTOMFIELD', 'customfield');
  61. define('COURSE_DB_QUERY_LIMIT', 1000);
  62. /** Searching for all courses that have no value for the specified custom field. */
  63. define('COURSE_CUSTOMFIELD_EMPTY', -1);
  64. // Course activity chooser footer default display option.
  65. define('COURSE_CHOOSER_FOOTER_NONE', 'hidden');
  66. // Download course content options.
  67. define('DOWNLOAD_COURSE_CONTENT_DISABLED', 0);
  68. define('DOWNLOAD_COURSE_CONTENT_ENABLED', 1);
  69. define('DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT', 2);
  70. function make_log_url($module, $url) {
  71. switch ($module) {
  72. case 'course':
  73. if (strpos($url, 'report/') === 0) {
  74. // there is only one report type, course reports are deprecated
  75. $url = "/$url";
  76. break;
  77. }
  78. case 'file':
  79. case 'login':
  80. case 'lib':
  81. case 'admin':
  82. case 'category':
  83. case 'mnet course':
  84. if (strpos($url, '../') === 0) {
  85. $url = ltrim($url, '.');
  86. } else {
  87. $url = "/course/$url";
  88. }
  89. break;
  90. case 'calendar':
  91. $url = "/calendar/$url";
  92. break;
  93. case 'user':
  94. case 'blog':
  95. $url = "/$module/$url";
  96. break;
  97. case 'upload':
  98. $url = $url;
  99. break;
  100. case 'coursetags':
  101. $url = '/'.$url;
  102. break;
  103. case 'library':
  104. case '':
  105. $url = '/';
  106. break;
  107. case 'message':
  108. $url = "/message/$url";
  109. break;
  110. case 'notes':
  111. $url = "/notes/$url";
  112. break;
  113. case 'tag':
  114. $url = "/tag/$url";
  115. break;
  116. case 'role':
  117. $url = '/'.$url;
  118. break;
  119. case 'grade':
  120. $url = "/grade/$url";
  121. break;
  122. default:
  123. $url = "/mod/$module/$url";
  124. break;
  125. }
  126. //now let's sanitise urls - there might be some ugly nasties:-(
  127. $parts = explode('?', $url);
  128. $script = array_shift($parts);
  129. if (strpos($script, 'http') === 0) {
  130. $script = clean_param($script, PARAM_URL);
  131. } else {
  132. $script = clean_param($script, PARAM_PATH);
  133. }
  134. $query = '';
  135. if ($parts) {
  136. $query = implode('', $parts);
  137. $query = str_replace('&amp;', '&', $query); // both & and &amp; are stored in db :-|
  138. $parts = explode('&', $query);
  139. $eq = urlencode('=');
  140. foreach ($parts as $key=>$part) {
  141. $part = urlencode(urldecode($part));
  142. $part = str_replace($eq, '=', $part);
  143. $parts[$key] = $part;
  144. }
  145. $query = '?'.implode('&amp;', $parts);
  146. }
  147. return $script.$query;
  148. }
  149. function build_mnet_logs_array($hostid, $course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='',
  150. $modname="", $modid=0, $modaction="", $groupid=0) {
  151. global $CFG, $DB;
  152. // It is assumed that $date is the GMT time of midnight for that day,
  153. // and so the next 86400 seconds worth of logs are printed.
  154. /// Setup for group handling.
  155. // TODO: I don't understand group/context/etc. enough to be able to do
  156. // something interesting with it here
  157. // What is the context of a remote course?
  158. /// If the group mode is separate, and this user does not have editing privileges,
  159. /// then only the user's group can be viewed.
  160. //if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) {
  161. // $groupid = get_current_group($course->id);
  162. //}
  163. /// If this course doesn't have groups, no groupid can be specified.
  164. //else if (!$course->groupmode) {
  165. // $groupid = 0;
  166. //}
  167. $groupid = 0;
  168. $joins = array();
  169. $where = '';
  170. $qry = "SELECT l.*, u.firstname, u.lastname, u.picture
  171. FROM {mnet_log} l
  172. LEFT JOIN {user} u ON l.userid = u.id
  173. WHERE ";
  174. $params = array();
  175. $where .= "l.hostid = :hostid";
  176. $params['hostid'] = $hostid;
  177. // TODO: Is 1 really a magic number referring to the sitename?
  178. if ($course != SITEID || $modid != 0) {
  179. $where .= " AND l.course=:courseid";
  180. $params['courseid'] = $course;
  181. }
  182. if ($modname) {
  183. $where .= " AND l.module = :modname";
  184. $params['modname'] = $modname;
  185. }
  186. if ('site_errors' === $modid) {
  187. $where .= " AND ( l.action='error' OR l.action='infected' )";
  188. } else if ($modid) {
  189. //TODO: This assumes that modids are the same across sites... probably
  190. //not true
  191. $where .= " AND l.cmid = :modid";
  192. $params['modid'] = $modid;
  193. }
  194. if ($modaction) {
  195. $firstletter = substr($modaction, 0, 1);
  196. if ($firstletter == '-') {
  197. $where .= " AND ".$DB->sql_like('l.action', ':modaction', false, true, true);
  198. $params['modaction'] = '%'.substr($modaction, 1).'%';
  199. } else {
  200. $where .= " AND ".$DB->sql_like('l.action', ':modaction', false);
  201. $params['modaction'] = '%'.$modaction.'%';
  202. }
  203. }
  204. if ($user) {
  205. $where .= " AND l.userid = :user";
  206. $params['user'] = $user;
  207. }
  208. if ($date) {
  209. $enddate = $date + 86400;
  210. $where .= " AND l.time > :date AND l.time < :enddate";
  211. $params['date'] = $date;
  212. $params['enddate'] = $enddate;
  213. }
  214. $result = array();
  215. $result['totalcount'] = $DB->count_records_sql("SELECT COUNT('x') FROM {mnet_log} l WHERE $where", $params);
  216. if(!empty($result['totalcount'])) {
  217. $where .= " ORDER BY $order";
  218. $result['logs'] = $DB->get_records_sql("$qry $where", $params, $limitfrom, $limitnum);
  219. } else {
  220. $result['logs'] = array();
  221. }
  222. return $result;
  223. }
  224. /**
  225. * Checks the integrity of the course data.
  226. *
  227. * In summary - compares course_sections.sequence and course_modules.section.
  228. *
  229. * More detailed, checks that:
  230. * - course_sections.sequence contains each module id not more than once in the course
  231. * - for each moduleid from course_sections.sequence the field course_modules.section
  232. * refers to the same section id (this means course_sections.sequence is more
  233. * important if they are different)
  234. * - ($fullcheck only) each module in the course is present in one of
  235. * course_sections.sequence
  236. * - ($fullcheck only) removes non-existing course modules from section sequences
  237. *
  238. * If there are any mismatches, the changes are made and records are updated in DB.
  239. *
  240. * Course cache is NOT rebuilt if there are any errors!
  241. *
  242. * This function is used each time when course cache is being rebuilt with $fullcheck = false
  243. * and in CLI script admin/cli/fix_course_sequence.php with $fullcheck = true
  244. *
  245. * @param int $courseid id of the course
  246. * @param array $rawmods result of funciton {@link get_course_mods()} - containst
  247. * the list of enabled course modules in the course. Retrieved from DB if not specified.
  248. * Argument ignored in cashe of $fullcheck, the list is retrieved form DB anyway.
  249. * @param array $sections records from course_sections table for this course.
  250. * Retrieved from DB if not specified
  251. * @param bool $fullcheck Will add orphaned modules to their sections and remove non-existing
  252. * course modules from sequences. Only to be used in site maintenance mode when we are
  253. * sure that another user is not in the middle of the process of moving/removing a module.
  254. * @param bool $checkonly Only performs the check without updating DB, outputs all errors as debug messages.
  255. * @return array array of messages with found problems. Empty output means everything is ok
  256. */
  257. function course_integrity_check($courseid, $rawmods = null, $sections = null, $fullcheck = false, $checkonly = false) {
  258. global $DB;
  259. $messages = array();
  260. if ($sections === null) {
  261. $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section', 'id,section,sequence');
  262. }
  263. if ($fullcheck) {
  264. // Retrieve all records from course_modules regardless of module type visibility.
  265. $rawmods = $DB->get_records('course_modules', array('course' => $courseid), 'id', 'id,section');
  266. }
  267. if ($rawmods === null) {
  268. $rawmods = get_course_mods($courseid);
  269. }
  270. if (!$fullcheck && (empty($sections) || empty($rawmods))) {
  271. // If either of the arrays is empty, no modules are displayed anyway.
  272. return true;
  273. }
  274. $debuggingprefix = 'Failed integrity check for course ['.$courseid.']. ';
  275. // First make sure that each module id appears in section sequences only once.
  276. // If it appears in several section sequences the last section wins.
  277. // If it appears twice in one section sequence, the first occurence wins.
  278. $modsection = array();
  279. foreach ($sections as $sectionid => $section) {
  280. $sections[$sectionid]->newsequence = $section->sequence;
  281. if (!empty($section->sequence)) {
  282. $sequence = explode(",", $section->sequence);
  283. $sequenceunique = array_unique($sequence);
  284. if (count($sequenceunique) != count($sequence)) {
  285. // Some course module id appears in this section sequence more than once.
  286. ksort($sequenceunique); // Preserve initial order of modules.
  287. $sequence = array_values($sequenceunique);
  288. $sections[$sectionid]->newsequence = join(',', $sequence);
  289. $messages[] = $debuggingprefix.'Sequence for course section ['.
  290. $sectionid.'] is "'.$sections[$sectionid]->sequence.'", must be "'.$sections[$sectionid]->newsequence.'"';
  291. }
  292. foreach ($sequence as $cmid) {
  293. if (array_key_exists($cmid, $modsection) && isset($rawmods[$cmid])) {
  294. // Some course module id appears to be in more than one section's sequences.
  295. $wrongsectionid = $modsection[$cmid];
  296. $sections[$wrongsectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$wrongsectionid]->newsequence. ','), ',');
  297. $messages[] = $debuggingprefix.'Course module ['.$cmid.'] must be removed from sequence of section ['.
  298. $wrongsectionid.'] because it is also present in sequence of section ['.$sectionid.']';
  299. }
  300. $modsection[$cmid] = $sectionid;
  301. }
  302. }
  303. }
  304. // Add orphaned modules to their sections if they exist or to section 0 otherwise.
  305. if ($fullcheck) {
  306. foreach ($rawmods as $cmid => $mod) {
  307. if (!isset($modsection[$cmid])) {
  308. // This is a module that is not mentioned in course_section.sequence at all.
  309. // Add it to the section $mod->section or to the last available section.
  310. if ($mod->section && isset($sections[$mod->section])) {
  311. $modsection[$cmid] = $mod->section;
  312. } else {
  313. $firstsection = reset($sections);
  314. $modsection[$cmid] = $firstsection->id;
  315. }
  316. $sections[$modsection[$cmid]]->newsequence = trim($sections[$modsection[$cmid]]->newsequence.','.$cmid, ',');
  317. $messages[] = $debuggingprefix.'Course module ['.$cmid.'] is missing from sequence of section ['.
  318. $modsection[$cmid].']';
  319. }
  320. }
  321. foreach ($modsection as $cmid => $sectionid) {
  322. if (!isset($rawmods[$cmid])) {
  323. // Section $sectionid refers to module id that does not exist.
  324. $sections[$sectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$sectionid]->newsequence.','), ',');
  325. $messages[] = $debuggingprefix.'Course module ['.$cmid.
  326. '] does not exist but is present in the sequence of section ['.$sectionid.']';
  327. }
  328. }
  329. }
  330. // Update changed sections.
  331. if (!$checkonly && !empty($messages)) {
  332. foreach ($sections as $sectionid => $section) {
  333. if ($section->newsequence !== $section->sequence) {
  334. $DB->update_record('course_sections', array('id' => $sectionid, 'sequence' => $section->newsequence));
  335. }
  336. }
  337. }
  338. // Now make sure that all modules point to the correct sections.
  339. foreach ($rawmods as $cmid => $mod) {
  340. if (isset($modsection[$cmid]) && $modsection[$cmid] != $mod->section) {
  341. if (!$checkonly) {
  342. $DB->update_record('course_modules', array('id' => $cmid, 'section' => $modsection[$cmid]));
  343. }
  344. $messages[] = $debuggingprefix.'Course module ['.$cmid.
  345. '] points to section ['.$mod->section.'] instead of ['.$modsection[$cmid].']';
  346. }
  347. }
  348. return $messages;
  349. }
  350. /**
  351. * For a given course, returns an array of course activity objects
  352. * Each item in the array contains he following properties:
  353. */
  354. function get_array_of_activities($courseid) {
  355. // cm - course module id
  356. // mod - name of the module (eg forum)
  357. // section - the number of the section (eg week or topic)
  358. // name - the name of the instance
  359. // visible - is the instance visible or not
  360. // groupingid - grouping id
  361. // extra - contains extra string to include in any link
  362. global $CFG, $DB;
  363. $course = $DB->get_record('course', array('id'=>$courseid));
  364. if (empty($course)) {
  365. throw new moodle_exception('courseidnotfound');
  366. }
  367. $mod = array();
  368. $rawmods = get_course_mods($courseid);
  369. if (empty($rawmods)) {
  370. return $mod; // always return array
  371. }
  372. $courseformat = course_get_format($course);
  373. if ($sections = $DB->get_records('course_sections', array('course' => $courseid),
  374. 'section ASC', 'id,section,sequence,visible')) {
  375. // First check and correct obvious mismatches between course_sections.sequence and course_modules.section.
  376. if ($errormessages = course_integrity_check($courseid, $rawmods, $sections)) {
  377. debugging(join('<br>', $errormessages));
  378. $rawmods = get_course_mods($courseid);
  379. $sections = $DB->get_records('course_sections', array('course' => $courseid),
  380. 'section ASC', 'id,section,sequence,visible');
  381. }
  382. // Build array of activities.
  383. foreach ($sections as $section) {
  384. if (!empty($section->sequence)) {
  385. $sequence = explode(",", $section->sequence);
  386. foreach ($sequence as $seq) {
  387. if (empty($rawmods[$seq])) {
  388. continue;
  389. }
  390. // Adjust visibleoncoursepage, value in DB may not respect format availability.
  391. $rawmods[$seq]->visibleoncoursepage = (!$rawmods[$seq]->visible
  392. || $rawmods[$seq]->visibleoncoursepage
  393. || empty($CFG->allowstealth)
  394. || !$courseformat->allow_stealth_module_visibility($rawmods[$seq], $section)) ? 1 : 0;
  395. // Create an object that will be cached.
  396. $mod[$seq] = new stdClass();
  397. $mod[$seq]->id = $rawmods[$seq]->instance;
  398. $mod[$seq]->cm = $rawmods[$seq]->id;
  399. $mod[$seq]->mod = $rawmods[$seq]->modname;
  400. // Oh dear. Inconsistent names left here for backward compatibility.
  401. $mod[$seq]->section = $section->section;
  402. $mod[$seq]->sectionid = $rawmods[$seq]->section;
  403. $mod[$seq]->module = $rawmods[$seq]->module;
  404. $mod[$seq]->added = $rawmods[$seq]->added;
  405. $mod[$seq]->score = $rawmods[$seq]->score;
  406. $mod[$seq]->idnumber = $rawmods[$seq]->idnumber;
  407. $mod[$seq]->visible = $rawmods[$seq]->visible;
  408. $mod[$seq]->visibleoncoursepage = $rawmods[$seq]->visibleoncoursepage;
  409. $mod[$seq]->visibleold = $rawmods[$seq]->visibleold;
  410. $mod[$seq]->groupmode = $rawmods[$seq]->groupmode;
  411. $mod[$seq]->groupingid = $rawmods[$seq]->groupingid;
  412. $mod[$seq]->indent = $rawmods[$seq]->indent;
  413. $mod[$seq]->completion = $rawmods[$seq]->completion;
  414. $mod[$seq]->extra = "";
  415. $mod[$seq]->completiongradeitemnumber =
  416. $rawmods[$seq]->completiongradeitemnumber;
  417. $mod[$seq]->completionpassgrade = $rawmods[$seq]->completionpassgrade;
  418. $mod[$seq]->completionview = $rawmods[$seq]->completionview;
  419. $mod[$seq]->completionexpected = $rawmods[$seq]->completionexpected;
  420. $mod[$seq]->showdescription = $rawmods[$seq]->showdescription;
  421. $mod[$seq]->availability = $rawmods[$seq]->availability;
  422. $mod[$seq]->deletioninprogress = $rawmods[$seq]->deletioninprogress;
  423. $modname = $mod[$seq]->mod;
  424. $functionname = $modname."_get_coursemodule_info";
  425. if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
  426. continue;
  427. }
  428. include_once("$CFG->dirroot/mod/$modname/lib.php");
  429. if ($hasfunction = function_exists($functionname)) {
  430. if ($info = $functionname($rawmods[$seq])) {
  431. if (!empty($info->icon)) {
  432. $mod[$seq]->icon = $info->icon;
  433. }
  434. if (!empty($info->iconcomponent)) {
  435. $mod[$seq]->iconcomponent = $info->iconcomponent;
  436. }
  437. if (!empty($info->name)) {
  438. $mod[$seq]->name = $info->name;
  439. }
  440. if ($info instanceof cached_cm_info) {
  441. // When using cached_cm_info you can include three new fields
  442. // that aren't available for legacy code
  443. if (!empty($info->content)) {
  444. $mod[$seq]->content = $info->content;
  445. }
  446. if (!empty($info->extraclasses)) {
  447. $mod[$seq]->extraclasses = $info->extraclasses;
  448. }
  449. if (!empty($info->iconurl)) {
  450. // Convert URL to string as it's easier to store. Also serialized object contains \0 byte and can not be written to Postgres DB.
  451. $url = new moodle_url($info->iconurl);
  452. $mod[$seq]->iconurl = $url->out(false);
  453. }
  454. if (!empty($info->onclick)) {
  455. $mod[$seq]->onclick = $info->onclick;
  456. }
  457. if (!empty($info->customdata)) {
  458. $mod[$seq]->customdata = $info->customdata;
  459. }
  460. } else {
  461. // When using a stdclass, the (horrible) deprecated ->extra field
  462. // is available for BC
  463. if (!empty($info->extra)) {
  464. $mod[$seq]->extra = $info->extra;
  465. }
  466. }
  467. }
  468. }
  469. // When there is no modname_get_coursemodule_info function,
  470. // but showdescriptions is enabled, then we use the 'intro'
  471. // and 'introformat' fields in the module table
  472. if (!$hasfunction && $rawmods[$seq]->showdescription) {
  473. if ($modvalues = $DB->get_record($rawmods[$seq]->modname,
  474. array('id' => $rawmods[$seq]->instance), 'name, intro, introformat')) {
  475. // Set content from intro and introformat. Filters are disabled
  476. // because we filter it with format_text at display time
  477. $mod[$seq]->content = format_module_intro($rawmods[$seq]->modname,
  478. $modvalues, $rawmods[$seq]->id, false);
  479. // To save making another query just below, put name in here
  480. $mod[$seq]->name = $modvalues->name;
  481. }
  482. }
  483. if (!isset($mod[$seq]->name)) {
  484. $mod[$seq]->name = $DB->get_field($rawmods[$seq]->modname, "name", array("id"=>$rawmods[$seq]->instance));
  485. }
  486. // Minimise the database size by unsetting default options when they are
  487. // 'empty'. This list corresponds to code in the cm_info constructor.
  488. foreach (array('idnumber', 'groupmode', 'groupingid',
  489. 'indent', 'completion', 'extra', 'extraclasses', 'iconurl', 'onclick', 'content',
  490. 'icon', 'iconcomponent', 'customdata', 'availability', 'completionview',
  491. 'completionexpected', 'score', 'showdescription', 'deletioninprogress') as $property) {
  492. if (property_exists($mod[$seq], $property) &&
  493. empty($mod[$seq]->{$property})) {
  494. unset($mod[$seq]->{$property});
  495. }
  496. }
  497. // Special case: this value is usually set to null, but may be 0
  498. if (property_exists($mod[$seq], 'completiongradeitemnumber') &&
  499. is_null($mod[$seq]->completiongradeitemnumber)) {
  500. unset($mod[$seq]->completiongradeitemnumber);
  501. }
  502. }
  503. }
  504. }
  505. }
  506. return $mod;
  507. }
  508. /**
  509. * Returns the localised human-readable names of all used modules
  510. *
  511. * @param bool $plural if true returns the plural forms of the names
  512. * @return array where key is the module name (component name without 'mod_') and
  513. * the value is the human-readable string. Array sorted alphabetically by value
  514. */
  515. function get_module_types_names($plural = false) {
  516. static $modnames = null;
  517. global $DB, $CFG;
  518. if ($modnames === null) {
  519. $modnames = array(0 => array(), 1 => array());
  520. if ($allmods = $DB->get_records("modules")) {
  521. foreach ($allmods as $mod) {
  522. if (file_exists("$CFG->dirroot/mod/$mod->name/lib.php") && $mod->visible) {
  523. $modnames[0][$mod->name] = get_string("modulename", "$mod->name", null, true);
  524. $modnames[1][$mod->name] = get_string("modulenameplural", "$mod->name", null, true);
  525. }
  526. }
  527. }
  528. }
  529. return $modnames[(int)$plural];
  530. }
  531. /**
  532. * Set highlighted section. Only one section can be highlighted at the time.
  533. *
  534. * @param int $courseid course id
  535. * @param int $marker highlight section with this number, 0 means remove higlightin
  536. * @return void
  537. */
  538. function course_set_marker($courseid, $marker) {
  539. global $DB, $COURSE;
  540. $DB->set_field("course", "marker", $marker, array('id' => $courseid));
  541. if ($COURSE && $COURSE->id == $courseid) {
  542. $COURSE->marker = $marker;
  543. }
  544. core_courseformat\base::reset_course_cache($courseid);
  545. course_modinfo::clear_instance_cache($courseid);
  546. }
  547. /**
  548. * For a given course section, marks it visible or hidden,
  549. * and does the same for every activity in that section
  550. *
  551. * @param int $courseid course id
  552. * @param int $sectionnumber The section number to adjust
  553. * @param int $visibility The new visibility
  554. * @return array A list of resources which were hidden in the section
  555. */
  556. function set_section_visible($courseid, $sectionnumber, $visibility) {
  557. global $DB;
  558. $resourcestotoggle = array();
  559. if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) {
  560. course_update_section($courseid, $section, array('visible' => $visibility));
  561. // Determine which modules are visible for AJAX update
  562. $modules = !empty($section->sequence) ? explode(',', $section->sequence) : array();
  563. if (!empty($modules)) {
  564. list($insql, $params) = $DB->get_in_or_equal($modules);
  565. $select = 'id ' . $insql . ' AND visible = ?';
  566. array_push($params, $visibility);
  567. if (!$visibility) {
  568. $select .= ' AND visibleold = 1';
  569. }
  570. $resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params);
  571. }
  572. }
  573. return $resourcestotoggle;
  574. }
  575. /**
  576. * Return the course category context for the category with id $categoryid, except
  577. * that if $categoryid is 0, return the system context.
  578. *
  579. * @param integer $categoryid a category id or 0.
  580. * @return context the corresponding context
  581. */
  582. function get_category_or_system_context($categoryid) {
  583. if ($categoryid) {
  584. return context_coursecat::instance($categoryid, IGNORE_MISSING);
  585. } else {
  586. return context_system::instance();
  587. }
  588. }
  589. /**
  590. * Print the buttons relating to course requests.
  591. *
  592. * @param context $context current page context.
  593. */
  594. function print_course_request_buttons($context) {
  595. global $CFG, $DB, $OUTPUT;
  596. if (empty($CFG->enablecourserequests)) {
  597. return;
  598. }
  599. if (course_request::can_request($context)) {
  600. // Print a button to request a new course.
  601. $params = [];
  602. if ($context instanceof context_coursecat) {
  603. $params['category'] = $context->instanceid;
  604. }
  605. echo $OUTPUT->single_button(new moodle_url('/course/request.php', $params),
  606. get_string('requestcourse'), 'get');
  607. }
  608. /// Print a button to manage pending requests
  609. if (has_capability('moodle/site:approvecourse', $context)) {
  610. $disabled = !$DB->record_exists('course_request', array());
  611. echo $OUTPUT->single_button(new moodle_url('/course/pending.php'), get_string('coursespending'), 'get', array('disabled' => $disabled));
  612. }
  613. }
  614. /**
  615. * Does the user have permission to edit things in this category?
  616. *
  617. * @param integer $categoryid The id of the category we are showing, or 0 for system context.
  618. * @return boolean has_any_capability(array(...), ...); in the appropriate context.
  619. */
  620. function can_edit_in_category($categoryid = 0) {
  621. $context = get_category_or_system_context($categoryid);
  622. return has_any_capability(array('moodle/category:manage', 'moodle/course:create'), $context);
  623. }
  624. /// MODULE FUNCTIONS /////////////////////////////////////////////////////////////////
  625. function add_course_module($mod) {
  626. global $DB;
  627. $mod->added = time();
  628. unset($mod->id);
  629. $cmid = $DB->insert_record("course_modules", $mod);
  630. rebuild_course_cache($mod->course, true);
  631. return $cmid;
  632. }
  633. /**
  634. * Creates a course section and adds it to the specified position
  635. *
  636. * @param int|stdClass $courseorid course id or course object
  637. * @param int $position position to add to, 0 means to the end. If position is greater than
  638. * number of existing secitons, the section is added to the end. This will become sectionnum of the
  639. * new section. All existing sections at this or bigger position will be shifted down.
  640. * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist
  641. * @return stdClass created section object
  642. */
  643. function course_create_section($courseorid, $position = 0, $skipcheck = false) {
  644. global $DB;
  645. $courseid = is_object($courseorid) ? $courseorid->id : $courseorid;
  646. // Find the last sectionnum among existing sections.
  647. if ($skipcheck) {
  648. $lastsection = $position - 1;
  649. } else {
  650. $lastsection = (int)$DB->get_field_sql('SELECT max(section) from {course_sections} WHERE course = ?', [$courseid]);
  651. }
  652. // First add section to the end.
  653. $cw = new stdClass();
  654. $cw->course = $courseid;
  655. $cw->section = $lastsection + 1;
  656. $cw->summary = '';
  657. $cw->summaryformat = FORMAT_HTML;
  658. $cw->sequence = '';
  659. $cw->name = null;
  660. $cw->visible = 1;
  661. $cw->availability = null;
  662. $cw->timemodified = time();
  663. $cw->id = $DB->insert_record("course_sections", $cw);
  664. // Now move it to the specified position.
  665. if ($position > 0 && $position <= $lastsection) {
  666. $course = is_object($courseorid) ? $courseorid : get_course($courseorid);
  667. move_section_to($course, $cw->section, $position, true);
  668. $cw->section = $position;
  669. }
  670. core\event\course_section_created::create_from_section($cw)->trigger();
  671. rebuild_course_cache($courseid, true);
  672. return $cw;
  673. }
  674. /**
  675. * Creates missing course section(s) and rebuilds course cache
  676. *
  677. * @param int|stdClass $courseorid course id or course object
  678. * @param int|array $sections list of relative section numbers to create
  679. * @return bool if there were any sections created
  680. */
  681. function course_create_sections_if_missing($courseorid, $sections) {
  682. if (!is_array($sections)) {
  683. $sections = array($sections);
  684. }
  685. $existing = array_keys(get_fast_modinfo($courseorid)->get_section_info_all());
  686. if ($newsections = array_diff($sections, $existing)) {
  687. foreach ($newsections as $sectionnum) {
  688. course_create_section($courseorid, $sectionnum, true);
  689. }
  690. return true;
  691. }
  692. return false;
  693. }
  694. /**
  695. * Adds an existing module to the section
  696. *
  697. * Updates both tables {course_sections} and {course_modules}
  698. *
  699. * Note: This function does not use modinfo PROVIDED that the section you are
  700. * adding the module to already exists. If the section does not exist, it will
  701. * build modinfo if necessary and create the section.
  702. *
  703. * @param int|stdClass $courseorid course id or course object
  704. * @param int $cmid id of the module already existing in course_modules table
  705. * @param int $sectionnum relative number of the section (field course_sections.section)
  706. * If section does not exist it will be created
  707. * @param int|stdClass $beforemod id or object with field id corresponding to the module
  708. * before which the module needs to be included. Null for inserting in the
  709. * end of the section
  710. * @return int The course_sections ID where the module is inserted
  711. */
  712. function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod = null) {
  713. global $DB, $COURSE;
  714. if (is_object($beforemod)) {
  715. $beforemod = $beforemod->id;
  716. }
  717. if (is_object($courseorid)) {
  718. $courseid = $courseorid->id;
  719. } else {
  720. $courseid = $courseorid;
  721. }
  722. // Do not try to use modinfo here, there is no guarantee it is valid!
  723. $section = $DB->get_record('course_sections',
  724. array('course' => $courseid, 'section' => $sectionnum), '*', IGNORE_MISSING);
  725. if (!$section) {
  726. // This function call requires modinfo.
  727. course_create_sections_if_missing($courseorid, $sectionnum);
  728. $section = $DB->get_record('course_sections',
  729. array('course' => $courseid, 'section' => $sectionnum), '*', MUST_EXIST);
  730. }
  731. $modarray = explode(",", trim($section->sequence));
  732. if (empty($section->sequence)) {
  733. $newsequence = "$cmid";
  734. } else if ($beforemod && ($key = array_keys($modarray, $beforemod))) {
  735. $insertarray = array($cmid, $beforemod);
  736. array_splice($modarray, $key[0], 1, $insertarray);
  737. $newsequence = implode(",", $modarray);
  738. } else {
  739. $newsequence = "$section->sequence,$cmid";
  740. }
  741. $DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id));
  742. $DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid));
  743. if (is_object($courseorid)) {
  744. rebuild_course_cache($courseorid->id, true);
  745. } else {
  746. rebuild_course_cache($courseorid, true);
  747. }
  748. return $section->id; // Return course_sections ID that was used.
  749. }
  750. /**
  751. * Change the group mode of a course module.
  752. *
  753. * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
  754. * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
  755. *
  756. * @param int $id course module ID.
  757. * @param int $groupmode the new groupmode value.
  758. * @return bool True if the $groupmode was updated.
  759. */
  760. function set_coursemodule_groupmode($id, $groupmode) {
  761. global $DB;
  762. $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST);
  763. if ($cm->groupmode != $groupmode) {
  764. $DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id));
  765. rebuild_course_cache($cm->course, true);
  766. }
  767. return ($cm->groupmode != $groupmode);
  768. }
  769. function set_coursemodule_idnumber($id, $idnumber) {
  770. global $DB;
  771. $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST);
  772. if ($cm->idnumber != $idnumber) {
  773. $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
  774. rebuild_course_cache($cm->course, true);
  775. }
  776. return ($cm->idnumber != $idnumber);
  777. }
  778. /**
  779. * Set the visibility of a module and inherent properties.
  780. *
  781. * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs
  782. * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}.
  783. *
  784. * From 2.4 the parameter $prevstateoverrides has been removed, the logic it triggered
  785. * has been moved to {@link set_section_visible()} which was the only place from which
  786. * the parameter was used.
  787. *
  788. * @param int $id of the module
  789. * @param int $visible state of the module
  790. * @param int $visibleoncoursepage state of the module on the course page
  791. * @return bool false when the module was not found, true otherwise
  792. */
  793. function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1) {
  794. global $DB, $CFG;
  795. require_once($CFG->libdir.'/gradelib.php');
  796. require_once($CFG->dirroot.'/calendar/lib.php');
  797. if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) {
  798. return false;
  799. }
  800. // Create events and propagate visibility to associated grade items if the value has changed.
  801. // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades.
  802. if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) {
  803. return true;
  804. }
  805. if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) {
  806. return false;
  807. }
  808. if (($cm->visible != $visible) &&
  809. ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) {
  810. foreach($events as $event) {
  811. if ($visible) {
  812. $event = new calendar_event($event);
  813. $event->toggle_visibility(true);
  814. } else {
  815. $event = new calendar_event($event);
  816. $event->toggle_visibility(false);
  817. }
  818. }
  819. }
  820. // Updating visible and visibleold to keep them in sync. Only changing a section visibility will
  821. // affect visibleold to allow for an original visibility restore. See set_section_visible().
  822. $cminfo = new stdClass();
  823. $cminfo->id = $id;
  824. $cminfo->visible = $visible;
  825. $cminfo->visibleoncoursepage = $visibleoncoursepage;
  826. $cminfo->visibleold = $visible;
  827. $DB->update_record('course_modules', $cminfo);
  828. // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there.
  829. // Note that this must be done after updating the row in course_modules, in case
  830. // the modules grade_item_update function needs to access $cm->visible.
  831. if ($cm->visible != $visible &&
  832. plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) &&
  833. component_callback_exists('mod_' . $modulename, 'grade_item_update')) {
  834. $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST);
  835. component_callback('mod_' . $modulename, 'grade_item_update', array($instance));
  836. } else if ($cm->visible != $visible) {
  837. $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course));
  838. if ($grade_items) {
  839. foreach ($grade_items as $grade_item) {
  840. $grade_item->set_hidden(!$visible);
  841. }
  842. }
  843. }
  844. rebuild_course_cache($cm->course, true);
  845. return true;
  846. }
  847. /**
  848. * Changes the course module name
  849. *
  850. * @param int $id course module id
  851. * @param string $name new value for a name
  852. * @return bool whether a change was made
  853. */
  854. function set_coursemodule_name($id, $name) {
  855. global $CFG, $DB;
  856. require_once($CFG->libdir . '/gradelib.php');
  857. $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST);
  858. $module = new \stdClass();
  859. $module->id = $cm->instance;
  860. // Escape strings as they would be by mform.
  861. if (!empty($CFG->formatstringstriptags)) {
  862. $module->name = clean_param($name, PARAM_TEXT);
  863. } else {
  864. $module->name = clean_param($name, PARAM_CLEANHTML);
  865. }
  866. if ($module->name === $cm->name || strval($module->name) === '') {
  867. return false;
  868. }
  869. if (\core_text::strlen($module->name) > 255) {
  870. throw new \moodle_exception('maximumchars', 'moodle', '', 255);
  871. }
  872. $module->timemodified = time();
  873. $DB->update_record($cm->modname, $module);
  874. $cm->name = $module->name;
  875. \core\event\course_module_updated::create_from_cm($cm)->trigger();
  876. rebuild_course_cache($cm->course, true);
  877. // Attempt to update the grade item if relevant.
  878. $grademodule = $DB->get_record($cm->modname, array('id' => $cm->instance));
  879. $grademodule->cmidnumber = $cm->idnumber;
  880. $grademodule->modname = $cm->modname;
  881. grade_update_mod_grades($grademodule);
  882. // Update calendar events with the new name.
  883. course_module_update_calendar_events($cm->modname, $grademodule, $cm);
  884. return true;
  885. }
  886. /**
  887. * This function will handle the whole deletion process of a module. This includes calling
  888. * the modules delete_instance function, deleting files, events, grades, conditional data,
  889. * the data in the course_module and course_sections table and adding a module deletion
  890. * event to the DB.
  891. *
  892. * @param int $cmid the course module id
  893. * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook.
  894. * @throws moodle_exception
  895. * @since Moodle 2.5
  896. */
  897. function course_delete_module($cmid, $async = false) {
  898. // Check the 'course_module_background_deletion_recommended' hook first.
  899. // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested.
  900. // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it.
  901. // It's up to plugins to handle things like whether or not they are enabled.
  902. if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
  903. foreach ($pluginsfunction as $plugintype => $plugins) {
  904. foreach ($plugins as $pluginfunction) {
  905. if ($pluginfunction()) {
  906. return course_module_flag_for_async_deletion($cmid);
  907. }
  908. }
  909. }
  910. }
  911. global $CFG, $DB;
  912. require_once($CFG->libdir.'/gradelib.php');
  913. require_once($CFG->libdir.'/questionlib.php');
  914. require_once($CFG->dirroot.'/blog/lib.php');
  915. require_once($CFG->dirroot.'/calendar/lib.php');
  916. // Get the course module.
  917. if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
  918. return true;
  919. }
  920. // Get the module context.
  921. $modcontext = context_module::instance($cm->id);
  922. // Get the course module name.
  923. $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
  924. // Get the file location of the delete_instance function for this module.
  925. $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
  926. // Include the file required to call the delete_instance function for this module.
  927. if (file_exists($modlib)) {
  928. require_once($modlib);
  929. } else {
  930. throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null,
  931. "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
  932. }
  933. $deleteinstancefunction = $modulename . '_delete_instance';
  934. // Ensure the delete_instance function exists for this module.
  935. if (!function_exists($deleteinstancefunction)) {
  936. throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
  937. "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
  938. }
  939. // Allow plugins to use this course module before we completely delete it.
  940. if ($pluginsfunction = get_plugins_with_function('pre_course_module_delete')) {
  941. foreach ($pluginsfunction as $plugintype => $plugins) {
  942. foreach ($plugins as $pluginfunction) {
  943. $pluginfunction($cm);
  944. }
  945. }
  946. }
  947. question_delete_activity($cm);
  948. // Call the delete_instance function, if it returns false throw an exception.
  949. if (!$deleteinstancefunction($cm->instance)) {
  950. throw new moodle_exception('cannotdeletemoduleinstance', '', '', null,
  951. "Cannot delete the module $modulename (instance).");
  952. }
  953. // Remove all module files in case modules forget to do that.
  954. $fs = get_file_storage();
  955. $fs->delete_area_files($modcontext->id);
  956. // Delete events from calendar.
  957. if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) {
  958. $coursecontext = context_course::instance($cm->course);
  959. foreach($events as $event) {
  960. $event->context = $coursecontext;
  961. $calendarevent = calendar_event::load($event);
  962. $calendarevent->delete();
  963. }
  964. }
  965. // Delete grade items, outcome items and grades attached to modules.
  966. if ($grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $modulename,
  967. 'iteminstance' => $cm->instance, 'courseid' => $cm->course))) {
  968. foreach ($grade_items as $grade_item) {
  969. $grade_item->delete('moddelete');
  970. }
  971. }
  972. // Delete associated blogs and blog tag instances.
  973. blog_remove_associations_for_module($modcontext->id);
  974. // Delete completion and availability data; it is better to do this even if the
  975. // features are not turned on, in case they were turned on previously (these will be
  976. // very quick on an empty table).
  977. $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id));
  978. $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id,
  979. 'course' => $cm->course,
  980. 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY));
  981. // Delete all tag instances associated with the instance of this module.
  982. core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id);
  983. core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id);
  984. // Notify the competency subsystem.
  985. \core_competency\api::hook_course_module_deleted($cm);
  986. // Delete the context.
  987. context_helper::delete_instance(CONTEXT_MODULE, $cm->id);
  988. // Delete the module from the course_modules table.
  989. $DB->delete_records('course_modules', array('id' => $cm->id));
  990. // Delete module from that section.
  991. if (!delete_mod_from_section($cm->id, $cm->section)) {
  992. throw new moodle_exception('cannotdeletemodulefromsection', '', '', null,
  993. "Cannot delete the module $modulename (instance) from section.");
  994. }
  995. // Trigger event for course module delete action.
  996. $event = \core\event\course_module_deleted::create(array(
  997. 'courseid' => $cm->course,
  998. 'context' => $modcontext,
  999. 'objectid' => $cm->id,
  1000. 'other' => array(
  1001. 'modulename' => $modulename,
  1002. 'instanceid' => $cm->instance,
  1003. )
  1004. ));
  1005. $event->add_record_snapshot('course_modules', $cm);
  1006. $event->trigger();
  1007. rebuild_course_cache($cm->course, true);
  1008. }
  1009. /**
  1010. * Schedule a course module for deletion in the background using an adhoc task.
  1011. *
  1012. * This method should not be called directly. Instead, please use course_delete_module($cmid, true), to denote async deletion.
  1013. * The real deletion of the module is handled by the task, which calls 'course_delete_module($cmid)'.
  1014. *
  1015. * @param int $cmid the course module id.
  1016. * @return bool whether the module was successfully scheduled for deletion.
  1017. * @throws \moodle_exception
  1018. */
  1019. function course_module_flag_for_async_deletion($cmid) {
  1020. global $CFG, $DB, $USER;
  1021. require_once($CFG->libdir.'/gradelib.php');
  1022. require_once($CFG->libdir.'/questionlib.php');
  1023. require_once($CFG->dirroot.'/blog/lib.php');
  1024. require_once($CFG->dirroot.'/calendar/lib.php');
  1025. // Get the course module.
  1026. if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) {
  1027. return true;
  1028. }
  1029. // We need to be reasonably certain the deletion is going to succeed before we background the process.
  1030. // Make the necessary delete_instance checks, etc. before proceeding further. Throw exceptions if required.
  1031. // Get the course module name.
  1032. $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST);
  1033. // Get the file location of the delete_instance function for this module.
  1034. $modlib = "$CFG->dirroot/mod/$modulename/lib.php";
  1035. // Include the file required to call the delete_instance function for this module.
  1036. if (file_exists($modlib)) {
  1037. require_once($modlib);
  1038. } else {
  1039. throw new \moodle_exception('cannotdeletemodulemissinglib', '', '', null,
  1040. "Cannot delete this module as the file mod/$modulename/lib.php is missing.");
  1041. }
  1042. $deleteinstancefunction = $modulename . '_delete_instance';
  1043. // Ensure the delete_instance function exists for this module.
  1044. if (!function_exists($deleteinstancefunction)) {
  1045. throw new \moodle_exception('cannotdeletemodulemissingfunc', '', '', null,
  1046. "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php.");
  1047. }
  1048. // We are going to defer the deletion as we can't be sure how long the module's pre_delete code will run for.
  1049. $cm->deletioninprogress = '1';
  1050. $DB->update_record('course_modules', $cm);
  1051. // Create an adhoc task for the deletion of the course module. The task takes an array of course modules for removal.
  1052. $removaltask = new \core_course\task\co

Large files files are truncated, but you can click here to view the full file