Proyectos de Subversion Moodle

Rev

Rev 11 | | Comparar con el anterior | Ultima modificación | Ver Log |

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