Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402] [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 // We first run the test as admin. 1152 $this->setAdminUser(); 1153 $sections = core_course_external::get_course_contents($course->id, array()); 1154 // We need to execute the return values cleaning process to simulate the web service server. 1155 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1156 1157 $modinfo = get_fast_modinfo($course); 1158 $testexecuted = 0; 1159 foreach ($sections[0]['modules'] as $module) { 1160 if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') { 1161 $cm = $modinfo->cms[$forumcm->id]; 1162 $formattedtext = format_text($cm->content, FORMAT_HTML, 1163 array('noclean' => true, 'para' => false, 'filter' => false)); 1164 $this->assertEquals($formattedtext, $module['description']); 1165 $this->assertEquals($forumcm->instance, $module['instance']); 1166 $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']); 1167 $this->assertStringContainsString('1 unread post', $module['afterlink']); 1168 $this->assertFalse($module['noviewlink']); 1169 $this->assertNotEmpty($module['description']); // Module showdescription is on. 1170 $testexecuted = $testexecuted + 2; 1171 } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') { 1172 $cm = $modinfo->cms[$labelcm->id]; 1173 $formattedtext = format_text($cm->content, FORMAT_HTML, 1174 array('noclean' => true, 'para' => false, 'filter' => false)); 1175 $this->assertEquals($formattedtext, $module['description']); 1176 $this->assertEquals($labelcm->instance, $module['instance']); 1177 $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']); 1178 $this->assertTrue($module['noviewlink']); 1179 $this->assertNotEmpty($module['description']); // Label always prints the description. 1180 $testexecuted = $testexecuted + 1; 1181 } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') { 1182 $this->assertStringContainsString('customcompletionrules', $module['customdata']); 1183 $this->assertFalse($module['noviewlink']); 1184 $this->assertArrayNotHasKey('description', $module); 1185 $testexecuted = $testexecuted + 1; 1186 } 1187 } 1188 foreach ($sections[2]['modules'] as $module) { 1189 if ($module['id'] == $urlcm->id and $module['modname'] == 'url') { 1190 $this->assertStringContainsString('width=100,height=100', $module['onclick']); 1191 $testexecuted = $testexecuted + 1; 1192 } 1193 } 1194 1195 $CFG->forum_allowforcedreadtracking = 0; // Recover original value. 1196 forum_tp_count_forum_unread_posts($forumcm, $course, true); // Reset static cache for further tests. 1197 1198 $this->assertEquals(5, $testexecuted); 1199 $this->assertEquals(0, $sections[0]['section']); 1200 1201 $this->assertCount(6, $sections[0]['modules']); 1202 $this->assertCount(1, $sections[1]['modules']); 1203 $this->assertCount(1, $sections[2]['modules']); 1204 $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions. 1205 $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity. 1206 $this->assertNotEmpty($sections[3]['availabilityinfo']); 1207 $this->assertEquals(1, $sections[1]['section']); 1208 $this->assertEquals(2, $sections[2]['section']); 1209 $this->assertEquals(3, $sections[3]['section']); 1210 $this->assertEquals(4, $sections[4]['section']); 1211 $this->assertStringContainsString('<iframe', $sections[2]['summary']); 1212 $this->assertStringContainsString('</iframe>', $sections[2]['summary']); 1213 $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']); 1214 try { 1215 $sections = core_course_external::get_course_contents($course->id, 1216 array(array("name" => "invalid", "value" => 1))); 1217 $this->fail('Exception expected due to invalid option.'); 1218 } catch (moodle_exception $e) { 1219 $this->assertEquals('errorinvalidparam', $e->errorcode); 1220 } 1221 } 1222 1223 1224 /** 1225 * Test get_course_contents as student 1226 */ 1227 public function test_get_course_contents_student() { 1228 global $DB; 1229 $this->resetAfterTest(true); 1230 1231 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1232 1233 $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student')); 1234 $user = self::getDataGenerator()->create_user(); 1235 self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid); 1236 $this->setUser($user); 1237 1238 $sections = core_course_external::get_course_contents($course->id, array()); 1239 // We need to execute the return values cleaning process to simulate the web service server. 1240 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1241 1242 $this->assertCount(4, $sections); // Nothing for the not visible section. 1243 $this->assertCount(6, $sections[0]['modules']); 1244 $this->assertCount(1, $sections[1]['modules']); 1245 $this->assertCount(1, $sections[2]['modules']); 1246 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. 1247 1248 $this->assertNotEmpty($sections[3]['availabilityinfo']); 1249 $this->assertEquals(1, $sections[1]['section']); 1250 $this->assertEquals(2, $sections[2]['section']); 1251 $this->assertEquals(3, $sections[3]['section']); 1252 // The module with the availability restriction met is returning contents. 1253 $this->assertNotEmpty($sections[1]['modules'][0]['contents']); 1254 // The module with the availability restriction not met is not returning contents. 1255 $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]); 1256 1257 // Now include flag for returning stealth information (fake section). 1258 $sections = core_course_external::get_course_contents($course->id, 1259 array(array("name" => "includestealthmodules", "value" => 1))); 1260 // We need to execute the return values cleaning process to simulate the web service server. 1261 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1262 1263 $this->assertCount(5, $sections); // Include fake section with stealth activities. 1264 $this->assertCount(6, $sections[0]['modules']); 1265 $this->assertCount(1, $sections[1]['modules']); 1266 $this->assertCount(1, $sections[2]['modules']); 1267 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. 1268 $this->assertCount(1, $sections[4]['modules']); // One stealth module. 1269 $this->assertEquals(-1, $sections[4]['id']); 1270 } 1271 1272 /** 1273 * Test get_course_contents excluding modules 1274 */ 1275 public function test_get_course_contents_excluding_modules() { 1276 $this->resetAfterTest(true); 1277 1278 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1279 1280 // Test exclude modules. 1281 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1))); 1282 1283 // We need to execute the return values cleaning process to simulate the web service server. 1284 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1285 1286 $this->assertEmpty($sections[0]['modules']); 1287 $this->assertEmpty($sections[1]['modules']); 1288 } 1289 1290 /** 1291 * Test get_course_contents excluding contents 1292 */ 1293 public function test_get_course_contents_excluding_contents() { 1294 $this->resetAfterTest(true); 1295 1296 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1297 1298 // Test exclude modules. 1299 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1))); 1300 1301 // We need to execute the return values cleaning process to simulate the web service server. 1302 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1303 1304 foreach ($sections as $section) { 1305 foreach ($section['modules'] as $module) { 1306 // Only resources return contents. 1307 if (isset($module['contents'])) { 1308 $this->assertEmpty($module['contents']); 1309 } 1310 } 1311 } 1312 } 1313 1314 /** 1315 * Test get_course_contents filtering by section number 1316 */ 1317 public function test_get_course_contents_section_number() { 1318 $this->resetAfterTest(true); 1319 1320 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1321 1322 // Test exclude modules. 1323 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0))); 1324 1325 // We need to execute the return values cleaning process to simulate the web service server. 1326 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1327 1328 $this->assertCount(1, $sections); 1329 $this->assertCount(6, $sections[0]['modules']); 1330 } 1331 1332 /** 1333 * Test get_course_contents filtering by cmid 1334 */ 1335 public function test_get_course_contents_cmid() { 1336 $this->resetAfterTest(true); 1337 1338 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1339 1340 // Test exclude modules. 1341 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id))); 1342 1343 // We need to execute the return values cleaning process to simulate the web service server. 1344 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1345 1346 $this->assertCount(4, $sections); 1347 $this->assertCount(1, $sections[0]['modules']); 1348 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]); 1349 } 1350 1351 1352 /** 1353 * Test get_course_contents filtering by cmid and section 1354 */ 1355 public function test_get_course_contents_section_cmid() { 1356 $this->resetAfterTest(true); 1357 1358 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1359 1360 // Test exclude modules. 1361 $sections = core_course_external::get_course_contents($course->id, array( 1362 array("name" => "cmid", "value" => $forumcm->id), 1363 array("name" => "sectionnumber", "value" => 0) 1364 )); 1365 1366 // We need to execute the return values cleaning process to simulate the web service server. 1367 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1368 1369 $this->assertCount(1, $sections); 1370 $this->assertCount(1, $sections[0]['modules']); 1371 $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]); 1372 } 1373 1374 /** 1375 * Test get_course_contents filtering by modname 1376 */ 1377 public function test_get_course_contents_modname() { 1378 $this->resetAfterTest(true); 1379 1380 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1381 1382 // Test exclude modules. 1383 $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum"))); 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(4, $sections); 1389 $this->assertCount(2, $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_modid() { 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( 1403 array("name" => "modname", "value" => "page"), 1404 array("name" => "modid", "value" => $pagecm->instance), 1405 )); 1406 1407 // We need to execute the return values cleaning process to simulate the web service server. 1408 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1409 1410 $this->assertCount(4, $sections); 1411 $this->assertCount(1, $sections[0]['modules']); 1412 $this->assertEquals("page", $sections[0]['modules'][0]["modname"]); 1413 $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]); 1414 } 1415 1416 /** 1417 * Test get_course_contents returns downloadcontent value. 1418 */ 1419 public function test_get_course_contents_downloadcontent() { 1420 $this->resetAfterTest(); 1421 1422 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1423 1424 // Test exclude modules. 1425 $sections = core_course_external::get_course_contents($course->id, [ 1426 ['name' => 'modname', 'value' => 'page'], 1427 ['name' => 'modid', 'value' => $pagecm->instance] 1428 ]); 1429 1430 // We need to execute the return values cleaning process to simulate the web service server. 1431 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1432 $this->assertCount(1, $sections[0]['modules']); 1433 $this->assertEquals('page', $sections[0]['modules'][0]['modname']); 1434 $this->assertEquals($pagecm->downloadcontent, $sections[0]['modules'][0]['downloadcontent']); 1435 $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $sections[0]['modules'][0]['downloadcontent']); 1436 } 1437 1438 /** 1439 * Test get course contents completion manual 1440 */ 1441 public function test_get_course_contents_completion_manual() { 1442 global $CFG; 1443 $this->resetAfterTest(true); 1444 1445 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) = 1446 $this->prepare_get_course_contents_test(); 1447 availability_completion\condition::wipe_static_cache(); 1448 1449 // Test activity not completed yet. 1450 $result = core_course_external::get_course_contents($course->id, array( 1451 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance))); 1452 // We need to execute the return values cleaning process to simulate the web service server. 1453 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1454 1455 $completiondata = $result[0]['modules'][0]["completiondata"]; 1456 $this->assertCount(1, $result[0]['modules']); 1457 $this->assertEquals("forum", $result[0]['modules'][0]["modname"]); 1458 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]); 1459 $this->assertEquals(0, $completiondata['state']); 1460 $this->assertEquals(0, $completiondata['timecompleted']); 1461 $this->assertEmpty($completiondata['overrideby']); 1462 $this->assertFalse($completiondata['valueused']); 1463 $this->assertTrue($completiondata['hascompletion']); 1464 $this->assertFalse($completiondata['isautomatic']); 1465 $this->assertFalse($completiondata['istrackeduser']); 1466 $this->assertTrue($completiondata['uservisible']); 1467 1468 // Set activity completed. 1469 core_completion_external::update_activity_completion_status_manually($forumcm->id, true); 1470 1471 $result = core_course_external::get_course_contents($course->id, array( 1472 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance))); 1473 // We need to execute the return values cleaning process to simulate the web service server. 1474 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1475 1476 $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']); 1477 $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']); 1478 $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']); 1479 1480 // Test activity with completion value that is used in an availability condition. 1481 $result = core_course_external::get_course_contents($course->id, array( 1482 array("name" => "modname", "value" => "label"), array("name" => "modid", "value" => $labelcm->instance))); 1483 // We need to execute the return values cleaning process to simulate the web service server. 1484 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1485 1486 $completiondata = $result[0]['modules'][0]["completiondata"]; 1487 $this->assertCount(1, $result[0]['modules']); 1488 $this->assertEquals("label", $result[0]['modules'][0]["modname"]); 1489 $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]); 1490 $this->assertEquals(0, $completiondata['state']); 1491 $this->assertEquals(0, $completiondata['timecompleted']); 1492 $this->assertEmpty($completiondata['overrideby']); 1493 $this->assertTrue($completiondata['valueused']); 1494 $this->assertTrue($completiondata['hascompletion']); 1495 $this->assertFalse($completiondata['isautomatic']); 1496 $this->assertFalse($completiondata['istrackeduser']); 1497 $this->assertTrue($completiondata['uservisible']); 1498 1499 // Disable completion. 1500 $CFG->enablecompletion = 0; 1501 $result = core_course_external::get_course_contents($course->id, array( 1502 array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance))); 1503 // We need to execute the return values cleaning process to simulate the web service server. 1504 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1505 1506 $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]); 1507 } 1508 1509 /** 1510 * Test get course contents completion auto 1511 */ 1512 public function test_get_course_contents_completion_auto() { 1513 global $CFG; 1514 $this->resetAfterTest(true); 1515 1516 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) = 1517 $this->prepare_get_course_contents_test(); 1518 availability_completion\condition::wipe_static_cache(); 1519 1520 // Test activity not completed yet. 1521 $result = core_course_external::get_course_contents($course->id, [ 1522 [ 1523 "name" => "modname", 1524 "value" => "forum" 1525 ], 1526 [ 1527 "name" => "modid", 1528 "value" => $forumcompleteautocm->instance 1529 ] 1530 ]); 1531 // We need to execute the return values cleaning process to simulate the web service server. 1532 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1533 1534 $forummod = $result[0]['modules'][0]; 1535 $completiondata = $forummod["completiondata"]; 1536 $this->assertCount(1, $result[0]['modules']); 1537 $this->assertEquals("forum", $forummod["modname"]); 1538 $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $forummod["completion"]); 1539 $this->assertEquals(0, $completiondata['state']); 1540 $this->assertEquals(0, $completiondata['timecompleted']); 1541 $this->assertEmpty($completiondata['overrideby']); 1542 $this->assertFalse($completiondata['valueused']); 1543 $this->assertTrue($completiondata['hascompletion']); 1544 $this->assertTrue($completiondata['isautomatic']); 1545 $this->assertFalse($completiondata['istrackeduser']); 1546 $this->assertTrue($completiondata['uservisible']); 1547 $this->assertCount(1, $completiondata['details']); 1548 } 1549 1550 /** 1551 * Test mimetype is returned for resources with showtype set. 1552 */ 1553 public function test_get_course_contents_including_mimetype() { 1554 $this->resetAfterTest(true); 1555 1556 $this->setAdminUser(); 1557 $course = self::getDataGenerator()->create_course(); 1558 1559 $record = new stdClass(); 1560 $record->course = $course->id; 1561 $record->showtype = 1; 1562 $resource = self::getDataGenerator()->create_module('resource', $record); 1563 1564 $result = core_course_external::get_course_contents($course->id); 1565 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1566 $this->assertCount(1, $result[0]['modules']); // One module, first section. 1567 $customdata = json_decode($result[0]['modules'][0]['customdata']); 1568 $displayoptions = unserialize($customdata->displayoptions); 1569 $this->assertEquals('text/plain', $displayoptions['filedetails']['mimetype']); 1570 } 1571 1572 /** 1573 * Test contents info is returned. 1574 */ 1575 public function test_get_course_contents_contentsinfo() { 1576 global $USER; 1577 1578 $this->resetAfterTest(true); 1579 $this->setAdminUser(); 1580 $timenow = time(); 1581 1582 $course = self::getDataGenerator()->create_course(); 1583 1584 $record = new stdClass(); 1585 $record->course = $course->id; 1586 // One resource with one file. 1587 $resource1 = self::getDataGenerator()->create_module('resource', $record); 1588 1589 // More type of files. 1590 $record->files = file_get_unused_draft_itemid(); 1591 $usercontext = context_user::instance($USER->id); 1592 $extensions = array('txt', 'png', 'pdf'); 1593 $fs = get_file_storage(); 1594 foreach ($extensions as $key => $extension) { 1595 // Add actual file there. 1596 $filerecord = array('component' => 'user', 'filearea' => 'draft', 1597 'contextid' => $usercontext->id, 'itemid' => $record->files, 1598 'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/'); 1599 $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file'); 1600 } 1601 1602 // Create file reference. 1603 $repos = repository::get_instances(array('type' => 'user')); 1604 $userrepository = reset($repos); 1605 1606 // Create a user private file. 1607 $userfilerecord = new stdClass; 1608 $userfilerecord->contextid = $usercontext->id; 1609 $userfilerecord->component = 'user'; 1610 $userfilerecord->filearea = 'private'; 1611 $userfilerecord->itemid = 0; 1612 $userfilerecord->filepath = '/'; 1613 $userfilerecord->filename = 'userfile.txt'; 1614 $userfilerecord->source = 'test'; 1615 $userfile = $fs->create_file_from_string($userfilerecord, 'User file content'); 1616 $userfileref = $fs->pack_reference($userfilerecord); 1617 1618 // Clone latest "normal" file. 1619 $filerefrecord = clone (object) $filerecord; 1620 $filerefrecord->filename = 'testref.txt'; 1621 $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref); 1622 // Set main file pointing to the file reference. 1623 file_set_sortorder($usercontext->id, 'user', 'draft', $record->files, $filerefrecord->filepath, 1624 $filerefrecord->filename, 1); 1625 1626 // Once the reference has been created, create the file resource. 1627 $resource2 = self::getDataGenerator()->create_module('resource', $record); 1628 1629 $result = core_course_external::get_course_contents($course->id); 1630 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1631 $this->assertCount(2, $result[0]['modules']); 1632 foreach ($result[0]['modules'] as $module) { 1633 if ($module['instance'] == $resource1->id) { 1634 $this->assertEquals(1, $module['contentsinfo']['filescount']); 1635 $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']); 1636 $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']); 1637 $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']); 1638 } else { 1639 $this->assertEquals(count($extensions) + 1, $module['contentsinfo']['filescount']); 1640 $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] + 1641 $module['contents'][2]['filesize'] + $module['contents'][3]['filesize']; 1642 $this->assertEquals($filessize, $module['contentsinfo']['filessize']); 1643 $this->assertEquals('user', $module['contentsinfo']['repositorytype']); 1644 $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']); 1645 $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']); 1646 } 1647 } 1648 } 1649 1650 /** 1651 * Test get_course_contents when hidden sections are displayed. 1652 */ 1653 public function test_get_course_contents_hiddensections() { 1654 global $DB; 1655 $this->resetAfterTest(true); 1656 1657 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1658 // Force returning hidden sections. 1659 $course->hiddensections = 0; 1660 update_course($course); 1661 1662 $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student')); 1663 $user = self::getDataGenerator()->create_user(); 1664 self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid); 1665 $this->setUser($user); 1666 1667 $sections = core_course_external::get_course_contents($course->id, array()); 1668 // We need to execute the return values cleaning process to simulate the web service server. 1669 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1670 1671 $this->assertCount(5, $sections); // All the sections, including the "not visible" one. 1672 $this->assertCount(6, $sections[0]['modules']); 1673 $this->assertCount(1, $sections[1]['modules']); 1674 $this->assertCount(1, $sections[2]['modules']); 1675 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. 1676 $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden. 1677 1678 $this->assertNotEmpty($sections[3]['availabilityinfo']); 1679 $this->assertEquals(1, $sections[1]['section']); 1680 $this->assertEquals(2, $sections[2]['section']); 1681 $this->assertEquals(3, $sections[3]['section']); 1682 // The module with the availability restriction met is returning contents. 1683 $this->assertNotEmpty($sections[1]['modules'][0]['contents']); 1684 // The module with the availability restriction not met is not returning contents. 1685 $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]); 1686 1687 // Now include flag for returning stealth information (fake section). 1688 $sections = core_course_external::get_course_contents($course->id, 1689 array(array("name" => "includestealthmodules", "value" => 1))); 1690 // We need to execute the return values cleaning process to simulate the web service server. 1691 $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections); 1692 1693 $this->assertCount(6, $sections); // Include fake section with stealth activities. 1694 $this->assertCount(6, $sections[0]['modules']); 1695 $this->assertCount(1, $sections[1]['modules']); 1696 $this->assertCount(1, $sections[2]['modules']); 1697 $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions. 1698 $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden. 1699 $this->assertCount(1, $sections[5]['modules']); // One stealth module. 1700 $this->assertEquals(-1, $sections[5]['id']); 1701 } 1702 1703 /** 1704 * Test get course contents dates. 1705 */ 1706 public function test_get_course_contents_dates() { 1707 $this->resetAfterTest(true); 1708 1709 $this->setAdminUser(); 1710 set_config('enablecourserelativedates', 1); 1711 1712 // Course with just main section. 1713 $timenow = time(); 1714 $course = self::getDataGenerator()->create_course( 1715 ['numsections' => 0, 'relativedatesmode' => true, 'startdate' => $timenow - DAYSECS]); 1716 1717 $teacher = self::getDataGenerator()->create_user(); 1718 self::getDataGenerator()->enrol_user($teacher->id, $course->id, 'editingteacher'); 1719 1720 $this->setUser($teacher); 1721 1722 // Create resource (empty dates). 1723 $resource = self::getDataGenerator()->create_module('resource', ['course' => $course->id]); 1724 // Create activities with dates. 1725 $resource = self::getDataGenerator()->create_module('forum', ['course' => $course->id, 'duedate' => $timenow]); 1726 $resource = self::getDataGenerator()->create_module('choice', 1727 ['course' => $course->id, 'timeopen' => $timenow, 'timeclose' => $timenow + DAYSECS]); 1728 $resource = self::getDataGenerator()->create_module('assign', 1729 ['course' => $course->id, 'allowsubmissionsfromdate' => $timenow]); 1730 1731 $result = core_course_external::get_course_contents($course->id); 1732 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1733 1734 foreach ($result[0]['modules'] as $module) { 1735 if ($module['modname'] == 'resource') { 1736 $this->assertEmpty($module['dates']); 1737 } else if ($module['modname'] == 'forum') { 1738 $this->assertCount(1, $module['dates']); 1739 $this->assertEquals('duedate', $module['dates'][0]['dataid']); 1740 $this->assertEquals($timenow, $module['dates'][0]['timestamp']); 1741 } else if ($module['modname'] == 'choice') { 1742 $this->assertCount(2, $module['dates']); 1743 $this->assertEquals('timeopen', $module['dates'][0]['dataid']); 1744 $this->assertEquals($timenow, $module['dates'][0]['timestamp']); 1745 $this->assertEquals('timeclose', $module['dates'][1]['dataid']); 1746 $this->assertEquals($timenow + DAYSECS, $module['dates'][1]['timestamp']); 1747 } else if ($module['modname'] == 'assign') { 1748 $this->assertCount(1, $module['dates']); 1749 $this->assertEquals('allowsubmissionsfromdate', $module['dates'][0]['dataid']); 1750 $this->assertEquals($timenow, $module['dates'][0]['timestamp']); 1751 $this->assertEquals($course->startdate, $module['dates'][0]['relativeto']); 1752 } 1753 } 1754 } 1755 1756 /** 1757 * Test get_course_contents for courses with invalid course format. 1758 */ 1759 public function test_get_course_contents_invalid_format() { 1760 global $DB; 1761 $this->resetAfterTest(); 1762 1763 list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test(); 1764 1765 $DB->set_field('course', 'format', 'fakeformat', ['id' => $course->id]); 1766 1767 // WS should falback to default course format (topics) and avoid exceptions (but debugging will happen). 1768 $result = core_course_external::get_course_contents($course->id); 1769 $this->assertDebuggingCalled(); 1770 $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result); 1771 } 1772 1773 /** 1774 * Test duplicate_course 1775 */ 1776 public function test_duplicate_course() { 1777 $this->resetAfterTest(true); 1778 1779 // Create one course with three modules. 1780 $course = self::getDataGenerator()->create_course(); 1781 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id)); 1782 $forumcm = get_coursemodule_from_id('forum', $forum->cmid); 1783 $forumcontext = context_module::instance($forum->cmid); 1784 $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id)); 1785 $datacontext = context_module::instance($data->cmid); 1786 $datacm = get_coursemodule_from_instance('page', $data->id); 1787 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id)); 1788 $pagecontext = context_module::instance($page->cmid); 1789 $pagecm = get_coursemodule_from_instance('page', $page->id); 1790 1791 // Set the required capabilities by the external function. 1792 $coursecontext = context_course::instance($course->id); 1793 $categorycontext = context_coursecat::instance($course->category); 1794 $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id); 1795 $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid); 1796 $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid); 1797 $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid); 1798 $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid); 1799 // Optional capabilities to copy user data. 1800 $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid); 1801 $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid); 1802 1803 $newcourse['fullname'] = 'Course duplicate'; 1804 $newcourse['shortname'] = 'courseduplicate'; 1805 $newcourse['categoryid'] = $course->category; 1806 $newcourse['visible'] = true; 1807 $newcourse['options'][] = array('name' => 'users', 'value' => true); 1808 1809 $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'], 1810 $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']); 1811 1812 // We need to execute the return values cleaning process to simulate the web service server. 1813 $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate); 1814 1815 // Check that the course has been duplicated. 1816 $this->assertEquals($newcourse['shortname'], $duplicate['shortname']); 1817 } 1818 1819 /** 1820 * Test update_courses 1821 */ 1822 public function test_update_courses() { 1823 global $DB, $CFG, $USER, $COURSE; 1824 1825 // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this 1826 // trick because we are both updating and getting (for testing) course information 1827 // in the same request and core_course_external::update_courses() 1828 // is overwriting $COURSE all over the time with OLD values, so later 1829 // use of get_course() fetches those OLD values instead of the updated ones. 1830 // See MDL-39723 for more info. 1831 $origcourse = clone($COURSE); 1832 1833 $this->resetAfterTest(true); 1834 1835 // Set the required capabilities by the external function. 1836 $contextid = context_system::instance()->id; 1837 $roleid = $this->assignUserCapability('moodle/course:update', $contextid); 1838 $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid); 1839 $this->assignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid); 1840 $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid); 1841 $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid); 1842 $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid); 1843 $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid); 1844 $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid); 1845 $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid); 1846 $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid); 1847 1848 // Create category and courses. 1849 $category1 = self::getDataGenerator()->create_category(); 1850 $category2 = self::getDataGenerator()->create_category(); 1851 1852 $originalcourse1 = self::getDataGenerator()->create_course(); 1853 self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid); 1854 1855 $originalcourse2 = self::getDataGenerator()->create_course(); 1856 self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid); 1857 1858 // Course with custom fields. 1859 $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']); 1860 $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text', 1861 'categoryid' => $fieldcategory->get('id'), 1862 'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL, 'locked' => 1]]; 1863 $field = self::getDataGenerator()->create_custom_field($customfield); 1864 1865 $originalcourse3 = self::getDataGenerator()->create_course(['customfield_test' => 'Test value']); 1866 self::getDataGenerator()->enrol_user($USER->id, $originalcourse3->id, $roleid); 1867 1868 // Course values to be updated. 1869 $course1['id'] = $originalcourse1->id; 1870 $course1['fullname'] = 'Updated test course 1'; 1871 $course1['shortname'] = 'Udestedtestcourse1'; 1872 $course1['categoryid'] = $category1->id; 1873 1874 $course2['id'] = $originalcourse2->id; 1875 $course2['fullname'] = 'Updated test course 2'; 1876 $course2['shortname'] = 'Updestedtestcourse2'; 1877 $course2['categoryid'] = $category2->id; 1878 $course2['idnumber'] = 'Updatedidnumber2'; 1879 $course2['summary'] = 'Updaated description for course 2'; 1880 $course2['summaryformat'] = FORMAT_HTML; 1881 $course2['format'] = 'topics'; 1882 $course2['showgrades'] = 1; 1883 $course2['newsitems'] = 3; 1884 $course2['startdate'] = 1420092000; // 01/01/2015. 1885 $course2['enddate'] = 1422669600; // 01/31/2015. 1886 $course2['maxbytes'] = 100000; 1887 $course2['showreports'] = 1; 1888 $course2['visible'] = 0; 1889 $course2['hiddensections'] = 0; 1890 $course2['groupmode'] = 0; 1891 $course2['groupmodeforce'] = 0; 1892 $course2['defaultgroupingid'] = 0; 1893 $course2['enablecompletion'] = 1; 1894 $course2['lang'] = 'en'; 1895 $course2['forcetheme'] = 'classic'; 1896 1897 $course3['id'] = $originalcourse3->id; 1898 $updatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'Updated test value']; 1899 $course3['customfields'] = [$updatedcustomfieldvalue]; 1900 $courses = array($course1, $course2, $course3); 1901 1902 $updatedcoursewarnings = core_course_external::update_courses($courses); 1903 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1904 $updatedcoursewarnings); 1905 $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line. 1906 1907 // Check that right number of courses were created. 1908 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 1909 1910 // Check that the courses were correctly created. 1911 foreach ($courses as $course) { 1912 $courseinfo = course_get_format($course['id'])->get_course(); 1913 $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course['id']); 1914 if ($course['id'] == $course2['id']) { 1915 $this->assertEquals($course2['fullname'], $courseinfo->fullname); 1916 $this->assertEquals($course2['shortname'], $courseinfo->shortname); 1917 $this->assertEquals($course2['categoryid'], $courseinfo->category); 1918 $this->assertEquals($course2['idnumber'], $courseinfo->idnumber); 1919 $this->assertEquals($course2['summary'], $courseinfo->summary); 1920 $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat); 1921 $this->assertEquals($course2['format'], $courseinfo->format); 1922 $this->assertEquals($course2['showgrades'], $courseinfo->showgrades); 1923 $this->assertEquals($course2['newsitems'], $courseinfo->newsitems); 1924 $this->assertEquals($course2['startdate'], $courseinfo->startdate); 1925 $this->assertEquals($course2['enddate'], $courseinfo->enddate); 1926 $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes); 1927 $this->assertEquals($course2['showreports'], $courseinfo->showreports); 1928 $this->assertEquals($course2['visible'], $courseinfo->visible); 1929 $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections); 1930 $this->assertEquals($course2['groupmode'], $courseinfo->groupmode); 1931 $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce); 1932 $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid); 1933 $this->assertEquals($course2['lang'], $courseinfo->lang); 1934 1935 if (!empty($CFG->allowcoursethemes)) { 1936 $this->assertEquals($course2['forcetheme'], $courseinfo->theme); 1937 } 1938 1939 $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion); 1940 $this->assertEquals(['test' => null], (array)$customfields); 1941 } else if ($course['id'] == $course1['id']) { 1942 $this->assertEquals($course1['fullname'], $courseinfo->fullname); 1943 $this->assertEquals($course1['shortname'], $courseinfo->shortname); 1944 $this->assertEquals($course1['categoryid'], $courseinfo->category); 1945 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat); 1946 $this->assertEquals('topics', $courseinfo->format); 1947 $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number()); 1948 $this->assertEquals(0, $courseinfo->newsitems); 1949 $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat); 1950 $this->assertEquals(['test' => null], (array)$customfields); 1951 } else if ($course['id'] == $course3['id']) { 1952 $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields); 1953 } else { 1954 throw new moodle_exception('Unexpected shortname'); 1955 } 1956 } 1957 1958 $courses = array($course1); 1959 // Try update course without update capability. 1960 $user = self::getDataGenerator()->create_user(); 1961 $this->setUser($user); 1962 $this->unassignUserCapability('moodle/course:update', $contextid, $roleid); 1963 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1964 $updatedcoursewarnings = core_course_external::update_courses($courses); 1965 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1966 $updatedcoursewarnings); 1967 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1968 1969 // Try update course category without capability. 1970 $this->assignUserCapability('moodle/course:update', $contextid, $roleid); 1971 $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid); 1972 $user = self::getDataGenerator()->create_user(); 1973 $this->setUser($user); 1974 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1975 $course1['categoryid'] = $category2->id; 1976 $courses = array($course1); 1977 $updatedcoursewarnings = core_course_external::update_courses($courses); 1978 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1979 $updatedcoursewarnings); 1980 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1981 1982 // Try update course fullname without capability. 1983 $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid); 1984 $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid); 1985 $user = self::getDataGenerator()->create_user(); 1986 $this->setUser($user); 1987 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 1988 $updatedcoursewarnings = core_course_external::update_courses($courses); 1989 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1990 $updatedcoursewarnings); 1991 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 1992 $course1['fullname'] = 'Testing fullname without permission'; 1993 $courses = array($course1); 1994 $updatedcoursewarnings = core_course_external::update_courses($courses); 1995 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 1996 $updatedcoursewarnings); 1997 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 1998 1999 // Try update course shortname without capability. 2000 $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid); 2001 $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid); 2002 $user = self::getDataGenerator()->create_user(); 2003 $this->setUser($user); 2004 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 2005 $updatedcoursewarnings = core_course_external::update_courses($courses); 2006 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2007 $updatedcoursewarnings); 2008 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 2009 $course1['shortname'] = 'Testing shortname without permission'; 2010 $courses = array($course1); 2011 $updatedcoursewarnings = core_course_external::update_courses($courses); 2012 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2013 $updatedcoursewarnings); 2014 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 2015 2016 // Try update course idnumber without capability. 2017 $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid); 2018 $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid); 2019 $user = self::getDataGenerator()->create_user(); 2020 $this->setUser($user); 2021 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 2022 $updatedcoursewarnings = core_course_external::update_courses($courses); 2023 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2024 $updatedcoursewarnings); 2025 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 2026 $course1['idnumber'] = 'NEWIDNUMBER'; 2027 $courses = array($course1); 2028 $updatedcoursewarnings = core_course_external::update_courses($courses); 2029 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2030 $updatedcoursewarnings); 2031 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 2032 2033 // Try update course summary without capability. 2034 $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid); 2035 $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid); 2036 $user = self::getDataGenerator()->create_user(); 2037 $this->setUser($user); 2038 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 2039 $updatedcoursewarnings = core_course_external::update_courses($courses); 2040 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2041 $updatedcoursewarnings); 2042 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 2043 $course1['summary'] = 'New summary'; 2044 $courses = array($course1); 2045 $updatedcoursewarnings = core_course_external::update_courses($courses); 2046 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2047 $updatedcoursewarnings); 2048 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 2049 2050 // Try update course with invalid summary format. 2051 $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid); 2052 $user = self::getDataGenerator()->create_user(); 2053 $this->setUser($user); 2054 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 2055 $updatedcoursewarnings = core_course_external::update_courses($courses); 2056 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2057 $updatedcoursewarnings); 2058 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 2059 $course1['summaryformat'] = 10; 2060 $courses = array($course1); 2061 $updatedcoursewarnings = core_course_external::update_courses($courses); 2062 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2063 $updatedcoursewarnings); 2064 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 2065 2066 // Try update course visibility without capability. 2067 $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid); 2068 $user = self::getDataGenerator()->create_user(); 2069 $this->setUser($user); 2070 self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid); 2071 $course1['summaryformat'] = FORMAT_MOODLE; 2072 $courses = array($course1); 2073 $updatedcoursewarnings = core_course_external::update_courses($courses); 2074 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2075 $updatedcoursewarnings); 2076 $this->assertEquals(0, count($updatedcoursewarnings['warnings'])); 2077 $course1['visible'] = 0; 2078 $courses = array($course1); 2079 $updatedcoursewarnings = core_course_external::update_courses($courses); 2080 $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(), 2081 $updatedcoursewarnings); 2082 $this->assertEquals(1, count($updatedcoursewarnings['warnings'])); 2083 2084 // Try update course custom fields without capability. 2085 $this->unassignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid); 2086 $user = self::getDataGenerator()->create_user(); 2087 $this->setUser($user); 2088 self::getDataGenerator()->enrol_user($user->id, $course3['id'], $roleid); 2089 2090 $newupdatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'New updated value']; 2091 $course3['customfields'] = [$newupdatedcustomfieldvalue]; 2092 2093 core_course_external::update_courses([$course3]); 2094 2095 // Custom field was not updated. 2096 $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course3['id']); 2097 $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields); 2098 } 2099 2100 /** 2101 * Test delete course_module. 2102 */ 2103 public function test_delete_modules() { 2104 global $DB; 2105 2106 // Ensure we reset the data after this test. 2107 $this->resetAfterTest(true); 2108 2109 // Create a user. 2110 $user = self::getDataGenerator()->create_user(); 2111 2112 // Set the tests to run as the user. 2113 self::setUser($user); 2114 2115 // Create a course to add the modules. 2116 $course = self::getDataGenerator()->create_course(); 2117 2118 // Create two test modules. 2119 $record = new stdClass(); 2120 $record->course = $course->id; 2121 $module1 = self::getDataGenerator()->create_module('forum', $record); 2122 $module2 = self::getDataGenerator()->create_module('assign', $record); 2123 2124 // Check the forum was correctly created. 2125 $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id))); 2126 2127 // Check the assignment was correctly created. 2128 $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id))); 2129 2130 // Check data exists in the course modules table. 2131 $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2', 2132 array('module1' => $module1->cmid, 'module2' => $module2->cmid))); 2133 2134 // Enrol the user in the course. 2135 $enrol = enrol_get_plugin('manual'); 2136 $enrolinstances = enrol_get_instances($course->id, true); 2137 foreach ($enrolinstances as $courseenrolinstance) { 2138 if ($courseenrolinstance->enrol == "manual") { 2139 $instance = $courseenrolinstance; 2140 break; 2141 } 2142 } 2143 $enrol->enrol_user($instance, $user->id); 2144 2145 // Assign capabilities to delete module 1. 2146 $modcontext = context_module::instance($module1->cmid); 2147 $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id); 2148 2149 // Assign capabilities to delete module 2. 2150 $modcontext = context_module::instance($module2->cmid); 2151 $newrole = create_role('Role 2', 'role2', 'Role 2 description'); 2152 $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole); 2153 2154 // Deleting these module instances. 2155 core_course_external::delete_modules(array($module1->cmid, $module2->cmid)); 2156 2157 // Check the forum was deleted. 2158 $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id))); 2159 2160 // Check the assignment was deleted. 2161 $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id))); 2162 2163 // Check we retrieve no data in the course modules table. 2164 $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2', 2165 array('module1' => $module1->cmid, 'module2' => $module2->cmid))); 2166 2167 // Call with non-existent course module id and ensure exception thrown. 2168 try { 2169 core_course_external::delete_modules(array('1337')); 2170 $this->fail('Exception expected due to missing course module.'); 2171 } catch (dml_missing_record_exception $e) { 2172 $this->assertEquals('invalidcoursemodule', $e->errorcode); 2173 } 2174 2175 // Create two modules. 2176 $module1 = self::getDataGenerator()->create_module('forum', $record); 2177 $module2 = self::getDataGenerator()->create_module('assign', $record); 2178 2179 // Since these modules were recreated the user will not have capabilities 2180 // to delete them, ensure exception is thrown if they try. 2181 try { 2182 core_course_external::delete_modules(array($module1->cmid, $module2->cmid)); 2183 $this->fail('Exception expected due to missing capability.'); 2184 } catch (moodle_exception $e) { 2185 $this->assertEquals('nopermissions', $e->errorcode); 2186 } 2187 2188 // Unenrol user from the course. 2189 $enrol->unenrol_user($instance, $user->id); 2190 2191 // Try and delete modules from the course the user was unenrolled in, make sure exception thrown. 2192 try { 2193 core_course_external::delete_modules(array($module1->cmid, $module2->cmid)); 2194 $this->fail('Exception expected due to being unenrolled from the course.'); 2195 } catch (moodle_exception $e) { 2196 $this->assertEquals('requireloginerror', $e->errorcode); 2197 } 2198 } 2199 2200 /** 2201 * Test import_course into an empty course 2202 */ 2203 public function test_import_course_empty() { 2204 global $USER; 2205 2206 $this->resetAfterTest(true); 2207 2208 $course1 = self::getDataGenerator()->create_course(); 2209 $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test')); 2210 $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test')); 2211 2212 $course2 = self::getDataGenerator()->create_course(); 2213 2214 $course1cms = get_fast_modinfo($course1->id)->get_cms(); 2215 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2216 2217 // Verify the state of the courses before we do the import. 2218 $this->assertCount(2, $course1cms); 2219 $this->assertEmpty($course2cms); 2220 2221 // Setup the user to run the operation (ugly hack because validate_context() will 2222 // fail as the email is not set by $this->setAdminUser()). 2223 $this->setAdminUser(); 2224 $USER->email = 'emailtopass@example.com'; 2225 2226 // Import from course1 to course2. 2227 core_course_external::import_course($course1->id, $course2->id, 0); 2228 2229 // Verify that now we have two modules in both courses. 2230 $course1cms = get_fast_modinfo($course1->id)->get_cms(); 2231 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2232 $this->assertCount(2, $course1cms); 2233 $this->assertCount(2, $course2cms); 2234 2235 // Verify that the names transfered across correctly. 2236 foreach ($course2cms as $cm) { 2237 if ($cm->modname === 'page') { 2238 $this->assertEquals($cm->name, $page->name); 2239 } else if ($cm->modname === 'forum') { 2240 $this->assertEquals($cm->name, $forum->name); 2241 } else { 2242 $this->fail('Unknown CM found.'); 2243 } 2244 } 2245 } 2246 2247 /** 2248 * Test import_course into an filled course 2249 */ 2250 public function test_import_course_filled() { 2251 global $USER; 2252 2253 $this->resetAfterTest(true); 2254 2255 // Add forum and page to course1. 2256 $course1 = self::getDataGenerator()->create_course(); 2257 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test')); 2258 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test')); 2259 2260 // Add quiz to course 2. 2261 $course2 = self::getDataGenerator()->create_course(); 2262 $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test')); 2263 2264 $course1cms = get_fast_modinfo($course1->id)->get_cms(); 2265 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2266 2267 // Verify the state of the courses before we do the import. 2268 $this->assertCount(2, $course1cms); 2269 $this->assertCount(1, $course2cms); 2270 2271 // Setup the user to run the operation (ugly hack because validate_context() will 2272 // fail as the email is not set by $this->setAdminUser()). 2273 $this->setAdminUser(); 2274 $USER->email = 'emailtopass@example.com'; 2275 2276 // Import from course1 to course2 without deleting content. 2277 core_course_external::import_course($course1->id, $course2->id, 0); 2278 2279 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2280 2281 // Verify that now we have three modules in course2. 2282 $this->assertCount(3, $course2cms); 2283 2284 // Verify that the names transfered across correctly. 2285 foreach ($course2cms as $cm) { 2286 if ($cm->modname === 'page') { 2287 $this->assertEquals($cm->name, $page->name); 2288 } else if ($cm->modname === 'forum') { 2289 $this->assertEquals($cm->name, $forum->name); 2290 } else if ($cm->modname === 'quiz') { 2291 $this->assertEquals($cm->name, $quiz->name); 2292 } else { 2293 $this->fail('Unknown CM found.'); 2294 } 2295 } 2296 } 2297 2298 /** 2299 * Test import_course with only blocks set to backup 2300 */ 2301 public function test_import_course_blocksonly() { 2302 global $USER, $DB; 2303 2304 $this->resetAfterTest(true); 2305 2306 // Add forum and page to course1. 2307 $course1 = self::getDataGenerator()->create_course(); 2308 $course1ctx = context_course::instance($course1->id); 2309 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test')); 2310 $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id)); 2311 2312 $course2 = self::getDataGenerator()->create_course(); 2313 $course2ctx = context_course::instance($course2->id); 2314 $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id)); 2315 $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms()); 2316 2317 // Setup the user to run the operation (ugly hack because validate_context() will 2318 // fail as the email is not set by $this->setAdminUser()). 2319 $this->setAdminUser(); 2320 $USER->email = 'emailtopass@example.com'; 2321 2322 // Import from course1 to course2 without deleting content, but excluding 2323 // activities. 2324 $options = array( 2325 array('name' => 'activities', 'value' => 0), 2326 array('name' => 'blocks', 'value' => 1), 2327 array('name' => 'filters', 'value' => 0), 2328 ); 2329 2330 core_course_external::import_course($course1->id, $course2->id, 0, $options); 2331 2332 $newcmcount = count(get_fast_modinfo($course2->id)->get_cms()); 2333 $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id)); 2334 // Check that course modules haven't changed, but that blocks have. 2335 $this->assertEquals($initialcmcount, $newcmcount); 2336 $this->assertEquals(($initialblockcount + 1), $newblockcount); 2337 } 2338 2339 /** 2340 * Test import_course into an filled course, deleting content. 2341 */ 2342 public function test_import_course_deletecontent() { 2343 global $USER; 2344 $this->resetAfterTest(true); 2345 2346 // Add forum and page to course1. 2347 $course1 = self::getDataGenerator()->create_course(); 2348 $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test')); 2349 $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test')); 2350 2351 // Add quiz to course 2. 2352 $course2 = self::getDataGenerator()->create_course(); 2353 $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test')); 2354 2355 $course1cms = get_fast_modinfo($course1->id)->get_cms(); 2356 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2357 2358 // Verify the state of the courses before we do the import. 2359 $this->assertCount(2, $course1cms); 2360 $this->assertCount(1, $course2cms); 2361 2362 // Setup the user to run the operation (ugly hack because validate_context() will 2363 // fail as the email is not set by $this->setAdminUser()). 2364 $this->setAdminUser(); 2365 $USER->email = 'emailtopass@example.com'; 2366 2367 // Import from course1 to course2, deleting content. 2368 core_course_external::import_course($course1->id, $course2->id, 1); 2369 2370 $course2cms = get_fast_modinfo($course2->id)->get_cms(); 2371 2372 // Verify that now we have two modules in course2. 2373 $this->assertCount(2, $course2cms); 2374 2375 // Verify that the course only contains the imported modules. 2376 foreach ($course2cms as $cm) { 2377 if ($cm->modname === 'page') { 2378 $this->assertEquals($cm->name, $page->name); 2379 } else if ($cm->modname === 'forum') { 2380 $this->assertEquals($cm->name, $forum->name); 2381 } else { 2382 $this->fail('Unknown CM found: '.$cm->name); 2383 } 2384 } 2385 } 2386 2387 /** 2388 * Ensure import_course handles incorrect deletecontent option correctly. 2389 */ 2390 public function test_import_course_invalid_deletecontent_option() { 2391 $this->resetAfterTest(true); 2392 2393 $course1 = self::getDataGenerator()->create_course(); 2394 $course2 = self::getDataGenerator()->create_course(); 2395 2396 $this->expectException('moodle_exception'); 2397 $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1)); 2398 // Import from course1 to course2, with invalid option 2399 core_course_external::import_course($course1->id, $course2->id, -1);; 2400 } 2401 2402 /** 2403 * Test view_course function 2404 */ 2405 public function test_view_course() { 2406 2407 $this->resetAfterTest(); 2408 2409 // Course without sections. 2410 $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true)); 2411 $this->setAdminUser(); 2412 2413 // Redirect events to the sink, so we can recover them later. 2414 $sink = $this->redirectEvents(); 2415 2416 $result = core_course_external::view_course($course->id, 1); 2417 $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result); 2418 $events = $sink->get_events(); 2419 $event = reset($events); 2420 2421 // Check the event details are correct. 2422 $this->assertInstanceOf('\core\event\course_viewed', $event); 2423 $this->assertEquals(context_course::instance($course->id), $event->get_context()); 2424 $this->assertEquals(1, $event->other['coursesectionnumber']); 2425 2426 $result = core_course_external::view_course($course->id); 2427 $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result); 2428 $events = $sink->get_events(); 2429 $event = array_pop($events); 2430 $sink->close(); 2431 2432 // Check the event details are correct. 2433 $this->assertInstanceOf('\core\event\course_viewed', $event); 2434 $this->assertEquals(context_course::instance($course->id), $event->get_context()); 2435 $this->assertEmpty($event->other); 2436 2437 } 2438 2439 /** 2440 * Test get_course_module 2441 */ 2442 public function test_get_course_module() { 2443 global $DB; 2444 2445 $this->resetAfterTest(true); 2446 2447 $this->setAdminUser(); 2448 $course = self::getDataGenerator()->create_course(['enablecompletion' => 1]); 2449 $record = array( 2450 'course' => $course->id, 2451 'name' => 'First Assignment' 2452 ); 2453 $options = array( 2454 'idnumber' => 'ABC', 2455 'visible' => 0, 2456 'completion' => COMPLETION_TRACKING_AUTOMATIC, 2457 'completiongradeitemnumber' => 0, 2458 'completionpassgrade' => 1, 2459 ); 2460 // Hidden activity. 2461 $assign = self::getDataGenerator()->create_module('assign', $record, $options); 2462 2463 $outcomescale = 'Distinction, Very Good, Good, Pass, Fail'; 2464 2465 // Insert a custom grade scale to be used by an outcome. 2466 $gradescale = new grade_scale(); 2467 $gradescale->name = 'gettcoursemodulescale'; 2468 $gradescale->courseid = $course->id; 2469 $gradescale->userid = 0; 2470 $gradescale->scale = $outcomescale; 2471 $gradescale->description = 'This scale is used to mark standard assignments.'; 2472 $gradescale->insert(); 2473 2474 // Insert an outcome. 2475 $data = new stdClass(); 2476 $data->courseid = $course->id; 2477 $data->fullname = 'Team work'; 2478 $data->shortname = 'Team work'; 2479 $data->scaleid = $gradescale->id; 2480 $outcome = new grade_outcome($data, false); 2481 $outcome->insert(); 2482 2483 $outcomegradeitem = new grade_item(); 2484 $outcomegradeitem->itemname = $outcome->shortname; 2485 $outcomegradeitem->itemtype = 'mod'; 2486 $outcomegradeitem->itemmodule = 'assign'; 2487 $outcomegradeitem->iteminstance = $assign->id; 2488 $outcomegradeitem->outcomeid = $outcome->id; 2489 $outcomegradeitem->cmid = 0; 2490 $outcomegradeitem->courseid = $course->id; 2491 $outcomegradeitem->aggregationcoef = 0; 2492 $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000. 2493 $outcomegradeitem->gradetype = GRADE_TYPE_SCALE; 2494 $outcomegradeitem->scaleid = $outcome->scaleid; 2495 $outcomegradeitem->insert(); 2496 2497 $assignmentgradeitem = grade_item::fetch( 2498 array( 2499 'itemtype' => 'mod', 2500 'itemmodule' => 'assign', 2501 'iteminstance' => $assign->id, 2502 'itemnumber' => 0, 2503 'courseid' => $course->id 2504 ) 2505 ); 2506 $outcomegradeitem->set_parent($assignmentgradeitem->categoryid); 2507 $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder); 2508 2509 // Test admin user can see the complete hidden activity. 2510 $result = core_course_external::get_course_module($assign->cmid); 2511 $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result); 2512 2513 $this->assertCount(0, $result['warnings']); 2514 // Test we retrieve all the fields. 2515 $this->assertCount(30, $result['cm']); 2516 $this->assertEquals($record['name'], $result['cm']['name']); 2517 $this->assertEquals($options['idnumber'], $result['cm']['idnumber']); 2518 $this->assertEquals(100, $result['cm']['grade']); 2519 $this->assertEquals(0.0, $result['cm']['gradepass']); 2520 $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']); 2521 $this->assertEmpty($result['cm']['advancedgrading'][0]['method']); 2522 $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']); 2523 $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']); 2524 2525 $student = $this->getDataGenerator()->create_user(); 2526 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 2527 2528 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 2529 $this->setUser($student); 2530 2531 // The user shouldn't be able to see the activity. 2532 try { 2533 core_course_external::get_course_module($assign->cmid); 2534 $this->fail('Exception expected due to invalid permissions.'); 2535 } catch (moodle_exception $e) { 2536 $this->assertEquals('requireloginerror', $e->errorcode); 2537 } 2538 2539 // Make module visible. 2540 set_coursemodule_visible($assign->cmid, 1); 2541 2542 // Test student user. 2543 $result = core_course_external::get_course_module($assign->cmid); 2544 $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result); 2545 2546 $this->assertCount(0, $result['warnings']); 2547 // Test we retrieve only the few files we can see. 2548 $this->assertCount(12, $result['cm']); 2549 $this->assertEquals($assign->cmid, $result['cm']['id']); 2550 $this->assertEquals($course->id, $result['cm']['course']); 2551 $this->assertEquals('assign', $result['cm']['modname']); 2552 $this->assertEquals($assign->id, $result['cm']['instance']); 2553 2554 } 2555 2556 /** 2557 * Test get_course_module_by_instance 2558 */ 2559 public function test_get_course_module_by_instance() { 2560 global $DB; 2561 2562 $this->resetAfterTest(true); 2563 2564 $this->setAdminUser(); 2565 $course = self::getDataGenerator()->create_course(); 2566 $record = array( 2567 'course' => $course->id, 2568 'name' => 'First quiz', 2569 'grade' => 90.00 2570 ); 2571 $options = array( 2572 'idnumber' => 'ABC', 2573 'visible' => 0 2574 ); 2575 // Hidden activity. 2576 $quiz = self::getDataGenerator()->create_module('quiz', $record, $options); 2577 2578 // Test admin user can see the complete hidden activity. 2579 $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id); 2580 $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result); 2581 2582 $this->assertCount(0, $result['warnings']); 2583 // Test we retrieve all the fields. 2584 $this->assertCount(28, $result['cm']); 2585 $this->assertEquals($record['name'], $result['cm']['name']); 2586 $this->assertEquals($record['grade'], $result['cm']['grade']); 2587 $this->assertEquals($options['idnumber'], $result['cm']['idnumber']); 2588 $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $result['cm']['downloadcontent']); 2589 2590 $student = $this->getDataGenerator()->create_user(); 2591 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 2592 2593 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 2594 $this->setUser($student); 2595 2596 // The user shouldn't be able to see the activity. 2597 try { 2598 core_course_external::get_course_module_by_instance('quiz', $quiz->id); 2599 $this->fail('Exception expected due to invalid permissions.'); 2600 } catch (moodle_exception $e) { 2601 $this->assertEquals('requireloginerror', $e->errorcode); 2602 } 2603 2604 // Make module visible. 2605 set_coursemodule_visible($quiz->cmid, 1); 2606 2607 // Test student user. 2608 $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id); 2609 $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result); 2610 2611 $this->assertCount(0, $result['warnings']); 2612 // Test we retrieve only the few files we can see. 2613 $this->assertCount(12, $result['cm']); 2614 $this->assertEquals($quiz->cmid, $result['cm']['id']); 2615 $this->assertEquals($course->id, $result['cm']['course']); 2616 $this->assertEquals('quiz', $result['cm']['modname']); 2617 $this->assertEquals($quiz->id, $result['cm']['instance']); 2618 2619 // Try with an invalid module name. 2620 try { 2621 core_course_external::get_course_module_by_instance('abc', $quiz->id); 2622 $this->fail('Exception expected due to invalid module name.'); 2623 } catch (dml_read_exception $e) { 2624 $this->assertEquals('dmlreadexception', $e->errorcode); 2625 } 2626 2627 } 2628 2629 /** 2630 * Test get_user_navigation_options 2631 */ 2632 public function test_get_user_navigation_options() { 2633 global $USER; 2634 2635 $this->resetAfterTest(); 2636 $course1 = self::getDataGenerator()->create_course(); 2637 $course2 = self::getDataGenerator()->create_course(); 2638 2639 // Create a viewer user. 2640 $viewer = self::getDataGenerator()->create_user(); 2641 $this->getDataGenerator()->enrol_user($viewer->id, $course1->id); 2642 $this->getDataGenerator()->enrol_user($viewer->id, $course2->id); 2643 2644 $this->setUser($viewer->id); 2645 $courses = array($course1->id , $course2->id, SITEID); 2646 2647 $result = core_course_external::get_user_navigation_options($courses); 2648 $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result); 2649 2650 $this->assertCount(0, $result['warnings']); 2651 $this->assertCount(3, $result['courses']); 2652 2653 foreach ($result['courses'] as $course) { 2654 $navoptions = new stdClass; 2655 foreach ($course['options'] as $option) { 2656 $navoptions->{$option['name']} = $option['available']; 2657 } 2658 $this->assertCount(8, $course['options']); 2659 if ($course['id'] == SITEID) { 2660 $this->assertTrue($navoptions->blogs); 2661 $this->assertFalse($navoptions->notes); 2662 $this->assertFalse($navoptions->participants); 2663 $this->assertTrue($navoptions->badges); 2664 $this->assertTrue($navoptions->tags); 2665 $this->assertFalse($navoptions->grades); 2666 $this->assertFalse($navoptions->search); 2667 $this->assertTrue($navoptions->competencies); 2668 } else { 2669 $this->assertTrue($navoptions->blogs); 2670 $this->assertFalse($navoptions->notes); 2671 $this->assertTrue($navoptions->participants); 2672 $this->assertFalse($navoptions->badges); 2673 $this->assertFalse($navoptions->tags); 2674 $this->assertTrue($navoptions->grades); 2675 $this->assertFalse($navoptions->search); 2676 $this->assertTrue($navoptions->competencies); 2677 } 2678 } 2679 } 2680 2681 /** 2682 * Test get_user_administration_options 2683 */ 2684 public function test_get_user_administration_options() { 2685 global $USER; 2686 2687 $this->resetAfterTest(); 2688 $course1 = self::getDataGenerator()->create_course(); 2689 $course2 = self::getDataGenerator()->create_course(); 2690 2691 // Create a viewer user. 2692 $viewer = self::getDataGenerator()->create_user(); 2693 $this->getDataGenerator()->enrol_user($viewer->id, $course1->id); 2694 $this->getDataGenerator()->enrol_user($viewer->id, $course2->id); 2695 2696 $this->setUser($viewer->id); 2697 $courses = array($course1->id , $course2->id, SITEID); 2698 2699 $result = core_course_external::get_user_administration_options($courses); 2700 $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result); 2701 2702 $this->assertCount(0, $result['warnings']); 2703 $this->assertCount(3, $result['courses']); 2704 2705 foreach ($result['courses'] as $course) { 2706 $adminoptions = new stdClass; 2707 foreach ($course['options'] as $option) { 2708 $adminoptions->{$option['name']} = $option['available']; 2709 } 2710 if ($course['id'] == SITEID) { 2711 $this->assertCount(17, $course['options']); 2712 $this->assertFalse($adminoptions->update); 2713 $this->assertFalse($adminoptions->filters); 2714 $this->assertFalse($adminoptions->reports); 2715 $this->assertFalse($adminoptions->backup); 2716 $this->assertFalse($adminoptions->restore); 2717 $this->assertFalse($adminoptions->files); 2718 $this->assertFalse(!isset($adminoptions->tags)); 2719 $this->assertFalse($adminoptions->gradebook); 2720 $this->assertFalse($adminoptions->outcomes); 2721 $this->assertFalse($adminoptions->badges); 2722 $this->assertFalse($adminoptions->import); 2723 $this->assertFalse($adminoptions->reset); 2724 $this->assertFalse($adminoptions->roles); 2725 $this->assertFalse($adminoptions->editcompletion); 2726 $this->assertFalse($adminoptions->copy); 2727 } else { 2728 $this->assertCount(15, $course['options']); 2729 $this->assertFalse($adminoptions->update); 2730 $this->assertFalse($adminoptions->filters); 2731 $this->assertFalse($adminoptions->reports); 2732 $this->assertFalse($adminoptions->backup); 2733 $this->assertFalse($adminoptions->restore); 2734 $this->assertFalse($adminoptions->files); 2735 $this->assertFalse($adminoptions->tags); 2736 $this->assertFalse($adminoptions->gradebook); 2737 $this->assertFalse($adminoptions->outcomes); 2738 $this->assertTrue($adminoptions->badges); 2739 $this->assertFalse($adminoptions->import); 2740 $this->assertFalse($adminoptions->reset); 2741 $this->assertFalse($adminoptions->roles); 2742 $this->assertFalse($adminoptions->editcompletion); 2743 $this->assertFalse($adminoptions->copy); 2744 } 2745 } 2746 } 2747 2748 /** 2749 * Test get_courses_by_fields 2750 */ 2751 public function test_get_courses_by_field() { 2752 global $DB, $USER; 2753 $this->resetAfterTest(true); 2754 2755 $this->setAdminUser(); 2756 2757 $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1')); 2758 $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id)); 2759 $course1 = self::getDataGenerator()->create_course( 2760 array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics')); 2761 2762 $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']); 2763 $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text', 2764 'categoryid' => $fieldcategory->get('id')]; 2765 $field = self::getDataGenerator()->create_custom_field($customfield); 2766 $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value']; 2767 // Create course image. 2768 $draftid = file_get_unused_draft_itemid(); 2769 $filerecord = [ 2770 'component' => 'user', 2771 'filearea' => 'draft', 2772 'contextid' => context_user::instance($USER->id)->id, 2773 'itemid' => $draftid, 2774 'filename' => 'image.jpg', 2775 'filepath' => '/', 2776 ]; 2777 $fs = get_file_storage(); 2778 $fs->create_file_from_pathname($filerecord, __DIR__ . '/fixtures/image.jpg'); 2779 $course2 = self::getDataGenerator()->create_course([ 2780 'visible' => 0, 2781 'category' => $category2->id, 2782 'idnumber' => 'i2', 2783 'customfields' => [$customfieldvalue], 2784 'overviewfiles_filemanager' => $draftid 2785 ]); 2786 2787 $student1 = self::getDataGenerator()->create_user(); 2788 $user1 = self::getDataGenerator()->create_user(); 2789 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 2790 self::getDataGenerator()->enrol_user($student1->id, $course1->id, $studentrole->id); 2791 self::getDataGenerator()->enrol_user($student1->id, $course2->id, $studentrole->id); 2792 2793 self::setAdminUser(); 2794 // As admins, we should be able to retrieve everything. 2795 $result = core_course_external::get_courses_by_field(); 2796 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2797 $this->assertCount(3, $result['courses']); 2798 // Expect to receive all the fields. 2799 $this->assertCount(41, $result['courses'][0]); 2800 $this->assertCount(42, $result['courses'][1]); // One more field because is not the site course. 2801 $this->assertCount(42, $result['courses'][2]); // One more field because is not the site course. 2802 2803 $result = core_course_external::get_courses_by_field('id', $course1->id); 2804 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2805 $this->assertCount(1, $result['courses']); 2806 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2807 // Expect to receive all the fields. 2808 $this->assertCount(42, $result['courses'][0]); 2809 // Check default values for course format topics. 2810 $this->assertCount(3, $result['courses'][0]['courseformatoptions']); 2811 foreach ($result['courses'][0]['courseformatoptions'] as $option) { 2812 switch ($option['name']) { 2813 case 'hiddensections': 2814 $this->assertEquals(1, $option['value']); 2815 break; 2816 case 'coursedisplay': 2817 $this->assertEquals(0, $option['value']); 2818 break; 2819 case 'indentation': 2820 $this->assertEquals(1, $option['value']); 2821 break; 2822 default: 2823 } 2824 } 2825 $this->assertStringContainsString('/course/generated', $result['courses'][0]['courseimage']); 2826 2827 $result = core_course_external::get_courses_by_field('id', $course2->id); 2828 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2829 $this->assertCount(1, $result['courses']); 2830 $this->assertEquals($course2->id, $result['courses'][0]['id']); 2831 // Check custom fields properly returned. 2832 $this->assertEquals([ 2833 'shortname' => $customfield['shortname'], 2834 'name' => $customfield['name'], 2835 'type' => $customfield['type'], 2836 'value' => $customfieldvalue['value'], 2837 'valueraw' => $customfieldvalue['value'], 2838 ], $result['courses'][0]['customfields'][0]); 2839 $this->assertStringContainsString('/course/overviewfiles', $result['courses'][0]['courseimage']); 2840 2841 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id"); 2842 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2843 $this->assertCount(2, $result['courses']); 2844 2845 // Check default filters. 2846 $this->assertCount(6, $result['courses'][0]['filters']); 2847 $this->assertCount(6, $result['courses'][1]['filters']); 2848 2849 $result = core_course_external::get_courses_by_field('category', $category1->id); 2850 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2851 $this->assertCount(1, $result['courses']); 2852 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2853 $this->assertEquals('Cat 1', $result['courses'][0]['categoryname']); 2854 2855 $result = core_course_external::get_courses_by_field('shortname', 'c1'); 2856 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2857 $this->assertCount(1, $result['courses']); 2858 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2859 2860 $result = core_course_external::get_courses_by_field('idnumber', 'i2'); 2861 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2862 $this->assertCount(1, $result['courses']); 2863 $this->assertEquals($course2->id, $result['courses'][0]['id']); 2864 2865 $result = core_course_external::get_courses_by_field('idnumber', 'x'); 2866 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2867 $this->assertCount(0, $result['courses']); 2868 2869 // Change filter value. 2870 filter_set_local_state('mediaplugin', context_course::instance($course1->id)->id, TEXTFILTER_OFF); 2871 2872 self::setUser($student1); 2873 // All visible courses (including front page) for normal student. 2874 $result = core_course_external::get_courses_by_field(); 2875 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2876 $this->assertCount(2, $result['courses']); 2877 $this->assertCount(34, $result['courses'][0]); 2878 $this->assertCount(35, $result['courses'][1]); // One field more (course format options), not present in site course. 2879 2880 $result = core_course_external::get_courses_by_field('id', $course1->id); 2881 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2882 $this->assertCount(1, $result['courses']); 2883 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2884 // Expect to receive all the files that a student can see. 2885 $this->assertCount(35, $result['courses'][0]); 2886 2887 // Check default filters. 2888 $filters = $result['courses'][0]['filters']; 2889 $this->assertCount(6, $filters); 2890 $found = false; 2891 foreach ($filters as $filter) { 2892 if ($filter['filter'] == 'mediaplugin' and $filter['localstate'] == TEXTFILTER_OFF) { 2893 $found = true; 2894 } 2895 } 2896 $this->assertTrue($found); 2897 2898 // Course 2 is not visible. 2899 $result = core_course_external::get_courses_by_field('id', $course2->id); 2900 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2901 $this->assertCount(0, $result['courses']); 2902 2903 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id"); 2904 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2905 $this->assertCount(1, $result['courses']); 2906 2907 $result = core_course_external::get_courses_by_field('category', $category1->id); 2908 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2909 $this->assertCount(1, $result['courses']); 2910 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2911 2912 $result = core_course_external::get_courses_by_field('shortname', 'c1'); 2913 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2914 $this->assertCount(1, $result['courses']); 2915 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2916 2917 $result = core_course_external::get_courses_by_field('idnumber', 'i2'); 2918 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2919 $this->assertCount(0, $result['courses']); 2920 2921 $result = core_course_external::get_courses_by_field('idnumber', 'x'); 2922 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2923 $this->assertCount(0, $result['courses']); 2924 2925 self::setUser($user1); 2926 // All visible courses (including front page) for authenticated user. 2927 $result = core_course_external::get_courses_by_field(); 2928 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2929 $this->assertCount(2, $result['courses']); 2930 $this->assertCount(34, $result['courses'][0]); // Site course. 2931 $this->assertCount(17, $result['courses'][1]); // Only public information, not enrolled. 2932 2933 $result = core_course_external::get_courses_by_field('id', $course1->id); 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 // Expect to receive all the files that a authenticated can see. 2938 $this->assertCount(17, $result['courses'][0]); 2939 2940 // Course 2 is not visible. 2941 $result = core_course_external::get_courses_by_field('id', $course2->id); 2942 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2943 $this->assertCount(0, $result['courses']); 2944 2945 $result = core_course_external::get_courses_by_field('ids', "$course1->id,$course2->id"); 2946 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2947 $this->assertCount(1, $result['courses']); 2948 2949 $result = core_course_external::get_courses_by_field('category', $category1->id); 2950 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2951 $this->assertCount(1, $result['courses']); 2952 $this->assertEquals($course1->id, $result['courses'][0]['id']); 2953 2954 $result = core_course_external::get_courses_by_field('shortname', 'c1'); 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 2959 $result = core_course_external::get_courses_by_field('idnumber', 'i2'); 2960 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2961 $this->assertCount(0, $result['courses']); 2962 2963 $result = core_course_external::get_courses_by_field('idnumber', 'x'); 2964 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 2965 $this->assertCount(0, $result['courses']); 2966 } 2967 2968 /** 2969 * Test retrieving courses by field returns custom field data 2970 */ 2971 public function test_get_courses_by_field_customfields(): void { 2972 $this->resetAfterTest(); 2973 $this->setAdminUser(); 2974 2975 $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]); 2976 $datefield = $this->getDataGenerator()->create_custom_field([ 2977 'categoryid' => $fieldcategory->get('id'), 2978 'shortname' => 'mydate', 2979 'name' => 'My date', 2980 'type' => 'date', 2981 ]); 2982 2983 $newcourse = $this->getDataGenerator()->create_course(['customfields' => [ 2984 [ 2985 'shortname' => $datefield->get('shortname'), 2986 'value' => 1580389200, // 30/01/2020 13:00 GMT. 2987 ], 2988 ]]); 2989 2990 $result = external_api::clean_returnvalue( 2991 core_course_external::get_courses_by_field_returns(), 2992 core_course_external::get_courses_by_field('id', $newcourse->id) 2993 ); 2994 2995 $this->assertCount(1, $result['courses']); 2996 $course = reset($result['courses']); 2997 2998 $this->assertArrayHasKey('customfields', $course); 2999 $this->assertCount(1, $course['customfields']); 3000 3001 // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version. 3002 $this->assertEquals([ 3003 'name' => $datefield->get('name'), 3004 'shortname' => $datefield->get('shortname'), 3005 'type' => $datefield->get('type'), 3006 'value' => userdate(1580389200), 3007 'valueraw' => 1580389200, 3008 ], reset($course['customfields'])); 3009 } 3010 3011 public function test_get_courses_by_field_invalid_field() { 3012 $this->expectException('invalid_parameter_exception'); 3013 $result = core_course_external::get_courses_by_field('zyx', 'x'); 3014 } 3015 3016 public function test_get_courses_by_field_invalid_courses() { 3017 $result = core_course_external::get_courses_by_field('id', '-1'); 3018 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 3019 $this->assertCount(0, $result['courses']); 3020 } 3021 3022 /** 3023 * Test get_courses_by_field_invalid_theme_and_lang 3024 */ 3025 public function test_get_courses_by_field_invalid_theme_and_lang() { 3026 $this->resetAfterTest(true); 3027 $this->setAdminUser(); 3028 3029 $course = self::getDataGenerator()->create_course(array('theme' => 'kkt', 'lang' => 'kkl')); 3030 $result = core_course_external::get_courses_by_field('id', $course->id); 3031 $result = external_api::clean_returnvalue(core_course_external::get_courses_by_field_returns(), $result); 3032 $this->assertEmpty($result['courses']['0']['theme']); 3033 $this->assertEmpty($result['courses']['0']['lang']); 3034 } 3035 3036 3037 public function test_check_updates() { 3038 global $DB; 3039 $this->resetAfterTest(true); 3040 $this->setAdminUser(); 3041 3042 // Create different types of activities. 3043 $course = self::getDataGenerator()->create_course(); 3044 $tocreate = array('assign', 'book', 'choice', 'folder', 'forum', 'glossary', 'imscp', 'label', 'lti', 'page', 'quiz', 3045 'resource', 'scorm', 'survey', 'url', 'wiki'); 3046 3047 $modules = array(); 3048 foreach ($tocreate as $modname) { 3049 $modules[$modname]['instance'] = $this->getDataGenerator()->create_module($modname, array('course' => $course->id)); 3050 $modules[$modname]['cm'] = get_coursemodule_from_id(false, $modules[$modname]['instance']->cmid); 3051 $modules[$modname]['context'] = context_module::instance($modules[$modname]['instance']->cmid); 3052 } 3053 3054 $student = self::getDataGenerator()->create_user(); 3055 $studentrole = $DB->get_record('role', array('shortname' => 'student')); 3056 self::getDataGenerator()->enrol_user($student->id, $course->id, $studentrole->id); 3057 $this->setUser($student); 3058 3059 $since = time(); 3060 $this->waitForSecond(); 3061 $params = array(); 3062 foreach ($modules as $modname => $data) { 3063 $params[$data['cm']->id] = array( 3064 'contextlevel' => 'module', 3065 'id' => $data['cm']->id, 3066 'since' => $since 3067 ); 3068 } 3069 3070 // Check there is nothing updated because modules are fresh new. 3071 $result = core_course_external::check_updates($course->id, $params); 3072 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result); 3073 $this->assertCount(0, $result['instances']); 3074 $this->assertCount(0, $result['warnings']); 3075 3076 // Test with get_updates_since the same data. 3077 $result = core_course_external::get_updates_since($course->id, $since); 3078 $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result); 3079 $this->assertCount(0, $result['instances']); 3080 $this->assertCount(0, $result['warnings']); 3081 3082 // Update a module after a second. 3083 $this->waitForSecond(); 3084 set_coursemodule_name($modules['forum']['cm']->id, 'New forum name'); 3085 3086 $found = false; 3087 $result = core_course_external::check_updates($course->id, $params); 3088 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result); 3089 $this->assertCount(1, $result['instances']); 3090 $this->assertCount(0, $result['warnings']); 3091 foreach ($result['instances'] as $module) { 3092 foreach ($module['updates'] as $update) { 3093 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') { 3094 $found = true; 3095 } 3096 } 3097 } 3098 $this->assertTrue($found); 3099 3100 // Test with get_updates_since the same data. 3101 $result = core_course_external::get_updates_since($course->id, $since); 3102 $result = external_api::clean_returnvalue(core_course_external::get_updates_since_returns(), $result); 3103 $this->assertCount(1, $result['instances']); 3104 $this->assertCount(0, $result['warnings']); 3105 $found = false; 3106 $this->assertCount(1, $result['instances']); 3107 $this->assertCount(0, $result['warnings']); 3108 foreach ($result['instances'] as $module) { 3109 foreach ($module['updates'] as $update) { 3110 if ($module['id'] == $modules['forum']['cm']->id and $update['name'] == 'configuration') { 3111 $found = true; 3112 } 3113 } 3114 } 3115 $this->assertTrue($found); 3116 3117 // Do not retrieve the configuration field. 3118 $filter = array('files'); 3119 $found = false; 3120 $result = core_course_external::check_updates($course->id, $params, $filter); 3121 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result); 3122 $this->assertCount(0, $result['instances']); 3123 $this->assertCount(0, $result['warnings']); 3124 $this->assertFalse($found); 3125 3126 // Add invalid cmid. 3127 $params[] = array( 3128 'contextlevel' => 'module', 3129 'id' => -2, 3130 'since' => $since 3131 ); 3132 $result = core_course_external::check_updates($course->id, $params); 3133 $result = external_api::clean_returnvalue(core_course_external::check_updates_returns(), $result); 3134 $this->assertCount(1, $result['warnings']); 3135 $this->assertEquals(-2, $result['warnings'][0]['itemid']); 3136 } 3137 3138 /** 3139 * Test cases for the get_enrolled_courses_by_timeline_classification test. 3140 */ 3141 public function get_get_enrolled_courses_by_timeline_classification_test_cases():array { 3142 $now = time(); 3143 $day = 86400; 3144 3145 $coursedata = [ 3146 [ 3147 'shortname' => 'apast', 3148 'startdate' => $now - ($day * 2), 3149 'enddate' => $now - $day 3150 ], 3151 [ 3152 'shortname' => 'bpast', 3153 'startdate' => $now - ($day * 2), 3154 'enddate' => $now - $day 3155 ], 3156 [ 3157 'shortname' => 'cpast', 3158 'startdate' => $now - ($day * 2), 3159 'enddate' => $now - $day 3160 ], 3161 [ 3162 'shortname' => 'dpast', 3163 'startdate' => $now - ($day * 2), 3164 'enddate' => $now - $day 3165 ], 3166 [ 3167 'shortname' => 'epast', 3168 'startdate' => $now - ($day * 2), 3169 'enddate' => $now - $day 3170 ], 3171 [ 3172 'shortname' => 'ainprogress', 3173 'startdate' => $now - $day, 3174 'enddate' => $now + $day 3175 ], 3176 [ 3177 'shortname' => 'binprogress', 3178 'startdate' => $now - $day, 3179 'enddate' => $now + $day 3180 ], 3181 [ 3182 'shortname' => 'cinprogress', 3183 'startdate' => $now - $day, 3184 'enddate' => $now + $day 3185 ], 3186 [ 3187 'shortname' => 'dinprogress', 3188 'startdate' => $now - $day, 3189 'enddate' => $now + $day 3190 ], 3191 [ 3192 'shortname' => 'einprogress', 3193 'startdate' => $now - $day, 3194 'enddate' => $now + $day 3195 ], 3196 [ 3197 'shortname' => 'afuture', 3198 'startdate' => $now + $day 3199 ], 3200 [ 3201 'shortname' => 'bfuture', 3202 'startdate' => $now + $day 3203 ], 3204 [ 3205 'shortname' => 'cfuture', 3206 'startdate' => $now + $day 3207 ], 3208 [ 3209 'shortname' => 'dfuture', 3210 'startdate' => $now + $day 3211 ], 3212 [ 3213 'shortname' => 'efuture', 3214 'startdate' => $now + $day 3215 ] 3216 ]; 3217 3218 // Raw enrolled courses result set should be returned in this order: 3219 // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast, 3220 // dfuture, dinprogress, dpast, efuture, einprogress, epast 3221 // 3222 // By classification the offset values for each record should be: 3223 // COURSE_TIMELINE_FUTURE 3224 // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture) 3225 // COURSE_TIMELINE_INPROGRESS 3226 // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress) 3227 // COURSE_TIMELINE_PAST 3228 // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast). 3229 // 3230 // NOTE: The offset applies to the unfiltered full set of courses before the classification 3231 // filtering is done. 3232 // E.g. In our example if an offset of 2 is given then it would mean the first 3233 // two courses (afuture, ainprogress) are ignored. 3234 return [ 3235 'empty set' => [ 3236 'coursedata' => [], 3237 'classification' => 'future', 3238 'limit' => 2, 3239 'offset' => 0, 3240 'sort' => 'shortname ASC', 3241 'expectedcourses' => [], 3242 'expectednextoffset' => 0, 3243 ], 3244 // COURSE_TIMELINE_FUTURE. 3245 'future not limit no offset' => [ 3246 'coursedata' => $coursedata, 3247 'classification' => 'future', 3248 'limit' => 0, 3249 'offset' => 0, 3250 'sort' => 'shortname ASC', 3251 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'], 3252 'expectednextoffset' => 15, 3253 ], 3254 'future no offset' => [ 3255 'coursedata' => $coursedata, 3256 'classification' => 'future', 3257 'limit' => 2, 3258 'offset' => 0, 3259 'sort' => 'shortname ASC', 3260 'expectedcourses' => ['afuture', 'bfuture'], 3261 'expectednextoffset' => 4, 3262 ], 3263 'future offset' => [ 3264 'coursedata' => $coursedata, 3265 'classification' => 'future', 3266 'limit' => 2, 3267 'offset' => 2, 3268 'sort' => 'shortname ASC', 3269 'expectedcourses' => ['bfuture', 'cfuture'], 3270 'expectednextoffset' => 7, 3271 ], 3272 'future exact limit' => [ 3273 'coursedata' => $coursedata, 3274 'classification' => 'future', 3275 'limit' => 5, 3276 'offset' => 0, 3277 'sort' => 'shortname ASC', 3278 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'], 3279 'expectednextoffset' => 13, 3280 ], 3281 'future limit less results' => [ 3282 'coursedata' => $coursedata, 3283 'classification' => 'future', 3284 'limit' => 10, 3285 'offset' => 0, 3286 'sort' => 'shortname ASC', 3287 'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'], 3288 'expectednextoffset' => 15, 3289 ], 3290 'future limit less results with offset' => [ 3291 'coursedata' => $coursedata, 3292 'classification' => 'future', 3293 'limit' => 10, 3294 'offset' => 5, 3295 'sort' => 'shortname ASC', 3296 'expectedcourses' => ['cfuture', 'dfuture', 'efuture'], 3297 'expectednextoffset' => 15, 3298 ], 3299 'all no limit or offset' => [ 3300 'coursedata' => $coursedata, 3301 'classification' => 'all', 3302 'limit' => 0, 3303 'offset' => 0, 3304 'sort' => 'shortname ASC', 3305 'expectedcourses' => [ 3306 'afuture', 3307 'ainprogress', 3308 'apast', 3309 'bfuture', 3310 'binprogress', 3311 'bpast', 3312 'cfuture', 3313 'cinprogress', 3314 'cpast', 3315 'dfuture', 3316 'dinprogress', 3317 'dpast', 3318 'efuture', 3319 'einprogress', 3320 'epast' 3321 ], 3322 'expectednextoffset' => 15, 3323 ], 3324 'all limit no offset' => [ 3325 'coursedata' => $coursedata, 3326 'classification' => 'all', 3327 'limit' => 5, 3328 'offset' => 0, 3329 'sort' => 'shortname ASC', 3330 'expectedcourses' => [ 3331 'afuture', 3332 'ainprogress', 3333 'apast', 3334 'bfuture', 3335 'binprogress' 3336 ], 3337 'expectednextoffset' => 5, 3338 ], 3339 'all limit and offset' => [ 3340 'coursedata' => $coursedata, 3341 'classification' => 'all', 3342 'limit' => 5, 3343 'offset' => 5, 3344 'sort' => 'shortname ASC', 3345 'expectedcourses' => [ 3346 'bpast', 3347 'cfuture', 3348 'cinprogress', 3349 'cpast', 3350 'dfuture' 3351 ], 3352 'expectednextoffset' => 10, 3353 ], 3354 'all offset past result set' => [ 3355 'coursedata' => $coursedata, 3356 'classification' => 'all', 3357 'limit' => 5, 3358 'offset' => 50, 3359 'sort' => 'shortname ASC', 3360 'expectedcourses' => [], 3361 'expectednextoffset' => 50, 3362 ], 3363 'all limit and offset with sort ul.timeaccess desc' => [ 3364 'coursedata' => $coursedata, 3365 'classification' => 'inprogress', 3366 'limit' => 0, 3367 'offset' => 0, 3368 'sort' => 'ul.timeaccess desc', 3369 'expectedcourses' => [ 3370 'ainprogress', 3371 'binprogress', 3372 'cinprogress', 3373 'dinprogress', 3374 'einprogress' 3375 ], 3376 'expectednextoffset' => 15, 3377 ], 3378 'all limit and offset with sort sql injection for sort or 1==1' => [ 3379 'coursedata' => $coursedata, 3380 'classification' => 'all', 3381 'limit' => 5, 3382 'offset' => 5, 3383 'sort' => 'ul.timeaccess desc or 1==1', 3384 'expectedcourses' => [], 3385 'expectednextoffset' => 0, 3386 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', 3387 ], 3388 'all limit and offset with sql injection of sort a custom one' => [ 3389 'coursedata' => $coursedata, 3390 'classification' => 'all', 3391 'limit' => 5, 3392 'offset' => 5, 3393 'sort' => "ul.timeaccess LIMIT 1--", 3394 'expectedcourses' => [], 3395 'expectednextoffset' => 0, 3396 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', 3397 ], 3398 'all limit and offset with wrong sort direction' => [ 3399 'coursedata' => $coursedata, 3400 'classification' => 'all', 3401 'limit' => 5, 3402 'offset' => 5, 3403 'sort' => "ul.timeaccess abcdasc", 3404 'expectedcourses' => [], 3405 'expectednextoffset' => 0, 3406 'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()', 3407 ], 3408 'all limit and offset with wrong sort direction' => [ 3409 'coursedata' => $coursedata, 3410 'classification' => 'all', 3411 'limit' => 5, 3412 'offset' => 5, 3413 'sort' => "ul.timeaccess.foo ascd", 3414 'expectedcourses' => [], 3415 'expectednextoffset' => 0, 3416 'expectedexception' => 'Invalid sort direction in $sort parameter in enrol_get_my_courses()', 3417 ], 3418 'all limit and offset with wrong sort param' => [ 3419 'coursedata' => $coursedata, 3420 'classification' => 'all', 3421 'limit' => 5, 3422 'offset' => 5, 3423 'sort' => "foobar", 3424 'expectedcourses' => [], 3425 'expectednextoffset' => 0, 3426 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', 3427 ], 3428 'all limit and offset with wrong field name' => [ 3429 'coursedata' => $coursedata, 3430 'classification' => 'all', 3431 'limit' => 5, 3432 'offset' => 5, 3433 'sort' => "ul.foobar", 3434 'expectedcourses' => [], 3435 'expectednextoffset' => 0, 3436 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', 3437 ], 3438 'all limit and offset with wrong field separator' => [ 3439 'coursedata' => $coursedata, 3440 'classification' => 'all', 3441 'limit' => 5, 3442 'offset' => 5, 3443 'sort' => "ul.timeaccess.foo", 3444 'expectedcourses' => [], 3445 'expectednextoffset' => 0, 3446 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', 3447 ], 3448 'all limit and offset with wrong field separator #' => [ 3449 'coursedata' => $coursedata, 3450 'classification' => 'all', 3451 'limit' => 5, 3452 'offset' => 5, 3453 'sort' => "ul#timeaccess", 3454 'expectedcourses' => [], 3455 'expectednextoffset' => 0, 3456 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', 3457 ], 3458 'all limit and offset with wrong field separator $' => [ 3459 'coursedata' => $coursedata, 3460 'classification' => 'all', 3461 'limit' => 5, 3462 'offset' => 5, 3463 'sort' => 'ul$timeaccess', 3464 'expectedcourses' => [], 3465 'expectednextoffset' => 0, 3466 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', 3467 ], 3468 'all limit and offset with wrong field name' => [ 3469 'coursedata' => $coursedata, 3470 'classification' => 'all', 3471 'limit' => 5, 3472 'offset' => 5, 3473 'sort' => 'timeaccess123', 3474 'expectedcourses' => [], 3475 'expectednextoffset' => 0, 3476 'expectedexception' => 'Invalid $sort parameter in enrol_get_my_courses()', 3477 ], 3478 'all limit and offset with no sort direction for ul' => [ 3479 'coursedata' => $coursedata, 3480 'classification' => 'inprogress', 3481 'limit' => 0, 3482 'offset' => 0, 3483 'sort' => "ul.timeaccess", 3484 'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'], 3485 'expectednextoffset' => 15, 3486 ], 3487 'all limit and offset with valid field name and no prefix, test for ul' => [ 3488 'coursedata' => $coursedata, 3489 'classification' => 'inprogress', 3490 'limit' => 0, 3491 'offset' => 0, 3492 'sort' => "timeaccess", 3493 'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'], 3494 'expectednextoffset' => 15, 3495 ], 3496 'all limit and offset with valid field name and no prefix' => [ 3497 'coursedata' => $coursedata, 3498 'classification' => 'all', 3499 'limit' => 5, 3500 'offset' => 5, 3501 'sort' => "fullname", 3502 'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'], 3503 'expectednextoffset' => 10, 3504 ], 3505 'all limit and offset with valid field name and no prefix and with sort direction' => [ 3506 'coursedata' => $coursedata, 3507 'classification' => 'all', 3508 'limit' => 5, 3509 'offset' => 5, 3510 'sort' => "fullname desc", 3511 'expectedcourses' => ['bpast', 'cpast', 'dfuture', 'dpast', 'efuture'], 3512 'expectednextoffset' => 10, 3513 ], 3514 'Search courses for courses containing bfut' => [ 3515 'coursedata' => $coursedata, 3516 'classification' => 'search', 3517 'limit' => 0, 3518 'offset' => 0, 3519 'sort' => null, 3520 'expectedcourses' => ['bfuture'], 3521 'expectednextoffset' => 1, 3522 'expectedexception' => null, 3523 'searchvalue' => 'bfut', 3524 ], 3525 'Search courses for courses containing inp' => [ 3526 'coursedata' => $coursedata, 3527 'classification' => 'search', 3528 'limit' => 0, 3529 'offset' => 0, 3530 'sort' => null, 3531 'expectedcourses' => ['ainprogress', 'binprogress', 'cinprogress', 'dinprogress', 'einprogress'], 3532 'expectednextoffset' => 5, 3533 'expectedexception' => null, 3534 'searchvalue' => 'inp', 3535 ], 3536 'Search courses for courses containing fail' => [ 3537 'coursedata' => $coursedata, 3538 'classification' => 'search', 3539 'limit' => 0, 3540 'offset' => 0, 3541 'sort' => null, 3542 'expectedcourses' => [], 3543 'expectednextoffset' => 0, 3544 'expectedexception' => null, 3545 'searchvalue' => 'fail', 3546 ], 3547 'Search courses for courses containing !`~[]C' => [ 3548 'coursedata' => $coursedata, 3549 'classification' => 'search', 3550 'limit' => 0, 3551 'offset' => 0, 3552 'sort' => null, 3553 'expectedcourses' => [], 3554 'expectednextoffset' => 0, 3555 'expectedexception' => null, 3556 'searchvalue' => '!`~[]C', 3557 ], 3558 ]; 3559 } 3560 3561 /** 3562 * Test the get_enrolled_courses_by_timeline_classification function. 3563 * 3564 * @dataProvider get_get_enrolled_courses_by_timeline_classification_test_cases() 3565 * @param array $coursedata Courses to create 3566 * @param string $classification Timeline classification 3567 * @param int $limit Maximum number of results 3568 * @param int $offset Offset the unfiltered courses result set by this amount 3569 * @param string $sort sort the courses 3570 * @param array $expectedcourses Expected courses in result 3571 * @param int $expectednextoffset Expected next offset value in result 3572 * @param string|null $expectedexception Expected exception string 3573 * @param string|null $searchvalue If we are searching, what do we need to look for? 3574 */ 3575 public function test_get_enrolled_courses_by_timeline_classification( 3576 $coursedata, 3577 $classification, 3578 $limit, 3579 $offset, 3580 $sort, 3581 $expectedcourses, 3582 $expectednextoffset, 3583 $expectedexception = null, 3584 $searchvalue = null 3585 ) { 3586 $this->resetAfterTest(); 3587 $generator = $this->getDataGenerator(); 3588 3589 $courses = array_map(function($coursedata) use ($generator) { 3590 return $generator->create_course($coursedata); 3591 }, $coursedata); 3592 3593 $student = $generator->create_user(); 3594 3595 foreach ($courses as $course) { 3596 $generator->enrol_user($student->id, $course->id, 'student'); 3597 } 3598 3599 $this->setUser($student); 3600 3601 if (isset($expectedexception)) { 3602 $this->expectException('coding_exception'); 3603 $this->expectExceptionMessage($expectedexception); 3604 } 3605 3606 // NOTE: The offset applies to the unfiltered full set of courses before the classification 3607 // filtering is done. 3608 // E.g. In our example if an offset of 2 is given then it would mean the first 3609 // two courses (afuture, ainprogress) are ignored. 3610 $result = core_course_external::get_enrolled_courses_by_timeline_classification( 3611 $classification, 3612 $limit, 3613 $offset, 3614 $sort, 3615 null, 3616 null, 3617 $searchvalue 3618 ); 3619 $result = external_api::clean_returnvalue( 3620 core_course_external::get_enrolled_courses_by_timeline_classification_returns(), 3621 $result 3622 ); 3623 3624 $actual = array_map(function($course) { 3625 return $course['shortname']; 3626 }, $result['courses']); 3627 3628 $this->assertEqualsCanonicalizing($expectedcourses, $actual); 3629 $this->assertEquals($expectednextoffset, $result['nextoffset']); 3630 } 3631 3632 /** 3633 * Test the get_recent_courses function. 3634 */ 3635 public function test_get_recent_courses() { 3636 global $USER, $DB; 3637 3638 $this->resetAfterTest(); 3639 $generator = $this->getDataGenerator(); 3640 3641 set_config('hiddenuserfields', 'lastaccess'); 3642 3643 $courses = array(); 3644 for ($i = 1; $i < 12; $i++) { 3645 $courses[] = $generator->create_course(); 3646 }; 3647 3648 $student = $generator->create_user(); 3649 $teacher = $generator->create_user(); 3650 3651 foreach ($courses as $course) { 3652 $generator->enrol_user($student->id, $course->id, 'student'); 3653 } 3654 3655 $generator->enrol_user($teacher->id, $courses[0]->id, 'teacher'); 3656 3657 $this->setUser($student); 3658 3659 $result = core_course_external::get_recent_courses($USER->id); 3660 3661 // No course accessed. 3662 $this->assertCount(0, $result); 3663 3664 foreach ($courses as $course) { 3665 core_course_external::view_course($course->id); 3666 } 3667 3668 // Every course accessed. 3669 $result = core_course_external::get_recent_courses($USER->id); 3670 $this->assertCount( 11, $result); 3671 3672 // Every course accessed, result limited to 10 courses. 3673 $result = core_course_external::get_recent_courses($USER->id, 10); 3674 $this->assertCount(10, $result); 3675 3676 $guestcourse = $generator->create_course( 3677 (object)array('shortname' => 'guestcourse', 3678 'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED, 3679 'enrol_guest_password_0' => '')); 3680 core_course_external::view_course($guestcourse->id); 3681 3682 // Every course accessed, even the not enrolled one. 3683 $result = core_course_external::get_recent_courses($USER->id); 3684 $this->assertCount(12, $result); 3685 3686 // Offset 5, return 7 out of 12. 3687 $result = core_course_external::get_recent_courses($USER->id, 0, 5); 3688 $this->assertCount(7, $result); 3689 3690 // Offset 5 and limit 3, return 3 out of 12. 3691 $result = core_course_external::get_recent_courses($USER->id, 3, 5); 3692 $this->assertCount(3, $result); 3693 3694 // Sorted by course id ASC. 3695 $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id ASC'); 3696 $this->assertEquals($courses[0]->id, array_shift($result)->id); 3697 3698 // Sorted by course id DESC. 3699 $result = core_course_external::get_recent_courses($USER->id, 0, 0, 'id DESC'); 3700 $this->assertEquals($guestcourse->id, array_shift($result)->id); 3701 3702 // If last access is hidden, only get the courses where has viewhiddenuserfields capability. 3703 $this->setUser($teacher); 3704 $teacherroleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher')); 3705 $usercontext = context_user::instance($student->id); 3706 $this->assignUserCapability('moodle/user:viewdetails', $usercontext, $teacherroleid); 3707 3708 // Sorted by course id DESC. 3709 $result = core_course_external::get_recent_courses($student->id); 3710 $this->assertCount(1, $result); 3711 $this->assertEquals($courses[0]->id, array_shift($result)->id); 3712 } 3713 3714 /** 3715 * Test get enrolled users by cmid function. 3716 */ 3717 public function test_get_enrolled_users_by_cmid() { 3718 global $PAGE; 3719 $this->resetAfterTest(true); 3720 3721 $user1 = self::getDataGenerator()->create_user(); 3722 $user2 = self::getDataGenerator()->create_user(); 3723 $user3 = self::getDataGenerator()->create_user(); 3724 3725 $user1picture = new user_picture($user1); 3726 $user1picture->size = 1; 3727 $user1->profileimage = $user1picture->get_url($PAGE)->out(false); 3728 3729 $user2picture = new user_picture($user2); 3730 $user2picture->size = 1; 3731 $user2->profileimage = $user2picture->get_url($PAGE)->out(false); 3732 3733 $user3picture = new user_picture($user3); 3734 $user3picture->size = 1; 3735 $user3->profileimage = $user3picture->get_url($PAGE)->out(false); 3736 3737 // Set the first created user to the test user. 3738 self::setUser($user1); 3739 3740 // Create course to add the module. 3741 $course1 = self::getDataGenerator()->create_course(); 3742 3743 // Forum with tracking off. 3744 $record = new stdClass(); 3745 $record->course = $course1->id; 3746 $forum1 = self::getDataGenerator()->create_module('forum', $record); 3747 3748 // Following lines enrol and assign default role id to the users. 3749 $this->getDataGenerator()->enrol_user($user1->id, $course1->id); 3750 $this->getDataGenerator()->enrol_user($user2->id, $course1->id); 3751 // Enrol a suspended user in the course. 3752 $this->getDataGenerator()->enrol_user($user3->id, $course1->id, null, 'manual', 0, 0, ENROL_USER_SUSPENDED); 3753 3754 // Create what we expect to be returned when querying the course module. 3755 $expectedusers = array( 3756 'users' => array(), 3757 'warnings' => array(), 3758 ); 3759 3760 $expectedusers['users'][0] = [ 3761 'id' => $user1->id, 3762 'fullname' => fullname($user1), 3763 'firstname' => $user1->firstname, 3764 'lastname' => $user1->lastname, 3765 'profileimage' => $user1->profileimage, 3766 ]; 3767 $expectedusers['users'][1] = [ 3768 'id' => $user2->id, 3769 'fullname' => fullname($user2), 3770 'firstname' => $user2->firstname, 3771 'lastname' => $user2->lastname, 3772 'profileimage' => $user2->profileimage, 3773 ]; 3774 $expectedusers['users'][2] = [ 3775 'id' => $user3->id, 3776 'fullname' => fullname($user3), 3777 'firstname' => $user3->firstname, 3778 'lastname' => $user3->lastname, 3779 'profileimage' => $user3->profileimage, 3780 ]; 3781 3782 // Test getting the users in a given context. 3783 $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid); 3784 $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users); 3785 3786 $this->assertEquals(3, count($users['users'])); 3787 $this->assertEquals($expectedusers, $users); 3788 3789 // Test getting only the active users in a given context. 3790 $users = core_course_external::get_enrolled_users_by_cmid($forum1->cmid, 0, true); 3791 $users = external_api::clean_returnvalue(core_course_external::get_enrolled_users_by_cmid_returns(), $users); 3792 3793 $expectedusers['users'] = [ 3794 [ 3795 'id' => $user1->id, 3796 'fullname' => fullname($user1), 3797 'firstname' => $user1->firstname, 3798 'lastname' => $user1->lastname, 3799 'profileimage' => $user1->profileimage, 3800 ], 3801 [ 3802 'id' => $user2->id, 3803 'fullname' => fullname($user2), 3804 'firstname' => $user2->firstname, 3805 'lastname' => $user2->lastname, 3806 'profileimage' => $user2->profileimage, 3807 ] 3808 ]; 3809 3810 $this->assertEquals(2, count($users['users'])); 3811 $this->assertEquals($expectedusers, $users); 3812 } 3813 3814 /** 3815 * Verify that content items can be added to user favourites. 3816 */ 3817 public function test_add_content_item_to_user_favourites() { 3818 $this->resetAfterTest(); 3819 3820 $course = $this->getDataGenerator()->create_course(); 3821 $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); 3822 $this->setUser($user); 3823 3824 // Using the internal API, confirm that no items are set as favourites for the user. 3825 $contentitemservice = new \core_course\local\service\content_item_service( 3826 new \core_course\local\repository\content_item_readonly_repository() 3827 ); 3828 $contentitems = $contentitemservice->get_all_content_items($user); 3829 $favourited = array_filter($contentitems, function($contentitem) { 3830 return $contentitem->favourite == true; 3831 }); 3832 $this->assertCount(0, $favourited); 3833 3834 // Using the external API, favourite a content item for the user. 3835 $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))]; 3836 $contentitem = core_course_external::add_content_item_to_user_favourites('mod_assign', $assign->id, $user->id); 3837 $contentitem = external_api::clean_returnvalue(core_course_external::add_content_item_to_user_favourites_returns(), 3838 $contentitem); 3839 3840 // Verify the returned item is a favourite. 3841 $this->assertTrue($contentitem['favourite']); 3842 3843 // Using the internal API, confirm we see a single favourite item. 3844 $contentitems = $contentitemservice->get_all_content_items($user); 3845 $favourited = array_values(array_filter($contentitems, function($contentitem) { 3846 return $contentitem->favourite == true; 3847 })); 3848 $this->assertCount(1, $favourited); 3849 $this->assertEquals('assign', $favourited[0]->name); 3850 } 3851 3852 /** 3853 * Verify that content items can be removed from user favourites. 3854 */ 3855 public function test_remove_content_item_from_user_favourites() { 3856 $this->resetAfterTest(); 3857 3858 $course = $this->getDataGenerator()->create_course(); 3859 $user = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher'); 3860 $this->setUser($user); 3861 3862 // Using the internal API, set a favourite for the user. 3863 $contentitemservice = new \core_course\local\service\content_item_service( 3864 new \core_course\local\repository\content_item_readonly_repository() 3865 ); 3866 $contentitems = $contentitemservice->get_all_content_items($user); 3867 $assign = $contentitems[array_search('assign', array_column($contentitems, 'name'))]; 3868 $contentitemservice->add_to_user_favourites($user, $assign->componentname, $assign->id); 3869 3870 $contentitems = $contentitemservice->get_all_content_items($user); 3871 $favourited = array_filter($contentitems, function($contentitem) { 3872 return $contentitem->favourite == true; 3873 }); 3874 $this->assertCount(1, $favourited); 3875 3876 // Now, verify the external API can remove the favourite. 3877 $contentitem = core_course_external::remove_content_item_from_user_favourites('mod_assign', $assign->id); 3878 $contentitem = external_api::clean_returnvalue(core_course_external::remove_content_item_from_user_favourites_returns(), 3879 $contentitem); 3880 3881 // Verify the returned item is a favourite. 3882 $this->assertFalse($contentitem['favourite']); 3883 3884 // Using the internal API, confirm we see no favourite items. 3885 $contentitems = $contentitemservice->get_all_content_items($user); 3886 $favourited = array_filter($contentitems, function($contentitem) { 3887 return $contentitem->favourite == true; 3888 }); 3889 $this->assertCount(0, $favourited); 3890 } 3891 3892 /** 3893 * Test the web service returning course content items for inclusion in activity choosers, etc. 3894 */ 3895 public function test_get_course_content_items() { 3896 $this->resetAfterTest(); 3897 3898 $course = self::getDataGenerator()->create_course(); 3899 $user = self::getDataGenerator()->create_and_enrol($course, 'editingteacher'); 3900 3901 // Fetch available content items as the editing teacher. 3902 $this->setUser($user); 3903 $result = core_course_external::get_course_content_items($course->id); 3904 $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result); 3905 3906 $contentitemservice = new \core_course\local\service\content_item_service( 3907 new \core_course\local\repository\content_item_readonly_repository() 3908 ); 3909 3910 // Check if the webservice returns exactly what the service defines, albeit in array form. 3911 $serviceitemsasarray = array_map(function($item) { 3912 return (array) $item; 3913 }, $contentitemservice->get_content_items_for_user_in_course($user, $course)); 3914 3915 $this->assertEquals($serviceitemsasarray, $result['content_items']); 3916 } 3917 3918 /** 3919 * Test the web service returning course content items, specifically in case where the user can't manage activities. 3920 */ 3921 public function test_get_course_content_items_no_permission_to_manage() { 3922 $this->resetAfterTest(); 3923 3924 $course = self::getDataGenerator()->create_course(); 3925 $user = self::getDataGenerator()->create_and_enrol($course, 'student'); 3926 3927 // Fetch available content items as a student, who won't have the permission to manage activities. 3928 $this->setUser($user); 3929 $result = core_course_external::get_course_content_items($course->id); 3930 $result = external_api::clean_returnvalue(core_course_external::get_course_content_items_returns(), $result); 3931 3932 $this->assertEmpty($result['content_items']); 3933 } 3934 3935 /** 3936 * Test toggling the recommendation of an activity. 3937 */ 3938 public function test_toggle_activity_recommendation() { 3939 global $CFG; 3940 3941 $this->resetAfterTest(); 3942 3943 $context = context_system::instance(); 3944 $usercontext = context_user::instance($CFG->siteguest); 3945 $component = 'core_course'; 3946 $favouritefactory = \core_favourites\service_factory::get_service_for_user_context($usercontext); 3947 3948 $areaname = 'test_core'; 3949 $areaid = 3; 3950 3951 // Test we have the favourite. 3952 $this->setAdminUser(); 3953 $result = core_course_external::toggle_activity_recommendation($areaname, $areaid); 3954 $this->assertTrue($favouritefactory->favourite_exists($component, 3955 \core_course\local\service\content_item_service::RECOMMENDATION_PREFIX . $areaname, $areaid, $context)); 3956 $this->assertTrue($result['status']); 3957 // Test that it is now gone. 3958 $result = core_course_external::toggle_activity_recommendation($areaname, $areaid); 3959 $this->assertFalse($favouritefactory->favourite_exists($component, $areaname, $areaid, $context)); 3960 $this->assertFalse($result['status']); 3961 } 3962 3963 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body