MDL-70412 groups: Improving create_group data generator
[moodle.git] / lib / testing / generator / data_generator.php
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/>.
17 /**
18  * Data generator.
19  *
20  * @package    core
21  * @category   test
22  * @copyright  2012 Petr Skoda {@link http://skodak.org}
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 defined('MOODLE_INTERNAL') || die();
28 /**
29  * Data generator class for unit tests and other tools that need to create fake test sites.
30  *
31  * @package    core
32  * @category   test
33  * @copyright  2012 Petr Skoda {@link http://skodak.org}
34  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
35  */
36 class testing_data_generator {
37     /** @var int The number of grade categories created */
38     protected $gradecategorycounter = 0;
39     /** @var int The number of grade items created */
40     protected $gradeitemcounter = 0;
41     /** @var int The number of grade outcomes created */
42     protected $gradeoutcomecounter = 0;
43     protected $usercounter = 0;
44     protected $categorycount = 0;
45     protected $cohortcount = 0;
46     protected $coursecount = 0;
47     protected $scalecount = 0;
48     protected $groupcount = 0;
49     protected $groupingcount = 0;
50     protected $rolecount = 0;
51     protected $tagcount = 0;
53     /** @var array list of plugin generators */
54     protected $generators = array();
56     /** @var array lis of common last names */
57     public $lastnames = array(
58         'Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Miller', 'Davis', 'García', 'Rodríguez', 'Wilson',
59         'Müller', 'Schmidt', 'Schneider', 'Fischer', 'Meyer', 'Weber', 'Schulz', 'Wagner', 'Becker', 'Hoffmann',
60         'Novák', 'Svoboda', 'Novotný', 'Dvořák', 'Černý', 'Procházková', 'Kučerová', 'Veselá', 'Horáková', 'Němcová',
61         'Смирнов', 'Иванов', 'Кузнецов', 'Соколов', 'Попов', 'Лебедева', 'Козлова', 'Новикова', 'Морозова', 'Петрова',
62         '王', '李', '张', '刘', '陈', '楊', '黃', '趙', '吳', '周',
63         '佐藤', '鈴木', '高橋', '田中', '渡辺', '伊藤', '山本', '中村', '小林', '斎藤',
64     );
66     /** @var array lis of common first names */
67     public $firstnames = array(
68         'Jacob', 'Ethan', 'Michael', 'Jayden', 'William', 'Isabella', 'Sophia', 'Emma', 'Olivia', 'Ava',
69         'Lukas', 'Leon', 'Luca', 'Timm', 'Paul', 'Leonie', 'Leah', 'Lena', 'Hanna', 'Laura',
70         'Jakub', 'Jan', 'Tomáš', 'Lukáš', 'Matěj', 'Tereza', 'Eliška', 'Anna', 'Adéla', 'Karolína',
71         'Даниил', 'Максим', 'Артем', 'Иван', 'Александр', 'София', 'Анастасия', 'Дарья', 'Мария', 'Полина',
72         '伟', '伟', '芳', '伟', '秀英', '秀英', '娜', '秀英', '伟', '敏',
73         '翔', '大翔', '拓海', '翔太', '颯太', '陽菜', 'さくら', '美咲', '葵', '美羽',
74     );
76     public $loremipsum = <<<EOD
77 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.
78 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.
79 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.
80 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.
81 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.
82 EOD;
84     /**
85      * To be called from data reset code only,
86      * do not use in tests.
87      * @return void
88      */
89     public function reset() {
90         $this->usercounter = 0;
91         $this->categorycount = 0;
92         $this->coursecount = 0;
93         $this->scalecount = 0;
95         foreach ($this->generators as $generator) {
96             $generator->reset();
97         }
98     }
100     /**
101      * Return generator for given plugin or component.
102      * @param string $component the component name, e.g. 'mod_forum' or 'core_question'.
103      * @return component_generator_base or rather an instance of the appropriate subclass.
104      */
105     public function get_plugin_generator($component) {
106         list($type, $plugin) = core_component::normalize_component($component);
107         $cleancomponent = $type . '_' . $plugin;
108         if ($cleancomponent != $component) {
109             debugging("Please specify the component you want a generator for as " .
110                     "{$cleancomponent}, not {$component}.", DEBUG_DEVELOPER);
111             $component = $cleancomponent;
112         }
114         if (isset($this->generators[$component])) {
115             return $this->generators[$component];
116         }
118         $dir = core_component::get_component_directory($component);
119         $lib = $dir . '/tests/generator/lib.php';
120         if (!$dir || !is_readable($lib)) {
121             throw new coding_exception("Component {$component} does not support " .
122                     "generators yet. Missing tests/generator/lib.php.");
123         }
125         include_once($lib);
126         $classname = $component . '_generator';
128         if (!class_exists($classname)) {
129             throw new coding_exception("Component {$component} does not support " .
130                     "data generators yet. Class {$classname} not found.");
131         }
133         $this->generators[$component] = new $classname($this);
134         return $this->generators[$component];
135     }
137     /**
138      * Create a test user
139      * @param array|stdClass $record
140      * @param array $options
141      * @return stdClass user record
142      */
143     public function create_user($record=null, array $options=null) {
144         global $DB, $CFG;
145         require_once($CFG->dirroot.'/user/lib.php');
147         $this->usercounter++;
148         $i = $this->usercounter;
150         $record = (array)$record;
152         if (!isset($record['auth'])) {
153             $record['auth'] = 'manual';
154         }
156         if (!isset($record['firstname']) and !isset($record['lastname'])) {
157             $country = rand(0, 5);
158             $firstname = rand(0, 4);
159             $lastname = rand(0, 4);
160             $female = rand(0, 1);
161             $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)];
162             $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)];
164         } else if (!isset($record['firstname'])) {
165             $record['firstname'] = 'Firstname'.$i;
167         } else if (!isset($record['lastname'])) {
168             $record['lastname'] = 'Lastname'.$i;
169         }
171         if (!isset($record['firstnamephonetic'])) {
172             $firstnamephonetic = rand(0, 59);
173             $record['firstnamephonetic'] = $this->firstnames[$firstnamephonetic];
174         }
176         if (!isset($record['lastnamephonetic'])) {
177             $lastnamephonetic = rand(0, 59);
178             $record['lastnamephonetic'] = $this->lastnames[$lastnamephonetic];
179         }
181         if (!isset($record['middlename'])) {
182             $middlename = rand(0, 59);
183             $record['middlename'] = $this->firstnames[$middlename];
184         }
186         if (!isset($record['alternatename'])) {
187             $alternatename = rand(0, 59);
188             $record['alternatename'] = $this->firstnames[$alternatename];
189         }
191         if (!isset($record['idnumber'])) {
192             $record['idnumber'] = '';
193         }
195         if (!isset($record['mnethostid'])) {
196             $record['mnethostid'] = $CFG->mnet_localhost_id;
197         }
199         if (!isset($record['username'])) {
200             $record['username'] = 'username'.$i;
201             $j = 2;
202             while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) {
203                 $record['username'] = 'username'.$i.'_'.$j;
204                 $j++;
205             }
206         }
208         if (isset($record['password'])) {
209             $record['password'] = hash_internal_user_password($record['password']);
210         }
212         if (!isset($record['email'])) {
213             $record['email'] = $record['username'].'@example.com';
214         }
216         if (!isset($record['confirmed'])) {
217             $record['confirmed'] = 1;
218         }
220         if (!isset($record['lastip'])) {
221             $record['lastip'] = '0.0.0.0';
222         }
224         $tobedeleted = !empty($record['deleted']);
225         unset($record['deleted']);
227         $userid = user_create_user($record, false, false);
229         if ($extrafields = array_intersect_key($record, ['password' => 1, 'timecreated' => 1])) {
230             $DB->update_record('user', ['id' => $userid] + $extrafields);
231         }
233         if (!$tobedeleted) {
234             // All new not deleted users must have a favourite self-conversation.
235             $selfconversation = \core_message\api::create_conversation(
236                 \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
237                 [$userid]
238             );
239             \core_message\api::set_favourite_conversation($selfconversation->id, $userid);
241             // Save custom profile fields data.
242             $hasprofilefields = array_filter($record, function($key){
243                 return strpos($key, 'profile_field_') === 0;
244             }, ARRAY_FILTER_USE_KEY);
245             if ($hasprofilefields) {
246                 require_once($CFG->dirroot.'/user/profile/lib.php');
247                 $usernew = (object)(['id' => $userid] + $record);
248                 profile_save_data($usernew);
249             }
250         }
252         $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
254         if (!$tobedeleted && isset($record['interests'])) {
255             require_once($CFG->dirroot . '/user/editlib.php');
256             if (!is_array($record['interests'])) {
257                 $record['interests'] = preg_split('/\s*,\s*/', trim($record['interests']), -1, PREG_SPLIT_NO_EMPTY);
258             }
259             useredit_update_interests($user, $record['interests']);
260         }
262         \core\event\user_created::create_from_userid($userid)->trigger();
264         if ($tobedeleted) {
265             delete_user($user);
266             $user = $DB->get_record('user', array('id' => $userid));
267         }
268         return $user;
269     }
271     /**
272      * Create a test course category
273      * @param array|stdClass $record
274      * @param array $options
275      * @return core_course_category course category record
276      */
277     public function create_category($record=null, array $options=null) {
278         $this->categorycount++;
279         $i = $this->categorycount;
281         $record = (array)$record;
283         if (!isset($record['name'])) {
284             $record['name'] = 'Course category '.$i;
285         }
287         if (!isset($record['description'])) {
288             $record['description'] = "Test course category $i\n$this->loremipsum";
289         }
291         if (!isset($record['idnumber'])) {
292             $record['idnumber'] = '';
293         }
295         return core_course_category::create($record);
296     }
298     /**
299      * Create test cohort.
300      * @param array|stdClass $record
301      * @param array $options
302      * @return stdClass cohort record
303      */
304     public function create_cohort($record=null, array $options=null) {
305         global $DB, $CFG;
306         require_once("$CFG->dirroot/cohort/lib.php");
308         $this->cohortcount++;
309         $i = $this->cohortcount;
311         $record = (array)$record;
313         if (!isset($record['contextid'])) {
314             $record['contextid'] = context_system::instance()->id;
315         }
317         if (!isset($record['name'])) {
318             $record['name'] = 'Cohort '.$i;
319         }
321         if (!isset($record['idnumber'])) {
322             $record['idnumber'] = '';
323         }
325         if (!isset($record['description'])) {
326             $record['description'] = "Description for '{$record['name']}' \n$this->loremipsum";
327         }
329         if (!isset($record['descriptionformat'])) {
330             $record['descriptionformat'] = FORMAT_MOODLE;
331         }
333         if (!isset($record['visible'])) {
334             $record['visible'] = 1;
335         }
337         if (!isset($record['component'])) {
338             $record['component'] = '';
339         }
341         $id = cohort_add_cohort((object)$record);
343         return $DB->get_record('cohort', array('id'=>$id), '*', MUST_EXIST);
344     }
346     /**
347      * Create a test course
348      * @param array|stdClass $record
349      * @param array $options with keys:
350      *      'createsections'=>bool precreate all sections
351      * @return stdClass course record
352      */
353     public function create_course($record=null, array $options=null) {
354         global $DB, $CFG;
355         require_once("$CFG->dirroot/course/lib.php");
357         $this->coursecount++;
358         $i = $this->coursecount;
360         $record = (array)$record;
362         if (!isset($record['fullname'])) {
363             $record['fullname'] = 'Test course '.$i;
364         }
366         if (!isset($record['shortname'])) {
367             $record['shortname'] = 'tc_'.$i;
368         }
370         if (!isset($record['idnumber'])) {
371             $record['idnumber'] = '';
372         }
374         if (!isset($record['format'])) {
375             $record['format'] = 'topics';
376         }
378         if (!isset($record['newsitems'])) {
379             $record['newsitems'] = 0;
380         }
382         if (!isset($record['numsections'])) {
383             $record['numsections'] = 5;
384         }
386         if (!isset($record['summary'])) {
387             $record['summary'] = "Test course $i\n$this->loremipsum";
388         }
390         if (!isset($record['summaryformat'])) {
391             $record['summaryformat'] = FORMAT_MOODLE;
392         }
394         if (!isset($record['category'])) {
395             $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
396         }
398         if (!isset($record['startdate'])) {
399             $record['startdate'] = usergetmidnight(time());
400         }
402         if (isset($record['tags']) && !is_array($record['tags'])) {
403             $record['tags'] = preg_split('/\s*,\s*/', trim($record['tags']), -1, PREG_SPLIT_NO_EMPTY);
404         }
406         if (!empty($options['createsections']) && empty($record['numsections'])) {
407             // Since Moodle 3.3 function create_course() automatically creates sections if numsections is specified.
408             // For BC if 'createsections' is given but 'numsections' is not, assume the default value from config.
409             $record['numsections'] = get_config('moodlecourse', 'numsections');
410         }
412         if (!empty($record['customfields'])) {
413             foreach ($record['customfields'] as $field) {
414                 $record['customfield_'.$field['shortname']] = $field['value'];
415             }
416         }
418         $course = create_course((object)$record);
419         context_course::instance($course->id);
421         return $course;
422     }
424     /**
425      * Create course section if does not exist yet
426      * @param array|stdClass $record must contain 'course' and 'section' attributes
427      * @param array|null $options
428      * @return stdClass
429      * @throws coding_exception
430      */
431     public function create_course_section($record = null, array $options = null) {
432         global $DB;
434         $record = (array)$record;
436         if (empty($record['course'])) {
437             throw new coding_exception('course must be present in testing_data_generator::create_course_section() $record');
438         }
440         if (!isset($record['section'])) {
441             throw new coding_exception('section must be present in testing_data_generator::create_course_section() $record');
442         }
444         course_create_sections_if_missing($record['course'], $record['section']);
445         return get_fast_modinfo($record['course'])->get_section_info($record['section']);
446     }
448     /**
449      * Create a test block.
450      *
451      * The $record passed in becomes the basis for the new row added to the
452      * block_instances table. You only need to supply the values of interest.
453      * Any missing values have sensible defaults filled in, and ->blockname will be set based on $blockname.
454      *
455      * The $options array provides additional data, not directly related to what
456      * will be inserted in the block_instance table, which may affect the block
457      * that is created. The meanings of any data passed here depends on the particular
458      * type of block being created.
459      *
460      * @param string $blockname the type of block to create. E.g. 'html'.
461      * @param array|stdClass $record forms the basis for the entry to be inserted in the block_instances table.
462      * @param array $options further, block-specific options to control how the block is created.
463      * @return stdClass new block_instance record.
464      */
465     public function create_block($blockname, $record=null, array $options=array()) {
466         $generator = $this->get_plugin_generator('block_'.$blockname);
467         return $generator->create_instance($record, $options);
468     }
470     /**
471      * Create a test activity module.
472      *
473      * The $record should contain the same data that you would call from
474      * ->get_data() when the mod_[type]_mod_form is submitted, except that you
475      * only need to supply values of interest. The only required value is
476      * 'course'. Any missing values will have a sensible default supplied.
477      *
478      * The $options array provides additional data, not directly related to what
479      * would come back from the module edit settings form, which may affect the activity
480      * that is created. The meanings of any data passed here depends on the particular
481      * type of activity being created.
482      *
483      * @param string $modulename the type of activity to create. E.g. 'forum' or 'quiz'.
484      * @param array|stdClass $record data, as if from the module edit settings form.
485      * @param array $options additional data that may affect how the module is created.
486      * @return stdClass activity record new new record that was just inserted in the table
487      *      like 'forum' or 'quiz', with a ->cmid field added.
488      */
489     public function create_module($modulename, $record=null, array $options=null) {
490         $generator = $this->get_plugin_generator('mod_'.$modulename);
491         return $generator->create_instance($record, $options);
492     }
494     /**
495      * Create a test group for the specified course
496      *
497      * $record should be either an array or a stdClass containing infomation about the group to create.
498      * At the very least it needs to contain courseid.
499      * Default values are added for name, description, and descriptionformat if they are not present.
500      *
501      * This function calls groups_create_group() to create the group within the database.
502      * @see groups_create_group
503      * @param array|stdClass $record
504      * @return stdClass group record
505      */
506     public function create_group($record) {
507         global $DB, $CFG;
509         require_once($CFG->dirroot . '/group/lib.php');
511         $this->groupcount++;
512         $i = str_pad($this->groupcount, 4, '0', STR_PAD_LEFT);
514         $record = (array)$record;
516         if (empty($record['courseid'])) {
517             throw new coding_exception('courseid must be present in testing_data_generator::create_group() $record');
518         }
520         if (!isset($record['name'])) {
521             $record['name'] = 'group-' . $i;
522         }
524         if (!isset($record['description'])) {
525             $record['description'] = "Test Group $i\n{$this->loremipsum}";
526         }
528         if (!isset($record['descriptionformat'])) {
529             $record['descriptionformat'] = FORMAT_MOODLE;
530         }
532         $id = groups_create_group((object)$record);
534         // Allow tests to set group pictures.
535         if (!empty($record['picturepath'])) {
536             require_once($CFG->dirroot . '/lib/gdlib.php');
537             $grouppicture = process_new_icon(\context_course::instance($record['courseid']), 'group', 'icon', $id,
538                 $record['picturepath']);
540             $DB->set_field('groups', 'picture', $grouppicture, ['id' => $id]);
542             // Invalidate the group data as we've updated the group record.
543             cache_helper::invalidate_by_definition('core', 'groupdata', array(), [$record['courseid']]);
544         }
546         return $DB->get_record('groups', array('id'=>$id));
547     }
549     /**
550      * Create a test group member
551      * @param array|stdClass $record
552      * @throws coding_exception
553      * @return boolean
554      */
555     public function create_group_member($record) {
556         global $DB, $CFG;
558         require_once($CFG->dirroot . '/group/lib.php');
560         $record = (array)$record;
562         if (empty($record['userid'])) {
563             throw new coding_exception('user must be present in testing_util::create_group_member() $record');
564         }
566         if (!isset($record['groupid'])) {
567             throw new coding_exception('group must be present in testing_util::create_group_member() $record');
568         }
570         if (!isset($record['component'])) {
571             $record['component'] = null;
572         }
573         if (!isset($record['itemid'])) {
574             $record['itemid'] = 0;
575         }
577         return groups_add_member($record['groupid'], $record['userid'], $record['component'], $record['itemid']);
578     }
580     /**
581      * Create a test grouping for the specified course
582      *
583      * $record should be either an array or a stdClass containing infomation about the grouping to create.
584      * At the very least it needs to contain courseid.
585      * Default values are added for name, description, and descriptionformat if they are not present.
586      *
587      * This function calls groups_create_grouping() to create the grouping within the database.
588      * @see groups_create_grouping
589      * @param array|stdClass $record
590      * @return stdClass grouping record
591      */
592     public function create_grouping($record) {
593         global $DB, $CFG;
595         require_once($CFG->dirroot . '/group/lib.php');
597         $this->groupingcount++;
598         $i = $this->groupingcount;
600         $record = (array)$record;
602         if (empty($record['courseid'])) {
603             throw new coding_exception('courseid must be present in testing_data_generator::create_grouping() $record');
604         }
606         if (!isset($record['name'])) {
607             $record['name'] = 'grouping-' . $i;
608         }
610         if (!isset($record['description'])) {
611             $record['description'] = "Test Grouping $i\n{$this->loremipsum}";
612         }
614         if (!isset($record['descriptionformat'])) {
615             $record['descriptionformat'] = FORMAT_MOODLE;
616         }
618         $id = groups_create_grouping((object)$record);
620         return $DB->get_record('groupings', array('id'=>$id));
621     }
623     /**
624      * Create a test grouping group
625      * @param array|stdClass $record
626      * @throws coding_exception
627      * @return boolean
628      */
629     public function create_grouping_group($record) {
630         global $DB, $CFG;
632         require_once($CFG->dirroot . '/group/lib.php');
634         $record = (array)$record;
636         if (empty($record['groupingid'])) {
637             throw new coding_exception('grouping must be present in testing::create_grouping_group() $record');
638         }
640         if (!isset($record['groupid'])) {
641             throw new coding_exception('group must be present in testing_util::create_grouping_group() $record');
642         }
644         return groups_assign_grouping($record['groupingid'], $record['groupid']);
645     }
647     /**
648      * Create an instance of a repository.
649      *
650      * @param string type of repository to create an instance for.
651      * @param array|stdClass $record data to use to up set the instance.
652      * @param array $options options
653      * @return stdClass repository instance record
654      * @since Moodle 2.5.1
655      */
656     public function create_repository($type, $record=null, array $options = null) {
657         $generator = $this->get_plugin_generator('repository_'.$type);
658         return $generator->create_instance($record, $options);
659     }
661     /**
662      * Create an instance of a repository.
663      *
664      * @param string type of repository to create an instance for.
665      * @param array|stdClass $record data to use to up set the instance.
666      * @param array $options options
667      * @return repository_type object
668      * @since Moodle 2.5.1
669      */
670     public function create_repository_type($type, $record=null, array $options = null) {
671         $generator = $this->get_plugin_generator('repository_'.$type);
672         return $generator->create_type($record, $options);
673     }
676     /**
677      * Create a test scale
678      * @param array|stdClass $record
679      * @param array $options
680      * @return stdClass block instance record
681      */
682     public function create_scale($record=null, array $options=null) {
683         global $DB;
685         $this->scalecount++;
686         $i = $this->scalecount;
688         $record = (array)$record;
690         if (!isset($record['name'])) {
691             $record['name'] = 'Test scale '.$i;
692         }
694         if (!isset($record['scale'])) {
695             $record['scale'] = 'A,B,C,D,F';
696         }
698         if (!isset($record['courseid'])) {
699             $record['courseid'] = 0;
700         }
702         if (!isset($record['userid'])) {
703             $record['userid'] = 0;
704         }
706         if (!isset($record['description'])) {
707             $record['description'] = 'Test scale description '.$i;
708         }
710         if (!isset($record['descriptionformat'])) {
711             $record['descriptionformat'] = FORMAT_MOODLE;
712         }
714         $record['timemodified'] = time();
716         if (isset($record['id'])) {
717             $DB->import_record('scale', $record);
718             $DB->get_manager()->reset_sequence('scale');
719             $id = $record['id'];
720         } else {
721             $id = $DB->insert_record('scale', $record);
722         }
724         return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST);
725     }
727     /**
728      * Creates a new role in the system.
729      *
730      * You can fill $record with the role 'name',
731      * 'shortname', 'description' and 'archetype'.
732      *
733      * If an archetype is specified it's capabilities,
734      * context where the role can be assigned and
735      * all other properties are copied from the archetype;
736      * if no archetype is specified it will create an
737      * empty role.
738      *
739      * @param array|stdClass $record
740      * @return int The new role id
741      */
742     public function create_role($record=null) {
743         global $DB;
745         $this->rolecount++;
746         $i = $this->rolecount;
748         $record = (array)$record;
750         if (empty($record['shortname'])) {
751             $record['shortname'] = 'role-' . $i;
752         }
754         if (empty($record['name'])) {
755             $record['name'] = 'Test role ' . $i;
756         }
758         if (empty($record['description'])) {
759             $record['description'] = 'Test role ' . $i . ' description';
760         }
762         if (empty($record['archetype'])) {
763             $record['archetype'] = '';
764         } else {
765             $archetypes = get_role_archetypes();
766             if (empty($archetypes[$record['archetype']])) {
767                 throw new coding_exception('\'role\' requires the field \'archetype\' to specify a ' .
768                     'valid archetype shortname (editingteacher, student...)');
769             }
770         }
772         // Creates the role.
773         if (!$newroleid = create_role($record['name'], $record['shortname'], $record['description'], $record['archetype'])) {
774             throw new coding_exception('There was an error creating \'' . $record['shortname'] . '\' role');
775         }
777         // If no archetype was specified we allow it to be added to all contexts,
778         // otherwise we allow it in the archetype contexts.
779         if (!$record['archetype']) {
780             $contextlevels = array_keys(context_helper::get_all_levels());
781         } else {
782             // Copying from the archetype default rol.
783             $archetyperoleid = $DB->get_field(
784                 'role',
785                 'id',
786                 array('shortname' => $record['archetype'], 'archetype' => $record['archetype'])
787             );
788             $contextlevels = get_role_contextlevels($archetyperoleid);
789         }
790         set_role_contextlevels($newroleid, $contextlevels);
792         if ($record['archetype']) {
794             // We copy all the roles the archetype can assign, override, switch to and view.
795             if ($record['archetype']) {
796                 $types = array('assign', 'override', 'switch', 'view');
797                 foreach ($types as $type) {
798                     $rolestocopy = get_default_role_archetype_allows($type, $record['archetype']);
799                     foreach ($rolestocopy as $tocopy) {
800                         $functionname = "core_role_set_{$type}_allowed";
801                         $functionname($newroleid, $tocopy);
802                     }
803                 }
804             }
806             // Copying the archetype capabilities.
807             $sourcerole = $DB->get_record('role', array('id' => $archetyperoleid));
808             role_cap_duplicate($sourcerole, $newroleid);
809         }
811         return $newroleid;
812     }
814     /**
815      * Create a tag.
816      *
817      * @param array|stdClass $record
818      * @return stdClass the tag record
819      */
820     public function create_tag($record = null) {
821         global $DB, $USER;
823         $this->tagcount++;
824         $i = $this->tagcount;
826         $record = (array) $record;
828         if (!isset($record['userid'])) {
829             $record['userid'] = $USER->id;
830         }
832         if (!isset($record['rawname'])) {
833             if (isset($record['name'])) {
834                 $record['rawname'] = $record['name'];
835             } else {
836                 $record['rawname'] = 'Tag name ' . $i;
837             }
838         }
840         // Attribute 'name' should be a lowercase version of 'rawname', if not set.
841         if (!isset($record['name'])) {
842             $record['name'] = core_text::strtolower($record['rawname']);
843         } else {
844             $record['name'] = core_text::strtolower($record['name']);
845         }
847         if (!isset($record['tagcollid'])) {
848             $record['tagcollid'] = core_tag_collection::get_default();
849         }
851         if (!isset($record['description'])) {
852             $record['description'] = 'Tag description';
853         }
855         if (!isset($record['descriptionformat'])) {
856             $record['descriptionformat'] = FORMAT_MOODLE;
857         }
859         if (!isset($record['flag'])) {
860             $record['flag'] = 0;
861         }
863         if (!isset($record['timemodified'])) {
864             $record['timemodified'] = time();
865         }
867         $id = $DB->insert_record('tag', $record);
869         return $DB->get_record('tag', array('id' => $id), '*', MUST_EXIST);
870     }
872     /**
873      * Helper method which combines $defaults with the values specified in $record.
874      * If $record is an object, it is converted to an array.
875      * Then, for each key that is in $defaults, but not in $record, the value
876      * from $defaults is copied.
877      * @param array $defaults the default value for each field with
878      * @param array|stdClass $record
879      * @return array updated $record.
880      */
881     public function combine_defaults_and_record(array $defaults, $record) {
882         $record = (array) $record;
884         foreach ($defaults as $key => $defaults) {
885             if (!array_key_exists($key, $record)) {
886                 $record[$key] = $defaults;
887             }
888         }
889         return $record;
890     }
892     /**
893      * Simplified enrolment of user to course using default options.
894      *
895      * It is strongly recommended to use only this method for 'manual' and 'self' plugins only!!!
896      *
897      * @param int $userid
898      * @param int $courseid
899      * @param int|string $roleidorshortname optional role id or role shortname, use only with manual plugin
900      * @param string $enrol name of enrol plugin,
901      *     there must be exactly one instance in course,
902      *     it must support enrol_user() method.
903      * @param int $timestart (optional) 0 means unknown
904      * @param int $timeend (optional) 0 means forever
905      * @param int $status (optional) default to ENROL_USER_ACTIVE for new enrolments
906      * @return bool success
907      */
908     public function enrol_user($userid, $courseid, $roleidorshortname = null, $enrol = 'manual',
909             $timestart = 0, $timeend = 0, $status = null) {
910         global $DB;
912         // If role is specified by shortname, convert it into an id.
913         if (!is_numeric($roleidorshortname) && is_string($roleidorshortname)) {
914             $roleid = $DB->get_field('role', 'id', array('shortname' => $roleidorshortname), MUST_EXIST);
915         } else {
916             $roleid = $roleidorshortname;
917         }
919         if (!$plugin = enrol_get_plugin($enrol)) {
920             return false;
921         }
923         $instances = $DB->get_records('enrol', array('courseid'=>$courseid, 'enrol'=>$enrol));
924         if (count($instances) != 1) {
925             return false;
926         }
927         $instance = reset($instances);
929         if (is_null($roleid) and $instance->roleid) {
930             $roleid = $instance->roleid;
931         }
933         $plugin->enrol_user($instance, $userid, $roleid, $timestart, $timeend, $status);
934         return true;
935     }
937     /**
938      * Assigns the specified role to a user in the context.
939      *
940      * @param int $roleid
941      * @param int $userid
942      * @param int $contextid Defaults to the system context
943      * @return int new/existing id of the assignment
944      */
945     public function role_assign($roleid, $userid, $contextid = false) {
947         // Default to the system context.
948         if (!$contextid) {
949             $context = context_system::instance();
950             $contextid = $context->id;
951         }
953         if (empty($roleid)) {
954             throw new coding_exception('roleid must be present in testing_data_generator::role_assign() arguments');
955         }
957         if (empty($userid)) {
958             throw new coding_exception('userid must be present in testing_data_generator::role_assign() arguments');
959         }
961         return role_assign($roleid, $userid, $contextid);
962     }
964     /**
965      * Create a grade_category.
966      *
967      * @param array|stdClass $record
968      * @return stdClass the grade category record
969      */
970     public function create_grade_category($record = null) {
971         global $CFG;
973         $this->gradecategorycounter++;
975         $record = (array)$record;
977         if (empty($record['courseid'])) {
978             throw new coding_exception('courseid must be present in testing::create_grade_category() $record');
979         }
981         if (!isset($record['fullname'])) {
982             $record['fullname'] = 'Grade category ' . $this->gradecategorycounter;
983         }
985         // For gradelib classes.
986         require_once($CFG->libdir . '/gradelib.php');
987         // Create new grading category in this course.
988         $gradecategory = new grade_category(array('courseid' => $record['courseid']), false);
989         $gradecategory->apply_default_settings();
990         grade_category::set_properties($gradecategory, $record);
991         $gradecategory->apply_forced_settings();
992         $gradecategory->insert();
994         // This creates a default grade item for the category
995         $gradeitem = $gradecategory->load_grade_item();
997         $gradecategory->update_from_db();
998         return $gradecategory->get_record_data();
999     }
1001     /**
1002      * Create a grade_item.
1003      *
1004      * @param array|stdClass $record
1005      * @return stdClass the grade item record
1006      */
1007     public function create_grade_item($record = null) {
1008         global $CFG;
1009         require_once("$CFG->libdir/gradelib.php");
1011         $this->gradeitemcounter++;
1013         if (!isset($record['itemtype'])) {
1014             $record['itemtype'] = 'manual';
1015         }
1017         if (!isset($record['itemname'])) {
1018             $record['itemname'] = 'Grade item ' . $this->gradeitemcounter;
1019         }
1021         if (isset($record['outcomeid'])) {
1022             $outcome = new grade_outcome(array('id' => $record['outcomeid']));
1023             $record['scaleid'] = $outcome->scaleid;
1024         }
1025         if (isset($record['scaleid'])) {
1026             $record['gradetype'] = GRADE_TYPE_SCALE;
1027         } else if (!isset($record['gradetype'])) {
1028             $record['gradetype'] = GRADE_TYPE_VALUE;
1029         }
1031         // Create new grade item in this course.
1032         $gradeitem = new grade_item($record, false);
1033         $gradeitem->insert();
1035         $gradeitem->update_from_db();
1036         return $gradeitem->get_record_data();
1037     }
1039     /**
1040      * Create a grade_outcome.
1041      *
1042      * @param array|stdClass $record
1043      * @return stdClass the grade outcome record
1044      */
1045     public function create_grade_outcome($record = null) {
1046         global $CFG;
1048         $this->gradeoutcomecounter++;
1049         $i = $this->gradeoutcomecounter;
1051         if (!isset($record['fullname'])) {
1052             $record['fullname'] = 'Grade outcome ' . $i;
1053         }
1055         // For gradelib classes.
1056         require_once($CFG->libdir . '/gradelib.php');
1057         // Create new grading outcome in this course.
1058         $gradeoutcome = new grade_outcome($record, false);
1059         $gradeoutcome->insert();
1061         $gradeoutcome->update_from_db();
1062         return $gradeoutcome->get_record_data();
1063     }
1065     /**
1066      * Helper function used to create an LTI tool.
1067      *
1068      * @param array $data
1069      * @return stdClass the tool
1070      */
1071     public function create_lti_tool($data = array()) {
1072         global $DB;
1074         $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1075         $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
1077         // Create a course if no course id was specified.
1078         if (empty($data->courseid)) {
1079             $course = $this->create_course();
1080             $data->courseid = $course->id;
1081         } else {
1082             $course = get_course($data->courseid);
1083         }
1085         if (!empty($data->cmid)) {
1086             $data->contextid = context_module::instance($data->cmid)->id;
1087         } else {
1088             $data->contextid = context_course::instance($data->courseid)->id;
1089         }
1091         // Set it to enabled if no status was specified.
1092         if (!isset($data->status)) {
1093             $data->status = ENROL_INSTANCE_ENABLED;
1094         }
1096         // Add some extra necessary fields to the data.
1097         $data->name = 'Test LTI';
1098         $data->roleinstructor = $studentrole->id;
1099         $data->rolelearner = $teacherrole->id;
1101         // Get the enrol LTI plugin.
1102         $enrolplugin = enrol_get_plugin('lti');
1103         $instanceid = $enrolplugin->add_instance($course, (array) $data);
1105         // Get the tool associated with this instance.
1106         return $DB->get_record('enrol_lti_tools', array('enrolid' => $instanceid));
1107     }
1109     /**
1110      * Helper function used to create an event.
1111      *
1112      * @param   array   $data
1113      * @return  stdClass
1114      */
1115     public function create_event($data = []) {
1116         global $CFG;
1118         require_once($CFG->dirroot . '/calendar/lib.php');
1119         $record = new \stdClass();
1120         $record->name = 'event name';
1121         $record->repeat = 0;
1122         $record->repeats = 0;
1123         $record->timestart = time();
1124         $record->timeduration = 0;
1125         $record->timesort = 0;
1126         $record->eventtype = 'user';
1127         $record->courseid = 0;
1128         $record->categoryid = 0;
1130         foreach ($data as $key => $value) {
1131             $record->$key = $value;
1132         }
1134         switch ($record->eventtype) {
1135             case 'user':
1136                 unset($record->categoryid);
1137                 unset($record->courseid);
1138                 unset($record->groupid);
1139                 break;
1140             case 'group':
1141                 unset($record->categoryid);
1142                 break;
1143             case 'course':
1144                 unset($record->categoryid);
1145                 unset($record->groupid);
1146                 break;
1147             case 'category':
1148                 unset($record->courseid);
1149                 unset($record->groupid);
1150                 break;
1151             case 'site':
1152                 unset($record->categoryid);
1153                 unset($record->courseid);
1154                 unset($record->groupid);
1155                 break;
1156         }
1158         $event = new calendar_event($record);
1159         $event->create($record);
1161         return $event->properties();
1162     }
1164     /**
1165      * Create a new course custom field category with the given name.
1166      *
1167      * @param   array $data Array with data['name'] of category
1168      * @return  \core_customfield\category_controller   The created category
1169      */
1170     public function create_custom_field_category($data) : \core_customfield\category_controller {
1171         return $this->get_plugin_generator('core_customfield')->create_category($data);
1172     }
1174     /**
1175      * Create a new custom field
1176      *
1177      * @param   array $data Array with 'name', 'shortname' and 'type' of the field
1178      * @return  \core_customfield\field_controller   The created field
1179      */
1180     public function create_custom_field($data) : \core_customfield\field_controller {
1181         global $DB;
1182         if (empty($data['categoryid']) && !empty($data['category'])) {
1183             $data['categoryid'] = $DB->get_field('customfield_category', 'id', ['name' => $data['category']]);
1184             unset($data['category']);
1185         }
1186         return $this->get_plugin_generator('core_customfield')->create_field($data);
1187     }
1189     /**
1190      * Create a new category for custom profile fields.
1191      *
1192      * @param array $data Array with 'name' and optionally 'sortorder'
1193      * @return \stdClass New category object
1194      */
1195     public function create_custom_profile_field_category(array $data): \stdClass {
1196         global $DB;
1198         // Pick next sortorder if not defined.
1199         if (!array_key_exists('sortorder', $data)) {
1200             $data['sortorder'] = (int)$DB->get_field_sql('SELECT MAX(sortorder) FROM {user_info_category}') + 1;
1201         }
1203         $category = (object)[
1204             'name' => $data['name'],
1205             'sortorder' => $data['sortorder']
1206         ];
1207         $category->id = $DB->insert_record('user_info_category', $category);
1209         return $category;
1210     }
1212     /**
1213      * Creates a new custom profile field.
1214      *
1215      * Optional fields are:
1216      *
1217      * categoryid (or use 'category' to specify by name). If you don't specify
1218      * either, it will add the field to a 'Testing' category, which will be created for you if
1219      * necessary.
1220      *
1221      * sortorder (if you don't specify this, it will pick the next one in the category).
1222      *
1223      * all the other database fields (if you don't specify this, it will pick sensible defaults
1224      * based on the data type).
1225      *
1226      * @param array $data Array with 'datatype', 'shortname', and 'name'
1227      * @return \stdClass Database object from the user_info_field table
1228      */
1229     public function create_custom_profile_field(array $data): \stdClass {
1230         global $DB, $CFG;
1231         require_once($CFG->dirroot . '/user/profile/lib.php');
1233         // Set up category if necessary.
1234         if (!array_key_exists('categoryid', $data)) {
1235             if (array_key_exists('category', $data)) {
1236                 $data['categoryid'] = $DB->get_field('user_info_category', 'id',
1237                         ['name' => $data['category']], MUST_EXIST);
1238             } else {
1239                 // Make up a 'Testing' category or use existing.
1240                 $data['categoryid'] = $DB->get_field('user_info_category', 'id', ['name' => 'Testing']);
1241                 if (!$data['categoryid']) {
1242                     $created = $this->create_custom_profile_field_category(['name' => 'Testing']);
1243                     $data['categoryid'] = $created->id;
1244                 }
1245             }
1246         }
1248         // Pick sort order if necessary.
1249         if (!array_key_exists('sortorder', $data)) {
1250             $data['sortorder'] = (int)$DB->get_field_sql(
1251                     'SELECT MAX(sortorder) FROM {user_info_field} WHERE categoryid = ?',
1252                     [$data['categoryid']]) + 1;
1253         }
1255         // Defaults for other values.
1256         $defaults = [
1257             'description' => '',
1258             'descriptionformat' => 0,
1259             'required' => 0,
1260             'locked' => 0,
1261             'visible' => PROFILE_VISIBLE_ALL,
1262             'forceunique' => 0,
1263             'signup' => 0,
1264             'defaultdata' => '',
1265             'defaultdataformat' => 0,
1266             'param1' => '',
1267             'param2' => '',
1268             'param3' => '',
1269             'param4' => '',
1270             'param5' => ''
1271         ];
1273         // Type-specific defaults for other values.
1274         $typedefaults = [
1275             'text' => [
1276                 'param1' => 30,
1277                 'param2' => 2048
1278             ],
1279             'menu' => [
1280                 'param1' => "Yes\nNo",
1281                 'defaultdata' => 'No'
1282             ],
1283             'datetime' => [
1284                 'param1' => '2010',
1285                 'param2' => '2015',
1286                 'param3' => 1
1287             ],
1288             'checkbox' => [
1289                 'defaultdata' => 0
1290             ]
1291         ];
1292         foreach ($typedefaults[$data['datatype']] as $field => $value) {
1293             $defaults[$field] = $value;
1294         }
1296         foreach ($defaults as $field => $value) {
1297             if (!array_key_exists($field, $data)) {
1298                 $data[$field] = $value;
1299             }
1300         }
1302         $data['id'] = $DB->insert_record('user_info_field', $data);
1303         return (object)$data;
1304     }
1306     /**
1307      * Create a new user, and enrol them in the specified course as the supplied role.
1308      *
1309      * @param   \stdClass   $course The course to enrol in
1310      * @param   string      $role The role to give within the course
1311      * @param   \stdClass   $userparams User parameters
1312      * @return  \stdClass   The created user
1313      */
1314     public function create_and_enrol($course, $role = 'student', $userparams = null, $enrol = 'manual',
1315             $timestart = 0, $timeend = 0, $status = null) {
1316         global $DB;
1318         $user = $this->create_user($userparams);
1319         $roleid = $DB->get_field('role', 'id', ['shortname' => $role ]);
1321         $this->enrol_user($user->id, $course->id, $roleid, $enrol, $timestart, $timeend, $status);
1323         return $user;
1324     }
1326     /**
1327      * Create a new last access record for a given user in a course.
1328      *
1329      * @param   \stdClass   $user The user
1330      * @param   \stdClass   $course The course the user accessed
1331      * @param   int         $timestamp The timestamp for when the user last accessed the course
1332      * @return  \stdClass   The user_lastaccess record
1333      */
1334     public function create_user_course_lastaccess(\stdClass $user, \stdClass $course, int $timestamp): \stdClass {
1335         global $DB;
1337         $record = [
1338             'userid' => $user->id,
1339             'courseid' => $course->id,
1340             'timeaccess' => $timestamp,
1341         ];
1343         $recordid = $DB->insert_record('user_lastaccess', $record);
1345         return $DB->get_record('user_lastaccess', ['id' => $recordid], '*', MUST_EXIST);
1346     }