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