MDL-69068 behat: New step to support singular generators
[moodle.git] / lib / behat / classes / behat_core_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 generators for acceptance testing.
19  *
20  * @package   core
21  * @category  test
22  * @copyright 2012 David MonllaĆ³
23  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
26 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
28 defined('MOODLE_INTERNAL') || die();
31 /**
32  * Behat data generator class for core entities.
33  *
34  * @package   core
35  * @category  test
36  * @copyright 2012 David MonllaĆ³
37  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class behat_core_generator extends behat_generator_base {
41     protected function get_creatable_entities(): array {
42         $entities = [
43             'users' => [
44                 'singular' => 'user',
45                 'datagenerator' => 'user',
46                 'required' => ['username'],
47             ],
48             'categories' => [
49                 'singular' => 'category',
50                 'datagenerator' => 'category',
51                 'required' => ['idnumber'],
52                 'switchids' => ['category' => 'parent'],
53             ],
54             'courses' => [
55                 'singular' => 'course',
56                 'datagenerator' => 'course',
57                 'required' => ['shortname'],
58                 'switchids' => ['category' => 'category'],
59             ],
60             'groups' => [
61                 'singular' => 'group',
62                 'datagenerator' => 'group',
63                 'required' => ['idnumber', 'course'],
64                 'switchids' => ['course' => 'courseid'],
65             ],
66             'groupings' => [
67                 'singular' => 'grouping',
68                 'datagenerator' => 'grouping',
69                 'required' => ['idnumber', 'course'],
70                 'switchids' => ['course' => 'courseid'],
71             ],
72             'course enrolments' => [
73                 'singular' => 'course enrolment',
74                 'datagenerator' => 'enrol_user',
75                 'required' => ['user', 'course', 'role'],
76                 'switchids' => ['user' => 'userid', 'course' => 'courseid', 'role' => 'roleid'],
77             ],
78             'custom field categories' => [
79                 'singular' => 'custom field category',
80                 'datagenerator' => 'custom_field_category',
81                 'required' => ['name', 'component', 'area', 'itemid'],
82                 'switchids' => [],
83             ],
84             'custom fields' => [
85                 'singular' => 'custom field',
86                 'datagenerator' => 'custom_field',
87                 'required' => ['name', 'category', 'type', 'shortname'],
88                 'switchids' => [],
89             ],
90             'permission overrides' => [
91                 'singular' => 'permission override',
92                 'datagenerator' => 'permission_override',
93                 'required' => ['capability', 'permission', 'role', 'contextlevel', 'reference'],
94                 'switchids' => ['role' => 'roleid'],
95             ],
96             'system role assigns' => [
97                 'singular' => 'system role assignment',
98                 'datagenerator' => 'system_role_assign',
99                 'required' => ['user', 'role'],
100                 'switchids' => ['user' => 'userid', 'role' => 'roleid'],
101             ],
102             'role assigns' => [
103                 'singular' => 'role assignment',
104                 'datagenerator' => 'role_assign',
105                 'required' => ['user', 'role', 'contextlevel', 'reference'],
106                 'switchids' => ['user' => 'userid', 'role' => 'roleid'],
107             ],
108             'activities' => [
109                 'singular' => 'activity',
110                 'datagenerator' => 'activity',
111                 'required' => ['activity', 'idnumber', 'course'],
112                 'switchids' => ['course' => 'course', 'gradecategory' => 'gradecat', 'grouping' => 'groupingid'],
113             ],
114             'blocks' => [
115                 'singular' => 'block',
116                 'datagenerator' => 'block_instance',
117                 'required' => ['blockname', 'contextlevel', 'reference'],
118             ],
119             'group members' => [
120                 'singular' => 'group member',
121                 'datagenerator' => 'group_member',
122                 'required' => ['user', 'group'],
123                 'switchids' => ['user' => 'userid', 'group' => 'groupid'],
124             ],
125             'grouping groups' => [
126                 'singular' => 'grouping group',
127                 'datagenerator' => 'grouping_group',
128                 'required' => ['grouping', 'group'],
129                 'switchids' => ['grouping' => 'groupingid', 'group' => 'groupid'],
130             ],
131             'cohorts' => [
132                 'singular' => 'cohort',
133                 'datagenerator' => 'cohort',
134                 'required' => ['idnumber'],
135             ],
136             'cohort members' => [
137                 'singular' => 'cohort member',
138                 'datagenerator' => 'cohort_member',
139                 'required' => ['user', 'cohort'],
140                 'switchids' => ['user' => 'userid', 'cohort' => 'cohortid'],
141             ],
142             'roles' => [
143                 'singular' => 'role',
144                 'datagenerator' => 'role',
145                 'required' => ['shortname'],
146             ],
147             'grade categories' => [
148                 'singular' => 'grade category',
149                 'datagenerator' => 'grade_category',
150                 'required' => ['fullname', 'course'],
151                 'switchids' => ['course' => 'courseid', 'gradecategory' => 'parent'],
152             ],
153             'grade items' => [
154                 'singular' => 'grade item',
155                 'datagenerator' => 'grade_item',
156                 'required' => ['course'],
157                 'switchids' => [
158                     'scale' => 'scaleid',
159                     'outcome' => 'outcomeid',
160                     'course' => 'courseid',
161                     'gradecategory' => 'categoryid',
162                 ],
163             ],
164             'grade outcomes' => [
165                 'singular' => 'grade outcome',
166                 'datagenerator' => 'grade_outcome',
167                 'required' => ['shortname', 'scale'],
168                 'switchids' => ['course' => 'courseid', 'gradecategory' => 'categoryid', 'scale' => 'scaleid'],
169             ],
170             'scales' => [
171                 'singular' => 'scale',
172                 'datagenerator' => 'scale',
173                 'required' => ['name', 'scale'],
174                 'switchids' => ['course' => 'courseid'],
175             ],
176             'question categories' => [
177                 'singular' => 'question category',
178                 'datagenerator' => 'question_category',
179                 'required' => ['name', 'contextlevel', 'reference'],
180                 'switchids' => ['questioncategory' => 'parent'],
181             ],
182             'questions' => [
183                 'singular' => 'question',
184                 'datagenerator' => 'question',
185                 'required' => ['qtype', 'questioncategory', 'name'],
186                 'switchids' => ['questioncategory' => 'category', 'user' => 'createdby'],
187             ],
188             'tags' => [
189                 'singular' => 'tag',
190                 'datagenerator' => 'tag',
191                 'required' => ['name'],
192             ],
193             'events' => [
194                 'singular' => 'event',
195                 'datagenerator' => 'event',
196                 'required' => ['name', 'eventtype'],
197                 'switchids' => [
198                     'user' => 'userid',
199                     'course' => 'courseid',
200                     'category' => 'categoryid',
201                 ],
202             ],
203             'message contacts' => [
204                 'singular' => 'message contact',
205                 'datagenerator' => 'message_contacts',
206                 'required' => ['user', 'contact'],
207                 'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
208             ],
209             'private messages' => [
210                 'singular' => 'private message',
211                 'datagenerator' => 'private_messages',
212                 'required' => ['user', 'contact', 'message'],
213                 'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
214             ],
215             'favourite conversations' => [
216                 'singular' => 'favourite conversation',
217                 'datagenerator' => 'favourite_conversations',
218                 'required' => ['user', 'contact'],
219                 'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
220             ],
221             'group messages' => [
222                 'singular' => 'group message',
223                 'datagenerator' => 'group_messages',
224                 'required' => ['user', 'group', 'message'],
225                 'switchids' => ['user' => 'userid', 'group' => 'groupid'],
226             ],
227             'muted group conversations' => [
228                 'singular' => 'muted group conversation',
229                 'datagenerator' => 'mute_group_conversations',
230                 'required' => ['user', 'group', 'course'],
231                 'switchids' => ['user' => 'userid', 'group' => 'groupid', 'course' => 'courseid'],
232             ],
233             'muted private conversations' => [
234                 'singular' => 'muted private conversation',
235                 'datagenerator' => 'mute_private_conversations',
236                 'required' => ['user', 'contact'],
237                 'switchids' => ['user' => 'userid', 'contact' => 'contactid'],
238             ],
239             'language customisations' => [
240                 'singular' => 'language customisation',
241                 'datagenerator' => 'customlang',
242                 'required' => ['component', 'stringid', 'value'],
243             ],
244             'analytics models' => [
245                 'singular' => 'analytics model',
246                 'datagenerator' => 'analytics_model',
247                 'required' => ['target', 'indicators', 'timesplitting', 'enabled'],
248             ],
249             'user preferences' => [
250                 'singular' => 'user preference',
251                 'datagenerator' => 'user_preferences',
252                 'required' => array('user', 'preference', 'value'),
253                 'switchids' => array('user' => 'userid'),
254             ],
255             'contentbank contents' => [
256                 'singular' => 'contentbank content',
257                 'datagenerator' => 'contentbank_content',
258                 'required' => array('contextlevel', 'reference', 'contenttype', 'user', 'contentname'),
259                 'switchids' => array('user' => 'userid')
260             ],
261             'badge external backpacks' => [
262                 'singular' => 'badge external backpack',
263                 'datagenerator' => 'badge_external_backpack',
264                 'required' => ['backpackapiurl', 'backpackweburl', 'apiversion']
265             ],
266             'setup backpacks connected' => [
267                 'singular' => 'setup backpack connected',
268                 'datagenerator' => 'setup_backpack_connected',
269                 'required' => ['user', 'externalbackpack'],
270                 'switchids' => ['user' => 'userid', 'externalbackpack' => 'externalbackpackid']
271             ],
272             'last access times' => [
273                 'singular' => 'last access time',
274                 'datagenerator' => 'last_access_times',
275                 'required' => ['user', 'course', 'lastaccess'],
276                 'switchids' => ['user' => 'userid', 'course' => 'courseid'],
277             ],
278         ];
280         return $entities;
281     }
283     /**
284      * Remove any empty custom fields, to avoid errors when creating the course.
285      *
286      * @param array $data
287      * @return array
288      */
289     protected function preprocess_course($data) {
290         foreach ($data as $fieldname => $value) {
291             if ($value === '' && strpos($fieldname, 'customfield_') === 0) {
292                 unset($data[$fieldname]);
293             }
294         }
295         return $data;
296     }
298     /**
299      * If password is not set it uses the username.
300      *
301      * @param array $data
302      * @return array
303      */
304     protected function preprocess_user($data) {
305         if (!isset($data['password'])) {
306             $data['password'] = $data['username'];
307         }
308         return $data;
309     }
311     /**
312      * If contextlevel and reference are specified for cohort, transform them to the contextid.
313      *
314      * @param array $data
315      * @return array
316      */
317     protected function preprocess_cohort($data) {
318         if (isset($data['contextlevel'])) {
319             if (!isset($data['reference'])) {
320                 throw new Exception('If field contextlevel is specified, field reference must also be present');
321             }
322             $context = $this->get_context($data['contextlevel'], $data['reference']);
323             unset($data['contextlevel']);
324             unset($data['reference']);
325             $data['contextid'] = $context->id;
326         }
327         return $data;
328     }
330     /**
331      * Preprocesses the creation of a grade item. Converts gradetype text to a number.
332      *
333      * @param array $data
334      * @return array
335      */
336     protected function preprocess_grade_item($data) {
337         global $CFG;
338         require_once("$CFG->libdir/grade/constants.php");
340         if (isset($data['gradetype'])) {
341             $data['gradetype'] = constant("GRADE_TYPE_" . strtoupper($data['gradetype']));
342         }
344         if (!empty($data['category']) && !empty($data['courseid'])) {
345             $cat = grade_category::fetch(array('fullname' => $data['category'], 'courseid' => $data['courseid']));
346             if (!$cat) {
347                 throw new Exception('Could not resolve category with name "' . $data['category'] . '"');
348             }
349             unset($data['category']);
350             $data['categoryid'] = $cat->id;
351         }
353         return $data;
354     }
356     /**
357      * Adapter to modules generator.
358      *
359      * @throws Exception Custom exception for test writers
360      * @param array $data
361      * @return void
362      */
363     protected function process_activity($data) {
364         global $DB, $CFG;
366         // The the_following_exists() method checks that the field exists.
367         $activityname = $data['activity'];
368         unset($data['activity']);
370         // Convert scale name into scale id (negative number indicates using scale).
371         if (isset($data['grade']) && strlen($data['grade']) && !is_number($data['grade'])) {
372             $data['grade'] = - $this->get_scale_id($data['grade']);
373             require_once("$CFG->libdir/grade/constants.php");
375             if (!isset($data['gradetype'])) {
376                 $data['gradetype'] = GRADE_TYPE_SCALE;
377             }
378         }
380         // We split $data in the activity $record and the course module $options.
381         $cmoptions = array();
382         $cmcolumns = $DB->get_columns('course_modules');
383         foreach ($cmcolumns as $key => $value) {
384             if (isset($data[$key])) {
385                 $cmoptions[$key] = $data[$key];
386             }
387         }
389         // Custom exception.
390         try {
391             $this->datagenerator->create_module($activityname, $data, $cmoptions);
392         } catch (coding_exception $e) {
393             throw new Exception('\'' . $activityname . '\' activity can not be added using this step,' .
394                     ' use the step \'I add a "ACTIVITY_OR_RESOURCE_NAME_STRING" to section "SECTION_NUMBER"\' instead');
395         }
396     }
398     /**
399      * Add a block to a page.
400      *
401      * @param array $data should mostly match the fields of the block_instances table.
402      *     The block type is specified by blockname.
403      *     The parentcontextid is set from contextlevel and reference.
404      *     Missing values are filled in by testing_block_generator::prepare_record.
405      *     $data is passed to create_block as both $record and $options. Normally
406      *     the keys are different, so this is a way to let people set values in either place.
407      */
408     protected function process_block_instance($data) {
410         if (empty($data['blockname'])) {
411             throw new Exception('\'blocks\' requires the field \'block\' type to be specified');
412         }
414         if (empty($data['contextlevel'])) {
415             throw new Exception('\'blocks\' requires the field \'contextlevel\' to be specified');
416         }
418         if (!isset($data['reference'])) {
419             throw new Exception('\'blocks\' requires the field \'reference\' to be specified');
420         }
422         $context = $this->get_context($data['contextlevel'], $data['reference']);
423         $data['parentcontextid'] = $context->id;
425         // Pass $data as both $record and $options. I think that is unlikely to
426         // cause problems since the relevant key names are different.
427         // $options is not used in most blocks I have seen, but where it is, it is necessary.
428         $this->datagenerator->create_block($data['blockname'], $data, $data);
429     }
431     /**
432      * Creates language customisation.
433      *
434      * @throws Exception
435      * @throws dml_exception
436      * @param array $data
437      * @return void
438      */
439     protected function process_customlang($data) {
440         global $CFG, $DB, $USER;
442         require_once($CFG->dirroot . '/' . $CFG->admin . '/tool/customlang/locallib.php');
443         require_once($CFG->libdir . '/adminlib.php');
445         if (empty($data['component'])) {
446             throw new Exception('\'customlang\' requires the field \'component\' type to be specified');
447         }
449         if (empty($data['stringid'])) {
450             throw new Exception('\'customlang\' requires the field \'stringid\' to be specified');
451         }
453         if (!isset($data['value'])) {
454             throw new Exception('\'customlang\' requires the field \'value\' to be specified');
455         }
457         $now = time();
459         tool_customlang_utils::checkout($USER->lang);
461         $record = $DB->get_record_sql("SELECT s.*
462                                          FROM {tool_customlang} s
463                                          JOIN {tool_customlang_components} c ON s.componentid = c.id
464                                         WHERE c.name = ? AND s.lang = ? AND s.stringid = ?",
465                 array($data['component'], $USER->lang, $data['stringid']));
467         if (empty($data['value']) && !is_null($record->local)) {
468             $record->local = null;
469             $record->modified = 1;
470             $record->outdated = 0;
471             $record->timecustomized = null;
472             $DB->update_record('tool_customlang', $record);
473             tool_customlang_utils::checkin($USER->lang);
474         }
476         if (!empty($data['value']) && $data['value'] != $record->local) {
477             $record->local = $data['value'];
478             $record->modified = 1;
479             $record->outdated = 0;
480             $record->timecustomized = $now;
481             $DB->update_record('tool_customlang', $record);
482             tool_customlang_utils::checkin($USER->lang);
483         }
484     }
486     /**
487      * Adapter to enrol_user() data generator.
488      *
489      * @throws Exception
490      * @param array $data
491      * @return void
492      */
493     protected function process_enrol_user($data) {
494         global $SITE;
496         if (empty($data['roleid'])) {
497             throw new Exception('\'course enrolments\' requires the field \'role\' to be specified');
498         }
500         if (!isset($data['userid'])) {
501             throw new Exception('\'course enrolments\' requires the field \'user\' to be specified');
502         }
504         if (!isset($data['courseid'])) {
505             throw new Exception('\'course enrolments\' requires the field \'course\' to be specified');
506         }
508         if (!isset($data['enrol'])) {
509             $data['enrol'] = 'manual';
510         }
512         if (!isset($data['timestart'])) {
513             $data['timestart'] = 0;
514         }
516         if (!isset($data['timeend'])) {
517             $data['timeend'] = 0;
518         }
520         if (!isset($data['status'])) {
521             $data['status'] = null;
522         }
524         // If the provided course shortname is the site shortname we consider it a system role assign.
525         if ($data['courseid'] == $SITE->id) {
526             // Frontpage course assign.
527             $context = context_course::instance($data['courseid']);
528             role_assign($data['roleid'], $data['userid'], $context->id);
530         } else {
531             // Course assign.
532             $this->datagenerator->enrol_user($data['userid'], $data['courseid'], $data['roleid'], $data['enrol'],
533                     $data['timestart'], $data['timeend'], $data['status']);
534         }
536     }
538     /**
539      * Allows/denies a capability at the specified context
540      *
541      * @throws Exception
542      * @param array $data
543      * @return void
544      */
545     protected function process_permission_override($data) {
547         // Will throw an exception if it does not exist.
548         $context = $this->get_context($data['contextlevel'], $data['reference']);
550         switch ($data['permission']) {
551             case get_string('allow', 'role'):
552                 $permission = CAP_ALLOW;
553                 break;
554             case get_string('prevent', 'role'):
555                 $permission = CAP_PREVENT;
556                 break;
557             case get_string('prohibit', 'role'):
558                 $permission = CAP_PROHIBIT;
559                 break;
560             default:
561                 throw new Exception('The \'' . $data['permission'] . '\' permission does not exist');
562                 break;
563         }
565         if (is_null(get_capability_info($data['capability']))) {
566             throw new Exception('The \'' . $data['capability'] . '\' capability does not exist');
567         }
569         role_change_permission($data['roleid'], $context, $data['capability'], $permission);
570     }
572     /**
573      * Assigns a role to a user at system context
574      *
575      * Used by "system role assigns" can be deleted when
576      * system role assign will be deprecated in favour of
577      * "role assigns"
578      *
579      * @throws Exception
580      * @param array $data
581      * @return void
582      */
583     protected function process_system_role_assign($data) {
585         if (empty($data['roleid'])) {
586             throw new Exception('\'system role assigns\' requires the field \'role\' to be specified');
587         }
589         if (!isset($data['userid'])) {
590             throw new Exception('\'system role assigns\' requires the field \'user\' to be specified');
591         }
593         $context = context_system::instance();
595         $this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id);
596     }
598     /**
599      * Assigns a role to a user at the specified context
600      *
601      * @throws Exception
602      * @param array $data
603      * @return void
604      */
605     protected function process_role_assign($data) {
607         if (empty($data['roleid'])) {
608             throw new Exception('\'role assigns\' requires the field \'role\' to be specified');
609         }
611         if (!isset($data['userid'])) {
612             throw new Exception('\'role assigns\' requires the field \'user\' to be specified');
613         }
615         if (empty($data['contextlevel'])) {
616             throw new Exception('\'role assigns\' requires the field \'contextlevel\' to be specified');
617         }
619         if (!isset($data['reference'])) {
620             throw new Exception('\'role assigns\' requires the field \'reference\' to be specified');
621         }
623         // Getting the context id.
624         $context = $this->get_context($data['contextlevel'], $data['reference']);
626         $this->datagenerator->role_assign($data['roleid'], $data['userid'], $context->id);
627     }
629     /**
630      * Creates a role.
631      *
632      * @param array $data
633      * @return void
634      */
635     protected function process_role($data) {
637         // We require the user to fill the role shortname.
638         if (empty($data['shortname'])) {
639             throw new Exception('\'role\' requires the field \'shortname\' to be specified');
640         }
642         $this->datagenerator->create_role($data);
643     }
645     /**
646      * Adds members to cohorts
647      *
648      * @param array $data
649      * @return void
650      */
651     protected function process_cohort_member($data) {
652         cohort_add_member($data['cohortid'], $data['userid']);
653     }
655     /**
656      * Create a question category.
657      *
658      * @param array $data the row of data from the behat script.
659      */
660     protected function process_question_category($data) {
661         global $DB;
663         $context = $this->get_context($data['contextlevel'], $data['reference']);
665         // The way this class works, we have already looked up the given parent category
666         // name and found a matching category. However, it is possible, particularly
667         // for the 'top' category, for there to be several categories with the
668         // same name. So far one will have been picked at random, but we need
669         // the one from the right context. So, if we have the wrong category, try again.
670         // (Just fixing it here, rather than getting it right first time, is a bit
671         // of a bodge, but in general this class assumes that names are unique,
672         // and normally they are, so this was the easiest fix.)
673         if (!empty($data['parent'])) {
674             $foundparent = $DB->get_record('question_categories', ['id' => $data['parent']], '*', MUST_EXIST);
675             if ($foundparent->contextid != $context->id) {
676                 $rightparentid = $DB->get_field('question_categories', 'id',
677                         ['contextid' => $context->id, 'name' => $foundparent->name]);
678                 if (!$rightparentid) {
679                     throw new Exception('The specified question category with name "' . $foundparent->name .
680                             '" does not exist in context "' . $context->get_context_name() . '"."');
681                 }
682                 $data['parent'] = $rightparentid;
683             }
684         }
686         $data['contextid'] = $context->id;
687         $this->datagenerator->get_plugin_generator('core_question')->create_question_category($data);
688     }
690     /**
691      * Create a question.
692      *
693      * Creating questions relies on the question/type/.../tests/helper.php mechanism.
694      * We start with test_question_maker::get_question_form_data($data['qtype'], $data['template'])
695      * and then overlay the values from any other fields of $data that are set.
696      *
697      * There is a special case that allows you to set qtype to 'missingtype'.
698      * This creates an example of broken question, such as you might get if you
699      * install a question type, create some questions of that type, and then
700      * uninstall the question type (which is prevented through the UI but can
701      * still happen). This special lets tests verify that these questions are
702      * handled OK.
703      *
704      * @param array $data the row of data from the behat script.
705      */
706     protected function process_question($data) {
707         global $DB;
709         if (array_key_exists('questiontext', $data)) {
710             $data['questiontext'] = array(
711                     'text'   => $data['questiontext'],
712                     'format' => FORMAT_HTML,
713             );
714         }
716         if (array_key_exists('generalfeedback', $data)) {
717             $data['generalfeedback'] = array(
718                     'text'   => $data['generalfeedback'],
719                     'format' => FORMAT_HTML,
720             );
721         }
723         $which = null;
724         if (!empty($data['template'])) {
725             $which = $data['template'];
726         }
728         $missingtypespecialcase = false;
729         if ($data['qtype'] === 'missingtype') {
730             $data['qtype'] = 'essay'; // Actual type uses here does not matter. We just need any question.
731             $missingtypespecialcase = true;
732         }
734         $questiondata = $this->datagenerator->get_plugin_generator('core_question')
735             ->create_question($data['qtype'], $which, $data);
737         if ($missingtypespecialcase) {
738             $DB->set_field('question', 'qtype', 'unknownqtype', ['id' => $questiondata->id]);
739         }
740     }
742     /**
743      * Adds user to contacts
744      *
745      * @param array $data
746      * @return void
747      */
748     protected function process_message_contacts($data) {
749         \core_message\api::add_contact($data['userid'], $data['contactid']);
750     }
752     /**
753      * Send a new message from user to contact in a private conversation
754      *
755      * @param array $data
756      * @return void
757      */
758     protected function process_private_messages(array $data) {
759         if (empty($data['format'])) {
760             $data['format'] = 'FORMAT_PLAIN';
761         }
763         if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) {
764             $conversation = \core_message\api::create_conversation(
765                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
766                     [$data['userid'], $data['contactid']]
767             );
768             $conversationid = $conversation->id;
769         }
770         \core_message\api::send_message_to_conversation(
771                 $data['userid'],
772                 $conversationid,
773                 $data['message'],
774                 constant($data['format'])
775         );
776     }
778     /**
779      * Send a new message from user to a group conversation
780      *
781      * @param array $data
782      * @return void
783      */
784     protected function process_group_messages(array $data) {
785         global $DB;
787         if (empty($data['format'])) {
788             $data['format'] = 'FORMAT_PLAIN';
789         }
791         $group = $DB->get_record('groups', ['id' => $data['groupid']]);
792         $coursecontext = context_course::instance($group->courseid);
793         if (!$conversation = \core_message\api::get_conversation_by_area('core_group', 'groups', $data['groupid'],
794                 $coursecontext->id)) {
795             $members = $DB->get_records_menu('groups_members', ['groupid' => $data['groupid']], '', 'userid, id');
796             $conversation = \core_message\api::create_conversation(
797                     \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
798                     array_keys($members),
799                     $group->name,
800                     \core_message\api::MESSAGE_CONVERSATION_ENABLED,
801                     'core_group',
802                     'groups',
803                     $group->id,
804                     $coursecontext->id);
805         }
806         \core_message\api::send_message_to_conversation(
807                 $data['userid'],
808                 $conversation->id,
809                 $data['message'],
810                 constant($data['format'])
811         );
812     }
814     /**
815      * Mark a private conversation as favourite for user
816      *
817      * @param array $data
818      * @return void
819      */
820     protected function process_favourite_conversations(array $data) {
821         if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) {
822             $conversation = \core_message\api::create_conversation(
823                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
824                     [$data['userid'], $data['contactid']]
825             );
826             $conversationid = $conversation->id;
827         }
828         \core_message\api::set_favourite_conversation($conversationid, $data['userid']);
829     }
831     /**
832      * Mute an existing group conversation for user
833      *
834      * @param array $data
835      * @return void
836      */
837     protected function process_mute_group_conversations(array $data) {
838         if (groups_is_member($data['groupid'], $data['userid'])) {
839             $context = context_course::instance($data['courseid']);
840             $conversation = \core_message\api::get_conversation_by_area(
841                     'core_group',
842                     'groups',
843                     $data['groupid'],
844                     $context->id
845             );
846             if ($conversation) {
847                 \core_message\api::mute_conversation($data['userid'], $conversation->id);
848             }
849         }
850     }
852     /**
853      * Mute a private conversation for user
854      *
855      * @param array $data
856      * @return void
857      */
858     protected function process_mute_private_conversations(array $data) {
859         if (!$conversationid = \core_message\api::get_conversation_between_users([$data['userid'], $data['contactid']])) {
860             $conversation = \core_message\api::create_conversation(
861                     \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
862                     [$data['userid'], $data['contactid']]
863             );
864             $conversationid = $conversation->id;
865         }
866         \core_message\api::mute_conversation($data['userid'], $conversationid);
867     }
869     /**
870      * Transform indicators string into array.
871      *
872      * @param array $data
873      * @return array
874      */
875     protected function preprocess_analytics_model($data) {
876         $data['indicators'] = explode(',', $data['indicators']);
877         return $data;
878     }
880     /**
881      * Creates an analytics model
882      *
883      * @param target $data
884      * @return void
885      */
886     protected function process_analytics_model($data) {
887         \core_analytics\manager::create_declared_model($data);
888     }
890     /**
891      * Set a preference value for user
892      *
893      * @param array $data
894      * @return void
895      */
896     protected function process_user_preferences(array $data) {
897         set_user_preference($data['preference'], $data['value'], $data['userid']);
898     }
900     /**
901      * Create content in the given context's content bank.
902      *
903      * @param array $data
904      * @return void
905      */
906     protected function process_contentbank_content(array $data) {
907         global $CFG;
909         if (empty($data['contextlevel'])) {
910             throw new Exception('contentbank_content requires the field contextlevel to be specified');
911         }
913         if (!isset($data['reference'])) {
914             throw new Exception('contentbank_content requires the field reference to be specified');
915         }
917         if (empty($data['contenttype'])) {
918             throw new Exception('contentbank_content requires the field contenttype to be specified');
919         }
921         $contenttypeclass = "\\".$data['contenttype']."\\contenttype";
922         if (class_exists($contenttypeclass)) {
923             $context = $this->get_context($data['contextlevel'], $data['reference']);
924             $contenttype = new $contenttypeclass($context);
925             $record = new stdClass();
926             $record->usercreated = $data['userid'];
927             $record->name = $data['contentname'];
928             $content = $contenttype->create_content($record);
930             if (!empty($data['filepath'])) {
931                 $filename = basename($data['filepath']);
932                 $fs = get_file_storage();
933                 $filerecord = array(
934                     'component' => 'contentbank',
935                     'filearea' => 'public',
936                     'contextid' => $context->id,
937                     'userid' => $data['userid'],
938                     'itemid' => $content->get_id(),
939                     'filename' => $filename,
940                     'filepath' => '/'
941                 );
942                 $fs->create_file_from_pathname($filerecord, $CFG->dirroot . $data['filepath']);
943             }
944         } else {
945             throw new Exception('The specified "' . $data['contenttype'] . '" contenttype does not exist');
946         }
947     }
949     /**
950      * Create a exetrnal backpack.
951      *
952      * @param array $data
953      */
954     protected function process_badge_external_backpack(array $data) {
955         global $DB;
956         $DB->insert_record('badge_external_backpack', $data, true);
957     }
959     /**
960      * Setup a backpack connected for user.
961      *
962      * @param array $data
963      * @throws dml_exception
964      */
965     protected function process_setup_backpack_connected(array $data) {
966         global $DB;
968         if (empty($data['userid'])) {
969             throw new Exception('\'setup backpack connected\' requires the field \'user\' to be specified');
970         }
971         if (empty($data['externalbackpackid'])) {
972             throw new Exception('\'setup backpack connected\' requires the field \'externalbackpack\' to be specified');
973         }
974         // Dummy badge_backpack_oauth2 data.
975         $timenow = time();
976         $backpackoauth2 = new stdClass();
977         $backpackoauth2->usermodified = $data['userid'];
978         $backpackoauth2->timecreated = $timenow;
979         $backpackoauth2->timemodified = $timenow;
980         $backpackoauth2->userid = $data['userid'];
981         $backpackoauth2->issuerid = 1;
982         $backpackoauth2->externalbackpackid = $data['externalbackpackid'];
983         $backpackoauth2->token = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
984         $backpackoauth2->refreshtoken = '0123456789abcdefghijk';
985         $backpackoauth2->expires = $timenow + 3600;
986         $backpackoauth2->scope = 'https://purl.imsglobal.org/spec/ob/v2p1/scope/assertion.create';
987         $backpackoauth2->scope .= ' https://purl.imsglobal.org/spec/ob/v2p1/scope/assertion.readonly offline_access';
988         $DB->insert_record('badge_backpack_oauth2', $backpackoauth2);
990         // Dummy badge_backpack data.
991         $backpack = new stdClass();
992         $backpack->userid = $data['userid'];
993         $backpack->email = 'student@behat.moodle';
994         $backpack->backpackuid = 0;
995         $backpack->autosync = 0;
996         $backpack->password = '';
997         $backpack->externalbackpackid = $data['externalbackpackid'];
998         $DB->insert_record('badge_backpack', $backpack);
999     }
1001     /**
1002      * Creates user last access data within given courses.
1003      *
1004      * @param array $data
1005      * @return void
1006      */
1007     protected function process_last_access_times(array $data) {
1008         global $DB;
1010         if (!isset($data['userid'])) {
1011             throw new Exception('\'last acces times\' requires the field \'user\' to be specified');
1012         }
1014         if (!isset($data['courseid'])) {
1015             throw new Exception('\'last acces times\' requires the field \'course\' to be specified');
1016         }
1018         if (!isset($data['lastaccess'])) {
1019             throw new Exception('\'last acces times\' requires the field \'lastaccess\' to be specified');
1020         }
1022         $userdata = [];
1023         $userdata['old'] = $DB->get_record('user', ['id' => $data['userid']], 'firstaccess, lastaccess, lastlogin, currentlogin');
1024         $userdata['new'] = [
1025             'firstaccess' => $userdata['old']->firstaccess,
1026             'lastaccess' => $userdata['old']->lastaccess,
1027             'lastlogin' => $userdata['old']->lastlogin,
1028             'currentlogin' => $userdata['old']->currentlogin,
1029         ];
1031         // Check for lastaccess data for this course.
1032         $lastaccessdata = [
1033             'userid' => $data['userid'],
1034             'courseid' => $data['courseid'],
1035         ];
1037         $lastaccessid = $DB->get_field('user_lastaccess', 'id', $lastaccessdata);
1039         $dbdata = (object) $lastaccessdata;
1040         $dbdata->timeaccess = $data['lastaccess'];
1042         // Set the course last access time.
1043         if ($lastaccessid) {
1044             $dbdata->id = $lastaccessid;
1045             $DB->update_record('user_lastaccess', $dbdata);
1046         } else {
1047             $DB->insert_record('user_lastaccess', $dbdata);
1048         }
1050         // Store changes to other user access times as needed.
1052         // Update first access if this is the user's first login, or this access is earlier than their current first access.
1053         if (empty($userdata['new']['firstaccess']) ||
1054                 $userdata['new']['firstaccess'] > $data['lastaccess']) {
1055             $userdata['new']['firstaccess'] = $data['lastaccess'];
1056         }
1058         // Update last access if it is the user's most recent access.
1059         if (empty($userdata['new']['lastaccess']) ||
1060                 $userdata['new']['lastaccess'] < $data['lastaccess']) {
1061             $userdata['new']['lastaccess'] = $data['lastaccess'];
1062         }
1064         // Update last and current login if it is the user's most recent access.
1065         if (empty($userdata['new']['lastlogin']) ||
1066                 $userdata['new']['lastlogin'] < $data['lastaccess']) {
1067             $userdata['new']['lastlogin'] = $data['lastaccess'];
1068             $userdata['new']['currentlogin'] = $data['lastaccess'];
1069         }
1071         $updatedata = [];
1073         if ($userdata['new']['firstaccess'] != $userdata['old']->firstaccess) {
1074             $updatedata['firstaccess'] = $userdata['new']['firstaccess'];
1075         }
1077         if ($userdata['new']['lastaccess'] != $userdata['old']->lastaccess) {
1078             $updatedata['lastaccess'] = $userdata['new']['lastaccess'];
1079         }
1081         if ($userdata['new']['lastlogin'] != $userdata['old']->lastlogin) {
1082             $updatedata['lastlogin'] = $userdata['new']['lastlogin'];
1083         }
1085         if ($userdata['new']['currentlogin'] != $userdata['old']->currentlogin) {
1086             $updatedata['currentlogin'] = $userdata['new']['currentlogin'];
1087         }
1089         // Only update user access data if there have been any changes.
1090         if (!empty($updatedata)) {
1091             $updatedata['id'] = $data['userid'];
1092             $updatedata = (object) $updatedata;
1093             $DB->update_record('user', $updatedata);
1094         }
1095     }