Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
  • Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 37 and 311] [Versions 38 and 311] [Versions 39 and 311]

       1  <?php
       2  // This file is part of Moodle - http://moodle.org/
       3  //
       4  // Moodle is free software: you can redistribute it and/or modify
       5  // it under the terms of the GNU General Public License as published by
       6  // the Free Software Foundation, either version 3 of the License, or
       7  // (at your option) any later version.
       8  //
       9  // Moodle is distributed in the hope that it will be useful,
      10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
      11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
      12  // GNU General Public License for more details.
      13  //
      14  // You should have received a copy of the GNU General Public License
      15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
      16  
      17  /**
      18   * External course functions unit tests
      19   *
      20   * @package    core_course
      21   * @category   external
      22   * @copyright  2012 Jerome Mouneyrac
      23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      24   */
      25  
      26  defined('MOODLE_INTERNAL') || die();
      27  
      28  global $CFG;
      29  
      30  require_once($CFG->dirroot . '/webservice/tests/helpers.php');
      31  
      32  /**
      33   * External course functions unit tests
      34   *
      35   * @package    core_course
      36   * @category   external
      37   * @copyright  2012 Jerome Mouneyrac
      38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
      39   */
      40  class core_course_externallib_testcase extends externallib_advanced_testcase {
      41  
      42      /**
      43       * Tests set up
      44       */
      45      protected function setUp(): void {
      46          global $CFG;
      47          require_once($CFG->dirroot . '/course/externallib.php');
      48      }
      49  
      50      /**
      51       * Test create_categories
      52       */
      53      public function test_create_categories() {
      54  
      55          global $DB;
      56  
      57          $this->resetAfterTest(true);
      58  
      59          // Set the required capabilities by the external function
      60          $contextid = context_system::instance()->id;
      61          $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
      62  
      63          // Create base categories.
      64          $category1 = new stdClass();
      65          $category1->name = 'Root Test Category 1';
      66          $category2 = new stdClass();
      67          $category2->name = 'Root Test Category 2';
      68          $category2->idnumber = 'rootcattest2';
      69          $category2->desc = 'Description for root test category 1';
      70          $category2->theme = 'classic';
      71          $categories = array(
      72              array('name' => $category1->name, 'parent' => 0),
      73              array('name' => $category2->name, 'parent' => 0, 'idnumber' => $category2->idnumber,
      74                  'description' => $category2->desc, 'theme' => $category2->theme)
      75          );
      76  
      77          $createdcats = core_course_external::create_categories($categories);
      78  
      79          // We need to execute the return values cleaning process to simulate the web service server.
      80          $createdcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdcats);
      81  
      82          // Initially confirm that base data was inserted correctly.
      83          $this->assertEquals($category1->name, $createdcats[0]['name']);
      84          $this->assertEquals($category2->name, $createdcats[1]['name']);
      85  
      86          // Save the ids.
      87          $category1->id = $createdcats[0]['id'];
      88          $category2->id = $createdcats[1]['id'];
      89  
      90          // Create on sub category.
      91          $category3 = new stdClass();
      92          $category3->name = 'Sub Root Test Category 3';
      93          $subcategories = array(
      94              array('name' => $category3->name, 'parent' => $category1->id)
      95          );
      96  
      97          $createdsubcats = core_course_external::create_categories($subcategories);
      98  
      99          // We need to execute the return values cleaning process to simulate the web service server.
     100          $createdsubcats = external_api::clean_returnvalue(core_course_external::create_categories_returns(), $createdsubcats);
     101  
     102          // Confirm that sub categories were inserted correctly.
     103          $this->assertEquals($category3->name, $createdsubcats[0]['name']);
     104  
     105          // Save the ids.
     106          $category3->id = $createdsubcats[0]['id'];
     107  
     108          // Calling the ws function should provide a new sortorder to give category1,
     109          // category2, category3. New course categories are ordered by id not name.
     110          $category1 = $DB->get_record('course_categories', array('id' => $category1->id));
     111          $category2 = $DB->get_record('course_categories', array('id' => $category2->id));
     112          $category3 = $DB->get_record('course_categories', array('id' => $category3->id));
     113  
     114          // sortorder sequence (and sortorder) must be:
     115          // category 1
     116          //   category 3
     117          // category 2
     118          $this->assertGreaterThan($category1->sortorder, $category3->sortorder);
     119          $this->assertGreaterThan($category3->sortorder, $category2->sortorder);
     120  
     121          // Call without required capability
     122          $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
     123          $this->expectException('required_capability_exception');
     124          $createdsubcats = core_course_external::create_categories($subcategories);
     125  
     126      }
     127  
     128      /**
     129       * Test delete categories
     130       */
     131      public function test_delete_categories() {
     132          global $DB;
     133  
     134          $this->resetAfterTest(true);
     135  
     136          // Set the required capabilities by the external function
     137          $contextid = context_system::instance()->id;
     138          $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
     139  
     140          $category1  = self::getDataGenerator()->create_category();
     141          $category2  = self::getDataGenerator()->create_category(
     142                  array('parent' => $category1->id));
     143          $category3  = self::getDataGenerator()->create_category();
     144          $category4  = self::getDataGenerator()->create_category(
     145                  array('parent' => $category3->id));
     146          $category5  = self::getDataGenerator()->create_category(
     147                  array('parent' => $category4->id));
     148  
     149          //delete category 1 and 2 + delete category 4, category 5 moved under category 3
     150          core_course_external::delete_categories(array(
     151              array('id' => $category1->id, 'recursive' => 1),
     152              array('id' => $category4->id)
     153          ));
     154  
     155          //check $category 1 and 2 are deleted
     156          $notdeletedcount = $DB->count_records_select('course_categories',
     157              'id IN ( ' . $category1->id . ',' . $category2->id . ',' . $category4->id . ')');
     158          $this->assertEquals(0, $notdeletedcount);
     159  
     160          //check that $category5 as $category3 for parent
     161          $dbcategory5 = $DB->get_record('course_categories', array('id' => $category5->id));
     162          $this->assertEquals($dbcategory5->path, $category3->path . '/' . $category5->id);
     163  
     164           // Call without required capability
     165          $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
     166          $this->expectException('required_capability_exception');
     167          $createdsubcats = core_course_external::delete_categories(
     168                  array(array('id' => $category3->id)));
     169      }
     170  
     171      /**
     172       * Test get categories
     173       */
     174      public function test_get_categories() {
     175          global $DB;
     176  
     177          $this->resetAfterTest(true);
     178  
     179          $generatedcats = array();
     180          $category1data['idnumber'] = 'idnumbercat1';
     181          $category1data['name'] = 'Category 1 for PHPunit test';
     182          $category1data['description'] = 'Category 1 description';
     183          $category1data['descriptionformat'] = FORMAT_MOODLE;
     184          $category1  = self::getDataGenerator()->create_category($category1data);
     185          $generatedcats[$category1->id] = $category1;
     186          $category2  = self::getDataGenerator()->create_category(
     187                  array('parent' => $category1->id));
     188          $generatedcats[$category2->id] = $category2;
     189          $category6  = self::getDataGenerator()->create_category(
     190                  array('parent' => $category1->id, 'visible' => 0));
     191          $generatedcats[$category6->id] = $category6;
     192          $category3  = self::getDataGenerator()->create_category();
     193          $generatedcats[$category3->id] = $category3;
     194          $category4  = self::getDataGenerator()->create_category(
     195                  array('parent' => $category3->id));
     196          $generatedcats[$category4->id] = $category4;
     197          $category5  = self::getDataGenerator()->create_category(
     198                  array('parent' => $category4->id));
     199          $generatedcats[$category5->id] = $category5;
     200  
     201          // Set the required capabilities by the external function.
     202          $context = context_system::instance();
     203          $roleid = $this->assignUserCapability('moodle/category:manage', $context->id);
     204          $this->assignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
     205  
     206          // Retrieve category1 + sub-categories except not visible ones
     207          $categories = core_course_external::get_categories(array(
     208              array('key' => 'id', 'value' => $category1->id),
     209              array('key' => 'visible', 'value' => 1)), 1);
     210  
     211          // We need to execute the return values cleaning process to simulate the web service server.
     212          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
     213  
     214          // Check we retrieve the good total number of categories.
     215          $this->assertEquals(2, count($categories));
     216  
     217          // Check the return values
     218          foreach ($categories as $category) {
     219              $generatedcat = $generatedcats[$category['id']];
     220              $this->assertEquals($category['idnumber'], $generatedcat->idnumber);
     221              $this->assertEquals($category['name'], $generatedcat->name);
     222              // Description was converted to the HTML format.
     223              $this->assertEquals($category['description'], format_text($generatedcat->description, FORMAT_MOODLE, array('para' => false)));
     224              $this->assertEquals($category['descriptionformat'], FORMAT_HTML);
     225          }
     226  
     227          // Check categories by ids.
     228          $ids = implode(',', array_keys($generatedcats));
     229          $categories = core_course_external::get_categories(array(
     230              array('key' => 'ids', 'value' => $ids)), 0);
     231  
     232          // We need to execute the return values cleaning process to simulate the web service server.
     233          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
     234  
     235          // Check we retrieve the good total number of categories.
     236          $this->assertEquals(6, count($categories));
     237          // Check ids.
     238          $returnedids = [];
     239          foreach ($categories as $category) {
     240              $returnedids[] = $category['id'];
     241          }
     242          // Sort the arrays upon comparision.
     243          $this->assertEqualsCanonicalizing(array_keys($generatedcats), $returnedids);
     244  
     245          // Check different params.
     246          $categories = core_course_external::get_categories(array(
     247              array('key' => 'id', 'value' => $category1->id),
     248              array('key' => 'ids', 'value' => $category1->id),
     249              array('key' => 'idnumber', 'value' => $category1->idnumber),
     250              array('key' => 'visible', 'value' => 1)), 0);
     251  
     252          // We need to execute the return values cleaning process to simulate the web service server.
     253          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
     254  
     255          $this->assertEquals(1, count($categories));
     256  
     257          // Same query, but forcing a parameters clean.
     258          $categories = core_course_external::get_categories(array(
     259              array('key' => 'id', 'value' => "$category1->id"),
     260              array('key' => 'idnumber', 'value' => $category1->idnumber),
     261              array('key' => 'name', 'value' => $category1->name . "<br/>"),
     262              array('key' => 'visible', 'value' => '1')), 0);
     263          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
     264  
     265          $this->assertEquals(1, count($categories));
     266  
     267          // Retrieve categories from parent.
     268          $categories = core_course_external::get_categories(array(
     269              array('key' => 'parent', 'value' => $category3->id)), 1);
     270          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
     271  
     272          $this->assertEquals(2, count($categories));
     273  
     274          // Retrieve all categories.
     275          $categories = core_course_external::get_categories();
     276  
     277          // We need to execute the return values cleaning process to simulate the web service server.
     278          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
     279  
     280          $this->assertEquals($DB->count_records('course_categories'), count($categories));
     281  
     282          $this->unassignUserCapability('moodle/category:viewhiddencategories', $context->id, $roleid);
     283  
     284          // Ensure maxdepthcategory is 2 and retrieve all categories without category:viewhiddencategories capability.
     285          // It should retrieve all visible categories as well.
     286          set_config('maxcategorydepth', 2);
     287          $categories = core_course_external::get_categories();
     288  
     289          // We need to execute the return values cleaning process to simulate the web service server.
     290          $categories = external_api::clean_returnvalue(core_course_external::get_categories_returns(), $categories);
     291  
     292          $this->assertEquals($DB->count_records('course_categories', array('visible' => 1)), count($categories));
     293  
     294          // Call without required capability (it will fail cause of the search on idnumber).
     295          $this->expectException('moodle_exception');
     296          $categories = core_course_external::get_categories(array(
     297              array('key' => 'id', 'value' => $category1->id),
     298              array('key' => 'idnumber', 'value' => $category1->idnumber),
     299              array('key' => 'visible', 'value' => 1)), 0);
     300      }
     301  
     302      /**
     303       * Test update_categories
     304       */
     305      public function test_update_categories() {
     306          global $DB;
     307  
     308          $this->resetAfterTest(true);
     309  
     310          // Set the required capabilities by the external function
     311          $contextid = context_system::instance()->id;
     312          $roleid = $this->assignUserCapability('moodle/category:manage', $contextid);
     313  
     314          // Create base categories.
     315          $category1data['idnumber'] = 'idnumbercat1';
     316          $category1data['name'] = 'Category 1 for PHPunit test';
     317          $category1data['description'] = 'Category 1 description';
     318          $category1data['descriptionformat'] = FORMAT_MOODLE;
     319          $category1  = self::getDataGenerator()->create_category($category1data);
     320          $category2  = self::getDataGenerator()->create_category(
     321                  array('parent' => $category1->id));
     322          $category3  = self::getDataGenerator()->create_category();
     323          $category4  = self::getDataGenerator()->create_category(
     324                  array('parent' => $category3->id));
     325          $category5  = self::getDataGenerator()->create_category(
     326                  array('parent' => $category4->id));
     327  
     328          // We update all category1 attribut.
     329          // Then we move cat4 and cat5 parent: cat3 => cat1
     330          $categories = array(
     331              array('id' => $category1->id,
     332                  'name' => $category1->name . '_updated',
     333                  'idnumber' => $category1->idnumber . '_updated',
     334                  'description' => $category1->description . '_updated',
     335                  'descriptionformat' => FORMAT_HTML,
     336                  'theme' => $category1->theme),
     337              array('id' => $category4->id, 'parent' => $category1->id));
     338  
     339          core_course_external::update_categories($categories);
     340  
     341          // Check the values were updated.
     342          $dbcategories = $DB->get_records_select('course_categories',
     343                  'id IN (' . $category1->id . ',' . $category2->id . ',' . $category2->id
     344                  . ',' . $category3->id . ',' . $category4->id . ',' . $category5->id .')');
     345          $this->assertEquals($category1->name . '_updated',
     346                  $dbcategories[$category1->id]->name);
     347          $this->assertEquals($category1->idnumber . '_updated',
     348                  $dbcategories[$category1->id]->idnumber);
     349          $this->assertEquals($category1->description . '_updated',
     350                  $dbcategories[$category1->id]->description);
     351          $this->assertEquals(FORMAT_HTML, $dbcategories[$category1->id]->descriptionformat);
     352  
     353          // Check that category4 and category5 have been properly moved.
     354          $this->assertEquals('/' . $category1->id . '/' . $category4->id,
     355                  $dbcategories[$category4->id]->path);
     356          $this->assertEquals('/' . $category1->id . '/' . $category4->id . '/' . $category5->id,
     357                  $dbcategories[$category5->id]->path);
     358  
     359          // Call without required capability.
     360          $this->unassignUserCapability('moodle/category:manage', $contextid, $roleid);
     361          $this->expectException('required_capability_exception');
     362          core_course_external::update_categories($categories);
     363      }
     364  
     365      /**
     366       * Test create_courses numsections
     367       */
     368      public function test_create_course_numsections() {
     369          global $DB;
     370  
     371          $this->resetAfterTest(true);
     372  
     373          // Set the required capabilities by the external function.
     374          $contextid = context_system::instance()->id;
     375          $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
     376          $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
     377  
     378          $numsections = 10;
     379          $category  = self::getDataGenerator()->create_category();
     380  
     381          // Create base categories.
     382          $course1['fullname'] = 'Test course 1';
     383          $course1['shortname'] = 'Testcourse1';
     384          $course1['categoryid'] = $category->id;
     385          $course1['courseformatoptions'][] = array('name' => 'numsections', 'value' => $numsections);
     386  
     387          $courses = array($course1);
     388  
     389          $createdcourses = core_course_external::create_courses($courses);
     390          foreach ($createdcourses as $createdcourse) {
     391              $existingsections = $DB->get_records('course_sections', array('course' => $createdcourse['id']));
     392              $modinfo = get_fast_modinfo($createdcourse['id']);
     393              $sections = $modinfo->get_section_info_all();
     394              $this->assertEquals(count($sections), $numsections + 1); // Includes generic section.
     395              $this->assertEquals(count($existingsections), $numsections + 1); // Includes generic section.
     396          }
     397      }
     398  
     399      /**
     400       * Test create_courses
     401       */
     402      public function test_create_courses() {
     403          global $DB;
     404  
     405          $this->resetAfterTest(true);
     406  
     407          // Enable course completion.
     408          set_config('enablecompletion', 1);
     409          // Enable course themes.
     410          set_config('allowcoursethemes', 1);
     411  
     412          // Custom fields.
     413          $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
     414  
     415          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
     416              'categoryid' => $fieldcategory->get('id'),
     417              'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL]];
     418          $field = self::getDataGenerator()->create_custom_field($customfield);
     419  
     420          // Set the required capabilities by the external function
     421          $contextid = context_system::instance()->id;
     422          $roleid = $this->assignUserCapability('moodle/course:create', $contextid);
     423          $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
     424          $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
     425  
     426          $category  = self::getDataGenerator()->create_category();
     427  
     428          // Create base categories.
     429          $course1['fullname'] = 'Test course 1';
     430          $course1['shortname'] = 'Testcourse1';
     431          $course1['categoryid'] = $category->id;
     432          $course2['fullname'] = 'Test course 2';
     433          $course2['shortname'] = 'Testcourse2';
     434          $course2['categoryid'] = $category->id;
     435          $course2['idnumber'] = 'testcourse2idnumber';
     436          $course2['summary'] = 'Description for course 2';
     437          $course2['summaryformat'] = FORMAT_MOODLE;
     438          $course2['format'] = 'weeks';
     439          $course2['showgrades'] = 1;
     440          $course2['newsitems'] = 3;
     441          $course2['startdate'] = 1420092000; // 01/01/2015.
     442          $course2['enddate'] = 1422669600; // 01/31/2015.
     443          $course2['numsections'] = 4;
     444          $course2['maxbytes'] = 100000;
     445          $course2['showreports'] = 1;
     446          $course2['visible'] = 0;
     447          $course2['hiddensections'] = 0;
     448          $course2['groupmode'] = 0;
     449          $course2['groupmodeforce'] = 0;
     450          $course2['defaultgroupingid'] = 0;
     451          $course2['enablecompletion'] = 1;
     452          $course2['completionnotify'] = 1;
     453          $course2['lang'] = 'en';
     454          $course2['forcetheme'] = 'classic';
     455          $course2['courseformatoptions'][] = array('name' => 'automaticenddate', 'value' => 0);
     456          $course3['fullname'] = 'Test course 3';
     457          $course3['shortname'] = 'Testcourse3';
     458          $course3['categoryid'] = $category->id;
     459          $course3['format'] = 'topics';
     460          $course3options = array('numsections' => 8,
     461              'hiddensections' => 1,
     462              'coursedisplay' => 1);
     463          $course3['courseformatoptions'] = array();
     464          foreach ($course3options as $key => $value) {
     465              $course3['courseformatoptions'][] = array('name' => $key, 'value' => $value);
     466          }
     467          $course4['fullname'] = 'Test course with custom fields';
     468          $course4['shortname'] = 'Testcoursecustomfields';
     469          $course4['categoryid'] = $category->id;
     470          $course4['customfields'] = [['shortname' => $customfield['shortname'], 'value' => 'Test value']];
     471          $courses = array($course4, $course1, $course2, $course3);
     472  
     473          $createdcourses = core_course_external::create_courses($courses);
     474  
     475          // We need to execute the return values cleaning process to simulate the web service server.
     476          $createdcourses = external_api::clean_returnvalue(core_course_external::create_courses_returns(), $createdcourses);
     477  
     478          // Check that right number of courses were created.
     479          $this->assertEquals(4, count($createdcourses));
     480  
     481          // Check that the courses were correctly created.
     482          foreach ($createdcourses as $createdcourse) {
     483              $courseinfo = course_get_format($createdcourse['id'])->get_course();
     484  
     485              if ($createdcourse['shortname'] == $course2['shortname']) {
     486                  $this->assertEquals($courseinfo->fullname, $course2['fullname']);
     487                  $this->assertEquals($courseinfo->shortname, $course2['shortname']);
     488                  $this->assertEquals($courseinfo->category, $course2['categoryid']);
     489                  $this->assertEquals($courseinfo->idnumber, $course2['idnumber']);
     490                  $this->assertEquals($courseinfo->summary, $course2['summary']);
     491                  $this->assertEquals($courseinfo->summaryformat, $course2['summaryformat']);
     492                  $this->assertEquals($courseinfo->format, $course2['format']);
     493                  $this->assertEquals($courseinfo->showgrades, $course2['showgrades']);
     494                  $this->assertEquals($courseinfo->newsitems, $course2['newsitems']);
     495                  $this->assertEquals($courseinfo->startdate, $course2['startdate']);
     496                  $this->assertEquals($courseinfo->enddate, $course2['enddate']);
     497                  $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(), $course2['numsections']);
     498                  $this->assertEquals($courseinfo->maxbytes, $course2['maxbytes']);
     499                  $this->assertEquals($courseinfo->showreports, $course2['showreports']);
     500                  $this->assertEquals($courseinfo->visible, $course2['visible']);
     501                  $this->assertEquals($courseinfo->hiddensections, $course2['hiddensections']);
     502                  $this->assertEquals($courseinfo->groupmode, $course2['groupmode']);
     503                  $this->assertEquals($courseinfo->groupmodeforce, $course2['groupmodeforce']);
     504                  $this->assertEquals($courseinfo->defaultgroupingid, $course2['defaultgroupingid']);
     505                  $this->assertEquals($courseinfo->completionnotify, $course2['completionnotify']);
     506                  $this->assertEquals($courseinfo->lang, $course2['lang']);
     507                  $this->assertEquals($courseinfo->theme, $course2['forcetheme']);
     508  
     509                  // We enabled completion at the beginning of the test.
     510                  $this->assertEquals($courseinfo->enablecompletion, $course2['enablecompletion']);
     511  
     512              } else if ($createdcourse['shortname'] == $course1['shortname']) {
     513                  $courseconfig = get_config('moodlecourse');
     514                  $this->assertEquals($courseinfo->fullname, $course1['fullname']);
     515                  $this->assertEquals($courseinfo->shortname, $course1['shortname']);
     516                  $this->assertEquals($courseinfo->category, $course1['categoryid']);
     517                  $this->assertEquals($courseinfo->summaryformat, FORMAT_HTML);
     518                  $this->assertEquals($courseinfo->format, $courseconfig->format);
     519                  $this->assertEquals($courseinfo->showgrades, $courseconfig->showgrades);
     520                  $this->assertEquals($courseinfo->newsitems, $courseconfig->newsitems);
     521                  $this->assertEquals($courseinfo->maxbytes, $courseconfig->maxbytes);
     522                  $this->assertEquals($courseinfo->showreports, $courseconfig->showreports);
     523                  $this->assertEquals($courseinfo->groupmode, $courseconfig->groupmode);
     524                  $this->assertEquals($courseinfo->groupmodeforce, $courseconfig->groupmodeforce);
     525                  $this->assertEquals($courseinfo->defaultgroupingid, 0);
     526              } else if ($createdcourse['shortname'] == $course3['shortname']) {
     527                  $this->assertEquals($courseinfo->fullname, $course3['fullname']);
     528                  $this->assertEquals($courseinfo->shortname, $course3['shortname']);
     529                  $this->assertEquals($courseinfo->category, $course3['categoryid']);
     530                  $this->assertEquals($courseinfo->format, $course3['format']);
     531                  $this->assertEquals($courseinfo->hiddensections, $course3options['hiddensections']);
     532                  $this->assertEquals(course_get_format($createdcourse['id'])->get_last_section_number(),
     533                      $course3options['numsections']);
     534                  $this->assertEquals($courseinfo->coursedisplay, $course3options['coursedisplay']);
     535              } else if ($createdcourse['shortname'] == $course4['shortname']) {
     536                  $this->assertEquals($courseinfo->fullname, $course4['fullname']);
     537                  $this->assertEquals($courseinfo->shortname, $course4['shortname']);
     538                  $this->assertEquals($courseinfo->category, $course4['categoryid']);
     539  
     540                  $handler = core_course\customfield\course_handler::create();
     541                  $customfields = $handler->export_instance_data_object($createdcourse['id']);
     542                  $this->assertEquals((object)['test' => 'Test value'], $customfields);
     543              } else {
     544                  throw new moodle_exception('Unexpected shortname');
     545              }
     546          }
     547  
     548          // Call without required capability
     549          $this->unassignUserCapability('moodle/course:create', $contextid, $roleid);
     550          $this->expectException('required_capability_exception');
     551          $createdsubcats = core_course_external::create_courses($courses);
     552      }
     553  
     554      /**
     555       * Data provider for testing empty fields produce expected exceptions
     556       *
     557       * @see test_create_courses_empty_field
     558       * @see test_update_courses_empty_field
     559       *
     560       * @return array
     561       */
     562      public function course_empty_field_provider(): array {
     563          return [
     564              [[
     565                  'fullname' => '',
     566                  'shortname' => 'ws101',
     567              ], 'fullname'],
     568              [[
     569                  'fullname' => ' ',
     570                  'shortname' => 'ws101',
     571              ], 'fullname'],
     572              [[
     573                  'fullname' => 'Web Services',
     574                  'shortname' => '',
     575              ], 'shortname'],
     576              [[
     577                  'fullname' => 'Web Services',
     578                  'shortname' => ' ',
     579              ], 'shortname'],
     580          ];
     581      }
     582  
     583      /**
     584       * Test creating courses with empty fields throws an exception
     585       *
     586       * @param array $course
     587       * @param string $expectedemptyfield
     588       *
     589       * @dataProvider course_empty_field_provider
     590       */
     591      public function test_create_courses_empty_field(array $course, string $expectedemptyfield): void {
     592          $this->resetAfterTest();
     593          $this->setAdminUser();
     594  
     595          // Create a category for the new course.
     596          $course['categoryid'] = $this->getDataGenerator()->create_category()->id;
     597  
     598          $this->expectException(moodle_exception::class);
     599          $this->expectExceptionMessageMatches("/{$expectedemptyfield}/");
     600          core_course_external::create_courses([$course]);
     601      }
     602  
     603      /**
     604       * Test updating courses with empty fields returns warnings
     605       *
     606       * @param array $course
     607       * @param string $expectedemptyfield
     608       *
     609       * @dataProvider course_empty_field_provider
     610       */
     611      public function test_update_courses_empty_field(array $course, string $expectedemptyfield): void {
     612          $this->resetAfterTest();
     613          $this->setAdminUser();
     614  
     615          // Create a course to update.
     616          $course['id'] = $this->getDataGenerator()->create_course()->id;
     617  
     618          $result = core_course_external::update_courses([$course]);
     619          $result = core_course_external::clean_returnvalue(core_course_external::update_courses_returns(), $result);
     620  
     621          $this->assertCount(1, $result['warnings']);
     622  
     623          $warning = reset($result['warnings']);
     624          $this->assertEquals('errorinvalidparam', $warning['warningcode']);
     625          $this->assertStringContainsString($expectedemptyfield, $warning['message']);
     626      }
     627  
     628      /**
     629       * Test delete_courses
     630       */
     631      public function test_delete_courses() {
     632          global $DB, $USER;
     633  
     634          $this->resetAfterTest(true);
     635  
     636          // Admin can delete a course.
     637          $this->setAdminUser();
     638          // Validate_context() will fail as the email is not set by $this->setAdminUser().
     639          $USER->email = 'emailtopass@example.com';
     640  
     641          $course1  = self::getDataGenerator()->create_course();
     642          $course2  = self::getDataGenerator()->create_course();
     643          $course3  = self::getDataGenerator()->create_course();
     644  
     645          // Delete courses.
     646          $result = core_course_external::delete_courses(array($course1->id, $course2->id));
     647          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
     648          // Check for 0 warnings.
     649          $this->assertEquals(0, count($result['warnings']));
     650  
     651          // Check $course 1 and 2 are deleted.
     652          $notdeletedcount = $DB->count_records_select('course',
     653              'id IN ( ' . $course1->id . ',' . $course2->id . ')');
     654          $this->assertEquals(0, $notdeletedcount);
     655  
     656          // Try to delete non-existent course.
     657          $result = core_course_external::delete_courses(array($course1->id));
     658          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
     659          // Check for 1 warnings.
     660          $this->assertEquals(1, count($result['warnings']));
     661  
     662          // Try to delete Frontpage course.
     663          $result = core_course_external::delete_courses(array(0));
     664          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
     665          // Check for 1 warnings.
     666          $this->assertEquals(1, count($result['warnings']));
     667  
     668           // Fail when the user has access to course (enrolled) but does not have permission or is not admin.
     669          $student1 = self::getDataGenerator()->create_user();
     670          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
     671          $this->getDataGenerator()->enrol_user($student1->id,
     672                                                $course3->id,
     673                                                $studentrole->id);
     674          $this->setUser($student1);
     675          $result = core_course_external::delete_courses(array($course3->id));
     676          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
     677          // Check for 1 warnings.
     678          $this->assertEquals(1, count($result['warnings']));
     679  
     680           // Fail when the user is not allow to access the course (enrolled) or is not admin.
     681          $this->setGuestUser();
     682          $this->expectException('require_login_exception');
     683  
     684          $result = core_course_external::delete_courses(array($course3->id));
     685          $result = external_api::clean_returnvalue(core_course_external::delete_courses_returns(), $result);
     686      }
     687  
     688      /**
     689       * Test get_courses
     690       */
     691      public function test_get_courses () {
     692          global $DB;
     693  
     694          $this->resetAfterTest(true);
     695  
     696          $generatedcourses = array();
     697          $coursedata['idnumber'] = 'idnumbercourse1';
     698          // Adding tags here to check that format_string is applied.
     699          $coursedata['fullname'] = '<b>Course 1 for PHPunit test</b>';
     700          $coursedata['shortname'] = '<b>Course 1 for PHPunit test</b>';
     701          $coursedata['summary'] = 'Course 1 description';
     702          $coursedata['summaryformat'] = FORMAT_MOODLE;
     703          $course1  = self::getDataGenerator()->create_course($coursedata);
     704  
     705          $fieldcategory = self::getDataGenerator()->create_custom_field_category(
     706              ['name' => 'Other fields']);
     707  
     708          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
     709              'categoryid' => $fieldcategory->get('id')];
     710          $field = self::getDataGenerator()->create_custom_field($customfield);
     711  
     712          $customfieldvalue = ['shortname' => 'test', 'value' => 'Test value'];
     713  
     714          $generatedcourses[$course1->id] = $course1;
     715          $course2  = self::getDataGenerator()->create_course();
     716          $generatedcourses[$course2->id] = $course2;
     717          $course3  = self::getDataGenerator()->create_course(array('format' => 'topics'));
     718          $generatedcourses[$course3->id] = $course3;
     719          $course4  = self::getDataGenerator()->create_course(['customfields' => [$customfieldvalue]]);
     720          $generatedcourses[$course4->id] = $course4;
     721  
     722          // Set the required capabilities by the external function.
     723          $context = context_system::instance();
     724          $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
     725          $this->assignUserCapability('moodle/course:update',
     726                  context_course::instance($course1->id)->id, $roleid);
     727          $this->assignUserCapability('moodle/course:update',
     728                  context_course::instance($course2->id)->id, $roleid);
     729          $this->assignUserCapability('moodle/course:update',
     730                  context_course::instance($course3->id)->id, $roleid);
     731          $this->assignUserCapability('moodle/course:update',
     732                  context_course::instance($course4->id)->id, $roleid);
     733  
     734          $courses = core_course_external::get_courses(array('ids' =>
     735              array($course1->id, $course2->id, $course4->id)));
     736  
     737          // We need to execute the return values cleaning process to simulate the web service server.
     738          $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
     739  
     740          // Check we retrieve the good total number of courses.
     741          $this->assertEquals(3, count($courses));
     742  
     743          foreach ($courses as $course) {
     744              $coursecontext = context_course::instance($course['id']);
     745              $dbcourse = $generatedcourses[$course['id']];
     746              $this->assertEquals($course['idnumber'], $dbcourse->idnumber);
     747              $this->assertEquals($course['fullname'], external_format_string($dbcourse->fullname, $coursecontext->id));
     748              $this->assertEquals($course['displayname'], external_format_string(get_course_display_name_for_list($dbcourse),
     749                  $coursecontext->id));
     750              // Summary was converted to the HTML format.
     751              $this->assertEquals($course['summary'], format_text($dbcourse->summary, FORMAT_MOODLE, array('para' => false)));
     752              $this->assertEquals($course['summaryformat'], FORMAT_HTML);
     753              $this->assertEquals($course['shortname'], external_format_string($dbcourse->shortname, $coursecontext->id));
     754              $this->assertEquals($course['categoryid'], $dbcourse->category);
     755              $this->assertEquals($course['format'], $dbcourse->format);
     756              $this->assertEquals($course['showgrades'], $dbcourse->showgrades);
     757              $this->assertEquals($course['newsitems'], $dbcourse->newsitems);
     758              $this->assertEquals($course['startdate'], $dbcourse->startdate);
     759              $this->assertEquals($course['enddate'], $dbcourse->enddate);
     760              $this->assertEquals($course['numsections'], course_get_format($dbcourse)->get_last_section_number());
     761              $this->assertEquals($course['maxbytes'], $dbcourse->maxbytes);
     762              $this->assertEquals($course['showreports'], $dbcourse->showreports);
     763              $this->assertEquals($course['visible'], $dbcourse->visible);
     764              $this->assertEquals($course['hiddensections'], $dbcourse->hiddensections);
     765              $this->assertEquals($course['groupmode'], $dbcourse->groupmode);
     766              $this->assertEquals($course['groupmodeforce'], $dbcourse->groupmodeforce);
     767              $this->assertEquals($course['defaultgroupingid'], $dbcourse->defaultgroupingid);
     768              $this->assertEquals($course['completionnotify'], $dbcourse->completionnotify);
     769              $this->assertEquals($course['lang'], $dbcourse->lang);
     770              $this->assertEquals($course['forcetheme'], $dbcourse->theme);
     771              $this->assertEquals($course['enablecompletion'], $dbcourse->enablecompletion);
     772              if ($dbcourse->format === 'topics') {
     773                  $this->assertEquals($course['courseformatoptions'], array(
     774                      array('name' => 'hiddensections', 'value' => $dbcourse->hiddensections),
     775                      array('name' => 'coursedisplay', 'value' => $dbcourse->coursedisplay),
     776                  ));
     777              }
     778  
     779              // Assert custom field that we previously added to test course 4.
     780              if ($dbcourse->id == $course4->id) {
     781                  $this->assertEquals([
     782                      'shortname' => $customfield['shortname'],
     783                      'name' => $customfield['name'],
     784                      'type' => $customfield['type'],
     785                      'value' => $customfieldvalue['value'],
     786                      'valueraw' => $customfieldvalue['value'],
     787                  ], $course['customfields'][0]);
     788              }
     789          }
     790  
     791          // Get all courses in the DB
     792          $courses = core_course_external::get_courses(array());
     793  
     794          // We need to execute the return values cleaning process to simulate the web service server.
     795          $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
     796  
     797          $this->assertEquals($DB->count_records('course'), count($courses));
     798      }
     799  
     800      /**
     801       * Test retrieving courses returns custom field data
     802       */
     803      public function test_get_courses_customfields(): void {
     804          $this->resetAfterTest();
     805          $this->setAdminUser();
     806  
     807          $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
     808          $datefield = $this->getDataGenerator()->create_custom_field([
     809              'categoryid' => $fieldcategory->get('id'),
     810              'shortname' => 'mydate',
     811              'name' => 'My date',
     812              'type' => 'date',
     813          ]);
     814  
     815          $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
     816              [
     817                  'shortname' => $datefield->get('shortname'),
     818                  'value' => 1580389200, // 30/01/2020 13:00 GMT.
     819              ],
     820          ]]);
     821  
     822          $courses = external_api::clean_returnvalue(
     823              core_course_external::get_courses_returns(),
     824              core_course_external::get_courses(['ids' => [$newcourse->id]])
     825          );
     826  
     827          $this->assertCount(1, $courses);
     828          $course = reset($courses);
     829  
     830          $this->assertArrayHasKey('customfields', $course);
     831          $this->assertCount(1, $course['customfields']);
     832  
     833          // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
     834          $this->assertEquals([
     835              'name' => $datefield->get('name'),
     836              'shortname' => $datefield->get('shortname'),
     837              'type' => $datefield->get('type'),
     838              'value' => userdate(1580389200),
     839              'valueraw' => 1580389200,
     840          ], reset($course['customfields']));
     841      }
     842  
     843      /**
     844       * Test get_courses without capability
     845       */
     846      public function test_get_courses_without_capability() {
     847          $this->resetAfterTest(true);
     848  
     849          $course1 = $this->getDataGenerator()->create_course();
     850          $this->setUser($this->getDataGenerator()->create_user());
     851  
     852          // No permissions are required to get the site course.
     853          $courses = core_course_external::get_courses(array('ids' => [SITEID]));
     854          $courses = external_api::clean_returnvalue(core_course_external::get_courses_returns(), $courses);
     855  
     856          $this->assertEquals(1, count($courses));
     857          $this->assertEquals('PHPUnit test site', $courses[0]['fullname']);
     858          $this->assertEquals('site', $courses[0]['format']);
     859  
     860          // Requesting course without being enrolled or capability to view it will throw an exception.
     861          try {
     862              core_course_external::get_courses(array('ids' => [$course1->id]));
     863              $this->fail('Exception expected');
     864          } catch (moodle_exception $e) {
     865              $this->assertEquals(1, preg_match('/Course or activity not accessible. \(Not enrolled\)/', $e->getMessage()));
     866          }
     867      }
     868  
     869      /**
     870       * Test search_courses
     871       */
     872      public function test_search_courses () {
     873  
     874          global $DB;
     875  
     876          $this->resetAfterTest(true);
     877          $this->setAdminUser();
     878          $generatedcourses = array();
     879          $coursedata1['fullname'] = 'FIRST COURSE';
     880          $course1  = self::getDataGenerator()->create_course($coursedata1);
     881  
     882          $page = new moodle_page();
     883          $page->set_course($course1);
     884          $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
     885  
     886          $coursedata2['fullname'] = 'SECOND COURSE';
     887          $course2  = self::getDataGenerator()->create_course($coursedata2);
     888  
     889          $page = new moodle_page();
     890          $page->set_course($course2);
     891          $page->blocks->add_blocks([BLOCK_POS_LEFT => ['news_items'], BLOCK_POS_RIGHT => []], 'course-view-*');
     892  
     893          // Search by name.
     894          $results = core_course_external::search_courses('search', 'FIRST');
     895          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
     896          $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
     897          $this->assertCount(1, $results['courses']);
     898  
     899          // Create the forum.
     900          $record = new stdClass();
     901          $record->introformat = FORMAT_HTML;
     902          $record->course = $course2->id;
     903          // Set Aggregate type = Average of ratings.
     904          $forum = self::getDataGenerator()->create_module('forum', $record);
     905  
     906          // Search by module.
     907          $results = core_course_external::search_courses('modulelist', 'forum');
     908          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
     909          $this->assertEquals(1, $results['total']);
     910  
     911          // Enable coursetag option.
     912          set_config('block_tags_showcoursetags', true);
     913          // Add tag 'TAG-LABEL ON SECOND COURSE' to Course2.
     914          core_tag_tag::set_item_tags('core', 'course', $course2->id, context_course::instance($course2->id),
     915                  array('TAG-LABEL ON SECOND COURSE'));
     916          $taginstance = $DB->get_record('tag_instance',
     917                  array('itemtype' => 'course', 'itemid' => $course2->id), '*', MUST_EXIST);
     918  
     919          // Search by tagid.
     920          $results = core_course_external::search_courses('tagid', $taginstance->tagid);
     921          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
     922          $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
     923  
     924          // Search by block (use news_items default block).
     925          $blockid = $DB->get_field('block', 'id', array('name' => 'news_items'));
     926          $results = core_course_external::search_courses('blocklist', $blockid);
     927          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
     928          $this->assertEquals(2, $results['total']);
     929  
     930          // Now as a normal user.
     931          $user = self::getDataGenerator()->create_user();
     932  
     933          // Add a 3rd, hidden, course we shouldn't see, even when enrolled as student.
     934          $coursedata3['fullname'] = 'HIDDEN COURSE';
     935          $coursedata3['visible'] = 0;
     936          $course3  = self::getDataGenerator()->create_course($coursedata3);
     937          $this->getDataGenerator()->enrol_user($user->id, $course3->id, 'student');
     938  
     939          $this->getDataGenerator()->enrol_user($user->id, $course2->id, 'student');
     940          $this->setUser($user);
     941  
     942          $results = core_course_external::search_courses('search', 'FIRST');
     943          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
     944          $this->assertCount(1, $results['courses']);
     945          $this->assertEquals(1, $results['total']);
     946          $this->assertEquals($coursedata1['fullname'], $results['courses'][0]['fullname']);
     947  
     948          // Check that we can see all courses without the limit to enrolled setting.
     949          $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 0);
     950          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
     951          $this->assertCount(2, $results['courses']);
     952          $this->assertEquals(2, $results['total']);
     953  
     954          // Check that we only see our enrolled course when limiting.
     955          $results = core_course_external::search_courses('search', 'COURSE', 0, 0, array(), 1);
     956          $results = external_api::clean_returnvalue(core_course_external::search_courses_returns(), $results);
     957          $this->assertCount(1, $results['courses']);
     958          $this->assertEquals(1, $results['total']);
     959          $this->assertEquals($coursedata2['fullname'], $results['courses'][0]['fullname']);
     960  
     961          // Search by block (use news_items default block). Should fail (only admins allowed).
     962          $this->expectException('required_capability_exception');
     963          $results = core_course_external::search_courses('blocklist', $blockid);
     964      }
     965  
     966      /**
     967       * Test searching for courses returns custom field data
     968       */
     969      public function test_search_courses_customfields(): void {
     970          $this->resetAfterTest();
     971          $this->setAdminUser();
     972  
     973          $fieldcategory = $this->getDataGenerator()->create_custom_field_category([]);
     974          $datefield = $this->getDataGenerator()->create_custom_field([
     975              'categoryid' => $fieldcategory->get('id'),
     976              'shortname' => 'mydate',
     977              'name' => 'My date',
     978              'type' => 'date',
     979          ]);
     980  
     981          $newcourse = $this->getDataGenerator()->create_course(['customfields' => [
     982              [
     983                  'shortname' => $datefield->get('shortname'),
     984                  'value' => 1580389200, // 30/01/2020 13:00 GMT.
     985              ],
     986          ]]);
     987  
     988          $result = external_api::clean_returnvalue(
     989              core_course_external::search_courses_returns(),
     990              core_course_external::search_courses('search', $newcourse->shortname)
     991          );
     992  
     993          $this->assertCount(1, $result['courses']);
     994          $course = reset($result['courses']);
     995  
     996          $this->assertArrayHasKey('customfields', $course);
     997          $this->assertCount(1, $course['customfields']);
     998  
     999          // Assert the received custom field, "value" containing a human-readable version and "valueraw" the unmodified version.
    1000          $this->assertEquals([
    1001              'name' => $datefield->get('name'),
    1002              'shortname' => $datefield->get('shortname'),
    1003              'type' => $datefield->get('type'),
    1004              'value' => userdate(1580389200),
    1005              'valueraw' => 1580389200,
    1006          ], reset($course['customfields']));
    1007      }
    1008  
    1009      /**
    1010       * Create a course with contents
    1011       * @return array A list with the course object and course modules objects
    1012       */
    1013      private function prepare_get_course_contents_test() {
    1014          global $DB, $CFG;
    1015  
    1016          $CFG->allowstealth = 1; // Allow stealth activities.
    1017          $CFG->enablecompletion = true;
    1018          // Course with 4 sections (apart from the main section), with completion and not displaying hidden sections.
    1019          $course  = self::getDataGenerator()->create_course(['numsections' => 4, 'enablecompletion' => 1, 'hiddensections' => 1]);
    1020  
    1021          $forumdescription = 'This is the forum description';
    1022          $forum = $this->getDataGenerator()->create_module('forum',
    1023              array('course' => $course->id, 'intro' => $forumdescription, 'trackingtype' => 2),
    1024              array('showdescription' => true, 'completion' => COMPLETION_TRACKING_MANUAL));
    1025          $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
    1026          // Add discussions to the tracking forced forum.
    1027          $record = new stdClass();
    1028          $record->course = $course->id;
    1029          $record->userid = 0;
    1030          $record->forum = $forum->id;
    1031          $discussionforce = $this->getDataGenerator()->get_plugin_generator('mod_forum')->create_discussion($record);
    1032          $data = $this->getDataGenerator()->create_module('data',
    1033              array('assessed' => 1, 'scale' => 100, 'course' => $course->id, 'completion' => 2, 'completionentries' => 3));
    1034          $datacm = get_coursemodule_from_instance('data', $data->id);
    1035          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
    1036          $pagecm = get_coursemodule_from_instance('page', $page->id);
    1037          // This is an stealth page (set by visibleoncoursepage).
    1038          $pagestealth = $this->getDataGenerator()->create_module('page', array('course' => $course->id, 'visibleoncoursepage' => 0));
    1039          $labeldescription = 'This is a very long label to test if more than 50 characters are returned.
    1040                  So bla bla bla bla <b>bold bold bold</b> bla bla bla bla.';
    1041          $label = $this->getDataGenerator()->create_module('label', array('course' => $course->id,
    1042              'intro' => $labeldescription, 'completion' => COMPLETION_TRACKING_MANUAL));
    1043          $labelcm = get_coursemodule_from_instance('label', $label->id);
    1044          $tomorrow = time() + DAYSECS;
    1045          // Module with availability restrictions not met.
    1046          $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '},'
    1047                  .'{"type":"completion","cm":' . $label->cmid .',"e":1}],"showc":[true,true]}';
    1048          $url = $this->getDataGenerator()->create_module('url',
    1049              array('course' => $course->id, 'name' => 'URL: % & $ ../', 'section' => 2, 'display' => RESOURCELIB_DISPLAY_POPUP,
    1050                  'popupwidth' => 100, 'popupheight' => 100),
    1051              array('availability' => $availability));
    1052          $urlcm = get_coursemodule_from_instance('url', $url->id);
    1053          // Module for the last section.
    1054          $this->getDataGenerator()->create_module('url',
    1055              array('course' => $course->id, 'name' => 'URL for last section', 'section' => 3));
    1056          // Module for section 1 with availability restrictions met.
    1057          $yesterday = time() - DAYSECS;
    1058          $this->getDataGenerator()->create_module('url',
    1059              array('course' => $course->id, 'name' => 'URL restrictions met', 'section' => 1),
    1060              array('availability' => '{"op":"&","c":[{"type":"date","d":">=","t":'. $yesterday .'}],"showc":[true]}'));
    1061  
    1062          // Set the required capabilities by the external function.
    1063          $context = context_course::instance($course->id);
    1064          $roleid = $this->assignUserCapability('moodle/course:view', $context->id);
    1065          $this->assignUserCapability('moodle/course:update', $context->id, $roleid);
    1066          $this->assignUserCapability('mod/data:view', $context->id, $roleid);
    1067  
    1068          $conditions = array('course' => $course->id, 'section' => 2);
    1069          $DB->set_field('course_sections', 'summary', 'Text with iframe <iframe src="https://moodle.org"></iframe>', $conditions);
    1070  
    1071          // Add date availability condition not met for section 3.
    1072          $availability = '{"op":"&","c":[{"type":"date","d":">=","t":' . $tomorrow . '}],"showc":[true]}';
    1073          $DB->set_field('course_sections', 'availability', $availability,
    1074                  array('course' => $course->id, 'section' => 3));
    1075  
    1076          // Create resource for last section.
    1077          $pageinhiddensection = $this->getDataGenerator()->create_module('page',
    1078              array('course' => $course->id, 'name' => 'Page in hidden section', 'section' => 4));
    1079          // Set not visible last section.
    1080          $DB->set_field('course_sections', 'visible', 0,
    1081                  array('course' => $course->id, 'section' => 4));
    1082  
    1083          $forumcompleteauto = $this->getDataGenerator()->create_module('forum',
    1084              array('course' => $course->id, 'intro' => 'forum completion tracking auto', 'trackingtype' => 2),
    1085              array('showdescription' => true, 'completionview' => 1, 'completion' => COMPLETION_TRACKING_AUTOMATIC));
    1086          $forumcompleteautocm = get_coursemodule_from_id('forum', $forumcompleteauto->cmid);
    1087  
    1088          rebuild_course_cache($course->id, true);
    1089  
    1090          return array($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm);
    1091      }
    1092  
    1093      /**
    1094       * Test get_course_contents
    1095       */
    1096      public function test_get_course_contents() {
    1097          global $CFG;
    1098          $this->resetAfterTest(true);
    1099  
    1100          $CFG->forum_allowforcedreadtracking = 1;
    1101          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1102  
    1103          // We first run the test as admin.
    1104          $this->setAdminUser();
    1105          $sections = core_course_external::get_course_contents($course->id, array());
    1106          // We need to execute the return values cleaning process to simulate the web service server.
    1107          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1108  
    1109          $modinfo = get_fast_modinfo($course);
    1110          $testexecuted = 0;
    1111          foreach ($sections[0]['modules'] as $module) {
    1112              if ($module['id'] == $forumcm->id and $module['modname'] == 'forum') {
    1113                  $cm = $modinfo->cms[$forumcm->id];
    1114                  $formattedtext = format_text($cm->content, FORMAT_HTML,
    1115                      array('noclean' => true, 'para' => false, 'filter' => false));
    1116                  $this->assertEquals($formattedtext, $module['description']);
    1117                  $this->assertEquals($forumcm->instance, $module['instance']);
    1118                  $this->assertEquals(context_module::instance($forumcm->id)->id, $module['contextid']);
    1119                  $this->assertStringContainsString('1 unread post', $module['afterlink']);
    1120                  $this->assertFalse($module['noviewlink']);
    1121                  $this->assertNotEmpty($module['description']);  // Module showdescription is on.
    1122                  $testexecuted = $testexecuted + 2;
    1123              } else if ($module['id'] == $labelcm->id and $module['modname'] == 'label') {
    1124                  $cm = $modinfo->cms[$labelcm->id];
    1125                  $formattedtext = format_text($cm->content, FORMAT_HTML,
    1126                      array('noclean' => true, 'para' => false, 'filter' => false));
    1127                  $this->assertEquals($formattedtext, $module['description']);
    1128                  $this->assertEquals($labelcm->instance, $module['instance']);
    1129                  $this->assertEquals(context_module::instance($labelcm->id)->id, $module['contextid']);
    1130                  $this->assertTrue($module['noviewlink']);
    1131                  $this->assertNotEmpty($module['description']);  // Label always prints the description.
    1132                  $testexecuted = $testexecuted + 1;
    1133              } else if ($module['id'] == $datacm->id and $module['modname'] == 'data') {
    1134                  $this->assertStringContainsString('customcompletionrules', $module['customdata']);
    1135                  $this->assertFalse($module['noviewlink']);
    1136                  $this->assertArrayNotHasKey('description', $module);
    1137                  $testexecuted = $testexecuted + 1;
    1138              }
    1139          }
    1140          foreach ($sections[2]['modules'] as $module) {
    1141              if ($module['id'] == $urlcm->id and $module['modname'] == 'url') {
    1142                  $this->assertStringContainsString('width=100,height=100', $module['onclick']);
    1143                  $testexecuted = $testexecuted + 1;
    1144              }
    1145          }
    1146  
    1147          $CFG->forum_allowforcedreadtracking = 0;    // Recover original value.
    1148          forum_tp_count_forum_unread_posts($forumcm, $course, true);    // Reset static cache for further tests.
    1149  
    1150          $this->assertEquals(5, $testexecuted);
    1151          $this->assertEquals(0, $sections[0]['section']);
    1152  
    1153          $this->assertCount(6, $sections[0]['modules']);
    1154          $this->assertCount(1, $sections[1]['modules']);
    1155          $this->assertCount(1, $sections[2]['modules']);
    1156          $this->assertCount(1, $sections[3]['modules']); // One module for the section with availability restrictions.
    1157          $this->assertCount(1, $sections[4]['modules']); // One module for the hidden section with a visible activity.
    1158          $this->assertNotEmpty($sections[3]['availabilityinfo']);
    1159          $this->assertEquals(1, $sections[1]['section']);
    1160          $this->assertEquals(2, $sections[2]['section']);
    1161          $this->assertEquals(3, $sections[3]['section']);
    1162          $this->assertEquals(4, $sections[4]['section']);
    1163          $this->assertStringContainsString('<iframe', $sections[2]['summary']);
    1164          $this->assertStringContainsString('</iframe>', $sections[2]['summary']);
    1165          $this->assertNotEmpty($sections[2]['modules'][0]['availabilityinfo']);
    1166          try {
    1167              $sections = core_course_external::get_course_contents($course->id,
    1168                                                                      array(array("name" => "invalid", "value" => 1)));
    1169              $this->fail('Exception expected due to invalid option.');
    1170          } catch (moodle_exception $e) {
    1171              $this->assertEquals('errorinvalidparam', $e->errorcode);
    1172          }
    1173      }
    1174  
    1175  
    1176      /**
    1177       * Test get_course_contents as student
    1178       */
    1179      public function test_get_course_contents_student() {
    1180          global $DB;
    1181          $this->resetAfterTest(true);
    1182  
    1183          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1184  
    1185          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
    1186          $user = self::getDataGenerator()->create_user();
    1187          self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
    1188          $this->setUser($user);
    1189  
    1190          $sections = core_course_external::get_course_contents($course->id, array());
    1191          // We need to execute the return values cleaning process to simulate the web service server.
    1192          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1193  
    1194          $this->assertCount(4, $sections); // Nothing for the not visible section.
    1195          $this->assertCount(6, $sections[0]['modules']);
    1196          $this->assertCount(1, $sections[1]['modules']);
    1197          $this->assertCount(1, $sections[2]['modules']);
    1198          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
    1199  
    1200          $this->assertNotEmpty($sections[3]['availabilityinfo']);
    1201          $this->assertEquals(1, $sections[1]['section']);
    1202          $this->assertEquals(2, $sections[2]['section']);
    1203          $this->assertEquals(3, $sections[3]['section']);
    1204          // The module with the availability restriction met is returning contents.
    1205          $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
    1206          // The module with the availability restriction not met is not returning contents.
    1207          $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
    1208  
    1209          // Now include flag for returning stealth information (fake section).
    1210          $sections = core_course_external::get_course_contents($course->id,
    1211              array(array("name" => "includestealthmodules", "value" => 1)));
    1212          // We need to execute the return values cleaning process to simulate the web service server.
    1213          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1214  
    1215          $this->assertCount(5, $sections); // Include fake section with stealth activities.
    1216          $this->assertCount(6, $sections[0]['modules']);
    1217          $this->assertCount(1, $sections[1]['modules']);
    1218          $this->assertCount(1, $sections[2]['modules']);
    1219          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
    1220          $this->assertCount(1, $sections[4]['modules']); // One stealth module.
    1221          $this->assertEquals(-1, $sections[4]['id']);
    1222      }
    1223  
    1224      /**
    1225       * Test get_course_contents excluding modules
    1226       */
    1227      public function test_get_course_contents_excluding_modules() {
    1228          $this->resetAfterTest(true);
    1229  
    1230          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1231  
    1232          // Test exclude modules.
    1233          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludemodules", "value" => 1)));
    1234  
    1235          // We need to execute the return values cleaning process to simulate the web service server.
    1236          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1237  
    1238          $this->assertEmpty($sections[0]['modules']);
    1239          $this->assertEmpty($sections[1]['modules']);
    1240      }
    1241  
    1242      /**
    1243       * Test get_course_contents excluding contents
    1244       */
    1245      public function test_get_course_contents_excluding_contents() {
    1246          $this->resetAfterTest(true);
    1247  
    1248          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1249  
    1250          // Test exclude modules.
    1251          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "excludecontents", "value" => 1)));
    1252  
    1253          // We need to execute the return values cleaning process to simulate the web service server.
    1254          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1255  
    1256          foreach ($sections as $section) {
    1257              foreach ($section['modules'] as $module) {
    1258                  // Only resources return contents.
    1259                  if (isset($module['contents'])) {
    1260                      $this->assertEmpty($module['contents']);
    1261                  }
    1262              }
    1263          }
    1264      }
    1265  
    1266      /**
    1267       * Test get_course_contents filtering by section number
    1268       */
    1269      public function test_get_course_contents_section_number() {
    1270          $this->resetAfterTest(true);
    1271  
    1272          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1273  
    1274          // Test exclude modules.
    1275          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "sectionnumber", "value" => 0)));
    1276  
    1277          // We need to execute the return values cleaning process to simulate the web service server.
    1278          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1279  
    1280          $this->assertCount(1, $sections);
    1281          $this->assertCount(6, $sections[0]['modules']);
    1282      }
    1283  
    1284      /**
    1285       * Test get_course_contents filtering by cmid
    1286       */
    1287      public function test_get_course_contents_cmid() {
    1288          $this->resetAfterTest(true);
    1289  
    1290          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1291  
    1292          // Test exclude modules.
    1293          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "cmid", "value" => $forumcm->id)));
    1294  
    1295          // We need to execute the return values cleaning process to simulate the web service server.
    1296          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1297  
    1298          $this->assertCount(4, $sections);
    1299          $this->assertCount(1, $sections[0]['modules']);
    1300          $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
    1301      }
    1302  
    1303  
    1304      /**
    1305       * Test get_course_contents filtering by cmid and section
    1306       */
    1307      public function test_get_course_contents_section_cmid() {
    1308          $this->resetAfterTest(true);
    1309  
    1310          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1311  
    1312          // Test exclude modules.
    1313          $sections = core_course_external::get_course_contents($course->id, array(
    1314                                                                          array("name" => "cmid", "value" => $forumcm->id),
    1315                                                                          array("name" => "sectionnumber", "value" => 0)
    1316                                                                          ));
    1317  
    1318          // We need to execute the return values cleaning process to simulate the web service server.
    1319          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1320  
    1321          $this->assertCount(1, $sections);
    1322          $this->assertCount(1, $sections[0]['modules']);
    1323          $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
    1324      }
    1325  
    1326      /**
    1327       * Test get_course_contents filtering by modname
    1328       */
    1329      public function test_get_course_contents_modname() {
    1330          $this->resetAfterTest(true);
    1331  
    1332          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1333  
    1334          // Test exclude modules.
    1335          $sections = core_course_external::get_course_contents($course->id, array(array("name" => "modname", "value" => "forum")));
    1336  
    1337          // We need to execute the return values cleaning process to simulate the web service server.
    1338          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1339  
    1340          $this->assertCount(4, $sections);
    1341          $this->assertCount(2, $sections[0]['modules']);
    1342          $this->assertEquals($forumcm->id, $sections[0]['modules'][0]["id"]);
    1343      }
    1344  
    1345      /**
    1346       * Test get_course_contents filtering by modname
    1347       */
    1348      public function test_get_course_contents_modid() {
    1349          $this->resetAfterTest(true);
    1350  
    1351          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1352  
    1353          // Test exclude modules.
    1354          $sections = core_course_external::get_course_contents($course->id, array(
    1355                                                                              array("name" => "modname", "value" => "page"),
    1356                                                                              array("name" => "modid", "value" => $pagecm->instance),
    1357                                                                              ));
    1358  
    1359          // We need to execute the return values cleaning process to simulate the web service server.
    1360          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1361  
    1362          $this->assertCount(4, $sections);
    1363          $this->assertCount(1, $sections[0]['modules']);
    1364          $this->assertEquals("page", $sections[0]['modules'][0]["modname"]);
    1365          $this->assertEquals($pagecm->instance, $sections[0]['modules'][0]["instance"]);
    1366      }
    1367  
    1368      /**
    1369       * Test get course contents completion manual
    1370       */
    1371      public function test_get_course_contents_completion_manual() {
    1372          global $CFG;
    1373          $this->resetAfterTest(true);
    1374  
    1375          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
    1376              $this->prepare_get_course_contents_test();
    1377          availability_completion\condition::wipe_static_cache();
    1378  
    1379          // Test activity not completed yet.
    1380          $result = core_course_external::get_course_contents($course->id, array(
    1381              array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
    1382          // We need to execute the return values cleaning process to simulate the web service server.
    1383          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
    1384  
    1385          $completiondata = $result[0]['modules'][0]["completiondata"];
    1386          $this->assertCount(1, $result[0]['modules']);
    1387          $this->assertEquals("forum", $result[0]['modules'][0]["modname"]);
    1388          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
    1389          $this->assertEquals(0, $completiondata['state']);
    1390          $this->assertEquals(0, $completiondata['timecompleted']);
    1391          $this->assertEmpty($completiondata['overrideby']);
    1392          $this->assertFalse($completiondata['valueused']);
    1393          $this->assertTrue($completiondata['hascompletion']);
    1394          $this->assertFalse($completiondata['isautomatic']);
    1395          $this->assertFalse($completiondata['istrackeduser']);
    1396          $this->assertTrue($completiondata['uservisible']);
    1397  
    1398          // Set activity completed.
    1399          core_completion_external::update_activity_completion_status_manually($forumcm->id, true);
    1400  
    1401          $result = core_course_external::get_course_contents($course->id, array(
    1402              array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
    1403          // We need to execute the return values cleaning process to simulate the web service server.
    1404          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
    1405  
    1406          $this->assertEquals(COMPLETION_COMPLETE, $result[0]['modules'][0]["completiondata"]['state']);
    1407          $this->assertNotEmpty($result[0]['modules'][0]["completiondata"]['timecompleted']);
    1408          $this->assertEmpty($result[0]['modules'][0]["completiondata"]['overrideby']);
    1409  
    1410          // Test activity with completion value that is used in an availability condition.
    1411          $result = core_course_external::get_course_contents($course->id, array(
    1412                  array("name" => "modname", "value" => "label"), array("name" => "modid", "value" => $labelcm->instance)));
    1413          // We need to execute the return values cleaning process to simulate the web service server.
    1414          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
    1415  
    1416          $completiondata = $result[0]['modules'][0]["completiondata"];
    1417          $this->assertCount(1, $result[0]['modules']);
    1418          $this->assertEquals("label", $result[0]['modules'][0]["modname"]);
    1419          $this->assertEquals(COMPLETION_TRACKING_MANUAL, $result[0]['modules'][0]["completion"]);
    1420          $this->assertEquals(0, $completiondata['state']);
    1421          $this->assertEquals(0, $completiondata['timecompleted']);
    1422          $this->assertEmpty($completiondata['overrideby']);
    1423          $this->assertTrue($completiondata['valueused']);
    1424          $this->assertTrue($completiondata['hascompletion']);
    1425          $this->assertFalse($completiondata['isautomatic']);
    1426          $this->assertFalse($completiondata['istrackeduser']);
    1427          $this->assertTrue($completiondata['uservisible']);
    1428  
    1429          // Disable completion.
    1430          $CFG->enablecompletion = 0;
    1431          $result = core_course_external::get_course_contents($course->id, array(
    1432              array("name" => "modname", "value" => "forum"), array("name" => "modid", "value" => $forumcm->instance)));
    1433          // We need to execute the return values cleaning process to simulate the web service server.
    1434          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
    1435  
    1436          $this->assertArrayNotHasKey('completiondata', $result[0]['modules'][0]);
    1437      }
    1438  
    1439      /**
    1440       * Test get course contents completion auto
    1441       */
    1442      public function test_get_course_contents_completion_auto() {
    1443          global $CFG;
    1444          $this->resetAfterTest(true);
    1445  
    1446          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm, $forumcompleteautocm) =
    1447              $this->prepare_get_course_contents_test();
    1448          availability_completion\condition::wipe_static_cache();
    1449  
    1450          // Test activity not completed yet.
    1451          $result = core_course_external::get_course_contents($course->id, [
    1452              [
    1453                  "name" => "modname",
    1454                  "value" => "forum"
    1455              ],
    1456              [
    1457                  "name" => "modid",
    1458                  "value" => $forumcompleteautocm->instance
    1459              ]
    1460          ]);
    1461          // We need to execute the return values cleaning process to simulate the web service server.
    1462          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
    1463  
    1464          $forummod = $result[0]['modules'][0];
    1465          $completiondata = $forummod["completiondata"];
    1466          $this->assertCount(1, $result[0]['modules']);
    1467          $this->assertEquals("forum", $forummod["modname"]);
    1468          $this->assertEquals(COMPLETION_TRACKING_AUTOMATIC, $forummod["completion"]);
    1469          $this->assertEquals(0, $completiondata['state']);
    1470          $this->assertEquals(0, $completiondata['timecompleted']);
    1471          $this->assertEmpty($completiondata['overrideby']);
    1472          $this->assertFalse($completiondata['valueused']);
    1473          $this->assertTrue($completiondata['hascompletion']);
    1474          $this->assertTrue($completiondata['isautomatic']);
    1475          $this->assertFalse($completiondata['istrackeduser']);
    1476          $this->assertTrue($completiondata['uservisible']);
    1477          $this->assertCount(1, $completiondata['details']);
    1478      }
    1479  
    1480      /**
    1481       * Test mimetype is returned for resources with showtype set.
    1482       */
    1483      public function test_get_course_contents_including_mimetype() {
    1484          $this->resetAfterTest(true);
    1485  
    1486          $this->setAdminUser();
    1487          $course = self::getDataGenerator()->create_course();
    1488  
    1489          $record = new stdClass();
    1490          $record->course = $course->id;
    1491          $record->showtype = 1;
    1492          $resource = self::getDataGenerator()->create_module('resource', $record);
    1493  
    1494          $result = core_course_external::get_course_contents($course->id);
    1495          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
    1496          $this->assertCount(1, $result[0]['modules']);   // One module, first section.
    1497          $customdata = json_decode($result[0]['modules'][0]['customdata']);
    1498          $displayoptions = unserialize($customdata->displayoptions);
    1499          $this->assertEquals('text/plain', $displayoptions['filedetails']['mimetype']);
    1500      }
    1501  
    1502      /**
    1503       * Test contents info is returned.
    1504       */
    1505      public function test_get_course_contents_contentsinfo() {
    1506          global $USER;
    1507  
    1508          $this->resetAfterTest(true);
    1509          $this->setAdminUser();
    1510          $timenow = time();
    1511  
    1512          $course = self::getDataGenerator()->create_course();
    1513  
    1514          $record = new stdClass();
    1515          $record->course = $course->id;
    1516          // One resource with one file.
    1517          $resource1 = self::getDataGenerator()->create_module('resource', $record);
    1518  
    1519          // More type of files.
    1520          $record->files = file_get_unused_draft_itemid();
    1521          $usercontext = context_user::instance($USER->id);
    1522          $extensions = array('txt', 'png', 'pdf');
    1523          $fs = get_file_storage();
    1524          foreach ($extensions as $key => $extension) {
    1525              // Add actual file there.
    1526              $filerecord = array('component' => 'user', 'filearea' => 'draft',
    1527                      'contextid' => $usercontext->id, 'itemid' => $record->files,
    1528                      'filename' => 'resource' . $key . '.' . $extension, 'filepath' => '/');
    1529              $fs->create_file_from_string($filerecord, 'Test resource ' . $key . ' file');
    1530          }
    1531  
    1532          // Create file reference.
    1533          $repos = repository::get_instances(array('type' => 'user'));
    1534          $userrepository = reset($repos);
    1535  
    1536          // Create a user private file.
    1537          $userfilerecord = new stdClass;
    1538          $userfilerecord->contextid = $usercontext->id;
    1539          $userfilerecord->component = 'user';
    1540          $userfilerecord->filearea  = 'private';
    1541          $userfilerecord->itemid    = 0;
    1542          $userfilerecord->filepath  = '/';
    1543          $userfilerecord->filename  = 'userfile.txt';
    1544          $userfilerecord->source    = 'test';
    1545          $userfile = $fs->create_file_from_string($userfilerecord, 'User file content');
    1546          $userfileref = $fs->pack_reference($userfilerecord);
    1547  
    1548          // Clone latest "normal" file.
    1549          $filerefrecord = clone (object) $filerecord;
    1550          $filerefrecord->filename = 'testref.txt';
    1551          $fileref = $fs->create_file_from_reference($filerefrecord, $userrepository->id, $userfileref);
    1552          // Set main file pointing to the file reference.
    1553          file_set_sortorder($usercontext->id, 'user', 'draft', $record->files, $filerefrecord->filepath,
    1554              $filerefrecord->filename, 1);
    1555  
    1556          // Once the reference has been created, create the file resource.
    1557          $resource2 = self::getDataGenerator()->create_module('resource', $record);
    1558  
    1559          $result = core_course_external::get_course_contents($course->id);
    1560          $result = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $result);
    1561          $this->assertCount(2, $result[0]['modules']);
    1562          foreach ($result[0]['modules'] as $module) {
    1563              if ($module['instance'] == $resource1->id) {
    1564                  $this->assertEquals(1, $module['contentsinfo']['filescount']);
    1565                  $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
    1566                  $this->assertEquals($module['contents'][0]['filesize'], $module['contentsinfo']['filessize']);
    1567                  $this->assertEquals(array('text/plain'), $module['contentsinfo']['mimetypes']);
    1568              } else {
    1569                  $this->assertEquals(count($extensions) + 1, $module['contentsinfo']['filescount']);
    1570                  $filessize = $module['contents'][0]['filesize'] + $module['contents'][1]['filesize'] +
    1571                      $module['contents'][2]['filesize'] + $module['contents'][3]['filesize'];
    1572                  $this->assertEquals($filessize, $module['contentsinfo']['filessize']);
    1573                  $this->assertEquals('user', $module['contentsinfo']['repositorytype']);
    1574                  $this->assertGreaterThanOrEqual($timenow, $module['contentsinfo']['lastmodified']);
    1575                  $this->assertEquals(array('text/plain', 'image/png', 'application/pdf'), $module['contentsinfo']['mimetypes']);
    1576              }
    1577          }
    1578      }
    1579  
    1580      /**
    1581       * Test get_course_contents when hidden sections are displayed.
    1582       */
    1583      public function test_get_course_contents_hiddensections() {
    1584          global $DB;
    1585          $this->resetAfterTest(true);
    1586  
    1587          list($course, $forumcm, $datacm, $pagecm, $labelcm, $urlcm) = $this->prepare_get_course_contents_test();
    1588          // Force returning hidden sections.
    1589          $course->hiddensections = 0;
    1590          update_course($course);
    1591  
    1592          $studentroleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
    1593          $user = self::getDataGenerator()->create_user();
    1594          self::getDataGenerator()->enrol_user($user->id, $course->id, $studentroleid);
    1595          $this->setUser($user);
    1596  
    1597          $sections = core_course_external::get_course_contents($course->id, array());
    1598          // We need to execute the return values cleaning process to simulate the web service server.
    1599          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1600  
    1601          $this->assertCount(5, $sections); // All the sections, including the "not visible" one.
    1602          $this->assertCount(6, $sections[0]['modules']);
    1603          $this->assertCount(1, $sections[1]['modules']);
    1604          $this->assertCount(1, $sections[2]['modules']);
    1605          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
    1606          $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
    1607  
    1608          $this->assertNotEmpty($sections[3]['availabilityinfo']);
    1609          $this->assertEquals(1, $sections[1]['section']);
    1610          $this->assertEquals(2, $sections[2]['section']);
    1611          $this->assertEquals(3, $sections[3]['section']);
    1612          // The module with the availability restriction met is returning contents.
    1613          $this->assertNotEmpty($sections[1]['modules'][0]['contents']);
    1614          // The module with the availability restriction not met is not returning contents.
    1615          $this->assertArrayNotHasKey('contents', $sections[2]['modules'][0]);
    1616  
    1617          // Now include flag for returning stealth information (fake section).
    1618          $sections = core_course_external::get_course_contents($course->id,
    1619              array(array("name" => "includestealthmodules", "value" => 1)));
    1620          // We need to execute the return values cleaning process to simulate the web service server.
    1621          $sections = external_api::clean_returnvalue(core_course_external::get_course_contents_returns(), $sections);
    1622  
    1623          $this->assertCount(6, $sections); // Include fake section with stealth activities.
    1624          $this->assertCount(6, $sections[0]['modules']);
    1625          $this->assertCount(1, $sections[1]['modules']);
    1626          $this->assertCount(1, $sections[2]['modules']);
    1627          $this->assertCount(0, $sections[3]['modules']); // No modules for the section with availability restrictions.
    1628          $this->assertCount(0, $sections[4]['modules']); // No modules for the section hidden.
    1629          $this->assertCount(1, $sections[5]['modules']); // One stealth module.
    1630          $this->assertEquals(-1, $sections[5]['id']);
    1631      }
    1632  
    1633      /**
    1634       * Test duplicate_course
    1635       */
    1636      public function test_duplicate_course() {
    1637          $this->resetAfterTest(true);
    1638  
    1639          // Create one course with three modules.
    1640          $course  = self::getDataGenerator()->create_course();
    1641          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
    1642          $forumcm = get_coursemodule_from_id('forum', $forum->cmid);
    1643          $forumcontext = context_module::instance($forum->cmid);
    1644          $data = $this->getDataGenerator()->create_module('data', array('assessed'=>1, 'scale'=>100, 'course'=>$course->id));
    1645          $datacontext = context_module::instance($data->cmid);
    1646          $datacm = get_coursemodule_from_instance('page', $data->id);
    1647          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course->id));
    1648          $pagecontext = context_module::instance($page->cmid);
    1649          $pagecm = get_coursemodule_from_instance('page', $page->id);
    1650  
    1651          // Set the required capabilities by the external function.
    1652          $coursecontext = context_course::instance($course->id);
    1653          $categorycontext = context_coursecat::instance($course->category);
    1654          $roleid = $this->assignUserCapability('moodle/course:create', $categorycontext->id);
    1655          $this->assignUserCapability('moodle/course:view', $categorycontext->id, $roleid);
    1656          $this->assignUserCapability('moodle/restore:restorecourse', $categorycontext->id, $roleid);
    1657          $this->assignUserCapability('moodle/backup:backupcourse', $coursecontext->id, $roleid);
    1658          $this->assignUserCapability('moodle/backup:configure', $coursecontext->id, $roleid);
    1659          // Optional capabilities to copy user data.
    1660          $this->assignUserCapability('moodle/backup:userinfo', $coursecontext->id, $roleid);
    1661          $this->assignUserCapability('moodle/restore:userinfo', $categorycontext->id, $roleid);
    1662  
    1663          $newcourse['fullname'] = 'Course duplicate';
    1664          $newcourse['shortname'] = 'courseduplicate';
    1665          $newcourse['categoryid'] = $course->category;
    1666          $newcourse['visible'] = true;
    1667          $newcourse['options'][] = array('name' => 'users', 'value' => true);
    1668  
    1669          $duplicate = core_course_external::duplicate_course($course->id, $newcourse['fullname'],
    1670                  $newcourse['shortname'], $newcourse['categoryid'], $newcourse['visible'], $newcourse['options']);
    1671  
    1672          // We need to execute the return values cleaning process to simulate the web service server.
    1673          $duplicate = external_api::clean_returnvalue(core_course_external::duplicate_course_returns(), $duplicate);
    1674  
    1675          // Check that the course has been duplicated.
    1676          $this->assertEquals($newcourse['shortname'], $duplicate['shortname']);
    1677      }
    1678  
    1679      /**
    1680       * Test update_courses
    1681       */
    1682      public function test_update_courses() {
    1683          global $DB, $CFG, $USER, $COURSE;
    1684  
    1685          // Get current $COURSE to be able to restore it later (defaults to $SITE). We need this
    1686          // trick because we are both updating and getting (for testing) course information
    1687          // in the same request and core_course_external::update_courses()
    1688          // is overwriting $COURSE all over the time with OLD values, so later
    1689          // use of get_course() fetches those OLD values instead of the updated ones.
    1690          // See MDL-39723 for more info.
    1691          $origcourse = clone($COURSE);
    1692  
    1693          $this->resetAfterTest(true);
    1694  
    1695          // Set the required capabilities by the external function.
    1696          $contextid = context_system::instance()->id;
    1697          $roleid = $this->assignUserCapability('moodle/course:update', $contextid);
    1698          $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
    1699          $this->assignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
    1700          $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
    1701          $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
    1702          $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
    1703          $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
    1704          $this->assignUserCapability('moodle/course:visibility', $contextid, $roleid);
    1705          $this->assignUserCapability('moodle/course:viewhiddencourses', $contextid, $roleid);
    1706          $this->assignUserCapability('moodle/course:setforcedlanguage', $contextid, $roleid);
    1707  
    1708          // Create category and courses.
    1709          $category1  = self::getDataGenerator()->create_category();
    1710          $category2  = self::getDataGenerator()->create_category();
    1711  
    1712          $originalcourse1 = self::getDataGenerator()->create_course();
    1713          self::getDataGenerator()->enrol_user($USER->id, $originalcourse1->id, $roleid);
    1714  
    1715          $originalcourse2 = self::getDataGenerator()->create_course();
    1716          self::getDataGenerator()->enrol_user($USER->id, $originalcourse2->id, $roleid);
    1717  
    1718          // Course with custom fields.
    1719          $fieldcategory = self::getDataGenerator()->create_custom_field_category(['name' => 'Other fields']);
    1720          $customfield = ['shortname' => 'test', 'name' => 'Custom field', 'type' => 'text',
    1721              'categoryid' => $fieldcategory->get('id'),
    1722              'configdata' => ['visibility' => \core_course\customfield\course_handler::VISIBLETOALL, 'locked' => 1]];
    1723          $field = self::getDataGenerator()->create_custom_field($customfield);
    1724  
    1725          $originalcourse3 = self::getDataGenerator()->create_course(['customfield_test' => 'Test value']);
    1726          self::getDataGenerator()->enrol_user($USER->id, $originalcourse3->id, $roleid);
    1727  
    1728          // Course values to be updated.
    1729          $course1['id'] = $originalcourse1->id;
    1730          $course1['fullname'] = 'Updated test course 1';
    1731          $course1['shortname'] = 'Udestedtestcourse1';
    1732          $course1['categoryid'] = $category1->id;
    1733  
    1734          $course2['id'] = $originalcourse2->id;
    1735          $course2['fullname'] = 'Updated test course 2';
    1736          $course2['shortname'] = 'Updestedtestcourse2';
    1737          $course2['categoryid'] = $category2->id;
    1738          $course2['idnumber'] = 'Updatedidnumber2';
    1739          $course2['summary'] = 'Updaated description for course 2';
    1740          $course2['summaryformat'] = FORMAT_HTML;
    1741          $course2['format'] = 'topics';
    1742          $course2['showgrades'] = 1;
    1743          $course2['newsitems'] = 3;
    1744          $course2['startdate'] = 1420092000; // 01/01/2015.
    1745          $course2['enddate'] = 1422669600; // 01/31/2015.
    1746          $course2['maxbytes'] = 100000;
    1747          $course2['showreports'] = 1;
    1748          $course2['visible'] = 0;
    1749          $course2['hiddensections'] = 0;
    1750          $course2['groupmode'] = 0;
    1751          $course2['groupmodeforce'] = 0;
    1752          $course2['defaultgroupingid'] = 0;
    1753          $course2['enablecompletion'] = 1;
    1754          $course2['lang'] = 'en';
    1755          $course2['forcetheme'] = 'classic';
    1756  
    1757          $course3['id'] = $originalcourse3->id;
    1758          $updatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'Updated test value'];
    1759          $course3['customfields'] = [$updatedcustomfieldvalue];
    1760          $courses = array($course1, $course2, $course3);
    1761  
    1762          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1763          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1764                  $updatedcoursewarnings);
    1765          $COURSE = $origcourse; // Restore $COURSE. Instead of using the OLD one set by the previous line.
    1766  
    1767          // Check that right number of courses were created.
    1768          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
    1769  
    1770          // Check that the courses were correctly created.
    1771          foreach ($courses as $course) {
    1772              $courseinfo = course_get_format($course['id'])->get_course();
    1773              $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course['id']);
    1774              if ($course['id'] == $course2['id']) {
    1775                  $this->assertEquals($course2['fullname'], $courseinfo->fullname);
    1776                  $this->assertEquals($course2['shortname'], $courseinfo->shortname);
    1777                  $this->assertEquals($course2['categoryid'], $courseinfo->category);
    1778                  $this->assertEquals($course2['idnumber'], $courseinfo->idnumber);
    1779                  $this->assertEquals($course2['summary'], $courseinfo->summary);
    1780                  $this->assertEquals($course2['summaryformat'], $courseinfo->summaryformat);
    1781                  $this->assertEquals($course2['format'], $courseinfo->format);
    1782                  $this->assertEquals($course2['showgrades'], $courseinfo->showgrades);
    1783                  $this->assertEquals($course2['newsitems'], $courseinfo->newsitems);
    1784                  $this->assertEquals($course2['startdate'], $courseinfo->startdate);
    1785                  $this->assertEquals($course2['enddate'], $courseinfo->enddate);
    1786                  $this->assertEquals($course2['maxbytes'], $courseinfo->maxbytes);
    1787                  $this->assertEquals($course2['showreports'], $courseinfo->showreports);
    1788                  $this->assertEquals($course2['visible'], $courseinfo->visible);
    1789                  $this->assertEquals($course2['hiddensections'], $courseinfo->hiddensections);
    1790                  $this->assertEquals($course2['groupmode'], $courseinfo->groupmode);
    1791                  $this->assertEquals($course2['groupmodeforce'], $courseinfo->groupmodeforce);
    1792                  $this->assertEquals($course2['defaultgroupingid'], $courseinfo->defaultgroupingid);
    1793                  $this->assertEquals($course2['lang'], $courseinfo->lang);
    1794  
    1795                  if (!empty($CFG->allowcoursethemes)) {
    1796                      $this->assertEquals($course2['forcetheme'], $courseinfo->theme);
    1797                  }
    1798  
    1799                  $this->assertEquals($course2['enablecompletion'], $courseinfo->enablecompletion);
    1800                  $this->assertEquals(['test' => null], (array)$customfields);
    1801              } else if ($course['id'] == $course1['id']) {
    1802                  $this->assertEquals($course1['fullname'], $courseinfo->fullname);
    1803                  $this->assertEquals($course1['shortname'], $courseinfo->shortname);
    1804                  $this->assertEquals($course1['categoryid'], $courseinfo->category);
    1805                  $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
    1806                  $this->assertEquals('topics', $courseinfo->format);
    1807                  $this->assertEquals(5, course_get_format($course['id'])->get_last_section_number());
    1808                  $this->assertEquals(0, $courseinfo->newsitems);
    1809                  $this->assertEquals(FORMAT_MOODLE, $courseinfo->summaryformat);
    1810                  $this->assertEquals(['test' => null], (array)$customfields);
    1811              } else if ($course['id'] == $course3['id']) {
    1812                  $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields);
    1813              } else {
    1814                  throw new moodle_exception('Unexpected shortname');
    1815              }
    1816          }
    1817  
    1818          $courses = array($course1);
    1819          // Try update course without update capability.
    1820          $user = self::getDataGenerator()->create_user();
    1821          $this->setUser($user);
    1822          $this->unassignUserCapability('moodle/course:update', $contextid, $roleid);
    1823          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
    1824          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1825          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1826                                                                      $updatedcoursewarnings);
    1827          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
    1828  
    1829          // Try update course category without capability.
    1830          $this->assignUserCapability('moodle/course:update', $contextid, $roleid);
    1831          $this->unassignUserCapability('moodle/course:changecategory', $contextid, $roleid);
    1832          $user = self::getDataGenerator()->create_user();
    1833          $this->setUser($user);
    1834          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
    1835          $course1['categoryid'] = $category2->id;
    1836          $courses = array($course1);
    1837          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1838          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1839                                                                      $updatedcoursewarnings);
    1840          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
    1841  
    1842          // Try update course fullname without capability.
    1843          $this->assignUserCapability('moodle/course:changecategory', $contextid, $roleid);
    1844          $this->unassignUserCapability('moodle/course:changefullname', $contextid, $roleid);
    1845          $user = self::getDataGenerator()->create_user();
    1846          $this->setUser($user);
    1847          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
    1848          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1849          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1850                                                                      $updatedcoursewarnings);
    1851          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
    1852          $course1['fullname'] = 'Testing fullname without permission';
    1853          $courses = array($course1);
    1854          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1855          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1856                                                                      $updatedcoursewarnings);
    1857          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
    1858  
    1859          // Try update course shortname without capability.
    1860          $this->assignUserCapability('moodle/course:changefullname', $contextid, $roleid);
    1861          $this->unassignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
    1862          $user = self::getDataGenerator()->create_user();
    1863          $this->setUser($user);
    1864          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
    1865          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1866          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1867                                                                      $updatedcoursewarnings);
    1868          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
    1869          $course1['shortname'] = 'Testing shortname without permission';
    1870          $courses = array($course1);
    1871          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1872          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1873                                                                      $updatedcoursewarnings);
    1874          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
    1875  
    1876          // Try update course idnumber without capability.
    1877          $this->assignUserCapability('moodle/course:changeshortname', $contextid, $roleid);
    1878          $this->unassignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
    1879          $user = self::getDataGenerator()->create_user();
    1880          $this->setUser($user);
    1881          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
    1882          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1883          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1884                                                                      $updatedcoursewarnings);
    1885          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
    1886          $course1['idnumber'] = 'NEWIDNUMBER';
    1887          $courses = array($course1);
    1888          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1889          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1890                                                                      $updatedcoursewarnings);
    1891          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
    1892  
    1893          // Try update course summary without capability.
    1894          $this->assignUserCapability('moodle/course:changeidnumber', $contextid, $roleid);
    1895          $this->unassignUserCapability('moodle/course:changesummary', $contextid, $roleid);
    1896          $user = self::getDataGenerator()->create_user();
    1897          $this->setUser($user);
    1898          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
    1899          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1900          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1901                                                                      $updatedcoursewarnings);
    1902          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
    1903          $course1['summary'] = 'New summary';
    1904          $courses = array($course1);
    1905          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1906          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1907                                                                      $updatedcoursewarnings);
    1908          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
    1909  
    1910          // Try update course with invalid summary format.
    1911          $this->assignUserCapability('moodle/course:changesummary', $contextid, $roleid);
    1912          $user = self::getDataGenerator()->create_user();
    1913          $this->setUser($user);
    1914          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
    1915          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1916          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1917                                                                      $updatedcoursewarnings);
    1918          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
    1919          $course1['summaryformat'] = 10;
    1920          $courses = array($course1);
    1921          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1922          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1923                                                                      $updatedcoursewarnings);
    1924          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
    1925  
    1926          // Try update course visibility without capability.
    1927          $this->unassignUserCapability('moodle/course:visibility', $contextid, $roleid);
    1928          $user = self::getDataGenerator()->create_user();
    1929          $this->setUser($user);
    1930          self::getDataGenerator()->enrol_user($user->id, $course1['id'], $roleid);
    1931          $course1['summaryformat'] = FORMAT_MOODLE;
    1932          $courses = array($course1);
    1933          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1934          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1935                                                                      $updatedcoursewarnings);
    1936          $this->assertEquals(0, count($updatedcoursewarnings['warnings']));
    1937          $course1['visible'] = 0;
    1938          $courses = array($course1);
    1939          $updatedcoursewarnings = core_course_external::update_courses($courses);
    1940          $updatedcoursewarnings = external_api::clean_returnvalue(core_course_external::update_courses_returns(),
    1941                                                                      $updatedcoursewarnings);
    1942          $this->assertEquals(1, count($updatedcoursewarnings['warnings']));
    1943  
    1944          // Try update course custom fields without capability.
    1945          $this->unassignUserCapability('moodle/course:changelockedcustomfields', $contextid, $roleid);
    1946          $user = self::getDataGenerator()->create_user();
    1947          $this->setUser($user);
    1948          self::getDataGenerator()->enrol_user($user->id, $course3['id'], $roleid);
    1949  
    1950          $newupdatedcustomfieldvalue = ['shortname' => 'test', 'value' => 'New updated value'];
    1951          $course3['customfields'] = [$newupdatedcustomfieldvalue];
    1952  
    1953          core_course_external::update_courses([$course3]);
    1954  
    1955          // Custom field was not updated.
    1956          $customfields = \core_course\customfield\course_handler::create()->export_instance_data_object($course3['id']);
    1957          $this->assertEquals(['test' => $updatedcustomfieldvalue['value']], (array)$customfields);
    1958      }
    1959  
    1960      /**
    1961       * Test delete course_module.
    1962       */
    1963      public function test_delete_modules() {
    1964          global $DB;
    1965  
    1966          // Ensure we reset the data after this test.
    1967          $this->resetAfterTest(true);
    1968  
    1969          // Create a user.
    1970          $user = self::getDataGenerator()->create_user();
    1971  
    1972          // Set the tests to run as the user.
    1973          self::setUser($user);
    1974  
    1975          // Create a course to add the modules.
    1976          $course = self::getDataGenerator()->create_course();
    1977  
    1978          // Create two test modules.
    1979          $record = new stdClass();
    1980          $record->course = $course->id;
    1981          $module1 = self::getDataGenerator()->create_module('forum', $record);
    1982          $module2 = self::getDataGenerator()->create_module('assign', $record);
    1983  
    1984          // Check the forum was correctly created.
    1985          $this->assertEquals(1, $DB->count_records('forum', array('id' => $module1->id)));
    1986  
    1987          // Check the assignment was correctly created.
    1988          $this->assertEquals(1, $DB->count_records('assign', array('id' => $module2->id)));
    1989  
    1990          // Check data exists in the course modules table.
    1991          $this->assertEquals(2, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
    1992                  array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
    1993  
    1994          // Enrol the user in the course.
    1995          $enrol = enrol_get_plugin('manual');
    1996          $enrolinstances = enrol_get_instances($course->id, true);
    1997          foreach ($enrolinstances as $courseenrolinstance) {
    1998              if ($courseenrolinstance->enrol == "manual") {
    1999                  $instance = $courseenrolinstance;
    2000                  break;
    2001              }
    2002          }
    2003          $enrol->enrol_user($instance, $user->id);
    2004  
    2005          // Assign capabilities to delete module 1.
    2006          $modcontext = context_module::instance($module1->cmid);
    2007          $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id);
    2008  
    2009          // Assign capabilities to delete module 2.
    2010          $modcontext = context_module::instance($module2->cmid);
    2011          $newrole = create_role('Role 2', 'role2', 'Role 2 description');
    2012          $this->assignUserCapability('moodle/course:manageactivities', $modcontext->id, $newrole);
    2013  
    2014          // Deleting these module instances.
    2015          core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
    2016  
    2017          // Check the forum was deleted.
    2018          $this->assertEquals(0, $DB->count_records('forum', array('id' => $module1->id)));
    2019  
    2020          // Check the assignment was deleted.
    2021          $this->assertEquals(0, $DB->count_records('assign', array('id' => $module2->id)));
    2022  
    2023          // Check we retrieve no data in the course modules table.
    2024          $this->assertEquals(0, $DB->count_records_select('course_modules', 'id = :module1 OR id = :module2',
    2025                  array('module1' => $module1->cmid, 'module2' => $module2->cmid)));
    2026  
    2027          // Call with non-existent course module id and ensure exception thrown.
    2028          try {
    2029              core_course_external::delete_modules(array('1337'));
    2030              $this->fail('Exception expected due to missing course module.');
    2031          } catch (dml_missing_record_exception $e) {
    2032              $this->assertEquals('invalidcoursemodule', $e->errorcode);
    2033          }
    2034  
    2035          // Create two modules.
    2036          $module1 = self::getDataGenerator()->create_module('forum', $record);
    2037          $module2 = self::getDataGenerator()->create_module('assign', $record);
    2038  
    2039          // Since these modules were recreated the user will not have capabilities
    2040          // to delete them, ensure exception is thrown if they try.
    2041          try {
    2042              core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
    2043              $this->fail('Exception expected due to missing capability.');
    2044          } catch (moodle_exception $e) {
    2045              $this->assertEquals('nopermissions', $e->errorcode);
    2046          }
    2047  
    2048          // Unenrol user from the course.
    2049          $enrol->unenrol_user($instance, $user->id);
    2050  
    2051          // Try and delete modules from the course the user was unenrolled in, make sure exception thrown.
    2052          try {
    2053              core_course_external::delete_modules(array($module1->cmid, $module2->cmid));
    2054              $this->fail('Exception expected due to being unenrolled from the course.');
    2055          } catch (moodle_exception $e) {
    2056              $this->assertEquals('requireloginerror', $e->errorcode);
    2057          }
    2058      }
    2059  
    2060      /**
    2061       * Test import_course into an empty course
    2062       */
    2063      public function test_import_course_empty() {
    2064          global $USER;
    2065  
    2066          $this->resetAfterTest(true);
    2067  
    2068          $course1  = self::getDataGenerator()->create_course();
    2069          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course1->id, 'name' => 'Forum test'));
    2070          $page = $this->getDataGenerator()->create_module('page', array('course' => $course1->id, 'name' => 'Page test'));
    2071  
    2072          $course2  = self::getDataGenerator()->create_course();
    2073  
    2074          $course1cms = get_fast_modinfo($course1->id)->get_cms();
    2075          $course2cms = get_fast_modinfo($course2->id)->get_cms();
    2076  
    2077          // Verify the state of the courses before we do the import.
    2078          $this->assertCount(2, $course1cms);
    2079          $this->assertEmpty($course2cms);
    2080  
    2081          // Setup the user to run the operation (ugly hack because validate_context() will
    2082          // fail as the email is not set by $this->setAdminUser()).
    2083          $this->setAdminUser();
    2084          $USER->email = 'emailtopass@example.com';
    2085  
    2086          // Import from course1 to course2.
    2087          core_course_external::import_course($course1->id, $course2->id, 0);
    2088  
    2089          // Verify that now we have two modules in both courses.
    2090          $course1cms = get_fast_modinfo($course1->id)->get_cms();
    2091          $course2cms = get_fast_modinfo($course2->id)->get_cms();
    2092          $this->assertCount(2, $course1cms);
    2093          $this->assertCount(2, $course2cms);
    2094  
    2095          // Verify that the names transfered across correctly.
    2096          foreach ($course2cms as $cm) {
    2097              if ($cm->modname === 'page') {
    2098                  $this->assertEquals($cm->name, $page->name);
    2099              } else if ($cm->modname === 'forum') {
    2100                  $this->assertEquals($cm->name, $forum->name);
    2101              } else {
    2102                  $this->fail('Unknown CM found.');
    2103              }
    2104          }
    2105      }
    2106  
    2107      /**
    2108       * Test import_course into an filled course
    2109       */
    2110      public function test_import_course_filled() {
    2111          global $USER;
    2112  
    2113          $this->resetAfterTest(true);
    2114  
    2115          // Add forum and page to course1.
    2116          $course1  = self::getDataGenerator()->create_course();
    2117          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
    2118          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
    2119  
    2120          // Add quiz to course 2.
    2121          $course2  = self::getDataGenerator()->create_course();
    2122          $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
    2123  
    2124          $course1cms = get_fast_modinfo($course1->id)->get_cms();
    2125          $course2cms = get_fast_modinfo($course2->id)->get_cms();
    2126  
    2127          // Verify the state of the courses before we do the import.
    2128          $this->assertCount(2, $course1cms);
    2129          $this->assertCount(1, $course2cms);
    2130  
    2131          // Setup the user to run the operation (ugly hack because validate_context() will
    2132          // fail as the email is not set by $this->setAdminUser()).
    2133          $this->setAdminUser();
    2134          $USER->email = 'emailtopass@example.com';
    2135  
    2136          // Import from course1 to course2 without deleting content.
    2137          core_course_external::import_course($course1->id, $course2->id, 0);
    2138  
    2139          $course2cms = get_fast_modinfo($course2->id)->get_cms();
    2140  
    2141          // Verify that now we have three modules in course2.
    2142          $this->assertCount(3, $course2cms);
    2143  
    2144          // Verify that the names transfered across correctly.
    2145          foreach ($course2cms as $cm) {
    2146              if ($cm->modname === 'page') {
    2147                  $this->assertEquals($cm->name, $page->name);
    2148              } else if ($cm->modname === 'forum') {
    2149                  $this->assertEquals($cm->name, $forum->name);
    2150              } else if ($cm->modname === 'quiz') {
    2151                  $this->assertEquals($cm->name, $quiz->name);
    2152              } else {
    2153                  $this->fail('Unknown CM found.');
    2154              }
    2155          }
    2156      }
    2157  
    2158      /**
    2159       * Test import_course with only blocks set to backup
    2160       */
    2161      public function test_import_course_blocksonly() {
    2162          global $USER, $DB;
    2163  
    2164          $this->resetAfterTest(true);
    2165  
    2166          // Add forum and page to course1.
    2167          $course1  = self::getDataGenerator()->create_course();
    2168          $course1ctx = context_course::instance($course1->id);
    2169          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
    2170          $block = $this->getDataGenerator()->create_block('online_users', array('parentcontextid' => $course1ctx->id));
    2171  
    2172          $course2  = self::getDataGenerator()->create_course();
    2173          $course2ctx = context_course::instance($course2->id);
    2174          $initialblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
    2175          $initialcmcount = count(get_fast_modinfo($course2->id)->get_cms());
    2176  
    2177          // Setup the user to run the operation (ugly hack because validate_context() will
    2178          // fail as the email is not set by $this->setAdminUser()).
    2179          $this->setAdminUser();
    2180          $USER->email = 'emailtopass@example.com';
    2181  
    2182          // Import from course1 to course2 without deleting content, but excluding
    2183          // activities.
    2184          $options = array(
    2185              array('name' => 'activities', 'value' => 0),
    2186              array('name' => 'blocks', 'value' => 1),
    2187              array('name' => 'filters', 'value' => 0),
    2188          );
    2189  
    2190          core_course_external::import_course($course1->id, $course2->id, 0, $options);
    2191  
    2192          $newcmcount = count(get_fast_modinfo($course2->id)->get_cms());
    2193          $newblockcount = $DB->count_records('block_instances', array('parentcontextid' => $course2ctx->id));
    2194          // Check that course modules haven't changed, but that blocks have.
    2195          $this->assertEquals($initialcmcount, $newcmcount);
    2196          $this->assertEquals(($initialblockcount + 1), $newblockcount);
    2197      }
    2198  
    2199      /**
    2200       * Test import_course into an filled course, deleting content.
    2201       */
    2202      public function test_import_course_deletecontent() {
    2203          global $USER;
    2204          $this->resetAfterTest(true);
    2205  
    2206          // Add forum and page to course1.
    2207          $course1  = self::getDataGenerator()->create_course();
    2208          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course1->id, 'name' => 'Forum test'));
    2209          $page = $this->getDataGenerator()->create_module('page', array('course'=>$course1->id, 'name' => 'Page test'));
    2210  
    2211          // Add quiz to course 2.
    2212          $course2  = self::getDataGenerator()->create_course();
    2213          $quiz = $this->getDataGenerator()->create_module('quiz', array('course'=>$course2->id, 'name' => 'Page test'));
    2214  
    2215          $course1cms = get_fast_modinfo($course1->id)->get_cms();
    2216          $course2cms = get_fast_modinfo($course2->id)->get_cms();
    2217  
    2218          // Verify the state of the courses before we do the import.
    2219          $this->assertCount(2, $course1cms);
    2220          $this->assertCount(1, $course2cms);
    2221  
    2222          // Setup the user to run the operation (ugly hack because validate_context() will
    2223          // fail as the email is not set by $this->setAdminUser()).
    2224          $this->setAdminUser();
    2225          $USER->email = 'emailtopass@example.com';
    2226  
    2227          // Import from course1 to course2,  deleting content.
    2228          core_course_external::import_course($course1->id, $course2->id, 1);
    2229  
    2230          $course2cms = get_fast_modinfo($course2->id)->get_cms();
    2231  
    2232          // Verify that now we have two modules in course2.
    2233          $this->assertCount(2, $course2cms);
    2234  
    2235          // Verify that the course only contains the imported modules.
    2236          foreach ($course2cms as $cm) {
    2237              if ($cm->modname === 'page') {
    2238                  $this->assertEquals($cm->name, $page->name);
    2239              } else if ($cm->modname === 'forum') {
    2240                  $this->assertEquals($cm->name, $forum->name);
    2241              } else {
    2242                  $this->fail('Unknown CM found: '.$cm->name);
    2243              }
    2244          }
    2245      }
    2246  
    2247      /**
    2248       * Ensure import_course handles incorrect deletecontent option correctly.
    2249       */
    2250      public function test_import_course_invalid_deletecontent_option() {
    2251          $this->resetAfterTest(true);
    2252  
    2253          $course1  = self::getDataGenerator()->create_course();
    2254          $course2  = self::getDataGenerator()->create_course();
    2255  
    2256          $this->expectException('moodle_exception');
    2257          $this->expectExceptionMessage(get_string('invalidextparam', 'webservice', -1));
    2258          // Import from course1 to course2, with invalid option
    2259          core_course_external::import_course($course1->id, $course2->id, -1);;
    2260      }
    2261  
    2262      /**
    2263       * Test view_course function
    2264       */
    2265      public function test_view_course() {
    2266  
    2267          $this->resetAfterTest();
    2268  
    2269          // Course without sections.
    2270          $course = $this->getDataGenerator()->create_course(array('numsections' => 5), array('createsections' => true));
    2271          $this->setAdminUser();
    2272  
    2273          // Redirect events to the sink, so we can recover them later.
    2274          $sink = $this->redirectEvents();
    2275  
    2276          $result = core_course_external::view_course($course->id, 1);
    2277          $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
    2278          $events = $sink->get_events();
    2279          $event = reset($events);
    2280  
    2281          // Check the event details are correct.
    2282          $this->assertInstanceOf('\core\event\course_viewed', $event);
    2283          $this->assertEquals(context_course::instance($course->id), $event->get_context());
    2284          $this->assertEquals(1, $event->other['coursesectionnumber']);
    2285  
    2286          $result = core_course_external::view_course($course->id);
    2287          $result = external_api::clean_returnvalue(core_course_external::view_course_returns(), $result);
    2288          $events = $sink->get_events();
    2289          $event = array_pop($events);
    2290          $sink->close();
    2291  
    2292          // Check the event details are correct.
    2293          $this->assertInstanceOf('\core\event\course_viewed', $event);
    2294          $this->assertEquals(context_course::instance($course->id), $event->get_context());
    2295          $this->assertEmpty($event->other);
    2296  
    2297      }
    2298  
    2299      /**
    2300       * Test get_course_module
    2301       */
    2302      public function test_get_course_module() {
    2303          global $DB;
    2304  
    2305          $this->resetAfterTest(true);
    2306  
    2307          $this->setAdminUser();
    2308          $course = self::getDataGenerator()->create_course();
    2309          $record = array(
    2310              'course' => $course->id,
    2311              'name' => 'First Assignment'
    2312          );
    2313          $options = array(
    2314              'idnumber' => 'ABC',
    2315              'visible' => 0
    2316          );
    2317          // Hidden activity.
    2318          $assign = self::getDataGenerator()->create_module('assign', $record, $options);
    2319  
    2320          $outcomescale = 'Distinction, Very Good, Good, Pass, Fail';
    2321  
    2322          // Insert a custom grade scale to be used by an outcome.
    2323          $gradescale = new grade_scale();
    2324          $gradescale->name        = 'gettcoursemodulescale';
    2325          $gradescale->courseid    = $course->id;
    2326          $gradescale->userid      = 0;
    2327          $gradescale->scale       = $outcomescale;
    2328          $gradescale->description = 'This scale is used to mark standard assignments.';
    2329          $gradescale->insert();
    2330  
    2331          // Insert an outcome.
    2332          $data = new stdClass();
    2333          $data->courseid = $course->id;
    2334          $data->fullname = 'Team work';
    2335          $data->shortname = 'Team work';
    2336          $data->scaleid = $gradescale->id;
    2337          $outcome = new grade_outcome($data, false);
    2338          $outcome->insert();
    2339  
    2340          $outcomegradeitem = new grade_item();
    2341          $outcomegradeitem->itemname = $outcome->shortname;
    2342          $outcomegradeitem->itemtype = 'mod';
    2343          $outcomegradeitem->itemmodule = 'assign';
    2344          $outcomegradeitem->iteminstance = $assign->id;
    2345          $outcomegradeitem->outcomeid = $outcome->id;
    2346          $outcomegradeitem->cmid = 0;
    2347          $outcomegradeitem->courseid = $course->id;
    2348          $outcomegradeitem->aggregationcoef = 0;
    2349          $outcomegradeitem->itemnumber = 1000; // Outcomes start at 1000.
    2350          $outcomegradeitem->gradetype = GRADE_TYPE_SCALE;
    2351          $outcomegradeitem->scaleid = $outcome->scaleid;
    2352          $outcomegradeitem->insert();
    2353  
    2354          $assignmentgradeitem = grade_item::fetch(
    2355              array(
    2356                  'itemtype' => 'mod',
    2357                  'itemmodule' => 'assign',
    2358                  'iteminstance' => $assign->id,
    2359                  'itemnumber' => 0,
    2360                  'courseid' => $course->id
    2361              )
    2362          );
    2363          $outcomegradeitem->set_parent($assignmentgradeitem->categoryid);
    2364          $outcomegradeitem->move_after_sortorder($assignmentgradeitem->sortorder);
    2365  
    2366          // Test admin user can see the complete hidden activity.
    2367          $result = core_course_external::get_course_module($assign->cmid);
    2368          $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
    2369  
    2370          $this->assertCount(0, $result['warnings']);
    2371          // Test we retrieve all the fields.
    2372          $this->assertCount(28, $result['cm']);
    2373          $this->assertEquals($record['name'], $result['cm']['name']);
    2374          $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
    2375          $this->assertEquals(100, $result['cm']['grade']);
    2376          $this->assertEquals(0.0, $result['cm']['gradepass']);
    2377          $this->assertEquals('submissions', $result['cm']['advancedgrading'][0]['area']);
    2378          $this->assertEmpty($result['cm']['advancedgrading'][0]['method']);
    2379          $this->assertEquals($outcomescale, $result['cm']['outcomes'][0]['scale']);
    2380  
    2381          $student = $this->getDataGenerator()->create_user();
    2382          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
    2383  
    2384          self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
    2385          $this->setUser($student);
    2386  
    2387          // The user shouldn't be able to see the activity.
    2388          try {
    2389              core_course_external::get_course_module($assign->cmid);
    2390              $this->fail('Exception expected due to invalid permissions.');
    2391          } catch (moodle_exception $e) {
    2392              $this->assertEquals('requireloginerror', $e->errorcode);
    2393          }
    2394  
    2395          // Make module visible.
    2396          set_coursemodule_visible($assign->cmid, 1);
    2397  
    2398          // Test student user.
    2399          $result = core_course_external::get_course_module($assign->cmid);
    2400          $result = external_api::clean_returnvalue(core_course_external::get_course_module_returns(), $result);
    2401  
    2402          $this->assertCount(0, $result['warnings']);
    2403          // Test we retrieve only the few files we can see.
    2404          $this->assertCount(11, $result['cm']);
    2405          $this->assertEquals($assign->cmid, $result['cm']['id']);
    2406          $this->assertEquals($course->id, $result['cm']['course']);
    2407          $this->assertEquals('assign', $result['cm']['modname']);
    2408          $this->assertEquals($assign->id, $result['cm']['instance']);
    2409  
    2410      }
    2411  
    2412      /**
    2413       * Test get_course_module_by_instance
    2414       */
    2415      public function test_get_course_module_by_instance() {
    2416          global $DB;
    2417  
    2418          $this->resetAfterTest(true);
    2419  
    2420          $this->setAdminUser();
    2421          $course = self::getDataGenerator()->create_course();
    2422          $record = array(
    2423              'course' => $course->id,
    2424              'name' => 'First quiz',
    2425              'grade' => 90.00
    2426          );
    2427          $options = array(
    2428              'idnumber' => 'ABC',
    2429              'visible' => 0
    2430          );
    2431          // Hidden activity.
    2432          $quiz = self::getDataGenerator()->create_module('quiz', $record, $options);
    2433  
    2434          // Test admin user can see the complete hidden activity.
    2435          $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
    2436          $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
    2437  
    2438          $this->assertCount(0, $result['warnings']);
    2439          // Test we retrieve all the fields.
    2440          $this->assertCount(26, $result['cm']);
    2441          $this->assertEquals($record['name'], $result['cm']['name']);
    2442          $this->assertEquals($record['grade'], $result['cm']['grade']);
    2443          $this->assertEquals($options['idnumber'], $result['cm']['idnumber']);
    2444  
    2445          $student = $this->getDataGenerator()->create_user();
    2446          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
    2447  
    2448          self::getDataGenerator()->enrol_user($student->id,  $course->id, $studentrole->id);
    2449          $this->setUser($student);
    2450  
    2451          // The user shouldn't be able to see the activity.
    2452          try {
    2453              core_course_external::get_course_module_by_instance('quiz', $quiz->id);
    2454              $this->fail('Exception expected due to invalid permissions.');
    2455          } catch (moodle_exception $e) {
    2456              $this->assertEquals('requireloginerror', $e->errorcode);
    2457          }
    2458  
    2459          // Make module visible.
    2460          set_coursemodule_visible($quiz->cmid, 1);
    2461  
    2462          // Test student user.
    2463          $result = core_course_external::get_course_module_by_instance('quiz', $quiz->id);
    2464          $result = external_api::clean_returnvalue(core_course_external::get_course_module_by_instance_returns(), $result);
    2465  
    2466          $this->assertCount(0, $result['warnings']);
    2467          // Test we retrieve only the few files we can see.
    2468          $this->assertCount(11, $result['cm']);
    2469          $this->assertEquals($quiz->cmid, $result['cm']['id']);
    2470          $this->assertEquals($course->id, $result['cm']['course']);
    2471          $this->assertEquals('quiz', $result['cm']['modname']);
    2472          $this->assertEquals($quiz->id, $result['cm']['instance']);
    2473  
    2474          // Try with an invalid module name.
    2475          try {
    2476              core_course_external::get_course_module_by_instance('abc', $quiz->id);
    2477              $this->fail('Exception expected due to invalid module name.');
    2478          } catch (dml_read_exception $e) {
    2479              $this->assertEquals('dmlreadexception', $e->errorcode);
    2480          }
    2481  
    2482      }
    2483  
    2484      /**
    2485       * Test get_user_navigation_options
    2486       */
    2487      public function test_get_user_navigation_options() {
    2488          global $USER;
    2489  
    2490          $this->resetAfterTest();
    2491          $course1 = self::getDataGenerator()->create_course();
    2492          $course2 = self::getDataGenerator()->create_course();
    2493  
    2494          // Create a viewer user.
    2495          $viewer = self::getDataGenerator()->create_user();
    2496          $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
    2497          $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
    2498  
    2499          $this->setUser($viewer->id);
    2500          $courses = array($course1->id , $course2->id, SITEID);
    2501  
    2502          $result = core_course_external::get_user_navigation_options($courses);
    2503          $result = external_api::clean_returnvalue(core_course_external::get_user_navigation_options_returns(), $result);
    2504  
    2505          $this->assertCount(0, $result['warnings']);
    2506          $this->assertCount(3, $result['courses']);
    2507  
    2508          foreach ($result['courses'] as $course) {
    2509              $navoptions = new stdClass;
    2510              foreach ($course['options'] as $option) {
    2511                  $navoptions->{$option['name']} = $option['available'];
    2512              }
    2513              $this->assertCount(9, $course['options']);
    2514              if ($course['id'] == SITEID) {
    2515                  $this->assertTrue($navoptions->blogs);
    2516                  $this->assertFalse($navoptions->notes);
    2517                  $this->assertFalse($navoptions->participants);
    2518                  $this->assertTrue($navoptions->badges);
    2519                  $this->assertTrue($navoptions->tags);
    2520                  $this->assertFalse($navoptions->grades);
    2521                  $this->assertFalse($navoptions->search);
    2522                  $this->assertTrue($navoptions->calendar);
    2523                  $this->assertTrue($navoptions->competencies);
    2524              } else {
    2525                  $this->assertTrue($navoptions->blogs);
    2526                  $this->assertFalse($navoptions->notes);
    2527                  $this->assertTrue($navoptions->participants);
    2528                  $this->assertFalse($navoptions->badges);
    2529                  $this->assertFalse($navoptions->tags);
    2530                  $this->assertTrue($navoptions->grades);
    2531                  $this->assertFalse($navoptions->search);
    2532                  $this->assertFalse($navoptions->calendar);
    2533                  $this->assertTrue($navoptions->competencies);
    2534              }
    2535          }
    2536      }
    2537  
    2538      /**
    2539       * Test get_user_administration_options
    2540       */
    2541      public function test_get_user_administration_options() {
    2542          global $USER;
    2543  
    2544          $this->resetAfterTest();
    2545          $course1 = self::getDataGenerator()->create_course();
    2546          $course2 = self::getDataGenerator()->create_course();
    2547  
    2548          // Create a viewer user.
    2549          $viewer = self::getDataGenerator()->create_user();
    2550          $this->getDataGenerator()->enrol_user($viewer->id, $course1->id);
    2551          $this->getDataGenerator()->enrol_user($viewer->id, $course2->id);
    2552  
    2553          $this->setUser($viewer->id);
    2554          $courses = array($course1->id , $course2->id, SITEID);
    2555  
    2556          $result = core_course_external::get_user_administration_options($courses);
    2557          $result = external_api::clean_returnvalue(core_course_external::get_user_administration_options_returns(), $result);
    2558  
    2559          $this->assertCount(0, $result['warnings']);
    2560          $this->assertCount(3, $result['courses']);
    2561  
    2562          foreach ($result['courses'] as $course) {
    2563              $adminoptions = new stdClass;
    2564              foreach ($course['options'] as $option) {
    2565                  $adminoptions->{$option['name']} = $option['available'];
    2566              }
    2567              if ($course['id'] == SITEID) {
    2568                  $this->assertCount(17, $course['options']);
    2569                  $this->assertFalse($adminoptions->update);
    2570                  $this->assertFalse($adminoptions->filters);
    2571                  $this->assertFalse($adminoptions->reports);
    2572                  $this->assertFalse($adminoptions->backup);
    2573                  $this->assertFalse($adminoptions->restore);
    2574                  $this->assertFalse($adminoptions->files);
    2575                  $this->assertFalse(!isset($adminoptions->tags));
    2576                  $this->assertFalse($adminoptions->gradebook);
    2577                  $this->assertFalse($adminoptions->outcomes);
    2578                  $this->assertFalse($adminoptions->badges);
    2579                  $this->assertFalse($adminoptions->import);
    2580                  $this->assertFalse($adminoptions->reset);
    2581                  $this->assertFalse($adminoptions->roles);
    2582                  $this->assertFalse($adminoptions->editcompletion);
    2583                  $this->assertFalse($adminoptions->copy);
    2584              } else {
    2585                  $this->assertCount(15, $course['options']);
    2586                  $this->assertFalse($adminoptions->update);
    2587                  $this->assertFalse($adminoptions->filters);
    2588                  $this->assertFalse($adminoptions->reports);
    2589                  $this->assertFalse($adminoptions->backup);
    2590                  $this->assertFalse($adminoptions->restore);
    2591                  $this->assertFalse($adminoptions->files);
    2592                  $this->assertFalse($adminoptions->tags);
    2593                  $this->assertFalse($adminoptions->gradebook);
    2594                  $this->assertFalse($adminoptions->outcomes);
    2595                  $this->assertTrue($adminoptions->badges);
    2596                  $this->assertFalse($adminoptions->import);
    2597                  $this->assertFalse($adminoptions->reset);
    2598                  $this->assertFalse($adminoptions->roles);
    2599                  $this->assertFalse($adminoptions->editcompletion);
    2600                  $this->assertFalse($adminoptions->copy);
    2601              }
    2602          }
    2603      }
    2604  
    2605      /**
    2606       * Test get_courses_by_fields
    2607       */
    2608      public function test_get_courses_by_field() {
    2609          global $DB;
    2610          $this->resetAfterTest(true);
    2611  
    2612          $category1 = self::getDataGenerator()->create_category(array('name' => 'Cat 1'));
    2613          $category2 = self::getDataGenerator()->create_category(array('parent' => $category1->id));
    2614          $course1 = self::getDataGenerator()->create_course(
    2615              array('category' => $category1->id, 'shortname' => 'c1', 'format' => 'topics'));
    2616  
    2617          $fieldcategory = self::getDataGenerator()->