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