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 * External course functions unit tests 19 * 20 * @package core_course 21 * @category external 22 * @copyright 2012 Jerome Mouneyrac 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 global $CFG; 29 30 require_once($CFG->dirroot . '/webservice/tests/helpers.php'); 31 32 /** 33 * External course functions unit tests 34 * 35 * @package core_course 36 * @category external 37 * @copyright 2012 Jerome Mouneyrac 38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 39 */ 40 class core_course_externallib_testcase extends externallib_advanced_testcase { 41 42 /** 43 * Tests set up 44 */ 45 protected function setUp(): void { 46 global $CFG; 47 require_once($CFG->dirroot . '/course/externallib.php'); 48 } 49 50 /** 51 * Test create_categories 52 */ 53 public function test_create_categories() { 54 55 global $DB; 56 57 $this->resetAfterTest(true); 58 59 // Set the required capabilities by the external function 60 $contextid = context_system::instance()->id; 61 $roleid = $this->assignUserCapability('moodle/category:manage', $contextid); 62 63 // Create base categories. 64 $category1 = new stdClass(); 65 $category1->name = 'Root Test Category 1'; 66 $category2 = new stdClass(); 67 $category2->name = 'Root Test Category 2'; 68 $category2->idnumber = 'rootcattest2'; 69 $category2->desc = 'Description for root test category 1'; 70 $category2->theme = 'classic'; 71 $categories = array( 72 array('name' => $category1->name, 'parent' => 0), 73 array('name' => $category2->name, 'parent' => 0, 'idnumber' => $category2->idnumber, 74 'description' => $category2->desc, 'theme' => $category2->theme) 75 ); 76 77 $createdcats = core_course_external::create_categories($categories); 78 79 // We need to execute the return values cleaning process to simulate the web service server. 80 $createdcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdcats); 81 82 // Initially confirm that base data was inserted correctly. 83 $this->assertEquals($category1->name, $createdcats[0]['name']); 84 $this->assertEquals($category2->name, $createdcats[1]['name']); 85 86 // Save the ids. 87 $category1->id = $createdcats[0]['id']; 88 $category2->id = $createdcats[1]['id']; 89 90 // Create on sub category. 91 $category3 = new stdClass(); 92 $category3->name = 'Sub Root Test Category 3'; 93 $subcategories = array( 94 array('name' => $category3->name, 'parent' => $category1->id) 95 ); 96 97 $createdsubcats = core_course_external::create_categories($subcategories); 98 99 // We need to execute the return values cleaning process to simulate the web service server. 100 $createdsubcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdsubcats); 101 102 // Confirm that sub categories were inserted correctly. 103 $this->assertEquals($category3->name, $createdsubcats[0]['name']); 104 105 // Save the ids. 106 $category3->id = $createdsubcats[0]['id']; 107 108 // Calling the ws function should provide a new sortorder to give category1, 109 // category2, category3. New course categories are ordered by id not name. 110 $category1 = $DB->get_record('course_categories', array('id' => $category1->id)); 111 $category2 = $DB->get_record('course_categories', array('id' => $category2->id)); 112 $category3 = $DB->get_record('course_categories', array('id' => $category3->id)); 113 114 // sortorder sequence (and sortorder) must be: 115 // category 1 116 // category 3 117 // category 2 118 $this->assertGreaterThan($category1->sortorder, $category3->sortorder); 119 $this->assertGreaterThan($category3->sortorder, $category2->sortorder); 120 121 // Call without required capability 122 $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid); 123 $this->expectException('required_capability_exception'); 124 $createdsubcats = core_course_external::create_categories($subcategories); 125 126 } 127 128 /** 129 * Test delete categories 130 */ 131 public function test_delete_categories() { 132 global $DB; 133 134 $this->resetAfterTest(true); 135 136 // Set the required capabilities by the external function 137 $contextid = context_system::instance()->id; 138 $roleid = $this->assignUserCapability('moodle/category:manage', $contextid); 139 140 $category1 = self::getDataGenerator()->create_category(); 141 $category2 = self::getDataGenerator()->create_category( 142 array('parent' => $category1->id)); 143 $category3 = self::getDataGenerator()->create_category(); 144 $category4 = self::getDataGenerator()->create_category( 145 array('parent' => $category3->id)); 146 $category5 = self::getDataGenerator()->create_category( 147 array('parent' => $category4->id)); 148 149 //delete category 1 and 2 + delete category 4, category 5 moved under category 3 150 core_course_external::delete_categories(array( 151 array('id' => $category1->id, 'recursive' => 1), 152 array('id' => $category4->id) 153 )); 154 155 //check $category 1 and 2 are deleted 156 $notdeletedcount = $DB->count_records_select('course_categories', 157 'id IN ( ' . $category1->id . ',' . $category2->id . ',' . $category4->id . ')'); 158 $this->assertEquals(0, $notdeletedcount); 159 160 //check that $category5 as $category3 for parent 161 $dbcategory5 = $DB->get_record('course_categories', array('id' => $category5->id)); 162 $this->assertEquals($dbcategory5->path, $category3->path . '/' . $category5->id); 163 164 // Call without required capability 165 $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid); 166 $this->expectException('required_capability_exception'); 167 $createdsubcats = core_course_external::delete_categories( 168 array(array('id' => $category3->id))); 169 } 170 171 /** 172 * Test get categories 173 */ 174 public function test_get_categories() { 175 global $DB; 176 177 $this->resetAfterTest(true); 178 179 $generatedcats = array(); 180 $category1data['idnumber'] = 'idnumbercat1'; 181 $category1data['name'] = 'Category 1 for PHPunit test'; 182 $category1data['description'] = 'Category 1 description'; 183 $category1data['descriptionformat'] = FORMAT_MOODLE; 184 $category1 = self::getDataGenerator()->create_category($category1data); 185 $generatedcats[$category1->id] = $category1; 186 $category2 = self::getDataGenerator()->create_category( 187 array('parent' => $category1->id)); 188 $generatedcats[$category2->id] = $category2; 189 $category6 = self::getDataGenerator()->create_category( 190 array('parent' => $category1->id, 'visible' => 0)); 191 $generatedcats[$category6->id] = $category6; 192 $category3 = self::getDataGenerator()->create_category(); 193 $generatedcats[$category3->id] = $category3; 194 $category4 = self::getDataGenerator()->create_category( 195 array('parent' => $category3->id)); 196 $generatedcats[$category4->id] = $category4; 197 $category5 = self::getDataGenerator()->create_category( 198 array('parent' => $category4->id)); 199 $generatedcats[$category5->id] = $category5; 200 201 // Set the required capabilities by the external function. 202 $context = context_system::instance(); 203 $roleid = $this->assignUserCapability('moodle/category:manage', $context->id); 204 $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid); 205 206 // Retrieve category1 + sub-categories except not visible ones 207 $categories = core_course_external::get_categories(array( 208 array('key' => 'id', 'value' => $category1->id), 209 array('key' => 'visible', 'value' => 1)), 1); 210 211 // We need to execute the return values cleaning process to simulate the web service server. 212 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); 213 214 // Check we retrieve the good total number of categories. 215 $this->assertEquals(2, count($categories)); 216 217 // Check the return values 218 foreach ($categories as $category) { 219 $generatedcat = $generatedcats[$category['id']]; 220 $this->assertEquals($category['idnumber'], $generatedcat->idnumber); 221 $this->assertEquals($category['name'], $generatedcat->name); 222 // Description was converted to the HTML format. 223 $this->assertEquals($category['description'], format_text($generatedcat->description, FORMAT_MOODLE, array('para' => false))); 224 $this->assertEquals($category['descriptionformat'], FORMAT_HTML); 225 } 226 227 // Check categories by ids. 228 $ids = implode(',', array_keys($generatedcats)); 229 $categories = core_course_external::get_categories(array( 230 array('key' => 'ids', 'value' => $ids)), 0); 231 232 // We need to execute the return values cleaning process to simulate the web service server. 233 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); 234 235 // Check we retrieve the good total number of categories. 236 $this->assertEquals(6, count($categories)); 237 // Check ids. 238 $returnedids = []; 239 foreach ($categories as $category) { 240 $returnedids[] = $category['id']; 241 } 242 // Sort the arrays upon comparision. 243 $this->assertEqualsCanonicalizing(array_keys($generatedcats), $returnedids); 244 245 // Check different params. 246 $categories = core_course_external::get_categories(array( 247 array('key' => 'id', 'value' => $category1->id), 248 array('key' => 'ids', 'value' => $category1->id), 249 array('key' => 'idnumber', 'value' => $category1->idnumber), 250 array('key' => 'visible', 'value' => 1)), 0); 251 252 // We need to execute the return values cleaning process to simulate the web service server. 253 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); 254 255 $this->assertEquals(1, count($categories)); 256 257 // Same query, but forcing a parameters clean. 258 $categories = core_course_external::get_categories(array( 259 array('key' => 'id', 'value' => "$category1->id"), 260 array('key' => 'idnumber', 'value' => $category1->idnumber), 261 array('key' => 'name', 'value' => $category1->name . "<br/>"), 262 array('key' => 'visible', 'value' => '1')), 0); 263 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); 264 265 $this->assertEquals(1, count($categories)); 266 267 // Retrieve categories from parent. 268 $categories = core_course_external::get_categories(array( 269 array('key' => 'parent', 'value' => $category3->id)), 1); 270 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); 271 272 $this->assertEquals(2, count($categories)); 273 274 // Retrieve all categories. 275 $categories = core_course_external::get_categories(); 276 277 // We need to execute the return values cleaning process to simulate the web service server. 278 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); 279 280 $this->assertEquals($DB->count_records('course_categories'), count($categories)); 281 282 $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid); 283 284 // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability. 285 // It should retrieve all visible categories as well. 286 set_config('maxcategorydepth', 2); 287 $categories = core_course_external::get_categories(); 288 289 // We need to execute the return values cleaning process to simulate the web service server. 290 $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories); 291 292 $this->assertEquals($DB->count_records('course_categories', array('visible' => 1)), count($categories)); 293 294 // Call without required capability (it will fail cause of the search on idnumber). 295 $this->expectException('moodle_exception'); 296 $categories = core_course_external::get_categories(array( 297 array('key' => 'id', 'value' => $category1->id), 298 array('key' => 'idnumber', 'value' => $category1->idnumber), 299 array('key' => 'visible', 'value' => 1)), 0); 300 } 301 302 /** 303 * Test update_categories 304 */ 305 public function test_update_categories() { 306 global $DB; 307 308 $this->resetAfterTest(true); 309 310 // Set the required capabilities by the external function 311 $contextid = context_system::instance()->id; 312 $roleid = $this->assignUserCapability('moodle/category:manage', $contextid); 313 314 // Create base categories. 315 $category1data['idnumber'] = 'idnumbercat1'; 316 $category1data['name'] = 'Category 1 for PHPunit test'; 317 $category1data['description'] = 'Category 1 description'; 318 $category1data['descriptionformat'] = FORMAT_MOODLE; 319 $category1 = self::getDataGenerator()->create_category($category1data); 320 $category2 = self::getDataGenerator()->create_category( 321 array('parent' => $category1->id)); 322 $category3 = self::getDataGenerator()->create_category(); 323 $category4 = self::getDataGenerator()->create_category( 324 array('parent' => $category3->id)); 325 $category5 = self::getDataGenerator()->create_category( 326 array('parent' => $category4->id)); 327 328 // We update all category1 attribut. 329 // Then we move cat4 and cat5 parent: cat3 => cat1 330 $categories = array( 331 array('id' => $category1->id, 332 'name' => $category1->name . '_updated', 333 'idnumber' => $category1->idnumber . '_updated', 334 'description' => $category1->description . '_updated', 335 'descriptionformat' => FORMAT_HTML, 336 'theme' => $category1->theme), 337 array('id' => $category4->id, 'parent' => $category1->id)); 338 339 core_course_external::update_categories($categories); 340 341 // Check the values were updated. 342 $dbcategories = $DB->get_records_select('course_categories', 343 'id IN (' . $category1->id . ',' . $category2->id . ',' . $category2->id 344 . ',' . $category3->id . ',' . $category4->id . ',' . $category5->id .')'); 345 $this->assertEquals($category1->name . '_updated', 346 $dbcategories[$category1->id]->name); 347 $this->assertEquals($category1->idnumber . '_updated', 348 $dbcategories[$category1->id]->idnumber); 349 $this->assertEquals($category1->description . '_updated', 350 $dbcategories[$category1->id]->description); 351 $this->assertEquals(FORMAT_HTML, $dbcategories[$category1->id]->descriptionformat); 352 353 // Check that category4 and category5 have been properly moved. 354 $this->assertEquals('/' . $category1->id . '/' . $category4->id, 355 $dbcategories[$category4->id]->path); 356 $this->assertEquals('/' . $category1->id . '/' . $category4->id . '/' . $category5->id, 357 $dbcategories[$category5->id]->path); 358 359 // Call without required capability. 360 $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid); 361 $this->expectException('required_capability_exception'); 362 core_course_external::update_categories($categories); 363 } 364 365 /** 366 * Test create_courses numsections 367 */ 368 public function test_create_course_numsections() { 369 global $DB; 370 371 $this->resetAfterTest(true); 372 373 // Set the required capabilities by the external function. 374 $contextid = context_system::instance()->id; 375 $roleid = $this->assignUserCapability('moodle/course:create', $contextid); 376 $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid); 377 378 $numsections = 10; 379 $category = self::getDataGenerator()->create_category(); 380 381 // Create base categories. 382 $course1['fullname'] = 'Test course 1'; 383 $course1['shortname'] = 'Testcourse1'; 384 $course1['categoryid'] = $category->id; 385 $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections); 386 387 $courses = array($course1); 388 389 $createdcourses = core_course_external::create_courses($courses); 390 foreach ($createdcourses as $createdcourse) { 391 $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id'])); 392 $modinfo = get_fast_modinfo($createdcourse['id']); 393 $sections = $modinfo->get_section_info_all(); 394 $this->assertEquals(count($sections), $numsections + 1); // Includes generic section. 395 $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section. 396 } 397 } 398 399 /** 400 * Test create_courses 401 */ 402 public function test_create_courses() { 403 global $DB; 404 405 $this->resetAfterTest(true); 406 407 // Enable course completion. 408 set_config('enablecompletion', 1); 409 // Enable course themes. 410 set_config('allowcoursethemes', 1); 411 412 // Custom fields. 413 $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']); 414 415 $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text', 416 'categoryid' => $fieldcategory->get('id'), 417 'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL]]; 418 $field = self::getDataGenerator()->create_custom_field($customfield); 419 420 // Set the required capabilities by the external function 421 $contextid = context_system::instance()->id; 422 $roleid = $this->assignUserCapability('moodle/course:create', $contextid); 423 $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid); 424 $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid); 425 426 $category = self::getDataGenerator()->create_category(); 427 428 // Create base categories. 429 $course1['fullname'] = 'Test course 1'; 430 $course1['shortname'] = 'Testcourse1'; 431 $course1['categoryid'] = $category->id; 432 $course2['fullname'] = 'Test course 2'; 433 $course2['shortname'] = 'Testcourse2'; 434 $course2['categoryid'] = $category->id; 435 $course2['idnumber'] = 'testcourse2idnumber'; 436 $course2['summary'] = 'Description for course 2'; 437 $course2['summaryformat'] = FORMAT_MOODLE; 438 $course2['format'] = 'weeks'; 439 $course2['showgrades'] = 1; 440 $course2['newsitems'] = 3; 441 $course2['startdate'] = 1420092000; // 01/01/2015. 442 $course2['enddate'] = 1422669600; // 01/31/2015. 443 $course2['numsections'] = 4; 444 $course2['maxbytes'] = 100000; 445 $course2['showreports'] = 1; 446 $course2['visible'] = 0; 447 $course2['hiddensections'] = 0; 448 $course2['groupmode'] = 0; 449 $course2['groupmodeforce'] = 0; 450 $course2['defaultgroupingid'] = 0; 451 $course2['enablecompletion'] = 1; 452 $course2['completionnotify'] = 1; 453 $course2['lang'] = 'en'; 454 $course2['forcetheme'] = 'classic'; 455 $course2['courseformatoptions'][] = array('name' => 'automaticenddate', 'value' => 0); 456 $course3['fullname'] = 'Test course 3'; 457 $course3['shortname'] = 'Testcourse3'; 458 $course3['categoryid'] = $category->id; 459 $course3['format'] = 'topics'; 460 $course3options = array('numsections' => 8, 461 'hiddensections' => 1, 462 'coursedisplay' => 1); 463 $course3['courseformatoptions'] = array(); 464 foreach ($course3options as $key => $value) { 465 $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value); 466 } 467 $course4['fullname'] = 'Test course with custom fields'; 468 $course4['shortname'] = 'Testcoursecustomfields'; 469 $course4['categoryid'] = $category->id; 470 $course4['customfields'] = [['shortname' => $customfield['shortname'], 'value' => 'Test value']]; 471 $courses = array($course4, $course1, $course2, $course3); 472 473 $createdcourses = core_course_external::create_courses($courses); 474 475 // We need to execute the return values cleaning process to simulate the web service server. 476 $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses); 477 478 // Check that right number of courses were created. 479 $this->assertEquals(4, count($createdcourses)); 480 481 // Check that the courses were correctly created. 482 foreach ($createdcourses as $createdcourse) { 483 $courseinfo = course_get_format($createdcourse['id'])->get_course(); 484 485 if ($createdcourse['shortname'] == $course2['shortname']) { 486 $this->assertEquals($courseinfo->fullname, $course2['fullname']); 487 $this->assertEquals($courseinfo->shortname, $course2['shortname']); 488 $this->assertEquals($courseinfo->category, $course2['categoryid']); 489 $this->assertEquals($courseinfo->idnumber, $course2['idnumber']); 490 $this->assertEquals($courseinfo->summary, $course2['summary']); 491 $this->assertEquals($courseinfo->summaryformat, $course2['summaryformat']); 492 $this->assertEquals($courseinfo->format, $course2['format']); 493 $this->assertEquals($courseinfo->showgrades, $course2['showgrades']); 494 $this->assertEquals($courseinfo->newsitems, $course2['newsitems']); 495 $this->assertEquals($courseinfo->startdate, $course2['startdate']); 496 $this->assertEquals($courseinfo->enddate, $course2['enddate']); 497 $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(), $course2['numsections']); 498 $this->assertEquals($courseinfo->maxbytes, $course2['maxbytes']); 499 $this->assertEquals($courseinfo->showreports, $course2['showreports']); 500 $this->assertEquals($courseinfo->visible, $course2['visible']); 501 $this->assertEquals($courseinfo->hiddensections, $course2['hiddensections']); 502 $this->assertEquals($courseinfo->groupmode, $course2['groupmode']); 503 $this->assertEquals($courseinfo->groupmodeforce, $course2['groupmodeforce']); 504 $this->assertEquals($courseinfo->defaultgroupingid, $course2['defaultgroupingid']); 505 $this->assertEquals($courseinfo->completionnotify, $course2['completionnotify']); 506 $this->assertEquals($courseinfo->lang, $course2['lang']); 507 $this->assertEquals($courseinfo->theme, $course2['forcetheme']); 508 509 // We enabled completion at the beginning of the test. 510 $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']); 511 512 } else if ($createdcourse['shortname'] == $course1['shortname']) { 513 $courseconfig = get_config('moodlecourse'); 514 $this->assertEquals($courseinfo->fullname, $course1['fullname']); 515 $this->assertEquals($courseinfo->shortname, $course1['shortname']); 516 $this->assertEquals($courseinfo->category, $course1['categoryid']); 517 $this->assertEquals($courseinfo->summaryformat, FORMAT_HTML); 518 $this->assertEquals($courseinfo->format, $courseconfig->format); 519 $this->assertEquals($courseinfo->showgrades, $courseconfig->showgrades); 520 $this->assertEquals($courseinfo->newsitems, $courseconfig->newsitems); 521 $this->assertEquals($courseinfo->maxbytes, $courseconfig->maxbytes); 522 $this->assertEquals($courseinfo->showreports, $courseconfig->showreports); 523 $this->assertEquals($courseinfo->groupmode, $courseconfig->groupmode); 524 $this->assertEquals($courseinfo->groupmodeforce, $courseconfig->groupmodeforce); 525 $this->assertEquals($courseinfo->defaultgroupingid, 0); 526 } else if ($createdcourse['shortname'] == $course3['shortname']) { 527 $this->assertEquals($courseinfo->fullname, $course3['fullname']); 528 $this->assertEquals($courseinfo->shortname, $course3['shortname']); 529 $this->assertEquals($courseinfo->category, $course3['categoryid']); 530 $this->assertEquals($courseinfo->format, $course3['format']); 531 $this->assertEquals($courseinfo->hiddensections, $course3options['hiddensections']); 532 $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(), 533 $course3options['numsections']); 534 $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']); 535 } else if ($createdcourse['shortname'] == $course4['shortname']) { 536 $this->assertEquals($courseinfo->fullname, $course4['fullname']); 537 $this->assertEquals($courseinfo->shortname, $course4['shortname']); 538 $this->assertEquals($courseinfo->category, $course4['categoryid']); 539 540 $handler = core_course\customfield\course_handler::create(); 541 $customfields = $handler->export_instance_data_object($createdcourse['id']); 542 $this->assertEquals((object)['test' => 'Test value'], $customfields); 543 } else { 544 throw new moodle_exception('Unexpected shortname'); 545 } 546 } 547 548 // Call without required capability 549 $this->unassignUserCapability('moodle/course:create', $contextid, $roleid); 550 $this->expectException('required_capability_exception'); 551 $createdsubcats = core_course_external::create_courses($courses); 552 } 553 554 /** 555 * Data provider for testing empty fields produce expected exceptions 556 * 557 * @see test_create_courses_empty_field 558 * @see test_update_courses_empty_field 559 * 560 * @return array 561 */ 562 public function course_empty_field_provider(): array { 563 return [ 564 [[ 565 'fullname' => '', 566 'shortname' => 'ws101', 567 ], 'fullname'], 568 [[ 569 'fullname' => ' ', 570 'shortname' => 'ws101', 571 ], 'fullname'], 572 [[ 573 'fullname' => 'Web Services', 574 'shortname' => '', 575 ], 'shortname'], 576 [[ 577 'fullname' => 'Web Services', 578 'shortname' => ' ', 579 ], 'shortname'], 580 ]; 581 } 582 583 /** 584 * Test creating courses with empty fields throws an exception 585 * 586 * @param array $course 587 * @param string $expectedemptyfield 588 * 589 * @dataProvider course_empty_field_provider 590 */ 591 public function test_create_courses_empty_field(array $course, string $expectedemptyfield): void { 592 $this->resetAfterTest(); 593 $this->setAdminUser(); 594 595 // Create a category for the new course. 596 $course['categoryid'] = $this->getDataGenerator()->create_category()->id; 597 598 $this->expectException(moodle_exception::class); 599 $this->expectExceptionMessageMatches("/{$expectedemptyfield}/"); 600 core_course_external::create_courses([$course]); 601 } 602 603 /** 604 * Test updating courses with empty fields returns warnings 605 * 606 * @param array $course 607 * @param string $expectedemptyfield 608 * 609 * @dataProvider course_empty_field_provider 610 */ 611 public function test_update_courses_empty_field(array $course, string $expectedemptyfield): void { 612 $this->resetAfterTest(); 613 $this->setAdminUser(); 614 615 // Create a course to update. 616 $course['id'] = $this->getDataGenerator()->create_course()->id; 617 618 $result = core_course_external::update_courses([$course]); 619 $result = core_course_external::clean_returnvalue(core_course_external::update_courses_returns(), $result); 620 621 $this->assertCount(1, $result['warnings']); 622 623 $warning = reset($result['warnings']); 624 $this->assertEquals('errorinvalidparam', $warning['warningcode']); 625 $this->assertStringContainsString($expectedemptyfield, $warning['message']); 626 } 627 628 /** 629 * Test delete_courses 630 */ 631 public function test_delete_courses() { 632 global $DB, $USER; 633 634 $this->resetAfterTest(true); 635 636 // Admin can delete a course. 637 $this->setAdminUser(); 638 // Validate_context() will fail as the email is not set by $this->setAdminUser(). 639 $USER->email = 'emailtopass@example.com'; 640 641 $course1 = self::getDataGenerator()->create_course(); 642 $course2 = self::getDataGenerator()->create_course(); 643 $course3 = self::getDataGenerator()->create_course(); 644 645 // Delete courses. 646 $result = core_course_external::delete_courses(array($course1->id, $course2->id)); 647 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result); 648 // Check for 0 warnings. 649 $this->assertEquals(0, count($result['warnings'])); 650 651 // Check $course 1 and 2 are deleted. 652 $notdeletedcount = $DB->count_records_select('course', 653 'id IN ( ' . $course1->id . ',' . $course2->id . ')'); 654 $this->assertEquals(0, $notdeletedcount); 655 656 // Try to delete non-existent course. 657 $result = core_course_external::delete_courses(array($course1->id)); 658 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result); 659 // Check for 1 warnings. 660 $this->assertEquals(1, count($result['warnings'])); 661 662 // Try to delete Frontpage course. 663 $result = core_course_external::delete_courses(array(0)); 664 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result); 665 // Check for 1 warnings. 666 $this->assertEquals(1, count($result['warnings'])); 667 668 // Fail when the user has access to course (enrolled) but does not have permission or is not admin. 669 $student1 = self::getDataGenerator()->create_user(); 670 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 671 $this->getDataGenerator()->enrol_user($student1->id, 672 $course3->id, 673 $studentrole->id); 674 $this->setUser($student1); 675 $result = core_course_external::delete_courses(array($course3->id)); 676 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result); 677 // Check for 1 warnings. 678 $this->assertEquals(1, count($result['warnings'])); 679 680 // Fail when the user is not allow to access the course (enrolled) or is not admin. 681 $this->setGuestUser(); 682 $this->expectException('require_login_exception'); 683 684 $result = core_course_external::delete_courses(array($course3->id)); 685 $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result); 686 } 687 688 /** 689 * Test get_courses 690 */ 691 public function test_get_courses () { 692 global $DB; 693 694 $this->resetAfterTest(true); 695 696 $generatedcourses = array(); 697 $coursedata['idnumber'] = 'idnumbercourse1'; 698 // Adding tags here to check that format_string is applied. 699 $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>'; 700 $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>'; 701 $coursedata['summary'] = 'Course 1 description'; 702 $coursedata['summaryformat'] = FORMAT_MOODLE; 703 $course1 = self::getDataGenerator()->create_course($coursedata); 704 705 $fieldcategory = self::getDataGenerator()->create_custom_field_category( 706 ['name' => 'Other fields']); 707 708 $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text', 709 'categoryid' => $fieldcategory->get('id')]; 710 $field = self::getDataGenerator()->create_custom_field($customfield); 711 712 $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value']; 713 714 $generatedcourses[$course1->id] = $course1; 715 $course2 = self::getDataGenerator()->create_course(); 716 $generatedcourses[$course2->id] = $course2; 717 $course3 = self::getDataGenerator()->create_course(array('format' => 'topics')); 718 $generatedcourses[$course3->id] = $course3; 719 $course4 = self::getDataGenerator()->create_course(['customfields' => [$customfieldvalue]]); 720 $generatedcourses[$course4->id] = $course4; 721 722 // Set the required capabilities by the external function. 723 $context = context_system::instance(); 724 $roleid = $this->assignUserCapability('moodle/course:view', $context->id); 725 $this->assignUserCapability('moodle/course:update', 726 context_course::instance($course1->id)->id, $roleid); 727 $this->assignUserCapability('moodle/course:update', 728 context_course::instance($course2->id)->id, $roleid); 729 $this->assignUserCapability('moodle/course:update', 730 context_course::instance($course3->id)->id, $roleid); 731 $this->assignUserCapability('moodle/course:update', 732 context_course::instance($course4->id)->id, $roleid); 733 734 $courses = core_course_external::get_courses(array('ids' => 735 array($course1->id, $course2->id, $course4->id))); 736 737 // We need to execute the return values cleaning process to simulate the web service server. 738 $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses); 739 740 // Check we retrieve the good total number of courses. 741 $this->assertEquals(3, count($courses)); 742 743 foreach ($courses as $course) { 744 $coursecontext = context_course::instance($course['id']); 745 $dbcourse = $generatedcourses[$course['id']]; 746 $this->assertEquals($course['idnumber'], $dbcourse->idnumber); 747 $this->assertEquals($course['fullname'], external_format_string($dbcourse->fullname, $coursecontext->id)); 748 $this->assertEquals($course['displayname'], external_format_string(get_course_display_name_for_list($dbcourse), 749 $coursecontext->id)); 750 // Summary was converted to the HTML format. 751 $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false))); 752 $this->assertEquals($course['summaryformat'], FORMAT_HTML); 753 $this->assertEquals($course['shortname'], external_format_string($dbcourse->shortname, $coursecontext->id)); 754 $this->assertEquals($course['categoryid'], $dbcourse->category); 755 $this->assertEquals($course['format'], $dbcourse->format); 756 $this->assertEquals($course['showgrades'], $dbcourse->showgrades); 757 $this->assertEquals($course['newsitems'], $dbcourse->newsitems); 758 $this->assertEquals($course['startdate'], $dbcourse->startdate); 759 $this->assertEquals($course['enddate'], $dbcourse->enddate); 760 $this->assertEquals($course['numsections'], course_get_format($dbcourse)->get_last_section_number()); 761 $this->assertEquals($course['maxbytes'], $dbcourse->maxbytes); 762 $this->assertEquals($course['showreports'], $dbcourse->showreports); 763 $this->assertEquals($course['visible'], $dbcourse->visible); 764 $this->assertEquals($course['hiddensections'], $dbcourse->hiddensections); 765 $this->assertEquals($course['groupmode'], $dbcourse->groupmode); 766 $this->assertEquals($course['groupmodeforce'], $dbcourse->groupmodeforce); 767 $this->assertEquals($course['defaultgroupingid'], $dbcourse->defaultgroupingid); 768 $this->assertEquals($course['completionnotify'], $dbcourse->completionnotify); 769 $this->assertEquals($course['lang'], $dbcourse->lang); 770 $this->assertEquals($course['forcetheme'], $dbcourse->theme); 771 $this->assertEquals($course['enablecompletion'], $dbcourse->enablecompletion); 772 if ($dbcourse->format === 'topics') { 773 $this->assertEquals($course['courseformatoptions'], array( 774 array('name' => 'hiddensections', 'value' => $dbcourse->hiddensections), 775 array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay), 776 )); 777 } 778 779 // Assert custom field that we previously added to test course 4. 780 if ($dbcourse->id == $course4->id) { 781 $this->assertEquals([ 782 'shortname' => $customfield['shortname'], 783 'name' => $customfield['name'], 784 'type' => $customfield['type'], 785 'value' => $customfieldvalue['value'], 786 'valueraw' => $customfieldvalue['value'], 787 ], $course['customfields'][0]); 788 } 789 } 790 791 // Get all courses in the DB 792 $courses = core_course_external::get_courses(array()); 793 794 // We need to execute the return values cleaning process to simulate the web service server. 795 $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses); 796 797 $this->assertEquals($DB->count_records('course'), count($courses)); 798 } 799 800 /** 801 * Test retrieving courses returns custom field data 802 */ 803 public function test_get_courses_customfields(): void { 804 $this->resetAfterTest(); 805 $this->setAdminUser(); 806 807 $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]); 808 $datefield = $this->getDataGenerator()->create_custom_field([ 809 'categoryid' => $fieldcategory->get('id'), 810 'shortname' => 'mydate', 811 'name' => 'My date', 812 'type' => 'date', 813 ]); 814 815 $newcourse = $this->getDataGenerator()->create_course(['customfields' => [ 816 [ 817 'shortname' => $datefield->get('shortname'), 818 'value' => 1580389200, // 30/01/2020 13:00 GMT. 819 ], 820 ]]); 821 822 $courses = external_api::clean_returnvalue( 823 core_course_external::get_courses_returns(), 824 core_course_external::get_courses(['ids' => [$newcourse->id]]) 825 ); 826 827 $this->assertCount(1, $courses); 828 $course = reset($courses); 829 830 $this->assertArrayHasKey('customfields', $course); 831 $this->assertCount(1, $course['customfields']); 832 833 // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version. 834 $this->assertEquals([ 835 'name' => $datefield->get('name'), 836 'shortname' => $datefield->get('shortname'), 837 'type' => $datefield->get('type'), 838 'value' => userdate(1580389200), 839 'valueraw' => 1580389200, 840 ], reset($course['customfields'])); 841 } 842 843 /** 844 * Test get_courses without capability 845 */ 846 public function test_get_courses_without_capability() { 847 $this->resetAfterTest(true); 848 849 $course1 = $this->getDataGenerator()->create_course(); 850 $this->setUser($this->getDataGenerator()->create_user()); 851 852 // No permissions are required to get the site course. 853 $courses = core_course_external::get_courses(array('ids' => [SITEID])); 854 $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses); 855 856 $this->assertEquals(1, count($courses)); 857 $this->assertEquals('PHPUnit test site', $courses[0]['fullname']); 858 $this->assertEquals('site', $courses[0]['format']); 859 860 // Requesting course without being enrolled or capability to view it will throw an exception. 861 try { 862 core_course_external::get_courses(array('ids' => [$course1->id])); 863 $this->fail('Exception expected'); 864 } catch (moodle_exception $e) { 865 $this->assertEquals(1, preg_match('/Course or activity not accessible. \(Not enrolled\)/', $e->getMessage())); 866 } 867 } 868 869 /** 870 * Test search_courses 871 */ 872 public function test_search_courses () { 873 874 global $DB; 875 876 $this->resetAfterTest(true); 877 $this->setAdminUser(); 878 $generatedcourses = array(); 879 $coursedata1['fullname'] = 'FIRST COURSE'; 880 $course1 = self::getDataGenerator()->create_course($coursedata1); 881 882 $page = new moodle_page(); 883 $page->set_course($course1); 884 $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*'); 885 886 $coursedata2['fullname'] = 'SECOND COURSE'; 887 $course2 = self::getDataGenerator()->create_course($coursedata2); 888 889 $page = new moodle_page(); 890 $page->set_course($course2); 891 $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*'); 892 893 // Search by name. 894 $results = core_course_external::search_courses('search', 'FIRST'); 895 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results); 896 $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']); 897 $this->assertCount(1, $results['courses']); 898 899 // Create the forum. 900 $record = new stdClass(); 901 $record->introformat = FORMAT_HTML; 902 $record->course = $course2->id; 903 // Set Aggregate type = Average of ratings. 904 $forum = self::getDataGenerator()->create_module('forum', $record); 905 906 // Search by module. 907 $results = core_course_external::search_courses('modulelist', 'forum'); 908 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results); 909 $this->assertEquals(1, $results['total']); 910 911 // Enable coursetag option. 912 set_config('block_tags_showcoursetags', true); 913 // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2. 914 core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id), 915 array('TAG-LABEL ON SECOND COURSE')); 916 $taginstance = $DB->get_record('tag_instance', 917 array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST); 918 919 // Search by tagid. 920 $results = core_course_external::search_courses('tagid', $taginstance->tagid); 921 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results); 922 $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']); 923 924 // Search by block (use news_items default block). 925 $blockid = $DB->get_field('block', 'id', array('name' => 'news_items')); 926 $results = core_course_external::search_courses('blocklist', $blockid); 927 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results); 928 $this->assertEquals(2, $results['total']); 929 930 // Now as a normal user. 931 $user = self::getDataGenerator()->create_user(); 932 933 // Add a 3rd, hidden, course we shouldn't see, even when enrolled as student. 934 $coursedata3['fullname'] = 'HIDDEN COURSE'; 935 $coursedata3['visible'] = 0; 936 $course3 = self::getDataGenerator()->create_course($coursedata3); 937 $this->getDataGenerator()->enrol_user($user->id, $course3->id, 'student'); 938 939 $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student'); 940 $this->setUser($user); 941 942 $results = core_course_external::search_courses('search', 'FIRST'); 943 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results); 944 $this->assertCount(1, $results['courses']); 945 $this->assertEquals(1, $results['total']); 946 $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']); 947 948 // Check that we can see all courses without the limit to enrolled setting. 949 $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 0); 950 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results); 951 $this->assertCount(2, $results['courses']); 952 $this->assertEquals(2, $results['total']); 953 954 // Check that we only see our enrolled course when limiting. 955 $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 1); 956 $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results); 957 $this->assertCount(1, $results['courses']); 958 $this->assertEquals(1, $results['total']); 959 $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']); 960 961 // Search by block (use news_items default block). Should fail (only admins allowed). 962 $this->expectException('required_capability_exception'); 963 $results = core_course_external::search_courses('blocklist', $blockid); 964 } 965 966 /** 967 * Test searching for courses returns custom field data 968 */ 969 public function test_search_courses_customfields(): void { 970 $this->resetAfterTest(); 971 $this->setAdminUser(); 972 973 $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]); 974 $datefield = $this->getDataGenerator()->create_custom_field([ 975 'categoryid' => $fieldcategory->get('id'), 976 'shortname' => 'mydate', 977 'name' => 'My date', 978 'type' => 'date', 979 ]); 980 981 $newcourse = $this->getDataGenerator()->create_course(['customfields' => [ 982 [ 983 'shortname' => $datefield->get('shortname'), 984 'value' => 1580389200, // 30/01/2020 13:00 GMT. 985 ], 986 ]]); 987 988 $result = external_api::clean_returnvalue( 989 core_course_external::search_courses_returns(), 990 core_course_external::search_courses('search', $newcourse->shortname) 991 ); 992 993 $this->assertCount(1, $result['courses']); 994 $course = reset($result['courses']); 995 996 $this->assertArrayHasKey('customfields', $course); 997 $this->assertCount(1, $course['customfields']); 998 999 // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version. 1000 $this->assertEquals([ 1001 'name' => $datefield->get('name'), 1002 'shortname' => $datefield->get('shortname'), 1003 'type' => $datefield->get('type'), 1004 'value' => userdate(1580389200), 1005 'valueraw' => 1580389200, 1006 ], reset($course['customfields'])); 1007 } 1008 1009 /** 1010 * Create a course with contents 1011 * @return array A list with the course object and course modules objects 1012 */ 1013 private function prepare_get_course_contents_test() { 1014 global $DB, $CFG; 1015 1016 $CFG->allowstealth = 1; // Allow stealth activities. 1017 $CFG->enablecompletion = true; 1018 // Course with 4 sections (apart from the main section), with completion and not displaying hidden sections. 1019 $course = self::getDataGenerator()->create_course(['numsections' => 4, 'enablecompletion' => 1, 'hiddensections' => 1]); 1020 1021 $forumdescription = 'This is the forum description'; 1022 $forum = $this->getDataGenerator()->create_module('forum', 1023 array('course' => $course->id, 'intro' => $forumdescription, 'trackingtype' => 2), 1024 array('showdescription' => true, 'completion' => COMPLETION_TRACKING_MANUAL)); 1025 $forumcm = get_coursemodule_from_id('forum', $forum->cmid); 1026 // Add discussions to the tracking forced forum. 1027 $record = new stdClass(); 1028 $record->course = $course->id; 1029 $record->userid = 0; 1030 $record->forum = $forum->id; 1031 $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record); 1032 $data = $this->getDataGenerator()->create_module('data', 1033 array('assessed' => 1, 'scale' => 100, 'course' => $course->id, 'completion' => 2, 'completionentries' => 3)); 1034 $datacm = get_coursemodule_from_instance('data', $data->id); 1035 $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id)); 1036 $pagecm = get_coursemodule_from_instance('page', $page->id); 1037 // This is an stealth page (set by visibleoncoursepage). 1038 $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0)); 1039 $labeldescription = 'This is a very long label to test if more than 50 characters are returned. 1040 So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.'; 1041 $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id, 1042 'intro' => $labeldescription, 'completion' => COMPLETION_TRACKING_MANUAL)); 1043 $labelcm = get_coursemodule_from_instance('label', $label->id); 1044 $tomorrow = time() + DAYSECS; 1045 // Module with availability restrictions not met. 1046 $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '},' 1047 .'{"type":"completion","cm":' . $label->cmid .',"e":1}],"showc":[true,true]}'; 1048 $url = $this->getDataGenerator()->create_module('url', 1049 array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2, 'display' => RESOURCELIB_DISPLAY_POPUP, 1050 'popupwidth' => 100, 'popupheight' => 100), 1051 array('availability' => $availability)); 1052 $urlcm = get_coursemodule_from_instance('url', $url->id); 1053 // Module for the last section. 1054 $this->getDataGenerator()->create_module('url', 1055 array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3)); 1056 // Module for section 1 with availability restrictions met. 1057 $yesterday = time() - DAYSECS; 1058 $this->getDataGenerator()->create_module('url', 1059 array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1), 1060 array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}')); 1061 1062 // Set the required capabilities by the external function. 1063 $context = context_course::instance($course->id); 1064 $roleid = $this->assignUserCapability('moodle/course:view', $context->id); 1065 $this->assignUserCapability('moodle/course:update', $context->id, $roleid); 1066 $this->assignUserCapability('mod/data:view', $context->id, $roleid); 1067 1068 $conditions = array('course' => $course->id, 'section' => 2); 1069 $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions); 1070 1071 // Add date availability condition not met for section 3. 1072 $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}'; 1073 $DB->set_field('course_sections', 'availability', $availability, 1074 array('course' => $course->id, 'section' => 3)); 1075 1076 // Create resource for last section. 1077 $pageinhiddensection = $this->getDataGenerator()->create_module('page', 1078 array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4)); 1079 // Set not visible last section. 1080 $DB->set_field('course_sections', 'visible', 0, 1081 array('course' => $course->id, 'section' => 4)); 1082 1083 rebuild_course_cache($course->id, true); 1084 1085 return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm); 1086 } 1087 1088 /** 1089 * Test get_course_contents 1090 */ 1091 public function test_get_course_contents() { 1092 global $CFG; 1093 $this->resetAfterTest(true); 1094 1095 $CFG->forum_allowforcedreadtracking = 1; 1096 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1097 1098 // We first run the test as admin. 1099 $this->setAdminUser(); 1100 $sections = core_course_external::get_course_contents($course->id, array()); 1101 // We need to execute the return values cleaning process to simulate the web service server. 1102 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1103 1104 $modinfo = get_fast_modinfo($course); 1105 $testexecuted = 0; 1106 foreach ($sections[0]['modules'] as $module) { 1107 if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') { 1108 $cm = $modinfo->cms[$forumcm->id]; 1109 $formattedtext = format_text($cm->content, FORMAT_HTML, 1110 array('noclean' => true, 'para' => false, 'filter' => false)); 1111 $this->assertEquals($formattedtext, $module['description']); 1112 $this->assertEquals($forumcm->instance, $module['instance']); 1113 $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']); 1114 $this->assertStringContainsString('1 unread post', $module['afterlink']); 1115 $this->assertFalse($module['noviewlink']); 1116 $this->assertNotEmpty($module['description']); // Module showdescription is on. 1117 $testexecuted = $testexecuted + 2; 1118 } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') { 1119 $cm = $modinfo->cms[$labelcm->id]; 1120 $formattedtext = format_text($cm->content, FORMAT_HTML, 1121 array('noclean' => true, 'para' => false, 'filter' => false)); 1122 $this->assertEquals($formattedtext, $module['description']); 1123 $this->assertEquals($labelcm->instance, $module['instance']); 1124 $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']); 1125 $this->assertTrue($module['noviewlink']); 1126 $this->assertNotEmpty($module['description']); // Label always prints the description. 1127 $testexecuted = $testexecuted + 1; 1128 } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') { 1129 $this->assertStringContainsString('customcompletionrules', $module['customdata']); 1130 $this->assertFalse($module['noviewlink']); 1131 $this->assertArrayNotHasKey('description', $module); 1132 $testexecuted = $testexecuted + 1; 1133 } 1134 } 1135 foreach ($sections[2]['modules'] as $module) { 1136 if ($module['id'] == $urlcm->id and $module['modname'] == 'url') { 1137 $this->assertStringContainsString('width=100,height=100', $module['onclick']); 1138 $testexecuted = $testexecuted + 1; 1139 } 1140 } 1141 1142 $CFG->forum_allowforcedreadtracking = 0; // Recover original value. 1143 forum_tp_count_forum_unread_posts($forumcm, $course, true); // Reset static cache for further tests. 1144 1145 $this->assertEquals(5, $testexecuted); 1146 $this->assertEquals(0, $sections[0]['section']); 1147 1148 $this->assertCount(5, $sections[0]['modules']); 1149 $this->assertCount(1, $sections[1]['modules']); 1150 $this->assertCount(1, $sections[2]['modules']); 1151 $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions. 1152 $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity. 1153 $this->assertNotEmpty($sections[3]['availabilityinfo']); 1154 $this->assertEquals(1, $sections[1]['section']); 1155 $this->assertEquals(2, $sections[2]['section']); 1156 $this->assertEquals(3, $sections[3]['section']); 1157 $this->assertEquals(4, $sections[4]['section']); 1158 $this->assertStringContainsString('<iframe', $sections[2]['summary']); 1159 $this->assertStringContainsString('</iframe>', $sections[2]['summary']); 1160 $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']); 1161 try { 1162 $sections = core_course_external::get_course_contents($course->id, 1163 array(array("name" => "invalid", "value" => 1))); 1164 $this->fail('Exception expected due to invalid option.'); 1165 } catch (moodle_exception $e) { 1166 $this->assertEquals('errorinvalidparam', $e->errorcode); 1167 } 1168 } 1169 1170 1171 /** 1172 * Test get_course_contents as student 1173 */ 1174 public function test_get_course_contents_student() { 1175 global $DB; 1176 $this->resetAfterTest(true); 1177 1178 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1179 1180 $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student')); 1181 $user = self::getDataGenerator()->create_user(); 1182 self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid); 1183 $this->setUser($user); 1184 1185 $sections = core_course_external::get_course_contents($course->id, array()); 1186 // We need to execute the return values cleaning process to simulate the web service server. 1187 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1188 1189 $this->assertCount(4, $sections); // Nothing for the not visible section. 1190 $this->assertCount(5, $sections[0]['modules']); 1191 $this->assertCount(1, $sections[1]['modules']); 1192 $this->assertCount(1, $sections[2]['modules']); 1193 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. 1194 1195 $this->assertNotEmpty($sections[3]['availabilityinfo']); 1196 $this->assertEquals(1, $sections[1]['section']); 1197 $this->assertEquals(2, $sections[2]['section']); 1198 $this->assertEquals(3, $sections[3]['section']); 1199 // The module with the availability restriction met is returning contents. 1200 $this->assertNotEmpty($sections[1]['modules'][0]['contents']); 1201 // The module with the availability restriction not met is not returning contents. 1202 $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]); 1203 1204 // Now include flag for returning stealth information (fake section). 1205 $sections = core_course_external::get_course_contents($course->id, 1206 array(array("name" => "includestealthmodules", "value" => 1))); 1207 // We need to execute the return values cleaning process to simulate the web service server. 1208 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1209 1210 $this->assertCount(5, $sections); // Include fake section with stealth activities. 1211 $this->assertCount(5, $sections[0]['modules']); 1212 $this->assertCount(1, $sections[1]['modules']); 1213 $this->assertCount(1, $sections[2]['modules']); 1214 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. 1215 $this->assertCount(1, $sections[4]['modules']); // One stealth module. 1216 $this->assertEquals(-1, $sections[4]['id']); 1217 } 1218 1219 /** 1220 * Test get_course_contents excluding modules 1221 */ 1222 public function test_get_course_contents_excluding_modules() { 1223 $this->resetAfterTest(true); 1224 1225 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1226 1227 // Test exclude modules. 1228 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1))); 1229 1230 // We need to execute the return values cleaning process to simulate the web service server. 1231 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1232 1233 $this->assertEmpty($sections[0]['modules']); 1234 $this->assertEmpty($sections[1]['modules']); 1235 } 1236 1237 /** 1238 * Test get_course_contents excluding contents 1239 */ 1240 public function test_get_course_contents_excluding_contents() { 1241 $this->resetAfterTest(true); 1242 1243 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1244 1245 // Test exclude modules. 1246 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1))); 1247 1248 // We need to execute the return values cleaning process to simulate the web service server. 1249 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1250 1251 foreach ($sections as $section) { 1252 foreach ($section['modules'] as $module) { 1253 // Only resources return contents. 1254 if (isset($module['contents'])) { 1255 $this->assertEmpty($module['contents']); 1256 } 1257 } 1258 } 1259 } 1260 1261 /** 1262 * Test get_course_contents filtering by section number 1263 */ 1264 public function test_get_course_contents_section_number() { 1265 $this->resetAfterTest(true); 1266 1267 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1268 1269 // Test exclude modules. 1270 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0))); 1271 1272 // We need to execute the return values cleaning process to simulate the web service server. 1273 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1274 1275 $this->assertCount(1, $sections); 1276 $this->assertCount(5, $sections[0]['modules']); 1277 } 1278 1279 /** 1280 * Test get_course_contents filtering by cmid 1281 */ 1282 public function test_get_course_contents_cmid() { 1283 $this->resetAfterTest(true); 1284 1285 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1286 1287 // Test exclude modules. 1288 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id))); 1289 1290 // We need to execute the return values cleaning process to simulate the web service server. 1291 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1292 1293 $this->assertCount(4, $sections); 1294 $this->assertCount(1, $sections[0]['modules']); 1295 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]); 1296 } 1297 1298 1299 /** 1300 * Test get_course_contents filtering by cmid and section 1301 */ 1302 public function test_get_course_contents_section_cmid() { 1303 $this->resetAfterTest(true); 1304 1305 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1306 1307 // Test exclude modules. 1308 $sections = core_course_external::get_course_contents($course->id, array( 1309 array("name" => "cmid", "value" => $forumcm->id), 1310 array("name" => "sectionnumber", "value" => 0) 1311 )); 1312 1313 // We need to execute the return values cleaning process to simulate the web service server. 1314 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1315 1316 $this->assertCount(1, $sections); 1317 $this->assertCount(1, $sections[0]['modules']); 1318 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]); 1319 } 1320 1321 /** 1322 * Test get_course_contents filtering by modname 1323 */ 1324 public function test_get_course_contents_modname() { 1325 $this->resetAfterTest(true); 1326 1327 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1328 1329 // Test exclude modules. 1330 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum"))); 1331 1332 // We need to execute the return values cleaning process to simulate the web service server. 1333 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1334 1335 $this->assertCount(4, $sections); 1336 $this->assertCount(1, $sections[0]['modules']); 1337 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]); 1338 } 1339 1340 /** 1341 * Test get_course_contents filtering by modname 1342 */ 1343 public function test_get_course_contents_modid() { 1344 $this->resetAfterTest(true); 1345 1346 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1347 1348 // Test exclude modules. 1349 $sections = core_course_external::get_course_contents($course->id, array( 1350 array("name" => "modname", "value" => "page"), 1351 array("name" => "modid", "value" => $pagecm->instance), 1352 )); 1353 1354 // We need to execute the return values cleaning process to simulate the web service server. 1355 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1356 1357 $this->assertCount(4, $sections); 1358 $this->assertCount(1, $sections[0]['modules']); 1359 $this->assertEquals("page", $sections[0]['modules'][0]["modname"]); 1360 $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]); 1361 } 1362 1363 /** 1364 * Test get course contents completion 1365 */ 1366 public function test_get_course_contents_completion() { 1367 global $CFG; 1368 $this->resetAfterTest(true); 1369 1370 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1371 availability_completion\condition::wipe_static_cache(); 1372 1373 // Test activity not completed yet. 1374 $result = core_course_external::get_course_contents($course->id, array( 1375 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance))); 1376 // We need to execute the return values cleaning process to simulate the web service server. 1377 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1378 1379 $this->assertCount(1, $result[0]['modules']); 1380 $this->assertEquals("forum", $result[0]['modules'][0]["modname"]); 1381 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]); 1382 $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['state']); 1383 $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['timecompleted']); 1384 $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']); 1385 $this->assertFalse($result[0]['modules'][0]["completiondata"]['valueused']); 1386 1387 // Set activity completed. 1388 core_completion_external::update_activity_completion_status_manually($forumcm->id, true); 1389 1390 $result = core_course_external::get_course_contents($course->id, array( 1391 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance))); 1392 // We need to execute the return values cleaning process to simulate the web service server. 1393 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1394 1395 $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']); 1396 $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']); 1397 $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']); 1398 1399 // Test activity with completion value that is used in an availability condition. 1400 $result = core_course_external::get_course_contents($course->id, array( 1401 array("name" => "modname", "value" => "label"), array("name" => "modid", "value" => $labelcm->instance))); 1402 // We need to execute the return values cleaning process to simulate the web service server. 1403 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1404 1405 $this->assertCount(1, $result[0]['modules']); 1406 $this->assertEquals("label", $result[0]['modules'][0]["modname"]); 1407 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]); 1408 $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['state']); 1409 $this->assertEquals(0, $result[0]['modules'][0]["completiondata"]['timecompleted']); 1410 $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']); 1411 $this->assertTrue($result[0]['modules'][0]["completiondata"]['valueused']); 1412 1413 // Disable completion. 1414 $CFG->enablecompletion = 0; 1415 $result = core_course_external::get_course_contents($course->id, array( 1416 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance))); 1417 // We need to execute the return values cleaning process to simulate the web service server. 1418 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1419 1420 $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]); 1421 } 1422 1423 /** 1424 * Test mimetype is returned for resources with showtype set. 1425 */ 1426 public function test_get_course_contents_including_mimetype() { 1427 $this->resetAfterTest(true); 1428 1429 $this->setAdminUser(); 1430 $course = self::getDataGenerator()->create_course(); 1431 1432 $record = new stdClass(); 1433 $record->course = $course->id; 1434 $record->showtype = 1; 1435 $resource = self::getDataGenerator()->create_module('resource', $record); 1436 1437 $result = core_course_external::get_course_contents($course->id); 1438 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1439 $this->assertCount(1, $result[0]['modules']); // One module, first section. 1440 $customdata = unserialize(json_decode($result[0]['modules'][0]['customdata'])); 1441 $this->assertEquals('text/plain', $customdata['filedetails']['mimetype']); 1442 } 1443 1444 /** 1445 * Test contents info is returned. 1446 */ 1447 public function test_get_course_contents_contentsinfo() { 1448 global $USER; 1449 1450 $this->resetAfterTest(true); 1451 $this->setAdminUser(); 1452 $timenow = time(); 1453 1454 $course = self::getDataGenerator()->create_course(); 1455 1456 $record = new stdClass(); 1457 $record->course = $course->id; 1458 // One resource with one file. 1459 $resource1 = self::getDataGenerator()->create_module('resource', $record); 1460 1461 // More type of files. 1462 $record->files = file_get_unused_draft_itemid(); 1463 $usercontext = context_user::instance($USER->id); 1464 $extensions = array('txt', 'png', 'pdf'); 1465 $fs = get_file_storage(); 1466 foreach ($extensions as $key => $extension) { 1467 // Add actual file there. 1468 $filerecord = array('component' => 'user', 'filearea' => 'draft', 1469 'contextid' => $usercontext->id, 'itemid' => $record->files, 1470 'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/'); 1471 $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file'); 1472 } 1473 1474 // Create file reference. 1475 $repos = repository::get_instances(array('type' => 'user')); 1476 $userrepository = reset($repos); 1477 1478 // Create a user private file. 1479 $userfilerecord = new stdClass; 1480 $userfilerecord->contextid = $usercontext->id; 1481 $userfilerecord->component = 'user'; 1482 $userfilerecord->filearea = 'private'; 1483 $userfilerecord->itemid = 0; 1484 $userfilerecord->filepath = '/'; 1485 $userfilerecord->filename = 'userfile.txt'; 1486 $userfilerecord->source = 'test'; 1487 $userfile = $fs->create_file_from_string($userfilerecord, 'User file content'); 1488 $userfileref = $fs->pack_reference($userfilerecord); 1489 1490 // Clone latest "normal" file. 1491 $filerefrecord = clone (object) $filerecord; 1492 $filerefrecord->filename = 'testref.txt'; 1493 $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref); 1494 // Set main file pointing to the file reference. 1495 file_set_sortorder($usercontext->id, 'user', 'draft', $record->files, $filerefrecord->filepath, 1496 $filerefrecord->filename, 1); 1497 1498 // Once the reference has been created, create the file resource. 1499 $resource2 = self::getDataGenerator()->create_module('resource', $record); 1500 1501 $result = core_course_external::get_course_contents($course->id); 1502 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1503 $this->assertCount(2, $result[0]['modules']); 1504 foreach ($result[0]['modules'] as $module) { 1505 if ($module['instance'] == $resource1->id) { 1506 $this->assertEquals(1, $module['contentsinfo']['filescount']); 1507 $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']); 1508 $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']); 1509 $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']); 1510 } else { 1511 $this->assertEquals(count($extensions) + 1, $module['contentsinfo']['filescount']); 1512 $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] + 1513 $module['contents'][2]['filesize'] + $module['contents'][3]['filesize']; 1514 $this->assertEquals($filessize, $module['contentsinfo']['filessize']); 1515 $this->assertEquals('user', $module['contentsinfo']['repositorytype']); 1516 $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']); 1517 $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']); 1518 } 1519 } 1520 } 1521 1522 /** 1523 * Test get_course_contents when hidden sections are displayed. 1524 */ 1525 public function test_get_course_contents_hiddensections() { 1526 global $DB; 1527 $this->resetAfterTest(true); 1528 1529 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1530 // Force returning hidden sections. 1531 $course->hiddensections = 0; 1532 update_course($course); 1533 1534 $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student')); 1535 $user = self::getDataGenerator()->create_user(); 1536 self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid); 1537 $this->setUser($user); 1538 1539 $sections = core_course_external::get_course_contents($course->id, array()); 1540 // We need to execute the return values cleaning process to simulate the web service server. 1541 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1542 1543 $this->assertCount(5, $sections); // All the sections, including the "not visible" one. 1544 $this->assertCount(5, $sections[0]['modules']); 1545 $this->assertCount(1, $sections[1]['modules']); 1546 $this->assertCount(1, $sections[2]['modules']); 1547 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. 1548 $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden. 1549 1550 $this->assertNotEmpty($sections[3]['availabilityinfo']); 1551 $this->assertEquals(1, $sections[1]['section']); 1552 $this->assertEquals(2, $sections[2]['section']); 1553 $this->assertEquals(3, $sections[3]['section']); 1554 // The module with the availability restriction met is returning contents. 1555 $this->assertNotEmpty($sections[1]['modules'][0]['contents']); 1556 // The module with the availability restriction not met is not returning contents. 1557 $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]); 1558 1559 // Now include flag for returning stealth information (fake section). 1560 $sections = core_course_external::get_course_contents($course->id, 1561 array(array("name" => "includestealthmodules", "value" => 1))); 1562 // We need to execute the return values cleaning process to simulate the web service server. 1563 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1564 1565 $this->assertCount(6, $sections); // Include fake section with stealth activities. 1566 $this->assertCount(5, $sections[0]['modules']); 1567 $this->assertCount(1, $sections[1]['modules']); 1568 $this->assertCount(1, $sections[2]['modules']); 1569 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. 1570 $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden. 1571 $this->assertCount(1, $sections[5]['modules']); // One stealth module. 1572 $this->assertEquals(-1, $sections[5]['id']); 1573 } 1574 1575 /** 1576 * Test duplicate_course 1577 */ 1578 public function test_duplicate_course() { 1579 $this->resetAfterTest(true); 1580 1581 // Create one course with three modules. 1582 $course = self::getDataGenerator()->create_course(); 1583 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id)); 1584 $forumcm = get_coursemodule_from_id('forum', $forum->cmid); 1585 $forumcontext = context_module::instance($forum->cmid); 1586 $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id)); 1587 $datacontext = context_module::instance($data->cmid); 1588 $datacm = get_coursemodule_from_instance('page', $data->id); 1589 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id)); 1590 $pagecontext = context_module::instance($page->cmid); 1591 $pagecm = get_coursemodule_from_instance('page', $page->id); 1592 1593 // Set the required capabilities by the external function. 1594 $coursecontext = context_course::instance($course->id); 1595 $categorycontext = context_coursecat::instance($course->category); 1596 $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id); 1597 $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid); 1598 $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid); 1599 $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid); 1600 $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid); 1601 // Optional capabilities to copy user data. 1602 $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid); 1603 $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid); 1604 1605 $newcourse['fullname'] = 'Course duplicate'; 1606 $newcourse['shortname'] = 'courseduplicate'; 1607 $newcourse['categoryid'] = $course->category; 1608 $newcourse['visible'] = true; 1609 $newcourse['options'][] = array('name' => 'users', 'value' => true); 1610 1611 $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'], 1612 $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']); 1613 1614 // We need to execute the return values cleaning process to simulate the web service server. 1615 $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate); 1616 1617 // Check that the course has been duplicated. 1618 $this->assertEquals($newcourse['shortname'], $duplicate['shortname']); 1619 } 1620 1621 /** 1622 * Test update_courses 1623 */ 1624 public function test_update_courses() { 1625 global $DB, $CFG, $USER, $COURSE; 1626 1627 // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this 1628 // trick because we are both updating and getting (for testing) course information 1629 // in the same request and core_course_external::update_courses() 1630 // is overwriting $COURSE all over the time with OLD values, so later 1631 // use of get_course() fetches those OLD values instead of the updated ones. 1632 // See MDL-39723 for more info. 1633 $origcourse = clone($COURSE); 1634 1635 $this->resetAfterTest(true); 1636 1637 // Set the required capabilities by the external function. 1638 $contextid = context_system::instance()->id; 1639 $roleid = $this->assignUserCapability('moodle/course:update', $contextid); 1640 $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid); 1641 $this->assignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid); 1642 $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid); 1643 $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid); 1644 $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid); 1645 $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid); 1646 $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid); 1647 $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid); 1648 $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid); 1649 1650 // Create category and courses. 1651 $category1 = self::getDataGenerator()->create_category(); 1652 $category2 = self::getDataGenerator()->create_category(); 1653 1654 $originalcourse1 = self::getDataGenerator()->create_course(); 1655 self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid); 1656 1657 $originalcourse2 = self::getDataGenerator()->create_course(); 1658 self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid); 1659 1660 // Course with custom fields. 1661 $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']); 1662 $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text', 1663 'categoryid' => $fieldcategory->get('id'), 1664 'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL, 'locked' => 1]]; 1665 $field = self::getDataGenerator()->create_custom_field($customfield); 1666 1667 $originalcourse3 = self::getDataGenerator()->create_course(['customfield_test' => 'Test value']); 1668 self::getDataGenerator()->enrol_user($USER->id, $originalcourse3->id, $roleid); 1669 1670 // Course values to be updated. 1671 $course1['id'] = $originalcourse1->id; 1672 $course1['fullname'] = 'Updated test course 1'; 1673 $course1['shortname'] = 'Udestedtestcourse1'; 1674 $course1['categoryid'] = $category1->id; 1675 1676 $course2['id'] = $originalcourse2->id; 1677 $course2['fullname'] = 'Updated test course 2'; 1678 $course2['shortname'] = 'Updestedtestcourse2'; 1679 $course2['categoryid'] = $category2->id; 1680 $course2['idnumber'] = 'Updatedidnumber2'; 1681 $course2['summary'] = 'Updaated description for course 2'; 1682 $course2['summaryformat'] = FORMAT_HTML; 1683 $course2['format'] = 'topics'; 1684 $course2['showgrades'] = 1; 1685 $course2['newsitems'] = 3; 1686 $course2['startdate'] = 1420092000; // 01/01/2015. 1687 $course2['enddate'] = 1422669600; // 01/31/2015. 1688 $course2['maxbytes'] = 100000; 1689 $course2['showreports'] = 1; 1690 $course2['visible'] = 0; 1691 $course2['hiddensections'] = 0; 1692 $course2['groupmode'] = 0; 1693 $course2['groupmodeforce'] = 0; 1694 $course2['defaultgroupingid'] = 0; 1695 $course2['enablecompletion'] = 1; 1696 $course2['lang'] = 'en'; 1697 $course2['forcetheme'] = 'classic'; 1698 1699 $course3['id'] = $originalcourse3->id; 1700 $updatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'Updated test value']; 1701 $course3['customfields'] = [$updatedcustomfieldvalue]; 1702 $courses = array($course1, $course2, $course3); 1703 1704 $updatedcoursewarnings = core_course_external::update_courses($courses); 1705 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1706 $updatedcoursewarnings); 1707 $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line. 1708 1709 // Check that right number of courses were created. 1710 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 1711 1712 // Check that the courses were correctly created. 1713 foreach ($courses as $course) { 1714 $courseinfo = course_get_format($course['id'])->get_course(); 1715 $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course['id']); 1716 if ($course['id'] == $course2['id']) { 1717 $this->assertEquals($course2['fullname'], $courseinfo->fullname); 1718 $this->assertEquals($course2['shortname'], $courseinfo->shortname); 1719 $this->assertEquals($course2['categoryid'], $courseinfo->category); 1720 $this->assertEquals($course2['idnumber'], $courseinfo->idnumber); 1721 $this->assertEquals($course2['summary'], $courseinfo->summary); 1722 $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat); 1723 $this->assertEquals($course2['format'], $courseinfo->format); 1724 $this->assertEquals($course2['showgrades'], $courseinfo->showgrades); 1725 $this->assertEquals($course2['newsitems'], $courseinfo->newsitems); 1726 $this->assertEquals($course2['startdate'], $courseinfo->startdate); 1727 $this->assertEquals($course2['enddate'], $courseinfo->enddate); 1728 $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes); 1729 $this->assertEquals($course2['showreports'], $courseinfo->showreports); 1730 $this->assertEquals($course2['visible'], $courseinfo->visible); 1731 $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections); 1732 $this->assertEquals($course2['groupmode'], $courseinfo->groupmode); 1733 $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce); 1734 $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid); 1735 $this->assertEquals($course2['lang'], $courseinfo->lang); 1736 1737 if (!empty($CFG->allowcoursethemes)) { 1738 $this->assertEquals($course2['forcetheme'], $courseinfo->theme); 1739 } 1740 1741 $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion); 1742 $this->assertEquals(['test' => null], (array)$customfields); 1743 } else if ($course['id'] == $course1['id']) { 1744 $this->assertEquals($course1['fullname'], $courseinfo->fullname); 1745 $this->assertEquals($course1['shortname'], $courseinfo->shortname); 1746 $this->assertEquals($course1['categoryid'], $courseinfo->category); 1747 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat); 1748 $this->assertEquals('topics', $courseinfo->format); 1749 $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number()); 1750 $this->assertEquals(0, $courseinfo->newsitems); 1751 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat); 1752 $this->assertEquals(['test' => null], (array)$customfields); 1753 } else if ($course['id'] == $course3['id']) { 1754 $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields); 1755 } else { 1756 throw new moodle_exception('Unexpected shortname'); 1757 } 1758 } 1759 1760 $courses = array($course1); 1761 // Try update course without update capability. 1762 $user = self::getDataGenerator()->create_user(); 1763 $this->setUser($user); 1764 $this->unassignUserCapability('moodle/course:update', $contextid, $roleid); 1765 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1766 $updatedcoursewarnings = core_course_external::update_courses($courses); 1767 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1768 $updatedcoursewarnings); 1769 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1770 1771 // Try update course category without capability. 1772 $this->assignUserCapability('moodle/course:update', $contextid, $roleid); 1773 $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid); 1774 $user = self::getDataGenerator()->create_user(); 1775 $this->setUser($user); 1776 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1777 $course1['categoryid'] = $category2->id; 1778 $courses = array($course1); 1779 $updatedcoursewarnings = core_course_external::update_courses($courses); 1780 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1781 $updatedcoursewarnings); 1782 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1783 1784 // Try update course fullname without capability. 1785 $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid); 1786 $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid); 1787 $user = self::getDataGenerator()->create_user(); 1788 $this->setUser($user); 1789 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1790 $updatedcoursewarnings = core_course_external::update_courses($courses); 1791 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1792 $updatedcoursewarnings); 1793 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 1794 $course1['fullname'] = 'Testing fullname without permission'; 1795 $courses = array($course1); 1796 $updatedcoursewarnings = core_course_external::update_courses($courses); 1797 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1798 $updatedcoursewarnings); 1799 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1800 1801 // Try update course shortname without capability. 1802 $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid); 1803 $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid); 1804 $user = self::getDataGenerator()->create_user(); 1805 $this->setUser($user); 1806 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1807 $updatedcoursewarnings = core_course_external::update_courses($courses); 1808 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1809 $updatedcoursewarnings); 1810 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 1811 $course1['shortname'] = 'Testing shortname without permission'; 1812 $courses = array($course1); 1813 $updatedcoursewarnings = core_course_external::update_courses($courses); 1814 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1815 $updatedcoursewarnings); 1816 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1817 1818 // Try update course idnumber without capability. 1819 $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid); 1820 $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid); 1821 $user = self::getDataGenerator()->create_user(); 1822 $this->setUser($user); 1823 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1824 $updatedcoursewarnings = core_course_external::update_courses($courses); 1825 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1826 $updatedcoursewarnings); 1827 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 1828 $course1['idnumber'] = 'NEWIDNUMBER'; 1829 $courses = array($course1); 1830 $updatedcoursewarnings = core_course_external::update_courses($courses); 1831 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1832 $updatedcoursewarnings); 1833 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1834 1835 // Try update course summary without capability. 1836 $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid); 1837 $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid); 1838 $user = self::getDataGenerator()->create_user(); 1839 $this->setUser($user); 1840 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1841 $updatedcoursewarnings = core_course_external::update_courses($courses); 1842 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1843 $updatedcoursewarnings); 1844 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 1845 $course1['summary'] = 'New summary'; 1846 $courses = array($course1); 1847 $updatedcoursewarnings = core_course_external::update_courses($courses); 1848 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1849 $updatedcoursewarnings); 1850 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1851 1852 // Try update course with invalid summary format. 1853 $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid); 1854 $user = self::getDataGenerator()->create_user(); 1855 $this->setUser($user); 1856 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1857 $updatedcoursewarnings = core_course_external::update_courses($courses); 1858 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1859 $updatedcoursewarnings); 1860 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 1861 $course1['summaryformat'] = 10; 1862 $courses = array($course1); 1863 $updatedcoursewarnings = core_course_external::update_courses($courses); 1864 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1865 $updatedcoursewarnings); 1866 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1867 1868 // Try update course visibility without capability. 1869 $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid); 1870 $user = self::getDataGenerator()->create_user(); 1871 $this->setUser($user); 1872 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1873 $course1['summaryformat'] = FORMAT_MOODLE; 1874 $courses = array($course1); 1875 $updatedcoursewarnings = core_course_external::update_courses($courses); 1876 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1877 $updatedcoursewarnings); 1878 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 1879 $course1['visible'] = 0; 1880 $courses = array($course1); 1881 $updatedcoursewarnings = core_course_external::update_courses($courses); 1882 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1883 $updatedcoursewarnings); 1884 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1885 1886 // Try update course custom fields without capability. 1887 $this->unassignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid); 1888 $user = self::getDataGenerator()->create_user(); 1889 $this->setUser($user); 1890 self::getDataGenerator()->enrol_user($user->id, $course3['id'], $roleid); 1891 1892 $newupdatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'New updated value']; 1893 $course3['customfields'] = [$newupdatedcustomfieldvalue]; 1894 1895 core_course_external::update_courses([$course3]); 1896 1897 // Custom field was not updated. 1898 $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course3['id']); 1899 $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields); 1900 } 1901 1902 /** 1903 * Test delete course_module. 1904 */ 1905 public function test_delete_modules() { 1906 global $DB; 1907 1908 // Ensure we reset the data after this test. 1909 $this->resetAfterTest(true); 1910 1911 // Create a user. 1912 $user = self::getDataGenerator()->create_user(); 1913 1914 // Set the tests to run as the user. 1915 self::setUser($user); 1916 1917 // Create a course to add the modules. 1918 $course = self::getDataGenerator()->create_course(); 1919 1920 // Create two test modules. 1921 $record = new stdClass(); 1922 $record->course = $course->id; 1923 $module1 = self::getDataGenerator()->create_module('forum', $record); 1924 $module2 = self::getDataGenerator()->create_module('assign', $record); 1925 1926 // Check the forum was correctly created. 1927 $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id))); 1928 1929 // Check the assignment was correctly created. 1930 $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id))); 1931 1932 // Check data exists in the course modules table. 1933 $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2', 1934 array('module1' => $module1->cmid, 'module2' => $module2->cmid))); 1935 1936 // Enrol the user in the course. 1937 $enrol = enrol_get_plugin('manual'); 1938 $enrolinstances = enrol_get_instances($course->id, true); 1939 foreach ($enrolinstances as $courseenrolinstance) { 1940 if ($courseenrolinstance->enrol == "manual") { 1941 $instance = $courseenrolinstance; 1942 break; 1943 } 1944 } 1945 $enrol->enrol_user($instance, $user->id); 1946 1947 // Assign capabilities to delete module 1. 1948 $modcontext = context_module::instance($module1->cmid); 1949 $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id); 1950 1951 // Assign capabilities to delete module 2. 1952 $modcontext = context_module::instance($module2->cmid); 1953 $newrole = create_role('Role 2', 'role2', 'Role 2 description'); 1954 $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole); 1955 1956 // Deleting these module instances. 1957 core_course_external::delete_modules(array($module1->cmid, $module2->cmid)); 1958 1959 // Check the forum was deleted. 1960 $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id))); 1961 1962 // Check the assignment was deleted. 1963 $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id))); 1964 1965 // Check we retrieve no data in the course modules table. 1966 $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2', 1967 array('module1' => $module1->cmid, 'module2' => $module2->cmid))); 1968 1969 // Call with non-existent course module id and ensure exception thrown. 1970 try { 1971 core_course_external::delete_modules(array('1337')); 1972 $this->fail('Exception expected due to missing course module.'); 1973 } catch (dml_missing_record_exception $e) { 1974 $this->assertEquals('invalidcoursemodule', $e->errorcode); 1975 } 1976 1977 // Create two modules. 1978 $module1 = self::getDataGenerator()->create_module('forum', $record); 1979 $module2 = self::getDataGenerator()->create_module('assign', $record); 1980 1981 // Since these modules were recreated the user will not have capabilities 1982 // to delete them, ensure exception is thrown if they try. 1983 try { 1984 core_course_external::delete_modules(array($module1->cmid, $module2->cmid)); 1985 $this->fail('Exception expected due to missing capability.'); 1986 } catch (moodle_exception $e) { 1987 $this->assertEquals('nopermissions', $e->errorcode); 1988 } 1989 1990 // Unenrol user from the course. 1991 $enrol->unenrol_user($instance, $user->id); 1992 1993 // Try and delete modules from the course the user was unenrolled in, make sure exception thrown. 1994 try { 1995 core_course_external::delete_modules(array($module1->cmid, $module2->cmid)); 1996 $this->fail('Exception expected due to being unenrolled from the course.'); 1997 } catch (moodle_exception $e) { 1998 $this->assertEquals('requireloginerror', $e->errorcode); 1999 } 2000 } 2001 2002 /** 2003 * Test import_course into an empty course 2004 */ 2005 public function test_import_course_empty() { 2006 global $USER; 2007 2008 $this->resetAfterTest(true); 2009 2010 $course1 = self::getDataGenerator()->create_course(); 2011 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test')); 2012 $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test')); 2013 2014 $course2 = self::getDataGenerator()->create_course(); 2015 2016 $course1cms = get_fast_modinfo($course1->id)->get_cms(); 2017 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2018 2019 // Verify the state of the courses before we do the import. 2020 $this->assertCount(2, $course1cms); 2021 $this->assertEmpty($course2cms); 2022 2023 // Setup the user to run the operation (ugly hack because validate_context() will 2024 // fail as the email is not set by $this->setAdminUser()). 2025 $this->setAdminUser(); 2026 $USER->email = 'emailtopass@example.com'; 2027 2028 // Import from course1 to course2. 2029 core_course_external::import_course($course1->id, $course2->id, 0); 2030 2031 // Verify that now we have two modules in both courses. 2032 $course1cms = get_fast_modinfo($course1->id)->get_cms(); 2033 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2034 $this->assertCount(2, $course1cms); 2035 $this->assertCount(2, $course2cms); 2036 2037 // Verify that the names transfered across correctly. 2038 foreach ($course2cms as $cm) { 2039 if ($cm->modname === 'page') { 2040 $this->assertEquals($cm->name, $page->name); 2041 } else if ($cm->modname === 'forum') { 2042 $this->assertEquals($cm->name, $forum->name); 2043 } else { 2044 $this->fail('Unknown CM found.'); 2045 } 2046 } 2047 } 2048 2049 /** 2050 * Test import_course into an filled course 2051 */ 2052 public function test_import_course_filled() { 2053 global $USER; 2054 2055 $this->resetAfterTest(true); 2056 2057 // Add forum and page to course1. 2058 $course1 = self::getDataGenerator()->create_course(); 2059 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test')); 2060 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test')); 2061 2062 // Add quiz to course 2. 2063 $course2 = self::getDataGenerator()->create_course(); 2064 $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test')); 2065 2066 $course1cms = get_fast_modinfo($course1->id)->get_cms(); 2067 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2068 2069 // Verify the state of the courses before we do the import. 2070 $this->assertCount(2, $course1cms); 2071 $this->assertCount(1, $course2cms); 2072 2073 // Setup the user to run the operation (ugly hack because validate_context() will 2074 // fail as the email is not set by $this->setAdminUser()). 2075 $this->setAdminUser(); 2076 $USER->email = 'emailtopass@example.com'; 2077 2078 // Import from course1 to course2 without deleting content. 2079 core_course_external::import_course($course1->id, $course2->id, 0); 2080 2081 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2082 2083 // Verify that now we have three modules in course2. 2084 $this->assertCount(3, $course2cms); 2085 2086 // Verify that the names transfered across correctly. 2087 foreach ($course2cms as $cm) { 2088 if ($cm->modname === 'page') { 2089 $this->assertEquals($cm->name, $page->name); 2090 } else if ($cm->modname === 'forum') { 2091 $this->assertEquals($cm->name, $forum->name); 2092 } else if ($cm->modname === 'quiz') { 2093 $this->assertEquals($cm->name, $quiz->name); 2094 } else { 2095 $this->fail('Unknown CM found.'); 2096 } 2097 } 2098 } 2099 2100 /** 2101 * Test import_course with only blocks set to backup 2102 */ 2103 public function test_import_course_blocksonly() { 2104 global $USER, $DB; 2105 2106 $this->resetAfterTest(true); 2107 2108 // Add forum and page to course1. 2109 $course1 = self::getDataGenerator()->create_course(); 2110 $course1ctx = context_course::instance($course1->id); 2111 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test')); 2112 $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id)); 2113 2114 $course2 = self::getDataGenerator()->create_course(); 2115 $course2ctx = context_course::instance($course2->id); 2116 $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id)); 2117 $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms()); 2118 2119 // Setup the user to run the operation (ugly hack because validate_context() will 2120 // fail as the email is not set by $this->setAdminUser()). 2121 $this->setAdminUser(); 2122 $USER->email = 'emailtopass@example.com'; 2123 2124 // Import from course1 to course2 without deleting content, but excluding 2125 // activities. 2126 $options = array( 2127 array('name' => 'activities', 'value' => 0), 2128 array('name' => 'blocks', 'value' => 1), 2129 array('name' => 'filters', 'value' => 0), 2130 ); 2131 2132 core_course_external::import_course($course1->id, $course2->id, 0, $options); 2133 2134 $newcmcount = count(get_fast_modinfo($course2->id)->get_cms()); 2135 $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id)); 2136 // Check that course modules haven't changed, but that blocks have. 2137 $this->assertEquals($initialcmcount, $newcmcount); 2138 $this->assertEquals(($initialblockcount + 1), $newblockcount); 2139 } 2140 2141 /** 2142 * Test import_course into an filled course, deleting content. 2143 */ 2144 public function test_import_course_deletecontent() { 2145 global $USER; 2146 $this->resetAfterTest(true); 2147 2148 // Add forum and page to course1. 2149 $course1 = self::getDataGenerator()->create_course(); 2150 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test')); 2151 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test')); 2152 2153 // Add quiz to course 2. 2154 $course2 = self::getDataGenerator()->create_course(); 2155 $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test')); 2156 2157 $course1cms = get_fast_modinfo($course1->id)->get_cms(); 2158 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2159 2160 // Verify the state of the courses before we do the import. 2161 $this->assertCount(2, $course1cms); 2162 $this->assertCount(1, $course2cms); 2163 2164 // Setup the user to run the operation (ugly hack because validate_context() will 2165 // fail as the email is not set by $this->setAdminUser()). 2166 $this->setAdminUser(); 2167 $USER->email = 'emailtopass@example.com'; 2168 2169 // Import from course1 to course2, deleting content. 2170 core_course_external::import_course($course1->id, $course2->id, 1); 2171 2172 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2173 2174 // Verify that now we have two modules in course2. 2175 $this->assertCount(2, $course2cms); 2176 2177 // Verify that the course only contains the imported modules. 2178 foreach ($course2cms as $cm) { 2179 if ($cm->modname === 'page') { 2180 $this->assertEquals($cm->name, $page->name); 2181 } else if ($cm->modname === 'forum') { 2182 $this->assertEquals($cm->name, $forum->name); 2183 } else { 2184 $this->fail('Unknown CM found: '.$cm->name); 2185 } 2186 } 2187 } 2188 2189 /** 2190 * Ensure import_course handles incorrect deletecontent option correctly. 2191 */ 2192 public function test_import_course_invalid_deletecontent_option() { 2193 $this->resetAfterTest(true); 2194 2195 $course1 = self::getDataGenerator()->create_course(); 2196 $course2 = self::getDataGenerator()->create_course(); 2197 2198 $this->expectException('moodle_exception'); 2199 $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1)); 2200 // Import from course1 to course2, with invalid option 2201 core_course_external::import_course($course1->id, $course2->id, -1);; 2202 } 2203 2204 /** 2205 * Test view_course function 2206 */ 2207 public function test_view_course() { 2208 2209 $this->resetAfterTest(); 2210 2211 // Course without sections. 2212 $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true)); 2213 $this->setAdminUser(); 2214 2215 // Redirect events to the sink, so we can recover them later. 2216 $sink = $this->redirectEvents(); 2217 2218 $result = core_course_external::view_course($course->id, 1); 2219 $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result); 2220 $events = $sink->get_events(); 2221 $event = reset($events); 2222 2223 // Check the event details are correct. 2224 $this->assertInstanceOf('\core\event\course_viewed', $event); 2225 $this->assertEquals(context_course::instance($course->id), $event->get_context()); 2226 $this->assertEquals(1, $event->other['coursesectionnumber']); 2227 2228 $result = core_course_external::view_course($course->id); 2229 $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result); 2230 $events = $sink->get_events(); 2231 $event = array_pop($events); 2232 $sink->close(); 2233 2234 // Check the event details are correct. 2235 $this->assertInstanceOf('\core\event\course_viewed', $event); 2236 $this->assertEquals(context_course::instance($course->id), $event->get_context()); 2237 $this->assertEmpty($event->other); 2238 2239 } 2240 2241 /** 2242 * Test get_course_module 2243 */ 2244 public function test_get_course_module() { 2245 global $DB; 2246 2247 $this->resetAfterTest(true); 2248 2249 $this->setAdminUser(); 2250 $course = self::getDataGenerator()->create_course(); 2251 $record = array( 2252 'course' => $course->id, 2253 'name' => 'First Assignment' 2254 ); 2255 $options = array( 2256 'idnumber' => 'ABC', 2257 'visible' => 0 2258 ); 2259 // Hidden activity. 2260 $assign = self::getDataGenerator()->create_module('assign', $record, $options); 2261 2262 $outcomescale = 'Distinction, Very Good, Good, Pass, Fail'; 2263 2264 // Insert a custom grade scale to be used by an outcome. 2265 $gradescale = new grade_scale(); 2266 $gradescale->name = 'gettcoursemodulescale'; 2267 $gradescale->courseid = $course->id; 2268 $gradescale->userid = 0; 2269 $gradescale->scale = $outcomescale; 2270 $gradescale->description = 'This scale is used to mark standard assignments.'; 2271 $gradescale->insert(); 2272 2273 // Insert an outcome. 2274 $data = new stdClass(); 2275 $data->courseid = $course->id; 2276 $data->fullname = 'Team work'; 2277 $data->shortname = 'Team work'; 2278 $data->scaleid = $gradescale->id; 2279 $outcome = new grade_outcome($data, false); 2280 $outcome->insert(); 2281 2282 $outcomegradeitem = new grade_item(); 2283 $outcomegradeitem->itemname = $outcome->shortname; 2284 $outcomegradeitem->itemtype = 'mod'; 2285 $outcomegradeitem->itemmodule = 'assign'; 2286 $outcomegradeitem->iteminstance = $assign->id; 2287 $outcomegradeitem->outcomeid = $outcome->id; 2288 $outcomegradeitem->cmid = 0; 2289 $outcomegradeitem->courseid = $course->id; 2290 $outcomegradeitem->aggregationcoef = 0; 2291 $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000. 2292 $outcomegradeitem->gradetype = GRADE_TYPE_SCALE; 2293 $outcomegradeitem->scaleid = $outcome->scaleid; 2294 $outcomegradeitem->insert(); 2295 2296 $assignmentgradeitem = grade_item::fetch( 2297 array( 2298 'itemtype' => 'mod', 2299 'itemmodule' => 'assign', 2300 'iteminstance' => $assign->id, 2301 'itemnumber' => 0, 2302 'courseid' => $course->id 2303 ) 2304 ); 2305 $outcomegradeitem->set_parent($assignmentgradeitem->categoryid); 2306 $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder); 2307 2308 // Test admin user can see the complete hidden activity. 2309 $result = core_course_external::get_course_module($assign->cmid); 2310 $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result); 2311 2312 $this->assertCount(0, $result['warnings']); 2313 // Test we retrieve all the fields. 2314 $this->assertCount(28, $result['cm']); 2315 $this->assertEquals($record['name'], $result['cm']['name']); 2316 $this->assertEquals($options['idnumber'], $result['cm']['idnumber']); 2317 $this->assertEquals(100, $result['cm']['grade']); 2318 $this->assertEquals(0.0, $result['cm']['gradepass']); 2319 $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']); 2320 $this->assertEmpty($result['cm']['advancedgrading'][0]['method']); 2321 $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']); 2322 2323 $student = $this->getDataGenerator()->create_user(); 2324 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 2325 2326 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 2327 $this->setUser($student); 2328 2329 // The user shouldn't be able to see the activity. 2330 try { 2331 core_course_external::get_course_module($assign->cmid); 2332 $this->fail('Exception expected due to invalid permissions.'); 2333 } catch (moodle_exception $e) { 2334 $this->assertEquals('requireloginerror', $e->errorcode); 2335 } 2336 2337 // Make module visible. 2338 set_coursemodule_visible($assign->cmid, 1); 2339 2340 // Test student user. 2341 $result = core_course_external::get_course_module($assign->cmid); 2342 $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result); 2343 2344 $this->assertCount(0, $result['warnings']); 2345 // Test we retrieve only the few files we can see. 2346 $this->assertCount(11, $result['cm']); 2347 $this->assertEquals($assign->cmid, $result['cm']['id']); 2348 $this->assertEquals($course->id, $result['cm']['course']); 2349 $this->assertEquals('assign', $result['cm']['modname']); 2350 $this->assertEquals($assign->id, $result['cm']['instance']); 2351 2352 } 2353 2354 /** 2355 * Test get_course_module_by_instance 2356 */ 2357 public function test_get_course_module_by_instance() { 2358 global $DB; 2359 2360 $this->resetAfterTest(true); 2361 2362 $this->setAdminUser(); 2363 $course = self::getDataGenerator()->create_course(); 2364 $record = array( 2365 'course' => $course->id, 2366 'name' => 'First quiz', 2367 'grade' => 90.00 2368 ); 2369 $options = array( 2370 'idnumber' => 'ABC', 2371 'visible' => 0 2372 ); 2373 // Hidden activity. 2374 $quiz = self::getDataGenerator()->create_module('quiz', $record, $options); 2375 2376 // Test admin user can see the complete hidden activity. 2377 $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id); 2378 $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result); 2379 2380 $this->assertCount(0, $result['warnings']); 2381 // Test we retrieve all the fields. 2382 $this->assertCount(26, $result['cm']); 2383 $this->assertEquals($record['name'], $result['cm']['name']); 2384 $this->assertEquals($record['grade'], $result['cm']['grade']); 2385 $this->assertEquals($options['idnumber'], $result['cm']['idnumber']); 2386 2387 $student = $this->getDataGenerator()->create_user(); 2388 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 2389 2390 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 2391 $this->setUser($student); 2392 2393 // The user shouldn't be able to see the activity. 2394 try { 2395 core_course_external::get_course_module_by_instance('quiz', $quiz->id); 2396 $this->fail('Exception expected due to invalid permissions.'); 2397 } catch (moodle_exception $e) { 2398 $this->assertEquals('requireloginerror', $e->errorcode); 2399 } 2400 2401 // Make module visible. 2402 set_coursemodule_visible($quiz->cmid, 1); 2403 2404 // Test student user. 2405 $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id); 2406 $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result); 2407 2408 $this->assertCount(0, $result['warnings']); 2409 // Test we retrieve only the few files we can see. 2410 $this->assertCount(11, $result['cm']); 2411 $this->assertEquals($quiz->cmid, $result['cm']['id']); 2412 $this->assertEquals($course->id, $result['cm']['course']); 2413 $this->assertEquals('quiz', $result['cm']['modname']); 2414 $this->assertEquals($quiz->id, $result['cm']['instance']); 2415 2416 // Try with an invalid module name. 2417 try { 2418 core_course_external::get_course_module_by_instance('abc', $quiz->id); 2419 $this->fail('Exception expected due to invalid module name.'); 2420 } catch (dml_read_exception $e) { 2421 $this->assertEquals('dmlreadexception', $e->errorcode); 2422 } 2423 2424 } 2425 2426 /** 2427 * Test get_user_navigation_options 2428 */ 2429 public function test_get_user_navigation_options() { 2430 global $USER; 2431 2432 $this->resetAfterTest(); 2433 $course1 = self::getDataGenerator()->create_course(); 2434 $course2 = self::getDataGenerator()->create_course(); 2435 2436 // Create a viewer user. 2437 $viewer = self::getDataGenerator()->create_user(); 2438 $this->getDataGenerator()->enrol_user($viewer->id, $course1->id); 2439 $this->getDataGenerator()->enrol_user($viewer->id, $course2->id); 2440 2441 $this->setUser($viewer->id); 2442 $courses = array($course1->id , $course2->id, SITEID); 2443 2444 $result = core_course_external::get_user_navigation_options($courses); 2445 $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result); 2446 2447 $this->assertCount(0, $result['warnings']); 2448 $this->assertCount(3, $result['courses']); 2449 2450 foreach ($result['courses'] as $course) { 2451 $navoptions = new stdClass; 2452 foreach ($course['options'] as $option) { 2453 $navoptions->{$option['name']} = $option['available']; 2454 } 2455 $this->assertCount(9, $course['options']); 2456 if ($course['id'] == SITEID) { 2457 $this->assertTrue($navoptions->blogs); 2458 $this->assertFalse($navoptions->notes); 2459 $this->assertFalse($navoptions->participants); 2460 $this->assertTrue($navoptions->badges); 2461 $this->assertTrue($navoptions->tags); 2462 $this->assertFalse($navoptions->grades); 2463 $this->assertFalse($navoptions->search); 2464 $this->assertTrue($navoptions->calendar); 2465 $this->assertTrue($navoptions->competencies); 2466 } else { 2467 $this->assertTrue($navoptions->blogs); 2468 $this->assertFalse($navoptions->notes); 2469 $this->assertTrue($navoptions->participants); 2470 $this->assertTrue($navoptions->badges); 2471 $this->assertFalse($navoptions->tags); 2472 $this->assertTrue($navoptions->grades); 2473 $this->assertFalse($navoptions->search); 2474 $this->assertFalse($navoptions->calendar); 2475 $this->assertTrue($navoptions->competencies); 2476 } 2477 } 2478 } 2479 2480 /** 2481 * Test get_user_administration_options 2482 */ 2483 public function test_get_user_administration_options() { 2484 global $USER; 2485 2486 $this->resetAfterTest(); 2487 $course1 = self::getDataGenerator()->create_course(); 2488 $course2 = self::getDataGenerator()->create_course(); 2489 2490 // Create a viewer user. 2491 $viewer = self::getDataGenerator()->create_user(); 2492 $this->getDataGenerator()->enrol_user($viewer->id, $course1->id); 2493 $this->getDataGenerator()->enrol_user($viewer->id, $course2->id); 2494 2495 $this->setUser($viewer->id); 2496 $courses = array($course1->id , $course2->id, SITEID); 2497 2498 $result = core_course_external::get_user_administration_options($courses); 2499 $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result); 2500 2501 $this->assertCount(0, $result['warnings']); 2502 $this->assertCount(3, $result['courses']); 2503 2504 foreach ($result['courses'] as $course) { 2505 $adminoptions = new stdClass; 2506 foreach ($course['options'] as $option) { 2507 $adminoptions->{$option['name']} = $option['available']; 2508 } 2509 if ($course['id'] == SITEID) { 2510 $this->assertCount(17, $course['options']); 2511 $this->assertFalse($adminoptions->update); 2512 $this->assertFalse($adminoptions->filters); 2513 $this->assertFalse($adminoptions->reports); 2514 $this->assertFalse($adminoptions->backup); 2515 $this->assertFalse($adminoptions->restore); 2516 $this->assertFalse($adminoptions->files); 2517 $this->assertFalse(!isset($adminoptions->tags)); 2518 $this->assertFalse($adminoptions->gradebook); 2519 $this->assertFalse($adminoptions->outcomes); 2520 $this->assertFalse($adminoptions->badges); 2521 $this->assertFalse($adminoptions->import); 2522 $this->assertFalse($adminoptions->reset); 2523 $this->assertFalse($adminoptions->roles); 2524 $this->assertFalse($adminoptions->editcompletion); 2525 $this->assertFalse($adminoptions->copy); 2526 } else { 2527 $this->assertCount(15, $course['options']); 2528 $this->assertFalse($adminoptions->update); 2529 $this->assertFalse($adminoptions->filters); 2530 $this->assertFalse($adminoptions->reports); 2531 $this->assertFalse($adminoptions->backup); 2532 $this->assertFalse($adminoptions->restore); 2533 $this->assertFalse($adminoptions->files); 2534 $this->assertFalse($adminoptions->tags); 2535 $this->assertFalse($adminoptions->gradebook); 2536 $this->assertFalse($adminoptions->outcomes); 2537 $this->assertTrue($adminoptions->badges); 2538 $this->assertFalse($adminoptions->import); 2539 $this->assertFalse($adminoptions->reset); 2540 $this->assertFalse($adminoptions->roles); 2541 $this->assertFalse($adminoptions->editcompletion); 2542 $this->assertFalse($adminoptions->copy); 2543 } 2544 } 2545 } 2546 2547 /** 2548 * Test get_courses_by_fields 2549 */ 2550 public function test_get_courses_by_field() { 2551 global $DB; 2552 $this->resetAfterTest(true); 2553 2554 $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1')); 2555 $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id)); 2556 $course1 = self::getDataGenerator()->create_course( 2557 array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics')); 2558 2559 $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']); 2560 $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text', 2561 'categoryid' => $fieldcategory->get('id')]; 2562 $field = self::getDataGenerator()->create_custom_field($customfield); 2563 $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value']; 2564 $course2 = self::getDataGenerator()->create_course(array('visible' => 0, 'category' => $category2->id, 'idnumber' => 'i2', 'customfields' => [$customfieldvalue])); 2565 2566 $student1 = self::getDataGenerator()->create_user(); 2567 $user1 = self::getDataGenerator()->create_user(); 2568 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 2569 self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id); 2570 self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id); 2571 2572 self::setAdminUser(); 2573 // As admins, we should be able to retrieve everything. 2574 $result = core_course_external::get_courses_by_field(); 2575 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2576 $this->assertCount(3, $result['courses']); 2577 // Expect to receive all the fields. 2578 $this->assertCount(38, $result['courses'][0]); 2579 $this->assertCount(39, $result['courses'][1]); // One more field because is not the site course. 2580 $this->assertCount(39, $result['courses'][2]); // One more field because is not the site course. 2581 2582 $result = core_course_external::get_courses_by_field('id', $course1->id); 2583 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2584 $this->assertCount(1, $result['courses']); 2585 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2586 // Expect to receive all the fields. 2587 $this->assertCount(39, $result['courses'][0]); 2588 // Check default values for course format topics. 2589 $this->assertCount(2, $result['courses'][0]['courseformatoptions']); 2590 foreach ($result['courses'][0]['courseformatoptions'] as $option) { 2591 if ($option['name'] == 'hiddensections') { 2592 $this->assertEquals(0, $option['value']); 2593 } else { 2594 $this->assertEquals('coursedisplay', $option['name']); 2595 $this->assertEquals(0, $option['value']); 2596 } 2597 } 2598 2599 $result = core_course_external::get_courses_by_field('id', $course2->id); 2600 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2601 $this->assertCount(1, $result['courses']); 2602 $this->assertEquals($course2->id, $result['courses'][0]['id']); 2603 // Check custom fields properly returned. 2604 $this->assertEquals([ 2605 'shortname' => $customfield['shortname'], 2606 'name' => $customfield['name'], 2607 'type' => $customfield['type'], 2608 'value' => $customfieldvalue['value'], 2609 'valueraw' => $customfieldvalue['value'], 2610 ], $result['courses'][0]['customfields'][0]); 2611 2612 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id"); 2613 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2614 $this->assertCount(2, $result['courses']); 2615 2616 // Check default filters. 2617 $this->assertCount(6, $result['courses'][0]['filters']); 2618 $this->assertCount(6, $result['courses'][1]['filters']); 2619 2620 $result = core_course_external::get_courses_by_field('category', $category1->id); 2621 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2622 $this->assertCount(1, $result['courses']); 2623 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2624 $this->assertEquals('Cat 1', $result['courses'][0]['categoryname']); 2625 2626 $result = core_course_external::get_courses_by_field('shortname', 'c1'); 2627 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2628 $this->assertCount(1, $result['courses']); 2629 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2630 2631 $result = core_course_external::get_courses_by_field('idnumber', 'i2'); 2632 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2633 $this->assertCount(1, $result['courses']); 2634 $this->assertEquals($course2->id, $result['courses'][0]['id']); 2635 2636 $result = core_course_external::get_courses_by_field('idnumber', 'x'); 2637 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2638 $this->assertCount(0, $result['courses']); 2639 2640 // Change filter value. 2641 filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF); 2642 2643 self::setUser($student1); 2644 // All visible courses (including front page) for normal student. 2645 $result = core_course_external::get_courses_by_field(); 2646 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2647 $this->assertCount(2, $result['courses']); 2648 $this->assertCount(31, $result['courses'][0]); 2649 $this->assertCount(32, $result['courses'][1]); // One field more (course format options), not present in site course. 2650 2651 $result = core_course_external::get_courses_by_field('id', $course1->id); 2652 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2653 $this->assertCount(1, $result['courses']); 2654 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2655 // Expect to receive all the files that a student can see. 2656 $this->assertCount(32, $result['courses'][0]); 2657 2658 // Check default filters. 2659 $filters = $result['courses'][0]['filters']; 2660 $this->assertCount(6, $filters); 2661 $found = false; 2662 foreach ($filters as $filter) { 2663 if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) { 2664 $found = true; 2665 } 2666 } 2667 $this->assertTrue($found); 2668 2669 // Course 2 is not visible. 2670 $result = core_course_external::get_courses_by_field('id', $course2->id); 2671 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2672 $this->assertCount(0, $result['courses']); 2673 2674 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id"); 2675 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2676 $this->assertCount(1, $result['courses']); 2677 2678 $result = core_course_external::get_courses_by_field('category', $category1->id); 2679 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2680 $this->assertCount(1, $result['courses']); 2681 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2682 2683 $result = core_course_external::get_courses_by_field('shortname', 'c1'); 2684 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2685 $this->assertCount(1, $result['courses']); 2686 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2687 2688 $result = core_course_external::get_courses_by_field('idnumber', 'i2'); 2689 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2690 $this->assertCount(0, $result['courses']); 2691 2692 $result = core_course_external::get_courses_by_field('idnumber', 'x'); 2693 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2694 $this->assertCount(0, $result['courses']); 2695 2696 self::setUser($user1); 2697 // All visible courses (including front page) for authenticated user. 2698 $result = core_course_external::get_courses_by_field(); 2699 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2700 $this->assertCount(2, $result['courses']); 2701 $this->assertCount(31, $result['courses'][0]); // Site course. 2702 $this->assertCount(14, $result['courses'][1]); // Only public information, not enrolled. 2703 2704 $result = core_course_external::get_courses_by_field('id', $course1->id); 2705 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2706 $this->assertCount(1, $result['courses']); 2707 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2708 // Expect to receive all the files that a authenticated can see. 2709 $this->assertCount(14, $result['courses'][0]); 2710 2711 // Course 2 is not visible. 2712 $result = core_course_external::get_courses_by_field('id', $course2->id); 2713 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2714 $this->assertCount(0, $result['courses']); 2715 2716 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id"); 2717 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2718 $this->assertCount(1, $result['courses']); 2719 2720 $result = core_course_external::get_courses_by_field('category', $category1->id); 2721 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2722 $this->assertCount(1, $result['courses']); 2723 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2724 2725 $result = core_course_external::get_courses_by_field('shortname', 'c1'); 2726 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2727 $this->assertCount(1, $result['courses']); 2728 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2729 2730 $result = core_course_external::get_courses_by_field('idnumber', 'i2'); 2731 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2732 $this->assertCount(0, $result['courses']); 2733 2734 $result = core_course_external::get_courses_by_field('idnumber', 'x'); 2735 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2736 $this->assertCount(0, $result['courses']); 2737 } 2738 2739 /** 2740 * Test retrieving courses by field returns custom field data 2741 */ 2742 public function test_get_courses_by_field_customfields(): void { 2743 $this->resetAfterTest(); 2744 $this->setAdminUser(); 2745 2746 $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]); 2747 $datefield = $this->getDataGenerator()->create_custom_field([ 2748 'categoryid' => $fieldcategory->get('id'), 2749 'shortname' => 'mydate', 2750 'name' => 'My date', 2751 'type' => 'date', 2752 ]); 2753 2754 $newcourse = $this->getDataGenerator()->create_course(['customfields' => [ 2755 [ 2756 'shortname' => $datefield->get('shortname'), 2757 'value' => 1580389200, // 30/01/2020 13:00 GMT. 2758 ], 2759 ]]); 2760 2761 $result = external_api::clean_returnvalue( 2762 core_course_external::get_courses_by_field_returns(), 2763 core_course_external::get_courses_by_field('id', $newcourse->id) 2764 ); 2765 2766 $this->assertCount(1, $result['courses']); 2767 $course = reset($result['courses']); 2768 2769 $this->assertArrayHasKey('customfields', $course); 2770 $this->assertCount(1, $course['customfields']); 2771 2772 // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version. 2773 $this->assertEquals([ 2774 'name' => $datefield->get('name'), 2775 'shortname' => $datefield->get('shortname'), 2776 'type' => $datefield->get('type'), 2777 'value' => userdate(1580389200), 2778 'valueraw' => 1580389200, 2779 ], reset($course['customfields'])); 2780 } 2781 2782 public function test_get_courses_by_field_invalid_field() { 2783 $this->expectException('invalid_parameter_exception'); 2784 $result = core_course_external::get_courses_by_field('zyx', 'x'); 2785 } 2786 2787 public function test_get_courses_by_field_invalid_courses() { 2788 $result = core_course_external::get_courses_by_field('id', '-1'); 2789 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2790 $this->assertCount(0, $result['courses']); 2791 } 2792 2793 /** 2794 * Test get_courses_by_field_invalid_theme_and_lang 2795 */ 2796 public function test_get_courses_by_field_invalid_theme_and_lang() { 2797 $this->resetAfterTest(true); 2798 $this->setAdminUser(); 2799 2800 $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl')); 2801 $result = core_course_external::get_courses_by_field('id', $course->id); 2802 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2803 $this->assertEmpty($result['courses']['0']['theme']); 2804 $this->assertEmpty($result['courses']['0']['lang']); 2805 } 2806 2807 2808 public function test_check_updates() { 2809 global $DB; 2810 $this->resetAfterTest(true); 2811 $this->setAdminUser(); 2812 2813 // Create different types of activities. 2814 $course = self::getDataGenerator()->create_course(); 2815 $tocreate = array('assign', 'book', 'choice', 'folder', 'forum', 'glossary', 'imscp', 'label', 'lti', 'page', 'quiz', 2816 'resource', 'scorm', 'survey', 'url', 'wiki'); 2817 2818 $modules = array(); 2819 foreach ($tocreate as $modname) { 2820 $modules[$modname]['instance'] = $this->getDataGenerator()->create_module($modname, array('course' => $course->id)); 2821 $modules[$modname]['cm'] = get_coursemodule_from_id(false, $modules[$modname]['instance']->cmid); 2822 $modules[$modname]['context'] = context_module::instance($modules[$modname]['instance']->cmid); 2823 } 2824 2825 $student = self::getDataGenerator()->create_user(); 2826 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 2827 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 2828 $this->setUser($student); 2829 2830 $since = time(); 2831 $this->waitForSecond(); 2832 $params = array(); 2833 foreach ($modules as $modname => $data) { 2834 $params[$data['cm']->id] = array( 2835 'contextlevel' => 'module', 2836 'id' => $data['cm']->id, 2837 'since' => $since 2838 ); 2839 } 2840 2841 // Check there is nothing updated because modules are fresh new. 2842 $result = core_course_external::check_updates($course->id, $params); 2843 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result); 2844 $this->assertCount(0, $result['instances']); 2845 $this->assertCount(0, $result['warnings']); 2846 2847 // Test with get_updates_since the same data. 2848 $result = core_course_external::get_updates_since($course->id, $since); 2849 $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result); 2850 $this->assertCount(0, $result['instances']); 2851 $this->assertCount(0, $result['warnings']); 2852 2853 // Update a module after a second. 2854 $this->waitForSecond(); 2855 set_coursemodule_name($modules['forum']['cm']->id, 'New forum name'); 2856 2857 $found = false; 2858 $result = core_course_external::check_updates($course->id, $params); 2859 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result); 2860 $this->assertCount(1, $result['instances']); 2861 $this->assertCount(0, $result['warnings']); 2862 foreach ($result['instances'] as $module) { 2863 foreach ($module['updates'] as $update) { 2864 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') { 2865 $found = true; 2866 } 2867 } 2868 } 2869 $this->assertTrue($found); 2870 2871 // Test with get_updates_since the same data. 2872 $result = core_course_external::get_updates_since($course->id, $since); 2873 $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result); 2874 $this->assertCount(1, $result['instances']); 2875 $this->assertCount(0, $result['warnings']); 2876 $found = false; 2877 $this->assertCount(1, $result['instances']); 2878 $this->assertCount(0, $result['warnings']); 2879 foreach ($result['instances'] as $module) { 2880 foreach ($module['updates'] as $update) { 2881 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') { 2882 $found = true; 2883 } 2884 } 2885 } 2886 $this->assertTrue($found); 2887 2888 // Do not retrieve the configuration field. 2889 $filter = array('files'); 2890 $found = false; 2891 $result = core_course_external::check_updates($course->id, $params, $filter); 2892 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result); 2893 $this->assertCount(0, $result['instances']); 2894 $this->assertCount(0, $result['warnings']); 2895 $this->assertFalse($found); 2896 2897 // Add invalid cmid. 2898 $params[] = array( 2899 'contextlevel' => 'module', 2900 'id' => -2, 2901 'since' => $since 2902 ); 2903 $result = core_course_external::check_updates($course->id, $params); 2904 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result); 2905 $this->assertCount(1, $result['warnings']); 2906 $this->assertEquals(-2, $result['warnings'][0]['itemid']); 2907 } 2908 2909 /** 2910 * Test cases for the get_enrolled_courses_by_timeline_classification test. 2911 */ 2912 public function get_get_enrolled_courses_by_timeline_classification_test_cases():array { 2913 $now = time(); 2914 $day = 86400; 2915 2916 $coursedata = [ 2917 [ 2918 'shortname' => 'apast', 2919 'startdate' => $now - ($day * 2), 2920 'enddate' => $now - $day 2921 ], 2922 [ 2923 'shortname' => 'bpast', 2924 'startdate' => $now - ($day * 2), 2925 'enddate' => $now - $day 2926 ], 2927 [ 2928 'shortname' => 'cpast', 2929 'startdate' => $now - ($day * 2), 2930 'enddate' => $now - $day 2931 ], 2932 [ 2933 'shortname' => 'dpast', 2934 'startdate' => $now - ($day * 2), 2935 'enddate' => $now - $day 2936 ], 2937 [ 2938 'shortname' => 'epast', 2939 'startdate' => $now - ($day * 2), 2940 'enddate' => $now - $day 2941 ], 2942 [ 2943 'shortname' => 'ainprogress', 2944 'startdate' => $now - $day, 2945 'enddate' => $now + $day 2946 ], 2947 [ 2948 'shortname' => 'binprogress', 2949 'startdate' => $now - $day, 2950 'enddate' => $now + $day 2951 ], 2952 [ 2953 'shortname' => 'cinprogress', 2954 'startdate' => $now - $day, 2955 'enddate' => $now + $day 2956 ], 2957 [ 2958 'shortname' => 'dinprogress', 2959 'startdate' => $now - $day, 2960 'enddate' => $now + $day 2961 ], 2962 [ 2963 'shortname' => 'einprogress', 2964 'startdate' => $now - $day, 2965 'enddate' => $now + $day 2966 ], 2967 [ 2968 'shortname' => 'afuture', 2969 'startdate' => $now + $day 2970 ], 2971 [ 2972 'shortname' => 'bfuture', 2973 'startdate' => $now + $day 2974 ], 2975 [ 2976 'shortname' => 'cfuture', 2977 'startdate' => $now + $day 2978 ], 2979 [ 2980 'shortname' => 'dfuture', 2981 'startdate' => $now + $day 2982 ], 2983 [ 2984 'shortname' => 'efuture', 2985 'startdate' => $now + $day 2986 ] 2987 ]; 2988 2989 // Raw enrolled courses result set should be returned in this order: 2990 // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast, 2991 // dfuture, dinprogress, dpast, efuture, einprogress, epast 2992 // 2993 // By classification the offset values for each record should be: 2994 // COURSE_TIMELINE_FUTURE 2995 // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture) 2996 // COURSE_TIMELINE_INPROGRESS 2997 // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress) 2998 // COURSE_TIMELINE_PAST 2999 // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast). 3000 // 3001 // NOTE: The offset applies to the unfiltered full set of courses before the classification 3002 // filtering is done. 3003 // E.g. In our example if an offset of 2 is given then it would mean the first 3004 // two courses (afuture, ainprogress) are ignored. 3005 return [ 3006 'empty set' => [ 3007 'coursedata' => [], 3008 'classification' => 'future', 3009 'limit' => 2, 3010 'offset' => 0, 3011 'sort' => 'shortname ASC', 3012 'expectedcourses' => [], 3013 'expectednextoffset' => 0 3014 ], 3015 // COURSE_TIMELINE_FUTURE. 3016 'future not limit no offset' => [ 3017 'coursedata' => $coursedata, 3018 'classification' => 'future', 3019 'limit' => 0, 3020 'offset' => 0, 3021 'sort' => 'shortname ASC', 3022 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'], 3023 'expectednextoffset' => 15 3024 ], 3025 'future no offset' => [ 3026 'coursedata' => $coursedata, 3027 'classification' => 'future', 3028 'limit' => 2, 3029 'offset' => 0, 3030 'sort' => 'shortname ASC', 3031 'expectedcourses' => ['afuture', 'bfuture'], 3032 'expectednextoffset' => 4 3033 ], 3034 'future offset' => [ 3035 'coursedata' => $coursedata, 3036 'classification' => 'future', 3037 'limit' => 2, 3038 'offset' => 2, 3039 'sort' => 'shortname ASC', 3040 'expectedcourses' => ['bfuture', 'cfuture'], 3041 'expectednextoffset' => 7 3042 ], 3043 'future exact limit' => [ 3044 'coursedata' => $coursedata, 3045 'classification' => 'future', 3046 'limit' => 5, 3047 'offset' => 0, 3048 'sort' => 'shortname ASC', 3049 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'], 3050 'expectednextoffset' => 13 3051 ], 3052 'future limit less results' => [ 3053 'coursedata' => $coursedata, 3054 'classification' => 'future', 3055 'limit' => 10, 3056 'offset' => 0, 3057 'sort' => 'shortname ASC', 3058 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'], 3059 'expectednextoffset' => 15 3060 ], 3061 'future limit less results with offset' => [ 3062 'coursedata' => $coursedata, 3063 'classification' => 'future', 3064 'limit' => 10, 3065 'offset' => 5, 3066 'sort' => 'shortname ASC', 3067 'expectedcourses' => ['cfuture', 'dfuture', 'efuture'], 3068 'expectednextoffset' => 15 3069 ], 3070 'all no limit or offset' => [ 3071 'coursedata' => $coursedata, 3072 'classification' => 'all', 3073 'limit' => 0, 3074 'offset' => 0, 3075 'sort' => 'shortname ASC', 3076 'expectedcourses' => [ 3077 'afuture', 3078 'ainprogress', 3079 'apast', 3080 'bfuture', 3081 'binprogress', 3082 'bpast', 3083 'cfuture', 3084 'cinprogress', 3085 'cpast', 3086 'dfuture', 3087 'dinprogress', 3088 'dpast', 3089 'efuture', 3090 'einprogress', 3091 'epast' 3092 ], 3093 'expectednextoffset' => 15 3094 ], 3095 'all limit no offset' => [ 3096 'coursedata' => $coursedata, 3097 'classification' => 'all', 3098 'limit' => 5, 3099 'offset' => 0, 3100 'sort' => 'shortname ASC', 3101 'expectedcourses' => [ 3102 'afuture', 3103 'ainprogress', 3104 'apast', 3105 'bfuture', 3106 'binprogress' 3107 ], 3108 'expectednextoffset' => 5 3109 ], 3110 'all limit and offset' => [ 3111 'coursedata' => $coursedata, 3112 'classification' => 'all', 3113 'limit' => 5, 3114 'offset' => 5, 3115 'sort' => 'shortname ASC', 3116 'expectedcourses' => [ 3117 'bpast', 3118 'cfuture', 3119 'cinprogress', 3120 'cpast', 3121 'dfuture' 3122 ], 3123 'expectednextoffset' => 10 3124 ], 3125 'all offset past result set' => [ 3126 'coursedata' => $coursedata, 3127 'classification' => 'all', 3128 'limit' => 5, 3129 'offset' => 50, 3130 'sort' => 'shortname ASC', 3131 'expectedcourses' => [], 3132 'expectednextoffset' => 50 3133 ], 3134 'all limit and offset with sort ul.timeaccess desc' => [ 3135 'coursedata' => $coursedata, 3136 'classification' => 'inprogress', 3137 'limit' => 0, 3138 'offset' => 0, 3139 'sort' => 'ul.timeaccess desc', 3140 'expectedcourses' => [ 3141 'ainprogress', 3142 'binprogress', 3143 'cinprogress', 3144 'dinprogress', 3145 'einprogress' 3146 ], 3147 'expectednextoffset' => 15 3148 ], 3149 'all limit and offset with sort sql injection for sort or 1==1' => [ 3150 'coursedata' => $coursedata, 3151 'classification' => 'all', 3152 'limit' => 5, 3153 'offset' => 5, 3154 'sort' => 'ul.timeaccess desc or 1==1', 3155 'expectedcourses' => [], 3156 'expectednextoffset' => 0, 3157 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()' 3158 ], 3159 'all limit and offset with sql injection of sort a custom one' => [ 3160 'coursedata' => $coursedata, 3161 'classification' => 'all', 3162 'limit' => 5, 3163 'offset' => 5, 3164 'sort' => "ul.timeaccess LIMIT 1--", 3165 'expectedcourses' => [], 3166 'expectednextoffset' => 0, 3167 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()' 3168 ], 3169 'all limit and offset with wrong sort direction' => [ 3170 'coursedata' => $coursedata, 3171 'classification' => 'all', 3172 'limit' => 5, 3173 'offset' => 5, 3174 'sort' => "ul.timeaccess abcdasc", 3175 'expectedcourses' => [], 3176 'expectednextoffset' => 0, 3177 'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()' 3178 ], 3179 'all limit and offset with wrong sort direction' => [ 3180 'coursedata' => $coursedata, 3181 'classification' => 'all', 3182 'limit' => 5, 3183 'offset' => 5, 3184 'sort' => "ul.timeaccess.foo ascd", 3185 'expectedcourses' => [], 3186 'expectednextoffset' => 0, 3187 'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()' 3188 ], 3189 'all limit and offset with wrong sort param' => [ 3190 'coursedata' => $coursedata, 3191 'classification' => 'all', 3192 'limit' => 5, 3193 'offset' => 5, 3194 'sort' => "foobar", 3195 'expectedcourses' => [], 3196 'expectednextoffset' => 0, 3197 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()' 3198 ], 3199 'all limit and offset with wrong field name' => [ 3200 'coursedata' => $coursedata, 3201 'classification' => 'all', 3202 'limit' => 5, 3203 'offset' => 5, 3204 'sort' => "ul.foobar", 3205 'expectedcourses' => [], 3206 'expectednextoffset' => 0, 3207 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()' 3208 ], 3209 'all limit and offset with wrong field separator' => [ 3210 'coursedata' => $coursedata, 3211 'classification' => 'all', 3212 'limit' => 5, 3213 'offset' => 5, 3214 'sort' => "ul.timeaccess.foo", 3215 'expectedcourses' => [], 3216 'expectednextoffset' => 0, 3217 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()' 3218 ], 3219 'all limit and offset with wrong field separator #' => [ 3220 'coursedata' => $coursedata, 3221 'classification' => 'all', 3222 'limit' => 5, 3223 'offset' => 5, 3224 'sort' => "ul#timeaccess", 3225 'expectedcourses' => [], 3226 'expectednextoffset' => 0, 3227 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()' 3228 ], 3229 'all limit and offset with wrong field separator $' => [ 3230 'coursedata' => $coursedata, 3231 'classification' => 'all', 3232 'limit' => 5, 3233 'offset' => 5, 3234 'sort' => 'ul$timeaccess', 3235 'expectedcourses' => [], 3236 'expectednextoffset' => 0, 3237 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()' 3238 ], 3239 'all limit and offset with wrong field name' => [ 3240 'coursedata' => $coursedata, 3241 'classification' => 'all', 3242 'limit' => 5, 3243 'offset' => 5, 3244 'sort' => 'timeaccess123', 3245 'expectedcourses' => [], 3246 'expectednextoffset' => 0, 3247 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()' 3248 ], 3249 'all limit and offset with no sort direction for ul' => [ 3250 'coursedata' => $coursedata, 3251 'classification' => 'inprogress', 3252 'limit' => 0, 3253 'offset' => 0, 3254 'sort' => "ul.timeaccess", 3255 'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'], 3256 'expectednextoffset' => 15, 3257 ], 3258 'all limit and offset with valid field name and no prefix, test for ul' => [ 3259 'coursedata' => $coursedata, 3260 'classification' => 'inprogress', 3261 'limit' => 0, 3262 'offset' => 0, 3263 'sort' => "timeaccess", 3264 'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'], 3265 'expectednextoffset' => 15, 3266 ], 3267 'all limit and offset with valid field name and no prefix' => [ 3268 'coursedata' => $coursedata, 3269 'classification' => 'all', 3270 'limit' => 5, 3271 'offset' => 5, 3272 'sort' => "fullname", 3273 'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'], 3274 'expectednextoffset' => 10, 3275 ], 3276 'all limit and offset with valid field name and no prefix and with sort direction' => [ 3277 'coursedata' => $coursedata, 3278 'classification' => 'all', 3279 'limit' => 5, 3280 'offset' => 5, 3281 'sort' => "fullname desc", 3282 'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'], 3283 'expectednextoffset' => 10, 3284 ], 3285 ]; 3286 } 3287 3288 /** 3289 * Test the get_enrolled_courses_by_timeline_classification function. 3290 * 3291 * @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases() 3292 * @param array $coursedata Courses to create 3293 * @param string $classification Timeline classification 3294 * @param int $limit Maximum number of results 3295 * @param int $offset Offset the unfiltered courses result set by this amount 3296 * @param string $sort sort the courses 3297 * @param array $expectedcourses Expected courses in result 3298 * @param int $expectednextoffset Expected next offset value in result 3299 * @param string|null $expectedexception Expected exception string 3300 */ 3301 public function test_get_enrolled_courses_by_timeline_classification( 3302 $coursedata, 3303 $classification, 3304 $limit, 3305 $offset, 3306 $sort, 3307 $expectedcourses, 3308 $expectednextoffset, 3309 $expectedexception = null 3310 ) { 3311 $this->resetAfterTest(); 3312 $generator = $this->getDataGenerator(); 3313 3314 $courses = array_map(function($coursedata) use ($generator) { 3315 return $generator->create_course($coursedata); 3316 }, $coursedata); 3317 3318 $student = $generator->create_user(); 3319 3320 foreach ($courses as $course) { 3321 $generator->enrol_user($student->id, $course->id, 'student'); 3322 } 3323 3324 $this->setUser($student); 3325 3326 if (isset($expectedexception)) { 3327 $this->expectException('coding_exception'); 3328 $this->expectExceptionMessage($expectedexception); 3329 } 3330 3331 // NOTE: The offset applies to the unfiltered full set of courses before the classification 3332 // filtering is done. 3333 // E.g. In our example if an offset of 2 is given then it would mean the first 3334 // two courses (afuture, ainprogress) are ignored. 3335 $result = core_course_external::get_enrolled_courses_by_timeline_classification( 3336 $classification, 3337 $limit, 3338 $offset, 3339 $sort 3340 ); 3341 $result = external_api::clean_returnvalue( 3342 core_course_external::get_enrolled_courses_by_timeline_classification_returns(), 3343 $result 3344 ); 3345 3346 $actual = array_map(function($course) { 3347 return $course['shortname']; 3348 }, $result['courses']); 3349 3350 $this->assertEqualsCanonicalizing($expectedcourses, $actual); 3351 $this->assertEquals($expectednextoffset, $result['nextoffset']); 3352 } 3353 3354 /** 3355 * Test the get_recent_courses function. 3356 */ 3357 public function test_get_recent_courses() { 3358 global $USER, $DB; 3359 3360 $this->resetAfterTest(); 3361 $generator = $this->getDataGenerator(); 3362 3363 set_config('hiddenuserfields', 'lastaccess'); 3364 3365 $courses = array(); 3366 for ($i = 1; $i < 12; $i++) { 3367 $courses[] = $generator->create_course(); 3368 }; 3369 3370 $student = $generator->create_user(); 3371 $teacher = $generator->create_user(); 3372 3373 foreach ($courses as $course) { 3374 $generator->enrol_user($student->id, $course->id, 'student'); 3375 } 3376 3377 $generator->enrol_user($teacher->id, $courses[0]->id, 'teacher'); 3378 3379 $this->setUser($student); 3380 3381 $result = core_course_external::get_recent_courses($USER->id); 3382 3383 // No course accessed. 3384 $this->assertCount(0, $result); 3385 3386 foreach ($courses as $course) { 3387 core_course_external::view_course($course->id); 3388 } 3389 3390 // Every course accessed. 3391 $result = core_course_external::get_recent_courses($USER->id); 3392 $this->assertCount( 11, $result); 3393 3394 // Every course accessed, result limited to 10 courses. 3395 $result = core_course_external::get_recent_courses($USER->id, 10); 3396 $this->assertCount(10, $result); 3397 3398 $guestcourse = $generator->create_course( 3399 (object)array('shortname' => 'guestcourse', 3400 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED, 3401 'enrol_guest_password_0' => '')); 3402 core_course_external::view_course($guestcourse->id); 3403 3404 // Every course accessed, even the not enrolled one. 3405 $result = core_course_external::get_recent_courses($USER->id); 3406 $this->assertCount(12, $result); 3407 3408 // Offset 5, return 7 out of 12. 3409 $result = core_course_external::get_recent_courses($USER->id, 0, 5); 3410 $this->assertCount(7, $result); 3411 3412 // Offset 5 and limit 3, return 3 out of 12. 3413 $result = core_course_external::get_recent_courses($USER->id, 3, 5); 3414 $this->assertCount(3, $result); 3415 3416 // Sorted by course id ASC. 3417 $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id ASC'); 3418 $this->assertEquals($courses[0]->id, array_shift($result)->id); 3419 3420 // Sorted by course id DESC. 3421 $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id DESC'); 3422 $this->assertEquals($guestcourse->id, array_shift($result)->id); 3423 3424 // If last access is hidden, only get the courses where has viewhiddenuserfields capability. 3425 $this->setUser($teacher); 3426 $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); 3427 $usercontext = context_user::instance($student->id); 3428 $this->assignUserCapability('moodle/user:viewdetails', $usercontext, $teacherroleid); 3429 3430 // Sorted by course id DESC. 3431 $result = core_course_external::get_recent_courses($student->id); 3432 $this->assertCount(1, $result); 3433 $this->assertEquals($courses[0]->id, array_shift($result)->id); 3434 } 3435 3436 /** 3437 * Test get enrolled users by cmid function. 3438 */ 3439 public function test_get_enrolled_users_by_cmid() { 3440 global $PAGE; 3441 $this->resetAfterTest(true); 3442 3443 $user1 = self::getDataGenerator()->create_user(); 3444 $user2 = self::getDataGenerator()->create_user(); 3445 3446 $user1picture = new user_picture($user1); 3447 $user1picture->size = 1; 3448 $user1->profileimage = $user1picture->get_url($PAGE)->out(false); 3449 3450 $user2picture = new user_picture($user2); 3451 $user2picture->size = 1; 3452 $user2->profileimage = $user2picture->get_url($PAGE)->out(false); 3453 3454 // Set the first created user to the test user. 3455 self::setUser($user1); 3456 3457 // Create course to add the module. 3458 $course1 = self::getDataGenerator()->create_course(); 3459 3460 // Forum with tracking off. 3461 $record = new stdClass(); 3462 $record->course = $course1->id; 3463 $forum1 = self::getDataGenerator()->create_module('forum', $record); 3464 3465 // Following lines enrol and assign default role id to the users. 3466 $this->getDataGenerator()->enrol_user($user1->id, $course1->id); 3467 $this->getDataGenerator()->enrol_user($user2->id, $course1->id); 3468 3469 // Create what we expect to be returned when querying the course module. 3470 $expectedusers = array( 3471 'users' => array(), 3472 'warnings' => array(), 3473 ); 3474 3475 $expectedusers['users'][0] = [ 3476 'id' => $user1->id, 3477 'fullname' => fullname($user1), 3478 'firstname' => $user1->firstname, 3479 'lastname' => $user1->lastname, 3480 'profileimage' => $user1->profileimage, 3481 ]; 3482 $expectedusers['users'][1] = [ 3483 'id' => $user2->id, 3484 'fullname' => fullname($user2), 3485 'firstname' => $user2->firstname, 3486 'lastname' => $user2->lastname, 3487 'profileimage' => $user2->profileimage, 3488 ]; 3489 3490 // Test getting the users in a given context. 3491 $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid); 3492 $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users); 3493 3494 $this->assertEquals(2, count($users['users'])); 3495 $this->assertEquals($expectedusers, $users); 3496 } 3497 3498 /** 3499 * Verify that content items can be added to user favourites. 3500 */ 3501 public function test_add_content_item_to_user_favourites() { 3502 $this->resetAfterTest(); 3503 3504 $course = $this->getDataGenerator()->create_course(); 3505 $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); 3506 $this->setUser($user); 3507 3508 // Using the internal API, confirm that no items are set as favourites for the user. 3509 $contentitemservice = new \core_course\local\service\content_item_service( 3510 new \core_course\local\repository\content_item_readonly_repository() 3511 ); 3512 $contentitems = $contentitemservice->get_all_content_items($user); 3513 $favourited = array_filter($contentitems, function($contentitem) { 3514 return $contentitem->favourite == true; 3515 }); 3516 $this->assertCount(0, $favourited); 3517 3518 // Using the external API, favourite a content item for the user. 3519 $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))]; 3520 $contentitem = core_course_external::add_content_item_to_user_favourites('mod_assign', $assign->id, $user->id); 3521 $contentitem = external_api::clean_returnvalue(core_course_external::add_content_item_to_user_favourites_returns(), 3522 $contentitem); 3523 3524 // Verify the returned item is a favourite. 3525 $this->assertTrue($contentitem['favourite']); 3526 3527 // Using the internal API, confirm we see a single favourite item. 3528 $contentitems = $contentitemservice->get_all_content_items($user); 3529 $favourited = array_values(array_filter($contentitems, function($contentitem) { 3530 return $contentitem->favourite == true; 3531 })); 3532 $this->assertCount(1, $favourited); 3533 $this->assertEquals('assign', $favourited[0]->name); 3534 } 3535 3536 /** 3537 * Verify that content items can be removed from user favourites. 3538 */ 3539 public function test_remove_content_item_from_user_favourites() { 3540 $this->resetAfterTest(); 3541 3542 $course = $this->getDataGenerator()->create_course(); 3543 $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); 3544 $this->setUser($user); 3545 3546 // Using the internal API, set a favourite for the user. 3547 $contentitemservice = new \core_course\local\service\content_item_service( 3548 new \core_course\local\repository\content_item_readonly_repository() 3549 ); 3550 $contentitems = $contentitemservice->get_all_content_items($user); 3551 $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))]; 3552 $contentitemservice->add_to_user_favourites($user, $assign->componentname, $assign->id); 3553 3554 $contentitems = $contentitemservice->get_all_content_items($user); 3555 $favourited = array_filter($contentitems, function($contentitem) { 3556 return $contentitem->favourite == true; 3557 }); 3558 $this->assertCount(1, $favourited); 3559 3560 // Now, verify the external API can remove the favourite. 3561 $contentitem = core_course_external::remove_content_item_from_user_favourites('mod_assign', $assign->id); 3562 $contentitem = external_api::clean_returnvalue(core_course_external::remove_content_item_from_user_favourites_returns(), 3563 $contentitem); 3564 3565 // Verify the returned item is a favourite. 3566 $this->assertFalse($contentitem['favourite']); 3567 3568 // Using the internal API, confirm we see no favourite items. 3569 $contentitems = $contentitemservice->get_all_content_items($user); 3570 $favourited = array_filter($contentitems, function($contentitem) { 3571 return $contentitem->favourite == true; 3572 }); 3573 $this->assertCount(0, $favourited); 3574 } 3575 3576 /** 3577 * Test the web service returning course content items for inclusion in activity choosers, etc. 3578 */ 3579 public function test_get_course_content_items() { 3580 $this->resetAfterTest(); 3581 3582 $course = self::getDataGenerator()->create_course(); 3583 $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher'); 3584 3585 // Fetch available content items as the editing teacher. 3586 $this->setUser($user); 3587 $result = core_course_external::get_course_content_items($course->id); 3588 $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result); 3589 3590 $contentitemservice = new \core_course\local\service\content_item_service( 3591 new \core_course\local\repository\content_item_readonly_repository() 3592 ); 3593 3594 // Check if the webservice returns exactly what the service defines, albeit in array form. 3595 $serviceitemsasarray = array_map(function($item) { 3596 return (array) $item; 3597 }, $contentitemservice->get_content_items_for_user_in_course($user, $course)); 3598 3599 $this->assertEquals($serviceitemsasarray, $result['content_items']); 3600 } 3601 3602 /** 3603 * Test the web service returning course content items, specifically in case where the user can't manage activities. 3604 */ 3605 public function test_get_course_content_items_no_permission_to_manage() { 3606 $this->resetAfterTest(); 3607 3608 $course = self::getDataGenerator()->create_course(); 3609 $user = self::getDataGenerator()->create_and_enrol($course, 'student'); 3610 3611 // Fetch available content items as a student, who won't have the permission to manage activities. 3612 $this->setUser($user); 3613 $result = core_course_external::get_course_content_items($course->id); 3614 $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result); 3615 3616 $this->assertEmpty($result['content_items']); 3617 } 3618 3619 /** 3620 * Test toggling the recommendation of an activity. 3621 */ 3622 public function test_toggle_activity_recommendation() { 3623 global $CFG; 3624 3625 $this->resetAfterTest(); 3626 3627 $context = context_system::instance(); 3628 $usercontext = context_user::instance($CFG->siteguest); 3629 $component = 'core_course'; 3630 $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext); 3631 3632 $areaname = 'test_core'; 3633 $areaid = 3; 3634 3635 // Test we have the favourite. 3636 $this->setAdminUser(); 3637 $result = core_course_external::toggle_activity_recommendation($areaname, $areaid); 3638 $this->assertTrue($favouritefactory->favourite_exists($component, 3639 \core_course\local\service\content_item_service::RECOMMENDATION_PREFIX . $areaname, $areaid, $context)); 3640 $this->assertTrue($result['status']); 3641 // Test that it is now gone. 3642 $result = core_course_external::toggle_activity_recommendation($areaname, $areaid); 3643 $this->assertFalse($favouritefactory->favourite_exists($component, $areaname, $areaid, $context)); 3644 $this->assertFalse($result['status']); 3645 } 3646 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body