Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 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 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

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