Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]
1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 /** 18 * 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 22 * @package core_course 23 */ 24 25 defined('MOODLE_INTERNAL') || die; 26 27 use core_course\external\course_summary_exporter; 28 use core_courseformat\base as course_format; 29 use core\output\local\action_menu\subpanel as action_menu_subpanel; 30 31 require_once($CFG->libdir.'/completionlib.php'); 32 require_once($CFG->libdir.'/filelib.php'); 33 require_once($CFG->libdir.'/datalib.php'); 34 require_once($CFG->dirroot.'/course/format/lib.php'); 35 36 define('COURSE_MAX_LOGS_PER_PAGE', 1000); // Records. 37 define('COURSE_MAX_RECENT_PERIOD', 172800); // Two days, in seconds. 38 39 /** 40 * Number of courses to display when summaries are included. 41 * @var int 42 * @deprecated since 2.4, use $CFG->courseswithsummarieslimit instead. 43 */ 44 define('COURSE_MAX_SUMMARIES_PER_PAGE', 10); 45 46 // Max courses in log dropdown before switching to optional. 47 define('COURSE_MAX_COURSES_PER_DROPDOWN', 1000); 48 // Max users in log dropdown before switching to optional. 49 define('COURSE_MAX_USERS_PER_DROPDOWN', 1000); 50 define('FRONTPAGENEWS', '0'); 51 define('FRONTPAGECATEGORYNAMES', '2'); 52 define('FRONTPAGECATEGORYCOMBO', '4'); 53 define('FRONTPAGEENROLLEDCOURSELIST', '5'); 54 define('FRONTPAGEALLCOURSELIST', '6'); 55 define('FRONTPAGECOURSESEARCH', '7'); 56 // Important! Replaced with $CFG->frontpagecourselimit - maximum number of courses displayed on the frontpage. 57 define('EXCELROWS', 65535); 58 define('FIRSTUSEDEXCELROW', 3); 59 60 define('MOD_CLASS_ACTIVITY', 0); 61 define('MOD_CLASS_RESOURCE', 1); 62 63 define('COURSE_TIMELINE_ALLINCLUDINGHIDDEN', 'allincludinghidden'); 64 define('COURSE_TIMELINE_ALL', 'all'); 65 define('COURSE_TIMELINE_PAST', 'past'); 66 define('COURSE_TIMELINE_INPROGRESS', 'inprogress'); 67 define('COURSE_TIMELINE_FUTURE', 'future'); 68 define('COURSE_TIMELINE_SEARCH', 'search'); 69 define('COURSE_FAVOURITES', 'favourites'); 70 define('COURSE_TIMELINE_HIDDEN', 'hidden'); 71 define('COURSE_CUSTOMFIELD', 'customfield'); 72 define('COURSE_DB_QUERY_LIMIT', 1000); 73 /** Searching for all courses that have no value for the specified custom field. */ 74 define('COURSE_CUSTOMFIELD_EMPTY', -1); 75 76 // Course activity chooser footer default display option. 77 define('COURSE_CHOOSER_FOOTER_NONE', 'hidden'); 78 79 // Download course content options. 80 define('DOWNLOAD_COURSE_CONTENT_DISABLED', 0); 81 define('DOWNLOAD_COURSE_CONTENT_ENABLED', 1); 82 define('DOWNLOAD_COURSE_CONTENT_SITE_DEFAULT', 2); 83 84 function make_log_url($module, $url) { 85 switch ($module) { 86 case 'course': 87 if (strpos($url, 'report/') === 0) { 88 // there is only one report type, course reports are deprecated 89 $url = "/$url"; 90 break; 91 } 92 case 'file': 93 case 'login': 94 case 'lib': 95 case 'admin': 96 case 'category': 97 case 'mnet course': 98 if (strpos($url, '../') === 0) { 99 $url = ltrim($url, '.'); 100 } else { 101 $url = "/course/$url"; 102 } 103 break; 104 case 'calendar': 105 $url = "/calendar/$url"; 106 break; 107 case 'user': 108 case 'blog': 109 $url = "/$module/$url"; 110 break; 111 case 'upload': 112 $url = $url; 113 break; 114 case 'coursetags': 115 $url = '/'.$url; 116 break; 117 case 'library': 118 case '': 119 $url = '/'; 120 break; 121 case 'message': 122 $url = "/message/$url"; 123 break; 124 case 'notes': 125 $url = "/notes/$url"; 126 break; 127 case 'tag': 128 $url = "/tag/$url"; 129 break; 130 case 'role': 131 $url = '/'.$url; 132 break; 133 case 'grade': 134 $url = "/grade/$url"; 135 break; 136 default: 137 $url = "/mod/$module/$url"; 138 break; 139 } 140 141 //now let's sanitise urls - there might be some ugly nasties:-( 142 $parts = explode('?', $url); 143 $script = array_shift($parts); 144 if (strpos($script, 'http') === 0) { 145 $script = clean_param($script, PARAM_URL); 146 } else { 147 $script = clean_param($script, PARAM_PATH); 148 } 149 150 $query = ''; 151 if ($parts) { 152 $query = implode('', $parts); 153 $query = str_replace('&', '&', $query); // both & and & are stored in db :-| 154 $parts = explode('&', $query); 155 $eq = urlencode('='); 156 foreach ($parts as $key=>$part) { 157 $part = urlencode(urldecode($part)); 158 $part = str_replace($eq, '=', $part); 159 $parts[$key] = $part; 160 } 161 $query = '?'.implode('&', $parts); 162 } 163 164 return $script.$query; 165 } 166 167 168 function build_mnet_logs_array($hostid, $course, $user=0, $date=0, $order="l.time ASC", $limitfrom='', $limitnum='', 169 $modname="", $modid=0, $modaction="", $groupid=0) { 170 global $CFG, $DB; 171 172 // It is assumed that $date is the GMT time of midnight for that day, 173 // and so the next 86400 seconds worth of logs are printed. 174 175 /// Setup for group handling. 176 177 // TODO: I don't understand group/context/etc. enough to be able to do 178 // something interesting with it here 179 // What is the context of a remote course? 180 181 /// If the group mode is separate, and this user does not have editing privileges, 182 /// then only the user's group can be viewed. 183 //if ($course->groupmode == SEPARATEGROUPS and !has_capability('moodle/course:managegroups', context_course::instance($course->id))) { 184 // $groupid = get_current_group($course->id); 185 //} 186 /// If this course doesn't have groups, no groupid can be specified. 187 //else if (!$course->groupmode) { 188 // $groupid = 0; 189 //} 190 191 $groupid = 0; 192 193 $joins = array(); 194 $where = ''; 195 196 $qry = "SELECT l.*, u.firstname, u.lastname, u.picture 197 FROM {mnet_log} l 198 LEFT JOIN {user} u ON l.userid = u.id 199 WHERE "; 200 $params = array(); 201 202 $where .= "l.hostid = :hostid"; 203 $params['hostid'] = $hostid; 204 205 // TODO: Is 1 really a magic number referring to the sitename? 206 if ($course != SITEID || $modid != 0) { 207 $where .= " AND l.course=:courseid"; 208 $params['courseid'] = $course; 209 } 210 211 if ($modname) { 212 $where .= " AND l.module = :modname"; 213 $params['modname'] = $modname; 214 } 215 216 if ('site_errors' === $modid) { 217 $where .= " AND ( l.action='error' OR l.action='infected' )"; 218 } else if ($modid) { 219 //TODO: This assumes that modids are the same across sites... probably 220 //not true 221 $where .= " AND l.cmid = :modid"; 222 $params['modid'] = $modid; 223 } 224 225 if ($modaction) { 226 $firstletter = substr($modaction, 0, 1); 227 if ($firstletter == '-') { 228 $where .= " AND ".$DB->sql_like('l.action', ':modaction', false, true, true); 229 $params['modaction'] = '%'.substr($modaction, 1).'%'; 230 } else { 231 $where .= " AND ".$DB->sql_like('l.action', ':modaction', false); 232 $params['modaction'] = '%'.$modaction.'%'; 233 } 234 } 235 236 if ($user) { 237 $where .= " AND l.userid = :user"; 238 $params['user'] = $user; 239 } 240 241 if ($date) { 242 $enddate = $date + 86400; 243 $where .= " AND l.time > :date AND l.time < :enddate"; 244 $params['date'] = $date; 245 $params['enddate'] = $enddate; 246 } 247 248 $result = array(); 249 $result['totalcount'] = $DB->count_records_sql("SELECT COUNT('x') FROM {mnet_log} l WHERE $where", $params); 250 if(!empty($result['totalcount'])) { 251 $where .= " ORDER BY $order"; 252 $result['logs'] = $DB->get_records_sql("$qry $where", $params, $limitfrom, $limitnum); 253 } else { 254 $result['logs'] = array(); 255 } 256 return $result; 257 } 258 259 /** 260 * Checks the integrity of the course data. 261 * 262 * In summary - compares course_sections.sequence and course_modules.section. 263 * 264 * More detailed, checks that: 265 * - course_sections.sequence contains each module id not more than once in the course 266 * - for each moduleid from course_sections.sequence the field course_modules.section 267 * refers to the same section id (this means course_sections.sequence is more 268 * important if they are different) 269 * - ($fullcheck only) each module in the course is present in one of 270 * course_sections.sequence 271 * - ($fullcheck only) removes non-existing course modules from section sequences 272 * 273 * If there are any mismatches, the changes are made and records are updated in DB. 274 * 275 * Course cache is NOT rebuilt if there are any errors! 276 * 277 * This function is used each time when course cache is being rebuilt with $fullcheck = false 278 * and in CLI script admin/cli/fix_course_sequence.php with $fullcheck = true 279 * 280 * @param int $courseid id of the course 281 * @param array $rawmods result of funciton {@link get_course_mods()} - containst 282 * the list of enabled course modules in the course. Retrieved from DB if not specified. 283 * Argument ignored in cashe of $fullcheck, the list is retrieved form DB anyway. 284 * @param array $sections records from course_sections table for this course. 285 * Retrieved from DB if not specified 286 * @param bool $fullcheck Will add orphaned modules to their sections and remove non-existing 287 * course modules from sequences. Only to be used in site maintenance mode when we are 288 * sure that another user is not in the middle of the process of moving/removing a module. 289 * @param bool $checkonly Only performs the check without updating DB, outputs all errors as debug messages. 290 * @return array array of messages with found problems. Empty output means everything is ok 291 */ 292 function course_integrity_check($courseid, $rawmods = null, $sections = null, $fullcheck = false, $checkonly = false) { 293 global $DB; 294 $messages = array(); 295 if ($sections === null) { 296 $sections = $DB->get_records('course_sections', array('course' => $courseid), 'section', 'id,section,sequence'); 297 } 298 if ($fullcheck) { 299 // Retrieve all records from course_modules regardless of module type visibility. 300 $rawmods = $DB->get_records('course_modules', array('course' => $courseid), 'id', 'id,section'); 301 } 302 if ($rawmods === null) { 303 $rawmods = get_course_mods($courseid); 304 } 305 if (!$fullcheck && (empty($sections) || empty($rawmods))) { 306 // If either of the arrays is empty, no modules are displayed anyway. 307 return true; 308 } 309 $debuggingprefix = 'Failed integrity check for course ['.$courseid.']. '; 310 311 // First make sure that each module id appears in section sequences only once. 312 // If it appears in several section sequences the last section wins. 313 // If it appears twice in one section sequence, the first occurence wins. 314 $modsection = array(); 315 foreach ($sections as $sectionid => $section) { 316 $sections[$sectionid]->newsequence = $section->sequence; 317 if (!empty($section->sequence)) { 318 $sequence = explode(",", $section->sequence); 319 $sequenceunique = array_unique($sequence); 320 if (count($sequenceunique) != count($sequence)) { 321 // Some course module id appears in this section sequence more than once. 322 ksort($sequenceunique); // Preserve initial order of modules. 323 $sequence = array_values($sequenceunique); 324 $sections[$sectionid]->newsequence = join(',', $sequence); 325 $messages[] = $debuggingprefix.'Sequence for course section ['. 326 $sectionid.'] is "'.$sections[$sectionid]->sequence.'", must be "'.$sections[$sectionid]->newsequence.'"'; 327 } 328 foreach ($sequence as $cmid) { 329 if (array_key_exists($cmid, $modsection) && isset($rawmods[$cmid])) { 330 // Some course module id appears to be in more than one section's sequences. 331 $wrongsectionid = $modsection[$cmid]; 332 $sections[$wrongsectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$wrongsectionid]->newsequence. ','), ','); 333 $messages[] = $debuggingprefix.'Course module ['.$cmid.'] must be removed from sequence of section ['. 334 $wrongsectionid.'] because it is also present in sequence of section ['.$sectionid.']'; 335 } 336 $modsection[$cmid] = $sectionid; 337 } 338 } 339 } 340 341 // Add orphaned modules to their sections if they exist or to section 0 otherwise. 342 if ($fullcheck) { 343 foreach ($rawmods as $cmid => $mod) { 344 if (!isset($modsection[$cmid])) { 345 // This is a module that is not mentioned in course_section.sequence at all. 346 // Add it to the section $mod->section or to the last available section. 347 if ($mod->section && isset($sections[$mod->section])) { 348 $modsection[$cmid] = $mod->section; 349 } else { 350 $firstsection = reset($sections); 351 $modsection[$cmid] = $firstsection->id; 352 } 353 $sections[$modsection[$cmid]]->newsequence = trim($sections[$modsection[$cmid]]->newsequence.','.$cmid, ','); 354 $messages[] = $debuggingprefix.'Course module ['.$cmid.'] is missing from sequence of section ['. 355 $modsection[$cmid].']'; 356 } 357 } 358 foreach ($modsection as $cmid => $sectionid) { 359 if (!isset($rawmods[$cmid])) { 360 // Section $sectionid refers to module id that does not exist. 361 $sections[$sectionid]->newsequence = trim(preg_replace("/,$cmid,/", ',', ','.$sections[$sectionid]->newsequence.','), ','); 362 $messages[] = $debuggingprefix.'Course module ['.$cmid. 363 '] does not exist but is present in the sequence of section ['.$sectionid.']'; 364 } 365 } 366 } 367 368 // Update changed sections. 369 if (!$checkonly && !empty($messages)) { 370 foreach ($sections as $sectionid => $section) { 371 if ($section->newsequence !== $section->sequence) { 372 $DB->update_record('course_sections', array('id' => $sectionid, 'sequence' => $section->newsequence)); 373 } 374 } 375 } 376 377 // Now make sure that all modules point to the correct sections. 378 foreach ($rawmods as $cmid => $mod) { 379 if (isset($modsection[$cmid]) && $modsection[$cmid] != $mod->section) { 380 if (!$checkonly) { 381 $DB->update_record('course_modules', array('id' => $cmid, 'section' => $modsection[$cmid])); 382 } 383 $messages[] = $debuggingprefix.'Course module ['.$cmid. 384 '] points to section ['.$mod->section.'] instead of ['.$modsection[$cmid].']'; 385 } 386 } 387 388 return $messages; 389 } 390 391 /** 392 * Returns an array where the key is the module name (component name without 'mod_') 393 * and the value is a lang_string object with a human-readable string. 394 * 395 * @param bool $plural If true, the function returns the plural forms of the names. 396 * @param bool $resetcache If true, the static cache will be reset 397 * @return lang_string[] Localised human-readable names of all used modules. 398 */ 399 function get_module_types_names($plural = false, $resetcache = false) { 400 static $modnames = null; 401 global $DB, $CFG; 402 if ($modnames === null || $resetcache) { 403 $modnames = array(0 => array(), 1 => array()); 404 if ($allmods = $DB->get_records("modules")) { 405 foreach ($allmods as $mod) { 406 if (file_exists("$CFG->dirroot/mod/$mod->name/lib.php") && $mod->visible) { 407 $modnames[0][$mod->name] = get_string("modulename", "$mod->name", null, true); 408 $modnames[1][$mod->name] = get_string("modulenameplural", "$mod->name", null, true); 409 } 410 } 411 } 412 } 413 return $modnames[(int)$plural]; 414 } 415 416 /** 417 * Set highlighted section. Only one section can be highlighted at the time. 418 * 419 * @param int $courseid course id 420 * @param int $marker highlight section with this number, 0 means remove higlightin 421 * @return void 422 */ 423 function course_set_marker($courseid, $marker) { 424 global $DB, $COURSE; 425 $DB->set_field("course", "marker", $marker, array('id' => $courseid)); 426 if ($COURSE && $COURSE->id == $courseid) { 427 $COURSE->marker = $marker; 428 } 429 core_courseformat\base::reset_course_cache($courseid); 430 course_modinfo::clear_instance_cache($courseid); 431 } 432 433 /** 434 * For a given course section, marks it visible or hidden, 435 * and does the same for every activity in that section 436 * 437 * @param int $courseid course id 438 * @param int $sectionnumber The section number to adjust 439 * @param int $visibility The new visibility 440 * @return array A list of resources which were hidden in the section 441 */ 442 function set_section_visible($courseid, $sectionnumber, $visibility) { 443 global $DB; 444 445 $resourcestotoggle = array(); 446 if ($section = $DB->get_record("course_sections", array("course"=>$courseid, "section"=>$sectionnumber))) { 447 course_update_section($courseid, $section, array('visible' => $visibility)); 448 449 // Determine which modules are visible for AJAX update 450 $modules = !empty($section->sequence) ? explode(',', $section->sequence) : array(); 451 if (!empty($modules)) { 452 list($insql, $params) = $DB->get_in_or_equal($modules); 453 $select = 'id ' . $insql . ' AND visible = ?'; 454 array_push($params, $visibility); 455 if (!$visibility) { 456 $select .= ' AND visibleold = 1'; 457 } 458 $resourcestotoggle = $DB->get_fieldset_select('course_modules', 'id', $select, $params); 459 } 460 } 461 return $resourcestotoggle; 462 } 463 464 /** 465 * Return the course category context for the category with id $categoryid, except 466 * that if $categoryid is 0, return the system context. 467 * 468 * @param integer $categoryid a category id or 0. 469 * @return context the corresponding context 470 */ 471 function get_category_or_system_context($categoryid) { 472 if ($categoryid) { 473 return context_coursecat::instance($categoryid, IGNORE_MISSING); 474 } else { 475 return context_system::instance(); 476 } 477 } 478 479 /** 480 * Print the buttons relating to course requests. 481 * 482 * @param context $context current page context. 483 * @deprecated since Moodle 4.0 484 * @todo Final deprecation MDL-73976 485 */ 486 function print_course_request_buttons($context) { 487 global $CFG, $DB, $OUTPUT; 488 debugging("print_course_request_buttons() is deprecated. " . 489 "This is replaced with the category_action_bar tertiary navigation.", DEBUG_DEVELOPER); 490 if (empty($CFG->enablecourserequests)) { 491 return; 492 } 493 if (course_request::can_request($context)) { 494 // Print a button to request a new course. 495 $params = []; 496 if ($context instanceof context_coursecat) { 497 $params['category'] = $context->instanceid; 498 } 499 echo $OUTPUT->single_button(new moodle_url('/course/request.php', $params), 500 get_string('requestcourse'), 'get'); 501 } 502 /// Print a button to manage pending requests 503 if (has_capability('moodle/site:approvecourse', $context)) { 504 $disabled = !$DB->record_exists('course_request', array()); 505 echo $OUTPUT->single_button(new moodle_url('/course/pending.php'), get_string('coursespending'), 'get', array('disabled' => $disabled)); 506 } 507 } 508 509 /** 510 * Does the user have permission to edit things in this category? 511 * 512 * @param integer $categoryid The id of the category we are showing, or 0 for system context. 513 * @return boolean has_any_capability(array(...), ...); in the appropriate context. 514 */ 515 function can_edit_in_category($categoryid = 0) { 516 $context = get_category_or_system_context($categoryid); 517 return has_any_capability(array('moodle/category:manage', 'moodle/course:create'), $context); 518 } 519 520 /// MODULE FUNCTIONS ///////////////////////////////////////////////////////////////// 521 522 function add_course_module($mod) { 523 global $DB; 524 525 $mod->added = time(); 526 unset($mod->id); 527 528 $cmid = $DB->insert_record("course_modules", $mod); 529 rebuild_course_cache($mod->course, true); 530 return $cmid; 531 } 532 533 /** 534 * Creates a course section and adds it to the specified position 535 * 536 * @param int|stdClass $courseorid course id or course object 537 * @param int $position position to add to, 0 means to the end. If position is greater than 538 * number of existing secitons, the section is added to the end. This will become sectionnum of the 539 * new section. All existing sections at this or bigger position will be shifted down. 540 * @param bool $skipcheck the check has already been made and we know that the section with this position does not exist 541 * @return stdClass created section object 542 */ 543 function course_create_section($courseorid, $position = 0, $skipcheck = false) { 544 global $DB; 545 $courseid = is_object($courseorid) ? $courseorid->id : $courseorid; 546 547 // Find the last sectionnum among existing sections. 548 if ($skipcheck) { 549 $lastsection = $position - 1; 550 } else { 551 $lastsection = (int)$DB->get_field_sql('SELECT max(section) from {course_sections} WHERE course = ?', [$courseid]); 552 } 553 554 // First add section to the end. 555 $cw = new stdClass(); 556 $cw->course = $courseid; 557 $cw->section = $lastsection + 1; 558 $cw->summary = ''; 559 $cw->summaryformat = FORMAT_HTML; 560 $cw->sequence = ''; 561 $cw->name = null; 562 $cw->visible = 1; 563 $cw->availability = null; 564 $cw->timemodified = time(); 565 $cw->id = $DB->insert_record("course_sections", $cw); 566 567 // Now move it to the specified position. 568 if ($position > 0 && $position <= $lastsection) { 569 $course = is_object($courseorid) ? $courseorid : get_course($courseorid); 570 move_section_to($course, $cw->section, $position, true); 571 $cw->section = $position; 572 } 573 574 core\event\course_section_created::create_from_section($cw)->trigger(); 575 576 rebuild_course_cache($courseid, true); 577 return $cw; 578 } 579 580 /** 581 * Creates missing course section(s) and rebuilds course cache 582 * 583 * @param int|stdClass $courseorid course id or course object 584 * @param int|array $sections list of relative section numbers to create 585 * @return bool if there were any sections created 586 */ 587 function course_create_sections_if_missing($courseorid, $sections) { 588 if (!is_array($sections)) { 589 $sections = array($sections); 590 } 591 $existing = array_keys(get_fast_modinfo($courseorid)->get_section_info_all()); 592 if ($newsections = array_diff($sections, $existing)) { 593 foreach ($newsections as $sectionnum) { 594 course_create_section($courseorid, $sectionnum, true); 595 } 596 return true; 597 } 598 return false; 599 } 600 601 /** 602 * Adds an existing module to the section 603 * 604 * Updates both tables {course_sections} and {course_modules} 605 * 606 * Note: This function does not use modinfo PROVIDED that the section you are 607 * adding the module to already exists. If the section does not exist, it will 608 * build modinfo if necessary and create the section. 609 * 610 * @param int|stdClass $courseorid course id or course object 611 * @param int $cmid id of the module already existing in course_modules table 612 * @param int $sectionnum relative number of the section (field course_sections.section) 613 * If section does not exist it will be created 614 * @param int|stdClass $beforemod id or object with field id corresponding to the module 615 * before which the module needs to be included. Null for inserting in the 616 * end of the section 617 * @return int The course_sections ID where the module is inserted 618 */ 619 function course_add_cm_to_section($courseorid, $cmid, $sectionnum, $beforemod = null) { 620 global $DB, $COURSE; 621 if (is_object($beforemod)) { 622 $beforemod = $beforemod->id; 623 } 624 if (is_object($courseorid)) { 625 $courseid = $courseorid->id; 626 } else { 627 $courseid = $courseorid; 628 } 629 // Do not try to use modinfo here, there is no guarantee it is valid! 630 $section = $DB->get_record('course_sections', 631 array('course' => $courseid, 'section' => $sectionnum), '*', IGNORE_MISSING); 632 if (!$section) { 633 // This function call requires modinfo. 634 course_create_sections_if_missing($courseorid, $sectionnum); 635 $section = $DB->get_record('course_sections', 636 array('course' => $courseid, 'section' => $sectionnum), '*', MUST_EXIST); 637 } 638 639 $modarray = explode(",", trim($section->sequence)); 640 if (empty($section->sequence)) { 641 $newsequence = "$cmid"; 642 } else if ($beforemod && ($key = array_keys($modarray, $beforemod))) { 643 $insertarray = array($cmid, $beforemod); 644 array_splice($modarray, $key[0], 1, $insertarray); 645 $newsequence = implode(",", $modarray); 646 } else { 647 $newsequence = "$section->sequence,$cmid"; 648 } 649 $DB->set_field("course_sections", "sequence", $newsequence, array("id" => $section->id)); 650 $DB->set_field('course_modules', 'section', $section->id, array('id' => $cmid)); 651 rebuild_course_cache($courseid, true); 652 return $section->id; // Return course_sections ID that was used. 653 } 654 655 /** 656 * Change the group mode of a course module. 657 * 658 * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs 659 * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}. 660 * 661 * @param int $id course module ID. 662 * @param int $groupmode the new groupmode value. 663 * @return bool True if the $groupmode was updated. 664 */ 665 function set_coursemodule_groupmode($id, $groupmode) { 666 global $DB; 667 $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,groupmode', MUST_EXIST); 668 if ($cm->groupmode != $groupmode) { 669 $DB->set_field('course_modules', 'groupmode', $groupmode, array('id' => $cm->id)); 670 \course_modinfo::purge_course_module_cache($cm->course, $cm->id); 671 rebuild_course_cache($cm->course, false, true); 672 } 673 return ($cm->groupmode != $groupmode); 674 } 675 676 function set_coursemodule_idnumber($id, $idnumber) { 677 global $DB; 678 $cm = $DB->get_record('course_modules', array('id' => $id), 'id,course,idnumber', MUST_EXIST); 679 if ($cm->idnumber != $idnumber) { 680 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id)); 681 \course_modinfo::purge_course_module_cache($cm->course, $cm->id); 682 rebuild_course_cache($cm->course, false, true); 683 } 684 return ($cm->idnumber != $idnumber); 685 } 686 687 /** 688 * Set downloadcontent value to course module. 689 * 690 * @param int $id The id of the module. 691 * @param bool $downloadcontent Whether the module can be downloaded when download course content is enabled. 692 * @return bool True if downloadcontent has been updated, false otherwise. 693 */ 694 function set_downloadcontent(int $id, bool $downloadcontent): bool { 695 global $DB; 696 $cm = $DB->get_record('course_modules', ['id' => $id], 'id, course, downloadcontent', MUST_EXIST); 697 if ($cm->downloadcontent != $downloadcontent) { 698 $DB->set_field('course_modules', 'downloadcontent', $downloadcontent, ['id' => $cm->id]); 699 rebuild_course_cache($cm->course, true); 700 } 701 return ($cm->downloadcontent != $downloadcontent); 702 } 703 704 /** 705 * Set the visibility of a module and inherent properties. 706 * 707 * Note: Do not forget to trigger the event \core\event\course_module_updated as it needs 708 * to be triggered manually, refer to {@link \core\event\course_module_updated::create_from_cm()}. 709 * 710 * From 2.4 the parameter $prevstateoverrides has been removed, the logic it triggered 711 * has been moved to {@link set_section_visible()} which was the only place from which 712 * the parameter was used. 713 * 714 * If $rebuildcache is set to false, the calling code is responsible for ensuring the cache is purged 715 * and rebuilt as appropriate. Consider using this if set_coursemodule_visible is called multiple times 716 * (e.g. in a loop). 717 * 718 * @param int $id of the module 719 * @param int $visible state of the module 720 * @param int $visibleoncoursepage state of the module on the course page 721 * @param bool $rebuildcache If true (default), perform a partial cache purge and rebuild. 722 * @return bool false when the module was not found, true otherwise 723 */ 724 function set_coursemodule_visible($id, $visible, $visibleoncoursepage = 1, bool $rebuildcache = true) { 725 global $DB, $CFG; 726 require_once($CFG->libdir.'/gradelib.php'); 727 require_once($CFG->dirroot.'/calendar/lib.php'); 728 729 if (!$cm = $DB->get_record('course_modules', array('id'=>$id))) { 730 return false; 731 } 732 733 // Create events and propagate visibility to associated grade items if the value has changed. 734 // Only do this if it's changed to avoid accidently overwriting manual showing/hiding of student grades. 735 if ($cm->visible == $visible && $cm->visibleoncoursepage == $visibleoncoursepage) { 736 return true; 737 } 738 739 if (!$modulename = $DB->get_field('modules', 'name', array('id'=>$cm->module))) { 740 return false; 741 } 742 if (($cm->visible != $visible) && 743 ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename)))) { 744 foreach($events as $event) { 745 if ($visible) { 746 $event = new calendar_event($event); 747 $event->toggle_visibility(true); 748 } else { 749 $event = new calendar_event($event); 750 $event->toggle_visibility(false); 751 } 752 } 753 } 754 755 // Updating visible and visibleold to keep them in sync. Only changing a section visibility will 756 // affect visibleold to allow for an original visibility restore. See set_section_visible(). 757 $cminfo = new stdClass(); 758 $cminfo->id = $id; 759 $cminfo->visible = $visible; 760 $cminfo->visibleoncoursepage = $visibleoncoursepage; 761 $cminfo->visibleold = $visible; 762 $DB->update_record('course_modules', $cminfo); 763 764 // Hide the associated grade items so the teacher doesn't also have to go to the gradebook and hide them there. 765 // Note that this must be done after updating the row in course_modules, in case 766 // the modules grade_item_update function needs to access $cm->visible. 767 if ($cm->visible != $visible && 768 plugin_supports('mod', $modulename, FEATURE_CONTROLS_GRADE_VISIBILITY) && 769 component_callback_exists('mod_' . $modulename, 'grade_item_update')) { 770 $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST); 771 component_callback('mod_' . $modulename, 'grade_item_update', array($instance)); 772 } else if ($cm->visible != $visible) { 773 $grade_items = grade_item::fetch_all(array('itemtype'=>'mod', 'itemmodule'=>$modulename, 'iteminstance'=>$cm->instance, 'courseid'=>$cm->course)); 774 if ($grade_items) { 775 foreach ($grade_items as $grade_item) { 776 $grade_item->set_hidden(!$visible); 777 } 778 } 779 } 780 781 if ($rebuildcache) { 782 \course_modinfo::purge_course_module_cache($cm->course, $cm->id); 783 rebuild_course_cache($cm->course, false, true); 784 } 785 return true; 786 } 787 788 /** 789 * Changes the course module name 790 * 791 * @param int $id course module id 792 * @param string $name new value for a name 793 * @return bool whether a change was made 794 */ 795 function set_coursemodule_name($id, $name) { 796 global $CFG, $DB; 797 require_once($CFG->libdir . '/gradelib.php'); 798 799 $cm = get_coursemodule_from_id('', $id, 0, false, MUST_EXIST); 800 801 $module = new \stdClass(); 802 $module->id = $cm->instance; 803 804 // Escape strings as they would be by mform. 805 if (!empty($CFG->formatstringstriptags)) { 806 $module->name = clean_param($name, PARAM_TEXT); 807 } else { 808 $module->name = clean_param($name, PARAM_CLEANHTML); 809 } 810 if ($module->name === $cm->name || strval($module->name) === '') { 811 return false; 812 } 813 if (\core_text::strlen($module->name) > 255) { 814 throw new \moodle_exception('maximumchars', 'moodle', '', 255); 815 } 816 817 $module->timemodified = time(); 818 $DB->update_record($cm->modname, $module); 819 $cm->name = $module->name; 820 \core\event\course_module_updated::create_from_cm($cm)->trigger(); 821 \course_modinfo::purge_course_module_cache($cm->course, $cm->id); 822 rebuild_course_cache($cm->course, false, true); 823 824 // Attempt to update the grade item if relevant. 825 $grademodule = $DB->get_record($cm->modname, array('id' => $cm->instance)); 826 $grademodule->cmidnumber = $cm->idnumber; 827 $grademodule->modname = $cm->modname; 828 grade_update_mod_grades($grademodule); 829 830 // Update calendar events with the new name. 831 course_module_update_calendar_events($cm->modname, $grademodule, $cm); 832 833 return true; 834 } 835 836 /** 837 * This function will handle the whole deletion process of a module. This includes calling 838 * the modules delete_instance function, deleting files, events, grades, conditional data, 839 * the data in the course_module and course_sections table and adding a module deletion 840 * event to the DB. 841 * 842 * @param int $cmid the course module id 843 * @param bool $async whether or not to try to delete the module using an adhoc task. Async also depends on a plugin hook. 844 * @throws moodle_exception 845 * @since Moodle 2.5 846 */ 847 function course_delete_module($cmid, $async = false) { 848 // Check the 'course_module_background_deletion_recommended' hook first. 849 // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested. 850 // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it. 851 // It's up to plugins to handle things like whether or not they are enabled. 852 if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) { 853 foreach ($pluginsfunction as $plugintype => $plugins) { 854 foreach ($plugins as $pluginfunction) { 855 if ($pluginfunction()) { 856 return course_module_flag_for_async_deletion($cmid); 857 } 858 } 859 } 860 } 861 862 global $CFG, $DB; 863 864 require_once($CFG->libdir.'/gradelib.php'); 865 require_once($CFG->libdir.'/questionlib.php'); 866 require_once($CFG->dirroot.'/blog/lib.php'); 867 require_once($CFG->dirroot.'/calendar/lib.php'); 868 869 // Get the course module. 870 if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) { 871 return true; 872 } 873 874 // Get the module context. 875 $modcontext = context_module::instance($cm->id); 876 877 // Get the course module name. 878 $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST); 879 880 // Get the file location of the delete_instance function for this module. 881 $modlib = "$CFG->dirroot/mod/$modulename/lib.php"; 882 883 // Include the file required to call the delete_instance function for this module. 884 if (file_exists($modlib)) { 885 require_once($modlib); 886 } else { 887 throw new moodle_exception('cannotdeletemodulemissinglib', '', '', null, 888 "Cannot delete this module as the file mod/$modulename/lib.php is missing."); 889 } 890 891 $deleteinstancefunction = $modulename . '_delete_instance'; 892 893 // Ensure the delete_instance function exists for this module. 894 if (!function_exists($deleteinstancefunction)) { 895 throw new moodle_exception('cannotdeletemodulemissingfunc', '', '', null, 896 "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php."); 897 } 898 899 // Allow plugins to use this course module before we completely delete it. 900 if ($pluginsfunction = get_plugins_with_function('pre_course_module_delete')) { 901 foreach ($pluginsfunction as $plugintype => $plugins) { 902 foreach ($plugins as $pluginfunction) { 903 $pluginfunction($cm); 904 } 905 } 906 } 907 908 // Call the delete_instance function, if it returns false throw an exception. 909 if (!$deleteinstancefunction($cm->instance)) { 910 throw new moodle_exception('cannotdeletemoduleinstance', '', '', null, 911 "Cannot delete the module $modulename (instance)."); 912 } 913 914 question_delete_activity($cm); 915 916 // Remove all module files in case modules forget to do that. 917 $fs = get_file_storage(); 918 $fs->delete_area_files($modcontext->id); 919 920 // Delete events from calendar. 921 if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $modulename))) { 922 $coursecontext = context_course::instance($cm->course); 923 foreach($events as $event) { 924 $event->context = $coursecontext; 925 $calendarevent = calendar_event::load($event); 926 $calendarevent->delete(); 927 } 928 } 929 930 // Delete grade items, outcome items and grades attached to modules. 931 if ($grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $modulename, 932 'iteminstance' => $cm->instance, 'courseid' => $cm->course))) { 933 foreach ($grade_items as $grade_item) { 934 $grade_item->delete('moddelete'); 935 } 936 } 937 938 // Delete associated blogs and blog tag instances. 939 blog_remove_associations_for_module($modcontext->id); 940 941 // Delete completion and availability data; it is better to do this even if the 942 // features are not turned on, in case they were turned on previously (these will be 943 // very quick on an empty table). 944 $DB->delete_records('course_modules_completion', array('coursemoduleid' => $cm->id)); 945 $DB->delete_records('course_modules_viewed', ['coursemoduleid' => $cm->id]); 946 $DB->delete_records('course_completion_criteria', array('moduleinstance' => $cm->id, 947 'course' => $cm->course, 948 'criteriatype' => COMPLETION_CRITERIA_TYPE_ACTIVITY)); 949 950 // Delete all tag instances associated with the instance of this module. 951 core_tag_tag::delete_instances('mod_' . $modulename, null, $modcontext->id); 952 core_tag_tag::remove_all_item_tags('core', 'course_modules', $cm->id); 953 954 // Notify the competency subsystem. 955 \core_competency\api::hook_course_module_deleted($cm); 956 957 // Delete the context. 958 context_helper::delete_instance(CONTEXT_MODULE, $cm->id); 959 960 // Delete the module from the course_modules table. 961 $DB->delete_records('course_modules', array('id' => $cm->id)); 962 963 // Delete module from that section. 964 if (!delete_mod_from_section($cm->id, $cm->section)) { 965 throw new moodle_exception('cannotdeletemodulefromsection', '', '', null, 966 "Cannot delete the module $modulename (instance) from section."); 967 } 968 969 // Trigger event for course module delete action. 970 $event = \core\event\course_module_deleted::create(array( 971 'courseid' => $cm->course, 972 'context' => $modcontext, 973 'objectid' => $cm->id, 974 'other' => array( 975 'modulename' => $modulename, 976 'instanceid' => $cm->instance, 977 ) 978 )); 979 $event->add_record_snapshot('course_modules', $cm); 980 $event->trigger(); 981 \course_modinfo::purge_course_module_cache($cm->course, $cm->id); 982 rebuild_course_cache($cm->course, false, true); 983 } 984 985 /** 986 * Schedule a course module for deletion in the background using an adhoc task. 987 * 988 * This method should not be called directly. Instead, please use course_delete_module($cmid, true), to denote async deletion. 989 * The real deletion of the module is handled by the task, which calls 'course_delete_module($cmid)'. 990 * 991 * @param int $cmid the course module id. 992 * @return bool whether the module was successfully scheduled for deletion. 993 * @throws \moodle_exception 994 */ 995 function course_module_flag_for_async_deletion($cmid) { 996 global $CFG, $DB, $USER; 997 require_once($CFG->libdir.'/gradelib.php'); 998 require_once($CFG->libdir.'/questionlib.php'); 999 require_once($CFG->dirroot.'/blog/lib.php'); 1000 require_once($CFG->dirroot.'/calendar/lib.php'); 1001 1002 // Get the course module. 1003 if (!$cm = $DB->get_record('course_modules', array('id' => $cmid))) { 1004 return true; 1005 } 1006 1007 // We need to be reasonably certain the deletion is going to succeed before we background the process. 1008 // Make the necessary delete_instance checks, etc. before proceeding further. Throw exceptions if required. 1009 1010 // Get the course module name. 1011 $modulename = $DB->get_field('modules', 'name', array('id' => $cm->module), MUST_EXIST); 1012 1013 // Get the file location of the delete_instance function for this module. 1014 $modlib = "$CFG->dirroot/mod/$modulename/lib.php"; 1015 1016 // Include the file required to call the delete_instance function for this module. 1017 if (file_exists($modlib)) { 1018 require_once($modlib); 1019 } else { 1020 throw new \moodle_exception('cannotdeletemodulemissinglib', '', '', null, 1021 "Cannot delete this module as the file mod/$modulename/lib.php is missing."); 1022 } 1023 1024 $deleteinstancefunction = $modulename . '_delete_instance'; 1025 1026 // Ensure the delete_instance function exists for this module. 1027 if (!function_exists($deleteinstancefunction)) { 1028 throw new \moodle_exception('cannotdeletemodulemissingfunc', '', '', null, 1029 "Cannot delete this module as the function {$modulename}_delete_instance is missing in mod/$modulename/lib.php."); 1030 } 1031 1032 // We are going to defer the deletion as we can't be sure how long the module's pre_delete code will run for. 1033 $cm->deletioninprogress = '1'; 1034 $DB->update_record('course_modules', $cm); 1035 1036 // Create an adhoc task for the deletion of the course module. The task takes an array of course modules for removal. 1037 $removaltask = new \core_course\task\course_delete_modules(); 1038 $removaltask->set_custom_data(array( 1039 'cms' => array($cm), 1040 'userid' => $USER->id, 1041 'realuserid' => \core\session\manager::get_realuser()->id 1042 )); 1043 1044 // Queue the task for the next run. 1045 \core\task\manager::queue_adhoc_task($removaltask); 1046 1047 // Reset the course cache to hide the module. 1048 rebuild_course_cache($cm->course, true); 1049 } 1050 1051 /** 1052 * Checks whether the given course has any course modules scheduled for adhoc deletion. 1053 * 1054 * @param int $courseid the id of the course. 1055 * @param bool $onlygradable whether to check only gradable modules or all modules. 1056 * @return bool true if the course contains any modules pending deletion, false otherwise. 1057 */ 1058 function course_modules_pending_deletion(int $courseid, bool $onlygradable = false) : bool { 1059 if (empty($courseid)) { 1060 return false; 1061 } 1062 1063 if ($onlygradable) { 1064 // Fetch modules with grade items. 1065 if (!$coursegradeitems = grade_item::fetch_all(['itemtype' => 'mod', 'courseid' => $courseid])) { 1066 // Return early when there is none. 1067 return false; 1068 } 1069 } 1070 1071 $modinfo = get_fast_modinfo($courseid); 1072 foreach ($modinfo->get_cms() as $module) { 1073 if ($module->deletioninprogress == '1') { 1074 if ($onlygradable) { 1075 // Check if the module being deleted is in the list of course modules with grade items. 1076 foreach ($coursegradeitems as $coursegradeitem) { 1077 if ($coursegradeitem->itemmodule == $module->modname && $coursegradeitem->iteminstance == $module->instance) { 1078 // The module being deleted is within the gradable modules. 1079 return true; 1080 } 1081 } 1082 } else { 1083 return true; 1084 } 1085 } 1086 } 1087 return false; 1088 } 1089 1090 /** 1091 * Checks whether the course module, as defined by modulename and instanceid, is scheduled for deletion within the given course. 1092 * 1093 * @param int $courseid the course id. 1094 * @param string $modulename the module name. E.g. 'assign', 'book', etc. 1095 * @param int $instanceid the module instance id. 1096 * @return bool true if the course module is pending deletion, false otherwise. 1097 */ 1098 function course_module_instance_pending_deletion($courseid, $modulename, $instanceid) { 1099 if (empty($courseid) || empty($modulename) || empty($instanceid)) { 1100 return false; 1101 } 1102 $modinfo = get_fast_modinfo($courseid); 1103 $instances = $modinfo->get_instances_of($modulename); 1104 return isset($instances[$instanceid]) && $instances[$instanceid]->deletioninprogress; 1105 } 1106 1107 function delete_mod_from_section($modid, $sectionid) { 1108 global $DB; 1109 1110 if ($section = $DB->get_record("course_sections", array("id"=>$sectionid)) ) { 1111 1112 $modarray = explode(",", $section->sequence); 1113 1114 if ($key = array_keys ($modarray, $modid)) { 1115 array_splice($modarray, $key[0], 1); 1116 $newsequence = implode(",", $modarray); 1117 $DB->set_field("course_sections", "sequence", $newsequence, array("id"=>$section->id)); 1118 rebuild_course_cache($section->course, true); 1119 return true; 1120 } else { 1121 return false; 1122 } 1123 1124 } 1125 return false; 1126 } 1127 1128 /** 1129 * This function updates the calendar events from the information stored in the module table and the course 1130 * module table. 1131 * 1132 * @param string $modulename Module name 1133 * @param stdClass $instance Module object. Either the $instance or the $cm must be supplied. 1134 * @param stdClass $cm Course module object. Either the $instance or the $cm must be supplied. 1135 * @return bool Returns true if calendar events are updated. 1136 * @since Moodle 3.3.4 1137 */ 1138 function course_module_update_calendar_events($modulename, $instance = null, $cm = null) { 1139 global $DB; 1140 1141 if (isset($instance) || isset($cm)) { 1142 1143 if (!isset($instance)) { 1144 $instance = $DB->get_record($modulename, array('id' => $cm->instance), '*', MUST_EXIST); 1145 } 1146 if (!isset($cm)) { 1147 $cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course); 1148 } 1149 if (!empty($cm)) { 1150 course_module_calendar_event_update_process($instance, $cm); 1151 } 1152 return true; 1153 } 1154 return false; 1155 } 1156 1157 /** 1158 * Update all instances through out the site or in a course. 1159 * 1160 * @param string $modulename Module type to update. 1161 * @param integer $courseid Course id to update events. 0 for the whole site. 1162 * @return bool Returns True if the update was successful. 1163 * @since Moodle 3.3.4 1164 */ 1165 function course_module_bulk_update_calendar_events($modulename, $courseid = 0) { 1166 global $DB; 1167 1168 $instances = null; 1169 if ($courseid) { 1170 if (!$instances = $DB->get_records($modulename, array('course' => $courseid))) { 1171 return false; 1172 } 1173 } else { 1174 if (!$instances = $DB->get_records($modulename)) { 1175 return false; 1176 } 1177 } 1178 1179 foreach ($instances as $instance) { 1180 if ($cm = get_coursemodule_from_instance($modulename, $instance->id, $instance->course)) { 1181 course_module_calendar_event_update_process($instance, $cm); 1182 } 1183 } 1184 return true; 1185 } 1186 1187 /** 1188 * Calendar events for a module instance are updated. 1189 * 1190 * @param stdClass $instance Module instance object. 1191 * @param stdClass $cm Course Module object. 1192 * @since Moodle 3.3.4 1193 */ 1194 function course_module_calendar_event_update_process($instance, $cm) { 1195 // We need to call *_refresh_events() first because some modules delete 'old' events at the end of the code which 1196 // will remove the completion events. 1197 $refresheventsfunction = $cm->modname . '_refresh_events'; 1198 if (function_exists($refresheventsfunction)) { 1199 call_user_func($refresheventsfunction, $cm->course, $instance, $cm); 1200 } 1201 $completionexpected = (!empty($cm->completionexpected)) ? $cm->completionexpected : null; 1202 \core_completion\api::update_completion_date_event($cm->id, $cm->modname, $instance, $completionexpected); 1203 } 1204 1205 /** 1206 * Moves a section within a course, from a position to another. 1207 * Be very careful: $section and $destination refer to section number, 1208 * not id!. 1209 * 1210 * @param object $course 1211 * @param int $section Section number (not id!!!) 1212 * @param int $destination 1213 * @param bool $ignorenumsections 1214 * @return boolean Result 1215 */ 1216 function move_section_to($course, $section, $destination, $ignorenumsections = false) { 1217 /// Moves a whole course section up and down within the course 1218 global $USER, $DB; 1219 1220 if (!$destination && $destination != 0) { 1221 return true; 1222 } 1223 1224 // compartibility with course formats using field 'numsections' 1225 $courseformatoptions = course_get_format($course)->get_format_options(); 1226 if ((!$ignorenumsections && array_key_exists('numsections', $courseformatoptions) && 1227 ($destination > $courseformatoptions['numsections'])) || ($destination < 1)) { 1228 return false; 1229 } 1230 1231 // Get all sections for this course and re-order them (2 of them should now share the same section number) 1232 if (!$sections = $DB->get_records_menu('course_sections', array('course' => $course->id), 1233 'section ASC, id ASC', 'id, section')) { 1234 return false; 1235 } 1236 1237 $movedsections = reorder_sections($sections, $section, $destination); 1238 1239 // Update all sections. Do this in 2 steps to avoid breaking database 1240 // uniqueness constraint 1241 $transaction = $DB->start_delegated_transaction(); 1242 foreach ($movedsections as $id => $position) { 1243 if ((int) $sections[$id] !== $position) { 1244 $DB->set_field('course_sections', 'section', -$position, ['id' => $id]); 1245 // Invalidate the section cache by given section id. 1246 course_modinfo::purge_course_section_cache_by_id($course->id, $id); 1247 } 1248 } 1249 foreach ($movedsections as $id => $position) { 1250 if ((int) $sections[$id] !== $position) { 1251 $DB->set_field('course_sections', 'section', $position, ['id' => $id]); 1252 // Invalidate the section cache by given section id. 1253 course_modinfo::purge_course_section_cache_by_id($course->id, $id); 1254 } 1255 } 1256 1257 // If we move the highlighted section itself, then just highlight the destination. 1258 // Adjust the higlighted section location if we move something over it either direction. 1259 if ($section == $course->marker) { 1260 course_set_marker($course->id, $destination); 1261 } else if ($section > $course->marker && $course->marker >= $destination) { 1262 course_set_marker($course->id, $course->marker+1); 1263 } else if ($section < $course->marker && $course->marker <= $destination) { 1264 course_set_marker($course->id, $course->marker-1); 1265 } 1266 1267 $transaction->allow_commit(); 1268 rebuild_course_cache($course->id, true, true); 1269 return true; 1270 } 1271 1272 /** 1273 * This method will delete a course section and may delete all modules inside it. 1274 * 1275 * No permissions are checked here, use {@link course_can_delete_section()} to 1276 * check if section can actually be deleted. 1277 * 1278 * @param int|stdClass $course 1279 * @param int|stdClass|section_info $section 1280 * @param bool $forcedeleteifnotempty if set to false section will not be deleted if it has modules in it. 1281 * @param bool $async whether or not to try to delete the section using an adhoc task. Async also depends on a plugin hook. 1282 * @return bool whether section was deleted 1283 */ 1284 function course_delete_section($course, $section, $forcedeleteifnotempty = true, $async = false) { 1285 global $DB; 1286 1287 // Prepare variables. 1288 $courseid = (is_object($course)) ? $course->id : (int)$course; 1289 $sectionnum = (is_object($section)) ? $section->section : (int)$section; 1290 $section = $DB->get_record('course_sections', array('course' => $courseid, 'section' => $sectionnum)); 1291 if (!$section) { 1292 // No section exists, can't proceed. 1293 return false; 1294 } 1295 1296 // Check the 'course_module_background_deletion_recommended' hook first. 1297 // Only use asynchronous deletion if at least one plugin returns true and if async deletion has been requested. 1298 // Both are checked because plugins should not be allowed to dictate the deletion behaviour, only support/decline it. 1299 // It's up to plugins to handle things like whether or not they are enabled. 1300 if ($async && $pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) { 1301 foreach ($pluginsfunction as $plugintype => $plugins) { 1302 foreach ($plugins as $pluginfunction) { 1303 if ($pluginfunction()) { 1304 return course_delete_section_async($section, $forcedeleteifnotempty); 1305 } 1306 } 1307 } 1308 } 1309 1310 $format = course_get_format($course); 1311 $sectionname = $format->get_section_name($section); 1312 1313 // Delete section. 1314 $result = $format->delete_section($section, $forcedeleteifnotempty); 1315 1316 // Trigger an event for course section deletion. 1317 if ($result) { 1318 $context = context_course::instance($courseid); 1319 $event = \core\event\course_section_deleted::create( 1320 array( 1321 'objectid' => $section->id, 1322 'courseid' => $courseid, 1323 'context' => $context, 1324 'other' => array( 1325 'sectionnum' => $section->section, 1326 'sectionname' => $sectionname, 1327 ) 1328 ) 1329 ); 1330 $event->add_record_snapshot('course_sections', $section); 1331 $event->trigger(); 1332 } 1333 return $result; 1334 } 1335 1336 /** 1337 * Course section deletion, using an adhoc task for deletion of the modules it contains. 1338 * 1. Schedule all modules within the section for adhoc removal. 1339 * 2. Move all modules to course section 0. 1340 * 3. Delete the resulting empty section. 1341 * 1342 * @param \stdClass $section the section to schedule for deletion. 1343 * @param bool $forcedeleteifnotempty whether to force section deletion if it contains modules. 1344 * @return bool true if the section was scheduled for deletion, false otherwise. 1345 */ 1346 function course_delete_section_async($section, $forcedeleteifnotempty = true) { 1347 global $DB, $USER; 1348 1349 // Objects only, and only valid ones. 1350 if (!is_object($section) || empty($section->id)) { 1351 return false; 1352 } 1353 1354 // Does the object currently exist in the DB for removal (check for stale objects). 1355 $section = $DB->get_record('course_sections', array('id' => $section->id)); 1356 if (!$section || !$section->section) { 1357 // No section exists, or the section is 0. Can't proceed. 1358 return false; 1359 } 1360 1361 // Check whether the section can be removed. 1362 if (!$forcedeleteifnotempty && (!empty($section->sequence) || !empty($section->summary))) { 1363 return false; 1364 } 1365 1366 $format = course_get_format($section->course); 1367 $sectionname = $format->get_section_name($section); 1368 1369 // Flag those modules having no existing deletion flag. Some modules may have been scheduled for deletion manually, and we don't 1370 // want to create additional adhoc deletion tasks for these. Moving them to section 0 will suffice. 1371 $affectedmods = $DB->get_records_select('course_modules', 'course = ? AND section = ? AND deletioninprogress <> ?', 1372 [$section->course, $section->id, 1], '', 'id'); 1373 $DB->set_field('course_modules', 'deletioninprogress', '1', ['course' => $section->course, 'section' => $section->id]); 1374 1375 // Move all modules to section 0. 1376 $modules = $DB->get_records('course_modules', ['section' => $section->id], ''); 1377 $sectionzero = $DB->get_record('course_sections', ['course' => $section->course, 'section' => '0']); 1378 foreach ($modules as $mod) { 1379 moveto_module($mod, $sectionzero); 1380 } 1381 1382 // Create and queue an adhoc task for the deletion of the modules. 1383 $removaltask = new \core_course\task\course_delete_modules(); 1384 $data = array( 1385 'cms' => $affectedmods, 1386 'userid' => $USER->id, 1387 'realuserid' => \core\session\manager::get_realuser()->id 1388 ); 1389 $removaltask->set_custom_data($data); 1390 \core\task\manager::queue_adhoc_task($removaltask); 1391 1392 // Delete the now empty section, passing in only the section number, which forces the function to fetch a new object. 1393 // The refresh is needed because the section->sequence is now stale. 1394 $result = $format->delete_section($section->section, $forcedeleteifnotempty); 1395 1396 // Trigger an event for course section deletion. 1397 if ($result) { 1398 $context = \context_course::instance($section->course); 1399 $event = \core\event\course_section_deleted::create( 1400 array( 1401 'objectid' => $section->id, 1402 'courseid' => $section->course, 1403 'context' => $context, 1404 'other' => array( 1405 'sectionnum' => $section->section, 1406 'sectionname' => $sectionname, 1407 ) 1408 ) 1409 ); 1410 $event->add_record_snapshot('course_sections', $section); 1411 $event->trigger(); 1412 } 1413 rebuild_course_cache($section->course, true); 1414 1415 return $result; 1416 } 1417 1418 /** 1419 * Updates the course section 1420 * 1421 * This function does not check permissions or clean values - this has to be done prior to calling it. 1422 * 1423 * @param int|stdClass $course 1424 * @param stdClass $section record from course_sections table - it will be updated with the new values 1425 * @param array|stdClass $data 1426 */ 1427 function course_update_section($course, $section, $data) { 1428 global $DB; 1429 1430 $courseid = (is_object($course)) ? $course->id : (int)$course; 1431 1432 // Some fields can not be updated using this method. 1433 $data = array_diff_key((array)$data, array('id', 'course', 'section', 'sequence')); 1434 $changevisibility = (array_key_exists('visible', $data) && (bool)$data['visible'] != (bool)$section->visible); 1435 if (array_key_exists('name', $data) && \core_text::strlen($data['name']) > 255) { 1436 throw new moodle_exception('maximumchars', 'moodle', '', 255); 1437 } 1438 1439 // Update record in the DB and course format options. 1440 $data['id'] = $section->id; 1441 $data['timemodified'] = time(); 1442 $DB->update_record('course_sections', $data); 1443 // Invalidate the section cache by given section id. 1444 course_modinfo::purge_course_section_cache_by_id($courseid, $section->id); 1445 rebuild_course_cache($courseid, false, true); 1446 course_get_format($courseid)->update_section_format_options($data); 1447 1448 // Update fields of the $section object. 1449 foreach ($data as $key => $value) { 1450 if (property_exists($section, $key)) { 1451 $section->$key = $value; 1452 } 1453 } 1454 1455 // Trigger an event for course section update. 1456 $event = \core\event\course_section_updated::create( 1457 array( 1458 'objectid' => $section->id, 1459 'courseid' => $courseid, 1460 'context' => context_course::instance($courseid), 1461 'other' => array('sectionnum' => $section->section) 1462 ) 1463 ); 1464 $event->trigger(); 1465 1466 // If section visibility was changed, hide the modules in this section too. 1467 if ($changevisibility && !empty($section->sequence)) { 1468 $modules = explode(',', $section->sequence); 1469 $cmids = []; 1470 foreach ($modules as $moduleid) { 1471 if ($cm = get_coursemodule_from_id(null, $moduleid, $courseid)) { 1472 $cmids[] = $cm->id; 1473 if ($data['visible']) { 1474 // As we unhide the section, we use the previously saved visibility stored in visibleold. 1475 set_coursemodule_visible($moduleid, $cm->visibleold, $cm->visibleoncoursepage, false); 1476 } else { 1477 // We hide the section, so we hide the module but we store the original state in visibleold. 1478 set_coursemodule_visible($moduleid, 0, $cm->visibleoncoursepage, false); 1479 $DB->set_field('course_modules', 'visibleold', $cm->visible, ['id' => $moduleid]); 1480 } 1481 \core\event\course_module_updated::create_from_cm($cm)->trigger(); 1482 } 1483 } 1484 \course_modinfo::purge_course_modules_cache($courseid, $cmids); 1485 rebuild_course_cache($courseid, false, true); 1486 } 1487 } 1488 1489 /** 1490 * Checks if the current user can delete a section (if course format allows it and user has proper permissions). 1491 * 1492 * @param int|stdClass $course 1493 * @param int|stdClass|section_info $section 1494 * @return bool 1495 */ 1496 function course_can_delete_section($course, $section) { 1497 if (is_object($section)) { 1498 $section = $section->section; 1499 } 1500 if (!$section) { 1501 // Not possible to delete 0-section. 1502 return false; 1503 } 1504 // Course format should allow to delete sections. 1505 if (!course_get_format($course)->can_delete_section($section)) { 1506 return false; 1507 } 1508 // Make sure user has capability to update course and move sections. 1509 $context = context_course::instance(is_object($course) ? $course->id : $course); 1510 if (!has_all_capabilities(array('moodle/course:movesections', 'moodle/course:update'), $context)) { 1511 return false; 1512 } 1513 // Make sure user has capability to delete each activity in this section. 1514 $modinfo = get_fast_modinfo($course); 1515 if (!empty($modinfo->sections[$section])) { 1516 foreach ($modinfo->sections[$section] as $cmid) { 1517 if (!has_capability('moodle/course:manageactivities', context_module::instance($cmid))) { 1518 return false; 1519 } 1520 } 1521 } 1522 return true; 1523 } 1524 1525 /** 1526 * Reordering algorithm for course sections. Given an array of section->section indexed by section->id, 1527 * an original position number and a target position number, rebuilds the array so that the 1528 * move is made without any duplication of section positions. 1529 * Note: The target_position is the position AFTER WHICH the moved section will be inserted. If you want to 1530 * insert a section before the first one, you must give 0 as the target (section 0 can never be moved). 1531 * 1532 * @param array $sections 1533 * @param int $origin_position 1534 * @param int $target_position 1535 * @return array 1536 */ 1537 function reorder_sections($sections, $origin_position, $target_position) { 1538 if (!is_array($sections)) { 1539 return false; 1540 } 1541 1542 // We can't move section position 0 1543 if ($origin_position < 1) { 1544 echo "We can't move section position 0"; 1545 return false; 1546 } 1547 1548 // Locate origin section in sections array 1549 if (!$origin_key = array_search($origin_position, $sections)) { 1550 echo "searched position not in sections array"; 1551 return false; // searched position not in sections array 1552 } 1553 1554 // Extract origin section 1555 $origin_section = $sections[$origin_key]; 1556 unset($sections[$origin_key]); 1557 1558 // Find offset of target position (stupid PHP's array_splice requires offset instead of key index!) 1559 $found = false; 1560 $append_array = array(); 1561 foreach ($sections as $id => $position) { 1562 if ($found) { 1563 $append_array[$id] = $position; 1564 unset($sections[$id]); 1565 } 1566 if ($position == $target_position) { 1567 if ($target_position < $origin_position) { 1568 $append_array[$id] = $position; 1569 unset($sections[$id]); 1570 } 1571 $found = true; 1572 } 1573 } 1574 1575 // Append moved section 1576 $sections[$origin_key] = $origin_section; 1577 1578 // Append rest of array (if applicable) 1579 if (!empty($append_array)) { 1580 foreach ($append_array as $id => $position) { 1581 $sections[$id] = $position; 1582 } 1583 } 1584 1585 // Renumber positions 1586 $position = 0; 1587 foreach ($sections as $id => $p) { 1588 $sections[$id] = $position; 1589 $position++; 1590 } 1591 1592 return $sections; 1593 1594 } 1595 1596 /** 1597 * Move the module object $mod to the specified $section 1598 * If $beforemod exists then that is the module 1599 * before which $modid should be inserted 1600 * 1601 * @param stdClass|cm_info $mod 1602 * @param stdClass|section_info $section 1603 * @param int|stdClass $beforemod id or object with field id corresponding to the module 1604 * before which the module needs to be included. Null for inserting in the 1605 * end of the section 1606 * @return int new value for module visibility (0 or 1) 1607 */ 1608 function moveto_module($mod, $section, $beforemod=NULL) { 1609 global $OUTPUT, $DB; 1610 1611 // Current module visibility state - return value of this function. 1612 $modvisible = $mod->visible; 1613 1614 // Remove original module from original section. 1615 if (! delete_mod_from_section($mod->id, $mod->section)) { 1616 echo $OUTPUT->notification("Could not delete module from existing section"); 1617 } 1618 1619 // Add the module into the new section. 1620 course_add_cm_to_section($section->course, $mod->id, $section->section, $beforemod); 1621 1622 // If moving to a hidden section then hide module. 1623 if ($mod->section != $section->id) { 1624 if (!$section->visible && $mod->visible) { 1625 // Module was visible but must become hidden after moving to hidden section. 1626 $modvisible = 0; 1627 set_coursemodule_visible($mod->id, 0); 1628 // Set visibleold to 1 so module will be visible when section is made visible. 1629 $DB->set_field('course_modules', 'visibleold', 1, array('id' => $mod->id)); 1630 } 1631 if ($section->visible && !$mod->visible) { 1632 // Hidden module was moved to the visible section, restore the module visibility from visibleold. 1633 set_coursemodule_visible($mod->id, $mod->visibleold); 1634 $modvisible = $mod->visibleold; 1635 } 1636 } 1637 1638 return $modvisible; 1639 } 1640 1641 /** 1642 * Returns the list of all editing actions that current user can perform on the module 1643 * 1644 * @param cm_info $mod The module to produce editing buttons for 1645 * @param int $indent The current indenting (default -1 means no move left-right actions) 1646 * @param int $sr The section to link back to (used for creating the links) 1647 * @return array array of action_link or pix_icon objects 1648 */ 1649 function course_get_cm_edit_actions(cm_info $mod, $indent = -1, $sr = null) { 1650 global $COURSE, $SITE, $CFG; 1651 1652 static $str; 1653 1654 $coursecontext = context_course::instance($mod->course); 1655 $modcontext = context_module::instance($mod->id); 1656 $courseformat = course_get_format($mod->get_course()); 1657 $usecomponents = $courseformat->supports_components(); 1658 $sectioninfo = $mod->get_section_info(); 1659 1660 $editcaps = array('moodle/course:manageactivities', 'moodle/course:activityvisibility', 'moodle/role:assign'); 1661 $dupecaps = array('moodle/backup:backuptargetimport', 'moodle/restore:restoretargetimport'); 1662 1663 // No permission to edit anything. 1664 if (!has_any_capability($editcaps, $modcontext) and !has_all_capabilities($dupecaps, $coursecontext)) { 1665 return array(); 1666 } 1667 1668 $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext); 1669 1670 if (!isset($str)) { 1671 $str = get_strings( 1672 [ 1673 'delete', 'move', 'moveright', 'moveleft', 'editsettings', 1674 'duplicate', 'availability' 1675 ], 1676 'moodle' 1677 ); 1678 $str->assign = get_string('assignroles', 'role'); 1679 $str->groupmode = get_string('groupmode', 'group'); 1680 } 1681 1682 $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey())); 1683 1684 if ($sr !== null) { 1685 $baseurl->param('sr', $sr); 1686 } 1687 $actions = array(); 1688 1689 // Update. 1690 if ($hasmanageactivities) { 1691 $actions['update'] = new action_menu_link_secondary( 1692 new moodle_url($baseurl, array('update' => $mod->id)), 1693 new pix_icon('t/edit', '', 'moodle', array('class' => 'iconsmall')), 1694 $str->editsettings, 1695 array('class' => 'editing_update', 'data-action' => 'update') 1696 ); 1697 } 1698 1699 // Move (only for component compatible formats). 1700 if ($hasmanageactivities && $usecomponents) { 1701 $actions['move'] = new action_menu_link_secondary( 1702 new moodle_url($baseurl, [ 1703 'sesskey' => sesskey(), 1704 'copy' => $mod->id, 1705 ]), 1706 new pix_icon('i/dragdrop', '', 'moodle', ['class' => 'iconsmall']), 1707 $str->move, 1708 [ 1709 'class' => 'editing_movecm', 1710 'data-action' => 'moveCm', 1711 'data-id' => $mod->id, 1712 ] 1713 ); 1714 } 1715 1716 // Indent. 1717 if ($hasmanageactivities && $indent >= 0) { 1718 $indentlimits = new stdClass(); 1719 $indentlimits->min = 0; 1720 // Legacy indentation could continue using a limit of 16, 1721 // but components based formats will be forced to use one level indentation only. 1722 $indentlimits->max = ($usecomponents) ? 1 : 16; 1723 if (right_to_left()) { // Exchange arrows on RTL 1724 $rightarrow = 't/left'; 1725 $leftarrow = 't/right'; 1726 } else { 1727 $rightarrow = 't/right'; 1728 $leftarrow = 't/left'; 1729 } 1730 1731 if ($indent >= $indentlimits->max) { 1732 $enabledclass = 'hidden'; 1733 } else { 1734 $enabledclass = ''; 1735 } 1736 $actions['moveright'] = new action_menu_link_secondary( 1737 new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '1']), 1738 new pix_icon($rightarrow, '', 'moodle', ['class' => 'iconsmall']), 1739 $str->moveright, 1740 [ 1741 'class' => 'editing_moveright ' . $enabledclass, 1742 'data-action' => ($usecomponents) ? 'cmMoveRight' : 'moveright', 1743 'data-keepopen' => true, 1744 'data-sectionreturn' => $sr, 1745 'data-id' => $mod->id, 1746 ] 1747 ); 1748 1749 if ($indent <= $indentlimits->min) { 1750 $enabledclass = 'hidden'; 1751 } else { 1752 $enabledclass = ''; 1753 } 1754 $actions['moveleft'] = new action_menu_link_secondary( 1755 new moodle_url($baseurl, ['id' => $mod->id, 'indent' => '-1']), 1756 new pix_icon($leftarrow, '', 'moodle', ['class' => 'iconsmall']), 1757 $str->moveleft, 1758 [ 1759 'class' => 'editing_moveleft ' . $enabledclass, 1760 'data-action' => ($usecomponents) ? 'cmMoveLeft' : 'moveleft', 1761 'data-keepopen' => true, 1762 'data-sectionreturn' => $sr, 1763 'data-id' => $mod->id, 1764 ] 1765 ); 1766 1767 } 1768 1769 // Hide/Show/Available/Unavailable. 1770 if (has_capability('moodle/course:activityvisibility', $modcontext)) { 1771 $availabilityclass = $courseformat->get_output_classname('content\\cm\\visibility'); 1772 /** @var core_courseformat\output\local\content\cm\visibility */ 1773 $availability = new $availabilityclass($courseformat, $sectioninfo, $mod); 1774 $availabilitychoice = $availability->get_choice_list(); 1775 if ($availabilitychoice->count_options() > 1) { 1776 $actions['availability'] = new action_menu_subpanel( 1777 $str->availability, 1778 $availabilitychoice, 1779 ['class' => 'editing_availability'], 1780 new pix_icon('t/hide', '', 'moodle', array('class' => 'iconsmall')) 1781 ); 1782 } 1783 } 1784 1785 // Duplicate (require both target import caps to be able to duplicate and backup2 support, see modduplicate.php) 1786 if (has_all_capabilities($dupecaps, $coursecontext) && 1787 plugin_supports('mod', $mod->modname, FEATURE_BACKUP_MOODLE2) && 1788 course_allowed_module($mod->get_course(), $mod->modname)) { 1789 $actions['duplicate'] = new action_menu_link_secondary( 1790 new moodle_url($baseurl, ['duplicate' => $mod->id]), 1791 new pix_icon('t/copy', '', 'moodle', array('class' => 'iconsmall')), 1792 $str->duplicate, 1793 [ 1794 'class' => 'editing_duplicate', 1795 'data-action' => ($courseformat->supports_components()) ? 'cmDuplicate' : 'duplicate', 1796 'data-sectionreturn' => $sr, 1797 'data-id' => $mod->id, 1798 ] 1799 ); 1800 } 1801 1802 // Assign. 1803 if (has_capability('moodle/role:assign', $modcontext)){ 1804 $actions['assign'] = new action_menu_link_secondary( 1805 new moodle_url('/admin/roles/assign.php', array('contextid' => $modcontext->id)), 1806 new pix_icon('t/assignroles', '', 'moodle', array('class' => 'iconsmall')), 1807 $str->assign, 1808 array('class' => 'editing_assign', 'data-action' => 'assignroles', 'data-sectionreturn' => $sr) 1809 ); 1810 } 1811 1812 // Groupmode. 1813 if ($courseformat->show_groupmode($mod) && $usecomponents) { 1814 $groupmodeclass = $courseformat->get_output_classname('content\\cm\\groupmode'); 1815 /** @var core_courseformat\output\local\content\cm\groupmode */ 1816 $groupmode = new $groupmodeclass($courseformat, $sectioninfo, $mod); 1817 $actions['groupmode'] = new action_menu_subpanel( 1818 $str->groupmode, 1819 $groupmode->get_choice_list(), 1820 ['class' => 'editing_groupmode'], 1821 new pix_icon('i/groupv', '', 'moodle', ['class' => 'iconsmall']) 1822 ); 1823 } 1824 1825 // Delete. 1826 if ($hasmanageactivities) { 1827 $actions['delete'] = new action_menu_link_secondary( 1828 new moodle_url($baseurl, ['delete' => $mod->id]), 1829 new pix_icon('t/delete', '', 'moodle', ['class' => 'iconsmall']), 1830 $str->delete, 1831 [ 1832 'class' => 'editing_delete text-danger', 1833 'data-action' => ($usecomponents) ? 'cmDelete' : 'delete', 1834 'data-sectionreturn' => $sr, 1835 'data-id' => $mod->id, 1836 ] 1837 ); 1838 } 1839 1840 return $actions; 1841 } 1842 1843 /** 1844 * Returns the move action. 1845 * 1846 * @param cm_info $mod The module to produce a move button for 1847 * @param int $sr The section to link back to (used for creating the links) 1848 * @return The markup for the move action, or an empty string if not available. 1849 */ 1850 function course_get_cm_move(cm_info $mod, $sr = null) { 1851 global $OUTPUT; 1852 1853 static $str; 1854 static $baseurl; 1855 1856 $modcontext = context_module::instance($mod->id); 1857 $hasmanageactivities = has_capability('moodle/course:manageactivities', $modcontext); 1858 1859 if (!isset($str)) { 1860 $str = get_strings(array('move')); 1861 } 1862 1863 if (!isset($baseurl)) { 1864 $baseurl = new moodle_url('/course/mod.php', array('sesskey' => sesskey())); 1865 1866 if ($sr !== null) { 1867 $baseurl->param('sr', $sr); 1868 } 1869 } 1870 1871 if ($hasmanageactivities) { 1872 $pixicon = 'i/dragdrop'; 1873 1874 if (!course_ajax_enabled($mod->get_course())) { 1875 // Override for course frontpage until we get drag/drop working there. 1876 $pixicon = 't/move'; 1877 } 1878 1879 $attributes = [ 1880 'class' => 'editing_move', 1881 'data-action' => 'move', 1882 'data-sectionreturn' => $sr, 1883 'title' => $str->move, 1884 'aria-label' => $str->move, 1885 ]; 1886 return html_writer::link( 1887 new moodle_url($baseurl, ['copy' => $mod->id]), 1888 $OUTPUT->pix_icon($pixicon, '', 'moodle', ['class' => 'iconsmall']), 1889 $attributes 1890 ); 1891 } 1892 return ''; 1893 } 1894 1895 /** 1896 * given a course object with shortname & fullname, this function will 1897 * truncate the the number of chars allowed and add ... if it was too long 1898 */ 1899 function course_format_name ($course,$max=100) { 1900 1901 $context = context_course::instance($course->id); 1902 $shortname = format_string($course->shortname, true, array('context' => $context)); 1903 $fullname = format_string($course->fullname, true, array('context' => context_course::instance($course->id))); 1904 $str = $shortname.': '. $fullname; 1905 if (core_text::strlen($str) <= $max) { 1906 return $str; 1907 } 1908 else { 1909 return core_text::substr($str,0,$max-3).'...'; 1910 } 1911 } 1912 1913 /** 1914 * Is the user allowed to add this type of module to this course? 1915 * @param object $course the course settings. Only $course->id is used. 1916 * @param string $modname the module name. E.g. 'forum' or 'quiz'. 1917 * @param \stdClass $user the user to check, defaults to the global user if not provided. 1918 * @return bool whether the current user is allowed to add this type of module to this course. 1919 */ 1920 function course_allowed_module($course, $modname, \stdClass $user = null) { 1921 global $USER; 1922 $user = $user ?? $USER; 1923 if (is_numeric($modname)) { 1924 throw new coding_exception('Function course_allowed_module no longer 1925 supports numeric module ids. Please update your code to pass the module name.'); 1926 } 1927 1928 $capability = 'mod/' . $modname . ':addinstance'; 1929 if (!get_capability_info($capability)) { 1930 // Debug warning that the capability does not exist, but no more than once per page. 1931 static $warned = array(); 1932 $archetype = plugin_supports('mod', $modname, FEATURE_MOD_ARCHETYPE, MOD_ARCHETYPE_OTHER); 1933 if (!isset($warned[$modname]) && $archetype !== MOD_ARCHETYPE_SYSTEM) { 1934 debugging('The module ' . $modname . ' does not define the standard capability ' . 1935 $capability , DEBUG_DEVELOPER); 1936 $warned[$modname] = 1; 1937 } 1938 1939 // If the capability does not exist, the module can always be added. 1940 return true; 1941 } 1942 1943 $coursecontext = context_course::instance($course->id); 1944 return has_capability($capability, $coursecontext, $user); 1945 } 1946 1947 /** 1948 * Efficiently moves many courses around while maintaining 1949 * sortorder in order. 1950 * 1951 * @param array $courseids is an array of course ids 1952 * @param int $categoryid 1953 * @return bool success 1954 */ 1955 function move_courses($courseids, $categoryid) { 1956 global $DB; 1957 1958 if (empty($courseids)) { 1959 // Nothing to do. 1960 return false; 1961 } 1962 1963 if (!$category = $DB->get_record('course_categories', array('id' => $categoryid))) { 1964 return false; 1965 } 1966 1967 $courseids = array_reverse($courseids); 1968 $newparent = context_coursecat::instance($category->id); 1969 $i = 1; 1970 1971 list($where, $params) = $DB->get_in_or_equal($courseids); 1972 $dbcourses = $DB->get_records_select('course', 'id ' . $where, $params, '', 'id, category, shortname, fullname'); 1973 foreach ($dbcourses as $dbcourse) { 1974 $course = new stdClass(); 1975 $course->id = $dbcourse->id; 1976 $course->timemodified = time(); 1977 $course->category = $category->id; 1978 $course->sortorder = $category->sortorder + get_max_courses_in_category() - $i++; 1979 if ($category->visible == 0) { 1980 // Hide the course when moving into hidden category, do not update the visibleold flag - we want to get 1981 // to previous state if somebody unhides the category. 1982 $course->visible = 0; 1983 } 1984 1985 $DB->update_record('course', $course); 1986 1987 // Update context, so it can be passed to event. 1988 $context = context_course::instance($course->id); 1989 $context->update_moved($newparent); 1990 1991 // Trigger a course updated event. 1992 $event = \core\event\course_updated::create(array( 1993 'objectid' => $course->id, 1994 'context' => context_course::instance($course->id), 1995 'other' => array('shortname' => $dbcourse->shortname, 1996 'fullname' => $dbcourse->fullname, 1997 'updatedfields' => array('category' => $category->id)) 1998 )); 1999 $event->trigger(); 2000 } 2001 fix_course_sortorder(); 2002 cache_helper::purge_by_event('changesincourse'); 2003 2004 return true; 2005 } 2006 2007 /** 2008 * Returns the display name of the given section that the course prefers 2009 * 2010 * Implementation of this function is provided by course format 2011 * @see core_courseformat\base::get_section_name() 2012 * 2013 * @param int|stdClass $courseorid The course to get the section name for (object or just course id) 2014 * @param int|stdClass $section Section object from database or just field course_sections.section 2015 * @return string Display name that the course format prefers, e.g. "Week 2" 2016 */ 2017 function get_section_name($courseorid, $section) { 2018 return course_get_format($courseorid)->get_section_name($section); 2019 } 2020 2021 /** 2022 * Tells if current course format uses sections 2023 * 2024 * @param string $format Course format ID e.g. 'weeks' $course->format 2025 * @return bool 2026 */ 2027 function course_format_uses_sections($format) { 2028 $course = new stdClass(); 2029 $course->format = $format; 2030 return course_get_format($course)->uses_sections(); 2031 } 2032 2033 /** 2034 * Returns the information about the ajax support in the given source format 2035 * 2036 * The returned object's property (boolean)capable indicates that 2037 * the course format supports Moodle course ajax features. 2038 * 2039 * @param string $format 2040 * @return stdClass 2041 */ 2042 function course_format_ajax_support($format) { 2043 $course = new stdClass(); 2044 $course->format = $format; 2045 return course_get_format($course)->supports_ajax(); 2046 } 2047 2048 /** 2049 * Can the current user delete this course? 2050 * Course creators have exception, 2051 * 1 day after the creation they can sill delete the course. 2052 * @param int $courseid 2053 * @return boolean 2054 */ 2055 function can_delete_course($courseid) { 2056 global $USER; 2057 2058 $context = context_course::instance($courseid); 2059 2060 if (has_capability('moodle/course:delete', $context)) { 2061 return true; 2062 } 2063 2064 // hack: now try to find out if creator created this course recently (1 day) 2065 if (!has_capability('moodle/course:create', $context)) { 2066 return false; 2067 } 2068 2069 $since = time() - 60*60*24; 2070 $course = get_course($courseid); 2071 2072 if ($course->timecreated < $since) { 2073 return false; // Return if the course was not created in last 24 hours. 2074 } 2075 2076 $logmanger = get_log_manager(); 2077 $readers = $logmanger->get_readers('\core\log\sql_reader'); 2078 $reader = reset($readers); 2079 2080 if (empty($reader)) { 2081 return false; // No log reader found. 2082 } 2083 2084 // A proper reader. 2085 $select = "userid = :userid AND courseid = :courseid AND eventname = :eventname AND timecreated > :since"; 2086 $params = array('userid' => $USER->id, 'since' => $since, 'courseid' => $course->id, 'eventname' => '\core\event\course_created'); 2087 2088 return (bool)$reader->get_events_select_count($select, $params); 2089 } 2090 2091 /** 2092 * Save the Your name for 'Some role' strings. 2093 * 2094 * @param integer $courseid the id of this course. 2095 * @param array $data the data that came from the course settings form. 2096 */ 2097 function save_local_role_names($courseid, $data) { 2098 global $DB; 2099 $context = context_course::instance($courseid); 2100 2101 foreach ($data as $fieldname => $value) { 2102 if (strpos($fieldname, 'role_') !== 0) { 2103 continue; 2104 } 2105 list($ignored, $roleid) = explode('_', $fieldname); 2106 2107 // make up our mind whether we want to delete, update or insert 2108 if (!$value) { 2109 $DB->delete_records('role_names', array('contextid' => $context->id, 'roleid' => $roleid)); 2110 2111 } else if ($rolename = $DB->get_record('role_names', array('contextid' => $context->id, 'roleid' => $roleid))) { 2112 $rolename->name = $value; 2113 $DB->update_record('role_names', $rolename); 2114 2115 } else { 2116 $rolename = new stdClass; 2117 $rolename->contextid = $context->id; 2118 $rolename->roleid = $roleid; 2119 $rolename->name = $value; 2120 $DB->insert_record('role_names', $rolename); 2121 } 2122 // This will ensure the course contacts cache is purged.. 2123 core_course_category::role_assignment_changed($roleid, $context); 2124 } 2125 } 2126 2127 /** 2128 * Returns options to use in course overviewfiles filemanager 2129 * 2130 * @param null|stdClass|core_course_list_element|int $course either object that has 'id' property or just the course id; 2131 * may be empty if course does not exist yet (course create form) 2132 * @return array|null array of options such as maxfiles, maxbytes, accepted_types, etc. 2133 * or null if overviewfiles are disabled 2134 */ 2135 function course_overviewfiles_options($course) { 2136 global $CFG; 2137 if (empty($CFG->courseoverviewfileslimit)) { 2138 return null; 2139 } 2140 2141 // Create accepted file types based on config value, falling back to default all. 2142 $acceptedtypes = (new \core_form\filetypes_util)->normalize_file_types($CFG->courseoverviewfilesext); 2143 if (in_array('*', $acceptedtypes) || empty($acceptedtypes)) { 2144 $acceptedtypes = '*'; 2145 } 2146 2147 $options = array( 2148 'maxfiles' => $CFG->courseoverviewfileslimit, 2149 'maxbytes' => $CFG->maxbytes, 2150 'subdirs' => 0, 2151 'accepted_types' => $acceptedtypes 2152 ); 2153 if (!empty($course->id)) { 2154 $options['context'] = context_course::instance($course->id); 2155 } else if (is_int($course) && $course > 0) { 2156 $options['context'] = context_course::instance($course); 2157 } 2158 return $options; 2159 } 2160 2161 /** 2162 * Create a course and either return a $course object 2163 * 2164 * Please note this functions does not verify any access control, 2165 * the calling code is responsible for all validation (usually it is the form definition). 2166 * 2167 * @param array $editoroptions course description editor options 2168 * @param object $data - all the data needed for an entry in the 'course' table 2169 * @return object new course instance 2170 */ 2171 function create_course($data, $editoroptions = NULL) { 2172 global $DB, $CFG; 2173 2174 //check the categoryid - must be given for all new courses 2175 $category = $DB->get_record('course_categories', array('id'=>$data->category), '*', MUST_EXIST); 2176 2177 // Check if the shortname already exists. 2178 if (!empty($data->shortname)) { 2179 if ($DB->record_exists('course', array('shortname' => $data->shortname))) { 2180 throw new moodle_exception('shortnametaken', '', '', $data->shortname); 2181 } 2182 } 2183 2184 // Check if the idnumber already exists. 2185 if (!empty($data->idnumber)) { 2186 if ($DB->record_exists('course', array('idnumber' => $data->idnumber))) { 2187 throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber); 2188 } 2189 } 2190 2191 if (empty($CFG->enablecourserelativedates)) { 2192 // Make sure we're not setting the relative dates mode when the setting is disabled. 2193 unset($data->relativedatesmode); 2194 } 2195 2196 if ($errorcode = course_validate_dates((array)$data)) { 2197 throw new moodle_exception($errorcode); 2198 } 2199 2200 // Check if timecreated is given. 2201 $data->timecreated = !empty($data->timecreated) ? $data->timecreated : time(); 2202 $data->timemodified = $data->timecreated; 2203 2204 // place at beginning of any category 2205 $data->sortorder = 0; 2206 2207 if ($editoroptions) { 2208 // summary text is updated later, we need context to store the files first 2209 $data->summary = ''; 2210 $data->summary_format = $data->summary_editor['format']; 2211 } 2212 2213 // Get default completion settings as a fallback in case the enablecompletion field is not set. 2214 $courseconfig = get_config('moodlecourse'); 2215 $defaultcompletion = !empty($CFG->enablecompletion) ? $courseconfig->enablecompletion : COMPLETION_DISABLED; 2216 $enablecompletion = $data->enablecompletion ?? $defaultcompletion; 2217 // Unset showcompletionconditions when completion tracking is not enabled for the course. 2218 if ($enablecompletion == COMPLETION_DISABLED) { 2219 unset($data->showcompletionconditions); 2220 } else if (!isset($data->showcompletionconditions)) { 2221 // Show completion conditions should have a default value when completion is enabled. Set it to the site defaults. 2222 // This scenario can happen when a course is created through data generators or through a web service. 2223 $data->showcompletionconditions = $courseconfig->showcompletionconditions; 2224 } 2225 2226 if (!isset($data->visible)) { 2227 // data not from form, add missing visibility info 2228 $data->visible = $category->visible; 2229 } 2230 $data->visibleold = $data->visible; 2231 2232 $newcourseid = $DB->insert_record('course', $data); 2233 $context = context_course::instance($newcourseid, MUST_EXIST); 2234 2235 if ($editoroptions) { 2236 // Save the files used in the summary editor and store 2237 $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0); 2238 $DB->set_field('course', 'summary', $data->summary, array('id'=>$newcourseid)); 2239 $DB->set_field('course', 'summaryformat', $data->summary_format, array('id'=>$newcourseid)); 2240 } 2241 if ($overviewfilesoptions = course_overviewfiles_options($newcourseid)) { 2242 // Save the course overviewfiles 2243 $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0); 2244 } 2245 2246 // update course format options 2247 course_get_format($newcourseid)->update_course_format_options($data); 2248 2249 $course = course_get_format($newcourseid)->get_course(); 2250 2251 fix_course_sortorder(); 2252 // purge appropriate caches in case fix_course_sortorder() did not change anything 2253 cache_helper::purge_by_event('changesincourse'); 2254 2255 // Trigger a course created event. 2256 $event = \core\event\course_created::create(array( 2257 'objectid' => $course->id, 2258 'context' => $context, 2259 'other' => array('shortname' => $course->shortname, 2260 'fullname' => $course->fullname) 2261 )); 2262 2263 $event->trigger(); 2264 2265 // Setup the blocks 2266 blocks_add_default_course_blocks($course); 2267 2268 // Create default section and initial sections if specified (unless they've already been created earlier). 2269 // We do not want to call course_create_sections_if_missing() because to avoid creating course cache. 2270 $numsections = isset($data->numsections) ? $data->numsections : 0; 2271 $existingsections = $DB->get_fieldset_sql('SELECT section from {course_sections} WHERE course = ?', [$newcourseid]); 2272 $newsections = array_diff(range(0, $numsections), $existingsections); 2273 foreach ($newsections as $sectionnum) { 2274 course_create_section($newcourseid, $sectionnum, true); 2275 } 2276 2277 // Save any custom role names. 2278 save_local_role_names($course->id, (array)$data); 2279 2280 // set up enrolments 2281 enrol_course_updated(true, $course, $data); 2282 2283 // Update course tags. 2284 if (isset($data->tags)) { 2285 core_tag_tag::set_item_tags('core', 'course', $course->id, $context, $data->tags); 2286 } 2287 // Set up communication. 2288 if (core_communication\api::is_available()) { 2289 // Check for default provider config setting. 2290 $defaultprovider = get_config('moodlecourse', 'coursecommunicationprovider'); 2291 $provider = (isset($data->selectedcommunication)) ? $data->selectedcommunication : $defaultprovider; 2292 2293 if (!empty($provider)) { 2294 // Prepare the communication api data. 2295 $courseimage = course_get_courseimage($course); 2296 $communicationroomname = !empty($data->communicationroomname) ? $data->communicationroomname : $data->fullname; 2297 2298 // Communication api call. 2299 $communication = \core_communication\api::load_by_instance( 2300 context: $context, 2301 component: 'core_course', 2302 instancetype: 'coursecommunication', 2303 instanceid: $course->id, 2304 provider: $provider, 2305 ); 2306 $communication->create_and_configure_room( 2307 $communicationroomname, 2308 $courseimage ?: null, 2309 $data, 2310 ); 2311 } 2312 } 2313 2314 // Save custom fields if there are any of them in the form. 2315 $handler = core_course\customfield\course_handler::create(); 2316 // Make sure to set the handler's parent context first. 2317 $coursecatcontext = context_coursecat::instance($category->id); 2318 $handler->set_parent_context($coursecatcontext); 2319 // Save the custom field data. 2320 $data->id = $course->id; 2321 $handler->instance_form_save($data, true); 2322 2323 return $course; 2324 } 2325 2326 /** 2327 * Update a course. 2328 * 2329 * Please note this functions does not verify any access control, 2330 * the calling code is responsible for all validation (usually it is the form definition). 2331 * 2332 * @param object $data - all the data needed for an entry in the 'course' table 2333 * @param array $editoroptions course description editor options 2334 * @return void 2335 */ 2336 function update_course($data, $editoroptions = NULL) { 2337 global $DB, $CFG; 2338 2339 // Prevent changes on front page course. 2340 if ($data->id == SITEID) { 2341 throw new moodle_exception('invalidcourse', 'error'); 2342 } 2343 2344 $oldcourse = course_get_format($data->id)->get_course(); 2345 $context = context_course::instance($oldcourse->id); 2346 2347 // Make sure we're not changing whatever the course's relativedatesmode setting is. 2348 unset($data->relativedatesmode); 2349 2350 // Capture the updated fields for the log data. 2351 $updatedfields = []; 2352 foreach (get_object_vars($oldcourse) as $field => $value) { 2353 if ($field == 'summary_editor') { 2354 if (($data->$field)['text'] !== $value['text']) { 2355 // The summary might be very long, we don't wan't to fill up the log record with the full text. 2356 $updatedfields[$field] = '(updated)'; 2357 } 2358 } else if ($field == 'tags' && isset($data->tags)) { 2359 // Tags might not have the same array keys, just check the values. 2360 if (array_values($data->$field) !== array_values($value)) { 2361 $updatedfields[$field] = $data->$field; 2362 } 2363 } else { 2364 if (isset($data->$field) && $data->$field != $value) { 2365 $updatedfields[$field] = $data->$field; 2366 } 2367 } 2368 } 2369 2370 $data->timemodified = time(); 2371 2372 if ($editoroptions) { 2373 $data = file_postupdate_standard_editor($data, 'summary', $editoroptions, $context, 'course', 'summary', 0); 2374 } 2375 if ($overviewfilesoptions = course_overviewfiles_options($data->id)) { 2376 $data = file_postupdate_standard_filemanager($data, 'overviewfiles', $overviewfilesoptions, $context, 'course', 'overviewfiles', 0); 2377 } 2378 2379 // Check we don't have a duplicate shortname. 2380 if (!empty($data->shortname) && $oldcourse->shortname != $data->shortname) { 2381 if ($DB->record_exists_sql('SELECT id from {course} WHERE shortname = ? AND id <> ?', array($data->shortname, $data->id))) { 2382 throw new moodle_exception('shortnametaken', '', '', $data->shortname); 2383 } 2384 } 2385 2386 // Check we don't have a duplicate idnumber. 2387 if (!empty($data->idnumber) && $oldcourse->idnumber != $data->idnumber) { 2388 if ($DB->record_exists_sql('SELECT id from {course} WHERE idnumber = ? AND id <> ?', array($data->idnumber, $data->id))) { 2389 throw new moodle_exception('courseidnumbertaken', '', '', $data->idnumber); 2390 } 2391 } 2392 2393 if ($errorcode = course_validate_dates((array)$data)) { 2394 throw new moodle_exception($errorcode); 2395 } 2396 2397 if (!isset($data->category) or empty($data->category)) { 2398 // prevent nulls and 0 in category field 2399 unset($data->category); 2400 } 2401 $changesincoursecat = $movecat = (isset($data->category) and $oldcourse->category != $data->category); 2402 2403 if (!isset($data->visible)) { 2404 // data not from form, add missing visibility info 2405 $data->visible = $oldcourse->visible; 2406 } 2407 2408 if ($data->visible != $oldcourse->visible) { 2409 // reset the visibleold flag when manually hiding/unhiding course 2410 $data->visibleold = $data->visible; 2411 $changesincoursecat = true; 2412 } else { 2413 if ($movecat) { 2414 $newcategory = $DB->get_record('course_categories', array('id'=>$data->category)); 2415 if (empty($newcategory->visible)) { 2416 // make sure when moving into hidden category the course is hidden automatically 2417 $data->visible = 0; 2418 } 2419 } 2420 } 2421 2422 // Set newsitems to 0 if format does not support announcements. 2423 if (isset($data->format)) { 2424 $newcourseformat = course_get_format((object)['format' => $data->format]); 2425 if (!$newcourseformat->supports_news()) { 2426 $data->newsitems = 0; 2427 } 2428 } 2429 2430 // Set showcompletionconditions to null when completion tracking has been disabled for the course. 2431 if (isset($data->enablecompletion) && $data->enablecompletion == COMPLETION_DISABLED) { 2432 $data->showcompletionconditions = null; 2433 } 2434 2435 // Check if provider is selected. 2436 $provider = $data->selectedcommunication ?? null; 2437 // If the course moved to hidden category, set provider to none. 2438 if ($changesincoursecat && empty($data->visible)) { 2439 $provider = 'none'; 2440 } 2441 2442 // Attempt to get the communication provider if it wasn't provided in the data. 2443 if (empty($provider) && core_communication\api::is_available()) { 2444 $provider = \core_communication\api::load_by_instance( 2445 context: $context, 2446 component: 'core_course', 2447 instancetype: 'coursecommunication', 2448 instanceid: $data->id, 2449 )->get_provider(); 2450 } 2451 2452 // Communication api call. 2453 if (!empty($provider) && core_communication\api::is_available()) { 2454 // Prepare the communication api data. 2455 $courseimage = course_get_courseimage($data); 2456 2457 // This nasty logic is here because of hide course doesn't pass anything in the data object. 2458 if (!empty($data->communicationroomname)) { 2459 $communicationroomname = $data->communicationroomname; 2460 } else { 2461 $communicationroomname = $data->fullname ?? $oldcourse->fullname; 2462 } 2463 2464 // Update communication room membership of enrolled users. 2465 require_once($CFG->libdir . '/enrollib.php'); 2466 $courseusers = enrol_get_course_users($data->id); 2467 $enrolledusers = []; 2468 2469 foreach ($courseusers as $user) { 2470 $enrolledusers[] = $user->id; 2471 } 2472 2473 // Existing communication provider. 2474 $communication = \core_communication\api::load_by_instance( 2475 context: $context, 2476 component: 'core_course', 2477 instancetype: 'coursecommunication', 2478 instanceid: $data->id, 2479 ); 2480 $existingprovider = $communication->get_provider(); 2481 $addusersrequired = false; 2482 $enablenewprovider = false; 2483 $instanceexists = true; 2484 2485 // Action required changes if provider has changed. 2486 if ($provider !== $existingprovider) { 2487 // Provider changed, flag new one to be enabled. 2488 $enablenewprovider = true; 2489 2490 // If provider set to none, remove all the members from previous provider. 2491 if ($provider === 'none' && $existingprovider !== '') { 2492 $communication->remove_members_from_room($enrolledusers); 2493 } else if ( 2494 // If previous provider was not none and current provider is not none, 2495 // remove members from previous provider. 2496 $existingprovider !== '' && 2497 $existingprovider !== 'none' 2498 ) { 2499 $communication->remove_members_from_room($enrolledusers); 2500 $addusersrequired = true; 2501 } else if ( 2502 // If previous provider was none and current provider is not none, 2503 // remove members from previous provider. 2504 ($existingprovider === '' || $existingprovider === 'none') 2505 ) { 2506 $addusersrequired = true; 2507 } 2508 2509 // Disable previous provider, if one was enabled. 2510 if ($existingprovider !== '' && $existingprovider !== 'none') { 2511 $communication->update_room( 2512 active: \core_communication\processor::PROVIDER_INACTIVE, 2513 ); 2514 } 2515 2516 // Switch to the newly selected provider so it can be updated. 2517 if ($provider !== 'none') { 2518 $communication = \core_communication\api::load_by_instance( 2519 context: $context, 2520 component: 'core_course', 2521 instancetype: 'coursecommunication', 2522 instanceid: $data->id, 2523 provider: $provider, 2524 ); 2525 2526 // Create it if it does not exist. 2527 if ($communication->get_provider() === '') { 2528 $communication->create_and_configure_room( 2529 communicationroomname: $communicationroomname, 2530 avatar: $courseimage, 2531 instance: $data 2532 ); 2533 2534 $communication = \core_communication\api::load_by_instance( 2535 context: $context, 2536 component: 'core_course', 2537 instancetype: 'coursecommunication', 2538 instanceid: $data->id, 2539 provider: $provider, 2540 ); 2541 2542 $addusersrequired = true; 2543 $instanceexists = false; 2544 } 2545 } 2546 } 2547 2548 if ($provider !== 'none' && $instanceexists) { 2549 // Update the currently enabled provider's room data. 2550 // Newly created providers do not need to run this, the create process handles it. 2551 $communication->update_room( 2552 active: $enablenewprovider ? \core_communication\processor::PROVIDER_ACTIVE : null, 2553 communicationroomname: $communicationroomname, 2554 avatar: $courseimage, 2555 instance: $data, 2556 ); 2557 } 2558 2559 // Complete room membership tasks if required. 2560 // Newly created providers complete the user mapping but do not queue the task 2561 // (it will be handled by the room creation task). 2562 if ($addusersrequired) { 2563 $communication->add_members_to_room($enrolledusers, $instanceexists); 2564 } 2565 } 2566 2567 // Update custom fields if there are any of them in the form. 2568 $handler = core_course\customfield\course_handler::create(); 2569 $handler->instance_form_save($data); 2570 2571 // Update with the new data 2572 $DB->update_record('course', $data); 2573 // make sure the modinfo cache is reset 2574 rebuild_course_cache($data->id); 2575 2576 // Purge course image cache in case if course image has been updated. 2577 \cache::make('core', 'course_image')->delete($data->id); 2578 2579 // update course format options with full course data 2580 course_get_format($data->id)->update_course_format_options($data, $oldcourse); 2581 2582 $course = $DB->get_record('course', array('id'=>$data->id)); 2583 2584 if ($movecat) { 2585 $newparent = context_coursecat::instance($course->category); 2586 $context->update_moved($newparent); 2587 } 2588 $fixcoursesortorder = $movecat || (isset($data->sortorder) && ($oldcourse->sortorder != $data->sortorder)); 2589 if ($fixcoursesortorder) { 2590 fix_course_sortorder(); 2591 } 2592 2593 // purge appropriate caches in case fix_course_sortorder() did not change anything 2594 cache_helper::purge_by_event('changesincourse'); 2595 if ($changesincoursecat) { 2596 cache_helper::purge_by_event('changesincoursecat'); 2597 } 2598 2599 // Test for and remove blocks which aren't appropriate anymore 2600 blocks_remove_inappropriate($course); 2601 2602 // Save any custom role names. 2603 save_local_role_names($course->id, $data); 2604 2605 // update enrol settings 2606 enrol_course_updated(false, $course, $data); 2607 2608 // Update course tags. 2609 if (isset($data->tags)) { 2610 core_tag_tag::set_item_tags('core', 'course', $course->id, context_course::instance($course->id), $data->tags); 2611 } 2612 2613 // Trigger a course updated event. 2614 $event = \core\event\course_updated::create(array( 2615 'objectid' => $course->id, 2616 'context' => context_course::instance($course->id), 2617 'other' => array('shortname' => $course->shortname, 2618 'fullname' => $course->fullname, 2619 'updatedfields' => $updatedfields) 2620 )); 2621 2622 $event->trigger(); 2623 2624 if ($oldcourse->format !== $course->format) { 2625 // Remove all options stored for the previous format 2626 // We assume that new course format migrated everything it needed watching trigger 2627 // 'course_updated' and in method format_XXX::update_course_format_options() 2628 $DB->delete_records('course_format_options', 2629 array('courseid' => $course->id, 'format' => $oldcourse->format)); 2630 } 2631 } 2632 2633 /** 2634 * Calculate the average number of enrolled participants per course. 2635 * 2636 * This is intended for statistics purposes during the site registration. Only visible courses are taken into account. 2637 * Front page enrolments are excluded. 2638 * 2639 * @param bool $onlyactive Consider only active enrolments in enabled plugins and obey the enrolment time restrictions. 2640 * @param int $lastloginsince If specified, count only users who logged in after this timestamp. 2641 * @return float 2642 */ 2643 function average_number_of_participants(bool $onlyactive = false, int $lastloginsince = null): float { 2644 global $DB; 2645 2646 $params = []; 2647 2648 $sql = "SELECT DISTINCT ue.userid, e.courseid 2649 FROM {user_enrolments} ue 2650 JOIN {enrol} e ON e.id = ue.enrolid 2651 JOIN {course} c ON c.id = e.courseid "; 2652 2653 if ($onlyactive || $lastloginsince) { 2654 $sql .= "JOIN {user} u ON u.id = ue.userid "; 2655 } 2656 2657 $sql .= "WHERE e.courseid <> " . SITEID . " AND c.visible = 1 "; 2658 2659 if ($onlyactive) { 2660 $sql .= "AND ue.status = :active 2661 AND e.status = :enabled 2662 AND ue.timestart < :now1 2663 AND (ue.timeend = 0 OR ue.timeend > :now2) "; 2664 2665 // Same as in the enrollib - the rounding should help caching in the database. 2666 $now = round(time(), -2); 2667 2668 $params += [ 2669 'active' => ENROL_USER_ACTIVE, 2670 'enabled' => ENROL_INSTANCE_ENABLED, 2671 'now1' => $now, 2672 'now2' => $now, 2673 ]; 2674 } 2675 2676 if ($lastloginsince) { 2677 $sql .= "AND u.lastlogin > :lastlogin "; 2678 $params['lastlogin'] = $lastloginsince; 2679 } 2680 2681 $sql = "SELECT COUNT(*) 2682 FROM ($sql) total"; 2683 2684 $enrolmenttotal = $DB->count_records_sql($sql, $params); 2685 2686 // Get the number of visible courses (exclude the front page). 2687 $coursetotal = $DB->count_records('course', ['visible' => 1]); 2688 $coursetotal = $coursetotal - 1; 2689 2690 if (empty($coursetotal)) { 2691 $participantaverage = 0; 2692 2693 } else { 2694 $participantaverage = $enrolmenttotal / $coursetotal; 2695 } 2696 2697 return $participantaverage; 2698 } 2699 2700 /** 2701 * Average number of course modules 2702 * @return integer 2703 */ 2704 function average_number_of_courses_modules() { 2705 global $DB, $SITE; 2706 2707 //count total of visible course module (except front page) 2708 $sql = 'SELECT COUNT(*) FROM ( 2709 SELECT cm.course, cm.module 2710 FROM {course} c, {course_modules} cm 2711 WHERE c.id = cm.course 2712 AND c.id <> :siteid 2713 AND cm.visible = 1 2714 AND c.visible = 1) total'; 2715 $params = array('siteid' => $SITE->id); 2716 $moduletotal = $DB->count_records_sql($sql, $params); 2717 2718 2719 //count total of visible courses (minus front page) 2720 $coursetotal = $DB->count_records('course', array('visible' => 1)); 2721 $coursetotal = $coursetotal - 1 ; 2722 2723 //average of course module 2724 if (empty($coursetotal)) { 2725 $coursemoduleaverage = 0; 2726 } else { 2727 $coursemoduleaverage = $moduletotal / $coursetotal; 2728 } 2729 2730 return $coursemoduleaverage; 2731 } 2732 2733 /** 2734 * This class pertains to course requests and contains methods associated with 2735 * create, approving, and removing course requests. 2736 * 2737 * Please note we do not allow embedded images here because there is no context 2738 * to store them with proper access control. 2739 * 2740 * @copyright 2009 Sam Hemelryk 2741 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 2742 * @since Moodle 2.0 2743 * 2744 * @property-read int $id 2745 * @property-read string $fullname 2746 * @property-read string $shortname 2747 * @property-read string $summary 2748 * @property-read int $summaryformat 2749 * @property-read int $summarytrust 2750 * @property-read string $reason 2751 * @property-read int $requester 2752 */ 2753 class course_request { 2754 2755 /** 2756 * This is the stdClass that stores the properties for the course request 2757 * and is externally accessed through the __get magic method 2758 * @var stdClass 2759 */ 2760 protected $properties; 2761 2762 /** 2763 * An array of options for the summary editor used by course request forms. 2764 * This is initially set by {@link summary_editor_options()} 2765 * @var array 2766 * @static 2767 */ 2768 protected static $summaryeditoroptions; 2769 2770 /** 2771 * Static function to prepare the summary editor for working with a course 2772 * request. 2773 * 2774 * @static 2775 * @param null|stdClass $data Optional, an object containing the default values 2776 * for the form, these may be modified when preparing the 2777 * editor so this should be called before creating the form 2778 * @return stdClass An object that can be used to set the default values for 2779 * an mforms form 2780 */ 2781 public static function prepare($data=null) { 2782 if ($data === null) { 2783 $data = new stdClass; 2784 } 2785 $data = file_prepare_standard_editor($data, 'summary', self::summary_editor_options()); 2786 return $data; 2787 } 2788 2789 /** 2790 * Static function to create a new course request when passed an array of properties 2791 * for it. 2792 * 2793 * This function also handles saving any files that may have been used in the editor 2794 * 2795 * @static 2796 * @param stdClass $data 2797 * @return course_request The newly created course request 2798 */ 2799 public static function create($data) { 2800 global $USER, $DB, $CFG; 2801 $data->requester = $USER->id; 2802 2803 // Setting the default category if none set. 2804 if (empty($data->category) || !empty($CFG->lockrequestcategory)) { 2805 $data->category = $CFG->defaultrequestcategory; 2806 } 2807 2808 // Summary is a required field so copy the text over 2809 $data->summary = $data->summary_editor['text']; 2810 $data->summaryformat = $data->summary_editor['format']; 2811 2812 $data->id = $DB->insert_record('course_request', $data); 2813 2814 // Create a new course_request object and return it 2815 $request = new course_request($data); 2816 2817 // Notify the admin if required. 2818 if ($users = get_users_from_config($CFG->courserequestnotify, 'moodle/site:approvecourse')) { 2819 2820 $a = new stdClass; 2821 $a->link = "$CFG->wwwroot/course/pending.php"; 2822 $a->user = fullname($USER); 2823 $subject = get_string('courserequest'); 2824 $message = get_string('courserequestnotifyemail', 'admin', $a); 2825 foreach ($users as $user) { 2826 $request->notify($user, $USER, 'courserequested', $subject, $message); 2827 } 2828 } 2829 2830 return $request; 2831 } 2832 2833 /** 2834 * Returns an array of options to use with a summary editor 2835 * 2836 * @uses course_request::$summaryeditoroptions 2837 * @return array An array of options to use with the editor 2838 */ 2839 public static function summary_editor_options() { 2840 global $CFG; 2841 if (self::$summaryeditoroptions === null) { 2842 self::$summaryeditoroptions = array('maxfiles' => 0, 'maxbytes'=>0); 2843 } 2844 return self::$summaryeditoroptions; 2845 } 2846 2847 /** 2848 * Loads the properties for this course request object. Id is required and if 2849 * only id is provided then we load the rest of the properties from the database 2850 * 2851 * @param stdClass|int $properties Either an object containing properties 2852 * or the course_request id to load 2853 */ 2854 public function __construct($properties) { 2855 global $DB; 2856 if (empty($properties->id)) { 2857 if (empty($properties)) { 2858 throw new coding_exception('You must provide a course request id when creating a course_request object'); 2859 } 2860 $id = $properties; 2861 $properties = new stdClass; 2862 $properties->id = (int)$id; 2863 unset($id); 2864 } 2865 if (empty($properties->requester)) { 2866 if (!($this->properties = $DB->get_record('course_request', array('id' => $properties->id)))) { 2867 throw new \moodle_exception('unknowncourserequest'); 2868 } 2869 } else { 2870 $this->properties = $properties; 2871 } 2872 $this->properties->collision = null; 2873 } 2874 2875 /** 2876 * Returns the requested property 2877 * 2878 * @param string $key 2879 * @return mixed 2880 */ 2881 public function __get($key) { 2882 return $this->properties->$key; 2883 } 2884 2885 /** 2886 * Override this to ensure empty($request->blah) calls return a reliable answer... 2887 * 2888 * This is required because we define the __get method 2889 * 2890 * @param mixed $key 2891 * @return bool True is it not empty, false otherwise 2892 */ 2893 public function __isset($key) { 2894 return (!empty($this->properties->$key)); 2895 } 2896 2897 /** 2898 * Returns the user who requested this course 2899 * 2900 * Uses a static var to cache the results and cut down the number of db queries 2901 * 2902 * @staticvar array $requesters An array of cached users 2903 * @return stdClass The user who requested the course 2904 */ 2905 public function get_requester() { 2906 global $DB; 2907 static $requesters= array(); 2908 if (!array_key_exists($this->properties->requester, $requesters)) { 2909 $requesters[$this->properties->requester] = $DB->get_record('user', array('id'=>$this->properties->requester)); 2910 } 2911 return $requesters[$this->properties->requester]; 2912 } 2913 2914 /** 2915 * Checks that the shortname used by the course does not conflict with any other 2916 * courses that exist 2917 * 2918 * @param string|null $shortnamemark The string to append to the requests shortname 2919 * should a conflict be found 2920 * @return bool true is there is a conflict, false otherwise 2921 */ 2922 public function check_shortname_collision($shortnamemark = '[*]') { 2923 global $DB; 2924 2925 if ($this->properties->collision !== null) { 2926 return $this->properties->collision; 2927 } 2928 2929 if (empty($this->properties->shortname)) { 2930 debugging('Attempting to check a course request shortname before it has been set', DEBUG_DEVELOPER); 2931 $this->properties->collision = false; 2932 } else if ($DB->record_exists('course', array('shortname' => $this->properties->shortname))) { 2933 if (!empty($shortnamemark)) { 2934 $this->properties->shortname .= ' '.$shortnamemark; 2935 } 2936 $this->properties->collision = true; 2937 } else { 2938 $this->properties->collision = false; 2939 } 2940 return $this->properties->collision; 2941 } 2942 2943 /** 2944 * Checks user capability to approve a requested course 2945 * 2946 * If course was requested without category for some reason (might happen if $CFG->defaultrequestcategory is 2947 * misconfigured), we check capabilities 'moodle/site:approvecourse' and 'moodle/course:changecategory'. 2948 * 2949 * @return bool 2950 */ 2951 public function can_approve() { 2952 global $CFG; 2953 $category = null; 2954 if ($this->properties->category) { 2955 $category = core_course_category::get($this->properties->category, IGNORE_MISSING); 2956 } else if ($CFG->defaultrequestcategory) { 2957 $category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING); 2958 } 2959 if ($category) { 2960 return has_capability('moodle/site:approvecourse', $category->get_context()); 2961 } 2962 2963 // We can not determine the context where the course should be created. The approver should have 2964 // both capabilities to approve courses and change course category in the system context. 2965 return has_all_capabilities(['moodle/site:approvecourse', 'moodle/course:changecategory'], context_system::instance()); 2966 } 2967 2968 /** 2969 * Returns the category where this course request should be created 2970 * 2971 * Note that we don't check here that user has a capability to view 2972 * hidden categories if he has capabilities 'moodle/site:approvecourse' and 2973 * 'moodle/course:changecategory' 2974 * 2975 * @return core_course_category 2976 */ 2977 public function get_category() { 2978 global $CFG; 2979 if ($this->properties->category && ($category = core_course_category::get($this->properties->category, IGNORE_MISSING))) { 2980 return $category; 2981 } else if ($CFG->defaultrequestcategory && 2982 ($category = core_course_category::get($CFG->defaultrequestcategory, IGNORE_MISSING))) { 2983 return $category; 2984 } else { 2985 return core_course_category::get_default(); 2986 } 2987 } 2988 2989 /** 2990 * This function approves the request turning it into a course 2991 * 2992 * This function converts the course request into a course, at the same time 2993 * transferring any files used in the summary to the new course and then removing 2994 * the course request and the files associated with it. 2995 * 2996 * @return int The id of the course that was created from this request 2997 */ 2998 public function approve() { 2999 global $CFG, $DB, $USER; 3000 3001 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); 3002 3003 $user = $DB->get_record('user', array('id' => $this->properties->requester, 'deleted'=>0), '*', MUST_EXIST); 3004 3005 $courseconfig = get_config('moodlecourse'); 3006 3007 // Transfer appropriate settings 3008 $data = clone($this->properties); 3009 unset($data->id); 3010 unset($data->reason); 3011 unset($data->requester); 3012 3013 // Set category 3014 $category = $this->get_category(); 3015 $data->category = $category->id; 3016 // Set misc settings 3017 $data->requested = 1; 3018 3019 // Apply course default settings 3020 $data->format = $courseconfig->format; 3021 $data->newsitems = $courseconfig->newsitems; 3022 $data->showgrades = $courseconfig->showgrades; 3023 $data->showreports = $courseconfig->showreports; 3024 $data->maxbytes = $courseconfig->maxbytes; 3025 $data->groupmode = $courseconfig->groupmode; 3026 $data->groupmodeforce = $courseconfig->groupmodeforce; 3027 $data->visible = $courseconfig->visible; 3028 $data->visibleold = $data->visible; 3029 $data->lang = $courseconfig->lang; 3030 $data->enablecompletion = $courseconfig->enablecompletion; 3031 $data->numsections = $courseconfig->numsections; 3032 $data->startdate = usergetmidnight(time()); 3033 if ($courseconfig->courseenddateenabled) { 3034 $data->enddate = usergetmidnight(time()) + $courseconfig->courseduration; 3035 } 3036 3037 list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names(0, $data->fullname, $data->shortname); 3038 3039 $course = create_course($data); 3040 $context = context_course::instance($course->id, MUST_EXIST); 3041 3042 // add enrol instances 3043 if (!$DB->record_exists('enrol', array('courseid'=>$course->id, 'enrol'=>'manual'))) { 3044 if ($manual = enrol_get_plugin('manual')) { 3045 $manual->add_default_instance($course); 3046 } 3047 } 3048 3049 // enrol the requester as teacher if necessary 3050 if (!empty($CFG->creatornewroleid) and !is_viewing($context, $user, 'moodle/role:assign') and !is_enrolled($context, $user, 'moodle/role:assign')) { 3051 enrol_try_internal_enrol($course->id, $user->id, $CFG->creatornewroleid); 3052 } 3053 3054 $this->delete(); 3055 3056 $a = new stdClass(); 3057 $a->name = format_string($course->fullname, true, array('context' => context_course::instance($course->id))); 3058 $a->url = $CFG->wwwroot.'/course/view.php?id=' . $course->id; 3059 $this->notify($user, $USER, 'courserequestapproved', get_string('courseapprovedsubject'), get_string('courseapprovedemail2', 'moodle', $a), $course->id); 3060 3061 return $course->id; 3062 } 3063 3064 /** 3065 * Reject a course request 3066 * 3067 * This function rejects a course request, emailing the requesting user the 3068 * provided notice and then removing the request from the database 3069 * 3070 * @param string $notice The message to display to the user 3071 */ 3072 public function reject($notice) { 3073 global $USER, $DB; 3074 $user = $DB->get_record('user', array('id' => $this->properties->requester), '*', MUST_EXIST); 3075 $this->notify($user, $USER, 'courserequestrejected', get_string('courserejectsubject'), get_string('courserejectemail', 'moodle', $notice)); 3076 $this->delete(); 3077 } 3078 3079 /** 3080 * Deletes the course request and any associated files 3081 */ 3082 public function delete() { 3083 global $DB; 3084 $DB->delete_records('course_request', array('id' => $this->properties->id)); 3085 } 3086 3087 /** 3088 * Send a message from one user to another using events_trigger 3089 * 3090 * @param object $touser 3091 * @param object $fromuser 3092 * @param string $name 3093 * @param string $subject 3094 * @param string $message 3095 * @param int|null $courseid 3096 */ 3097 protected function notify($touser, $fromuser, $name, $subject, $message, $courseid = null) { 3098 $eventdata = new \core\message\message(); 3099 $eventdata->courseid = empty($courseid) ? SITEID : $courseid; 3100 $eventdata->component = 'moodle'; 3101 $eventdata->name = $name; 3102 $eventdata->userfrom = $fromuser; 3103 $eventdata->userto = $touser; 3104 $eventdata->subject = $subject; 3105 $eventdata->fullmessage = $message; 3106 $eventdata->fullmessageformat = FORMAT_PLAIN; 3107 $eventdata->fullmessagehtml = ''; 3108 $eventdata->smallmessage = ''; 3109 $eventdata->notification = 1; 3110 message_send($eventdata); 3111 } 3112 3113 /** 3114 * Checks if current user can request a course in this context 3115 * 3116 * @param context $context 3117 * @return bool 3118 */ 3119 public static function can_request(context $context) { 3120 global $CFG; 3121 if (empty($CFG->enablecourserequests)) { 3122 return false; 3123 } 3124 if (has_capability('moodle/course:create', $context)) { 3125 return false; 3126 } 3127 3128 if ($context instanceof context_system) { 3129 $defaultcontext = context_coursecat::instance($CFG->defaultrequestcategory, IGNORE_MISSING); 3130 return $defaultcontext && 3131 has_capability('moodle/course:request', $defaultcontext); 3132 } else if ($context instanceof context_coursecat) { 3133 if (!$CFG->lockrequestcategory || $CFG->defaultrequestcategory == $context->instanceid) { 3134 return has_capability('moodle/course:request', $context); 3135 } 3136 } 3137 return false; 3138 } 3139 } 3140 3141 /** 3142 * Return a list of page types 3143 * @param string $pagetype current page type 3144 * @param context $parentcontext Block's parent context 3145 * @param context $currentcontext Current context of block 3146 * @return array array of page types 3147 */ 3148 function course_page_type_list($pagetype, $parentcontext, $currentcontext) { 3149 if ($pagetype === 'course-index' || $pagetype === 'course-index-category') { 3150 // For courses and categories browsing pages (/course/index.php) add option to show on ANY category page 3151 $pagetypes = array('*' => get_string('page-x', 'pagetype'), 3152 'course-index-*' => get_string('page-course-index-x', 'pagetype'), 3153 ); 3154 } else if ($currentcontext && (!($coursecontext = $currentcontext->get_course_context(false)) || $coursecontext->instanceid == SITEID)) { 3155 // We know for sure that despite pagetype starts with 'course-' this is not a page in course context (i.e. /course/search.php, etc.) 3156 $pagetypes = array('*' => get_string('page-x', 'pagetype')); 3157 } else { 3158 // Otherwise consider it a page inside a course even if $currentcontext is null 3159 $pagetypes = array('*' => get_string('page-x', 'pagetype'), 3160 'course-*' => get_string('page-course-x', 'pagetype'), 3161 'course-view-*' => get_string('page-course-view-x', 'pagetype') 3162 ); 3163 } 3164 return $pagetypes; 3165 } 3166 3167 /** 3168 * Determine whether course ajax should be enabled for the specified course 3169 * 3170 * @param stdClass $course The course to test against 3171 * @return boolean Whether course ajax is enabled or note 3172 */ 3173 function course_ajax_enabled($course) { 3174 global $CFG, $PAGE, $SITE; 3175 3176 // The user must be editing for AJAX to be included 3177 if (!$PAGE->user_is_editing()) { 3178 return false; 3179 } 3180 3181 // Check that the theme suports 3182 if (!$PAGE->theme->enablecourseajax) { 3183 return false; 3184 } 3185 3186 // Check that the course format supports ajax functionality 3187 // The site 'format' doesn't have information on course format support 3188 if ($SITE->id !== $course->id) { 3189 $courseformatajaxsupport = course_format_ajax_support($course->format); 3190 if (!$courseformatajaxsupport->capable) { 3191 return false; 3192 } 3193 } 3194 3195 // All conditions have been met so course ajax should be enabled 3196 return true; 3197 } 3198 3199 /** 3200 * Include the relevant javascript and language strings for the resource 3201 * toolbox YUI module 3202 * 3203 * @param integer $id The ID of the course being applied to 3204 * @param array $usedmodules An array containing the names of the modules in use on the page 3205 * @param array $enabledmodules An array containing the names of the enabled (visible) modules on this site 3206 * @param stdClass $config An object containing configuration parameters for ajax modules including: 3207 * * resourceurl The URL to post changes to for resource changes 3208 * * sectionurl The URL to post changes to for section changes 3209 * * pageparams Additional parameters to pass through in the post 3210 * @return bool 3211 */ 3212 function include_course_ajax($course, $usedmodules = array(), $enabledmodules = null, $config = null) { 3213 global $CFG, $PAGE, $SITE; 3214 3215 // Init the course editor module to support UI components. 3216 $format = course_get_format($course); 3217 include_course_editor($format); 3218 3219 // Ensure that ajax should be included 3220 if (!course_ajax_enabled($course)) { 3221 return false; 3222 } 3223 3224 // Component based formats don't use YUI drag and drop anymore. 3225 if (!$format->supports_components() && course_format_uses_sections($course->format)) { 3226 3227 if (!$config) { 3228 $config = new stdClass(); 3229 } 3230 3231 // The URL to use for resource changes. 3232 if (!isset($config->resourceurl)) { 3233 $config->resourceurl = '/course/rest.php'; 3234 } 3235 3236 // The URL to use for section changes. 3237 if (!isset($config->sectionurl)) { 3238 $config->sectionurl = '/course/rest.php'; 3239 } 3240 3241 // Any additional parameters which need to be included on page submission. 3242 if (!isset($config->pageparams)) { 3243 $config->pageparams = array(); 3244 } 3245 3246 $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_section_dragdrop', 3247 array(array( 3248 'courseid' => $course->id, 3249 'ajaxurl' => $config->sectionurl, 3250 'config' => $config, 3251 )), null, true); 3252 3253 $PAGE->requires->yui_module('moodle-course-dragdrop', 'M.course.init_resource_dragdrop', 3254 array(array( 3255 'courseid' => $course->id, 3256 'ajaxurl' => $config->resourceurl, 3257 'config' => $config, 3258 )), null, true); 3259 3260 // Require various strings for the command toolbox. 3261 $PAGE->requires->strings_for_js(array( 3262 'moveleft', 3263 'deletechecktype', 3264 'deletechecktypename', 3265 'edittitle', 3266 'edittitleinstructions', 3267 'show', 3268 'hide', 3269 'highlight', 3270 'highlightoff', 3271 'groupsnone', 3272 'groupsvisible', 3273 'groupsseparate', 3274 'markthistopic', 3275 'markedthistopic', 3276 'movesection', 3277 'movecoursemodule', 3278 'movecoursesection', 3279 'movecontent', 3280 'tocontent', 3281 'emptydragdropregion', 3282 'afterresource', 3283 'aftersection', 3284 'totopofsection', 3285 ), 'moodle'); 3286 3287 // Include section-specific strings for formats which support sections. 3288 if (course_format_uses_sections($course->format)) { 3289 $PAGE->requires->strings_for_js(array( 3290 'showfromothers', 3291 'hidefromothers', 3292 ), 'format_' . $course->format); 3293 } 3294 3295 // For confirming resource deletion we need the name of the module in question. 3296 foreach ($usedmodules as $module => $modname) { 3297 $PAGE->requires->string_for_js('pluginname', $module); 3298 } 3299 3300 // Load drag and drop upload AJAX. 3301 require_once($CFG->dirroot.'/course/dnduploadlib.php'); 3302 dndupload_add_to_course($course, $enabledmodules); 3303 } 3304 3305 $PAGE->requires->js_call_amd('core_course/actions', 'initCoursePage', array($course->format)); 3306 3307 return true; 3308 } 3309 3310 /** 3311 * Include and configure the course editor modules. 3312 * 3313 * @param course_format $format the course format instance. 3314 */ 3315 function include_course_editor(course_format $format) { 3316 global $PAGE, $SITE; 3317 3318 $course = $format->get_course(); 3319 3320 if ($SITE->id === $course->id) { 3321 return; 3322 } 3323 3324 $statekey = course_format::session_cache($course); 3325 3326 // Edition mode and some format specs must be passed to the init method. 3327 $setup = (object)[ 3328 'editing' => $format->show_editor(), 3329 'supportscomponents' => $format->supports_components(), 3330 'statekey' => $statekey, 3331 'overriddenStrings' => $format->get_editor_custom_strings(), 3332 ]; 3333 // All the new editor elements will be loaded after the course is presented and 3334 // the initial course state will be generated using core_course_get_state webservice. 3335 $PAGE->requires->js_call_amd('core_courseformat/courseeditor', 'setViewFormat', [$course->id, $setup]); 3336 } 3337 3338 /** 3339 * Returns the sorted list of available course formats, filtered by enabled if necessary 3340 * 3341 * @param bool $enabledonly return only formats that are enabled 3342 * @return array array of sorted format names 3343 */ 3344 function get_sorted_course_formats($enabledonly = false) { 3345 global $CFG; 3346 $formats = core_component::get_plugin_list('format'); 3347 3348 if (!empty($CFG->format_plugins_sortorder)) { 3349 $order = explode(',', $CFG->format_plugins_sortorder); 3350 $order = array_merge(array_intersect($order, array_keys($formats)), 3351 array_diff(array_keys($formats), $order)); 3352 } else { 3353 $order = array_keys($formats); 3354 } 3355 if (!$enabledonly) { 3356 return $order; 3357 } 3358 $sortedformats = array(); 3359 foreach ($order as $formatname) { 3360 if (!get_config('format_'.$formatname, 'disabled')) { 3361 $sortedformats[] = $formatname; 3362 } 3363 } 3364 return $sortedformats; 3365 } 3366 3367 /** 3368 * The URL to use for the specified course (with section) 3369 * 3370 * @param int|stdClass $courseorid The course to get the section name for (either object or just course id) 3371 * @param int|stdClass $section Section object from database or just field course_sections.section 3372 * if omitted the course view page is returned 3373 * @param array $options options for view URL. At the moment core uses: 3374 * 'navigation' (bool) if true and section has no separate page, the function returns null 3375 * 'sr' (int) used by multipage formats to specify to which section to return 3376 * @return moodle_url The url of course 3377 */ 3378 function course_get_url($courseorid, $section = null, $options = array()) { 3379 return course_get_format($courseorid)->get_view_url($section, $options); 3380 } 3381 3382 /** 3383 * Create a module. 3384 * 3385 * It includes: 3386 * - capability checks and other checks 3387 * - create the module from the module info 3388 * 3389 * @param object $module 3390 * @return object the created module info 3391 * @throws moodle_exception if user is not allowed to perform the action or module is not allowed in this course 3392 */ 3393 function create_module($moduleinfo) { 3394 global $DB, $CFG; 3395 3396 require_once($CFG->dirroot . '/course/modlib.php'); 3397 3398 // Check manadatory attributs. 3399 $mandatoryfields = array('modulename', 'course', 'section', 'visible'); 3400 if (plugin_supports('mod', $moduleinfo->modulename, FEATURE_MOD_INTRO, true)) { 3401 $mandatoryfields[] = 'introeditor'; 3402 } 3403 foreach($mandatoryfields as $mandatoryfield) { 3404 if (!isset($moduleinfo->{$mandatoryfield})) { 3405 throw new moodle_exception('createmodulemissingattribut', '', '', $mandatoryfield); 3406 } 3407 } 3408 3409 // Some additional checks (capability / existing instances). 3410 $course = $DB->get_record('course', array('id'=>$moduleinfo->course), '*', MUST_EXIST); 3411 list($module, $context, $cw) = can_add_moduleinfo($course, $moduleinfo->modulename, $moduleinfo->section); 3412 3413 // Add the module. 3414 $moduleinfo->module = $module->id; 3415 $moduleinfo = add_moduleinfo($moduleinfo, $course, null); 3416 3417 return $moduleinfo; 3418 } 3419 3420 /** 3421 * Update a module. 3422 * 3423 * It includes: 3424 * - capability and other checks 3425 * - update the module 3426 * 3427 * @param object $module 3428 * @return object the updated module info 3429 * @throws moodle_exception if current user is not allowed to update the module 3430 */ 3431 function update_module($moduleinfo) { 3432 global $DB, $CFG; 3433 3434 require_once($CFG->dirroot . '/course/modlib.php'); 3435 3436 // Check the course module exists. 3437 $cm = get_coursemodule_from_id('', $moduleinfo->coursemodule, 0, false, MUST_EXIST); 3438 3439 // Check the course exists. 3440 $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST); 3441 3442 // Some checks (capaibility / existing instances). 3443 list($cm, $context, $module, $data, $cw) = can_update_moduleinfo($cm); 3444 3445 // Retrieve few information needed by update_moduleinfo. 3446 $moduleinfo->modulename = $cm->modname; 3447 if (!isset($moduleinfo->scale)) { 3448 $moduleinfo->scale = 0; 3449 } 3450 $moduleinfo->type = 'mod'; 3451 3452 // Update the module. 3453 list($cm, $moduleinfo) = update_moduleinfo($cm, $moduleinfo, $course, null); 3454 3455 return $moduleinfo; 3456 } 3457 3458 /** 3459 * Duplicate a module on the course for ajax. 3460 * 3461 * @see mod_duplicate_module() 3462 * @param object $course The course 3463 * @param object $cm The course module to duplicate 3464 * @param int $sr The section to link back to (used for creating the links) 3465 * @throws moodle_exception if the plugin doesn't support duplication 3466 * @return Object containing: 3467 * - fullcontent: The HTML markup for the created CM 3468 * - cmid: The CMID of the newly created CM 3469 * - redirect: Whether to trigger a redirect following this change 3470 */ 3471 function mod_duplicate_activity($course, $cm, $sr = null) { 3472 global $PAGE; 3473 3474 $newcm = duplicate_module($course, $cm); 3475 3476 $resp = new stdClass(); 3477 if ($newcm) { 3478 3479 $format = course_get_format($course); 3480 $renderer = $format->get_renderer($PAGE); 3481 $modinfo = $format->get_modinfo(); 3482 $section = $modinfo->get_section_info($newcm->sectionnum); 3483 3484 // Get the new element html content. 3485 $resp->fullcontent = $renderer->course_section_updated_cm_item($format, $section, $newcm); 3486 3487 $resp->cmid = $newcm->id; 3488 } else { 3489 // Trigger a redirect. 3490 $resp->redirect = true; 3491 } 3492 return $resp; 3493 } 3494 3495 /** 3496 * Api to duplicate a module. 3497 * 3498 * @param object $course course object. 3499 * @param object $cm course module object to be duplicated. 3500 * @param int $sectionid section ID new course module will be placed in. 3501 * @param bool $changename updates module name with text from duplicatedmodule lang string. 3502 * @since Moodle 2.8 3503 * 3504 * @throws Exception 3505 * @throws coding_exception 3506 * @throws moodle_exception 3507 * @throws restore_controller_exception 3508 * 3509 * @return cm_info|null cminfo object if we sucessfully duplicated the mod and found the new cm. 3510 */ 3511 function duplicate_module($course, $cm, int $sectionid = null, bool $changename = true): ?cm_info { 3512 global $CFG, $DB, $USER; 3513 require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php'); 3514 require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php'); 3515 require_once($CFG->libdir . '/filelib.php'); 3516 3517 $a = new stdClass(); 3518 $a->modtype = get_string('modulename', $cm->modname); 3519 $a->modname = format_string($cm->name); 3520 3521 if (!plugin_supports('mod', $cm->modname, FEATURE_BACKUP_MOODLE2)) { 3522 throw new moodle_exception('duplicatenosupport', 'error', '', $a); 3523 } 3524 3525 // Backup the activity. 3526 3527 $bc = new backup_controller(backup::TYPE_1ACTIVITY, $cm->id, backup::FORMAT_MOODLE, 3528 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id); 3529 3530 $backupid = $bc->get_backupid(); 3531 $backupbasepath = $bc->get_plan()->get_basepath(); 3532 3533 $bc->execute_plan(); 3534 3535 $bc->destroy(); 3536 3537 // Restore the backup immediately. 3538 3539 $rc = new restore_controller($backupid, $course->id, 3540 backup::INTERACTIVE_NO, backup::MODE_IMPORT, $USER->id, backup::TARGET_CURRENT_ADDING); 3541 3542 // Make sure that the restore_general_groups setting is always enabled when duplicating an activity. 3543 $plan = $rc->get_plan(); 3544 $groupsetting = $plan->get_setting('groups'); 3545 if (empty($groupsetting->get_value())) { 3546 $groupsetting->set_value(true); 3547 } 3548 3549 $cmcontext = context_module::instance($cm->id); 3550 if (!$rc->execute_precheck()) { 3551 $precheckresults = $rc->get_precheck_results(); 3552 if (is_array($precheckresults) && !empty($precheckresults['errors'])) { 3553 if (empty($CFG->keeptempdirectoriesonbackup)) { 3554 fulldelete($backupbasepath); 3555 } 3556 } 3557 } 3558 3559 $rc->execute_plan(); 3560 3561 // Now a bit hacky part follows - we try to get the cmid of the newly 3562 // restored copy of the module. 3563 $newcmid = null; 3564 $tasks = $rc->get_plan()->get_tasks(); 3565 foreach ($tasks as $task) { 3566 if (is_subclass_of($task, 'restore_activity_task')) { 3567 if ($task->get_old_contextid() == $cmcontext->id) { 3568 $newcmid = $task->get_moduleid(); 3569 break; 3570 } 3571 } 3572 } 3573 3574 $rc->destroy(); 3575 3576 if (empty($CFG->keeptempdirectoriesonbackup)) { 3577 fulldelete($backupbasepath); 3578 } 3579 3580 // If we know the cmid of the new course module, let us move it 3581 // right below the original one. otherwise it will stay at the 3582 // end of the section. 3583 if ($newcmid) { 3584 // Proceed with activity renaming before everything else. We don't use APIs here to avoid 3585 // triggering a lot of create/update duplicated events. 3586 $newcm = get_coursemodule_from_id($cm->modname, $newcmid, $cm->course); 3587 if ($changename) { 3588 // Add ' (copy)' language string postfix to duplicated module. 3589 $newname = get_string('duplicatedmodule', 'moodle', $newcm->name); 3590 set_coursemodule_name($newcm->id, $newname); 3591 } 3592 3593 $section = $DB->get_record('course_sections', ['id' => $sectionid ?? $cm->section, 'course' => $cm->course]); 3594 if (isset($sectionid)) { 3595 moveto_module($newcm, $section); 3596 } else { 3597 $modarray = explode(",", trim($section->sequence)); 3598 $cmindex = array_search($cm->id, $modarray); 3599 if ($cmindex !== false && $cmindex < count($modarray) - 1) { 3600 moveto_module($newcm, $section, $modarray[$cmindex + 1]); 3601 } 3602 } 3603 3604 // Update calendar events with the duplicated module. 3605 // The following line is to be removed in MDL-58906. 3606 course_module_update_calendar_events($newcm->modname, null, $newcm); 3607 3608 // Trigger course module created event. We can trigger the event only if we know the newcmid. 3609 $newcm = get_fast_modinfo($cm->course)->get_cm($newcmid); 3610 $event = \core\event\course_module_created::create_from_cm($newcm); 3611 $event->trigger(); 3612 } 3613 3614 return isset($newcm) ? $newcm : null; 3615 } 3616 3617 /** 3618 * Compare two objects to find out their correct order based on timestamp (to be used by usort). 3619 * Sorts by descending order of time. 3620 * 3621 * @param stdClass $a First object 3622 * @param stdClass $b Second object 3623 * @return int 0,1,-1 representing the order 3624 */ 3625 function compare_activities_by_time_desc($a, $b) { 3626 // Make sure the activities actually have a timestamp property. 3627 if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { 3628 return 0; 3629 } 3630 // We treat instances without timestamp as if they have a timestamp of 0. 3631 if ((!property_exists($a, 'timestamp')) && (property_exists($b,'timestamp'))) { 3632 return 1; 3633 } 3634 if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { 3635 return -1; 3636 } 3637 if ($a->timestamp == $b->timestamp) { 3638 return 0; 3639 } 3640 return ($a->timestamp > $b->timestamp) ? -1 : 1; 3641 } 3642 3643 /** 3644 * Compare two objects to find out their correct order based on timestamp (to be used by usort). 3645 * Sorts by ascending order of time. 3646 * 3647 * @param stdClass $a First object 3648 * @param stdClass $b Second object 3649 * @return int 0,1,-1 representing the order 3650 */ 3651 function compare_activities_by_time_asc($a, $b) { 3652 // Make sure the activities actually have a timestamp property. 3653 if ((!property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { 3654 return 0; 3655 } 3656 // We treat instances without timestamp as if they have a timestamp of 0. 3657 if ((!property_exists($a, 'timestamp')) && (property_exists($b, 'timestamp'))) { 3658 return -1; 3659 } 3660 if ((property_exists($a, 'timestamp')) && (!property_exists($b, 'timestamp'))) { 3661 return 1; 3662 } 3663 if ($a->timestamp == $b->timestamp) { 3664 return 0; 3665 } 3666 return ($a->timestamp < $b->timestamp) ? -1 : 1; 3667 } 3668 3669 /** 3670 * Changes the visibility of a course. 3671 * 3672 * @param int $courseid The course to change. 3673 * @param bool $show True to make it visible, false otherwise. 3674 * @return bool 3675 */ 3676 function course_change_visibility($courseid, $show = true) { 3677 $course = new stdClass; 3678 $course->id = $courseid; 3679 $course->visible = ($show) ? '1' : '0'; 3680 $course->visibleold = $course->visible; 3681 update_course($course); 3682 return true; 3683 } 3684 3685 /** 3686 * Changes the course sortorder by one, moving it up or down one in respect to sort order. 3687 * 3688 * @param stdClass|core_course_list_element $course 3689 * @param bool $up If set to true the course will be moved up one. Otherwise down one. 3690 * @return bool 3691 */ 3692 function course_change_sortorder_by_one($course, $up) { 3693 global $DB; 3694 $params = array($course->sortorder, $course->category); 3695 if ($up) { 3696 $select = 'sortorder < ? AND category = ?'; 3697 $sort = 'sortorder DESC'; 3698 } else { 3699 $select = 'sortorder > ? AND category = ?'; 3700 $sort = 'sortorder ASC'; 3701 } 3702 fix_course_sortorder(); 3703 $swapcourse = $DB->get_records_select('course', $select, $params, $sort, '*', 0, 1); 3704 if ($swapcourse) { 3705 $swapcourse = reset($swapcourse); 3706 $DB->set_field('course', 'sortorder', $swapcourse->sortorder, array('id' => $course->id)); 3707 $DB->set_field('course', 'sortorder', $course->sortorder, array('id' => $swapcourse->id)); 3708 // Finally reorder courses. 3709 fix_course_sortorder(); 3710 cache_helper::purge_by_event('changesincourse'); 3711 return true; 3712 } 3713 return false; 3714 } 3715 3716 /** 3717 * Changes the sort order of courses in a category so that the first course appears after the second. 3718 * 3719 * @param int|stdClass $courseorid The course to focus on. 3720 * @param int $moveaftercourseid The course to shifter after or 0 if you want it to be the first course in the category. 3721 * @return bool 3722 */ 3723 function course_change_sortorder_after_course($courseorid, $moveaftercourseid) { 3724 global $DB; 3725 3726 if (!is_object($courseorid)) { 3727 $course = get_course($courseorid); 3728 } else { 3729 $course = $courseorid; 3730 } 3731 3732 if ((int)$moveaftercourseid === 0) { 3733 // We've moving the course to the start of the queue. 3734 $sql = 'SELECT sortorder 3735 FROM {course} 3736 WHERE category = :categoryid 3737 ORDER BY sortorder'; 3738 $params = array( 3739 'categoryid' => $course->category 3740 ); 3741 $sortorder = $DB->get_field_sql($sql, $params, IGNORE_MULTIPLE); 3742 3743 $sql = 'UPDATE {course} 3744 SET sortorder = sortorder + 1 3745 WHERE category = :categoryid 3746 AND id <> :id'; 3747 $params = array( 3748 'categoryid' => $course->category, 3749 'id' => $course->id, 3750 ); 3751 $DB->execute($sql, $params); 3752 $DB->set_field('course', 'sortorder', $sortorder, array('id' => $course->id)); 3753 } else if ($course->id === $moveaftercourseid) { 3754 // They're the same - moronic. 3755 debugging("Invalid move after course given.", DEBUG_DEVELOPER); 3756 return false; 3757 } else { 3758 // Moving this course after the given course. It could be before it could be after. 3759 $moveaftercourse = get_course($moveaftercourseid); 3760 if ($course->category !== $moveaftercourse->category) { 3761 debugging("Cannot re-order courses. The given courses do not belong to the same category.", DEBUG_DEVELOPER); 3762 return false; 3763 } 3764 // Increment all courses in the same category that are ordered after the moveafter course. 3765 // This makes a space for the course we're moving. 3766 $sql = 'UPDATE {course} 3767 SET sortorder = sortorder + 1 3768 WHERE category = :categoryid 3769 AND sortorder > :sortorder'; 3770 $params = array( 3771 'categoryid' => $moveaftercourse->category, 3772 'sortorder' => $moveaftercourse->sortorder 3773 ); 3774 $DB->execute($sql, $params); 3775 $DB->set_field('course', 'sortorder', $moveaftercourse->sortorder + 1, array('id' => $course->id)); 3776 } 3777 fix_course_sortorder(); 3778 cache_helper::purge_by_event('changesincourse'); 3779 return true; 3780 } 3781 3782 /** 3783 * Trigger course viewed event. This API function is used when course view actions happens, 3784 * usually in course/view.php but also in external functions. 3785 * 3786 * @param stdClass $context course context object 3787 * @param int $sectionnumber section number 3788 * @since Moodle 2.9 3789 */ 3790 function course_view($context, $sectionnumber = 0) { 3791 3792 $eventdata = array('context' => $context); 3793 3794 if (!empty($sectionnumber)) { 3795 $eventdata['other']['coursesectionnumber'] = $sectionnumber; 3796 } 3797 3798 $event = \core\event\course_viewed::create($eventdata); 3799 $event->trigger(); 3800 3801 user_accesstime_log($context->instanceid); 3802 } 3803 3804 /** 3805 * Returns courses tagged with a specified tag. 3806 * 3807 * @param core_tag_tag $tag 3808 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag 3809 * are displayed on the page and the per-page limit may be bigger 3810 * @param int $fromctx context id where the link was displayed, may be used by callbacks 3811 * to display items in the same context first 3812 * @param int $ctx context id where to search for records 3813 * @param bool $rec search in subcontexts as well 3814 * @param int $page 0-based number of page being displayed 3815 * @return \core_tag\output\tagindex 3816 */ 3817 function course_get_tagged_courses($tag, $exclusivemode = false, $fromctx = 0, $ctx = 0, $rec = 1, $page = 0) { 3818 global $CFG, $PAGE; 3819 3820 $perpage = $exclusivemode ? $CFG->coursesperpage : 5; 3821 $displayoptions = array( 3822 'limit' => $perpage, 3823 'offset' => $page * $perpage, 3824 'viewmoreurl' => null, 3825 ); 3826 3827 $courserenderer = $PAGE->get_renderer('core', 'course'); 3828 $totalcount = core_course_category::search_courses_count(array('tagid' => $tag->id, 'ctx' => $ctx, 'rec' => $rec)); 3829 $content = $courserenderer->tagged_courses($tag->id, $exclusivemode, $ctx, $rec, $displayoptions); 3830 $totalpages = ceil($totalcount / $perpage); 3831 3832 return new core_tag\output\tagindex($tag, 'core', 'course', $content, 3833 $exclusivemode, $fromctx, $ctx, $rec, $page, $totalpages); 3834 } 3835 3836 /** 3837 * Implements callback inplace_editable() allowing to edit values in-place 3838 * 3839 * @param string $itemtype 3840 * @param int $itemid 3841 * @param mixed $newvalue 3842 * @return \core\output\inplace_editable 3843 */ 3844 function core_course_inplace_editable($itemtype, $itemid, $newvalue) { 3845 if ($itemtype === 'activityname') { 3846 return \core_courseformat\output\local\content\cm\title::update($itemid, $newvalue); 3847 } 3848 } 3849 3850 /** 3851 * This function calculates the minimum and maximum cutoff values for the timestart of 3852 * the given event. 3853 * 3854 * It will return an array with two values, the first being the minimum cutoff value and 3855 * the second being the maximum cutoff value. Either or both values can be null, which 3856 * indicates there is no minimum or maximum, respectively. 3857 * 3858 * If a cutoff is required then the function must return an array containing the cutoff 3859 * timestamp and error string to display to the user if the cutoff value is violated. 3860 * 3861 * A minimum and maximum cutoff return value will look like: 3862 * [ 3863 * [1505704373, 'The date must be after this date'], 3864 * [1506741172, 'The date must be before this date'] 3865 * ] 3866 * 3867 * @param calendar_event $event The calendar event to get the time range for 3868 * @param stdClass $course The course object to get the range from 3869 * @return array Returns an array with min and max date. 3870 */ 3871 function core_course_core_calendar_get_valid_event_timestart_range(\calendar_event $event, $course) { 3872 $mindate = null; 3873 $maxdate = null; 3874 3875 if ($course->startdate) { 3876 $mindate = [ 3877 $course->startdate, 3878 get_string('errorbeforecoursestart', 'calendar') 3879 ]; 3880 } 3881 3882 return [$mindate, $maxdate]; 3883 } 3884 3885 /** 3886 * Render the message drawer to be included in the top of the body of each page. 3887 * 3888 * @return string HTML 3889 */ 3890 function core_course_drawer(): string { 3891 global $PAGE; 3892 3893 // Only add course index on non-site course pages. 3894 if (!$PAGE->course || $PAGE->course->id == SITEID) { 3895 return ''; 3896 } 3897 3898 // Show course index to users can access the course only. 3899 if (!can_access_course($PAGE->course, null, '', true)) { 3900 return ''; 3901 } 3902 3903 $format = course_get_format($PAGE->course); 3904 $renderer = $format->get_renderer($PAGE); 3905 if (method_exists($renderer, 'course_index_drawer')) { 3906 return $renderer->course_index_drawer($format); 3907 } 3908 3909 return ''; 3910 } 3911 3912 /** 3913 * Returns course modules tagged with a specified tag ready for output on tag/index.php page 3914 * 3915 * This is a callback used by the tag area core/course_modules to search for course modules 3916 * tagged with a specific tag. 3917 * 3918 * @param core_tag_tag $tag 3919 * @param bool $exclusivemode if set to true it means that no other entities tagged with this tag 3920 * are displayed on the page and the per-page limit may be bigger 3921 * @param int $fromcontextid context id where the link was displayed, may be used by callbacks 3922 * to display items in the same context first 3923 * @param int $contextid context id where to search for records 3924 * @param bool $recursivecontext search in subcontexts as well 3925 * @param int $page 0-based number of page being displayed 3926 * @return \core_tag\output\tagindex 3927 */ 3928 function course_get_tagged_course_modules($tag, $exclusivemode = false, $fromcontextid = 0, $contextid = 0, 3929 $recursivecontext = 1, $page = 0) { 3930 global $OUTPUT; 3931 $perpage = $exclusivemode ? 20 : 5; 3932 3933 // Build select query. 3934 $ctxselect = context_helper::get_preload_record_columns_sql('ctx'); 3935 $query = "SELECT cm.id AS cmid, c.id AS courseid, $ctxselect 3936 FROM {course_modules} cm 3937 JOIN {tag_instance} tt ON cm.id = tt.itemid 3938 JOIN {course} c ON cm.course = c.id 3939 JOIN {context} ctx ON ctx.instanceid = cm.id AND ctx.contextlevel = :coursemodulecontextlevel 3940 WHERE tt.itemtype = :itemtype AND tt.tagid = :tagid AND tt.component = :component 3941 AND cm.deletioninprogress = 0 3942 AND c.id %COURSEFILTER% AND cm.id %ITEMFILTER%"; 3943 3944 $params = array('itemtype' => 'course_modules', 'tagid' => $tag->id, 'component' => 'core', 3945 'coursemodulecontextlevel' => CONTEXT_MODULE); 3946 if ($contextid) { 3947 $context = context::instance_by_id($contextid); 3948 $query .= $recursivecontext ? ' AND (ctx.id = :contextid OR ctx.path LIKE :path)' : ' AND ctx.id = :contextid'; 3949 $params['contextid'] = $context->id; 3950 $params['path'] = $context->path.'/%'; 3951 } 3952 3953 $query .= ' ORDER BY'; 3954 if ($fromcontextid) { 3955 // In order-clause specify that modules from inside "fromctx" context should be returned first. 3956 $fromcontext = context::instance_by_id($fromcontextid); 3957 $query .= ' (CASE WHEN ctx.id = :fromcontextid OR ctx.path LIKE :frompath THEN 0 ELSE 1 END),'; 3958 $params['fromcontextid'] = $fromcontext->id; 3959 $params['frompath'] = $fromcontext->path.'/%'; 3960 } 3961 $query .= ' c.sortorder, cm.id'; 3962 $totalpages = $page + 1; 3963 3964 // Use core_tag_index_builder to build and filter the list of items. 3965 // Request one item more than we need so we know if next page exists. 3966 $builder = new core_tag_index_builder('core', 'course_modules', $query, $params, $page * $perpage, $perpage + 1); 3967 while ($item = $builder->has_item_that_needs_access_check()) { 3968 context_helper::preload_from_record($item); 3969 $courseid = $item->courseid; 3970 if (!$builder->can_access_course($courseid)) { 3971 $builder->set_accessible($item, false); 3972 continue; 3973 } 3974 $modinfo = get_fast_modinfo($builder->get_course($courseid)); 3975 // Set accessibility of this item and all other items in the same course. 3976 $builder->walk(function ($taggeditem) use ($courseid, $modinfo, $builder) { 3977 if ($taggeditem->courseid == $courseid) { 3978 $cm = $modinfo->get_cm($taggeditem->cmid); 3979 $builder->set_accessible($taggeditem, $cm->uservisible); 3980 } 3981 }); 3982 } 3983 3984 $items = $builder->get_items(); 3985 if (count($items) > $perpage) { 3986 $totalpages = $page + 2; // We don't need exact page count, just indicate that the next page exists. 3987 array_pop($items); 3988 } 3989 3990 // Build the display contents. 3991 if ($items) { 3992 $tagfeed = new core_tag\output\tagfeed(); 3993 foreach ($items as $item) { 3994 context_helper::preload_from_record($item); 3995 $course = $builder->get_course($item->courseid); 3996 $modinfo = get_fast_modinfo($course); 3997 $cm = $modinfo->get_cm($item->cmid); 3998 $courseurl = course_get_url($item->courseid, $cm->sectionnum); 3999 $cmname = $cm->get_formatted_name(); 4000 if (!$exclusivemode) { 4001 $cmname = shorten_text($cmname, 100); 4002 } 4003 $cmname = html_writer::link($cm->url?:$courseurl, $cmname); 4004 $coursename = format_string($course->fullname, true, 4005 array('context' => context_course::instance($item->courseid))); 4006 $coursename = html_writer::link($courseurl, $coursename); 4007 $icon = html_writer::empty_tag('img', array('src' => $cm->get_icon_url())); 4008 $tagfeed->add($icon, $cmname, $coursename); 4009 } 4010 4011 $content = $OUTPUT->render_from_template('core_tag/tagfeed', 4012 $tagfeed->export_for_template($OUTPUT)); 4013 4014 return new core_tag\output\tagindex($tag, 'core', 'course_modules', $content, 4015 $exclusivemode, $fromcontextid, $contextid, $recursivecontext, $page, $totalpages); 4016 } 4017 } 4018 4019 /** 4020 * Return an object with the list of navigation options in a course that are avaialable or not for the current user. 4021 * This function also handles the frontpage course. 4022 * 4023 * @param stdClass $context context object (it can be a course context or the system context for frontpage settings) 4024 * @param stdClass $course the course where the settings are being rendered 4025 * @return stdClass the navigation options in a course and their availability status 4026 * @since Moodle 3.2 4027 */ 4028 function course_get_user_navigation_options($context, $course = null) { 4029 global $CFG, $USER; 4030 4031 $isloggedin = isloggedin(); 4032 $isguestuser = isguestuser(); 4033 $isfrontpage = $context->contextlevel == CONTEXT_SYSTEM; 4034 4035 if ($isfrontpage) { 4036 $sitecontext = $context; 4037 } else { 4038 $sitecontext = context_system::instance(); 4039 } 4040 4041 // Sets defaults for all options. 4042 $options = (object) [ 4043 'badges' => false, 4044 'blogs' => false, 4045 'competencies' => false, 4046 'grades' => false, 4047 'notes' => false, 4048 'participants' => false, 4049 'search' => false, 4050 'tags' => false, 4051 'communication' => false, 4052 ]; 4053 4054 $options->blogs = !empty($CFG->enableblogs) && 4055 ($CFG->bloglevel == BLOG_GLOBAL_LEVEL || 4056 ($CFG->bloglevel == BLOG_SITE_LEVEL and ($isloggedin and !$isguestuser))) 4057 && has_capability('moodle/blog:view', $sitecontext); 4058 4059 $options->notes = !empty($CFG->enablenotes) && has_any_capability(array('moodle/notes:manage', 'moodle/notes:view'), $context); 4060 4061 // Frontpage settings? 4062 if ($isfrontpage) { 4063 // We are on the front page, so make sure we use the proper capability (site:viewparticipants). 4064 $options->participants = course_can_view_participants($sitecontext); 4065 $options->badges = !empty($CFG->enablebadges) && has_capability('moodle/badges:viewbadges', $sitecontext); 4066 $options->tags = !empty($CFG->usetags) && $isloggedin; 4067 $options->search = !empty($CFG->enableglobalsearch) && has_capability('moodle/search:query', $sitecontext); 4068 } else { 4069 // We are in a course, so make sure we use the proper capability (course:viewparticipants). 4070 $options->participants = course_can_view_participants($context); 4071 4072 // Only display badges if they are enabled and the current user can manage them or if they can view them and have, 4073 // at least, one available badge. 4074 if (!empty($CFG->enablebadges) && !empty($CFG->badges_allowcoursebadges)) { 4075 $canmanage = has_any_capability([ 4076 'moodle/badges:createbadge', 4077 'moodle/badges:awardbadge', 4078 'moodle/badges:configurecriteria', 4079 'moodle/badges:configuremessages', 4080 'moodle/badges:configuredetails', 4081 'moodle/badges:deletebadge', 4082 ], 4083 $context 4084 ); 4085 $totalbadges = []; 4086 $canview = false; 4087 if (!$canmanage) { 4088 // This only needs to be calculated if the user can't manage badges (to improve performance). 4089 $canview = has_capability('moodle/badges:viewbadges', $context); 4090 if ($canview) { 4091 require_once($CFG->dirroot.'/lib/badgeslib.php'); 4092 if (is_null($course)) { 4093 $totalbadges = count(badges_get_badges(BADGE_TYPE_SITE, 0, '', '', 0, 0, $USER->id)); 4094 } else { 4095 $totalbadges = count(badges_get_badges(BADGE_TYPE_COURSE, $course->id, '', '', 0, 0, $USER->id)); 4096 } 4097 } 4098 } 4099 4100 $options->badges = ($canmanage || ($canview && $totalbadges > 0)); 4101 } 4102 // Add view grade report is permitted. 4103 $grades = false; 4104 4105 if (has_capability('moodle/grade:viewall', $context)) { 4106 $grades = true; 4107 } else if (!empty($course->showgrades)) { 4108 $reports = core_component::get_plugin_list('gradereport'); 4109 if (is_array($reports) && count($reports) > 0) { // Get all installed reports. 4110 arsort($reports); // User is last, we want to test it first. 4111 foreach ($reports as $plugin => $plugindir) { 4112 if (has_capability('gradereport/'.$plugin.':view', $context)) { 4113 // Stop when the first visible plugin is found. 4114 $grades = true; 4115 break; 4116 } 4117 } 4118 } 4119 } 4120 $options->grades = $grades; 4121 } 4122 4123 if (\core_communication\api::is_available()) { 4124 $options->communication = has_capability('moodle/course:configurecoursecommunication', $context); 4125 } 4126 4127 if (\core_competency\api::is_enabled()) { 4128 $capabilities = array('moodle/competency:coursecompetencyview', 'moodle/competency:coursecompetencymanage'); 4129 $options->competencies = has_any_capability($capabilities, $context); 4130 } 4131 return $options; 4132 } 4133 4134 /** 4135 * Return an object with the list of administration options in a course that are available or not for the current user. 4136 * This function also handles the frontpage settings. 4137 * 4138 * @param stdClass $course course object (for frontpage it should be a clone of $SITE) 4139 * @param stdClass $context context object (course context) 4140 * @return stdClass the administration options in a course and their availability status 4141 * @since Moodle 3.2 4142 */ 4143 function course_get_user_administration_options($course, $context) { 4144 global $CFG; 4145 $isfrontpage = $course->id == SITEID; 4146 $completionenabled = $CFG->enablecompletion && $course->enablecompletion; 4147 $hascompletionoptions = count(core_completion\manager::get_available_completion_options($course->id)) > 0; 4148 $options = new stdClass; 4149 $options->update = has_capability('moodle/course:update', $context); 4150 $options->editcompletion = $CFG->enablecompletion && $course->enablecompletion && 4151 ($options->update || $hascompletionoptions); 4152 $options->filters = has_capability('moodle/filter:manage', $context) && 4153 count(filter_get_available_in_context($context)) > 0; 4154 $options->reports = has_capability('moodle/site:viewreports', $context); 4155 $options->backup = has_capability('moodle/backup:backupcourse', $context); 4156 $options->restore = has_capability('moodle/restore:restorecourse', $context); 4157 $options->copy = \core_course\management\helper::can_copy_course($course->id); 4158 $options->files = ($course->legacyfiles == 2 && has_capability('moodle/course:managefiles', $context)); 4159 4160 if (!$isfrontpage) { 4161 $options->tags = has_capability('moodle/course:tag', $context); 4162 $options->gradebook = has_capability('moodle/grade:manage', $context); 4163 $options->outcomes = !empty($CFG->enableoutcomes) && has_capability('moodle/course:update', $context); 4164 $options->badges = !empty($CFG->enablebadges); 4165 $options->import = has_capability('moodle/restore:restoretargetimport', $context); 4166 $options->reset = has_capability('moodle/course:reset', $context); 4167 $options->roles = has_capability('moodle/role:switchroles', $context); 4168 } else { 4169 // Set default options to false. 4170 $listofoptions = array('tags', 'gradebook', 'outcomes', 'badges', 'import', 'publish', 'reset', 'roles', 'grades'); 4171 4172 foreach ($listofoptions as $option) { 4173 $options->$option = false; 4174 } 4175 } 4176 4177 return $options; 4178 } 4179 4180 /** 4181 * Validates course start and end dates. 4182 * 4183 * Checks that the end course date is not greater than the start course date. 4184 * 4185 * $coursedata['startdate'] or $coursedata['enddate'] may not be set, it depends on the form and user input. 4186 * 4187 * @param array $coursedata May contain startdate and enddate timestamps, depends on the user input. 4188 * @return mixed False if everything alright, error codes otherwise. 4189 */ 4190 function course_validate_dates($coursedata) { 4191 4192 // If both start and end dates are set end date should be later than the start date. 4193 if (!empty($coursedata['startdate']) && !empty($coursedata['enddate']) && 4194 ($coursedata['enddate'] < $coursedata['startdate'])) { 4195 return 'enddatebeforestartdate'; 4196 } 4197 4198 // If start date is not set end date can not be set. 4199 if (empty($coursedata['startdate']) && !empty($coursedata['enddate'])) { 4200 return 'nostartdatenoenddate'; 4201 } 4202 4203 return false; 4204 } 4205 4206 /** 4207 * Check for course updates in the given context level instances (only modules supported right Now) 4208 * 4209 * @param stdClass $course course object 4210 * @param array $tocheck instances to check for updates 4211 * @param array $filter check only for updates in these areas 4212 * @return array list of warnings and instances with updates information 4213 * @since Moodle 3.2 4214 */ 4215 function course_check_updates($course, $tocheck, $filter = array()) { 4216 global $CFG, $DB; 4217 4218 $instances = array(); 4219 $warnings = array(); 4220 $modulescallbacksupport = array(); 4221 $modinfo = get_fast_modinfo($course); 4222 4223 $supportedplugins = get_plugin_list_with_function('mod', 'check_updates_since'); 4224 4225 // Check instances. 4226 foreach ($tocheck as $instance) { 4227 if ($instance['contextlevel'] == 'module') { 4228 // Check module visibility. 4229 try { 4230 $cm = $modinfo->get_cm($instance['id']); 4231 } catch (Exception $e) { 4232 $warnings[] = array( 4233 'item' => 'module', 4234 'itemid' => $instance['id'], 4235 'warningcode' => 'cmidnotincourse', 4236 'message' => 'This module id does not belong to this course.' 4237 ); 4238 continue; 4239 } 4240 4241 if (!$cm->uservisible) { 4242 $warnings[] = array( 4243 'item' => 'module', 4244 'itemid' => $instance['id'], 4245 'warningcode' => 'nonuservisible', 4246 'message' => 'You don\'t have access to this module.' 4247 ); 4248 continue; 4249 } 4250 if (empty($supportedplugins['mod_' . $cm->modname])) { 4251 $warnings[] = array( 4252 'item' => 'module', 4253 'itemid' => $instance['id'], 4254 'warningcode' => 'missingcallback', 4255 'message' => 'This module does not implement the check_updates_since callback: ' . $instance['contextlevel'], 4256 ); 4257 continue; 4258 } 4259 // Retrieve the module instance. 4260 $instances[] = array( 4261 'contextlevel' => $instance['contextlevel'], 4262 'id' => $instance['id'], 4263 'updates' => call_user_func($cm->modname . '_check_updates_since', $cm, $instance['since'], $filter) 4264 ); 4265 4266 } else { 4267 $warnings[] = array( 4268 'item' => 'contextlevel', 4269 'itemid' => $instance['id'], 4270 'warningcode' => 'contextlevelnotsupported', 4271 'message' => 'Context level not yet supported ' . $instance['contextlevel'], 4272 ); 4273 } 4274 } 4275 return array($instances, $warnings); 4276 } 4277 4278 /** 4279 * This function classifies a course as past, in progress or future. 4280 * 4281 * This function may incur a DB hit to calculate course completion. 4282 * @param stdClass $course Course record 4283 * @param stdClass $user User record (optional - defaults to $USER). 4284 * @param completion_info $completioninfo Completion record for the user (optional - will be fetched if required). 4285 * @return string (one of COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_INPROGRESS or COURSE_TIMELINE_PAST) 4286 */ 4287 function course_classify_for_timeline($course, $user = null, $completioninfo = null) { 4288 global $USER; 4289 4290 if ($user == null) { 4291 $user = $USER; 4292 } 4293 4294 if ($completioninfo == null) { 4295 $completioninfo = new completion_info($course); 4296 } 4297 4298 // Let plugins override data for timeline classification. 4299 $pluginsfunction = get_plugins_with_function('extend_course_classify_for_timeline', 'lib.php'); 4300 foreach ($pluginsfunction as $plugintype => $plugins) { 4301 foreach ($plugins as $pluginfunction) { 4302 $pluginfunction($course, $user, $completioninfo); 4303 } 4304 } 4305 4306 $today = time(); 4307 // End date past. 4308 if (!empty($course->enddate) && (course_classify_end_date($course) < $today)) { 4309 return COURSE_TIMELINE_PAST; 4310 } 4311 4312 // Course was completed. 4313 if ($completioninfo->is_enabled() && $completioninfo->is_course_complete($user->id)) { 4314 return COURSE_TIMELINE_PAST; 4315 } 4316 4317 // Start date not reached. 4318 if (!empty($course->startdate) && (course_classify_start_date($course) > $today)) { 4319 return COURSE_TIMELINE_FUTURE; 4320 } 4321 4322 // Everything else is in progress. 4323 return COURSE_TIMELINE_INPROGRESS; 4324 } 4325 4326 /** 4327 * This function calculates the end date to use for display classification purposes, 4328 * incorporating the grace period, if any. 4329 * 4330 * @param stdClass $course The course record. 4331 * @return int The new enddate. 4332 */ 4333 function course_classify_end_date($course) { 4334 global $CFG; 4335 $coursegraceperiodafter = (empty($CFG->coursegraceperiodafter)) ? 0 : $CFG->coursegraceperiodafter; 4336 $enddate = (new \DateTimeImmutable())->setTimestamp($course->enddate)->modify("+{$coursegraceperiodafter} days"); 4337 return $enddate->getTimestamp(); 4338 } 4339 4340 /** 4341 * This function calculates the start date to use for display classification purposes, 4342 * incorporating the grace period, if any. 4343 * 4344 * @param stdClass $course The course record. 4345 * @return int The new startdate. 4346 */ 4347 function course_classify_start_date($course) { 4348 global $CFG; 4349 $coursegraceperiodbefore = (empty($CFG->coursegraceperiodbefore)) ? 0 : $CFG->coursegraceperiodbefore; 4350 $startdate = (new \DateTimeImmutable())->setTimestamp($course->startdate)->modify("-{$coursegraceperiodbefore} days"); 4351 return $startdate->getTimestamp(); 4352 } 4353 4354 /** 4355 * Group a list of courses into either past, future, or in progress. 4356 * 4357 * The return value will be an array indexed by the COURSE_TIMELINE_* constants 4358 * with each value being an array of courses in that group. 4359 * E.g. 4360 * [ 4361 * COURSE_TIMELINE_PAST => [... list of past courses ...], 4362 * COURSE_TIMELINE_FUTURE => [], 4363 * COURSE_TIMELINE_INPROGRESS => [] 4364 * ] 4365 * 4366 * @param array $courses List of courses to be grouped. 4367 * @return array 4368 */ 4369 function course_classify_courses_for_timeline(array $courses) { 4370 return array_reduce($courses, function($carry, $course) { 4371 $classification = course_classify_for_timeline($course); 4372 array_push($carry[$classification], $course); 4373 4374 return $carry; 4375 }, [ 4376 COURSE_TIMELINE_PAST => [], 4377 COURSE_TIMELINE_FUTURE => [], 4378 COURSE_TIMELINE_INPROGRESS => [] 4379 ]); 4380 } 4381 4382 /** 4383 * Get the list of enrolled courses for the current user. 4384 * 4385 * This function returns a Generator. The courses will be loaded from the database 4386 * in chunks rather than a single query. 4387 * 4388 * @param int $limit Restrict result set to this amount 4389 * @param int $offset Skip this number of records from the start of the result set 4390 * @param string|null $sort SQL string for sorting 4391 * @param string|null $fields SQL string for fields to be returned 4392 * @param int $dbquerylimit The number of records to load per DB request 4393 * @param array $includecourses courses ids to be restricted 4394 * @param array $hiddencourses courses ids to be excluded 4395 * @return Generator 4396 */ 4397 function course_get_enrolled_courses_for_logged_in_user( 4398 int $limit = 0, 4399 int $offset = 0, 4400 string $sort = null, 4401 string $fields = null, 4402 int $dbquerylimit = COURSE_DB_QUERY_LIMIT, 4403 array $includecourses = [], 4404 array $hiddencourses = [] 4405 ) : Generator { 4406 4407 $haslimit = !empty($limit); 4408 $recordsloaded = 0; 4409 $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit; 4410 4411 while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $includecourses, false, $offset, $hiddencourses)) { 4412 yield from $courses; 4413 4414 $recordsloaded += $querylimit; 4415 4416 if (count($courses) < $querylimit) { 4417 break; 4418 } 4419 if ($haslimit && $recordsloaded >= $limit) { 4420 break; 4421 } 4422 4423 $offset += $querylimit; 4424 } 4425 } 4426 4427 /** 4428 * Get the list of enrolled courses the current user searched for. 4429 * 4430 * This function returns a Generator. The courses will be loaded from the database 4431 * in chunks rather than a single query. 4432 * 4433 * @param int $limit Restrict result set to this amount 4434 * @param int $offset Skip this number of records from the start of the result set 4435 * @param string|null $sort SQL string for sorting 4436 * @param string|null $fields SQL string for fields to be returned 4437 * @param int $dbquerylimit The number of records to load per DB request 4438 * @param array $searchcriteria contains search criteria 4439 * @param array $options display options, same as in get_courses() except 'recursive' is ignored - 4440 * search is always category-independent 4441 * @return Generator 4442 */ 4443 function course_get_enrolled_courses_for_logged_in_user_from_search( 4444 int $limit = 0, 4445 int $offset = 0, 4446 string $sort = null, 4447 string $fields = null, 4448 int $dbquerylimit = COURSE_DB_QUERY_LIMIT, 4449 array $searchcriteria = [], 4450 array $options = [] 4451 ) : Generator { 4452 4453 $haslimit = !empty($limit); 4454 $recordsloaded = 0; 4455 $querylimit = (!$haslimit || $limit > $dbquerylimit) ? $dbquerylimit : $limit; 4456 $ids = core_course_category::search_courses($searchcriteria, $options); 4457 4458 // If no courses were found matching the criteria return back. 4459 if (empty($ids)) { 4460 return; 4461 } 4462 4463 while ($courses = enrol_get_my_courses($fields, $sort, $querylimit, $ids, false, $offset)) { 4464 yield from $courses; 4465 4466 $recordsloaded += $querylimit; 4467 4468 if (count($courses) < $querylimit) { 4469 break; 4470 } 4471 if ($haslimit && $recordsloaded >= $limit) { 4472 break; 4473 } 4474 4475 $offset += $querylimit; 4476 } 4477 } 4478 4479 /** 4480 * Search the given $courses for any that match the given $classification up to the specified 4481 * $limit. 4482 * 4483 * This function will return the subset of courses that match the classification as well as the 4484 * number of courses it had to process to build that subset. 4485 * 4486 * It is recommended that for larger sets of courses this function is given a Generator that loads 4487 * the courses from the database in chunks. 4488 * 4489 * @param array|Traversable $courses List of courses to process 4490 * @param string $classification One of the COURSE_TIMELINE_* constants 4491 * @param int $limit Limit the number of results to this amount 4492 * @return array First value is the filtered courses, second value is the number of courses processed 4493 */ 4494 function course_filter_courses_by_timeline_classification( 4495 $courses, 4496 string $classification, 4497 int $limit = 0 4498 ) : array { 4499 4500 if (!in_array($classification, 4501 [COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, COURSE_TIMELINE_INPROGRESS, 4502 COURSE_TIMELINE_FUTURE, COURSE_TIMELINE_HIDDEN, COURSE_TIMELINE_SEARCH])) { 4503 $message = 'Classification must be one of COURSE_TIMELINE_ALLINCLUDINGHIDDEN, COURSE_TIMELINE_ALL, COURSE_TIMELINE_PAST, ' 4504 . 'COURSE_TIMELINE_INPROGRESS, COURSE_TIMELINE_SEARCH or COURSE_TIMELINE_FUTURE'; 4505 throw new moodle_exception($message); 4506 } 4507 4508 $filteredcourses = []; 4509 $numberofcoursesprocessed = 0; 4510 $filtermatches = 0; 4511 4512 foreach ($courses as $course) { 4513 $numberofcoursesprocessed++; 4514 $pref = get_user_preferences('block_myoverview_hidden_course_' . $course->id, 0); 4515 4516 // Added as of MDL-63457 toggle viewability for each user. 4517 if ($classification == COURSE_TIMELINE_ALLINCLUDINGHIDDEN || ($classification == COURSE_TIMELINE_HIDDEN && $pref) || 4518 $classification == COURSE_TIMELINE_SEARCH|| 4519 (($classification == COURSE_TIMELINE_ALL || $classification == course_classify_for_timeline($course)) && !$pref)) { 4520 $filteredcourses[] = $course; 4521 $filtermatches++; 4522 } 4523 4524 if ($limit && $filtermatches >= $limit) { 4525 // We've found the number of requested courses. No need to continue searching. 4526 break; 4527 } 4528 } 4529 4530 // Return the number of filtered courses as well as the number of courses that were searched 4531 // in order to find the matching courses. This allows the calling code to do some kind of 4532 // pagination. 4533 return [$filteredcourses, $numberofcoursesprocessed]; 4534 } 4535 4536 /** 4537 * Search the given $courses for any that match the given $classification up to the specified 4538 * $limit. 4539 * 4540 * This function will return the subset of courses that are favourites as well as the 4541 * number of courses it had to process to build that subset. 4542 * 4543 * It is recommended that for larger sets of courses this function is given a Generator that loads 4544 * the courses from the database in chunks. 4545 * 4546 * @param array|Traversable $courses List of courses to process 4547 * @param array $favouritecourseids Array of favourite courses. 4548 * @param int $limit Limit the number of results to this amount 4549 * @return array First value is the filtered courses, second value is the number of courses processed 4550 */ 4551 function course_filter_courses_by_favourites( 4552 $courses, 4553 $favouritecourseids, 4554 int $limit = 0 4555 ) : array { 4556 4557 $filteredcourses = []; 4558 $numberofcoursesprocessed = 0; 4559 $filtermatches = 0; 4560 4561 foreach ($courses as $course) { 4562 $numberofcoursesprocessed++; 4563 4564 if (in_array($course->id, $favouritecourseids)) { 4565 $filteredcourses[] = $course; 4566 $filtermatches++; 4567 } 4568 4569 if ($limit && $filtermatches >= $limit) { 4570 // We've found the number of requested courses. No need to continue searching. 4571 break; 4572 } 4573 } 4574 4575 // Return the number of filtered courses as well as the number of courses that were searched 4576 // in order to find the matching courses. This allows the calling code to do some kind of 4577 // pagination. 4578 return [$filteredcourses, $numberofcoursesprocessed]; 4579 } 4580 4581 /** 4582 * Search the given $courses for any that have a $customfieldname value that matches the given 4583 * $customfieldvalue, up to the specified $limit. 4584 * 4585 * This function will return the subset of courses that matches the value as well as the 4586 * number of courses it had to process to build that subset. 4587 * 4588 * It is recommended that for larger sets of courses this function is given a Generator that loads 4589 * the courses from the database in chunks. 4590 * 4591 * @param array|Traversable $courses List of courses to process 4592 * @param string $customfieldname the shortname of the custom field to match against 4593 * @param string $customfieldvalue the value this custom field needs to match 4594 * @param int $limit Limit the number of results to this amount 4595 * @return array First value is the filtered courses, second value is the number of courses processed 4596 */ 4597 function course_filter_courses_by_customfield( 4598 $courses, 4599 $customfieldname, 4600 $customfieldvalue, 4601 int $limit = 0 4602 ) : array { 4603 global $DB; 4604 4605 if (!$courses) { 4606 return [[], 0]; 4607 } 4608 4609 // Prepare the list of courses to search through. 4610 $coursesbyid = []; 4611 foreach ($courses as $course) { 4612 $coursesbyid[$course->id] = $course; 4613 } 4614 if (!$coursesbyid) { 4615 return [[], 0]; 4616 } 4617 list($csql, $params) = $DB->get_in_or_equal(array_keys($coursesbyid), SQL_PARAMS_NAMED); 4618 4619 // Get the id of the custom field. 4620 $sql = " 4621 SELECT f.id 4622 FROM {customfield_field} f 4623 JOIN {customfield_category} cat ON cat.id = f.categoryid 4624 WHERE f.shortname = ? 4625 AND cat.component = 'core_course' 4626 AND cat.area = 'course' 4627 "; 4628 $fieldid = $DB->get_field_sql($sql, [$customfieldname]); 4629 if (!$fieldid) { 4630 return [[], 0]; 4631 } 4632 4633 // Get a list of courseids that match that custom field value. 4634 if ($customfieldvalue == COURSE_CUSTOMFIELD_EMPTY) { 4635 $comparevalue = $DB->sql_compare_text('cd.value'); 4636 $sql = " 4637 SELECT c.id 4638 FROM {course} c 4639 LEFT JOIN {customfield_data} cd ON cd.instanceid = c.id AND cd.fieldid = :fieldid 4640 WHERE c.id $csql 4641 AND (cd.value IS NULL OR $comparevalue = '' OR $comparevalue = '0') 4642 "; 4643 $params['fieldid'] = $fieldid; 4644 $matchcourseids = $DB->get_fieldset_sql($sql, $params); 4645 } else { 4646 $comparevalue = $DB->sql_compare_text('value'); 4647 $select = "fieldid = :fieldid AND $comparevalue = :customfieldvalue AND instanceid $csql"; 4648 $params['fieldid'] = $fieldid; 4649 $params['customfieldvalue'] = $customfieldvalue; 4650 $matchcourseids = $DB->get_fieldset_select('customfield_data', 'instanceid', $select, $params); 4651 } 4652 4653 // Prepare the list of courses to return. 4654 $filteredcourses = []; 4655 $numberofcoursesprocessed = 0; 4656 $filtermatches = 0; 4657 4658 foreach ($coursesbyid as $course) { 4659 $numberofcoursesprocessed++; 4660 4661 if (in_array($course->id, $matchcourseids)) { 4662 $filteredcourses[] = $course; 4663 $filtermatches++; 4664 } 4665 4666 if ($limit && $filtermatches >= $limit) { 4667 // We've found the number of requested courses. No need to continue searching. 4668 break; 4669 } 4670 } 4671 4672 // Return the number of filtered courses as well as the number of courses that were searched 4673 // in order to find the matching courses. This allows the calling code to do some kind of 4674 // pagination. 4675 return [$filteredcourses, $numberofcoursesprocessed]; 4676 } 4677 4678 /** 4679 * Check module updates since a given time. 4680 * This function checks for updates in the module config, file areas, completion, grades, comments and ratings. 4681 * 4682 * @param cm_info $cm course module data 4683 * @param int $from the time to check 4684 * @param array $fileareas additional file ares to check 4685 * @param array $filter if we need to filter and return only selected updates 4686 * @return stdClass object with the different updates 4687 * @since Moodle 3.2 4688 */ 4689 function course_check_module_updates_since($cm, $from, $fileareas = array(), $filter = array()) { 4690 global $DB, $CFG, $USER; 4691 4692 $context = $cm->context; 4693 $mod = $DB->get_record($cm->modname, array('id' => $cm->instance), '*', MUST_EXIST); 4694 4695 $updates = new stdClass(); 4696 $course = get_course($cm->course); 4697 $component = 'mod_' . $cm->modname; 4698 4699 // Check changes in the module configuration. 4700 if (isset($mod->timemodified) and (empty($filter) or in_array('configuration', $filter))) { 4701 $updates->configuration = (object) array('updated' => false); 4702 if ($updates->configuration->updated = $mod->timemodified > $from) { 4703 $updates->configuration->timeupdated = $mod->timemodified; 4704 } 4705 } 4706 4707 // Check for updates in files. 4708 if (plugin_supports('mod', $cm->modname, FEATURE_MOD_INTRO)) { 4709 $fileareas[] = 'intro'; 4710 } 4711 if (!empty($fileareas) and (empty($filter) or in_array('fileareas', $filter))) { 4712 $fs = get_file_storage(); 4713 $files = $fs->get_area_files($context->id, $component, $fileareas, false, "filearea, timemodified DESC", false, $from); 4714 foreach ($fileareas as $filearea) { 4715 $updates->{$filearea . 'files'} = (object) array('updated' => false); 4716 } 4717 foreach ($files as $file) { 4718 $updates->{$file->get_filearea() . 'files'}->updated = true; 4719 $updates->{$file->get_filearea() . 'files'}->itemids[] = $file->get_id(); 4720 } 4721 } 4722 4723 // Check completion. 4724 $supportcompletion = plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_HAS_RULES); 4725 $supportcompletion = $supportcompletion or plugin_supports('mod', $cm->modname, FEATURE_COMPLETION_TRACKS_VIEWS); 4726 if ($supportcompletion and (empty($filter) or in_array('completion', $filter))) { 4727 $updates->completion = (object) array('updated' => false); 4728 $completion = new completion_info($course); 4729 // Use wholecourse to cache all the modules the first time. 4730 $completiondata = $completion->get_data($cm, true); 4731 if ($updates->completion->updated = !empty($completiondata->timemodified) && $completiondata->timemodified > $from) { 4732 $updates->completion->timemodified = $completiondata->timemodified; 4733 } 4734 } 4735 4736 // Check grades. 4737 $supportgrades = plugin_supports('mod', $cm->modname, FEATURE_GRADE_HAS_GRADE); 4738 $supportgrades = $supportgrades or plugin_supports('mod', $cm->modname, FEATURE_GRADE_OUTCOMES); 4739 if ($supportgrades and (empty($filter) or (in_array('gradeitems', $filter) or in_array('outcomes', $filter)))) { 4740 require_once($CFG->libdir . '/gradelib.php'); 4741 $grades = grade_get_grades($course->id, 'mod', $cm->modname, $mod->id, $USER->id); 4742 4743 if (empty($filter) or in_array('gradeitems', $filter)) { 4744 $updates->gradeitems = (object) array('updated' => false); 4745 foreach ($grades->items as $gradeitem) { 4746 foreach ($gradeitem->grades as $grade) { 4747 if ($grade->datesubmitted > $from or $grade->dategraded > $from) { 4748 $updates->gradeitems->updated = true; 4749 $updates->gradeitems->itemids[] = $gradeitem->id; 4750 } 4751 } 4752 } 4753 } 4754 4755 if (empty($filter) or in_array('outcomes', $filter)) { 4756 $updates->outcomes = (object) array('updated' => false); 4757 foreach ($grades->outcomes as $outcome) { 4758 foreach ($outcome->grades as $grade) { 4759 if ($grade->datesubmitted > $from or $grade->dategraded > $from) { 4760 $updates->outcomes->updated = true; 4761 $updates->outcomes->itemids[] = $outcome->id; 4762 } 4763 } 4764 } 4765 } 4766 } 4767 4768 // Check comments. 4769 if (plugin_supports('mod', $cm->modname, FEATURE_COMMENT) and (empty($filter) or in_array('comments', $filter))) { 4770 $updates->comments = (object) array('updated' => false); 4771 require_once($CFG->dirroot . '/comment/lib.php'); 4772 require_once($CFG->dirroot . '/comment/locallib.php'); 4773 $manager = new comment_manager(); 4774 $comments = $manager->get_component_comments_since($course, $context, $component, $from, $cm); 4775 if (!empty($comments)) { 4776 $updates->comments->updated = true; 4777 $updates->comments->itemids = array_keys($comments); 4778 } 4779 } 4780 4781 // Check ratings. 4782 if (plugin_supports('mod', $cm->modname, FEATURE_RATE) and (empty($filter) or in_array('ratings', $filter))) { 4783 $updates->ratings = (object) array('updated' => false); 4784 require_once($CFG->dirroot . '/rating/lib.php'); 4785 $manager = new rating_manager(); 4786 $ratings = $manager->get_component_ratings_since($context, $component, $from); 4787 if (!empty($ratings)) { 4788 $updates->ratings->updated = true; 4789 $updates->ratings->itemids = array_keys($ratings); 4790 } 4791 } 4792 4793 return $updates; 4794 } 4795 4796 /** 4797 * Returns true if the user can view the participant page, false otherwise, 4798 * 4799 * @param context $context The context we are checking. 4800 * @return bool 4801 */ 4802 function course_can_view_participants($context) { 4803 $viewparticipantscap = 'moodle/course:viewparticipants'; 4804 if ($context->contextlevel == CONTEXT_SYSTEM) { 4805 $viewparticipantscap = 'moodle/site:viewparticipants'; 4806 } 4807 4808 return has_any_capability([$viewparticipantscap, 'moodle/course:enrolreview'], $context); 4809 } 4810 4811 /** 4812 * Checks if a user can view the participant page, if not throws an exception. 4813 * 4814 * @param context $context The context we are checking. 4815 * @throws required_capability_exception 4816 */ 4817 function course_require_view_participants($context) { 4818 if (!course_can_view_participants($context)) { 4819 $viewparticipantscap = 'moodle/course:viewparticipants'; 4820 if ($context->contextlevel == CONTEXT_SYSTEM) { 4821 $viewparticipantscap = 'moodle/site:viewparticipants'; 4822 } 4823 throw new required_capability_exception($context, $viewparticipantscap, 'nopermissions', ''); 4824 } 4825 } 4826 4827 /** 4828 * Return whether the user can download from the specified backup file area in the given context. 4829 * 4830 * @param string $filearea the backup file area. E.g. 'course', 'backup' or 'automated'. 4831 * @param \context $context 4832 * @param stdClass $user the user object. If not provided, the current user will be checked. 4833 * @return bool true if the user is allowed to download in the context, false otherwise. 4834 */ 4835 function can_download_from_backup_filearea($filearea, \context $context, stdClass $user = null) { 4836 $candownload = false; 4837 switch ($filearea) { 4838 case 'course': 4839 case 'backup': 4840 $candownload = has_capability('moodle/backup:downloadfile', $context, $user); 4841 break; 4842 case 'automated': 4843 // Given the automated backups may contain userinfo, we restrict access such that only users who are able to 4844 // restore with userinfo are able to download the file. Users can't create these backups, so checking 'backup:userinfo' 4845 // doesn't make sense here. 4846 $candownload = has_capability('moodle/backup:downloadfile', $context, $user) && 4847 has_capability('moodle/restore:userinfo', $context, $user); 4848 break; 4849 default: 4850 break; 4851 4852 } 4853 return $candownload; 4854 } 4855 4856 /** 4857 * Get a list of hidden courses 4858 * 4859 * @param int|object|null $user User override to get the filter from. Defaults to current user 4860 * @return array $ids List of hidden courses 4861 * @throws coding_exception 4862 */ 4863 function get_hidden_courses_on_timeline($user = null) { 4864 global $USER; 4865 4866 if (empty($user)) { 4867 $user = $USER->id; 4868 } 4869 4870 $preferences = get_user_preferences(null, null, $user); 4871 $ids = []; 4872 foreach ($preferences as $key => $value) { 4873 if (preg_match('/block_myoverview_hidden_course_(\d)+/', $key)) { 4874 $id = preg_split('/block_myoverview_hidden_course_/', $key); 4875 $ids[] = $id[1]; 4876 } 4877 } 4878 4879 return $ids; 4880 } 4881 4882 /** 4883 * Returns a list of the most recently courses accessed by a user 4884 * 4885 * @param int $userid User id from which the courses will be obtained 4886 * @param int $limit Restrict result set to this amount 4887 * @param int $offset Skip this number of records from the start of the result set 4888 * @param string|null $sort SQL string for sorting 4889 * @return array 4890 */ 4891 function course_get_recent_courses(int $userid = null, int $limit = 0, int $offset = 0, string $sort = null) { 4892 4893 global $CFG, $USER, $DB; 4894 4895 if (empty($userid)) { 4896 $userid = $USER->id; 4897 } 4898 4899 $basefields = [ 4900 'id', 'idnumber', 'summary', 'summaryformat', 'startdate', 'enddate', 'category', 4901 'shortname', 'fullname', 'timeaccess', 'component', 'visible', 4902 'showactivitydates', 'showcompletionconditions', 'pdfexportfont' 4903 ]; 4904 4905 if (empty($sort)) { 4906 $sort = 'timeaccess DESC'; 4907 } else { 4908 // The SQL string for sorting can define sorting by multiple columns. 4909 $rawsorts = explode(',', $sort); 4910 $sorts = array(); 4911 // Validate and trim the sort parameters in the SQL string for sorting. 4912 foreach ($rawsorts as $rawsort) { 4913 $sort = trim($rawsort); 4914 $sortparams = explode(' ', $sort); 4915 // A valid sort statement can not have more than 2 params (ex. 'summary desc' or 'timeaccess'). 4916 if (count($sortparams) > 2) { 4917 throw new invalid_parameter_exception( 4918 'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].'); 4919 } 4920 $sortfield = trim($sortparams[0]); 4921 // Validate the value which defines the field to sort by. 4922 if (!in_array($sortfield, $basefields)) { 4923 throw new invalid_parameter_exception('Invalid field in the sort parameter, allowed fields: ' . 4924 implode(', ', $basefields) . '.'); 4925 } 4926 $sortdirection = isset($sortparams[1]) ? trim($sortparams[1]) : ''; 4927 // Validate the value which defines the sort direction (if present). 4928 $allowedsortdirections = ['asc', 'desc']; 4929 if (!empty($sortdirection) && !in_array(strtolower($sortdirection), $allowedsortdirections)) { 4930 throw new invalid_parameter_exception('Invalid sort direction in the sort parameter, allowed values: ' . 4931 implode(', ', $allowedsortdirections) . '.'); 4932 } 4933 $sorts[] = $sort; 4934 } 4935 $sort = implode(',', $sorts); 4936 } 4937 4938 $ctxfields = context_helper::get_preload_record_columns_sql('ctx'); 4939 4940 $coursefields = 'c.' . join(',', $basefields); 4941 4942 // Ask the favourites service to give us the join SQL for favourited courses, 4943 // so we can include favourite information in the query. 4944 $usercontext = \context_user::instance($userid); 4945 $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext); 4946 list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_course', 'courses', 'fav', 'ul.courseid'); 4947 4948 $sql = "SELECT $coursefields, $ctxfields 4949 FROM {course} c 4950 JOIN {context} ctx 4951 ON ctx.contextlevel = :contextlevel 4952 AND ctx.instanceid = c.id 4953 JOIN {user_lastaccess} ul 4954 ON ul.courseid = c.id 4955 $favsql 4956 LEFT JOIN {enrol} eg ON eg.courseid = c.id AND eg.status = :statusenrolg AND eg.enrol = :guestenrol 4957 WHERE ul.userid = :userid 4958 AND c.visible = :visible 4959 AND (eg.id IS NOT NULL 4960 OR EXISTS (SELECT e.id 4961 FROM {enrol} e 4962 JOIN {user_enrolments} ue ON ue.enrolid = e.id 4963 WHERE e.courseid = c.id 4964 AND e.status = :statusenrol 4965 AND ue.status = :status 4966 AND ue.userid = :userid2 4967 AND ue.timestart < :now1 4968 AND (ue.timeend = 0 OR ue.timeend > :now2) 4969 )) 4970 ORDER BY $sort"; 4971 4972 $now = round(time(), -2); // Improves db caching. 4973 $params = ['userid' => $userid, 'contextlevel' => CONTEXT_COURSE, 'visible' => 1, 'status' => ENROL_USER_ACTIVE, 4974 'statusenrol' => ENROL_INSTANCE_ENABLED, 'guestenrol' => 'guest', 'now1' => $now, 'now2' => $now, 4975 'userid2' => $userid, 'statusenrolg' => ENROL_INSTANCE_ENABLED] + $favparams; 4976 4977 $recentcourses = $DB->get_records_sql($sql, $params, $offset, $limit); 4978 4979 // Filter courses if last access field is hidden. 4980 $hiddenfields = array_flip(explode(',', $CFG->hiddenuserfields)); 4981 4982 if ($userid != $USER->id && isset($hiddenfields['lastaccess'])) { 4983 $recentcourses = array_filter($recentcourses, function($course) { 4984 context_helper::preload_from_record($course); 4985 $context = context_course::instance($course->id, IGNORE_MISSING); 4986 // If last access was a hidden field, a user requesting info about another user would need permission to view hidden 4987 // fields. 4988 return has_capability('moodle/course:viewhiddenuserfields', $context); 4989 }); 4990 } 4991 4992 return $recentcourses; 4993 } 4994 4995 /** 4996 * Calculate the course start date and offset for the given user ids. 4997 * 4998 * If the course is a fixed date course then the course start date will be returned. 4999 * If the course is a relative date course then the course date will be calculated and 5000 * and offset provided. 5001 * 5002 * The dates are returned as an array with the index being the user id. The array 5003 * contains the start date and start offset values for the user. 5004 * 5005 * If the user is not enrolled in the course then the course start date will be returned. 5006 * 5007 * If we have a course which starts on 1563244000 and 2 users, id 123 and 456, where the 5008 * former is enrolled in the course at 1563244693 and the latter is not enrolled then the 5009 * return value would look like: 5010 * [ 5011 * '123' => [ 5012 * 'start' => 1563244693, 5013 * 'startoffset' => 693 5014 * ], 5015 * '456' => [ 5016 * 'start' => 1563244000, 5017 * 'startoffset' => 0 5018 * ] 5019 * ] 5020 * 5021 * @param stdClass $course The course to fetch dates for. 5022 * @param array $userids The list of user ids to get dates for. 5023 * @return array 5024 */ 5025 function course_get_course_dates_for_user_ids(stdClass $course, array $userids): array { 5026 if (empty($course->relativedatesmode)) { 5027 // This course isn't set to relative dates so we can early return with the course 5028 // start date. 5029 return array_reduce($userids, function($carry, $userid) use ($course) { 5030 $carry[$userid] = [ 5031 'start' => $course->startdate, 5032 'startoffset' => 0 5033 ]; 5034 return $carry; 5035 }, []); 5036 } 5037 5038 // We're dealing with a relative dates course now so we need to calculate some dates. 5039 $cache = cache::make('core', 'course_user_dates'); 5040 $dates = []; 5041 $uncacheduserids = []; 5042 5043 // Try fetching the values from the cache so that we don't need to do a DB request. 5044 foreach ($userids as $userid) { 5045 $cachekey = "{$course->id}_{$userid}"; 5046 $cachedvalue = $cache->get($cachekey); 5047 5048 if ($cachedvalue === false) { 5049 // Looks like we haven't seen this user for this course before so we'll have 5050 // to fetch it. 5051 $uncacheduserids[] = $userid; 5052 } else { 5053 [$start, $startoffset] = $cachedvalue; 5054 $dates[$userid] = [ 5055 'start' => $start, 5056 'startoffset' => $startoffset 5057 ]; 5058 } 5059 } 5060 5061 if (!empty($uncacheduserids)) { 5062 // Load the enrolments for any users we haven't seen yet. Set the "onlyactive" param 5063 // to false because it filters out users with enrolment start times in the future which 5064 // we don't want. 5065 $enrolments = enrol_get_course_users($course->id, false, $uncacheduserids); 5066 5067 foreach ($uncacheduserids as $userid) { 5068 // Find the user enrolment that has the earliest start date. 5069 $enrolment = array_reduce(array_values($enrolments), function($carry, $enrolment) use ($userid) { 5070 // Only consider enrolments for this user if the user enrolment is active and the 5071 // enrolment method is enabled. 5072 if ( 5073 $enrolment->uestatus == ENROL_USER_ACTIVE && 5074 $enrolment->estatus == ENROL_INSTANCE_ENABLED && 5075 $enrolment->id == $userid 5076 ) { 5077 if (is_null($carry)) { 5078 // Haven't found an enrolment yet for this user so use the one we just found. 5079 $carry = $enrolment; 5080 } else { 5081 // We've already found an enrolment for this user so let's use which ever one 5082 // has the earliest start time. 5083 $carry = $carry->uetimestart < $enrolment->uetimestart ? $carry : $enrolment; 5084 } 5085 } 5086 5087 return $carry; 5088 }, null); 5089 5090 if ($enrolment) { 5091 // The course is in relative dates mode so we calculate the student's start 5092 // date based on their enrolment start date. 5093 $start = $course->startdate > $enrolment->uetimestart ? $course->startdate : $enrolment->uetimestart; 5094 $startoffset = $start - $course->startdate; 5095 } else { 5096 // The user is not enrolled in the course so default back to the course start date. 5097 $start = $course->startdate; 5098 $startoffset = 0; 5099 } 5100 5101 $dates[$userid] = [ 5102 'start' => $start, 5103 'startoffset' => $startoffset 5104 ]; 5105 5106 $cachekey = "{$course->id}_{$userid}"; 5107 $cache->set($cachekey, [$start, $startoffset]); 5108 } 5109 } 5110 5111 return $dates; 5112 } 5113 5114 /** 5115 * Calculate the course start date and offset for the given user id. 5116 * 5117 * If the course is a fixed date course then the course start date will be returned. 5118 * If the course is a relative date course then the course date will be calculated and 5119 * and offset provided. 5120 * 5121 * The return array contains the start date and start offset values for the user. 5122 * 5123 * If the user is not enrolled in the course then the course start date will be returned. 5124 * 5125 * If we have a course which starts on 1563244000. If a user's enrolment starts on 1563244693 5126 * then the return would be: 5127 * [ 5128 * 'start' => 1563244693, 5129 * 'startoffset' => 693 5130 * ] 5131 * 5132 * If the use was not enrolled then the return would be: 5133 * [ 5134 * 'start' => 1563244000, 5135 * 'startoffset' => 0 5136 * ] 5137 * 5138 * @param stdClass $course The course to fetch dates for. 5139 * @param int $userid The user id to get dates for. 5140 * @return array 5141 */ 5142 function course_get_course_dates_for_user_id(stdClass $course, int $userid): array { 5143 return (course_get_course_dates_for_user_ids($course, [$userid]))[$userid]; 5144 } 5145 5146 /** 5147 * Renders the course copy form for the modal on the course management screen. 5148 * 5149 * @param array $args 5150 * @return string $o Form HTML. 5151 */ 5152 function course_output_fragment_new_base_form($args) { 5153 5154 $serialiseddata = json_decode($args['jsonformdata'], true); 5155 $formdata = []; 5156 if (!empty($serialiseddata)) { 5157 parse_str($serialiseddata, $formdata); 5158 } 5159 5160 $context = context_course::instance($args['courseid']); 5161 $copycaps = \core_course\management\helper::get_course_copy_capabilities(); 5162 require_all_capabilities($copycaps, $context); 5163 5164 $course = get_course($args['courseid']); 5165 $mform = new \core_backup\output\copy_form( 5166 null, 5167 array('course' => $course, 'returnto' => '', 'returnurl' => ''), 5168 'post', '', ['class' => 'ignoredirty'], true, $formdata); 5169 5170 if (!empty($serialiseddata)) { 5171 // If we were passed non-empty form data we want the mform to call validation functions and show errors. 5172 $mform->is_validated(); 5173 } 5174 5175 ob_start(); 5176 $mform->display(); 5177 $o = ob_get_contents(); 5178 ob_end_clean(); 5179 5180 return $o; 5181 } 5182 5183 /** 5184 * Get the current course image for the given course. 5185 * 5186 * @param \stdClass $course 5187 * @return null|stored_file 5188 */ 5189 function course_get_courseimage(\stdClass $course): ?stored_file { 5190 $courseinlist = new core_course_list_element($course); 5191 foreach ($courseinlist->get_course_overviewfiles() as $file) { 5192 if ($file->is_valid_image()) { 5193 return $file; 5194 } 5195 } 5196 return null; 5197 } 5198 5199 /** 5200 * Get course specific data for configuring a communication instance. 5201 * 5202 * @param integer $courseid The course id. 5203 * @return array Returns course data, context and heading. 5204 */ 5205 function course_get_communication_instance_data(int $courseid): array { 5206 // Do some checks and prepare instance specific data. 5207 $course = get_course($courseid); 5208 require_login($course); 5209 $context = context_course::instance($course->id); 5210 require_capability('moodle/course:configurecoursecommunication', $context); 5211 5212 $heading = $course->fullname; 5213 $returnurl = new moodle_url('/course/view.php', ['id' => $courseid]); 5214 5215 return [$course, $context, $heading, $returnurl]; 5216 } 5217 5218 /** 5219 * Update a course using communication configuration data. 5220 * 5221 * @param stdClass $data The data to update the course with. 5222 */ 5223 function course_update_communication_instance_data(stdClass $data): void { 5224 $data->id = $data->instanceid; // For correct use in update_course. 5225 update_course($data); 5226 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body