Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]

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