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