1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * 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 */
25
26defined('MOODLE_INTERNAL') || die();
27
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 */
36class 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;
52
53    /** @var array list of plugin generators */
54    protected $generators = array();
55
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    );
65
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    );
75
76    public $loremipsum = <<<EOD
77Lorem 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.
78Temporibus 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.
79Vivamus 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.
80Integer 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.
81In 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.
82EOD;
83
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;
94
95        foreach ($this->generators as $generator) {
96            $generator->reset();
97        }
98    }
99
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        // Note: This global is included so that generator have access to it.
107        // CFG is widely used in require statements.
108        global $CFG;
109        list($type, $plugin) = core_component::normalize_component($component);
110        $cleancomponent = $type . '_' . $plugin;
111        if ($cleancomponent != $component) {
112            debugging("Please specify the component you want a generator for as " .
113                    "{$cleancomponent}, not {$component}.", DEBUG_DEVELOPER);
114            $component = $cleancomponent;
115        }
116
117        if (isset($this->generators[$component])) {
118            return $this->generators[$component];
119        }
120
121        $dir = core_component::get_component_directory($component);
122        $lib = $dir . '/tests/generator/lib.php';
123        if (!$dir || !is_readable($lib)) {
124            throw new coding_exception("Component {$component} does not support " .
125                    "generators yet. Missing tests/generator/lib.php.");
126        }
127
128        include_once($lib);
129        $classname = $component . '_generator';
130
131        if (!class_exists($classname)) {
132            throw new coding_exception("Component {$component} does not support " .
133                    "data generators yet. Class {$classname} not found.");
134        }
135
136        $this->generators[$component] = new $classname($this);
137        return $this->generators[$component];
138    }
139
140    /**
141     * Create a test user
142     * @param array|stdClass $record
143     * @param array $options
144     * @return stdClass user record
145     */
146    public function create_user($record=null, array $options=null) {
147        global $DB, $CFG;
148        require_once($CFG->dirroot.'/user/lib.php');
149
150        $this->usercounter++;
151        $i = $this->usercounter;
152
153        $record = (array)$record;
154
155        if (!isset($record['auth'])) {
156            $record['auth'] = 'manual';
157        }
158
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)];
166
167        } else if (!isset($record['firstname'])) {
168            $record['firstname'] = 'Firstname'.$i;
169
170        } else if (!isset($record['lastname'])) {
171            $record['lastname'] = 'Lastname'.$i;
172        }
173
174        if (!isset($record['firstnamephonetic'])) {
175            $firstnamephonetic = rand(0, 59);
176            $record['firstnamephonetic'] = $this->firstnames[$firstnamephonetic];
177        }
178
179        if (!isset($record['lastnamephonetic'])) {
180            $lastnamephonetic = rand(0, 59);
181            $record['lastnamephonetic'] = $this->lastnames[$lastnamephonetic];
182        }
183
184        if (!isset($record['middlename'])) {
185            $middlename = rand(0, 59);
186            $record['middlename'] = $this->firstnames[$middlename];
187        }
188
189        if (!isset($record['alternatename'])) {
190            $alternatename = rand(0, 59);
191            $record['alternatename'] = $this->firstnames[$alternatename];
192        }
193
194        if (!isset($record['idnumber'])) {
195            $record['idnumber'] = '';
196        }
197
198        if (!isset($record['mnethostid'])) {
199            $record['mnethostid'] = $CFG->mnet_localhost_id;
200        }
201
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++;
208            }
209        }
210
211        if (isset($record['password'])) {
212            $record['password'] = hash_internal_user_password($record['password']);
213        }
214
215        if (!isset($record['email'])) {
216            $record['email'] = $record['username'].'@example.com';
217        }
218
219        if (!isset($record['confirmed'])) {
220            $record['confirmed'] = 1;
221        }
222
223        if (!isset($record['lastip'])) {
224            $record['lastip'] = '0.0.0.0';
225        }
226
227        $tobedeleted = !empty($record['deleted']);
228        unset($record['deleted']);
229
230        $userid = user_create_user($record, false, false);
231
232        if ($extrafields = array_intersect_key($record, ['password' => 1, 'timecreated' => 1])) {
233            $DB->update_record('user', ['id' => $userid] + $extrafields);
234        }
235
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]
241            );
242            \core_message\api::set_favourite_conversation($selfconversation->id, $userid);
243
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);
252            }
253        }
254
255        $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
256
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);
261            }
262            useredit_update_interests($user, $record['interests']);
263        }
264
265        \core\event\user_created::create_from_userid($userid)->trigger();
266
267        if ($tobedeleted) {
268            delete_user($user);
269            $user = $DB->get_record('user', array('id' => $userid));
270        }
271        return $user;
272    }
273
274    /**
275     * Create a test course category
276     * @param array|stdClass $record
277     * @param array $options
278     * @return core_course_category course category record
279     */
280    public function create_category($record=null, array $options=null) {
281        $this->categorycount++;
282        $i = $this->categorycount;
283
284        $record = (array)$record;
285
286        if (!isset($record['name'])) {
287            $record['name'] = 'Course category '.$i;
288        }
289
290        if (!isset($record['description'])) {
291            $record['description'] = "Test course category $i\n$this->loremipsum";
292        }
293
294        if (!isset($record['idnumber'])) {
295            $record['idnumber'] = '';
296        }
297
298        return core_course_category::create($record);
299    }
300
301    /**
302     * Create test cohort.
303     * @param array|stdClass $record
304     * @param array $options
305     * @return stdClass cohort record
306     */
307    public function create_cohort($record=null, array $options=null) {
308        global $DB, $CFG;
309        require_once("$CFG->dirroot/cohort/lib.php");
310
311        $this->cohortcount++;
312        $i = $this->cohortcount;
313
314        $record = (array)$record;
315
316        if (!isset($record['contextid'])) {
317            $record['contextid'] = context_system::instance()->id;
318        }
319
320        if (!isset($record['name'])) {
321            $record['name'] = 'Cohort '.$i;
322        }
323
324        if (!isset($record['idnumber'])) {
325            $record['idnumber'] = '';
326        }
327
328        if (!isset($record['description'])) {
329            $record['description'] = "Description for '{$record['name']}' \n$this->loremipsum";
330        }
331
332        if (!isset($record['descriptionformat'])) {
333            $record['descriptionformat'] = FORMAT_MOODLE;
334        }
335
336        if (!isset($record['visible'])) {
337            $record['visible'] = 1;
338        }
339
340        if (!isset($record['component'])) {
341            $record['component'] = '';
342        }
343
344        $id = cohort_add_cohort((object)$record);
345
346        return $DB->get_record('cohort', array('id'=>$id), '*', MUST_EXIST);
347    }
348
349    /**
350     * Create a test course
351     * @param array|stdClass $record
352     * @param array $options with keys:
353     *      'createsections'=>bool precreate all sections
354     * @return stdClass course record
355     */
356    public function create_course($record=null, array $options=null) {
357        global $DB, $CFG;
358        require_once("$CFG->dirroot/course/lib.php");
359
360        $this->coursecount++;
361        $i = $this->coursecount;
362
363        $record = (array)$record;
364
365        if (!isset($record['fullname'])) {
366            $record['fullname'] = 'Test course '.$i;
367        }
368
369        if (!isset($record['shortname'])) {
370            $record['shortname'] = 'tc_'.$i;
371        }
372
373        if (!isset($record['idnumber'])) {
374            $record['idnumber'] = '';
375        }
376
377        if (!isset($record['format'])) {
378            $record['format'] = 'topics';
379        }
380
381        if (!isset($record['newsitems'])) {
382            $record['newsitems'] = 0;
383        }
384
385        if (!isset($record['numsections'])) {
386            $record['numsections'] = 5;
387        }
388
389        if (!isset($record['summary'])) {
390            $record['summary'] = "Test course $i\n$this->loremipsum";
391        }
392
393        if (!isset($record['summaryformat'])) {
394            $record['summaryformat'] = FORMAT_MOODLE;
395        }
396
397        if (!isset($record['category'])) {
398            $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
399        }
400
401        if (!isset($record['startdate'])) {
402            $record['startdate'] = usergetmidnight(time());
403        }
404
405        if (isset($record['tags']) && !is_array($record['tags'])) {
406            $record['tags'] = preg_split('/\s*,\s*/', trim($record['tags']), -1, PREG_SPLIT_NO_EMPTY);
407        }
408
409        if (!empty($options['createsections']) && empty($record['numsections'])) {
410            // Since Moodle 3.3 function create_course() automatically creates sections if numsections is specified.
411            // For BC if 'createsections' is given but 'numsections' is not, assume the default value from config.
412            $record['numsections'] = get_config('moodlecourse', 'numsections');
413        }
414
415        if (!empty($record['customfields'])) {
416            foreach ($record['customfields'] as $field) {
417                $record['customfield_'.$field['shortname']] = $field['value'];
418            }
419        }
420
421        $course = create_course((object)$record);
422        context_course::instance($course->id);
423
424        return $course;
425    }
426
427    /**
428     * Create course section if does not exist yet
429     * @param array|stdClass $record must contain 'course' and 'section' attributes
430     * @param array|null $options
431     * @return stdClass
432     * @throws coding_exception
433     */
434    public function create_course_section($record = null, array $options = null) {
435        global $DB;
436
437        $record = (array)$record;
438
439        if (empty($record['course'])) {
440            throw new coding_exception('course must be present in testing_data_generator::create_course_section() $record');
441        }
442
443        if (!isset($record['section'])) {
444            throw new coding_exception('section must be present in testing_data_generator::create_course_section() $record');
445        }
446
447        course_create_sections_if_missing($record['course'], $record['section']);
448        return get_fast_modinfo($record['course'])->get_section_info($record['section']);
449    }
450
451    /**
452     * Create a test block.
453     *
454     * The $record passed in becomes the basis for the new row added to the
455     * block_instances table. You only need to supply the values of interest.
456     * Any missing values have sensible defaults filled in, and ->blockname will be set based on $blockname.
457     *
458     * The $options array provides additional data, not directly related to what
459     * will be inserted in the block_instance table, which may affect the block
460     * that is created. The meanings of any data passed here depends on the particular
461     * type of block being created.
462     *
463     * @param string $blockname the type of block to create. E.g. 'html'.
464     * @param array|stdClass $record forms the basis for the entry to be inserted in the block_instances table.
465     * @param array $options further, block-specific options to control how the block is created.
466     * @return stdClass new block_instance record.
467     */
468    public function create_block($blockname, $record=null, array $options=array()) {
469        $generator = $this->get_plugin_generator('block_'.$blockname);
470        return $generator->create_instance($record, $options);
471    }
472
473    /**
474     * Create a test activity module.
475     *
476     * The $record should contain the same data that you would call from
477     * ->get_data() when the mod_[type]_mod_form is submitted, except that you
478     * only need to supply values of interest. The only required value is
479     * 'course'. Any missing values will have a sensible default supplied.
480     *
481     * The $options array provides additional data, not directly related to what
482     * would come back from the module edit settings form, which may affect the activity
483     * that is created. The meanings of any data passed here depends on the particular
484     * type of activity being created.
485     *
486     * @param string $modulename the type of activity to create. E.g. 'forum' or 'quiz'.
487     * @param array|stdClass $record data, as if from the module edit settings form.
488     * @param array $options additional data that may affect how the module is created.
489     * @return stdClass activity record new new record that was just inserted in the table
490     *      like 'forum' or 'quiz', with a ->cmid field added.
491     */
492    public function create_module($modulename, $record=null, array $options=null) {
493        $generator = $this->get_plugin_generator('mod_'.$modulename);
494        return $generator->create_instance($record, $options);
495    }
496
497    /**
498     * Create a test group for the specified course
499     *
500     * $record should be either an array or a stdClass containing infomation about the group to create.
501     * At the very least it needs to contain courseid.
502     * Default values are added for name, description, and descriptionformat if they are not present.
503     *
504     * This function calls groups_create_group() to create the group within the database.
505     * @see groups_create_group
506     * @param array|stdClass $record
507     * @return stdClass group record
508     */
509    public function create_group($record) {
510        global $DB, $CFG;
511
512        require_once($CFG->dirroot . '/group/lib.php');
513
514        $this->groupcount++;
515        $i = str_pad($this->groupcount, 4, '0', STR_PAD_LEFT);
516
517        $record = (array)$record;
518
519        if (empty($record['courseid'])) {
520            throw new coding_exception('courseid must be present in testing_data_generator::create_group() $record');
521        }
522
523        if (!isset($record['name'])) {
524            $record['name'] = 'group-' . $i;
525        }
526
527        if (!isset($record['description'])) {
528            $record['description'] = "Test Group $i\n{$this->loremipsum}";
529        }
530
531        if (!isset($record['descriptionformat'])) {
532            $record['descriptionformat'] = FORMAT_MOODLE;
533        }
534
535        $id = groups_create_group((object)$record);
536
537        // Allow tests to set group pictures.
538        if (!empty($record['picturepath'])) {
539            require_once($CFG->dirroot . '/lib/gdlib.php');
540            $grouppicture = process_new_icon(\context_course::instance($record['courseid']), 'group', 'icon', $id,
541                $record['picturepath']);
542
543            $DB->set_field('groups', 'picture', $grouppicture, ['id' => $id]);
544
545            // Invalidate the group data as we've updated the group record.
546            cache_helper::invalidate_by_definition('core', 'groupdata', array(), [$record['courseid']]);
547        }
548
549        return $DB->get_record('groups', array('id'=>$id));
550    }
551
552    /**
553     * Create a test group member
554     * @param array|stdClass $record
555     * @throws coding_exception
556     * @return boolean
557     */
558    public function create_group_member($record) {
559        global $DB, $CFG;
560
561        require_once($CFG->dirroot . '/group/lib.php');
562
563        $record = (array)$record;
564
565        if (empty($record['userid'])) {
566            throw new coding_exception('user must be present in testing_util::create_group_member() $record');
567        }
568
569        if (!isset($record['groupid'])) {
570            throw new coding_exception('group must be present in testing_util::create_group_member() $record');
571        }
572
573        if (!isset($record['component'])) {
574            $record['component'] = null;
575        }
576        if (!isset($record['itemid'])) {
577            $record['itemid'] = 0;
578        }
579
580        return groups_add_member($record['groupid'], $record['userid'], $record['component'], $record['itemid']);
581    }
582
583    /**
584     * Create a test grouping for the specified course
585     *
586     * $record should be either an array or a stdClass containing infomation about the grouping to create.
587     * At the very least it needs to contain courseid.
588     * Default values are added for name, description, and descriptionformat if they are not present.
589     *
590     * This function calls groups_create_grouping() to create the grouping within the database.
591     * @see groups_create_grouping
592     * @param array|stdClass $record
593     * @return stdClass grouping record
594     */
595    public function create_grouping($record) {
596        global $DB, $CFG;
597
598        require_once($CFG->dirroot . '/group/lib.php');
599
600        $this->groupingcount++;
601        $i = $this->groupingcount;
602
603        $record = (array)$record;
604
605        if (empty($record['courseid'])) {
606            throw new coding_exception('courseid must be present in testing_data_generator::create_grouping() $record');
607        }
608
609        if (!isset($record['name'])) {
610            $record['name'] = 'grouping-' . $i;
611        }
612
613        if (!isset($record['description'])) {
614            $record['description'] = "Test Grouping $i\n{$this->loremipsum}";
615        }
616
617        if (!isset($record['descriptionformat'])) {
618            $record['descriptionformat'] = FORMAT_MOODLE;
619        }
620
621        $id = groups_create_grouping((object)$record);
622
623        return $DB->get_record('groupings', array('id'=>$id));
624    }
625
626    /**
627     * Create a test grouping group
628     * @param array|stdClass $record
629     * @throws coding_exception
630     * @return boolean
631     */
632    public function create_grouping_group($record) {
633        global $DB, $CFG;
634
635        require_once($CFG->dirroot . '/group/lib.php');
636
637        $record = (array)$record;
638
639        if (empty($record['groupingid'])) {
640            throw new coding_exception('grouping must be present in testing::create_grouping_group() $record');
641        }
642
643        if (!isset($record['groupid'])) {
644            throw new coding_exception('group must be present in testing_util::create_grouping_group() $record');
645        }
646
647        return groups_assign_grouping($record['groupingid'], $record['groupid']);
648    }
649
650    /**
651     * Create an instance of a repository.
652     *
653     * @param string type of repository to create an instance for.
654     * @param array|stdClass $record data to use to up set the instance.
655     * @param array $options options
656     * @return stdClass repository instance record
657     * @since Moodle 2.5.1
658     */
659    public function create_repository($type, $record=null, array $options = null) {
660        $generator = $this->get_plugin_generator('repository_'.$type);
661        return $generator->create_instance($record, $options);
662    }
663
664    /**
665     * Create an instance of a repository.
666     *
667     * @param string type of repository to create an instance for.
668     * @param array|stdClass $record data to use to up set the instance.
669     * @param array $options options
670     * @return repository_type object
671     * @since Moodle 2.5.1
672     */
673    public function create_repository_type($type, $record=null, array $options = null) {
674        $generator = $this->get_plugin_generator('repository_'.$type);
675        return $generator->create_type($record, $options);
676    }
677
678
679    /**
680     * Create a test scale
681     * @param array|stdClass $record
682     * @param array $options
683     * @return stdClass block instance record
684     */
685    public function create_scale($record=null, array $options=null) {
686        global $DB;
687
688        $this->scalecount++;
689        $i = $this->scalecount;
690
691        $record = (array)$record;
692
693        if (!isset($record['name'])) {
694            $record['name'] = 'Test scale '.$i;
695        }
696
697        if (!isset($record['scale'])) {
698            $record['scale'] = 'A,B,C,D,F';
699        }
700
701        if (!isset($record['courseid'])) {
702            $record['courseid'] = 0;
703        }
704
705        if (!isset($record['userid'])) {
706            $record['userid'] = 0;
707        }
708
709        if (!isset($record['description'])) {
710            $record['description'] = 'Test scale description '.$i;
711        }
712
713        if (!isset($record['descriptionformat'])) {
714            $record['descriptionformat'] = FORMAT_MOODLE;
715        }
716
717        $record['timemodified'] = time();
718
719        if (isset($record['id'])) {
720            $DB->import_record('scale', $record);
721            $DB->get_manager()->reset_sequence('scale');
722            $id = $record['id'];
723        } else {
724            $id = $DB->insert_record('scale', $record);
725        }
726
727        return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST);
728    }
729
730    /**
731     * Creates a new role in the system.
732     *
733     * You can fill $record with the role 'name',
734     * 'shortname', 'description' and 'archetype'.
735     *
736     * If an archetype is specified it's capabilities,
737     * context where the role can be assigned and
738     * all other properties are copied from the archetype;
739     * if no archetype is specified it will create an
740     * empty role.
741     *
742     * @param array|stdClass $record
743     * @return int The new role id
744     */
745    public function create_role($record=null) {
746        global $DB;
747
748        $this->rolecount++;
749        $i = $this->rolecount;
750
751        $record = (array)$record;
752
753        if (empty($record['shortname'])) {
754            $record['shortname'] = 'role-' . $i;
755        }
756
757        if (empty($record['name'])) {
758            $record['name'] = 'Test role ' . $i;
759        }
760
761        if (empty($record['description'])) {
762            $record['description'] = 'Test role ' . $i . ' description';
763        }
764
765        if (empty($record['archetype'])) {
766            $record['archetype'] = '';
767        } else {
768            $archetypes = get_role_archetypes();
769            if (empty($archetypes[$record['archetype']])) {
770                throw new coding_exception('\'role\' requires the field \'archetype\' to specify a ' .
771                    'valid archetype shortname (editingteacher, student...)');
772            }
773        }
774
775        // Creates the role.
776        if (!$newroleid = create_role($record['name'], $record['shortname'], $record['description'], $record['archetype'])) {
777            throw new coding_exception('There was an error creating \'' . $record['shortname'] . '\' role');
778        }
779
780        // If no archetype was specified we allow it to be added to all contexts,
781        // otherwise we allow it in the archetype contexts.
782        if (!$record['archetype']) {
783            $contextlevels = array_keys(context_helper::get_all_levels());
784        } else {
785            // Copying from the archetype default rol.
786            $archetyperoleid = $DB->get_field(
787                'role',
788                'id',
789                array('shortname' => $record['archetype'], 'archetype' => $record['archetype'])
790            );
791            $contextlevels = get_role_contextlevels($archetyperoleid);
792        }
793        set_role_contextlevels($newroleid, $contextlevels);
794
795        if ($record['archetype']) {
796
797            // We copy all the roles the archetype can assign, override, switch to and view.
798            if ($record['archetype']) {
799                $types = array('assign', 'override', 'switch', 'view');
800                foreach ($types as $type) {
801                    $rolestocopy = get_default_role_archetype_allows($type, $record['archetype']);
802                    foreach ($rolestocopy as $tocopy) {
803                        $functionname = "core_role_set_{$type}_allowed";
804                        $functionname($newroleid, $tocopy);
805                    }
806                }
807            }
808
809            // Copying the archetype capabilities.
810            $sourcerole = $DB->get_record('role', array('id' => $archetyperoleid));
811            role_cap_duplicate($sourcerole, $newroleid);
812        }
813
814        return $newroleid;
815    }
816
817    /**
818     * Create a tag.
819     *
820     * @param array|stdClass $record
821     * @return stdClass the tag record
822     */
823    public function create_tag($record = null) {
824        global $DB, $USER;
825
826        $this->tagcount++;
827        $i = $this->tagcount;
828
829        $record = (array) $record;
830
831        if (!isset($record['userid'])) {
832            $record['userid'] = $USER->id;
833        }
834
835        if (!isset($record['rawname'])) {
836            if (isset($record['name'])) {
837                $record['rawname'] = $record['name'];
838            } else {
839                $record['rawname'] = 'Tag name ' . $i;
840            }
841        }
842
843        // Attribute 'name' should be a lowercase version of 'rawname', if not set.
844        if (!isset($record['name'])) {
845            $record['name'] = core_text::strtolower($record['rawname']);
846        } else {
847            $record['name'] = core_text::strtolower($record['name']);
848        }
849
850        if (!isset($record['tagcollid'])) {
851            $record['tagcollid'] = core_tag_collection::get_default();
852        }
853
854        if (!isset($record['description'])) {
855            $record['description'] = 'Tag description';
856        }
857
858        if (!isset($record['descriptionformat'])) {
859            $record['descriptionformat'] = FORMAT_MOODLE;
860        }
861
862        if (!isset($record['flag'])) {
863            $record['flag'] = 0;
864        }
865
866        if (!isset($record['timemodified'])) {
867            $record['timemodified'] = time();
868        }
869
870        $id = $DB->insert_record('tag', $record);
871
872        return $DB->get_record('tag', array('id' => $id), '*', MUST_EXIST);
873    }
874
875    /**
876     * Helper method which combines $defaults with the values specified in $record.
877     * If $record is an object, it is converted to an array.
878     * Then, for each key that is in $defaults, but not in $record, the value
879     * from $defaults is copied.
880     * @param array $defaults the default value for each field with
881     * @param array|stdClass $record
882     * @return array updated $record.
883     */
884    public function combine_defaults_and_record(array $defaults, $record) {
885        $record = (array) $record;
886
887        foreach ($defaults as $key => $defaults) {
888            if (!array_key_exists($key, $record)) {
889                $record[$key] = $defaults;
890            }
891        }
892        return $record;
893    }
894
895    /**
896     * Simplified enrolment of user to course using default options.
897     *
898     * It is strongly recommended to use only this method for 'manual' and 'self' plugins only!!!
899     *
900     * @param int $userid
901     * @param int $courseid
902     * @param int|string $roleidorshortname optional role id or role shortname, use only with manual plugin
903     * @param string $enrol name of enrol plugin,
904     *     there must be exactly one instance in course,
905     *     it must support enrol_user() method.
906     * @param int $timestart (optional) 0 means unknown
907     * @param int $timeend (optional) 0 means forever
908     * @param int $status (optional) default to ENROL_USER_ACTIVE for new enrolments
909     * @return bool success
910     */
911    public function enrol_user($userid, $courseid, $roleidorshortname = null, $enrol = 'manual',
912            $timestart = 0, $timeend = 0, $status = null) {
913        global $DB;
914
915        // If role is specified by shortname, convert it into an id.
916        if (!is_numeric($roleidorshortname) && is_string($roleidorshortname)) {
917            $roleid = $DB->get_field('role', 'id', array('shortname' => $roleidorshortname), MUST_EXIST);
918        } else {
919            $roleid = $roleidorshortname;
920        }
921
922        if (!$plugin = enrol_get_plugin($enrol)) {
923            return false;
924        }
925
926        $instances = $DB->get_records('enrol', array('courseid'=>$courseid, 'enrol'=>$enrol));
927        if (count($instances) != 1) {
928            return false;
929        }
930        $instance = reset($instances);
931
932        if (is_null($roleid) and $instance->roleid) {
933            $roleid = $instance->roleid;
934        }
935
936        $plugin->enrol_user($instance, $userid, $roleid, $timestart, $timeend, $status);
937        return true;
938    }
939
940    /**
941     * Assigns the specified role to a user in the context.
942     *
943     * @param int $roleid
944     * @param int $userid
945     * @param int $contextid Defaults to the system context
946     * @return int new/existing id of the assignment
947     */
948    public function role_assign($roleid, $userid, $contextid = false) {
949
950        // Default to the system context.
951        if (!$contextid) {
952            $context = context_system::instance();
953            $contextid = $context->id;
954        }
955
956        if (empty($roleid)) {
957            throw new coding_exception('roleid must be present in testing_data_generator::role_assign() arguments');
958        }
959
960        if (empty($userid)) {
961            throw new coding_exception('userid must be present in testing_data_generator::role_assign() arguments');
962        }
963
964        return role_assign($roleid, $userid, $contextid);
965    }
966
967    /**
968     * Create a grade_category.
969     *
970     * @param array|stdClass $record
971     * @return stdClass the grade category record
972     */
973    public function create_grade_category($record = null) {
974        global $CFG;
975
976        $this->gradecategorycounter++;
977
978        $record = (array)$record;
979
980        if (empty($record['courseid'])) {
981            throw new coding_exception('courseid must be present in testing::create_grade_category() $record');
982        }
983
984        if (!isset($record['fullname'])) {
985            $record['fullname'] = 'Grade category ' . $this->gradecategorycounter;
986        }
987
988        // For gradelib classes.
989        require_once($CFG->libdir . '/gradelib.php');
990        // Create new grading category in this course.
991        $gradecategory = new grade_category(array('courseid' => $record['courseid']), false);
992        $gradecategory->apply_default_settings();
993        grade_category::set_properties($gradecategory, $record);
994        $gradecategory->apply_forced_settings();
995        $gradecategory->insert();
996
997        // This creates a default grade item for the category
998        $gradeitem = $gradecategory->load_grade_item();
999
1000        $gradecategory->update_from_db();
1001        return $gradecategory->get_record_data();
1002    }
1003
1004    /**
1005     * Create a grade_item.
1006     *
1007     * @param array|stdClass $record
1008     * @return stdClass the grade item record
1009     */
1010    public function create_grade_item($record = null) {
1011        global $CFG;
1012        require_once("$CFG->libdir/gradelib.php");
1013
1014        $this->gradeitemcounter++;
1015
1016        if (!isset($record['itemtype'])) {
1017            $record['itemtype'] = 'manual';
1018        }
1019
1020        if (!isset($record['itemname'])) {
1021            $record['itemname'] = 'Grade item ' . $this->gradeitemcounter;
1022        }
1023
1024        if (isset($record['outcomeid'])) {
1025            $outcome = new grade_outcome(array('id' => $record['outcomeid']));
1026            $record['scaleid'] = $outcome->scaleid;
1027        }
1028        if (isset($record['scaleid'])) {
1029            $record['gradetype'] = GRADE_TYPE_SCALE;
1030        } else if (!isset($record['gradetype'])) {
1031            $record['gradetype'] = GRADE_TYPE_VALUE;
1032        }
1033
1034        // Create new grade item in this course.
1035        $gradeitem = new grade_item($record, false);
1036        $gradeitem->insert();
1037
1038        $gradeitem->update_from_db();
1039        return $gradeitem->get_record_data();
1040    }
1041
1042    /**
1043     * Create a grade_outcome.
1044     *
1045     * @param array|stdClass $record
1046     * @return stdClass the grade outcome record
1047     */
1048    public function create_grade_outcome($record = null) {
1049        global $CFG;
1050
1051        $this->gradeoutcomecounter++;
1052        $i = $this->gradeoutcomecounter;
1053
1054        if (!isset($record['fullname'])) {
1055            $record['fullname'] = 'Grade outcome ' . $i;
1056        }
1057
1058        // For gradelib classes.
1059        require_once($CFG->libdir . '/gradelib.php');
1060        // Create new grading outcome in this course.
1061        $gradeoutcome = new grade_outcome($record, false);
1062        $gradeoutcome->insert();
1063
1064        $gradeoutcome->update_from_db();
1065        return $gradeoutcome->get_record_data();
1066    }
1067
1068    /**
1069     * Helper function used to create an LTI tool.
1070     *
1071     * @param array $data
1072     * @return stdClass the tool
1073     */
1074    public function create_lti_tool($data = array()) {
1075        global $DB;
1076
1077        $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1078        $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
1079
1080        // Create a course if no course id was specified.
1081        if (empty($data->courseid)) {
1082            $course = $this->create_course();
1083            $data->courseid = $course->id;
1084        } else {
1085            $course = get_course($data->courseid);
1086        }
1087
1088        if (!empty($data->cmid)) {
1089            $data->contextid = context_module::instance($data->cmid)->id;
1090        } else {
1091            $data->contextid = context_course::instance($data->courseid)->id;
1092        }
1093
1094        // Set it to enabled if no status was specified.
1095        if (!isset($data->status)) {
1096            $data->status = ENROL_INSTANCE_ENABLED;
1097        }
1098
1099        // Add some extra necessary fields to the data.
1100        $data->name = 'Test LTI';
1101        $data->roleinstructor = $studentrole->id;
1102        $data->rolelearner = $teacherrole->id;
1103
1104        // Get the enrol LTI plugin.
1105        $enrolplugin = enrol_get_plugin('lti');
1106        $instanceid = $enrolplugin->add_instance($course, (array) $data);
1107
1108        // Get the tool associated with this instance.
1109        return $DB->get_record('enrol_lti_tools', array('enrolid' => $instanceid));
1110    }
1111
1112    /**
1113     * Helper function used to create an event.
1114     *
1115     * @param   array   $data
1116     * @return  stdClass
1117     */
1118    public function create_event($data = []) {
1119        global $CFG;
1120
1121        require_once($CFG->dirroot . '/calendar/lib.php');
1122        $record = new \stdClass();
1123        $record->name = 'event name';
1124        $record->repeat = 0;
1125        $record->repeats = 0;
1126        $record->timestart = time();
1127        $record->timeduration = 0;
1128        $record->timesort = 0;
1129        $record->eventtype = 'user';
1130        $record->courseid = 0;
1131        $record->categoryid = 0;
1132
1133        foreach ($data as $key => $value) {
1134            $record->$key = $value;
1135        }
1136
1137        switch ($record->eventtype) {
1138            case 'user':
1139                unset($record->categoryid);
1140                unset($record->courseid);
1141                unset($record->groupid);
1142                break;
1143            case 'group':
1144                unset($record->categoryid);
1145                break;
1146            case 'course':
1147                unset($record->categoryid);
1148                unset($record->groupid);
1149                break;
1150            case 'category':
1151                unset($record->courseid);
1152                unset($record->groupid);
1153                break;
1154            case 'site':
1155                unset($record->categoryid);
1156                unset($record->courseid);
1157                unset($record->groupid);
1158                break;
1159        }
1160
1161        $event = new calendar_event($record);
1162        $event->create($record);
1163
1164        return $event->properties();
1165    }
1166
1167    /**
1168     * Create a new course custom field category with the given name.
1169     *
1170     * @param   array $data Array with data['name'] of category
1171     * @return  \core_customfield\category_controller   The created category
1172     */
1173    public function create_custom_field_category($data) : \core_customfield\category_controller {
1174        return $this->get_plugin_generator('core_customfield')->create_category($data);
1175    }
1176
1177    /**
1178     * Create a new custom field
1179     *
1180     * @param   array $data Array with 'name', 'shortname' and 'type' of the field
1181     * @return  \core_customfield\field_controller   The created field
1182     */
1183    public function create_custom_field($data) : \core_customfield\field_controller {
1184        global $DB;
1185        if (empty($data['categoryid']) && !empty($data['category'])) {
1186            $data['categoryid'] = $DB->get_field('customfield_category', 'id', ['name' => $data['category']]);
1187            unset($data['category']);
1188        }
1189        return $this->get_plugin_generator('core_customfield')->create_field($data);
1190    }
1191
1192    /**
1193     * Create a new user, and enrol them in the specified course as the supplied role.
1194     *
1195     * @param   \stdClass   $course The course to enrol in
1196     * @param   string      $role The role to give within the course
1197     * @param   \stdClass   $userparams User parameters
1198     * @return  \stdClass   The created user
1199     */
1200    public function create_and_enrol($course, $role = 'student', $userparams = null, $enrol = 'manual',
1201            $timestart = 0, $timeend = 0, $status = null) {
1202        global $DB;
1203
1204        $user = $this->create_user($userparams);
1205        $roleid = $DB->get_field('role', 'id', ['shortname' => $role ]);
1206
1207        $this->enrol_user($user->id, $course->id, $roleid, $enrol, $timestart, $timeend, $status);
1208
1209        return $user;
1210    }
1211
1212    /**
1213     * Create a new last access record for a given user in a course.
1214     *
1215     * @param   \stdClass   $user The user
1216     * @param   \stdClass   $course The course the user accessed
1217     * @param   int         $timestamp The timestamp for when the user last accessed the course
1218     * @return  \stdClass   The user_lastaccess record
1219     */
1220    public function create_user_course_lastaccess(\stdClass $user, \stdClass $course, int $timestamp): \stdClass {
1221        global $DB;
1222
1223        $record = [
1224            'userid' => $user->id,
1225            'courseid' => $course->id,
1226            'timeaccess' => $timestamp,
1227        ];
1228
1229        $recordid = $DB->insert_record('user_lastaccess', $record);
1230
1231        return $DB->get_record('user_lastaccess', ['id' => $recordid], '*', MUST_EXIST);
1232    }
1233}
1234