weekly release 4.5dev
[moodle.git] / lib / testing / generator / data_generator.php
blob4b8c42ae8fd1f8f77419c8d85dcf336804803773
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.
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/>.
17 defined('MOODLE_INTERNAL') || die();
19 /**
20 * Data generator class for unit tests and other tools that need to create fake test sites.
22 * @package core
23 * @category test
24 * @copyright 2012 Petr Skoda {@link http://skodak.org}
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 class testing_data_generator {
28 /** @var int The number of grade categories created */
29 protected $gradecategorycounter = 0;
30 /** @var int The number of grade items created */
31 protected $gradeitemcounter = 0;
32 /** @var int The number of grade outcomes created */
33 protected $gradeoutcomecounter = 0;
34 protected $usercounter = 0;
35 protected $categorycount = 0;
36 protected $cohortcount = 0;
37 protected $coursecount = 0;
38 protected $scalecount = 0;
39 protected $groupcount = 0;
40 protected $groupingcount = 0;
41 protected $rolecount = 0;
42 protected $tagcount = 0;
44 /** @var array list of plugin generators */
45 protected $generators = array();
47 /** @var array lis of common last names */
48 public $lastnames = array(
49 'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'García', 'Rodríguez', 'Wilson',
50 'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Meyer', 'Weber', 'Schulz', 'Wagner', 'Becker', 'Hoffmann',
51 'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházková', 'Kučerová', 'Veselá', 'Horáková', 'Němcová',
52 'Смирнов', 'Иванов', 'Кузнецов', 'Соколов', 'Попов', 'Лебедева', 'Козлова', 'Новикова', 'Морозова', 'Петрова',
53 '王', '李', '张', '刘', '陈', '楊', '黃', '趙', '吳', '周',
54 '佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '斎藤',
57 /** @var array lis of common first names */
58 public $firstnames = array(
59 'Jacob', 'Ethan', 'Michael', 'Jayden', 'William', 'Isabella', 'Sophia', 'Emma', 'Olivia', 'Ava',
60 'Lukas', 'Leon', 'Luca', 'Timm', 'Paul', 'Leonie', 'Leah', 'Lena', 'Hanna', 'Laura',
61 'Jakub', 'Jan', 'Tomáš', 'Lukáš', 'Matěj', 'Tereza', 'Eliška', 'Anna', 'Adéla', 'Karolína',
62 'Даниил', 'Максим', 'Артем', 'Иван', 'Александр', 'София', 'Анастасия', 'Дарья', 'Мария', 'Полина',
63 '伟', '伟', '芳', '伟', '秀英', '秀英', '娜', '秀英', '伟', '敏',
64 '翔', '大翔', '拓海', '翔太', '颯太', '陽菜', 'さくら', '美咲', '葵', '美羽',
67 public $loremipsum = <<<EOD
68 Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla non arcu lacinia neque faucibus fringilla. Vivamus porttitor turpis ac leo. Integer in sapien. Nullam eget nisl. Aliquam erat volutpat. Cras elementum. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Integer malesuada. Nullam lectus justo, vulputate eget mollis sed, tempor sed magna. Mauris elementum mauris vitae tortor. Aliquam erat volutpat.
69 Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Pellentesque ipsum. Cras pede libero, dapibus nec, pretium sit amet, tempor quis. Aliquam ante. Proin in tellus sit amet nibh dignissim sagittis. Vivamus porttitor turpis ac leo. Duis bibendum, lectus ut viverra rhoncus, dolor nunc faucibus libero, eget facilisis enim ipsum id lacus. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. Aliquam erat volutpat. Nulla est.
70 Vivamus luctus egestas leo. Aenean fermentum risus id tortor. Mauris dictum facilisis augue. Aliquam erat volutpat. Aliquam ornare wisi eu metus. Aliquam id dolor. Duis condimentum augue id magna semper rutrum. Donec iaculis gravida nulla. Pellentesque ipsum. Etiam dictum tincidunt diam. Quisque tincidunt scelerisque libero. Etiam egestas wisi a erat.
71 Integer lacinia. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris tincidunt sem sed arcu. Nullam feugiat, turpis at pulvinar vulputate, erat libero tristique tellus, nec bibendum odio risus sit amet ante. Aliquam id dolor. Maecenas sollicitudin. Et harum quidem rerum facilis est et expedita distinctio. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Nullam dapibus fermentum ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Pellentesque sapien. Duis risus. Mauris elementum mauris vitae tortor. Suspendisse nisl. Integer rutrum, orci vestibulum ullamcorper ultricies, lacus quam ultricies odio, vitae placerat pede sem sit amet enim.
72 In laoreet, magna id viverra tincidunt, sem odio bibendum justo, vel imperdiet sapien wisi sed libero. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Nullam justo enim, consectetuer nec, ullamcorper ac, vestibulum in, elit. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? Maecenas lorem. Etiam posuere lacus quis dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Curabitur ligula sapien, pulvinar a vestibulum quis, facilisis vel sapien. Nam sed tellus id magna elementum tincidunt. Suspendisse nisl. Vivamus luctus egestas leo. Nulla non arcu lacinia neque faucibus fringilla. Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. Etiam dictum tincidunt diam. Etiam commodo dui eget wisi. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Duis ante orci, molestie vitae vehicula venenatis, tincidunt ac pede. Pellentesque sapien.
73 EOD;
75 /**
76 * To be called from data reset code only,
77 * do not use in tests.
78 * @return void
80 public function reset() {
81 $this->gradecategorycounter = 0;
82 $this->gradeitemcounter = 0;
83 $this->gradeoutcomecounter = 0;
84 $this->usercounter = 0;
85 $this->categorycount = 0;
86 $this->cohortcount = 0;
87 $this->coursecount = 0;
88 $this->scalecount = 0;
89 $this->groupcount = 0;
90 $this->groupingcount = 0;
91 $this->rolecount = 0;
92 $this->tagcount = 0;
94 foreach ($this->generators as $generator) {
95 $generator->reset();
99 /**
100 * Return generator for given plugin or component.
101 * @param string $component the component name, e.g. 'mod_forum' or 'core_question'.
102 * @return component_generator_base or rather an instance of the appropriate subclass.
104 public function get_plugin_generator($component) {
105 // Note: This global is included so that generator have access to it.
106 // CFG is widely used in require statements.
107 global $CFG;
108 list($type, $plugin) = core_component::normalize_component($component);
109 $cleancomponent = $type . '_' . $plugin;
110 if ($cleancomponent != $component) {
111 debugging("Please specify the component you want a generator for as " .
112 "{$cleancomponent}, not {$component}.", DEBUG_DEVELOPER);
113 $component = $cleancomponent;
116 if (isset($this->generators[$component])) {
117 return $this->generators[$component];
120 $dir = core_component::get_component_directory($component);
121 $lib = $dir . '/tests/generator/lib.php';
122 if (!$dir || !is_readable($lib)) {
123 $this->generators[$component] = $this->get_default_plugin_generator($component);
125 return $this->generators[$component];
128 include_once($lib);
129 $classname = $component . '_generator';
131 if (class_exists($classname)) {
132 $this->generators[$component] = new $classname($this);
133 } else {
134 $this->generators[$component] = $this->get_default_plugin_generator($component, $classname);
137 return $this->generators[$component];
141 * Create a test user
142 * @param array|stdClass $record
143 * @param array $options
144 * @return stdClass user record
146 public function create_user($record=null, array $options=null) {
147 global $DB, $CFG;
148 require_once($CFG->dirroot.'/user/lib.php');
150 $this->usercounter++;
151 $i = $this->usercounter;
153 $record = (array)$record;
155 if (!isset($record['auth'])) {
156 $record['auth'] = 'manual';
159 if (!isset($record['firstname']) and !isset($record['lastname'])) {
160 $country = rand(0, 5);
161 $firstname = rand(0, 4);
162 $lastname = rand(0, 4);
163 $female = rand(0, 1);
164 $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)];
165 $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)];
167 } else if (!isset($record['firstname'])) {
168 $record['firstname'] = 'Firstname'.$i;
170 } else if (!isset($record['lastname'])) {
171 $record['lastname'] = 'Lastname'.$i;
174 if (!isset($record['firstnamephonetic'])) {
175 $firstnamephonetic = rand(0, 59);
176 $record['firstnamephonetic'] = $this->firstnames[$firstnamephonetic];
179 if (!isset($record['lastnamephonetic'])) {
180 $lastnamephonetic = rand(0, 59);
181 $record['lastnamephonetic'] = $this->lastnames[$lastnamephonetic];
184 if (!isset($record['middlename'])) {
185 $middlename = rand(0, 59);
186 $record['middlename'] = $this->firstnames[$middlename];
189 if (!isset($record['alternatename'])) {
190 $alternatename = rand(0, 59);
191 $record['alternatename'] = $this->firstnames[$alternatename];
194 if (!isset($record['idnumber'])) {
195 $record['idnumber'] = '';
198 if (!isset($record['mnethostid'])) {
199 $record['mnethostid'] = $CFG->mnet_localhost_id;
202 if (!isset($record['username'])) {
203 $record['username'] = 'username'.$i;
204 $j = 2;
205 while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) {
206 $record['username'] = 'username'.$i.'_'.$j;
207 $j++;
211 if (isset($record['password'])) {
212 $record['password'] = hash_internal_user_password($record['password']);
215 if (!isset($record['email'])) {
216 $record['email'] = $record['username'].'@example.com';
219 if (!isset($record['confirmed'])) {
220 $record['confirmed'] = 1;
223 if (!isset($record['lastip'])) {
224 $record['lastip'] = '0.0.0.0';
227 $tobedeleted = !empty($record['deleted']);
228 unset($record['deleted']);
230 $userid = user_create_user($record, false, false);
232 if ($extrafields = array_intersect_key($record, ['password' => 1, 'timecreated' => 1])) {
233 $DB->update_record('user', ['id' => $userid] + $extrafields);
236 if (!$tobedeleted) {
237 // All new not deleted users must have a favourite self-conversation.
238 $selfconversation = \core_message\api::create_conversation(
239 \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
240 [$userid]
242 \core_message\api::set_favourite_conversation($selfconversation->id, $userid);
244 // Save custom profile fields data.
245 $hasprofilefields = array_filter($record, function($key){
246 return strpos($key, 'profile_field_') === 0;
247 }, ARRAY_FILTER_USE_KEY);
248 if ($hasprofilefields) {
249 require_once($CFG->dirroot.'/user/profile/lib.php');
250 $usernew = (object)(['id' => $userid] + $record);
251 profile_save_data($usernew);
255 $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
257 if (!$tobedeleted && isset($record['interests'])) {
258 require_once($CFG->dirroot . '/user/editlib.php');
259 if (!is_array($record['interests'])) {
260 $record['interests'] = preg_split('/\s*,\s*/', trim($record['interests']), -1, PREG_SPLIT_NO_EMPTY);
262 useredit_update_interests($user, $record['interests']);
265 \core\event\user_created::create_from_userid($userid)->trigger();
267 if ($tobedeleted) {
268 delete_user($user);
269 $user = $DB->get_record('user', array('id' => $userid));
271 return $user;
275 * Create a test course category
276 * @param array|stdClass $record
277 * @param array $options
278 * @return core_course_category course category record
280 public function create_category($record=null, array $options=null) {
281 $this->categorycount++;
282 $i = $this->categorycount;
284 $record = (array)$record;
286 if (!isset($record['name'])) {
287 $record['name'] = 'Course category '.$i;
290 if (!isset($record['description'])) {
291 $record['description'] = "Test course category $i\n$this->loremipsum";
294 if (!isset($record['idnumber'])) {
295 $record['idnumber'] = '';
298 return core_course_category::create($record);
302 * Create test cohort.
303 * @param array|stdClass $record
304 * @param array $options
305 * @return stdClass cohort record
307 public function create_cohort($record=null, array $options=null) {
308 global $DB, $CFG;
309 require_once("$CFG->dirroot/cohort/lib.php");
311 $this->cohortcount++;
312 $i = $this->cohortcount;
314 $record = (array)$record;
316 if (!isset($record['contextid'])) {
317 $record['contextid'] = context_system::instance()->id;
320 if (!isset($record['name'])) {
321 $record['name'] = 'Cohort '.$i;
324 if (!isset($record['idnumber'])) {
325 $record['idnumber'] = '';
328 if (!isset($record['description'])) {
329 $record['description'] = "Description for '{$record['name']}' \n$this->loremipsum";
332 if (!isset($record['descriptionformat'])) {
333 $record['descriptionformat'] = FORMAT_MOODLE;
336 if (!isset($record['visible'])) {
337 $record['visible'] = 1;
340 if (!isset($record['component'])) {
341 $record['component'] = '';
344 $id = cohort_add_cohort((object)$record);
346 return $DB->get_record('cohort', array('id'=>$id), '*', MUST_EXIST);
350 * Create a test course
351 * @param array|stdClass $record Apart from the course information, the following can be also set:
352 * 'initsections' => bool for section name initialization, renaming them to "Section X". Default value is 0 (false).
353 * @param array $options with keys:
354 * 'createsections' => bool precreate all sections
355 * @return stdClass course record
357 public function create_course($record=null, array $options=null) {
358 global $DB, $CFG;
359 require_once("$CFG->dirroot/course/lib.php");
361 $this->coursecount++;
362 $i = $this->coursecount;
364 $record = (array)$record;
366 if (!isset($record['fullname'])) {
367 $record['fullname'] = 'Test course '.$i;
370 if (!isset($record['shortname'])) {
371 $record['shortname'] = 'tc_'.$i;
374 if (!isset($record['idnumber'])) {
375 $record['idnumber'] = '';
378 if (!isset($record['format'])) {
379 $record['format'] = 'topics';
382 if (!isset($record['newsitems'])) {
383 $record['newsitems'] = 0;
386 if (!isset($record['numsections'])) {
387 $record['numsections'] = 5;
390 if (!isset($record['summary'])) {
391 $record['summary'] = "Test course $i\n$this->loremipsum";
394 if (!isset($record['summaryformat'])) {
395 $record['summaryformat'] = FORMAT_MOODLE;
398 if (!isset($record['category'])) {
399 $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
402 if (!isset($record['startdate'])) {
403 $record['startdate'] = usergetmidnight(time());
406 if (isset($record['tags']) && !is_array($record['tags'])) {
407 $record['tags'] = preg_split('/\s*,\s*/', trim($record['tags']), -1, PREG_SPLIT_NO_EMPTY);
410 if (!empty($options['createsections']) && empty($record['numsections'])) {
411 // Since Moodle 3.3 function create_course() automatically creates sections if numsections is specified.
412 // For BC if 'createsections' is given but 'numsections' is not, assume the default value from config.
413 $record['numsections'] = get_config('moodlecourse', 'numsections');
416 if (!empty($record['customfields'])) {
417 foreach ($record['customfields'] as $field) {
418 $record['customfield_'.$field['shortname']] = $field['value'];
422 $initsections = !empty($record['initsections']);
423 unset($record['initsections']);
425 $course = create_course((object)$record);
426 if ($initsections) {
427 $this->init_sections($course);
429 context_course::instance($course->id);
431 return $course;
435 * Initializes sections for a specified course, such as configuring section names for courses using 'Section X'.
437 * @param stdClass $course The course object.
439 private function init_sections(stdClass $course): void {
440 global $DB;
442 $sections = $DB->get_records('course_sections', ['course' => $course->id], 'section');
443 foreach ($sections as $section) {
444 if ($section->section != 0) {
445 $DB->set_field(
446 table: 'course_sections',
447 newfield: 'name',
448 newvalue: get_string('section', 'core') . ' ' . $section->section,
449 conditions: [
450 'id' => $section->id,
458 * Create course section if does not exist yet
459 * @param array|stdClass $record must contain 'course' and 'section' attributes
460 * @param array|null $options
461 * @return section_info
462 * @throws coding_exception
464 public function create_course_section($record = null, array $options = null) {
465 global $DB;
467 $record = (array)$record;
469 if (empty($record['course'])) {
470 throw new coding_exception('course must be present in testing_data_generator::create_course_section() $record');
473 if (!isset($record['section'])) {
474 throw new coding_exception('section must be present in testing_data_generator::create_course_section() $record');
477 course_create_sections_if_missing($record['course'], $record['section']);
478 return get_fast_modinfo($record['course'])->get_section_info($record['section']);
482 * Create a test block.
484 * The $record passed in becomes the basis for the new row added to the
485 * block_instances table. You only need to supply the values of interest.
486 * Any missing values have sensible defaults filled in, and ->blockname will be set based on $blockname.
488 * The $options array provides additional data, not directly related to what
489 * will be inserted in the block_instance table, which may affect the block
490 * that is created. The meanings of any data passed here depends on the particular
491 * type of block being created.
493 * @param string $blockname the type of block to create. E.g. 'html'.
494 * @param array|stdClass $record forms the basis for the entry to be inserted in the block_instances table.
495 * @param array $options further, block-specific options to control how the block is created.
496 * @return stdClass new block_instance record.
498 public function create_block($blockname, $record=null, array $options=array()) {
499 $generator = $this->get_plugin_generator('block_'.$blockname);
500 return $generator->create_instance($record, $options);
504 * Create a test activity module.
506 * The $record should contain the same data that you would call from
507 * ->get_data() when the mod_[type]_mod_form is submitted, except that you
508 * only need to supply values of interest. The only required value is
509 * 'course'. Any missing values will have a sensible default supplied.
511 * The $options array provides additional data, not directly related to what
512 * would come back from the module edit settings form, which may affect the activity
513 * that is created. The meanings of any data passed here depends on the particular
514 * type of activity being created.
516 * @param string $modulename the type of activity to create. E.g. 'forum' or 'quiz'.
517 * @param array|stdClass $record data, as if from the module edit settings form.
518 * @param array $options additional data that may affect how the module is created.
519 * @return stdClass activity record new new record that was just inserted in the table
520 * like 'forum' or 'quiz', with a ->cmid field added.
522 public function create_module($modulename, $record=null, array $options=null) {
523 $generator = $this->get_plugin_generator('mod_'.$modulename);
524 return $generator->create_instance($record, $options);
528 * Create a test group for the specified course
530 * $record should be either an array or a stdClass containing infomation about the group to create.
531 * At the very least it needs to contain courseid.
532 * Default values are added for name, description, and descriptionformat if they are not present.
534 * This function calls groups_create_group() to create the group within the database.
535 * @see groups_create_group
536 * @param array|stdClass $record
537 * @return stdClass group record
539 public function create_group($record) {
540 global $DB, $CFG;
542 require_once($CFG->dirroot . '/group/lib.php');
544 $this->groupcount++;
545 $i = str_pad($this->groupcount, 4, '0', STR_PAD_LEFT);
547 $record = (array)$record;
549 if (empty($record['courseid'])) {
550 throw new coding_exception('courseid must be present in testing_data_generator::create_group() $record');
553 if (!isset($record['name'])) {
554 $record['name'] = 'group-' . $i;
557 if (!isset($record['description'])) {
558 $record['description'] = "Test Group $i\n{$this->loremipsum}";
561 if (!isset($record['descriptionformat'])) {
562 $record['descriptionformat'] = FORMAT_MOODLE;
565 if (!isset($record['visibility'])) {
566 $record['visibility'] = GROUPS_VISIBILITY_ALL;
569 if (!isset($record['participation'])) {
570 $record['participation'] = true;
573 $id = groups_create_group((object)$record);
575 // Allow tests to set group pictures.
576 if (!empty($record['picturepath'])) {
577 require_once($CFG->dirroot . '/lib/gdlib.php');
578 $grouppicture = process_new_icon(\context_course::instance($record['courseid']), 'group', 'icon', $id,
579 $record['picturepath']);
581 $DB->set_field('groups', 'picture', $grouppicture, ['id' => $id]);
583 // Invalidate the group data as we've updated the group record.
584 cache_helper::invalidate_by_definition('core', 'groupdata', array(), [$record['courseid']]);
587 return $DB->get_record('groups', array('id'=>$id));
591 * Create a test group member
592 * @param array|stdClass $record
593 * @throws coding_exception
594 * @return boolean
596 public function create_group_member($record) {
597 global $DB, $CFG;
599 require_once($CFG->dirroot . '/group/lib.php');
601 $record = (array)$record;
603 if (empty($record['userid'])) {
604 throw new coding_exception('user must be present in testing_util::create_group_member() $record');
607 if (!isset($record['groupid'])) {
608 throw new coding_exception('group must be present in testing_util::create_group_member() $record');
611 if (!isset($record['component'])) {
612 $record['component'] = null;
614 if (!isset($record['itemid'])) {
615 $record['itemid'] = 0;
618 return groups_add_member($record['groupid'], $record['userid'], $record['component'], $record['itemid']);
622 * Create a test grouping for the specified course
624 * $record should be either an array or a stdClass containing infomation about the grouping to create.
625 * At the very least it needs to contain courseid.
626 * Default values are added for name, description, and descriptionformat if they are not present.
628 * This function calls groups_create_grouping() to create the grouping within the database.
629 * @see groups_create_grouping
630 * @param array|stdClass $record
631 * @return stdClass grouping record
633 public function create_grouping($record) {
634 global $DB, $CFG;
636 require_once($CFG->dirroot . '/group/lib.php');
638 $this->groupingcount++;
639 $i = $this->groupingcount;
641 $record = (array)$record;
643 if (empty($record['courseid'])) {
644 throw new coding_exception('courseid must be present in testing_data_generator::create_grouping() $record');
647 if (!isset($record['name'])) {
648 $record['name'] = 'grouping-' . $i;
651 if (!isset($record['description'])) {
652 $record['description'] = "Test Grouping $i\n{$this->loremipsum}";
655 if (!isset($record['descriptionformat'])) {
656 $record['descriptionformat'] = FORMAT_MOODLE;
659 $id = groups_create_grouping((object)$record);
661 return $DB->get_record('groupings', array('id'=>$id));
665 * Create a test grouping group
666 * @param array|stdClass $record
667 * @throws coding_exception
668 * @return boolean
670 public function create_grouping_group($record) {
671 global $DB, $CFG;
673 require_once($CFG->dirroot . '/group/lib.php');
675 $record = (array)$record;
677 if (empty($record['groupingid'])) {
678 throw new coding_exception('grouping must be present in testing::create_grouping_group() $record');
681 if (!isset($record['groupid'])) {
682 throw new coding_exception('group must be present in testing_util::create_grouping_group() $record');
685 return groups_assign_grouping($record['groupingid'], $record['groupid']);
689 * Create an instance of a repository.
691 * @param string type of repository to create an instance for.
692 * @param array|stdClass $record data to use to up set the instance.
693 * @param array $options options
694 * @return stdClass repository instance record
695 * @since Moodle 2.5.1
697 public function create_repository($type, $record=null, array $options = null) {
698 $generator = $this->get_plugin_generator('repository_'.$type);
699 return $generator->create_instance($record, $options);
703 * Create an instance of a repository.
705 * @param string type of repository to create an instance for.
706 * @param array|stdClass $record data to use to up set the instance.
707 * @param array $options options
708 * @return repository_type object
709 * @since Moodle 2.5.1
711 public function create_repository_type($type, $record=null, array $options = null) {
712 $generator = $this->get_plugin_generator('repository_'.$type);
713 return $generator->create_type($record, $options);
718 * Create a test scale
719 * @param array|stdClass $record
720 * @param array $options
721 * @return stdClass block instance record
723 public function create_scale($record=null, array $options=null) {
724 global $DB;
726 $this->scalecount++;
727 $i = $this->scalecount;
729 $record = (array)$record;
731 if (!isset($record['name'])) {
732 $record['name'] = 'Test scale '.$i;
735 if (!isset($record['scale'])) {
736 $record['scale'] = 'A,B,C,D,F';
739 if (!isset($record['courseid'])) {
740 $record['courseid'] = 0;
743 if (!isset($record['userid'])) {
744 $record['userid'] = 0;
747 if (!isset($record['description'])) {
748 $record['description'] = 'Test scale description '.$i;
751 if (!isset($record['descriptionformat'])) {
752 $record['descriptionformat'] = FORMAT_MOODLE;
755 $record['timemodified'] = time();
757 if (isset($record['id'])) {
758 $DB->import_record('scale', $record);
759 $DB->get_manager()->reset_sequence('scale');
760 $id = $record['id'];
761 } else {
762 $id = $DB->insert_record('scale', $record);
765 return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST);
769 * Creates a new role in the system.
771 * You can fill $record with the role 'name',
772 * 'shortname', 'description' and 'archetype'.
774 * If an archetype is specified it's capabilities,
775 * context where the role can be assigned and
776 * all other properties are copied from the archetype;
777 * if no archetype is specified it will create an
778 * empty role.
780 * @param array|stdClass $record
781 * @return int The new role id
783 public function create_role($record=null) {
784 global $DB;
786 $this->rolecount++;
787 $i = $this->rolecount;
789 $record = (array)$record;
791 if (empty($record['shortname'])) {
792 $record['shortname'] = 'role-' . $i;
795 if (empty($record['name'])) {
796 $record['name'] = 'Test role ' . $i;
799 if (empty($record['description'])) {
800 $record['description'] = 'Test role ' . $i . ' description';
803 if (empty($record['archetype'])) {
804 $record['archetype'] = '';
805 } else {
806 $archetypes = get_role_archetypes();
807 if (empty($archetypes[$record['archetype']])) {
808 throw new coding_exception('\'role\' requires the field \'archetype\' to specify a ' .
809 'valid archetype shortname (editingteacher, student...)');
813 // Creates the role.
814 if (!$newroleid = create_role($record['name'], $record['shortname'], $record['description'], $record['archetype'])) {
815 throw new coding_exception('There was an error creating \'' . $record['shortname'] . '\' role');
818 // If no archetype was specified we allow it to be added to all contexts,
819 // otherwise we allow it in the archetype contexts.
820 if (!$record['archetype']) {
821 $contextlevels = [];
822 $usefallback = true;
823 foreach (context_helper::get_all_levels() as $level => $title) {
824 if (array_key_exists($title, $record)) {
825 $usefallback = false;
826 if (!empty($record[$title])) {
827 $contextlevels[] = $level;
832 if ($usefallback) {
833 $contextlevels = array_keys(context_helper::get_all_levels());
835 } else {
836 // Copying from the archetype default rol.
837 $archetyperoleid = $DB->get_field(
838 'role',
839 'id',
840 array('shortname' => $record['archetype'], 'archetype' => $record['archetype'])
842 $contextlevels = get_role_contextlevels($archetyperoleid);
844 set_role_contextlevels($newroleid, $contextlevels);
846 if ($record['archetype']) {
847 // We copy all the roles the archetype can assign, override, switch to and view.
848 if ($record['archetype']) {
849 $types = array('assign', 'override', 'switch', 'view');
850 foreach ($types as $type) {
851 $rolestocopy = get_default_role_archetype_allows($type, $record['archetype']);
852 foreach ($rolestocopy as $tocopy) {
853 $functionname = "core_role_set_{$type}_allowed";
854 $functionname($newroleid, $tocopy);
859 // Copying the archetype capabilities.
860 $sourcerole = $DB->get_record('role', array('id' => $archetyperoleid));
861 role_cap_duplicate($sourcerole, $newroleid);
864 $allcapabilities = get_all_capabilities();
865 $foundcapabilities = array_intersect(array_keys($allcapabilities), array_keys($record));
866 $systemcontext = \context_system::instance();
868 $allpermissions = [
869 'inherit' => CAP_INHERIT,
870 'allow' => CAP_ALLOW,
871 'prevent' => CAP_PREVENT,
872 'prohibit' => CAP_PROHIBIT,
875 foreach ($foundcapabilities as $capability) {
876 $permission = $record[$capability];
877 if (!array_key_exists($permission, $allpermissions)) {
878 throw new \coding_exception("Unknown capability permissions '{$permission}'");
880 assign_capability(
881 $capability,
882 $allpermissions[$permission],
883 $newroleid,
884 $systemcontext->id,
885 true
889 return $newroleid;
893 * Set role capabilities for the specified role.
895 * @param int $roleid The Role to set capabilities for
896 * @param array $rolecapabilities The list of capability =>permission to set for this role
897 * @param null|context $context The context to apply this capability to
899 public function create_role_capability(int $roleid, array $rolecapabilities, context $context = null): void {
900 // Map the capabilities into human-readable names.
901 $allpermissions = [
902 'inherit' => CAP_INHERIT,
903 'allow' => CAP_ALLOW,
904 'prevent' => CAP_PREVENT,
905 'prohibit' => CAP_PROHIBIT,
908 // Fetch all capabilities to check that they exist.
909 $allcapabilities = get_all_capabilities();
910 foreach ($rolecapabilities as $capability => $permission) {
911 if ($permission === '') {
912 // Allow items to be skipped.
913 continue;
916 if (!array_key_exists($capability, $allcapabilities)) {
917 throw new \coding_exception("Unknown capability '{$capability}'");
920 if (!array_key_exists($permission, $allpermissions)) {
921 throw new \coding_exception("Unknown capability permissions '{$permission}'");
924 assign_capability(
925 $capability,
926 $allpermissions[$permission],
927 $roleid,
928 $context->id,
929 true
935 * Create a tag.
937 * @param array|stdClass $record
938 * @return stdClass the tag record
940 public function create_tag($record = null) {
941 global $DB, $USER;
943 $this->tagcount++;
944 $i = $this->tagcount;
946 $record = (array) $record;
948 if (!isset($record['userid'])) {
949 $record['userid'] = $USER->id;
952 if (!isset($record['rawname'])) {
953 if (isset($record['name'])) {
954 $record['rawname'] = $record['name'];
955 } else {
956 $record['rawname'] = 'Tag name ' . $i;
960 // Attribute 'name' should be a lowercase version of 'rawname', if not set.
961 if (!isset($record['name'])) {
962 $record['name'] = core_text::strtolower($record['rawname']);
963 } else {
964 $record['name'] = core_text::strtolower($record['name']);
967 if (!isset($record['tagcollid'])) {
968 $record['tagcollid'] = core_tag_collection::get_default();
971 if (!isset($record['description'])) {
972 $record['description'] = 'Tag description';
975 if (!isset($record['descriptionformat'])) {
976 $record['descriptionformat'] = FORMAT_MOODLE;
979 if (!isset($record['flag'])) {
980 $record['flag'] = 0;
983 if (!isset($record['timemodified'])) {
984 $record['timemodified'] = time();
987 $id = $DB->insert_record('tag', $record);
989 return $DB->get_record('tag', array('id' => $id), '*', MUST_EXIST);
993 * Helper method which combines $defaults with the values specified in $record.
994 * If $record is an object, it is converted to an array.
995 * Then, for each key that is in $defaults, but not in $record, the value
996 * from $defaults is copied.
997 * @param array $defaults the default value for each field with
998 * @param array|stdClass $record
999 * @return array updated $record.
1001 public function combine_defaults_and_record(array $defaults, $record) {
1002 $record = (array) $record;
1004 foreach ($defaults as $key => $defaults) {
1005 if (!array_key_exists($key, $record)) {
1006 $record[$key] = $defaults;
1009 return $record;
1013 * Simplified enrolment of user to course using default options.
1015 * It is strongly recommended to use only this method for 'manual' and 'self' plugins only!!!
1017 * @param int $userid
1018 * @param int $courseid
1019 * @param int|string $roleidorshortname optional role id or role shortname, use only with manual plugin
1020 * @param string $enrol name of enrol plugin,
1021 * there must be exactly one instance in course,
1022 * it must support enrol_user() method.
1023 * @param int $timestart (optional) 0 means unknown
1024 * @param int $timeend (optional) 0 means forever
1025 * @param int $status (optional) default to ENROL_USER_ACTIVE for new enrolments
1026 * @return bool success
1028 public function enrol_user($userid, $courseid, $roleidorshortname = null, $enrol = 'manual',
1029 $timestart = 0, $timeend = 0, $status = null) {
1030 global $DB;
1032 // If role is specified by shortname, convert it into an id.
1033 if (!is_numeric($roleidorshortname) && is_string($roleidorshortname)) {
1034 $roleid = $DB->get_field('role', 'id', array('shortname' => $roleidorshortname), MUST_EXIST);
1035 } else {
1036 $roleid = $roleidorshortname;
1039 if (!$plugin = enrol_get_plugin($enrol)) {
1040 return false;
1043 $instances = $DB->get_records('enrol', array('courseid'=>$courseid, 'enrol'=>$enrol));
1044 if (count($instances) != 1) {
1045 return false;
1047 $instance = reset($instances);
1049 if (is_null($roleid) and $instance->roleid) {
1050 $roleid = $instance->roleid;
1053 $plugin->enrol_user($instance, $userid, $roleid, $timestart, $timeend, $status);
1054 return true;
1058 * Assigns the specified role to a user in the context.
1060 * @param int|string $role either an int role id or a string role shortname.
1061 * @param int $userid
1062 * @param int|context $contextid Defaults to the system context
1063 * @return int new/existing id of the assignment
1065 public function role_assign($role, $userid, $contextid = false) {
1066 global $DB;
1068 // Default to the system context.
1069 if (!$contextid) {
1070 $context = context_system::instance();
1071 $contextid = $context->id;
1074 if (empty($role)) {
1075 throw new coding_exception('roleid must be present in testing_data_generator::role_assign() arguments');
1077 if (!is_number($role)) {
1078 $role = $DB->get_field('role', 'id', ['shortname' => $role], MUST_EXIST);
1081 if (empty($userid)) {
1082 throw new coding_exception('userid must be present in testing_data_generator::role_assign() arguments');
1085 return role_assign($role, $userid, $contextid);
1089 * Create a grade_category.
1091 * @param array|stdClass $record
1092 * @return stdClass the grade category record
1094 public function create_grade_category($record = null) {
1095 global $CFG;
1097 $this->gradecategorycounter++;
1099 $record = (array)$record;
1101 if (empty($record['courseid'])) {
1102 throw new coding_exception('courseid must be present in testing::create_grade_category() $record');
1105 if (!isset($record['fullname'])) {
1106 $record['fullname'] = 'Grade category ' . $this->gradecategorycounter;
1109 // For gradelib classes.
1110 require_once($CFG->libdir . '/gradelib.php');
1111 // Create new grading category in this course.
1112 $gradecategory = new grade_category(array('courseid' => $record['courseid']), false);
1113 $gradecategory->apply_default_settings();
1114 grade_category::set_properties($gradecategory, $record);
1115 $gradecategory->apply_forced_settings();
1116 $gradecategory->insert();
1118 // This creates a default grade item for the category
1119 $gradeitem = $gradecategory->load_grade_item();
1121 $gradecategory->update_from_db();
1122 return $gradecategory->get_record_data();
1126 * Create a grade_grade.
1128 * @param array $record
1129 * @return grade_grade the grade record
1131 public function create_grade_grade(?array $record = null): grade_grade {
1132 global $DB, $USER;
1134 $item = $DB->get_record('grade_items', ['id' => $record['itemid']]);
1135 $userid = $record['userid'] ?? $USER->id;
1137 unset($record['itemid']);
1138 unset($record['userid']);
1140 if ($item->itemtype === 'mod') {
1141 $cm = get_coursemodule_from_instance($item->itemmodule, $item->iteminstance);
1142 $module = new $item->itemmodule(context_module::instance($cm->id), $cm, false);
1143 $record['attemptnumber'] = $record['attemptnumber'] ?? 0;
1145 $module->save_grade($userid, (object) $record);
1147 $grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
1148 } else {
1149 $grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
1150 $record['rawgrade'] = $record['rawgrade'] ?? $record['grade'] ?? null;
1151 $record['finalgrade'] = $record['finalgrade'] ?? $record['grade'] ?? null;
1153 unset($record['grade']);
1155 if ($grade) {
1156 $fields = $grade->required_fields + array_keys($grade->optional_fields);
1158 foreach ($fields as $field) {
1159 $grade->{$field} = $record[$field] ?? $grade->{$field};
1162 $grade->update();
1163 } else {
1164 $record['userid'] = $userid;
1165 $record['itemid'] = $item->id;
1167 $grade = new grade_grade($record, false);
1169 $grade->insert();
1173 return $grade;
1177 * Create a grade_item.
1179 * @param array|stdClass $record
1180 * @return stdClass the grade item record
1182 public function create_grade_item($record = null) {
1183 global $CFG;
1184 require_once("$CFG->libdir/gradelib.php");
1186 $this->gradeitemcounter++;
1188 if (!isset($record['itemtype'])) {
1189 $record['itemtype'] = 'manual';
1192 if (!isset($record['itemname'])) {
1193 $record['itemname'] = 'Grade item ' . $this->gradeitemcounter;
1196 if (isset($record['outcomeid'])) {
1197 $outcome = new grade_outcome(array('id' => $record['outcomeid']));
1198 $record['scaleid'] = $outcome->scaleid;
1200 if (isset($record['scaleid'])) {
1201 $record['gradetype'] = GRADE_TYPE_SCALE;
1202 } else if (!isset($record['gradetype'])) {
1203 $record['gradetype'] = GRADE_TYPE_VALUE;
1206 // Create new grade item in this course.
1207 $gradeitem = new grade_item($record, false);
1208 $gradeitem->insert();
1210 $gradeitem->update_from_db();
1211 return $gradeitem->get_record_data();
1215 * Create a grade_outcome.
1217 * @param array|stdClass $record
1218 * @return stdClass the grade outcome record
1220 public function create_grade_outcome($record = null) {
1221 global $CFG;
1223 $this->gradeoutcomecounter++;
1224 $i = $this->gradeoutcomecounter;
1226 if (!isset($record['fullname'])) {
1227 $record['fullname'] = 'Grade outcome ' . $i;
1230 // For gradelib classes.
1231 require_once($CFG->libdir . '/gradelib.php');
1232 // Create new grading outcome in this course.
1233 $gradeoutcome = new grade_outcome($record, false);
1234 $gradeoutcome->insert();
1236 $gradeoutcome->update_from_db();
1237 return $gradeoutcome->get_record_data();
1241 * Helper function used to create an LTI tool.
1243 * @param stdClass $data
1244 * @return stdClass the tool
1246 public function create_lti_tool($data = array()) {
1247 global $DB;
1249 $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1250 $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
1252 // Create a course if no course id was specified.
1253 if (empty($data->courseid)) {
1254 $course = $this->create_course();
1255 $data->courseid = $course->id;
1256 } else {
1257 $course = get_course($data->courseid);
1260 if (!empty($data->cmid)) {
1261 $data->contextid = context_module::instance($data->cmid)->id;
1262 } else {
1263 $data->contextid = context_course::instance($data->courseid)->id;
1266 // Set it to enabled if no status was specified.
1267 if (!isset($data->status)) {
1268 $data->status = ENROL_INSTANCE_ENABLED;
1271 // Default to legacy lti version.
1272 if (empty($data->ltiversion) || !in_array($data->ltiversion, ['LTI-1p0/LTI-2p0', 'LTI-1p3'])) {
1273 $data->ltiversion = 'LTI-1p0/LTI-2p0';
1276 // Add some extra necessary fields to the data.
1277 $data->name = $data->name ?? 'Test LTI';
1278 $data->roleinstructor = $teacherrole->id;
1279 $data->rolelearner = $studentrole->id;
1281 // Get the enrol LTI plugin.
1282 $enrolplugin = enrol_get_plugin('lti');
1283 $instanceid = $enrolplugin->add_instance($course, (array) $data);
1285 // Get the tool associated with this instance.
1286 return $DB->get_record('enrol_lti_tools', array('enrolid' => $instanceid));
1290 * Helper function used to create an event.
1292 * @param array $data
1293 * @return stdClass
1295 public function create_event($data = []) {
1296 global $CFG;
1298 require_once($CFG->dirroot . '/calendar/lib.php');
1299 $record = new \stdClass();
1300 $record->name = 'event name';
1301 $record->repeat = 0;
1302 $record->repeats = 0;
1303 $record->timestart = time();
1304 $record->timeduration = 0;
1305 $record->timesort = 0;
1306 $record->eventtype = 'user';
1307 $record->courseid = 0;
1308 $record->categoryid = 0;
1310 foreach ($data as $key => $value) {
1311 $record->$key = $value;
1314 switch ($record->eventtype) {
1315 case 'user':
1316 unset($record->categoryid);
1317 unset($record->courseid);
1318 unset($record->groupid);
1319 break;
1320 case 'group':
1321 unset($record->categoryid);
1322 break;
1323 case 'course':
1324 unset($record->categoryid);
1325 unset($record->groupid);
1326 break;
1327 case 'category':
1328 unset($record->courseid);
1329 unset($record->groupid);
1330 break;
1331 case 'site':
1332 unset($record->categoryid);
1333 unset($record->courseid);
1334 unset($record->groupid);
1335 break;
1338 $event = new calendar_event($record);
1339 $event->create($record);
1341 return $event->properties();
1345 * Create a new course custom field category with the given name.
1347 * @param array $data Array with data['name'] of category
1348 * @return \core_customfield\category_controller The created category
1350 public function create_custom_field_category($data): \core_customfield\category_controller {
1351 return $this->get_plugin_generator('core_customfield')->create_category($data);
1355 * Create a new custom field
1357 * @param array $data Array with 'name', 'shortname' and 'type' of the field
1358 * @return \core_customfield\field_controller The created field
1360 public function create_custom_field($data): \core_customfield\field_controller {
1361 global $DB;
1362 if (empty($data['categoryid']) && !empty($data['category'])) {
1363 $data['categoryid'] = $DB->get_field('customfield_category', 'id', ['name' => $data['category']]);
1364 unset($data['category']);
1366 return $this->get_plugin_generator('core_customfield')->create_field($data);
1370 * Create a new category for custom profile fields.
1372 * @param array $data Array with 'name' and optionally 'sortorder'
1373 * @return \stdClass New category object
1375 public function create_custom_profile_field_category(array $data): \stdClass {
1376 global $DB;
1378 // Pick next sortorder if not defined.
1379 if (!array_key_exists('sortorder', $data)) {
1380 $data['sortorder'] = (int)$DB->get_field_sql('SELECT MAX(sortorder) FROM {user_info_category}') + 1;
1383 $category = (object)[
1384 'name' => $data['name'],
1385 'sortorder' => $data['sortorder']
1387 $category->id = $DB->insert_record('user_info_category', $category);
1389 return $category;
1393 * Creates a new custom profile field.
1395 * Optional fields are:
1397 * categoryid (or use 'category' to specify by name). If you don't specify
1398 * either, it will add the field to a 'Testing' category, which will be created for you if
1399 * necessary.
1401 * sortorder (if you don't specify this, it will pick the next one in the category).
1403 * all the other database fields (if you don't specify this, it will pick sensible defaults
1404 * based on the data type).
1406 * @param array $data Array with 'datatype', 'shortname', and 'name'
1407 * @return \stdClass Database object from the user_info_field table
1409 public function create_custom_profile_field(array $data): \stdClass {
1410 global $DB, $CFG;
1411 require_once($CFG->dirroot . '/user/profile/lib.php');
1413 // Set up category if necessary.
1414 if (!array_key_exists('categoryid', $data)) {
1415 if (array_key_exists('category', $data)) {
1416 $data['categoryid'] = $DB->get_field('user_info_category', 'id',
1417 ['name' => $data['category']], MUST_EXIST);
1418 } else {
1419 // Make up a 'Testing' category or use existing.
1420 $data['categoryid'] = $DB->get_field('user_info_category', 'id', ['name' => 'Testing']);
1421 if (!$data['categoryid']) {
1422 $created = $this->create_custom_profile_field_category(['name' => 'Testing']);
1423 $data['categoryid'] = $created->id;
1428 // Pick sort order if necessary.
1429 if (!array_key_exists('sortorder', $data)) {
1430 $data['sortorder'] = (int)$DB->get_field_sql(
1431 'SELECT MAX(sortorder) FROM {user_info_field} WHERE categoryid = ?',
1432 [$data['categoryid']]) + 1;
1435 if ($data['datatype'] === 'menu' && isset($data['param1'])) {
1436 // Convert new lines to the proper character.
1437 $data['param1'] = str_replace('\n', "\n", $data['param1']);
1440 // Defaults for other values.
1441 $defaults = [
1442 'description' => '',
1443 'descriptionformat' => 0,
1444 'required' => 0,
1445 'locked' => 0,
1446 'visible' => PROFILE_VISIBLE_ALL,
1447 'forceunique' => 0,
1448 'signup' => 0,
1449 'defaultdata' => '',
1450 'defaultdataformat' => 0,
1451 'param1' => '',
1452 'param2' => '',
1453 'param3' => '',
1454 'param4' => '',
1455 'param5' => ''
1458 // Type-specific defaults for other values.
1459 $typedefaults = [
1460 'text' => [
1461 'param1' => 30,
1462 'param2' => 2048
1464 'menu' => [
1465 'param1' => "Yes\nNo",
1466 'defaultdata' => 'No'
1468 'datetime' => [
1469 'param1' => '2010',
1470 'param2' => '2015',
1471 'param3' => 1
1473 'checkbox' => [
1474 'defaultdata' => 0
1477 foreach ($typedefaults[$data['datatype']] ?? [] as $field => $value) {
1478 $defaults[$field] = $value;
1481 foreach ($defaults as $field => $value) {
1482 if (!array_key_exists($field, $data)) {
1483 $data[$field] = $value;
1487 $data['id'] = $DB->insert_record('user_info_field', $data);
1488 return (object)$data;
1492 * Create a new user, and enrol them in the specified course as the supplied role.
1494 * @param \stdClass $course The course to enrol in
1495 * @param string $role The role to give within the course
1496 * @param \stdClass|array $userparams User parameters
1497 * @return \stdClass The created user
1499 public function create_and_enrol($course, $role = 'student', $userparams = null, $enrol = 'manual',
1500 $timestart = 0, $timeend = 0, $status = null) {
1501 global $DB;
1503 $user = $this->create_user($userparams);
1504 $roleid = $DB->get_field('role', 'id', ['shortname' => $role ]);
1506 $this->enrol_user($user->id, $course->id, $roleid, $enrol, $timestart, $timeend, $status);
1508 return $user;
1512 * Create a new last access record for a given user in a course.
1514 * @param \stdClass $user The user
1515 * @param \stdClass $course The course the user accessed
1516 * @param int $timestamp The timestamp for when the user last accessed the course
1517 * @return \stdClass The user_lastaccess record
1519 public function create_user_course_lastaccess(\stdClass $user, \stdClass $course, int $timestamp): \stdClass {
1520 global $DB;
1522 $record = [
1523 'userid' => $user->id,
1524 'courseid' => $course->id,
1525 'timeaccess' => $timestamp,
1528 $recordid = $DB->insert_record('user_lastaccess', $record);
1530 return $DB->get_record('user_lastaccess', ['id' => $recordid], '*', MUST_EXIST);
1534 * Gets a default generator for a given component.
1536 * @param string $component The component name, e.g. 'mod_forum' or 'core_question'.
1537 * @param string $classname The name of the class missing from the generators file.
1538 * @return component_generator_base The generator.
1540 protected function get_default_plugin_generator(string $component, ?string $classname = null) {
1541 [$type, $plugin] = core_component::normalize_component($component);
1543 switch ($type) {
1544 case 'block':
1545 return new default_block_generator($this, $plugin);
1548 if (is_null($classname)) {
1549 throw new coding_exception("Component {$component} does not support " .
1550 "generators yet. Missing tests/generator/lib.php.");
1553 throw new coding_exception("Component {$component} does not support " .
1554 "data generators yet. Class {$classname} not found.");