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