Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

   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  
  26  defined('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   */
  36  class testing_data_generator {
  37      /** @var int The number of grade categories created */
  38      protected $gradecategorycounter = 0;
  39      /** @var int The number of grade items created */
  40      protected $gradeitemcounter = 0;
  41      /** @var int The number of grade outcomes created */
  42      protected $gradeoutcomecounter = 0;
  43      protected $usercounter = 0;
  44      protected $categorycount = 0;
  45      protected $cohortcount = 0;
  46      protected $coursecount = 0;
  47      protected $scalecount = 0;
  48      protected $groupcount = 0;
  49      protected $groupingcount = 0;
  50      protected $rolecount = 0;
  51      protected $tagcount = 0;
  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
  77  Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Nulla non arcu lacinia neque faucibus fringilla. Vivamus porttitor turpis ac leo. Integer in sapien. Nullam eget nisl. Aliquam erat volutpat. Cras elementum. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Integer malesuada. Nullam lectus justo, vulputate eget mollis sed, tempor sed magna. Mauris elementum mauris vitae tortor. Aliquam erat volutpat.
  78  Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Pellentesque ipsum. Cras pede libero, dapibus nec, pretium sit amet, tempor quis. Aliquam ante. Proin in tellus sit amet nibh dignissim sagittis. Vivamus porttitor turpis ac leo. Duis bibendum, lectus ut viverra rhoncus, dolor nunc faucibus libero, eget facilisis enim ipsum id lacus. In sem justo, commodo ut, suscipit at, pharetra vitae, orci. Aliquam erat volutpat. Nulla est.
  79  Vivamus luctus egestas leo. Aenean fermentum risus id tortor. Mauris dictum facilisis augue. Aliquam erat volutpat. Aliquam ornare wisi eu metus. Aliquam id dolor. Duis condimentum augue id magna semper rutrum. Donec iaculis gravida nulla. Pellentesque ipsum. Etiam dictum tincidunt diam. Quisque tincidunt scelerisque libero. Etiam egestas wisi a erat.
  80  Integer lacinia. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris tincidunt sem sed arcu. Nullam feugiat, turpis at pulvinar vulputate, erat libero tristique tellus, nec bibendum odio risus sit amet ante. Aliquam id dolor. Maecenas sollicitudin. Et harum quidem rerum facilis est et expedita distinctio. Mauris suscipit, ligula sit amet pharetra semper, nibh ante cursus purus, vel sagittis velit mauris vel metus. Nullam dapibus fermentum ipsum. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Pellentesque sapien. Duis risus. Mauris elementum mauris vitae tortor. Suspendisse nisl. Integer rutrum, orci vestibulum ullamcorper ultricies, lacus quam ultricies odio, vitae placerat pede sem sit amet enim.
  81  In laoreet, magna id viverra tincidunt, sem odio bibendum justo, vel imperdiet sapien wisi sed libero. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Nullam justo enim, consectetuer nec, ullamcorper ac, vestibulum in, elit. Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur? Maecenas lorem. Etiam posuere lacus quis dolor. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos hymenaeos. Curabitur ligula sapien, pulvinar a vestibulum quis, facilisis vel sapien. Nam sed tellus id magna elementum tincidunt. Suspendisse nisl. Vivamus luctus egestas leo. Nulla non arcu lacinia neque faucibus fringilla. Etiam dui sem, fermentum vitae, sagittis id, malesuada in, quam. Etiam dictum tincidunt diam. Etiam commodo dui eget wisi. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Proin pede metus, vulputate nec, fermentum fringilla, vehicula vitae, justo. Duis ante orci, molestie vitae vehicula venenatis, tincidunt ac pede. Pellentesque sapien.
  82  EOD;
  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              $this->generators[$component] = $this->get_default_plugin_generator($component);
 125  
 126              return $this->generators[$component];
 127          }
 128  
 129          include_once($lib);
 130          $classname = $component . '_generator';
 131  
 132          if (class_exists($classname)) {
 133              $this->generators[$component] = new $classname($this);
 134          } else {
 135              $this->generators[$component] = $this->get_default_plugin_generator($component, $classname);
 136          }
 137  
 138          return $this->generators[$component];
 139      }
 140  
 141      /**
 142       * Create a test user
 143       * @param array|stdClass $record
 144       * @param array $options
 145       * @return stdClass user record
 146       */
 147      public function create_user($record=null, array $options=null) {
 148          global $DB, $CFG;
 149          require_once($CFG->dirroot.'/user/lib.php');
 150  
 151          $this->usercounter++;
 152          $i = $this->usercounter;
 153  
 154          $record = (array)$record;
 155  
 156          if (!isset($record['auth'])) {
 157              $record['auth'] = 'manual';
 158          }
 159  
 160          if (!isset($record['firstname']) and !isset($record['lastname'])) {
 161              $country = rand(0, 5);
 162              $firstname = rand(0, 4);
 163              $lastname = rand(0, 4);
 164              $female = rand(0, 1);
 165              $record['firstname'] = $this->firstnames[($country*10) + $firstname + ($female*5)];
 166              $record['lastname'] = $this->lastnames[($country*10) + $lastname + ($female*5)];
 167  
 168          } else if (!isset($record['firstname'])) {
 169              $record['firstname'] = 'Firstname'.$i;
 170  
 171          } else if (!isset($record['lastname'])) {
 172              $record['lastname'] = 'Lastname'.$i;
 173          }
 174  
 175          if (!isset($record['firstnamephonetic'])) {
 176              $firstnamephonetic = rand(0, 59);
 177              $record['firstnamephonetic'] = $this->firstnames[$firstnamephonetic];
 178          }
 179  
 180          if (!isset($record['lastnamephonetic'])) {
 181              $lastnamephonetic = rand(0, 59);
 182              $record['lastnamephonetic'] = $this->lastnames[$lastnamephonetic];
 183          }
 184  
 185          if (!isset($record['middlename'])) {
 186              $middlename = rand(0, 59);
 187              $record['middlename'] = $this->firstnames[$middlename];
 188          }
 189  
 190          if (!isset($record['alternatename'])) {
 191              $alternatename = rand(0, 59);
 192              $record['alternatename'] = $this->firstnames[$alternatename];
 193          }
 194  
 195          if (!isset($record['idnumber'])) {
 196              $record['idnumber'] = '';
 197          }
 198  
 199          if (!isset($record['mnethostid'])) {
 200              $record['mnethostid'] = $CFG->mnet_localhost_id;
 201          }
 202  
 203          if (!isset($record['username'])) {
 204              $record['username'] = 'username'.$i;
 205              $j = 2;
 206              while ($DB->record_exists('user', array('username'=>$record['username'], 'mnethostid'=>$record['mnethostid']))) {
 207                  $record['username'] = 'username'.$i.'_'.$j;
 208                  $j++;
 209              }
 210          }
 211  
 212          if (isset($record['password'])) {
 213              $record['password'] = hash_internal_user_password($record['password']);
 214          }
 215  
 216          if (!isset($record['email'])) {
 217              $record['email'] = $record['username'].'@example.com';
 218          }
 219  
 220          if (!isset($record['confirmed'])) {
 221              $record['confirmed'] = 1;
 222          }
 223  
 224          if (!isset($record['lastip'])) {
 225              $record['lastip'] = '0.0.0.0';
 226          }
 227  
 228          $tobedeleted = !empty($record['deleted']);
 229          unset($record['deleted']);
 230  
 231          $userid = user_create_user($record, false, false);
 232  
 233          if ($extrafields = array_intersect_key($record, ['password' => 1, 'timecreated' => 1])) {
 234              $DB->update_record('user', ['id' => $userid] + $extrafields);
 235          }
 236  
 237          if (!$tobedeleted) {
 238              // All new not deleted users must have a favourite self-conversation.
 239              $selfconversation = \core_message\api::create_conversation(
 240                  \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
 241                  [$userid]
 242              );
 243              \core_message\api::set_favourite_conversation($selfconversation->id, $userid);
 244  
 245              // Save custom profile fields data.
 246              $hasprofilefields = array_filter($record, function($key){
 247                  return strpos($key, 'profile_field_') === 0;
 248              }, ARRAY_FILTER_USE_KEY);
 249              if ($hasprofilefields) {
 250                  require_once($CFG->dirroot.'/user/profile/lib.php');
 251                  $usernew = (object)(['id' => $userid] + $record);
 252                  profile_save_data($usernew);
 253              }
 254          }
 255  
 256          $user = $DB->get_record('user', array('id' => $userid), '*', MUST_EXIST);
 257  
 258          if (!$tobedeleted && isset($record['interests'])) {
 259              require_once($CFG->dirroot . '/user/editlib.php');
 260              if (!is_array($record['interests'])) {
 261                  $record['interests'] = preg_split('/\s*,\s*/', trim($record['interests']), -1, PREG_SPLIT_NO_EMPTY);
 262              }
 263              useredit_update_interests($user, $record['interests']);
 264          }
 265  
 266          \core\event\user_created::create_from_userid($userid)->trigger();
 267  
 268          if ($tobedeleted) {
 269              delete_user($user);
 270              $user = $DB->get_record('user', array('id' => $userid));
 271          }
 272          return $user;
 273      }
 274  
 275      /**
 276       * Create a test course category
 277       * @param array|stdClass $record
 278       * @param array $options
 279       * @return core_course_category course category record
 280       */
 281      public function create_category($record=null, array $options=null) {
 282          $this->categorycount++;
 283          $i = $this->categorycount;
 284  
 285          $record = (array)$record;
 286  
 287          if (!isset($record['name'])) {
 288              $record['name'] = 'Course category '.$i;
 289          }
 290  
 291          if (!isset($record['description'])) {
 292              $record['description'] = "Test course category $i\n$this->loremipsum";
 293          }
 294  
 295          if (!isset($record['idnumber'])) {
 296              $record['idnumber'] = '';
 297          }
 298  
 299          return core_course_category::create($record);
 300      }
 301  
 302      /**
 303       * Create test cohort.
 304       * @param array|stdClass $record
 305       * @param array $options
 306       * @return stdClass cohort record
 307       */
 308      public function create_cohort($record=null, array $options=null) {
 309          global $DB, $CFG;
 310          require_once("$CFG->dirroot/cohort/lib.php");
 311  
 312          $this->cohortcount++;
 313          $i = $this->cohortcount;
 314  
 315          $record = (array)$record;
 316  
 317          if (!isset($record['contextid'])) {
 318              $record['contextid'] = context_system::instance()->id;
 319          }
 320  
 321          if (!isset($record['name'])) {
 322              $record['name'] = 'Cohort '.$i;
 323          }
 324  
 325          if (!isset($record['idnumber'])) {
 326              $record['idnumber'] = '';
 327          }
 328  
 329          if (!isset($record['description'])) {
 330              $record['description'] = "Description for '{$record['name']}' \n$this->loremipsum";
 331          }
 332  
 333          if (!isset($record['descriptionformat'])) {
 334              $record['descriptionformat'] = FORMAT_MOODLE;
 335          }
 336  
 337          if (!isset($record['visible'])) {
 338              $record['visible'] = 1;
 339          }
 340  
 341          if (!isset($record['component'])) {
 342              $record['component'] = '';
 343          }
 344  
 345          $id = cohort_add_cohort((object)$record);
 346  
 347          return $DB->get_record('cohort', array('id'=>$id), '*', MUST_EXIST);
 348      }
 349  
 350      /**
 351       * Create a test course
 352       * @param array|stdClass $record
 353       * @param array $options with keys:
 354       *      'createsections'=>bool precreate all sections
 355       * @return stdClass course record
 356       */
 357      public function create_course($record=null, array $options=null) {
 358          global $DB, $CFG;
 359          require_once("$CFG->dirroot/course/lib.php");
 360  
 361          $this->coursecount++;
 362          $i = $this->coursecount;
 363  
 364          $record = (array)$record;
 365  
 366          if (!isset($record['fullname'])) {
 367              $record['fullname'] = 'Test course '.$i;
 368          }
 369  
 370          if (!isset($record['shortname'])) {
 371              $record['shortname'] = 'tc_'.$i;
 372          }
 373  
 374          if (!isset($record['idnumber'])) {
 375              $record['idnumber'] = '';
 376          }
 377  
 378          if (!isset($record['format'])) {
 379              $record['format'] = 'topics';
 380          }
 381  
 382          if (!isset($record['newsitems'])) {
 383              $record['newsitems'] = 0;
 384          }
 385  
 386          if (!isset($record['numsections'])) {
 387              $record['numsections'] = 5;
 388          }
 389  
 390          if (!isset($record['summary'])) {
 391              $record['summary'] = "Test course $i\n$this->loremipsum";
 392          }
 393  
 394          if (!isset($record['summaryformat'])) {
 395              $record['summaryformat'] = FORMAT_MOODLE;
 396          }
 397  
 398          if (!isset($record['category'])) {
 399              $record['category'] = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
 400          }
 401  
 402          if (!isset($record['startdate'])) {
 403              $record['startdate'] = usergetmidnight(time());
 404          }
 405  
 406          if (isset($record['tags']) && !is_array($record['tags'])) {
 407              $record['tags'] = preg_split('/\s*,\s*/', trim($record['tags']), -1, PREG_SPLIT_NO_EMPTY);
 408          }
 409  
 410          if (!empty($options['createsections']) && empty($record['numsections'])) {
 411              // Since Moodle 3.3 function create_course() automatically creates sections if numsections is specified.
 412              // For BC if 'createsections' is given but 'numsections' is not, assume the default value from config.
 413              $record['numsections'] = get_config('moodlecourse', 'numsections');
 414          }
 415  
 416          if (!empty($record['customfields'])) {
 417              foreach ($record['customfields'] as $field) {
 418                  $record['customfield_'.$field['shortname']] = $field['value'];
 419              }
 420          }
 421  
 422          $course = create_course((object)$record);
 423          context_course::instance($course->id);
 424  
 425          return $course;
 426      }
 427  
 428      /**
 429       * Create course section if does not exist yet
 430       * @param array|stdClass $record must contain 'course' and 'section' attributes
 431       * @param array|null $options
 432       * @return stdClass
 433       * @throws coding_exception
 434       */
 435      public function create_course_section($record = null, array $options = null) {
 436          global $DB;
 437  
 438          $record = (array)$record;
 439  
 440          if (empty($record['course'])) {
 441              throw new coding_exception('course must be present in testing_data_generator::create_course_section() $record');
 442          }
 443  
 444          if (!isset($record['section'])) {
 445              throw new coding_exception('section must be present in testing_data_generator::create_course_section() $record');
 446          }
 447  
 448          course_create_sections_if_missing($record['course'], $record['section']);
 449          return get_fast_modinfo($record['course'])->get_section_info($record['section']);
 450      }
 451  
 452      /**
 453       * Create a test block.
 454       *
 455       * The $record passed in becomes the basis for the new row added to the
 456       * block_instances table. You only need to supply the values of interest.
 457       * Any missing values have sensible defaults filled in, and ->blockname will be set based on $blockname.
 458       *
 459       * The $options array provides additional data, not directly related to what
 460       * will be inserted in the block_instance table, which may affect the block
 461       * that is created. The meanings of any data passed here depends on the particular
 462       * type of block being created.
 463       *
 464       * @param string $blockname the type of block to create. E.g. 'html'.
 465       * @param array|stdClass $record forms the basis for the entry to be inserted in the block_instances table.
 466       * @param array $options further, block-specific options to control how the block is created.
 467       * @return stdClass new block_instance record.
 468       */
 469      public function create_block($blockname, $record=null, array $options=array()) {
 470          $generator = $this->get_plugin_generator('block_'.$blockname);
 471          return $generator->create_instance($record, $options);
 472      }
 473  
 474      /**
 475       * Create a test activity module.
 476       *
 477       * The $record should contain the same data that you would call from
 478       * ->get_data() when the mod_[type]_mod_form is submitted, except that you
 479       * only need to supply values of interest. The only required value is
 480       * 'course'. Any missing values will have a sensible default supplied.
 481       *
 482       * The $options array provides additional data, not directly related to what
 483       * would come back from the module edit settings form, which may affect the activity
 484       * that is created. The meanings of any data passed here depends on the particular
 485       * type of activity being created.
 486       *
 487       * @param string $modulename the type of activity to create. E.g. 'forum' or 'quiz'.
 488       * @param array|stdClass $record data, as if from the module edit settings form.
 489       * @param array $options additional data that may affect how the module is created.
 490       * @return stdClass activity record new new record that was just inserted in the table
 491       *      like 'forum' or 'quiz', with a ->cmid field added.
 492       */
 493      public function create_module($modulename, $record=null, array $options=null) {
 494          $generator = $this->get_plugin_generator('mod_'.$modulename);
 495          return $generator->create_instance($record, $options);
 496      }
 497  
 498      /**
 499       * Create a test group for the specified course
 500       *
 501       * $record should be either an array or a stdClass containing infomation about the group to create.
 502       * At the very least it needs to contain courseid.
 503       * Default values are added for name, description, and descriptionformat if they are not present.
 504       *
 505       * This function calls groups_create_group() to create the group within the database.
 506       * @see groups_create_group
 507       * @param array|stdClass $record
 508       * @return stdClass group record
 509       */
 510      public function create_group($record) {
 511          global $DB, $CFG;
 512  
 513          require_once($CFG->dirroot . '/group/lib.php');
 514  
 515          $this->groupcount++;
 516          $i = str_pad($this->groupcount, 4, '0', STR_PAD_LEFT);
 517  
 518          $record = (array)$record;
 519  
 520          if (empty($record['courseid'])) {
 521              throw new coding_exception('courseid must be present in testing_data_generator::create_group() $record');
 522          }
 523  
 524          if (!isset($record['name'])) {
 525              $record['name'] = 'group-' . $i;
 526          }
 527  
 528          if (!isset($record['description'])) {
 529              $record['description'] = "Test Group $i\n{$this->loremipsum}";
 530          }
 531  
 532          if (!isset($record['descriptionformat'])) {
 533              $record['descriptionformat'] = FORMAT_MOODLE;
 534          }
 535  
 536          $id = groups_create_group((object)$record);
 537  
 538          // Allow tests to set group pictures.
 539          if (!empty($record['picturepath'])) {
 540              require_once($CFG->dirroot . '/lib/gdlib.php');
 541              $grouppicture = process_new_icon(\context_course::instance($record['courseid']), 'group', 'icon', $id,
 542                  $record['picturepath']);
 543  
 544              $DB->set_field('groups', 'picture', $grouppicture, ['id' => $id]);
 545  
 546              // Invalidate the group data as we've updated the group record.
 547              cache_helper::invalidate_by_definition('core', 'groupdata', array(), [$record['courseid']]);
 548          }
 549  
 550          return $DB->get_record('groups', array('id'=>$id));
 551      }
 552  
 553      /**
 554       * Create a test group member
 555       * @param array|stdClass $record
 556       * @throws coding_exception
 557       * @return boolean
 558       */
 559      public function create_group_member($record) {
 560          global $DB, $CFG;
 561  
 562          require_once($CFG->dirroot . '/group/lib.php');
 563  
 564          $record = (array)$record;
 565  
 566          if (empty($record['userid'])) {
 567              throw new coding_exception('user must be present in testing_util::create_group_member() $record');
 568          }
 569  
 570          if (!isset($record['groupid'])) {
 571              throw new coding_exception('group must be present in testing_util::create_group_member() $record');
 572          }
 573  
 574          if (!isset($record['component'])) {
 575              $record['component'] = null;
 576          }
 577          if (!isset($record['itemid'])) {
 578              $record['itemid'] = 0;
 579          }
 580  
 581          return groups_add_member($record['groupid'], $record['userid'], $record['component'], $record['itemid']);
 582      }
 583  
 584      /**
 585       * Create a test grouping for the specified course
 586       *
 587       * $record should be either an array or a stdClass containing infomation about the grouping to create.
 588       * At the very least it needs to contain courseid.
 589       * Default values are added for name, description, and descriptionformat if they are not present.
 590       *
 591       * This function calls groups_create_grouping() to create the grouping within the database.
 592       * @see groups_create_grouping
 593       * @param array|stdClass $record
 594       * @return stdClass grouping record
 595       */
 596      public function create_grouping($record) {
 597          global $DB, $CFG;
 598  
 599          require_once($CFG->dirroot . '/group/lib.php');
 600  
 601          $this->groupingcount++;
 602          $i = $this->groupingcount;
 603  
 604          $record = (array)$record;
 605  
 606          if (empty($record['courseid'])) {
 607              throw new coding_exception('courseid must be present in testing_data_generator::create_grouping() $record');
 608          }
 609  
 610          if (!isset($record['name'])) {
 611              $record['name'] = 'grouping-' . $i;
 612          }
 613  
 614          if (!isset($record['description'])) {
 615              $record['description'] = "Test Grouping $i\n{$this->loremipsum}";
 616          }
 617  
 618          if (!isset($record['descriptionformat'])) {
 619              $record['descriptionformat'] = FORMAT_MOODLE;
 620          }
 621  
 622          $id = groups_create_grouping((object)$record);
 623  
 624          return $DB->get_record('groupings', array('id'=>$id));
 625      }
 626  
 627      /**
 628       * Create a test grouping group
 629       * @param array|stdClass $record
 630       * @throws coding_exception
 631       * @return boolean
 632       */
 633      public function create_grouping_group($record) {
 634          global $DB, $CFG;
 635  
 636          require_once($CFG->dirroot . '/group/lib.php');
 637  
 638          $record = (array)$record;
 639  
 640          if (empty($record['groupingid'])) {
 641              throw new coding_exception('grouping must be present in testing::create_grouping_group() $record');
 642          }
 643  
 644          if (!isset($record['groupid'])) {
 645              throw new coding_exception('group must be present in testing_util::create_grouping_group() $record');
 646          }
 647  
 648          return groups_assign_grouping($record['groupingid'], $record['groupid']);
 649      }
 650  
 651      /**
 652       * Create an instance of a repository.
 653       *
 654       * @param string type of repository to create an instance for.
 655       * @param array|stdClass $record data to use to up set the instance.
 656       * @param array $options options
 657       * @return stdClass repository instance record
 658       * @since Moodle 2.5.1
 659       */
 660      public function create_repository($type, $record=null, array $options = null) {
 661          $generator = $this->get_plugin_generator('repository_'.$type);
 662          return $generator->create_instance($record, $options);
 663      }
 664  
 665      /**
 666       * Create an instance of a repository.
 667       *
 668       * @param string type of repository to create an instance for.
 669       * @param array|stdClass $record data to use to up set the instance.
 670       * @param array $options options
 671       * @return repository_type object
 672       * @since Moodle 2.5.1
 673       */
 674      public function create_repository_type($type, $record=null, array $options = null) {
 675          $generator = $this->get_plugin_generator('repository_'.$type);
 676          return $generator->create_type($record, $options);
 677      }
 678  
 679  
 680      /**
 681       * Create a test scale
 682       * @param array|stdClass $record
 683       * @param array $options
 684       * @return stdClass block instance record
 685       */
 686      public function create_scale($record=null, array $options=null) {
 687          global $DB;
 688  
 689          $this->scalecount++;
 690          $i = $this->scalecount;
 691  
 692          $record = (array)$record;
 693  
 694          if (!isset($record['name'])) {
 695              $record['name'] = 'Test scale '.$i;
 696          }
 697  
 698          if (!isset($record['scale'])) {
 699              $record['scale'] = 'A,B,C,D,F';
 700          }
 701  
 702          if (!isset($record['courseid'])) {
 703              $record['courseid'] = 0;
 704          }
 705  
 706          if (!isset($record['userid'])) {
 707              $record['userid'] = 0;
 708          }
 709  
 710          if (!isset($record['description'])) {
 711              $record['description'] = 'Test scale description '.$i;
 712          }
 713  
 714          if (!isset($record['descriptionformat'])) {
 715              $record['descriptionformat'] = FORMAT_MOODLE;
 716          }
 717  
 718          $record['timemodified'] = time();
 719  
 720          if (isset($record['id'])) {
 721              $DB->import_record('scale', $record);
 722              $DB->get_manager()->reset_sequence('scale');
 723              $id = $record['id'];
 724          } else {
 725              $id = $DB->insert_record('scale', $record);
 726          }
 727  
 728          return $DB->get_record('scale', array('id'=>$id), '*', MUST_EXIST);
 729      }
 730  
 731      /**
 732       * Creates a new role in the system.
 733       *
 734       * You can fill $record with the role 'name',
 735       * 'shortname', 'description' and 'archetype'.
 736       *
 737       * If an archetype is specified it's capabilities,
 738       * context where the role can be assigned and
 739       * all other properties are copied from the archetype;
 740       * if no archetype is specified it will create an
 741       * empty role.
 742       *
 743       * @param array|stdClass $record
 744       * @return int The new role id
 745       */
 746      public function create_role($record=null) {
 747          global $DB;
 748  
 749          $this->rolecount++;
 750          $i = $this->rolecount;
 751  
 752          $record = (array)$record;
 753  
 754          if (empty($record['shortname'])) {
 755              $record['shortname'] = 'role-' . $i;
 756          }
 757  
 758          if (empty($record['name'])) {
 759              $record['name'] = 'Test role ' . $i;
 760          }
 761  
 762          if (empty($record['description'])) {
 763              $record['description'] = 'Test role ' . $i . ' description';
 764          }
 765  
 766          if (empty($record['archetype'])) {
 767              $record['archetype'] = '';
 768          } else {
 769              $archetypes = get_role_archetypes();
 770              if (empty($archetypes[$record['archetype']])) {
 771                  throw new coding_exception('\'role\' requires the field \'archetype\' to specify a ' .
 772                      'valid archetype shortname (editingteacher, student...)');
 773              }
 774          }
 775  
 776          // Creates the role.
 777          if (!$newroleid = create_role($record['name'], $record['shortname'], $record['description'], $record['archetype'])) {
 778              throw new coding_exception('There was an error creating \'' . $record['shortname'] . '\' role');
 779          }
 780  
 781          // If no archetype was specified we allow it to be added to all contexts,
 782          // otherwise we allow it in the archetype contexts.
 783          if (!$record['archetype']) {
 784              $contextlevels = [];
 785              $usefallback = true;
 786              foreach (context_helper::get_all_levels() as $level => $title) {
 787                  if (array_key_exists($title, $record)) {
 788                      $usefallback = false;
 789                      if (!empty($record[$title])) {
 790                          $contextlevels[] = $level;
 791                      }
 792                  }
 793              }
 794  
 795              if ($usefallback) {
 796                  $contextlevels = array_keys(context_helper::get_all_levels());
 797              }
 798          } else {
 799              // Copying from the archetype default rol.
 800              $archetyperoleid = $DB->get_field(
 801                  'role',
 802                  'id',
 803                  array('shortname' => $record['archetype'], 'archetype' => $record['archetype'])
 804              );
 805              $contextlevels = get_role_contextlevels($archetyperoleid);
 806          }
 807          set_role_contextlevels($newroleid, $contextlevels);
 808  
 809          if ($record['archetype']) {
 810              // We copy all the roles the archetype can assign, override, switch to and view.
 811              if ($record['archetype']) {
 812                  $types = array('assign', 'override', 'switch', 'view');
 813                  foreach ($types as $type) {
 814                      $rolestocopy = get_default_role_archetype_allows($type, $record['archetype']);
 815                      foreach ($rolestocopy as $tocopy) {
 816                          $functionname = "core_role_set_{$type}_allowed";
 817                          $functionname($newroleid, $tocopy);
 818                      }
 819                  }
 820              }
 821  
 822              // Copying the archetype capabilities.
 823              $sourcerole = $DB->get_record('role', array('id' => $archetyperoleid));
 824              role_cap_duplicate($sourcerole, $newroleid);
 825          }
 826  
 827          $allcapabilities = get_all_capabilities();
 828          $foundcapabilities = array_intersect(array_keys($allcapabilities), array_keys($record));
 829          $systemcontext = \context_system::instance();
 830  
 831          $allpermissions = [
 832              'inherit' => CAP_INHERIT,
 833              'allow' => CAP_ALLOW,
 834              'prevent' => CAP_PREVENT,
 835              'prohibit' => CAP_PROHIBIT,
 836          ];
 837  
 838          foreach ($foundcapabilities as $capability) {
 839              $permission = $record[$capability];
 840              if (!array_key_exists($permission, $allpermissions)) {
 841                  throw new \coding_exception("Unknown capability permissions '{$permission}'");
 842              }
 843              assign_capability(
 844                  $capability,
 845                  $allpermissions[$permission],
 846                  $newroleid,
 847                  $systemcontext->id,
 848                  true
 849              );
 850          }
 851  
 852          return $newroleid;
 853      }
 854  
 855      /**
 856       * Set role capabilities for the specified role.
 857       *
 858       * @param int $roleid The Role to set capabilities for
 859       * @param array $rolecapabilities The list of capability =>permission to set for this role
 860       * @param null|context $context The context to apply this capability to
 861       */
 862      public function create_role_capability(int $roleid, array $rolecapabilities, context $context = null): void {
 863          // Map the capabilities into human-readable names.
 864          $allpermissions = [
 865              'inherit' => CAP_INHERIT,
 866              'allow' => CAP_ALLOW,
 867              'prevent' => CAP_PREVENT,
 868              'prohibit' => CAP_PROHIBIT,
 869          ];
 870  
 871          // Fetch all capabilities to check that they exist.
 872          $allcapabilities = get_all_capabilities();
 873          foreach ($rolecapabilities as $capability => $permission) {
 874              if ($permission === '') {
 875                  // Allow items to be skipped.
 876                  continue;
 877              }
 878  
 879              if (!array_key_exists($capability, $allcapabilities)) {
 880                  throw new \coding_exception("Unknown capability '{$capability}'");
 881              }
 882  
 883              if (!array_key_exists($permission, $allpermissions)) {
 884                  throw new \coding_exception("Unknown capability permissions '{$permission}'");
 885              }
 886  
 887              assign_capability(
 888                  $capability,
 889                  $allpermissions[$permission],
 890                  $roleid,
 891                  $context->id,
 892                  true
 893              );
 894          }
 895      }
 896  
 897      /**
 898       * Create a tag.
 899       *
 900       * @param array|stdClass $record
 901       * @return stdClass the tag record
 902       */
 903      public function create_tag($record = null) {
 904          global $DB, $USER;
 905  
 906          $this->tagcount++;
 907          $i = $this->tagcount;
 908  
 909          $record = (array) $record;
 910  
 911          if (!isset($record['userid'])) {
 912              $record['userid'] = $USER->id;
 913          }
 914  
 915          if (!isset($record['rawname'])) {
 916              if (isset($record['name'])) {
 917                  $record['rawname'] = $record['name'];
 918              } else {
 919                  $record['rawname'] = 'Tag name ' . $i;
 920              }
 921          }
 922  
 923          // Attribute 'name' should be a lowercase version of 'rawname', if not set.
 924          if (!isset($record['name'])) {
 925              $record['name'] = core_text::strtolower($record['rawname']);
 926          } else {
 927              $record['name'] = core_text::strtolower($record['name']);
 928          }
 929  
 930          if (!isset($record['tagcollid'])) {
 931              $record['tagcollid'] = core_tag_collection::get_default();
 932          }
 933  
 934          if (!isset($record['description'])) {
 935              $record['description'] = 'Tag description';
 936          }
 937  
 938          if (!isset($record['descriptionformat'])) {
 939              $record['descriptionformat'] = FORMAT_MOODLE;
 940          }
 941  
 942          if (!isset($record['flag'])) {
 943              $record['flag'] = 0;
 944          }
 945  
 946          if (!isset($record['timemodified'])) {
 947              $record['timemodified'] = time();
 948          }
 949  
 950          $id = $DB->insert_record('tag', $record);
 951  
 952          return $DB->get_record('tag', array('id' => $id), '*', MUST_EXIST);
 953      }
 954  
 955      /**
 956       * Helper method which combines $defaults with the values specified in $record.
 957       * If $record is an object, it is converted to an array.
 958       * Then, for each key that is in $defaults, but not in $record, the value
 959       * from $defaults is copied.
 960       * @param array $defaults the default value for each field with
 961       * @param array|stdClass $record
 962       * @return array updated $record.
 963       */
 964      public function combine_defaults_and_record(array $defaults, $record) {
 965          $record = (array) $record;
 966  
 967          foreach ($defaults as $key => $defaults) {
 968              if (!array_key_exists($key, $record)) {
 969                  $record[$key] = $defaults;
 970              }
 971          }
 972          return $record;
 973      }
 974  
 975      /**
 976       * Simplified enrolment of user to course using default options.
 977       *
 978       * It is strongly recommended to use only this method for 'manual' and 'self' plugins only!!!
 979       *
 980       * @param int $userid
 981       * @param int $courseid
 982       * @param int|string $roleidorshortname optional role id or role shortname, use only with manual plugin
 983       * @param string $enrol name of enrol plugin,
 984       *     there must be exactly one instance in course,
 985       *     it must support enrol_user() method.
 986       * @param int $timestart (optional) 0 means unknown
 987       * @param int $timeend (optional) 0 means forever
 988       * @param int $status (optional) default to ENROL_USER_ACTIVE for new enrolments
 989       * @return bool success
 990       */
 991      public function enrol_user($userid, $courseid, $roleidorshortname = null, $enrol = 'manual',
 992              $timestart = 0, $timeend = 0, $status = null) {
 993          global $DB;
 994  
 995          // If role is specified by shortname, convert it into an id.
 996          if (!is_numeric($roleidorshortname) && is_string($roleidorshortname)) {
 997              $roleid = $DB->get_field('role', 'id', array('shortname' => $roleidorshortname), MUST_EXIST);
 998          } else {
 999              $roleid = $roleidorshortname;
1000          }
1001  
1002          if (!$plugin = enrol_get_plugin($enrol)) {
1003              return false;
1004          }
1005  
1006          $instances = $DB->get_records('enrol', array('courseid'=>$courseid, 'enrol'=>$enrol));
1007          if (count($instances) != 1) {
1008              return false;
1009          }
1010          $instance = reset($instances);
1011  
1012          if (is_null($roleid) and $instance->roleid) {
1013              $roleid = $instance->roleid;
1014          }
1015  
1016          $plugin->enrol_user($instance, $userid, $roleid, $timestart, $timeend, $status);
1017          return true;
1018      }
1019  
1020      /**
1021       * Assigns the specified role to a user in the context.
1022       *
1023       * @param int $roleid
1024       * @param int $userid
1025       * @param int $contextid Defaults to the system context
1026       * @return int new/existing id of the assignment
1027       */
1028      public function role_assign($roleid, $userid, $contextid = false) {
1029  
1030          // Default to the system context.
1031          if (!$contextid) {
1032              $context = context_system::instance();
1033              $contextid = $context->id;
1034          }
1035  
1036          if (empty($roleid)) {
1037              throw new coding_exception('roleid must be present in testing_data_generator::role_assign() arguments');
1038          }
1039  
1040          if (empty($userid)) {
1041              throw new coding_exception('userid must be present in testing_data_generator::role_assign() arguments');
1042          }
1043  
1044          return role_assign($roleid, $userid, $contextid);
1045      }
1046  
1047      /**
1048       * Create a grade_category.
1049       *
1050       * @param array|stdClass $record
1051       * @return stdClass the grade category record
1052       */
1053      public function create_grade_category($record = null) {
1054          global $CFG;
1055  
1056          $this->gradecategorycounter++;
1057  
1058          $record = (array)$record;
1059  
1060          if (empty($record['courseid'])) {
1061              throw new coding_exception('courseid must be present in testing::create_grade_category() $record');
1062          }
1063  
1064          if (!isset($record['fullname'])) {
1065              $record['fullname'] = 'Grade category ' . $this->gradecategorycounter;
1066          }
1067  
1068          // For gradelib classes.
1069          require_once($CFG->libdir . '/gradelib.php');
1070          // Create new grading category in this course.
1071          $gradecategory = new grade_category(array('courseid' => $record['courseid']), false);
1072          $gradecategory->apply_default_settings();
1073          grade_category::set_properties($gradecategory, $record);
1074          $gradecategory->apply_forced_settings();
1075          $gradecategory->insert();
1076  
1077          // This creates a default grade item for the category
1078          $gradeitem = $gradecategory->load_grade_item();
1079  
1080          $gradecategory->update_from_db();
1081          return $gradecategory->get_record_data();
1082      }
1083  
1084      /**
1085       * Create a grade_grade.
1086       *
1087       * @param array $record
1088       * @return grade_grade the grade record
1089       */
1090      public function create_grade_grade(?array $record = null): grade_grade {
1091          global $DB, $USER;
1092  
1093          $item = $DB->get_record('grade_items', ['id' => $record['itemid']]);
1094          $userid = $record['userid'] ?? $USER->id;
1095  
1096          unset($record['itemid']);
1097          unset($record['userid']);
1098  
1099          if ($item->itemtype === 'mod') {
1100              $cm = get_coursemodule_from_instance($item->itemmodule, $item->iteminstance);
1101              $module = new $item->itemmodule(context_module::instance($cm->id), $cm, false);
1102              $record['attemptnumber'] = $record['attemptnumber'] ?? 0;
1103  
1104              $module->save_grade($userid, (object) $record);
1105  
1106              $grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
1107          } else {
1108              $grade = grade_grade::fetch(['userid' => $userid, 'itemid' => $item->id]);
1109              $record['rawgrade'] = $record['rawgrade'] ?? $record['grade'] ?? null;
1110              $record['finalgrade'] = $record['finalgrade'] ?? $record['grade'] ?? null;
1111  
1112              unset($record['grade']);
1113  
1114              if ($grade) {
1115                  $fields = $grade->required_fields + array_keys($grade->optional_fields);
1116  
1117                  foreach ($fields as $field) {
1118                      $grade->{$field} = $record[$field] ?? $grade->{$field};
1119                  }
1120  
1121                  $grade->update();
1122              } else {
1123                  $record['userid'] = $userid;
1124                  $record['itemid'] = $item->id;
1125  
1126                  $grade = new grade_grade($record, false);
1127  
1128                  $grade->insert();
1129              }
1130          }
1131  
1132          return $grade;
1133      }
1134  
1135      /**
1136       * Create a grade_item.
1137       *
1138       * @param array|stdClass $record
1139       * @return stdClass the grade item record
1140       */
1141      public function create_grade_item($record = null) {
1142          global $CFG;
1143          require_once("$CFG->libdir/gradelib.php");
1144  
1145          $this->gradeitemcounter++;
1146  
1147          if (!isset($record['itemtype'])) {
1148              $record['itemtype'] = 'manual';
1149          }
1150  
1151          if (!isset($record['itemname'])) {
1152              $record['itemname'] = 'Grade item ' . $this->gradeitemcounter;
1153          }
1154  
1155          if (isset($record['outcomeid'])) {
1156              $outcome = new grade_outcome(array('id' => $record['outcomeid']));
1157              $record['scaleid'] = $outcome->scaleid;
1158          }
1159          if (isset($record['scaleid'])) {
1160              $record['gradetype'] = GRADE_TYPE_SCALE;
1161          } else if (!isset($record['gradetype'])) {
1162              $record['gradetype'] = GRADE_TYPE_VALUE;
1163          }
1164  
1165          // Create new grade item in this course.
1166          $gradeitem = new grade_item($record, false);
1167          $gradeitem->insert();
1168  
1169          $gradeitem->update_from_db();
1170          return $gradeitem->get_record_data();
1171      }
1172  
1173      /**
1174       * Create a grade_outcome.
1175       *
1176       * @param array|stdClass $record
1177       * @return stdClass the grade outcome record
1178       */
1179      public function create_grade_outcome($record = null) {
1180          global $CFG;
1181  
1182          $this->gradeoutcomecounter++;
1183          $i = $this->gradeoutcomecounter;
1184  
1185          if (!isset($record['fullname'])) {
1186              $record['fullname'] = 'Grade outcome ' . $i;
1187          }
1188  
1189          // For gradelib classes.
1190          require_once($CFG->libdir . '/gradelib.php');
1191          // Create new grading outcome in this course.
1192          $gradeoutcome = new grade_outcome($record, false);
1193          $gradeoutcome->insert();
1194  
1195          $gradeoutcome->update_from_db();
1196          return $gradeoutcome->get_record_data();
1197      }
1198  
1199      /**
1200       * Helper function used to create an LTI tool.
1201       *
1202       * @param array $data
1203       * @return stdClass the tool
1204       */
1205      public function create_lti_tool($data = array()) {
1206          global $DB;
1207  
1208          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
1209          $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
1210  
1211          // Create a course if no course id was specified.
1212          if (empty($data->courseid)) {
1213              $course = $this->create_course();
1214              $data->courseid = $course->id;
1215          } else {
1216              $course = get_course($data->courseid);
1217          }
1218  
1219          if (!empty($data->cmid)) {
1220              $data->contextid = context_module::instance($data->cmid)->id;
1221          } else {
1222              $data->contextid = context_course::instance($data->courseid)->id;
1223          }
1224  
1225          // Set it to enabled if no status was specified.
1226          if (!isset($data->status)) {
1227              $data->status = ENROL_INSTANCE_ENABLED;
1228          }
1229  
1230          // Add some extra necessary fields to the data.
1231          $data->name = 'Test LTI';
1232          $data->roleinstructor = $studentrole->id;
1233          $data->rolelearner = $teacherrole->id;
1234  
1235          // Get the enrol LTI plugin.
1236          $enrolplugin = enrol_get_plugin('lti');
1237          $instanceid = $enrolplugin->add_instance($course, (array) $data);
1238  
1239          // Get the tool associated with this instance.
1240          return $DB->get_record('enrol_lti_tools', array('enrolid' => $instanceid));
1241      }
1242  
1243      /**
1244       * Helper function used to create an event.
1245       *
1246       * @param   array   $data
1247       * @return  stdClass
1248       */
1249      public function create_event($data = []) {
1250          global $CFG;
1251  
1252          require_once($CFG->dirroot . '/calendar/lib.php');
1253          $record = new \stdClass();
1254          $record->name = 'event name';
1255          $record->repeat = 0;
1256          $record->repeats = 0;
1257          $record->timestart = time();
1258          $record->timeduration = 0;
1259          $record->timesort = 0;
1260          $record->eventtype = 'user';
1261          $record->courseid = 0;
1262          $record->categoryid = 0;
1263  
1264          foreach ($data as $key => $value) {
1265              $record->$key = $value;
1266          }
1267  
1268          switch ($record->eventtype) {
1269              case 'user':
1270                  unset($record->categoryid);
1271                  unset($record->courseid);
1272                  unset($record->groupid);
1273                  break;
1274              case 'group':
1275                  unset($record->categoryid);
1276                  break;
1277              case 'course':
1278                  unset($record->categoryid);
1279                  unset($record->groupid);
1280                  break;
1281              case 'category':
1282                  unset($record->courseid);
1283                  unset($record->groupid);
1284                  break;
1285              case 'site':
1286                  unset($record->categoryid);
1287                  unset($record->courseid);
1288                  unset($record->groupid);
1289                  break;
1290          }
1291  
1292          $event = new calendar_event($record);
1293          $event->create($record);
1294  
1295          return $event->properties();
1296      }
1297  
1298      /**
1299       * Create a new course custom field category with the given name.
1300       *
1301       * @param   array $data Array with data['name'] of category
1302       * @return  \core_customfield\category_controller   The created category
1303       */
1304      public function create_custom_field_category($data) : \core_customfield\category_controller {
1305          return $this->get_plugin_generator('core_customfield')->create_category($data);
1306      }
1307  
1308      /**
1309       * Create a new custom field
1310       *
1311       * @param   array $data Array with 'name', 'shortname' and 'type' of the field
1312       * @return  \core_customfield\field_controller   The created field
1313       */
1314      public function create_custom_field($data) : \core_customfield\field_controller {
1315          global $DB;
1316          if (empty($data['categoryid']) && !empty($data['category'])) {
1317              $data['categoryid'] = $DB->get_field('customfield_category', 'id', ['name' => $data['category']]);
1318              unset($data['category']);
1319          }
1320          return $this->get_plugin_generator('core_customfield')->create_field($data);
1321      }
1322  
1323      /**
1324       * Create a new user, and enrol them in the specified course as the supplied role.
1325       *
1326       * @param   \stdClass   $course The course to enrol in
1327       * @param   string      $role The role to give within the course
1328       * @param   \stdClass   $userparams User parameters
1329       * @return  \stdClass   The created user
1330       */
1331      public function create_and_enrol($course, $role = 'student', $userparams = null, $enrol = 'manual',
1332              $timestart = 0, $timeend = 0, $status = null) {
1333          global $DB;
1334  
1335          $user = $this->create_user($userparams);
1336          $roleid = $DB->get_field('role', 'id', ['shortname' => $role ]);
1337  
1338          $this->enrol_user($user->id, $course->id, $roleid, $enrol, $timestart, $timeend, $status);
1339  
1340          return $user;
1341      }
1342  
1343      /**
1344       * Create a new last access record for a given user in a course.
1345       *
1346       * @param   \stdClass   $user The user
1347       * @param   \stdClass   $course The course the user accessed
1348       * @param   int         $timestamp The timestamp for when the user last accessed the course
1349       * @return  \stdClass   The user_lastaccess record
1350       */
1351      public function create_user_course_lastaccess(\stdClass $user, \stdClass $course, int $timestamp): \stdClass {
1352          global $DB;
1353  
1354          $record = [
1355              'userid' => $user->id,
1356              'courseid' => $course->id,
1357              'timeaccess' => $timestamp,
1358          ];
1359  
1360          $recordid = $DB->insert_record('user_lastaccess', $record);
1361  
1362          return $DB->get_record('user_lastaccess', ['id' => $recordid], '*', MUST_EXIST);
1363      }
1364  
1365      /**
1366       * Gets a default generator for a given component.
1367       *
1368       * @param string $component The component name, e.g. 'mod_forum' or 'core_question'.
1369       * @param string $classname The name of the class missing from the generators file.
1370       * @return component_generator_base The generator.
1371       */
1372      protected function get_default_plugin_generator(string $component, ?string $classname = null) {
1373          [$type, $plugin] = core_component::normalize_component($component);
1374  
1375          switch ($type) {
1376              case 'block':
1377                  return new default_block_generator($this, $plugin);
1378          }
1379  
1380          if (is_null($classname)) {
1381              throw new coding_exception("Component {$component} does not support " .
1382                  "generators yet. Missing tests/generator/lib.php.");
1383          }
1384  
1385          throw new coding_exception("Component {$component} does not support " .
1386              "data generators yet. Class {$classname} not found.");
1387      }
1388  
1389      /**
1390       * Create a new category for custom profile fields.
1391       *
1392       * @param array $data Array with 'name' and optionally 'sortorder'
1393       * @return \stdClass New category object
1394       */
1395      public function create_custom_profile_field_category(array $data): \stdClass {
1396          global $DB;
1397  
1398          // Pick next sortorder if not defined.
1399          if (!array_key_exists('sortorder', $data)) {
1400              $data['sortorder'] = (int)$DB->get_field_sql('SELECT MAX(sortorder) FROM {user_info_category}') + 1;
1401          }
1402  
1403          $category = (object)[
1404              'name' => $data['name'],
1405              'sortorder' => $data['sortorder']
1406          ];
1407          $category->id = $DB->insert_record('user_info_category', $category);
1408  
1409          return $category;
1410      }
1411  
1412      /**
1413       * Creates a new custom profile field.
1414       *
1415       * Optional fields are:
1416       *
1417       * categoryid (or use 'category' to specify by name). If you don't specify
1418       * either, it will add the field to a 'Testing' category, which will be created for you if
1419       * necessary.
1420       *
1421       * sortorder (if you don't specify this, it will pick the next one in the category).
1422       *
1423       * all the other database fields (if you don't specify this, it will pick sensible defaults
1424       * based on the data type).
1425       *
1426       * @param array $data Array with 'datatype', 'shortname', and 'name'
1427       * @return \stdClass Database object from the user_info_field table
1428       */
1429      public function create_custom_profile_field(array $data): \stdClass {
1430          global $DB, $CFG;
1431          require_once($CFG->dirroot . '/user/profile/lib.php');
1432  
1433          // Set up category if necessary.
1434          if (!array_key_exists('categoryid', $data)) {
1435              if (array_key_exists('category', $data)) {
1436                  $data['categoryid'] = $DB->get_field('user_info_category', 'id',
1437                          ['name' => $data['category']], MUST_EXIST);
1438              } else {
1439                  // Make up a 'Testing' category or use existing.
1440                  $data['categoryid'] = $DB->get_field('user_info_category', 'id', ['name' => 'Testing']);
1441                  if (!$data['categoryid']) {
1442                      $created = $this->create_custom_profile_field_category(['name' => 'Testing']);
1443                      $data['categoryid'] = $created->id;
1444                  }
1445              }
1446          }
1447  
1448          // Pick sort order if necessary.
1449          if (!array_key_exists('sortorder', $data)) {
1450              $data['sortorder'] = (int)$DB->get_field_sql(
1451                      'SELECT MAX(sortorder) FROM {user_info_field} WHERE categoryid = ?',
1452                      [$data['categoryid']]) + 1;
1453          }
1454  
1455          // Defaults for other values.
1456          $defaults = [
1457              'description' => '',
1458              'descriptionformat' => 0,
1459              'required' => 0,
1460              'locked' => 0,
1461              'visible' => PROFILE_VISIBLE_ALL,
1462              'forceunique' => 0,
1463              'signup' => 0,
1464              'defaultdata' => '',
1465              'defaultdataformat' => 0,
1466              'param1' => '',
1467              'param2' => '',
1468              'param3' => '',
1469              'param4' => '',
1470              'param5' => ''
1471          ];
1472  
1473          // Type-specific defaults for other values.
1474          $typedefaults = [
1475              'text' => [
1476                  'param1' => 30,
1477                  'param2' => 2048
1478              ],
1479              'menu' => [
1480                  'param1' => "Yes\nNo",
1481                  'defaultdata' => 'No'
1482              ],
1483              'datetime' => [
1484                  'param1' => '2010',
1485                  'param2' => '2015',
1486                  'param3' => 1
1487              ],
1488              'checkbox' => [
1489                  'defaultdata' => 0
1490              ]
1491          ];
1492          foreach ($typedefaults[$data['datatype']] ?? [] as $field => $value) {
1493              $defaults[$field] = $value;
1494          }
1495  
1496          foreach ($defaults as $field => $value) {
1497              if (!array_key_exists($field, $data)) {
1498                  $data[$field] = $value;
1499              }
1500          }
1501  
1502          $data['id'] = $DB->insert_record('user_info_field', $data);
1503          return (object)$data;
1504      }
1505  }