Search moodle.org's
Developer Documentation

See Release Notes

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

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

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  namespace core_course;
  18  
  19  use advanced_testcase;
  20  use backup_controller;
  21  use backup;
  22  use blog_entry;
  23  use cache;
  24  use calendar_event;
  25  use coding_exception;
  26  use comment;
  27  use completion_criteria_date;
  28  use completion_completion;
  29  use context_course;
  30  use context_module;
  31  use context_system;
  32  use context_coursecat;
  33  use core_completion_external;
  34  use core_external;
  35  use core_tag_index_builder;
  36  use core_tag_tag;
  37  use course_capability_assignment;
  38  use course_request;
  39  use core_course_category;
  40  use enrol_imsenterprise\imsenterprise_test;
  41  use core_external\external_api;
  42  use grade_item;
  43  use grading_manager;
  44  use moodle_exception;
  45  use moodle_url;
  46  use phpunit_util;
  47  use rating_manager;
  48  use restore_controller;
  49  use stdClass;
  50  use testing_data_generator;
  51  
  52  defined('MOODLE_INTERNAL') or die();
  53  
  54  // Require library globally because it's constants are used within dataProvider methods, executed before setUpBeforeClass.
  55  global $CFG;
  56  require_once($CFG->dirroot . '/course/lib.php');
  57  
  58  /**
  59   * Course related unit tests
  60   *
  61   * @package    core_course
  62   * @category   test
  63   * @copyright  2012 Petr Skoda {@link http://skodak.org}
  64   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  65   */
  66  class courselib_test extends advanced_testcase {
  67  
  68      /**
  69       * Load required libraries and fixtures.
  70       */
  71      public static function setUpBeforeClass(): void {
  72          global $CFG;
  73  
  74          require_once($CFG->dirroot . '/course/tests/fixtures/course_capability_assignment.php');
  75          require_once($CFG->dirroot . '/enrol/imsenterprise/tests/imsenterprise_test.php');
  76      }
  77  
  78      /**
  79       * Set forum specific test values for calling create_module().
  80       *
  81       * @param object $moduleinfo - the moduleinfo to add some specific values - passed in reference.
  82       */
  83      private function forum_create_set_values(&$moduleinfo) {
  84          // Completion specific to forum - optional.
  85          $moduleinfo->completionposts = 3;
  86          $moduleinfo->completiondiscussions = 1;
  87          $moduleinfo->completionreplies = 2;
  88  
  89          // Specific values to the Forum module.
  90          $moduleinfo->forcesubscribe = FORUM_INITIALSUBSCRIBE;
  91          $moduleinfo->type = 'single';
  92          $moduleinfo->trackingtype = FORUM_TRACKING_FORCED;
  93          $moduleinfo->maxbytes = 10240;
  94          $moduleinfo->maxattachments = 2;
  95  
  96          // Post threshold for blocking - specific to forum.
  97          $moduleinfo->blockperiod = 60*60*24;
  98          $moduleinfo->blockafter = 10;
  99          $moduleinfo->warnafter = 5;
 100  
 101          // Grading of whole forum settings.
 102          $moduleinfo->grade_forum = 0;
 103      }
 104  
 105      /**
 106       * Execute test asserts on the saved DB data by create_module($forum).
 107       *
 108       * @param object $moduleinfo - the specific forum values that were used to create a forum.
 109       * @param object $dbmodinstance - the DB values of the created forum.
 110       */
 111      private function forum_create_run_asserts($moduleinfo, $dbmodinstance) {
 112          // Compare values specific to forums.
 113          $this->assertEquals($moduleinfo->forcesubscribe, $dbmodinstance->forcesubscribe);
 114          $this->assertEquals($moduleinfo->type, $dbmodinstance->type);
 115          $this->assertEquals($moduleinfo->assessed, $dbmodinstance->assessed);
 116          $this->assertEquals($moduleinfo->completionposts, $dbmodinstance->completionposts);
 117          $this->assertEquals($moduleinfo->completiondiscussions, $dbmodinstance->completiondiscussions);
 118          $this->assertEquals($moduleinfo->completionreplies, $dbmodinstance->completionreplies);
 119          $this->assertEquals($moduleinfo->scale, $dbmodinstance->scale);
 120          $this->assertEquals($moduleinfo->assesstimestart, $dbmodinstance->assesstimestart);
 121          $this->assertEquals($moduleinfo->assesstimefinish, $dbmodinstance->assesstimefinish);
 122          $this->assertEquals($moduleinfo->rsstype, $dbmodinstance->rsstype);
 123          $this->assertEquals($moduleinfo->rssarticles, $dbmodinstance->rssarticles);
 124          $this->assertEquals($moduleinfo->trackingtype, $dbmodinstance->trackingtype);
 125          $this->assertEquals($moduleinfo->maxbytes, $dbmodinstance->maxbytes);
 126          $this->assertEquals($moduleinfo->maxattachments, $dbmodinstance->maxattachments);
 127          $this->assertEquals($moduleinfo->blockperiod, $dbmodinstance->blockperiod);
 128          $this->assertEquals($moduleinfo->blockafter, $dbmodinstance->blockafter);
 129          $this->assertEquals($moduleinfo->warnafter, $dbmodinstance->warnafter);
 130      }
 131  
 132      /**
 133       * Set assign module specific test values for calling create_module().
 134       *
 135       * @param object $moduleinfo - the moduleinfo to add some specific values - passed in reference.
 136       */
 137      private function assign_create_set_values(&$moduleinfo) {
 138          // Specific values to the Assign module.
 139          $moduleinfo->alwaysshowdescription = true;
 140          $moduleinfo->submissiondrafts = true;
 141          $moduleinfo->requiresubmissionstatement = true;
 142          $moduleinfo->sendnotifications = true;
 143          $moduleinfo->sendlatenotifications = true;
 144          $moduleinfo->duedate = time() + (7 * 24 * 3600);
 145          $moduleinfo->cutoffdate = time() + (7 * 24 * 3600);
 146          $moduleinfo->gradingduedate = time() + (7 * 24 * 3600);
 147          $moduleinfo->allowsubmissionsfromdate = time();
 148          $moduleinfo->teamsubmission = true;
 149          $moduleinfo->requireallteammemberssubmit = true;
 150          $moduleinfo->teamsubmissiongroupingid = true;
 151          $moduleinfo->blindmarking = true;
 152          $moduleinfo->markingworkflow = true;
 153          $moduleinfo->markingallocation = true;
 154          $moduleinfo->assignsubmission_onlinetext_enabled = true;
 155          $moduleinfo->assignsubmission_file_enabled = true;
 156          $moduleinfo->assignsubmission_file_maxfiles = 1;
 157          $moduleinfo->assignsubmission_file_maxsizebytes = 1000000;
 158          $moduleinfo->assignsubmission_comments_enabled = true;
 159          $moduleinfo->assignfeedback_comments_enabled = true;
 160          $moduleinfo->assignfeedback_offline_enabled = true;
 161          $moduleinfo->assignfeedback_file_enabled = true;
 162  
 163          // Advanced grading.
 164          $gradingmethods = grading_manager::available_methods();
 165          $moduleinfo->advancedgradingmethod_submissions = current(array_keys($gradingmethods));
 166      }
 167  
 168      /**
 169       * Execute test asserts on the saved DB data by create_module($assign).
 170       *
 171       * @param object $moduleinfo - the specific assign module values that were used to create an assign module.
 172       * @param object $dbmodinstance - the DB values of the created assign module.
 173       */
 174      private function assign_create_run_asserts($moduleinfo, $dbmodinstance) {
 175          global $DB;
 176  
 177          $this->assertEquals($moduleinfo->alwaysshowdescription, $dbmodinstance->alwaysshowdescription);
 178          $this->assertEquals($moduleinfo->submissiondrafts, $dbmodinstance->submissiondrafts);
 179          $this->assertEquals($moduleinfo->requiresubmissionstatement, $dbmodinstance->requiresubmissionstatement);
 180          $this->assertEquals($moduleinfo->sendnotifications, $dbmodinstance->sendnotifications);
 181          $this->assertEquals($moduleinfo->duedate, $dbmodinstance->duedate);
 182          $this->assertEquals($moduleinfo->cutoffdate, $dbmodinstance->cutoffdate);
 183          $this->assertEquals($moduleinfo->allowsubmissionsfromdate, $dbmodinstance->allowsubmissionsfromdate);
 184          $this->assertEquals($moduleinfo->teamsubmission, $dbmodinstance->teamsubmission);
 185          $this->assertEquals($moduleinfo->requireallteammemberssubmit, $dbmodinstance->requireallteammemberssubmit);
 186          $this->assertEquals($moduleinfo->teamsubmissiongroupingid, $dbmodinstance->teamsubmissiongroupingid);
 187          $this->assertEquals($moduleinfo->blindmarking, $dbmodinstance->blindmarking);
 188          $this->assertEquals($moduleinfo->markingworkflow, $dbmodinstance->markingworkflow);
 189          $this->assertEquals($moduleinfo->markingallocation, $dbmodinstance->markingallocation);
 190          // The goal not being to fully test assign_add_instance() we'll stop here for the assign tests - to avoid too many DB queries.
 191  
 192          // Advanced grading.
 193          $cm = get_coursemodule_from_instance('assign', $dbmodinstance->id);
 194          $contextmodule = context_module::instance($cm->id);
 195          $advancedgradingmethod = $DB->get_record('grading_areas',
 196              array('contextid' => $contextmodule->id,
 197                  'activemethod' => $moduleinfo->advancedgradingmethod_submissions));
 198          $this->assertEquals($moduleinfo->advancedgradingmethod_submissions, $advancedgradingmethod);
 199      }
 200  
 201      /**
 202       * Run some asserts test for a specific module for the function create_module().
 203       *
 204       * The function has been created (and is called) for $this->test_create_module().
 205       * Note that the call to MODULE_create_set_values and MODULE_create_run_asserts are done after the common set values/run asserts.
 206       * So if you want, you can overwrite the default values/asserts in the respective functions.
 207       * @param string $modulename Name of the module ('forum', 'assign', 'book'...).
 208       */
 209      private function create_specific_module_test($modulename) {
 210          global $DB, $CFG;
 211  
 212          $this->resetAfterTest(true);
 213  
 214          $this->setAdminUser();
 215  
 216          // Warnings: you'll need to change this line if ever you come to test a module not following Moodle standard.
 217          require_once($CFG->dirroot.'/mod/'. $modulename .'/lib.php');
 218  
 219          // Enable avaibility.
 220          // If not enabled all conditional fields will be ignored.
 221          set_config('enableavailability', 1);
 222  
 223          // Enable course completion.
 224          // If not enabled all completion settings will be ignored.
 225          set_config('enablecompletion', COMPLETION_ENABLED);
 226  
 227          // Enable forum RSS feeds.
 228          set_config('enablerssfeeds', 1);
 229          set_config('forum_enablerssfeeds', 1);
 230  
 231          $course = $this->getDataGenerator()->create_course(array('numsections'=>1, 'enablecompletion' => COMPLETION_ENABLED),
 232             array('createsections'=>true));
 233  
 234          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
 235  
 236          // Create assign module instance for test.
 237          $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 238          $params['course'] = $course->id;
 239          $instance = $generator->create_instance($params);
 240          $assigncm = get_coursemodule_from_instance('assign', $instance->id);
 241  
 242          // Module test values.
 243          $moduleinfo = new stdClass();
 244  
 245          // Always mandatory generic values to any module.
 246          $moduleinfo->modulename = $modulename;
 247          $moduleinfo->section = 1; // This is the section number in the course. Not the section id in the database.
 248          $moduleinfo->course = $course->id;
 249          $moduleinfo->groupingid = $grouping->id;
 250          $moduleinfo->visible = true;
 251          $moduleinfo->visibleoncoursepage = true;
 252  
 253          // Sometimes optional generic values for some modules.
 254          $moduleinfo->name = 'My test module';
 255          $moduleinfo->showdescription = 1; // standard boolean
 256          require_once($CFG->libdir . '/gradelib.php');
 257          $gradecats = grade_get_categories_menu($moduleinfo->course, false);
 258          $gradecatid = current(array_keys($gradecats)); // Retrieve the first key of $gradecats
 259          $moduleinfo->gradecat = $gradecatid;
 260          $moduleinfo->groupmode = VISIBLEGROUPS;
 261          $moduleinfo->cmidnumber = 'idnumber_XXX';
 262  
 263          // Completion common to all module.
 264          $moduleinfo->completion = COMPLETION_TRACKING_AUTOMATIC;
 265          $moduleinfo->completionview = COMPLETION_VIEW_REQUIRED;
 266          $moduleinfo->completiongradeitemnumber = 1;
 267          $moduleinfo->completionpassgrade = 0;
 268          $moduleinfo->completionexpected = time() + (7 * 24 * 3600);
 269  
 270          // Conditional activity.
 271          $moduleinfo->availability = '{"op":"&","showc":[true,true],"c":[' .
 272                  '{"type":"date","d":">=","t":' . time() . '},' .
 273                  '{"type":"date","d":"<","t":' . (time() + (7 * 24 * 3600)) . '}' .
 274                  ']}';
 275          $coursegradeitem = grade_item::fetch_course_item($moduleinfo->course); //the activity will become available only when the user reach some grade into the course itself.
 276          $moduleinfo->conditiongradegroup = array(array('conditiongradeitemid' => $coursegradeitem->id, 'conditiongrademin' => 10, 'conditiongrademax' => 80));
 277          $moduleinfo->conditionfieldgroup = array(array('conditionfield' => 'email', 'conditionfieldoperator' => \availability_profile\condition::OP_CONTAINS, 'conditionfieldvalue' => '@'));
 278          $moduleinfo->conditioncompletiongroup = array(array('conditionsourcecmid' => $assigncm->id, 'conditionrequiredcompletion' => COMPLETION_COMPLETE)); // "conditionsourcecmid == 0" => none
 279  
 280          // Grading and Advanced grading.
 281          require_once($CFG->dirroot . '/rating/lib.php');
 282          $moduleinfo->assessed = RATING_AGGREGATE_AVERAGE;
 283          $moduleinfo->scale = 10; // Note: it could be minus (for specific course scale). It is a signed number.
 284          $moduleinfo->assesstimestart = time();
 285          $moduleinfo->assesstimefinish = time() + (7 * 24 * 3600);
 286  
 287          // RSS.
 288          $moduleinfo->rsstype = 2;
 289          $moduleinfo->rssarticles = 10;
 290  
 291          // Optional intro editor (depends of module).
 292          $draftid_editor = 0;
 293          file_prepare_draft_area($draftid_editor, null, null, null, null);
 294          $moduleinfo->introeditor = array('text' => 'This is a module', 'format' => FORMAT_HTML, 'itemid' => $draftid_editor);
 295  
 296          // Following is the advanced grading method area called 'submissions' for the 'assign' module.
 297          if (plugin_supports('mod', $modulename, FEATURE_GRADE_HAS_GRADE, false) && !plugin_supports('mod', $modulename, FEATURE_RATE, false)) {
 298              $moduleinfo->grade = 100;
 299          }
 300  
 301          // Plagiarism form values.
 302          // No plagiarism plugin installed by default. Use this space to make your own test.
 303  
 304          // Values specific to the module.
 305          $modulesetvalues = $modulename.'_create_set_values';
 306          $this->$modulesetvalues($moduleinfo);
 307  
 308          // Create the module.
 309          $result = create_module($moduleinfo);
 310  
 311          // Retrieve the module info.
 312          $dbmodinstance = $DB->get_record($moduleinfo->modulename, array('id' => $result->instance));
 313          $dbcm = get_coursemodule_from_instance($moduleinfo->modulename, $result->instance);
 314          // We passed the course section number to create_courses but $dbcm contain the section id.
 315          // We need to retrieve the db course section number.
 316          $section = $DB->get_record('course_sections', array('course' => $dbcm->course, 'id' => $dbcm->section));
 317          // Retrieve the grade item.
 318          $gradeitem = $DB->get_record('grade_items', array('courseid' => $moduleinfo->course,
 319              'iteminstance' => $dbmodinstance->id, 'itemmodule' => $moduleinfo->modulename));
 320  
 321          // Compare the values common to all module instances.
 322          $this->assertEquals($moduleinfo->modulename, $dbcm->modname);
 323          $this->assertEquals($moduleinfo->section, $section->section);
 324          $this->assertEquals($moduleinfo->course, $dbcm->course);
 325          $this->assertEquals($moduleinfo->groupingid, $dbcm->groupingid);
 326          $this->assertEquals($moduleinfo->visible, $dbcm->visible);
 327          $this->assertEquals($moduleinfo->completion, $dbcm->completion);
 328          $this->assertEquals($moduleinfo->completionview, $dbcm->completionview);
 329          $this->assertEquals($moduleinfo->completiongradeitemnumber, $dbcm->completiongradeitemnumber);
 330          $this->assertEquals($moduleinfo->completionpassgrade, $dbcm->completionpassgrade);
 331          $this->assertEquals($moduleinfo->completionexpected, $dbcm->completionexpected);
 332          $this->assertEquals($moduleinfo->availability, $dbcm->availability);
 333          $this->assertEquals($moduleinfo->showdescription, $dbcm->showdescription);
 334          $this->assertEquals($moduleinfo->groupmode, $dbcm->groupmode);
 335          $this->assertEquals($moduleinfo->cmidnumber, $dbcm->idnumber);
 336          $this->assertEquals($moduleinfo->gradecat, $gradeitem->categoryid);
 337  
 338          // Optional grade testing.
 339          if (plugin_supports('mod', $modulename, FEATURE_GRADE_HAS_GRADE, false) && !plugin_supports('mod', $modulename, FEATURE_RATE, false)) {
 340              $this->assertEquals($moduleinfo->grade, $dbmodinstance->grade);
 341          }
 342  
 343          // Some optional (but quite common) to some module.
 344          $this->assertEquals($moduleinfo->name, $dbmodinstance->name);
 345          $this->assertEquals($moduleinfo->intro, $dbmodinstance->intro);
 346          $this->assertEquals($moduleinfo->introformat, $dbmodinstance->introformat);
 347  
 348          // Test specific to the module.
 349          $modulerunasserts = $modulename.'_create_run_asserts';
 350          $this->$modulerunasserts($moduleinfo, $dbmodinstance);
 351          return $moduleinfo;
 352      }
 353  
 354      /**
 355       * Create module associated blog and tags.
 356       *
 357       * @param object $course Course.
 358       * @param object $modulecontext The context of the module.
 359       */
 360      private function create_module_asscociated_blog($course, $modulecontext) {
 361          global $DB, $CFG;
 362  
 363          // Create default group.
 364          $group = new stdClass();
 365          $group->courseid = $course->id;
 366          $group->name = 'Group';
 367          $group->id = $DB->insert_record('groups', $group);
 368  
 369          // Create default user.
 370          $user = $this->getDataGenerator()->create_user(array(
 371              'username' => 'testuser',
 372              'firstname' => 'Firsname',
 373              'lastname' => 'Lastname'
 374          ));
 375  
 376          // Create default post.
 377          $post = new stdClass();
 378          $post->userid = $user->id;
 379          $post->groupid = $group->id;
 380          $post->content = 'test post content text';
 381          $post->module = 'blog';
 382          $post->id = $DB->insert_record('post', $post);
 383  
 384          // Create default tag.
 385          $tag = $this->getDataGenerator()->create_tag(array('userid' => $user->id,
 386              'rawname' => 'Testtagname', 'isstandard' => 1));
 387          // Apply the tag to the blog.
 388          $DB->insert_record('tag_instance', array('tagid' => $tag->id, 'itemtype' => 'user',
 389              'component' => 'core', 'itemid' => $post->id, 'ordering' => 0));
 390  
 391          require_once($CFG->dirroot . '/blog/locallib.php');
 392          $blog = new blog_entry($post->id);
 393          $blog->add_association($modulecontext->id);
 394  
 395          return $blog;
 396      }
 397  
 398      /**
 399       * Test create_module() for multiple modules defined in the $modules array (first declaration of the function).
 400       */
 401      public function test_create_module() {
 402          // Add the module name you want to test here.
 403          // Create the match MODULENAME_create_set_values() and MODULENAME_create_run_asserts().
 404          $modules = array('forum', 'assign');
 405          // Run all tests.
 406          foreach ($modules as $modulename) {
 407              $this->create_specific_module_test($modulename);
 408          }
 409      }
 410  
 411      /**
 412       * Test update_module() for multiple modules defined in the $modules array (first declaration of the function).
 413       */
 414      public function test_update_module() {
 415          // Add the module name you want to test here.
 416          // Create the match MODULENAME_update_set_values() and MODULENAME_update_run_asserts().
 417          $modules = array('forum');
 418          // Run all tests.
 419          foreach ($modules as $modulename) {
 420              $this->update_specific_module_test($modulename);
 421          }
 422      }
 423  
 424      /**
 425       * Set forum specific test values for calling update_module().
 426       *
 427       * @param object $moduleinfo - the moduleinfo to add some specific values - passed in reference.
 428       */
 429      private function forum_update_set_values(&$moduleinfo) {
 430          // Completion specific to forum - optional.
 431          $moduleinfo->completionposts = 3;
 432          $moduleinfo->completiondiscussions = 1;
 433          $moduleinfo->completionreplies = 2;
 434  
 435          // Specific values to the Forum module.
 436          $moduleinfo->forcesubscribe = FORUM_INITIALSUBSCRIBE;
 437          $moduleinfo->type = 'single';
 438          $moduleinfo->trackingtype = FORUM_TRACKING_FORCED;
 439          $moduleinfo->maxbytes = 10240;
 440          $moduleinfo->maxattachments = 2;
 441  
 442          // Post threshold for blocking - specific to forum.
 443          $moduleinfo->blockperiod = 60*60*24;
 444          $moduleinfo->blockafter = 10;
 445          $moduleinfo->warnafter = 5;
 446  
 447          // Grading of whole forum settings.
 448          $moduleinfo->grade_forum = 0;
 449      }
 450  
 451      /**
 452       * Execute test asserts on the saved DB data by update_module($forum).
 453       *
 454       * @param object $moduleinfo - the specific forum values that were used to update a forum.
 455       * @param object $dbmodinstance - the DB values of the updated forum.
 456       */
 457      private function forum_update_run_asserts($moduleinfo, $dbmodinstance) {
 458          // Compare values specific to forums.
 459          $this->assertEquals($moduleinfo->forcesubscribe, $dbmodinstance->forcesubscribe);
 460          $this->assertEquals($moduleinfo->type, $dbmodinstance->type);
 461          $this->assertEquals($moduleinfo->assessed, $dbmodinstance->assessed);
 462          $this->assertEquals($moduleinfo->completionposts, $dbmodinstance->completionposts);
 463          $this->assertEquals($moduleinfo->completiondiscussions, $dbmodinstance->completiondiscussions);
 464          $this->assertEquals($moduleinfo->completionreplies, $dbmodinstance->completionreplies);
 465          $this->assertEquals($moduleinfo->scale, $dbmodinstance->scale);
 466          $this->assertEquals($moduleinfo->assesstimestart, $dbmodinstance->assesstimestart);
 467          $this->assertEquals($moduleinfo->assesstimefinish, $dbmodinstance->assesstimefinish);
 468          $this->assertEquals($moduleinfo->rsstype, $dbmodinstance->rsstype);
 469          $this->assertEquals($moduleinfo->rssarticles, $dbmodinstance->rssarticles);
 470          $this->assertEquals($moduleinfo->trackingtype, $dbmodinstance->trackingtype);
 471          $this->assertEquals($moduleinfo->maxbytes, $dbmodinstance->maxbytes);
 472          $this->assertEquals($moduleinfo->maxattachments, $dbmodinstance->maxattachments);
 473          $this->assertEquals($moduleinfo->blockperiod, $dbmodinstance->blockperiod);
 474          $this->assertEquals($moduleinfo->blockafter, $dbmodinstance->blockafter);
 475          $this->assertEquals($moduleinfo->warnafter, $dbmodinstance->warnafter);
 476      }
 477  
 478  
 479  
 480      /**
 481       * Test a specific type of module.
 482       *
 483       * @param string $modulename - the module name to test
 484       */
 485      private function update_specific_module_test($modulename) {
 486          global $DB, $CFG;
 487  
 488          $this->resetAfterTest(true);
 489  
 490          $this->setAdminUser();
 491  
 492          // Warnings: you'll need to change this line if ever you come to test a module not following Moodle standard.
 493          require_once($CFG->dirroot.'/mod/'. $modulename .'/lib.php');
 494  
 495          // Enable avaibility.
 496          // If not enabled all conditional fields will be ignored.
 497          set_config('enableavailability', 1);
 498  
 499          // Enable course completion.
 500          // If not enabled all completion settings will be ignored.
 501          set_config('enablecompletion', COMPLETION_ENABLED);
 502  
 503          // Enable forum RSS feeds.
 504          set_config('enablerssfeeds', 1);
 505          set_config('forum_enablerssfeeds', 1);
 506  
 507          $course = $this->getDataGenerator()->create_course(array('numsections'=>1, 'enablecompletion' => COMPLETION_ENABLED),
 508             array('createsections'=>true));
 509  
 510          $grouping = $this->getDataGenerator()->create_grouping(array('courseid' => $course->id));
 511  
 512          // Create assign module instance for testing gradeitem.
 513          $generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
 514          $params['course'] = $course->id;
 515          $instance = $generator->create_instance($params);
 516          $assigncm = get_coursemodule_from_instance('assign', $instance->id);
 517  
 518          // Create the test forum to update.
 519          $initvalues = new stdClass();
 520          $initvalues->introformat = FORMAT_HTML;
 521          $initvalues->course = $course->id;
 522          $forum = self::getDataGenerator()->create_module('forum', $initvalues);
 523  
 524          // Retrieve course module.
 525          $cm = get_coursemodule_from_instance('forum', $forum->id);
 526  
 527          // Module test values.
 528          $moduleinfo = new stdClass();
 529  
 530          // Always mandatory generic values to any module.
 531          $moduleinfo->coursemodule = $cm->id;
 532          $moduleinfo->modulename = $modulename;
 533          $moduleinfo->course = $course->id;
 534          $moduleinfo->groupingid = $grouping->id;
 535          $moduleinfo->visible = true;
 536          $moduleinfo->visibleoncoursepage = true;
 537  
 538          // Sometimes optional generic values for some modules.
 539          $moduleinfo->name = 'My test module';
 540          $moduleinfo->showdescription = 1; // standard boolean
 541          require_once($CFG->libdir . '/gradelib.php');
 542          $gradecats = grade_get_categories_menu($moduleinfo->course, false);
 543          $gradecatid = current(array_keys($gradecats)); // Retrieve the first key of $gradecats
 544          $moduleinfo->gradecat = $gradecatid;
 545          $moduleinfo->groupmode = VISIBLEGROUPS;
 546          $moduleinfo->cmidnumber = 'idnumber_XXX';
 547  
 548          // Completion common to all module.
 549          $moduleinfo->completion = COMPLETION_TRACKING_AUTOMATIC;
 550          $moduleinfo->completionview = COMPLETION_VIEW_REQUIRED;
 551          $moduleinfo->completiongradeitemnumber = 1;
 552          $moduleinfo->completionpassgrade = 0;
 553          $moduleinfo->completionexpected = time() + (7 * 24 * 3600);
 554          $moduleinfo->completionunlocked = 1;
 555  
 556          // Conditional activity.
 557          $coursegradeitem = grade_item::fetch_course_item($moduleinfo->course); //the activity will become available only when the user reach some grade into the course itself.
 558          $moduleinfo->availability = json_encode(\core_availability\tree::get_root_json(
 559                  array(\availability_date\condition::get_json('>=', time()),
 560                  \availability_date\condition::get_json('<', time() + (7 * 24 * 3600)),
 561                  \availability_grade\condition::get_json($coursegradeitem->id, 10, 80),
 562                  \availability_profile\condition::get_json(false, 'email', 'contains', '@'),
 563                  \availability_completion\condition::get_json($assigncm->id, COMPLETION_COMPLETE)), '&'));
 564  
 565          // Grading and Advanced grading.
 566          require_once($CFG->dirroot . '/rating/lib.php');
 567          $moduleinfo->assessed = RATING_AGGREGATE_AVERAGE;
 568          $moduleinfo->scale = 10; // Note: it could be minus (for specific course scale). It is a signed number.
 569          $moduleinfo->assesstimestart = time();
 570          $moduleinfo->assesstimefinish = time() + (7 * 24 * 3600);
 571  
 572          // RSS.
 573          $moduleinfo->rsstype = 2;
 574          $moduleinfo->rssarticles = 10;
 575  
 576          // Optional intro editor (depends of module).
 577          $draftid_editor = 0;
 578          file_prepare_draft_area($draftid_editor, null, null, null, null);
 579          $moduleinfo->introeditor = array('text' => 'This is a module', 'format' => FORMAT_HTML, 'itemid' => $draftid_editor);
 580  
 581          // Following is the advanced grading method area called 'submissions' for the 'assign' module.
 582          if (plugin_supports('mod', $modulename, FEATURE_GRADE_HAS_GRADE, false) && !plugin_supports('mod', $modulename, FEATURE_RATE, false)) {
 583              $moduleinfo->grade = 100;
 584          }
 585          // Plagiarism form values.
 586          // No plagiarism plugin installed by default. Use this space to make your own test.
 587  
 588          // Values specific to the module.
 589          $modulesetvalues = $modulename.'_update_set_values';
 590          $this->$modulesetvalues($moduleinfo);
 591  
 592          // Create the module.
 593          $result = update_module($moduleinfo);
 594  
 595          // Retrieve the module info.
 596          $dbmodinstance = $DB->get_record($moduleinfo->modulename, array('id' => $result->instance));
 597          $dbcm = get_coursemodule_from_instance($moduleinfo->modulename, $result->instance);
 598          // Retrieve the grade item.
 599          $gradeitem = $DB->get_record('grade_items', array('courseid' => $moduleinfo->course,
 600              'iteminstance' => $dbmodinstance->id, 'itemmodule' => $moduleinfo->modulename));
 601  
 602          // Compare the values common to all module instances.
 603          $this->assertEquals($moduleinfo->modulename, $dbcm->modname);
 604          $this->assertEquals($moduleinfo->course, $dbcm->course);
 605          $this->assertEquals($moduleinfo->groupingid, $dbcm->groupingid);
 606          $this->assertEquals($moduleinfo->visible, $dbcm->visible);
 607          $this->assertEquals($moduleinfo->completion, $dbcm->completion);
 608          $this->assertEquals($moduleinfo->completionview, $dbcm->completionview);
 609          $this->assertEquals($moduleinfo->completiongradeitemnumber, $dbcm->completiongradeitemnumber);
 610          $this->assertEquals($moduleinfo->completionpassgrade, $dbcm->completionpassgrade);
 611          $this->assertEquals($moduleinfo->completionexpected, $dbcm->completionexpected);
 612          $this->assertEquals($moduleinfo->availability, $dbcm->availability);
 613          $this->assertEquals($moduleinfo->showdescription, $dbcm->showdescription);
 614          $this->assertEquals($moduleinfo->groupmode, $dbcm->groupmode);
 615          $this->assertEquals($moduleinfo->cmidnumber, $dbcm->idnumber);
 616          $this->assertEquals($moduleinfo->gradecat, $gradeitem->categoryid);
 617  
 618          // Optional grade testing.
 619          if (plugin_supports('mod', $modulename, FEATURE_GRADE_HAS_GRADE, false) && !plugin_supports('mod', $modulename, FEATURE_RATE, false)) {
 620              $this->assertEquals($moduleinfo->grade, $dbmodinstance->grade);
 621          }
 622  
 623          // Some optional (but quite common) to some module.
 624          $this->assertEquals($moduleinfo->name, $dbmodinstance->name);
 625          $this->assertEquals($moduleinfo->intro, $dbmodinstance->intro);
 626          $this->assertEquals($moduleinfo->introformat, $dbmodinstance->introformat);
 627  
 628          // Test specific to the module.
 629          $modulerunasserts = $modulename.'_update_run_asserts';
 630          $this->$modulerunasserts($moduleinfo, $dbmodinstance);
 631          return $moduleinfo;
 632     }
 633  
 634      /**
 635       * Data provider for course_delete module
 636       *
 637       * @return array An array of arrays contain test data
 638       */
 639      public function provider_course_delete_module() {
 640          $data = array();
 641  
 642          $data['assign'] = array('assign', array('duedate' => time()));
 643          $data['quiz'] = array('quiz', array('duedate' => time()));
 644  
 645          return $data;
 646      }
 647  
 648      /**
 649       * Test the create_course function
 650       */
 651      public function test_create_course() {
 652          global $DB;
 653          $this->resetAfterTest(true);
 654          $defaultcategory = $DB->get_field_select('course_categories', "MIN(id)", "parent=0");
 655  
 656          $course = new stdClass();
 657          $course->fullname = 'Apu loves Unit Təsts';
 658          $course->shortname = 'Spread the lŭve';
 659          $course->idnumber = '123';
 660          $course->summary = 'Awesome!';
 661          $course->summaryformat = FORMAT_PLAIN;
 662          $course->format = 'topics';
 663          $course->newsitems = 0;
 664          $course->category = $defaultcategory;
 665          $original = (array) $course;
 666  
 667          $created = create_course($course);
 668          $context = context_course::instance($created->id);
 669  
 670          // Compare original and created.
 671          $this->assertEquals($original, array_intersect_key((array) $created, $original));
 672  
 673          // Ensure default section is created.
 674          $sectioncreated = $DB->record_exists('course_sections', array('course' => $created->id, 'section' => 0));
 675          $this->assertTrue($sectioncreated);
 676  
 677          // Ensure that the shortname isn't duplicated.
 678          try {
 679              $created = create_course($course);
 680              $this->fail('Exception expected');
 681          } catch (moodle_exception $e) {
 682              $this->assertSame(get_string('shortnametaken', 'error', $course->shortname), $e->getMessage());
 683          }
 684  
 685          // Ensure that the idnumber isn't duplicated.
 686          $course->shortname .= '1';
 687          try {
 688              $created = create_course($course);
 689              $this->fail('Exception expected');
 690          } catch (moodle_exception $e) {
 691              $this->assertSame(get_string('courseidnumbertaken', 'error', $course->idnumber), $e->getMessage());
 692          }
 693      }
 694  
 695      public function test_create_course_with_generator() {
 696          global $DB;
 697          $this->resetAfterTest(true);
 698          $course = $this->getDataGenerator()->create_course();
 699  
 700          // Ensure default section is created.
 701          $sectioncreated = $DB->record_exists('course_sections', array('course' => $course->id, 'section' => 0));
 702          $this->assertTrue($sectioncreated);
 703      }
 704  
 705      public function test_create_course_sections() {
 706          global $DB;
 707          $this->resetAfterTest(true);
 708  
 709          $numsections = 5;
 710          $course = $this->getDataGenerator()->create_course(
 711                  array('shortname' => 'GrowingCourse',
 712                      'fullname' => 'Growing Course',
 713                      'numsections' => $numsections),
 714                  array('createsections' => true));
 715  
 716          // Ensure all 6 (0-5) sections were created and course content cache works properly
 717          $sectionscreated = array_keys(get_fast_modinfo($course)->get_section_info_all());
 718          $this->assertEquals(range(0, $numsections), $sectionscreated);
 719  
 720          // this will do nothing, section already exists
 721          $this->assertFalse(course_create_sections_if_missing($course, $numsections));
 722  
 723          // this will create new section
 724          $this->assertTrue(course_create_sections_if_missing($course, $numsections + 1));
 725  
 726          // Ensure all 7 (0-6) sections were created and modinfo/sectioninfo cache works properly
 727          $sectionscreated = array_keys(get_fast_modinfo($course)->get_section_info_all());
 728          $this->assertEquals(range(0, $numsections + 1), $sectionscreated);
 729      }
 730  
 731      public function test_update_course() {
 732          global $DB;
 733  
 734          $this->resetAfterTest();
 735  
 736          $defaultcategory = $DB->get_field_select('course_categories', 'MIN(id)', 'parent = 0');
 737  
 738          $course = new stdClass();
 739          $course->fullname = 'Apu loves Unit Təsts';
 740          $course->shortname = 'test1';
 741          $course->idnumber = '1';
 742          $course->summary = 'Awesome!';
 743          $course->summaryformat = FORMAT_PLAIN;
 744          $course->format = 'topics';
 745          $course->newsitems = 0;
 746          $course->numsections = 5;
 747          $course->category = $defaultcategory;
 748  
 749          $created = create_course($course);
 750          // Ensure the checks only work on idnumber/shortname that are not already ours.
 751          update_course($created);
 752  
 753          $course->shortname = 'test2';
 754          $course->idnumber = '2';
 755  
 756          $created2 = create_course($course);
 757  
 758          // Test duplicate idnumber.
 759          $created2->idnumber = '1';
 760          try {
 761              update_course($created2);
 762              $this->fail('Expected exception when trying to update a course with duplicate idnumber');
 763          } catch (moodle_exception $e) {
 764              $this->assertEquals(get_string('courseidnumbertaken', 'error', $created2->idnumber), $e->getMessage());
 765          }
 766  
 767          // Test duplicate shortname.
 768          $created2->idnumber = '2';
 769          $created2->shortname = 'test1';
 770          try {
 771              update_course($created2);
 772              $this->fail('Expected exception when trying to update a course with a duplicate shortname');
 773          } catch (moodle_exception $e) {
 774              $this->assertEquals(get_string('shortnametaken', 'error', $created2->shortname), $e->getMessage());
 775          }
 776      }
 777  
 778      public function test_update_course_section_time_modified() {
 779          global $DB;
 780  
 781          $this->resetAfterTest();
 782  
 783          // Create the course with sections.
 784          $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
 785          $sections = $DB->get_records('course_sections', array('course' => $course->id));
 786  
 787          // Get the last section's time modified value.
 788          $section = array_pop($sections);
 789          $oldtimemodified = $section->timemodified;
 790  
 791          // Update the section.
 792          $this->waitForSecond(); // Ensuring that the section update occurs at a different timestamp.
 793          course_update_section($course, $section, array());
 794  
 795          // Check that the time has changed.
 796          $section = $DB->get_record('course_sections', array('id' => $section->id));
 797          $newtimemodified = $section->timemodified;
 798          $this->assertGreaterThan($oldtimemodified, $newtimemodified);
 799      }
 800  
 801      /**
 802       * Relative dates mode settings provider for course creation.
 803       */
 804      public function create_course_relative_dates_provider() {
 805          return [
 806              [0, 0, 0],
 807              [0, 1, 0],
 808              [1, 0, 0],
 809              [1, 1, 1],
 810          ];
 811      }
 812  
 813      /**
 814       * Test create_course by attempting to change the relative dates mode.
 815       *
 816       * @dataProvider create_course_relative_dates_provider
 817       * @param int $setting The value for the 'enablecourserelativedates' admin setting.
 818       * @param int $mode The value for the course's 'relativedatesmode' field.
 819       * @param int $expectedvalue The expected value of the 'relativedatesmode' field after course creation.
 820       */
 821      public function test_relative_dates_mode_for_course_creation($setting, $mode, $expectedvalue) {
 822          global $DB;
 823  
 824          $this->resetAfterTest();
 825  
 826          set_config('enablecourserelativedates', $setting);
 827  
 828          // Generate a course with relative dates mode set to $mode.
 829          $course = $this->getDataGenerator()->create_course(['relativedatesmode' => $mode]);
 830  
 831          // Verify that the relative dates match what's expected.
 832          $relativedatesmode = $DB->get_field('course', 'relativedatesmode', ['id' => $course->id]);
 833          $this->assertEquals($expectedvalue, $relativedatesmode);
 834      }
 835  
 836      /**
 837       * Test update_course by attempting to change the relative dates mode.
 838       */
 839      public function test_relative_dates_mode_for_course_update() {
 840          global $DB;
 841  
 842          $this->resetAfterTest();
 843  
 844          set_config('enablecourserelativedates', 1);
 845  
 846          // Generate a course with relative dates mode set to 1.
 847          $course = $this->getDataGenerator()->create_course(['relativedatesmode' => 1]);
 848  
 849          // Attempt to update the course with a changed relativedatesmode.
 850          $course->relativedatesmode = 0;
 851          update_course($course);
 852  
 853          // Verify that the relative dates mode has not changed.
 854          $relativedatesmode = $DB->get_field('course', 'relativedatesmode', ['id' => $course->id]);
 855          $this->assertEquals(1, $relativedatesmode);
 856      }
 857  
 858      public function test_course_add_cm_to_section() {
 859          global $DB;
 860          $this->resetAfterTest(true);
 861  
 862          // Create course with 1 section.
 863          $course = $this->getDataGenerator()->create_course(
 864                  array('shortname' => 'GrowingCourse',
 865                      'fullname' => 'Growing Course',
 866                      'numsections' => 1),
 867                  array('createsections' => true));
 868  
 869          // Trash modinfo.
 870          rebuild_course_cache($course->id, true);
 871  
 872          // Create some cms for testing.
 873          $cmids = array();
 874          for ($i=0; $i<4; $i++) {
 875              $cmids[$i] = $DB->insert_record('course_modules', array('course' => $course->id));
 876          }
 877  
 878          // Add it to section that exists.
 879          course_add_cm_to_section($course, $cmids[0], 1);
 880  
 881          // Check it got added to sequence.
 882          $sequence = $DB->get_field('course_sections', 'sequence', array('course' => $course->id, 'section' => 1));
 883          $this->assertEquals($cmids[0], $sequence);
 884  
 885          // Add a second, this time using courseid variant of parameters.
 886          $coursecacherev = $DB->get_field('course', 'cacherev', array('id' => $course->id));
 887          course_add_cm_to_section($course->id, $cmids[1], 1);
 888          $sequence = $DB->get_field('course_sections', 'sequence', array('course' => $course->id, 'section' => 1));
 889          $this->assertEquals($cmids[0] . ',' . $cmids[1], $sequence);
 890  
 891          // Check that modinfo cache was reset but not rebuilt (important for performance if calling repeatedly).
 892          $newcacherev = $DB->get_field('course', 'cacherev', ['id' => $course->id]);
 893          $this->assertGreaterThan($coursecacherev, $newcacherev);
 894          $this->assertEmpty(cache::make('core', 'coursemodinfo')->get_versioned($course->id, $newcacherev));
 895  
 896          // Add one to section that doesn't exist (this might rebuild modinfo).
 897          course_add_cm_to_section($course, $cmids[2], 2);
 898          $this->assertEquals(3, $DB->count_records('course_sections', array('course' => $course->id)));
 899          $sequence = $DB->get_field('course_sections', 'sequence', array('course' => $course->id, 'section' => 2));
 900          $this->assertEquals($cmids[2], $sequence);
 901  
 902          // Add using the 'before' option.
 903          course_add_cm_to_section($course, $cmids[3], 2, $cmids[2]);
 904          $this->assertEquals(3, $DB->count_records('course_sections', array('course' => $course->id)));
 905          $sequence = $DB->get_field('course_sections', 'sequence', array('course' => $course->id, 'section' => 2));
 906          $this->assertEquals($cmids[3] . ',' . $cmids[2], $sequence);
 907      }
 908  
 909      public function test_reorder_sections() {
 910          global $DB;
 911          $this->resetAfterTest(true);
 912  
 913          $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true));
 914          $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
 915          $oldsections = array();
 916          $sections = array();
 917          foreach ($DB->get_records('course_sections', array('course'=>$course->id), 'id') as $section) {
 918              $oldsections[$section->section] = $section->id;
 919              $sections[$section->id] = $section->section;
 920          }
 921          ksort($oldsections);
 922  
 923          $neworder = reorder_sections($sections, 2, 4);
 924          $neworder = array_keys($neworder);
 925          $this->assertEquals($oldsections[0], $neworder[0]);
 926          $this->assertEquals($oldsections[1], $neworder[1]);
 927          $this->assertEquals($oldsections[2], $neworder[4]);
 928          $this->assertEquals($oldsections[3], $neworder[2]);
 929          $this->assertEquals($oldsections[4], $neworder[3]);
 930          $this->assertEquals($oldsections[5], $neworder[5]);
 931          $this->assertEquals($oldsections[6], $neworder[6]);
 932  
 933          $neworder = reorder_sections($sections, 4, 2);
 934          $neworder = array_keys($neworder);
 935          $this->assertEquals($oldsections[0], $neworder[0]);
 936          $this->assertEquals($oldsections[1], $neworder[1]);
 937          $this->assertEquals($oldsections[2], $neworder[3]);
 938          $this->assertEquals($oldsections[3], $neworder[4]);
 939          $this->assertEquals($oldsections[4], $neworder[2]);
 940          $this->assertEquals($oldsections[5], $neworder[5]);
 941          $this->assertEquals($oldsections[6], $neworder[6]);
 942  
 943          $neworder = reorder_sections(1, 2, 4);
 944          $this->assertFalse($neworder);
 945      }
 946  
 947      public function test_move_section_down() {
 948          global $DB;
 949          $this->resetAfterTest(true);
 950  
 951          $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true));
 952          $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
 953          $oldsections = array();
 954          foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
 955              $oldsections[$section->section] = $section->id;
 956          }
 957          ksort($oldsections);
 958  
 959          // Test move section down..
 960          move_section_to($course, 2, 4);
 961          $sections = array();
 962          foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
 963              $sections[$section->section] = $section->id;
 964          }
 965          ksort($sections);
 966  
 967          $this->assertEquals($oldsections[0], $sections[0]);
 968          $this->assertEquals($oldsections[1], $sections[1]);
 969          $this->assertEquals($oldsections[2], $sections[4]);
 970          $this->assertEquals($oldsections[3], $sections[2]);
 971          $this->assertEquals($oldsections[4], $sections[3]);
 972          $this->assertEquals($oldsections[5], $sections[5]);
 973          $this->assertEquals($oldsections[6], $sections[6]);
 974      }
 975  
 976      public function test_move_section_up() {
 977          global $DB;
 978          $this->resetAfterTest(true);
 979  
 980          $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true));
 981          $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
 982          $oldsections = array();
 983          foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
 984              $oldsections[$section->section] = $section->id;
 985          }
 986          ksort($oldsections);
 987  
 988          // Test move section up..
 989          move_section_to($course, 6, 4);
 990          $sections = array();
 991          foreach ($DB->get_records('course_sections', array('course'=>$course->id)) as $section) {
 992              $sections[$section->section] = $section->id;
 993          }
 994          ksort($sections);
 995  
 996          $this->assertEquals($oldsections[0], $sections[0]);
 997          $this->assertEquals($oldsections[1], $sections[1]);
 998          $this->assertEquals($oldsections[2], $sections[2]);
 999          $this->assertEquals($oldsections[3], $sections[3]);
1000          $this->assertEquals($oldsections[4], $sections[5]);
1001          $this->assertEquals($oldsections[5], $sections[6]);
1002          $this->assertEquals($oldsections[6], $sections[4]);
1003      }
1004  
1005      public function test_move_section_marker() {
1006          global $DB;
1007          $this->resetAfterTest(true);
1008  
1009          $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections'=>true));
1010          $course = $this->getDataGenerator()->create_course(array('numsections'=>10), array('createsections'=>true));
1011  
1012          // Set course marker to the section we are going to move..
1013          course_set_marker($course->id, 2);
1014          // Verify that the course marker is set correctly.
1015          $course = $DB->get_record('course', array('id' => $course->id));
1016          $this->assertEquals(2, $course->marker);
1017  
1018          // Test move the marked section down..
1019          move_section_to($course, 2, 4);
1020  
1021          // Verify that the course marker has been moved along with the section..
1022          $course = $DB->get_record('course', array('id' => $course->id));
1023          $this->assertEquals(4, $course->marker);
1024  
1025          // Test move the marked section up..
1026          move_section_to($course, 4, 3);
1027  
1028          // Verify that the course marker has been moved along with the section..
1029          $course = $DB->get_record('course', array('id' => $course->id));
1030          $this->assertEquals(3, $course->marker);
1031  
1032          // Test moving a non-marked section above the marked section..
1033          move_section_to($course, 4, 2);
1034  
1035          // Verify that the course marker has been moved down to accomodate..
1036          $course = $DB->get_record('course', array('id' => $course->id));
1037          $this->assertEquals(4, $course->marker);
1038  
1039          // Test moving a non-marked section below the marked section..
1040          move_section_to($course, 3, 6);
1041  
1042          // Verify that the course marker has been up to accomodate..
1043          $course = $DB->get_record('course', array('id' => $course->id));
1044          $this->assertEquals(3, $course->marker);
1045      }
1046  
1047      /**
1048       * Test move_section_to method with caching
1049       *
1050       * @covers ::move_section_to
1051       * @return void
1052       */
1053      public function test_move_section_with_section_cache(): void {
1054          $this->resetAfterTest();
1055          $this->setAdminUser();
1056          $cache = cache::make('core', 'coursemodinfo');
1057  
1058          // Generate the course and pre-requisite module.
1059          $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
1060          // Reset course cache.
1061          rebuild_course_cache($course->id, true);
1062  
1063          // Build course cache.
1064          get_fast_modinfo($course->id);
1065          // Get the course modinfo cache.
1066          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1067          // Get the section cache.
1068          $sectioncaches = $coursemodinfo->sectioncache;
1069  
1070          // Make sure that we will have 4 section caches here.
1071          $this->assertCount(4, $sectioncaches);
1072          $this->assertArrayHasKey(0, $sectioncaches);
1073          $this->assertArrayHasKey(1, $sectioncaches);
1074          $this->assertArrayHasKey(2, $sectioncaches);
1075          $this->assertArrayHasKey(3, $sectioncaches);
1076  
1077          // Move section.
1078          move_section_to($course, 2, 3);
1079          // Get the course modinfo cache.
1080          $coursemodinfo = $cache->get_versioned($course->id, $course->cacherev);
1081          // Get the section cache.
1082          $sectioncaches = $coursemodinfo->sectioncache;
1083  
1084          // Make sure that we will have 2 section caches left.
1085          $this->assertCount(2, $sectioncaches);
1086          $this->assertArrayHasKey(0, $sectioncaches);
1087          $this->assertArrayHasKey(1, $sectioncaches);
1088          $this->assertArrayNotHasKey(2, $sectioncaches);
1089          $this->assertArrayNotHasKey(3, $sectioncaches);
1090      }
1091  
1092      /**
1093       * Test move_section_to method.
1094       * Make sure that we only update the moving sections, not all the sections in the current course.
1095       *
1096       * @covers ::move_section_to
1097       * @return void
1098       */
1099      public function test_move_section_to(): void {
1100          global $DB, $CFG;
1101          $this->resetAfterTest();
1102          $this->setAdminUser();
1103  
1104          // Generate the course and pre-requisite module.
1105          $course = $this->getDataGenerator()->create_course(['format' => 'topics', 'numsections' => 3], ['createsections' => true]);
1106  
1107          ob_start();
1108          $DB->set_debug(true);
1109          // Move section.
1110          move_section_to($course, 2, 3);
1111          $DB->set_debug(false);
1112          $debuginfo = ob_get_contents();
1113          ob_end_clean();
1114          $sectionmovequerycount = substr_count($debuginfo, 'UPDATE ' . $CFG->phpunit_prefix . 'course_sections SET');
1115          // We are updating the course_section table in steps to avoid breaking database uniqueness constraint.
1116          // So the queries will be doubled. See: course/lib.php:1423
1117          // Make sure that we only need 4 queries to update the position of section 2 and section 3.
1118          $this->assertEquals(4, $sectionmovequerycount);
1119      }
1120  
1121      public function test_course_can_delete_section() {
1122          global $DB;
1123          $this->resetAfterTest(true);
1124  
1125          $generator = $this->getDataGenerator();
1126  
1127          $courseweeks = $generator->create_course(
1128              array('numsections' => 5, 'format' => 'weeks'),
1129              array('createsections' => true));
1130          $assign1 = $generator->create_module('assign', array('course' => $courseweeks, 'section' => 1));
1131          $assign2 = $generator->create_module('assign', array('course' => $courseweeks, 'section' => 2));
1132  
1133          $coursetopics = $generator->create_course(
1134              array('numsections' => 5, 'format' => 'topics'),
1135              array('createsections' => true));
1136  
1137          $coursesingleactivity = $generator->create_course(
1138              array('format' => 'singleactivity'),
1139              array('createsections' => true));
1140  
1141          // Enrol student and teacher.
1142          $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
1143          $student = $generator->create_user();
1144          $teacher = $generator->create_user();
1145  
1146          $generator->enrol_user($student->id, $courseweeks->id, $roleids['student']);
1147          $generator->enrol_user($teacher->id, $courseweeks->id, $roleids['editingteacher']);
1148  
1149          $generator->enrol_user($student->id, $coursetopics->id, $roleids['student']);
1150          $generator->enrol_user($teacher->id, $coursetopics->id, $roleids['editingteacher']);
1151  
1152          $generator->enrol_user($student->id, $coursesingleactivity->id, $roleids['student']);
1153          $generator->enrol_user($teacher->id, $coursesingleactivity->id, $roleids['editingteacher']);
1154  
1155          // Teacher should be able to delete sections (except for 0) in topics and weeks format.
1156          $this->setUser($teacher);
1157  
1158          // For topics and weeks formats will return false for section 0 and true for any other section.
1159          $this->assertFalse(course_can_delete_section($courseweeks, 0));
1160          $this->assertTrue(course_can_delete_section($courseweeks, 1));
1161  
1162          $this->assertFalse(course_can_delete_section($coursetopics, 0));
1163          $this->assertTrue(course_can_delete_section($coursetopics, 1));
1164  
1165          // For singleactivity course format no section can be deleted.
1166          $this->assertFalse(course_can_delete_section($coursesingleactivity, 0));
1167          $this->assertFalse(course_can_delete_section($coursesingleactivity, 1));
1168  
1169          // Now let's revoke a capability from teacher to manage activity in section 1.
1170          $modulecontext = context_module::instance($assign1->cmid);
1171          assign_capability('moodle/course:manageactivities', CAP_PROHIBIT, $roleids['editingteacher'],
1172              $modulecontext);
1173          $this->assertFalse(course_can_delete_section($courseweeks, 1));
1174          $this->assertTrue(course_can_delete_section($courseweeks, 2));
1175  
1176          // Student does not have permissions to delete sections.
1177          $this->setUser($student);
1178          $this->assertFalse(course_can_delete_section($courseweeks, 1));
1179          $this->assertFalse(course_can_delete_section($coursetopics, 1));
1180          $this->assertFalse(course_can_delete_section($coursesingleactivity, 1));
1181      }
1182  
1183      public function test_course_delete_section() {
1184          global $DB;
1185          $this->resetAfterTest(true);
1186  
1187          $generator = $this->getDataGenerator();
1188  
1189          $course = $generator->create_course(array('numsections' => 6, 'format' => 'topics'),
1190              array('createsections' => true));
1191          $assign0 = $generator->create_module('assign', array('course' => $course, 'section' => 0));
1192          $assign1 = $generator->create_module('assign', array('course' => $course, 'section' => 1));
1193          $assign21 = $generator->create_module('assign', array('course' => $course, 'section' => 2));
1194          $assign22 = $generator->create_module('assign', array('course' => $course, 'section' => 2));
1195          $assign3 = $generator->create_module('assign', array('course' => $course, 'section' => 3));
1196          $assign5 = $generator->create_module('assign', array('course' => $course, 'section' => 5));
1197          $assign6 = $generator->create_module('assign', array('course' => $course, 'section' => 6));
1198  
1199          $this->setAdminUser();
1200  
1201          // Attempt to delete non-existing section.
1202          $this->assertFalse(course_delete_section($course, 10, false));
1203          $this->assertFalse(course_delete_section($course, 9, true));
1204  
1205          // Attempt to delete 0-section.
1206          $this->assertFalse(course_delete_section($course, 0, true));
1207          $this->assertTrue($DB->record_exists('course_modules', array('id' => $assign0->cmid)));
1208  
1209          // Delete last section.
1210          $this->assertTrue(course_delete_section($course, 6, true));
1211          $this->assertFalse($DB->record_exists('course_modules', array('id' => $assign6->cmid)));
1212          $this->assertEquals(5, course_get_format($course)->get_last_section_number());
1213  
1214          // Delete empty section.
1215          $this->assertTrue(course_delete_section($course, 4, false));
1216          $this->assertEquals(4, course_get_format($course)->get_last_section_number());
1217  
1218          // Delete section in the middle (2).
1219          $this->assertFalse(course_delete_section($course, 2, false));
1220          $this->assertEquals(4, course_get_format($course)->get_last_section_number());
1221          $this->assertTrue(course_delete_section($course, 2, true));
1222          $this->assertFalse($DB->record_exists('course_modules', array('id' => $assign21->cmid)));
1223          $this->assertFalse($DB->record_exists('course_modules', array('id' => $assign22->cmid)));
1224          $this->assertEquals(3, course_get_format($course)->get_last_section_number());
1225          $this->assertEquals(array(0 => array($assign0->cmid),
1226              1 => array($assign1->cmid),
1227              2 => array($assign3->cmid),
1228              3 => array($assign5->cmid)), get_fast_modinfo($course)->sections);
1229  
1230          // Remove marked section.
1231          course_set_marker($course->id, 1);
1232          $this->assertTrue(course_get_format($course)->is_section_current(1));
1233          $this->assertTrue(course_delete_section($course, 1, true));
1234          $this->assertFalse(course_get_format($course)->is_section_current(1));
1235      }
1236  
1237      public function test_get_course_display_name_for_list() {
1238          global $CFG;
1239          $this->resetAfterTest(true);
1240  
1241          $course = $this->getDataGenerator()->create_course(array('shortname' => 'FROG101', 'fullname' => 'Introduction to pond life'));
1242  
1243          $CFG->courselistshortnames = 0;
1244          $this->assertEquals('Introduction to pond life', get_course_display_name_for_list($course));
1245  
1246          $CFG->courselistshortnames = 1;
1247          $this->assertEquals('FROG101 Introduction to pond life', get_course_display_name_for_list($course));
1248      }
1249  
1250      public function test_move_module_in_course() {
1251          global $DB;
1252  
1253          $this->resetAfterTest(true);
1254          // Setup fixture
1255          $course = $this->getDataGenerator()->create_course(array('numsections'=>5), array('createsections' => true));
1256          $forum = $this->getDataGenerator()->create_module('forum', array('course'=>$course->id));
1257  
1258          $cms = get_fast_modinfo($course)->get_cms();
1259          $cm = reset($cms);
1260  
1261          $newsection = get_fast_modinfo($course)->get_section_info(3);
1262          $oldsectionid = $cm->section;
1263  
1264          // Perform the move
1265          moveto_module($cm, $newsection);
1266  
1267          $cms = get_fast_modinfo($course)->get_cms();
1268          $cm = reset($cms);
1269  
1270          // Check that the cached modinfo contains the correct section info
1271          $modinfo = get_fast_modinfo($course);
1272          $this->assertTrue(empty($modinfo->sections[0]));
1273          $this->assertFalse(empty($modinfo->sections[3]));
1274  
1275          // Check that the old section's sequence no longer contains this ID
1276          $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
1277          $oldsequences = explode(',', $newsection->sequence);
1278          $this->assertFalse(in_array($cm->id, $oldsequences));
1279  
1280          // Check that the new section's sequence now contains this ID
1281          $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
1282          $newsequences = explode(',', $newsection->sequence);
1283          $this->assertTrue(in_array($cm->id, $newsequences));
1284  
1285          // Check that the section number has been changed in the cm
1286          $this->assertEquals($newsection->id, $cm->section);
1287  
1288  
1289          // Perform a second move as some issues were only seen on the second move
1290          $newsection = get_fast_modinfo($course)->get_section_info(2);
1291          $oldsectionid = $cm->section;
1292          moveto_module($cm, $newsection);
1293  
1294          $cms = get_fast_modinfo($course)->get_cms();
1295          $cm = reset($cms);
1296  
1297          // Check that the cached modinfo contains the correct section info
1298          $modinfo = get_fast_modinfo($course);
1299          $this->assertTrue(empty($modinfo->sections[0]));
1300          $this->assertFalse(empty($modinfo->sections[2]));
1301  
1302          // Check that the old section's sequence no longer contains this ID
1303          $oldsection = $DB->get_record('course_sections', array('id' => $oldsectionid));
1304          $oldsequences = explode(',', $newsection->sequence);
1305          $this->assertFalse(in_array($cm->id, $oldsequences));
1306  
1307          // Check that the new section's sequence now contains this ID
1308          $newsection = $DB->get_record('course_sections', array('id' => $newsection->id));
1309          $newsequences = explode(',', $newsection->sequence);
1310          $this->assertTrue(in_array($cm->id, $newsequences));
1311      }
1312  
1313      public function test_module_visibility() {
1314          $this->setAdminUser();
1315          $this->resetAfterTest(true);
1316  
1317          // Create course and modules.
1318          $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
1319          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
1320          $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(), 'course' => $course->id));
1321          $modules = compact('forum', 'assign');
1322  
1323          // Hiding the modules.
1324          foreach ($modules as $mod) {
1325              set_coursemodule_visible($mod->cmid, 0);
1326              $this->check_module_visibility($mod, 0, 0);
1327          }
1328  
1329          // Showing the modules.
1330          foreach ($modules as $mod) {
1331              set_coursemodule_visible($mod->cmid, 1);
1332              $this->check_module_visibility($mod, 1, 1);
1333          }
1334      }
1335  
1336      /**
1337       * Test rebuildcache = false behaviour.
1338       *
1339       * When we pass rebuildcache = false to set_coursemodule_visible, the corusemodinfo cache will still contain
1340       * the original visibility until we trigger a rebuild.
1341       *
1342       * @return void
1343       * @covers ::set_coursemodule_visible
1344       */
1345      public function test_module_visibility_no_rebuild(): void {
1346          $this->setAdminUser();
1347          $this->resetAfterTest(true);
1348  
1349          // Create course and modules.
1350          $course = $this->getDataGenerator()->create_course(['numsections' => 5]);
1351          $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
1352          $assign = $this->getDataGenerator()->create_module('assign', ['duedate' => time(), 'course' => $course->id]);
1353          $modules = compact('forum', 'assign');
1354  
1355          // Hiding the modules.
1356          foreach ($modules as $mod) {
1357              set_coursemodule_visible($mod->cmid, 0, 1, false);
1358              // The modinfo cache still has the original visibility until we manually trigger a rebuild.
1359              $cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid);
1360              $this->assertEquals(1, $cm->visible);
1361          }
1362  
1363          rebuild_course_cache($course->id);
1364  
1365          foreach ($modules as $mod) {
1366              $this->check_module_visibility($mod, 0, 0);
1367          }
1368  
1369          // Showing the modules.
1370          foreach ($modules as $mod) {
1371              set_coursemodule_visible($mod->cmid, 1, 1, false);
1372              $cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid);
1373              $this->assertEquals(0, $cm->visible);
1374          }
1375  
1376          rebuild_course_cache($course->id);
1377  
1378          foreach ($modules as $mod) {
1379              $this->check_module_visibility($mod, 1, 1);
1380          }
1381      }
1382  
1383      public function test_section_visibility_events() {
1384          $this->setAdminUser();
1385          $this->resetAfterTest(true);
1386  
1387          $course = $this->getDataGenerator()->create_course(array('numsections' => 1), array('createsections' => true));
1388          $sectionnumber = 1;
1389          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
1390              array('section' => $sectionnumber));
1391          $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(),
1392              'course' => $course->id), array('section' => $sectionnumber));
1393          $sink = $this->redirectEvents();
1394          set_section_visible($course->id, $sectionnumber, 0);
1395          $events = $sink->get_events();
1396  
1397          // Extract the number of events related to what we are testing, other events
1398          // such as course_section_updated could have been triggered.
1399          $count = 0;
1400          foreach ($events as $event) {
1401              if ($event instanceof \core\event\course_module_updated) {
1402                  $count++;
1403              }
1404          }
1405          $this->assertSame(2, $count);
1406          $sink->close();
1407      }
1408  
1409      public function test_section_visibility() {
1410          $this->setAdminUser();
1411          $this->resetAfterTest(true);
1412  
1413          // Create course.
1414          $course = $this->getDataGenerator()->create_course(array('numsections' => 3), array('createsections' => true));
1415  
1416          $sink = $this->redirectEvents();
1417  
1418          // Testing an empty section.
1419          $sectionnumber = 1;
1420          set_section_visible($course->id, $sectionnumber, 0);
1421          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1422          $this->assertEquals($section_info->visible, 0);
1423          set_section_visible($course->id, $sectionnumber, 1);
1424          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1425          $this->assertEquals($section_info->visible, 1);
1426  
1427          // Checking that an event was fired.
1428          $events = $sink->get_events();
1429          $this->assertInstanceOf('\core\event\course_section_updated', $events[0]);
1430          $sink->close();
1431  
1432          // Testing a section with visible modules.
1433          $sectionnumber = 2;
1434          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
1435                  array('section' => $sectionnumber));
1436          $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(),
1437                  'course' => $course->id), array('section' => $sectionnumber));
1438          $modules = compact('forum', 'assign');
1439          set_section_visible($course->id, $sectionnumber, 0);
1440          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1441          $this->assertEquals($section_info->visible, 0);
1442          foreach ($modules as $mod) {
1443              $this->check_module_visibility($mod, 0, 1);
1444          }
1445          set_section_visible($course->id, $sectionnumber, 1);
1446          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1447          $this->assertEquals($section_info->visible, 1);
1448          foreach ($modules as $mod) {
1449              $this->check_module_visibility($mod, 1, 1);
1450          }
1451  
1452          // Testing a section with hidden modules, which should stay hidden.
1453          $sectionnumber = 3;
1454          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
1455                  array('section' => $sectionnumber));
1456          $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(),
1457                  'course' => $course->id), array('section' => $sectionnumber));
1458          $modules = compact('forum', 'assign');
1459          foreach ($modules as $mod) {
1460              set_coursemodule_visible($mod->cmid, 0);
1461              $this->check_module_visibility($mod, 0, 0);
1462          }
1463          set_section_visible($course->id, $sectionnumber, 0);
1464          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1465          $this->assertEquals($section_info->visible, 0);
1466          foreach ($modules as $mod) {
1467              $this->check_module_visibility($mod, 0, 0);
1468          }
1469          set_section_visible($course->id, $sectionnumber, 1);
1470          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1471          $this->assertEquals($section_info->visible, 1);
1472          foreach ($modules as $mod) {
1473              $this->check_module_visibility($mod, 0, 0);
1474          }
1475      }
1476  
1477      /**
1478       * Helper function to assert that a module has correctly been made visible, or hidden.
1479       *
1480       * @param stdClass $mod module information
1481       * @param int $visibility the current state of the module
1482       * @param int $visibleold the current state of the visibleold property
1483       * @return void
1484       */
1485      public function check_module_visibility($mod, $visibility, $visibleold) {
1486          global $DB;
1487          $cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid);
1488          $this->assertEquals($visibility, $cm->visible);
1489          $this->assertEquals($visibleold, $cm->visibleold);
1490  
1491          // Check the module grade items.
1492          $grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $cm->modname,
1493                  'iteminstance' => $cm->instance, 'courseid' => $cm->course));
1494          if ($grade_items) {
1495              foreach ($grade_items as $grade_item) {
1496                  if ($visibility) {
1497                      $this->assertFalse($grade_item->is_hidden(), "$cm->modname grade_item not visible");
1498                  } else {
1499                      $this->assertTrue($grade_item->is_hidden(), "$cm->modname grade_item not hidden");
1500                  }
1501              }
1502          }
1503  
1504          // Check the events visibility.
1505          if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $cm->modname))) {
1506              foreach ($events as $event) {
1507                  $calevent = new calendar_event($event);
1508                  $this->assertEquals($visibility, $calevent->visible, "$cm->modname calendar_event visibility");
1509              }
1510          }
1511      }
1512  
1513      public function test_course_page_type_list() {
1514          global $DB;
1515          $this->resetAfterTest(true);
1516  
1517          // Create a category.
1518          $category = new stdClass();
1519          $category->name = 'Test Category';
1520  
1521          $testcategory = $this->getDataGenerator()->create_category($category);
1522  
1523          // Create a course.
1524          $course = new stdClass();
1525          $course->fullname = 'Apu loves Unit Təsts';
1526          $course->shortname = 'Spread the lŭve';
1527          $course->idnumber = '123';
1528          $course->summary = 'Awesome!';
1529          $course->summaryformat = FORMAT_PLAIN;
1530          $course->format = 'topics';
1531          $course->newsitems = 0;
1532          $course->numsections = 5;
1533          $course->category = $testcategory->id;
1534  
1535          $testcourse = $this->getDataGenerator()->create_course($course);
1536  
1537          // Create contexts.
1538          $coursecontext = context_course::instance($testcourse->id);
1539          $parentcontext = $coursecontext->get_parent_context(); // Not actually used.
1540          $pagetype = 'page-course-x'; // Not used either.
1541          $pagetypelist = course_page_type_list($pagetype, $parentcontext, $coursecontext);
1542  
1543          // Page type lists for normal courses.
1544          $testpagetypelist1 = array();
1545          $testpagetypelist1['*'] = 'Any page';
1546          $testpagetypelist1['course-*'] = 'Any course page';
1547          $testpagetypelist1['course-view-*'] = 'Any type of course main page';
1548  
1549          $this->assertEquals($testpagetypelist1, $pagetypelist);
1550  
1551          // Get the context for the front page course.
1552          $sitecoursecontext = context_course::instance(SITEID);
1553          $pagetypelist = course_page_type_list($pagetype, $parentcontext, $sitecoursecontext);
1554  
1555          // Page type list for the front page course.
1556          $testpagetypelist2 = array('*' => 'Any page');
1557          $this->assertEquals($testpagetypelist2, $pagetypelist);
1558  
1559          // Make sure that providing no current context to the function doesn't result in an error.
1560          // Calls made from generate_page_type_patterns() may provide null values.
1561          $pagetypelist = course_page_type_list($pagetype, null, null);
1562          $this->assertEquals($pagetypelist, $testpagetypelist1);
1563      }
1564  
1565      public function test_compare_activities_by_time_desc() {
1566  
1567          // Let's create some test data.
1568          $activitiesivities = array();
1569          $x = new stdClass();
1570          $x->timestamp = null;
1571          $activities[] = $x;
1572  
1573          $x = new stdClass();
1574          $x->timestamp = 1;
1575          $activities[] = $x;
1576  
1577          $x = new stdClass();
1578          $x->timestamp = 3;
1579          $activities[] = $x;
1580  
1581          $x = new stdClass();
1582          $x->timestamp = 0;
1583          $activities[] = $x;
1584  
1585          $x = new stdClass();
1586          $x->timestamp = 5;
1587          $activities[] = $x;
1588  
1589          $x = new stdClass();
1590          $activities[] = $x;
1591  
1592          $x = new stdClass();
1593          $x->timestamp = 5;
1594          $activities[] = $x;
1595  
1596          // Do the sorting.
1597          usort($activities, 'compare_activities_by_time_desc');
1598  
1599          // Let's check the result.
1600          $last = 10;
1601          foreach($activities as $activity) {
1602              if (empty($activity->timestamp)) {
1603                  $activity->timestamp = 0;
1604              }
1605              $this->assertLessThanOrEqual($last, $activity->timestamp);
1606          }
1607      }
1608  
1609      public function test_compare_activities_by_time_asc() {
1610  
1611          // Let's create some test data.
1612          $activities = array();
1613          $x = new stdClass();
1614          $x->timestamp = null;
1615          $activities[] = $x;
1616  
1617          $x = new stdClass();
1618          $x->timestamp = 1;
1619          $activities[] = $x;
1620  
1621          $x = new stdClass();
1622          $x->timestamp = 3;
1623          $activities[] = $x;
1624  
1625          $x = new stdClass();
1626          $x->timestamp = 0;
1627          $activities[] = $x;
1628  
1629          $x = new stdClass();
1630          $x->timestamp = 5;
1631          $activities[] = $x;
1632  
1633          $x = new stdClass();
1634          $activities[] = $x;
1635  
1636          $x = new stdClass();
1637          $x->timestamp = 5;
1638          $activities[] = $x;
1639  
1640          // Do the sorting.
1641          usort($activities, 'compare_activities_by_time_asc');
1642  
1643          // Let's check the result.
1644          $last = 0;
1645          foreach($activities as $activity) {
1646              if (empty($activity->timestamp)) {
1647                  $activity->timestamp = 0;
1648              }
1649              $this->assertGreaterThanOrEqual($last, $activity->timestamp);
1650          }
1651      }
1652  
1653      /**
1654       * Tests moving a module between hidden/visible sections and
1655       * verifies that the course/module visiblity seettings are
1656       * retained.
1657       */
1658      public function test_moveto_module_between_hidden_sections() {
1659          global $DB;
1660  
1661          $this->resetAfterTest(true);
1662  
1663          $course = $this->getDataGenerator()->create_course(array('numsections' => 4), array('createsections' => true));
1664          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
1665          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1666          $quiz= $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
1667  
1668          // Set the page as hidden
1669          set_coursemodule_visible($page->cmid, 0);
1670  
1671          // Set sections 3 as hidden.
1672          set_section_visible($course->id, 3, 0);
1673  
1674          $modinfo = get_fast_modinfo($course);
1675  
1676          $hiddensection = $modinfo->get_section_info(3);
1677          // New section is definitely not visible:
1678          $this->assertEquals($hiddensection->visible, 0);
1679  
1680          $forumcm = $modinfo->cms[$forum->cmid];
1681          $pagecm = $modinfo->cms[$page->cmid];
1682  
1683          // Move the forum and the page to a hidden section, make sure moveto_module returns 0 as new visibility state.
1684          $this->assertEquals(0, moveto_module($forumcm, $hiddensection));
1685          $this->assertEquals(0, moveto_module($pagecm, $hiddensection));
1686  
1687          $modinfo = get_fast_modinfo($course);
1688  
1689          // Verify that forum and page have been moved to the hidden section and quiz has not.
1690          $this->assertContainsEquals($forum->cmid, $modinfo->sections[3]);
1691          $this->assertContainsEquals($page->cmid, $modinfo->sections[3]);
1692          $this->assertNotContainsEquals($quiz->cmid, $modinfo->sections[3]);
1693  
1694          // Verify that forum has been made invisible.
1695          $forumcm = $modinfo->cms[$forum->cmid];
1696          $this->assertEquals($forumcm->visible, 0);
1697          // Verify that old state has been retained.
1698          $this->assertEquals($forumcm->visibleold, 1);
1699  
1700          // Verify that page has stayed invisible.
1701          $pagecm = $modinfo->cms[$page->cmid];
1702          $this->assertEquals($pagecm->visible, 0);
1703          // Verify that old state has been retained.
1704          $this->assertEquals($pagecm->visibleold, 0);
1705  
1706          // Verify that quiz has been unaffected.
1707          $quizcm = $modinfo->cms[$quiz->cmid];
1708          $this->assertEquals($quizcm->visible, 1);
1709  
1710          // Move forum and page back to visible section.
1711          // Make sure the visibility is restored to the original value (visible for forum and hidden for page).
1712          $visiblesection = $modinfo->get_section_info(2);
1713          $this->assertEquals(1, moveto_module($forumcm, $visiblesection));
1714          $this->assertEquals(0, moveto_module($pagecm, $visiblesection));
1715  
1716          $modinfo = get_fast_modinfo($course);
1717  
1718          // Double check that forum has been made visible.
1719          $forumcm = $modinfo->cms[$forum->cmid];
1720          $this->assertEquals($forumcm->visible, 1);
1721  
1722          // Double check that page has stayed invisible.
1723          $pagecm = $modinfo->cms[$page->cmid];
1724          $this->assertEquals($pagecm->visible, 0);
1725  
1726          // Move the page in the same section (this is what mod duplicate does).
1727          // Visibility of page remains 0.
1728          $this->assertEquals(0, moveto_module($pagecm, $visiblesection, $forumcm));
1729  
1730          // Double check that the the page is still hidden.
1731          $modinfo = get_fast_modinfo($course);
1732          $pagecm = $modinfo->cms[$page->cmid];
1733          $this->assertEquals($pagecm->visible, 0);
1734      }
1735  
1736      /**
1737       * Tests moving a module around in the same section. moveto_module()
1738       * is called this way in modduplicate.
1739       */
1740      public function test_moveto_module_in_same_section() {
1741          global $DB;
1742  
1743          $this->resetAfterTest(true);
1744  
1745          $course = $this->getDataGenerator()->create_course(array('numsections' => 3), array('createsections' => true));
1746          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1747          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
1748  
1749          // Simulate inconsistent visible/visibleold values (MDL-38713).
1750          $cm = $DB->get_record('course_modules', array('id' => $page->cmid), '*', MUST_EXIST);
1751          $cm->visible = 0;
1752          $cm->visibleold = 1;
1753          $DB->update_record('course_modules', $cm);
1754  
1755          $modinfo = get_fast_modinfo($course);
1756          $forumcm = $modinfo->cms[$forum->cmid];
1757          $pagecm = $modinfo->cms[$page->cmid];
1758  
1759          // Verify that page is hidden.
1760          $this->assertEquals($pagecm->visible, 0);
1761  
1762          // Verify section 0 is where all mods added.
1763          $section = $modinfo->get_section_info(0);
1764          $this->assertEquals($section->id, $forumcm->section);
1765          $this->assertEquals($section->id, $pagecm->section);
1766  
1767  
1768          // Move the page inside the hidden section. Make sure it is hidden.
1769          $this->assertEquals(0, moveto_module($pagecm, $section, $forumcm));
1770  
1771          // Double check that the the page is still hidden.
1772          $modinfo = get_fast_modinfo($course);
1773          $pagecm = $modinfo->cms[$page->cmid];
1774          $this->assertEquals($pagecm->visible, 0);
1775      }
1776  
1777      /**
1778       * Tests the function that deletes a course module
1779       *
1780       * @param string $type The type of module for the test
1781       * @param array $options The options for the module creation
1782       * @dataProvider provider_course_delete_module
1783       */
1784      public function test_course_delete_module($type, $options) {
1785          global $DB;
1786  
1787          $this->resetAfterTest(true);
1788          $this->setAdminUser();
1789  
1790          // Create course and modules.
1791          $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
1792          $options['course'] = $course->id;
1793  
1794          // Generate an assignment with due date (will generate a course event).
1795          $module = $this->getDataGenerator()->create_module($type, $options);
1796  
1797          // Get the module context.
1798          $modcontext = context_module::instance($module->cmid);
1799  
1800          $assocblog = $this->create_module_asscociated_blog($course, $modcontext);
1801  
1802          // Verify context exists.
1803          $this->assertInstanceOf('context_module', $modcontext);
1804  
1805          // Make module specific messes.
1806          switch ($type) {
1807              case 'assign':
1808                  // Add some tags to this assignment.
1809                  core_tag_tag::set_item_tags('mod_assign', 'assign', $module->id, $modcontext, array('Tag 1', 'Tag 2', 'Tag 3'));
1810                  core_tag_tag::set_item_tags('core', 'course_modules', $module->cmid, $modcontext, array('Tag 3', 'Tag 4', 'Tag 5'));
1811  
1812                  // Confirm the tag instances were added.
1813                  $criteria = array('component' => 'mod_assign', 'itemtype' => 'assign', 'contextid' => $modcontext->id);
1814                  $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
1815                  $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
1816                  $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
1817  
1818                  // Verify event assignment event has been generated.
1819                  $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type));
1820                  $this->assertEquals(1, $eventcount);
1821  
1822                  break;
1823              case 'quiz':
1824                  $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
1825                  $qcat = $qgen->create_question_category(array('contextid' => $modcontext->id));
1826                  $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
1827                  $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
1828                  break;
1829              default:
1830                  break;
1831          }
1832  
1833          // Run delete..
1834          course_delete_module($module->cmid);
1835  
1836          // Verify the context has been removed.
1837          $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
1838  
1839          // Verify the course_module record has been deleted.
1840          $cmcount = $DB->count_records('course_modules', array('id' => $module->cmid));
1841          $this->assertEmpty($cmcount);
1842  
1843          // Verify the blog_association record has been deleted.
1844          $this->assertCount(0, $DB->get_records('blog_association',
1845                  array('contextid' => $modcontext->id)));
1846  
1847          // Verify the blog post record has been deleted.
1848          $this->assertCount(0, $DB->get_records('post',
1849                  array('id' => $assocblog->id)));
1850  
1851          // Verify the tag instance record has been deleted.
1852          $this->assertCount(0, $DB->get_records('tag_instance',
1853                  array('itemid' => $assocblog->id)));
1854  
1855          // Test clean up of module specific messes.
1856          switch ($type) {
1857              case 'assign':
1858                  // Verify event assignment events have been removed.
1859                  $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type));
1860                  $this->assertEmpty($eventcount);
1861  
1862                  // Verify the tag instances were deleted.
1863                  $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
1864                  $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
1865  
1866                  $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
1867                  $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
1868                  break;
1869              case 'quiz':
1870                  // Verify category deleted.
1871                  $criteria = array('contextid' => $modcontext->id);
1872                  $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
1873  
1874                  // Verify questions deleted.
1875                  $criteria = [$qcat->id];
1876                  $sql = 'SELECT COUNT(q.id)
1877                            FROM {question} q
1878                            JOIN {question_versions} qv ON qv.questionid = q.id
1879                            JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
1880                            WHERE qbe.questioncategoryid = ?';
1881                  $this->assertEquals(0, $DB->count_records_sql($sql, $criteria));
1882                  break;
1883              default:
1884                  break;
1885          }
1886      }
1887  
1888      /**
1889       * Test that triggering a course_created event works as expected.
1890       */
1891      public function test_course_created_event() {
1892          global $DB;
1893  
1894          $this->resetAfterTest();
1895  
1896          // Catch the events.
1897          $sink = $this->redirectEvents();
1898  
1899          // Create the course with an id number which is used later when generating a course via the imsenterprise plugin.
1900          $data = new stdClass();
1901          $data->idnumber = 'idnumber';
1902          $course = $this->getDataGenerator()->create_course($data);
1903          // Get course from DB for comparison.
1904          $course = $DB->get_record('course', array('id' => $course->id));
1905  
1906          // Capture the event.
1907          $events = $sink->get_events();
1908          $sink->close();
1909  
1910          // Validate the event.
1911          $event = $events[0];
1912          $this->assertInstanceOf('\core\event\course_created', $event);
1913          $this->assertEquals('course', $event->objecttable);
1914          $this->assertEquals($course->id, $event->objectid);
1915          $this->assertEquals(context_course::instance($course->id), $event->get_context());
1916          $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
1917  
1918          // Now we want to trigger creating a course via the imsenterprise.
1919          // Delete the course we created earlier, as we want the imsenterprise plugin to create this.
1920          // We do not want print out any of the text this function generates while doing this, which is why
1921          // we are using ob_start() and ob_end_clean().
1922          ob_start();
1923          delete_course($course);
1924          ob_end_clean();
1925  
1926          // Create the XML file we want to use.
1927          $course->category = (array)$course->category;
1928          $imstestcase = new imsenterprise_test();
1929          $imstestcase->imsplugin = enrol_get_plugin('imsenterprise');
1930          $imstestcase->set_test_config();
1931          $imstestcase->set_xml_file(false, array($course));
1932  
1933          // Capture the event.
1934          $sink = $this->redirectEvents();
1935          $imstestcase->imsplugin->cron();
1936          $events = $sink->get_events();
1937          $sink->close();
1938          $event = null;
1939          foreach ($events as $eventinfo) {
1940              if ($eventinfo instanceof \core\event\course_created ) {
1941                  $event = $eventinfo;
1942                  break;
1943              }
1944          }
1945  
1946          // Validate the event triggered is \core\event\course_created. There is no need to validate the other values
1947          // as they have already been validated in the previous steps. Here we only want to make sure that when the
1948          // imsenterprise plugin creates a course an event is triggered.
1949          $this->assertInstanceOf('\core\event\course_created', $event);
1950          $this->assertEventContextNotUsed($event);
1951      }
1952  
1953      /**
1954       * Test that triggering a course_updated event works as expected.
1955       */
1956      public function test_course_updated_event() {
1957          global $DB;
1958  
1959          $this->resetAfterTest();
1960  
1961          // Create a course.
1962          $course = $this->getDataGenerator()->create_course();
1963  
1964          // Create a category we are going to move this course to.
1965          $category = $this->getDataGenerator()->create_category();
1966  
1967          // Create a hidden category we are going to move this course to.
1968          $categoryhidden = $this->getDataGenerator()->create_category(array('visible' => 0));
1969  
1970          // Update course and catch course_updated event.
1971          $sink = $this->redirectEvents();
1972          update_course($course);
1973          $events = $sink->get_events();
1974          $sink->close();
1975  
1976          // Get updated course information from the DB.
1977          $updatedcourse = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
1978          // Validate event.
1979          $event = array_shift($events);
1980          $this->assertInstanceOf('\core\event\course_updated', $event);
1981          $this->assertEquals('course', $event->objecttable);
1982          $this->assertEquals($updatedcourse->id, $event->objectid);
1983          $this->assertEquals(context_course::instance($course->id), $event->get_context());
1984          $url = new moodle_url('/course/edit.php', array('id' => $event->objectid));
1985          $this->assertEquals($url, $event->get_url());
1986          $this->assertEquals($updatedcourse, $event->get_record_snapshot('course', $event->objectid));
1987  
1988          // Move course and catch course_updated event.
1989          $sink = $this->redirectEvents();
1990          move_courses(array($course->id), $category->id);
1991          $events = $sink->get_events();
1992          $sink->close();
1993  
1994          // Return the moved course information from the DB.
1995          $movedcourse = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
1996          // Validate event.
1997          $event = array_shift($events);
1998          $this->assertInstanceOf('\core\event\course_updated', $event);
1999          $this->assertEquals('course', $event->objecttable);
2000          $this->assertEquals($movedcourse->id, $event->objectid);
2001          $this->assertEquals(context_course::instance($course->id), $event->get_context());
2002          $this->assertEquals($movedcourse, $event->get_record_snapshot('course', $movedcourse->id));
2003  
2004          // Move course to hidden category and catch course_updated event.
2005          $sink = $this->redirectEvents();
2006          move_courses(array($course->id), $categoryhidden->id);
2007          $events = $sink->get_events();
2008          $sink->close();
2009  
2010          // Return the moved course information from the DB.
2011          $movedcoursehidden = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
2012          // Validate event.
2013          $event = array_shift($events);
2014          $this->assertInstanceOf('\core\event\course_updated', $event);
2015          $this->assertEquals('course', $event->objecttable);
2016          $this->assertEquals($movedcoursehidden->id, $event->objectid);
2017          $this->assertEquals(context_course::instance($course->id), $event->get_context());
2018          $this->assertEquals($movedcoursehidden, $event->get_record_snapshot('course', $movedcoursehidden->id));
2019          $this->assertEventContextNotUsed($event);
2020      }
2021  
2022      /**
2023       * Test that triggering a course_updated event logs changes.
2024       */
2025      public function test_course_updated_event_with_changes() {
2026          global $DB;
2027  
2028          $this->resetAfterTest();
2029  
2030          // Create a course.
2031          $course = $this->getDataGenerator()->create_course((object)['visible' => 1]);
2032  
2033          $editedcourse = $DB->get_record('course', ['id' => $course->id]);
2034          $editedcourse->visible = 0;
2035  
2036          // Update course and catch course_updated event.
2037          $sink = $this->redirectEvents();
2038          update_course($editedcourse);
2039          $events = $sink->get_events();
2040          $sink->close();
2041  
2042          $event = array_shift($events);
2043          $this->assertInstanceOf('\core\event\course_updated', $event);
2044          $otherdata = [
2045              'shortname' => $course->shortname,
2046              'fullname' => $course->fullname,
2047              'updatedfields' => [
2048                  'visible' => 0
2049              ]
2050          ];
2051          $this->assertEquals($otherdata, $event->other);
2052  
2053      }
2054  
2055      /**
2056       * Test that triggering a course_deleted event works as expected.
2057       */
2058      public function test_course_deleted_event() {
2059          $this->resetAfterTest();
2060  
2061          // Create the course.
2062          $course = $this->getDataGenerator()->create_course();
2063  
2064          // Save the course context before we delete the course.
2065          $coursecontext = context_course::instance($course->id);
2066  
2067          // Catch the update event.
2068          $sink = $this->redirectEvents();
2069  
2070          // Call delete_course() which will trigger the course_deleted event and the course_content_deleted
2071          // event. This function prints out data to the screen, which we do not want during a PHPUnit test,
2072          // so use ob_start and ob_end_clean to prevent this.
2073          ob_start();
2074          delete_course($course);
2075          ob_end_clean();
2076  
2077          // Capture the event.
2078          $events = $sink->get_events();
2079          $sink->close();
2080  
2081          // Validate the event.
2082          $event = array_pop($events);
2083          $this->assertInstanceOf('\core\event\course_deleted', $event);
2084          $this->assertEquals('course', $event->objecttable);
2085          $this->assertEquals($course->id, $event->objectid);
2086          $this->assertEquals($coursecontext->id, $event->contextid);
2087          $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
2088          $eventdata = $event->get_data();
2089          $this->assertSame($course->idnumber, $eventdata['other']['idnumber']);
2090          $this->assertSame($course->fullname, $eventdata['other']['fullname']);
2091          $this->assertSame($course->shortname, $eventdata['other']['shortname']);
2092  
2093          $this->assertEventContextNotUsed($event);
2094      }
2095  
2096      /**
2097       * Test that triggering a course_content_deleted event works as expected.
2098       */
2099      public function test_course_content_deleted_event() {
2100          global $DB;
2101  
2102          $this->resetAfterTest();
2103  
2104          // Create the course.
2105          $course = $this->getDataGenerator()->create_course();
2106  
2107          // Get the course from the DB. The data generator adds some extra properties, such as
2108          // numsections, to the course object which will fail the assertions later on.
2109          $course = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
2110  
2111          // Save the course context before we delete the course.
2112          $coursecontext = context_course::instance($course->id);
2113  
2114          // Catch the update event.
2115          $sink = $this->redirectEvents();
2116  
2117          remove_course_contents($course->id, false);
2118  
2119          // Capture the event.
2120          $events = $sink->get_events();
2121          $sink->close();
2122  
2123          // Validate the event.
2124          $event = array_pop($events);
2125          $this->assertInstanceOf('\core\event\course_content_deleted', $event);
2126          $this->assertEquals('course', $event->objecttable);
2127          $this->assertEquals($course->id, $event->objectid);
2128          $this->assertEquals($coursecontext->id, $event->contextid);
2129          $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
2130          $this->assertEventContextNotUsed($event);
2131      }
2132  
2133      /**
2134       * Test that triggering a course_category_deleted event works as expected.
2135       */
2136      public function test_course_category_deleted_event() {
2137          $this->resetAfterTest();
2138  
2139          // Create a category.
2140          $category = $this->getDataGenerator()->create_category();
2141  
2142          // Save the original record/context before it is deleted.
2143          $categoryrecord = $category->get_db_record();
2144          $categorycontext = context_coursecat::instance($category->id);
2145  
2146          // Catch the update event.
2147          $sink = $this->redirectEvents();
2148  
2149          // Delete the category.
2150          $category->delete_full();
2151  
2152          // Capture the event.
2153          $events = $sink->get_events();
2154          $sink->close();
2155  
2156          // Validate the event.
2157          $event = $events[0];
2158          $this->assertInstanceOf('\core\event\course_category_deleted', $event);
2159          $this->assertEquals('course_categories', $event->objecttable);
2160          $this->assertEquals($category->id, $event->objectid);
2161          $this->assertEquals($categorycontext->id, $event->contextid);
2162          $this->assertEquals([
2163              'name' => $category->name,
2164          ], $event->other);
2165          $this->assertEquals($categoryrecord, $event->get_record_snapshot($event->objecttable, $event->objectid));
2166          $this->assertEquals(null, $event->get_url());
2167  
2168          // Create two categories.
2169          $category = $this->getDataGenerator()->create_category();
2170          $category2 = $this->getDataGenerator()->create_category();
2171  
2172          // Save the original record/context before it is moved and then deleted.
2173          $category2record = $category2->get_db_record();
2174          $category2context = context_coursecat::instance($category2->id);
2175  
2176          // Catch the update event.
2177          $sink = $this->redirectEvents();
2178  
2179          // Move the category.
2180          $category2->delete_move($category->id);
2181  
2182          // Capture the event.
2183          $events = $sink->get_events();
2184          $sink->close();
2185  
2186          // Validate the event.
2187          $event = $events[0];
2188          $this->assertInstanceOf('\core\event\course_category_deleted', $event);
2189          $this->assertEquals('course_categories', $event->objecttable);
2190          $this->assertEquals($category2->id, $event->objectid);
2191          $this->assertEquals($category2context->id, $event->contextid);
2192          $this->assertEquals([
2193              'name' => $category2->name,
2194              'contentmovedcategoryid' => $category->id,
2195          ], $event->other);
2196          $this->assertEquals($category2record, $event->get_record_snapshot($event->objecttable, $event->objectid));
2197          $this->assertEventContextNotUsed($event);
2198      }
2199  
2200      /**
2201       * Test that triggering a course_backup_created event works as expected.
2202       */
2203      public function test_course_backup_created_event() {
2204          global $CFG;
2205  
2206          // Get the necessary files to perform backup and restore.
2207          require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
2208          require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2209  
2210          $this->resetAfterTest();
2211  
2212          // Set to admin user.
2213          $this->setAdminUser();
2214  
2215          // The user id is going to be 2 since we are the admin user.
2216          $userid = 2;
2217  
2218          // Create a course.
2219          $course = $this->getDataGenerator()->create_course();
2220  
2221          // Create backup file and save it to the backup location.
2222          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
2223              backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userid);
2224          $sink = $this->redirectEvents();
2225          $bc->execute_plan();
2226  
2227          // Capture the event.
2228          $events = $sink->get_events();
2229          $sink->close();
2230  
2231          // Validate the event.
2232          $event = array_pop($events);
2233          $this->assertInstanceOf('\core\event\course_backup_created', $event);
2234          $this->assertEquals('course', $event->objecttable);
2235          $this->assertEquals($bc->get_courseid(), $event->objectid);
2236          $this->assertEquals(context_course::instance($bc->get_courseid())->id, $event->contextid);
2237  
2238          $url = new moodle_url('/course/view.php', array('id' => $event->objectid));
2239          $this->assertEquals($url, $event->get_url());
2240          $this->assertEventContextNotUsed($event);
2241  
2242          // Destroy the resource controller since we are done using it.
2243          $bc->destroy();
2244      }
2245  
2246      /**
2247       * Test that triggering a course_restored event works as expected.
2248       */
2249      public function test_course_restored_event() {
2250          global $CFG;
2251  
2252          // Get the necessary files to perform backup and restore.
2253          require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
2254          require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2255  
2256          $this->resetAfterTest();
2257  
2258          // Set to admin user.
2259          $this->setAdminUser();
2260  
2261          // The user id is going to be 2 since we are the admin user.
2262          $userid = 2;
2263  
2264          // Create a course.
2265          $course = $this->getDataGenerator()->create_course();
2266  
2267          // Create backup file and save it to the backup location.
2268          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
2269              backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userid);
2270          $bc->execute_plan();
2271          $results = $bc->get_results();
2272          $file = $results['backup_destination'];
2273          $fp = get_file_packer('application/vnd.moodle.backup');
2274          $filepath = $CFG->dataroot . '/temp/backup/test-restore-course-event';
2275          $file->extract_to_pathname($fp, $filepath);
2276          $bc->destroy();
2277  
2278          // Now we want to catch the restore course event.
2279          $sink = $this->redirectEvents();
2280  
2281          // Now restore the course to trigger the event.
2282          $rc = new restore_controller('test-restore-course-event', $course->id, backup::INTERACTIVE_NO,
2283              backup::MODE_GENERAL, $userid, backup::TARGET_NEW_COURSE);
2284          $rc->execute_precheck();
2285          $rc->execute_plan();
2286  
2287          // Capture the event.
2288          $events = $sink->get_events();
2289          $sink->close();
2290  
2291          // Validate the event.
2292          $event = array_pop($events);
2293          $this->assertInstanceOf('\core\event\course_restored', $event);
2294          $this->assertEquals('course', $event->objecttable);
2295          $this->assertEquals($rc->get_courseid(), $event->objectid);
2296          $this->assertEquals(context_course::instance($rc->get_courseid())->id, $event->contextid);
2297          $this->assertEventContextNotUsed($event);
2298  
2299          // Destroy the resource controller since we are done using it.
2300          $rc->destroy();
2301      }
2302  
2303      /**
2304       * Test that triggering a course_section_updated event works as expected.
2305       */
2306      public function test_course_section_updated_event() {
2307          global $DB;
2308  
2309          $this->resetAfterTest();
2310  
2311          // Create the course with sections.
2312          $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
2313          $sections = $DB->get_records('course_sections', array('course' => $course->id));
2314  
2315          $coursecontext = context_course::instance($course->id);
2316  
2317          $section = array_pop($sections);
2318          $section->name = 'Test section';
2319          $section->summary = 'Test section summary';
2320          $DB->update_record('course_sections', $section);
2321  
2322          // Trigger an event for course section update.
2323          $event = \core\event\course_section_updated::create(
2324                  array(
2325                      'objectid' => $section->id,
2326                      'courseid' => $course->id,
2327                      'context' => context_course::instance($course->id),
2328                      'other' => array(
2329                          'sectionnum' => $section->section
2330                      )
2331                  )
2332              );
2333          $event->add_record_snapshot('course_sections', $section);
2334          // Trigger and catch event.
2335          $sink = $this->redirectEvents();
2336          $event->trigger();
2337          $events = $sink->get_events();
2338          $sink->close();
2339  
2340          // Validate the event.
2341          $event = $events[0];
2342          $this->assertInstanceOf('\core\event\course_section_updated', $event);
2343          $this->assertEquals('course_sections', $event->objecttable);
2344          $this->assertEquals($section->id, $event->objectid);
2345          $this->assertEquals($course->id, $event->courseid);
2346          $this->assertEquals($coursecontext->id, $event->contextid);
2347          $this->assertEquals($section->section, $event->other['sectionnum']);
2348          $expecteddesc = "The user with id '{$event->userid}' updated section number '{$event->other['sectionnum']}' for the course with id '{$event->courseid}'";
2349          $this->assertEquals($expecteddesc, $event->get_description());
2350          $url = new moodle_url('/course/editsection.php', array('id' => $event->objectid));
2351          $this->assertEquals($url, $event->get_url());
2352          $this->assertEquals($section, $event->get_record_snapshot('course_sections', $event->objectid));
2353          $id = $section->id;
2354          $sectionnum = $section->section;
2355          $this->assertEventContextNotUsed($event);
2356      }
2357  
2358      /**
2359       * Test that triggering a course_section_deleted event works as expected.
2360       */
2361      public function test_course_section_deleted_event() {
2362          global $USER, $DB;
2363          $this->resetAfterTest();
2364          $sink = $this->redirectEvents();
2365  
2366          // Create the course with sections.
2367          $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
2368          $sections = $DB->get_records('course_sections', array('course' => $course->id), 'section');
2369          $coursecontext = context_course::instance($course->id);
2370          $section = array_pop($sections);
2371          course_delete_section($course, $section);
2372          $events = $sink->get_events();
2373          $event = array_pop($events); // Delete section event.
2374          $sink->close();
2375  
2376          // Validate event data.
2377          $this->assertInstanceOf('\core\event\course_section_deleted', $event);
2378          $this->assertEquals('course_sections', $event->objecttable);
2379          $this->assertEquals($section->id, $event->objectid);
2380          $this->assertEquals($course->id, $event->courseid);
2381          $this->assertEquals($coursecontext->id, $event->contextid);
2382          $this->assertEquals($section->section, $event->other['sectionnum']);
2383          $expecteddesc = "The user with id '{$event->userid}' deleted section number '{$event->other['sectionnum']}' " .
2384                  "(section name '{$event->other['sectionname']}') for the course with id '{$event->courseid}'";
2385          $this->assertEquals($expecteddesc, $event->get_description());
2386          $this->assertEquals($section, $event->get_record_snapshot('course_sections', $event->objectid));
2387          $this->assertNull($event->get_url());
2388          $this->assertEventContextNotUsed($event);
2389      }
2390  
2391      public function test_course_integrity_check() {
2392          global $DB;
2393  
2394          $this->resetAfterTest(true);
2395          $course = $this->getDataGenerator()->create_course(array('numsections' => 1),
2396             array('createsections'=>true));
2397  
2398          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
2399                  array('section' => 0));
2400          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id),
2401                  array('section' => 0));
2402          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id),
2403                  array('section' => 0));
2404          $correctseq = join(',', array($forum->cmid, $page->cmid, $quiz->cmid));
2405  
2406          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2407          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2408          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2409          $this->assertEquals($correctseq, $section0->sequence);
2410          $this->assertEmpty($section1->sequence);
2411          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2412          $this->assertEquals($section0->id, $cms[$page->cmid]->section);
2413          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2414          $this->assertEmpty(course_integrity_check($course->id));
2415  
2416          // Now let's make manual change in DB and let course_integrity_check() fix it:
2417  
2418          // 1. Module appears twice in one section.
2419          $DB->update_record('course_sections', array('id' => $section0->id, 'sequence' => $section0->sequence. ','. $page->cmid));
2420          $this->assertEquals(
2421                  array('Failed integrity check for course ['. $course->id.
2422                  ']. Sequence for course section ['. $section0->id. '] is "'.
2423                  $section0->sequence. ','. $page->cmid. '", must be "'.
2424                  $section0->sequence. '"'),
2425                  course_integrity_check($course->id));
2426          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2427          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2428          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2429          $this->assertEquals($correctseq, $section0->sequence);
2430          $this->assertEmpty($section1->sequence);
2431          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2432          $this->assertEquals($section0->id, $cms[$page->cmid]->section);
2433          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2434  
2435          // 2. Module appears in two sections (last section wins).
2436          $DB->update_record('course_sections', array('id' => $section1->id, 'sequence' => ''. $page->cmid));
2437          // First message about double mentioning in sequence, second message about wrong section field for $page.
2438          $this->assertEquals(array(
2439              'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2440              '] must be removed from sequence of section ['. $section0->id.
2441              '] because it is also present in sequence of section ['. $section1->id. ']',
2442              'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2443              '] points to section ['. $section0->id. '] instead of ['. $section1->id. ']'),
2444                  course_integrity_check($course->id));
2445          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2446          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2447          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2448          $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2449          $this->assertEquals(''. $page->cmid, $section1->sequence);
2450          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2451          $this->assertEquals($section1->id, $cms[$page->cmid]->section);
2452          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2453  
2454          // 3. Module id is not present in course_section.sequence (integrity check with $fullcheck = false).
2455          $DB->update_record('course_sections', array('id' => $section1->id, 'sequence' => ''));
2456          $this->assertEmpty(course_integrity_check($course->id)); // Not an error!
2457          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2458          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2459          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2460          $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2461          $this->assertEmpty($section1->sequence);
2462          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2463          $this->assertEquals($section1->id, $cms[$page->cmid]->section); // Not changed.
2464          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2465  
2466          // 4. Module id is not present in course_section.sequence (integrity check with $fullcheck = true).
2467          $this->assertEquals(array('Failed integrity check for course ['. $course->id. ']. Course module ['.
2468                  $page->cmid. '] is missing from sequence of section ['. $section1->id. ']'),
2469                  course_integrity_check($course->id, null, null, true)); // Error!
2470          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2471          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2472          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2473          $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2474          $this->assertEquals(''. $page->cmid, $section1->sequence);  // Yay, module added to section.
2475          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2476          $this->assertEquals($section1->id, $cms[$page->cmid]->section); // Not changed.
2477          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2478  
2479          // 5. Module id is not present in course_section.sequence and it's section is invalid (integrity check with $fullcheck = true).
2480          $DB->update_record('course_modules', array('id' => $page->cmid, 'section' => 8765));
2481          $DB->update_record('course_sections', array('id' => $section1->id, 'sequence' => ''));
2482          $this->assertEquals(array(
2483              'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2484              '] is missing from sequence of section ['. $section0->id. ']',
2485              'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2486              '] points to section [8765] instead of ['. $section0->id. ']'),
2487                  course_integrity_check($course->id, null, null, true));
2488          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2489          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2490          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2491          $this->assertEquals($forum->cmid. ','. $quiz->cmid. ','. $page->cmid, $section0->sequence); // Module added to section.
2492          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2493          $this->assertEquals($section0->id, $cms[$page->cmid]->section); // Section changed to section0.
2494          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2495  
2496          // 6. Module is deleted from course_modules but not deleted in sequence (integrity check with $fullcheck = true).
2497          $DB->delete_records('course_modules', array('id' => $page->cmid));
2498          $this->assertEquals(array('Failed integrity check for course ['. $course->id. ']. Course module ['.
2499                  $page->cmid. '] does not exist but is present in the sequence of section ['. $section0->id. ']'),
2500                  course_integrity_check($course->id, null, null, true));
2501          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2502          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2503          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2504          $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2505          $this->assertEmpty($section1->sequence);
2506          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2507          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2508          $this->assertEquals(2, count($cms));
2509      }
2510  
2511      /**
2512       * Tests for event related to course module creation.
2513       */
2514      public function test_course_module_created_event() {
2515          global $USER;
2516  
2517          $this->resetAfterTest();
2518          $this->setAdminUser();
2519  
2520          // Create an assign module.
2521          $sink = $this->redirectEvents();
2522          $course = $this->getDataGenerator()->create_course();
2523          $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
2524          $events = $sink->get_events();
2525          $eventscount = 0;
2526  
2527          // Validate event data.
2528          foreach ($events as $event) {
2529              if ($event instanceof \core\event\course_module_created) {
2530                  $eventscount++;
2531  
2532                  $this->assertEquals($module->cmid, $event->objectid);
2533                  $this->assertEquals($USER->id, $event->userid);
2534                  $this->assertEquals('course_modules', $event->objecttable);
2535                  $url = new moodle_url('/mod/assign/view.php', array('id' => $module->cmid));
2536                  $this->assertEquals($url, $event->get_url());
2537                  $this->assertEventContextNotUsed($event);
2538              }
2539          }
2540          // Only one \core\event\course_module_created event should be triggered.
2541          $this->assertEquals(1, $eventscount);
2542  
2543          // Let us see if duplicating an activity results in a nice course module created event.
2544          $sink->clear();
2545          $course = get_course($module->course);
2546          $cm = get_coursemodule_from_id('assign', $module->cmid, 0, false, MUST_EXIST);
2547          $newcm = duplicate_module($course, $cm);
2548          $events = $sink->get_events();
2549          $eventscount = 0;
2550          $sink->close();
2551  
2552          foreach ($events as $event) {
2553              if ($event instanceof \core\event\course_module_created) {
2554                  $eventscount++;
2555                  // Validate event data.
2556                  $this->assertInstanceOf('\core\event\course_module_created', $event);
2557                  $this->assertEquals($newcm->id, $event->objectid);
2558                  $this->assertEquals($USER->id, $event->userid);
2559                  $this->assertEquals($course->id, $event->courseid);
2560                  $url = new moodle_url('/mod/assign/view.php', array('id' => $newcm->id));
2561                  $this->assertEquals($url, $event->get_url());
2562              }
2563          }
2564  
2565          // Only one \core\event\course_module_created event should be triggered.
2566          $this->assertEquals(1, $eventscount);
2567      }
2568  
2569      /**
2570       * Tests for event validations related to course module creation.
2571       */
2572      public function test_course_module_created_event_exceptions() {
2573  
2574          $this->resetAfterTest();
2575  
2576          // Generate data.
2577          $modinfo = $this->create_specific_module_test('assign');
2578          $context = context_module::instance($modinfo->coursemodule);
2579  
2580          // Test not setting instanceid.
2581          try {
2582              $event = \core\event\course_module_created::create(array(
2583                  'courseid' => $modinfo->course,
2584                  'context'  => $context,
2585                  'objectid' => $modinfo->coursemodule,
2586                  'other'    => array(
2587                      'modulename' => 'assign',
2588                      'name'       => 'My assignment',
2589                  )
2590              ));
2591              $this->fail("Event validation should not allow \\core\\event\\course_module_created to be triggered without
2592                      other['instanceid']");
2593          } catch (coding_exception $e) {
2594              $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage());
2595          }
2596  
2597          // Test not setting modulename.
2598          try {
2599              $event = \core\event\course_module_created::create(array(
2600                  'courseid' => $modinfo->course,
2601                  'context'  => $context,
2602                  'objectid' => $modinfo->coursemodule,
2603                  'other'    => array(
2604                      'instanceid' => $modinfo->instance,
2605                      'name'       => 'My assignment',
2606                  )
2607              ));
2608              $this->fail("Event validation should not allow \\core\\event\\course_module_created to be triggered without
2609                      other['modulename']");
2610          } catch (coding_exception $e) {
2611              $this->assertStringContainsString("The 'modulename' value must be set in other.", $e->getMessage());
2612          }
2613  
2614          // Test not setting name.
2615  
2616          try {
2617              $event = \core\event\course_module_created::create(array(
2618                  'courseid' => $modinfo->course,
2619                  'context'  => $context,
2620                  'objectid' => $modinfo->coursemodule,
2621                  'other'    => array(
2622                      'modulename' => 'assign',
2623                      'instanceid' => $modinfo->instance,
2624                  )
2625              ));
2626              $this->fail("Event validation should not allow \\core\\event\\course_module_created to be triggered without
2627                      other['name']");
2628          } catch (coding_exception $e) {
2629              $this->assertStringContainsString("The 'name' value must be set in other.", $e->getMessage());
2630          }
2631  
2632      }
2633  
2634      /**
2635       * Tests for event related to course module updates.
2636       */
2637      public function test_course_module_updated_event() {
2638          global $USER, $DB;
2639          $this->resetAfterTest();
2640  
2641          // Update a forum module.
2642          $sink = $this->redirectEvents();
2643          $modinfo = $this->update_specific_module_test('forum');
2644          $events = $sink->get_events();
2645          $eventscount = 0;
2646          $sink->close();
2647  
2648          $cm = $DB->get_record('course_modules', array('id' => $modinfo->coursemodule), '*', MUST_EXIST);
2649          $mod = $DB->get_record('forum', array('id' => $cm->instance), '*', MUST_EXIST);
2650  
2651          // Validate event data.
2652          foreach ($events as $event) {
2653              if ($event instanceof \core\event\course_module_updated) {
2654                  $eventscount++;
2655  
2656                  $this->assertEquals($cm->id, $event->objectid);
2657                  $this->assertEquals($USER->id, $event->userid);
2658                  $this->assertEquals('course_modules', $event->objecttable);
2659                  $url = new moodle_url('/mod/forum/view.php', array('id' => $cm->id));
2660                  $this->assertEquals($url, $event->get_url());
2661                  $this->assertEventContextNotUsed($event);
2662              }
2663          }
2664  
2665          // Only one \core\event\course_module_updated event should be triggered.
2666          $this->assertEquals(1, $eventscount);
2667      }
2668  
2669      /**
2670       * Tests for create_from_cm method.
2671       */
2672      public function test_course_module_create_from_cm() {
2673          $this->resetAfterTest();
2674          $this->setAdminUser();
2675  
2676          // Create course and modules.
2677          $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
2678  
2679          // Generate an assignment.
2680          $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
2681  
2682          // Get the module context.
2683          $modcontext = context_module::instance($assign->cmid);
2684  
2685          // Get course module.
2686          $cm = get_coursemodule_from_id(null, $assign->cmid, $course->id, false, MUST_EXIST);
2687  
2688          // Create an event from course module.
2689          $event = \core\event\course_module_updated::create_from_cm($cm, $modcontext);
2690  
2691          // Trigger the events.
2692          $sink = $this->redirectEvents();
2693          $event->trigger();
2694          $events = $sink->get_events();
2695          $event2 = array_pop($events);
2696  
2697          // Test event data.
2698          $this->assertInstanceOf('\core\event\course_module_updated', $event);
2699          $this->assertEquals($cm->id, $event2->objectid);
2700          $this->assertEquals($modcontext, $event2->get_context());
2701          $this->assertEquals($cm->modname, $event2->other['modulename']);
2702          $this->assertEquals($cm->instance, $event2->other['instanceid']);
2703          $this->assertEquals($cm->name, $event2->other['name']);
2704          $this->assertEventContextNotUsed($event2);
2705      }
2706  
2707      /**
2708       * Tests for event validations related to course module update.
2709       */
2710      public function test_course_module_updated_event_exceptions() {
2711  
2712          $this->resetAfterTest();
2713  
2714          // Generate data.
2715          $modinfo = $this->create_specific_module_test('assign');
2716          $context = context_module::instance($modinfo->coursemodule);
2717  
2718          // Test not setting instanceid.
2719          try {
2720              $event = \core\event\course_module_updated::create(array(
2721                  'courseid' => $modinfo->course,
2722                  'context'  => $context,
2723                  'objectid' => $modinfo->coursemodule,
2724                  'other'    => array(
2725                      'modulename' => 'assign',
2726                      'name'       => 'My assignment',
2727                  )
2728              ));
2729              $this->fail("Event validation should not allow \\core\\event\\course_module_updated to be triggered without
2730                      other['instanceid']");
2731          } catch (coding_exception $e) {
2732              $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage());
2733          }
2734  
2735          // Test not setting modulename.
2736          try {
2737              $event = \core\event\course_module_updated::create(array(
2738                  'courseid' => $modinfo->course,
2739                  'context'  => $context,
2740                  'objectid' => $modinfo->coursemodule,
2741                  'other'    => array(
2742                      'instanceid' => $modinfo->instance,
2743                      'name'       => 'My assignment',
2744                  )
2745              ));
2746              $this->fail("Event validation should not allow \\core\\event\\course_module_updated to be triggered without
2747                      other['modulename']");
2748          } catch (coding_exception $e) {
2749              $this->assertStringContainsString("The 'modulename' value must be set in other.", $e->getMessage());
2750          }
2751  
2752          // Test not setting name.
2753  
2754          try {
2755              $event = \core\event\course_module_updated::create(array(
2756                  'courseid' => $modinfo->course,
2757                  'context'  => $context,
2758                  'objectid' => $modinfo->coursemodule,
2759                  'other'    => array(
2760                      'modulename' => 'assign',
2761                      'instanceid' => $modinfo->instance,
2762                  )
2763              ));
2764              $this->fail("Event validation should not allow \\core\\event\\course_module_updated to be triggered without
2765                      other['name']");
2766          } catch (coding_exception $e) {
2767              $this->assertStringContainsString("The 'name' value must be set in other.", $e->getMessage());
2768          }
2769  
2770      }
2771  
2772      /**
2773       * Tests for event related to course module delete.
2774       */
2775      public function test_course_module_deleted_event() {
2776          global $USER, $DB;
2777          $this->resetAfterTest();
2778  
2779          // Create and delete a module.
2780          $sink = $this->redirectEvents();
2781          $modinfo = $this->create_specific_module_test('forum');
2782          $cm = $DB->get_record('course_modules', array('id' => $modinfo->coursemodule), '*', MUST_EXIST);
2783          course_delete_module($modinfo->coursemodule);
2784          $events = $sink->get_events();
2785          $event = array_pop($events); // delete module event.;
2786          $sink->close();
2787  
2788          // Validate event data.
2789          $this->assertInstanceOf('\core\event\course_module_deleted', $event);
2790          $this->assertEquals($cm->id, $event->objectid);
2791          $this->assertEquals($USER->id, $event->userid);
2792          $this->assertEquals('course_modules', $event->objecttable);
2793          $this->assertEquals(null, $event->get_url());
2794          $this->assertEquals($cm, $event->get_record_snapshot('course_modules', $cm->id));
2795      }
2796  
2797      /**
2798       * Tests for event validations related to course module deletion.
2799       */
2800      public function test_course_module_deleted_event_exceptions() {
2801  
2802          $this->resetAfterTest();
2803  
2804          // Generate data.
2805          $modinfo = $this->create_specific_module_test('assign');
2806          $context = context_module::instance($modinfo->coursemodule);
2807  
2808          // Test not setting instanceid.
2809          try {
2810              $event = \core\event\course_module_deleted::create(array(
2811                  'courseid' => $modinfo->course,
2812                  'context'  => $context,
2813                  'objectid' => $modinfo->coursemodule,
2814                  'other'    => array(
2815                      'modulename' => 'assign',
2816                      'name'       => 'My assignment',
2817                  )
2818              ));
2819              $this->fail("Event validation should not allow \\core\\event\\course_module_deleted to be triggered without
2820                      other['instanceid']");
2821          } catch (coding_exception $e) {
2822              $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage());
2823          }
2824  
2825          // Test not setting modulename.
2826          try {
2827              $event = \core\event\course_module_deleted::create(array(
2828                  'courseid' => $modinfo->course,
2829                  'context'  => $context,
2830                  'objectid' => $modinfo->coursemodule,
2831                  'other'    => array(
2832                      'instanceid' => $modinfo->instance,
2833                      'name'       => 'My assignment',
2834                  )
2835              ));
2836              $this->fail("Event validation should not allow \\core\\event\\course_module_deleted to be triggered without
2837                      other['modulename']");
2838          } catch (coding_exception $e) {
2839              $this->assertStringContainsString("The 'modulename' value must be set in other.", $e->getMessage());
2840          }
2841      }
2842  
2843      /**
2844       * Returns a user object and its assigned new role.
2845       *
2846       * @param testing_data_generator $generator
2847       * @param $contextid
2848       * @return array The user object and the role ID
2849       */
2850      protected function get_user_objects(testing_data_generator $generator, $contextid) {
2851          global $USER;
2852  
2853          if (empty($USER->id)) {
2854              $user  = $generator->create_user();
2855              $this->setUser($user);
2856          }
2857          $roleid = create_role('Test role', 'testrole', 'Test role description');
2858          if (!is_array($contextid)) {
2859              $contextid = array($contextid);
2860          }
2861          foreach ($contextid as $cid) {
2862              $assignid = role_assign($roleid, $user->id, $cid);
2863          }
2864          return array($user, $roleid);
2865      }
2866  
2867      /**
2868       * Test course move after course.
2869       */
2870      public function test_course_change_sortorder_after_course() {
2871          global $DB;
2872  
2873          $this->resetAfterTest(true);
2874  
2875          $generator = $this->getDataGenerator();
2876          $category = $generator->create_category();
2877          $course3 = $generator->create_course(array('category' => $category->id));
2878          $course2 = $generator->create_course(array('category' => $category->id));
2879          $course1 = $generator->create_course(array('category' => $category->id));
2880          $context = $category->get_context();
2881  
2882          list($user, $roleid) = $this->get_user_objects($generator, $context->id);
2883          $caps = course_capability_assignment::allow('moodle/category:manage', $roleid, $context->id);
2884  
2885          $courses = $category->get_courses();
2886          $this->assertIsArray($courses);
2887          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2888          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2889          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2890  
2891          // Test moving down.
2892          $this->assertTrue(course_change_sortorder_after_course($course1->id, $course3->id));
2893          $courses = $category->get_courses();
2894          $this->assertIsArray($courses);
2895          $this->assertEquals(array($course2->id, $course3->id, $course1->id), array_keys($courses));
2896          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2897          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2898  
2899          // Test moving up.
2900          $this->assertTrue(course_change_sortorder_after_course($course1->id, $course2->id));
2901          $courses = $category->get_courses();
2902          $this->assertIsArray($courses);
2903          $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
2904          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2905          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2906  
2907          // Test moving to the top.
2908          $this->assertTrue(course_change_sortorder_after_course($course1->id, 0));
2909          $courses = $category->get_courses();
2910          $this->assertIsArray($courses);
2911          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2912          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2913          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2914      }
2915  
2916      /**
2917       * Tests changing the visibility of a course.
2918       */
2919      public function test_course_change_visibility() {
2920          global $DB;
2921  
2922          $this->resetAfterTest(true);
2923  
2924          $generator = $this->getDataGenerator();
2925          $category = $generator->create_category();
2926          $course = $generator->create_course(array('category' => $category->id));
2927  
2928          $this->assertEquals('1', $course->visible);
2929          $this->assertEquals('1', $course->visibleold);
2930  
2931          $this->assertTrue(course_change_visibility($course->id, false));
2932          $course = $DB->get_record('course', array('id' => $course->id));
2933          $this->assertEquals('0', $course->visible);
2934          $this->assertEquals('0', $course->visibleold);
2935  
2936          $this->assertTrue(course_change_visibility($course->id, true));
2937          $course = $DB->get_record('course', array('id' => $course->id));
2938          $this->assertEquals('1', $course->visible);
2939          $this->assertEquals('1', $course->visibleold);
2940      }
2941  
2942      /**
2943       * Tests moving the course up and down by one.
2944       */
2945      public function test_course_change_sortorder_by_one() {
2946          global $DB;
2947  
2948          $this->resetAfterTest(true);
2949  
2950          $generator = $this->getDataGenerator();
2951          $category = $generator->create_category();
2952          $course3 = $generator->create_course(array('category' => $category->id));
2953          $course2 = $generator->create_course(array('category' => $category->id));
2954          $course1 = $generator->create_course(array('category' => $category->id));
2955  
2956          $courses = $category->get_courses();
2957          $this->assertIsArray($courses);
2958          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2959          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2960          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2961  
2962          // Test moving down.
2963          $course1 = get_course($course1->id);
2964          $this->assertTrue(course_change_sortorder_by_one($course1, false));
2965          $courses = $category->get_courses();
2966          $this->assertIsArray($courses);
2967          $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
2968          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2969          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2970  
2971          // Test moving up.
2972          $course1 = get_course($course1->id);
2973          $this->assertTrue(course_change_sortorder_by_one($course1, true));
2974          $courses = $category->get_courses();
2975          $this->assertIsArray($courses);
2976          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2977          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2978          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2979  
2980          // Test moving the top course up one.
2981          $course1 = get_course($course1->id);
2982          $this->assertFalse(course_change_sortorder_by_one($course1, true));
2983          // Check nothing changed.
2984          $courses = $category->get_courses();
2985          $this->assertIsArray($courses);
2986          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2987          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2988          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2989  
2990          // Test moving the bottom course up down.
2991          $course3 = get_course($course3->id);
2992          $this->assertFalse(course_change_sortorder_by_one($course3, false));
2993          // Check nothing changed.
2994          $courses = $category->get_courses();
2995          $this->assertIsArray($courses);
2996          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2997          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2998          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2999      }
3000  
3001      public function test_view_resources_list() {
3002          $this->resetAfterTest();
3003  
3004          $course = self::getDataGenerator()->create_course();
3005          $coursecontext = context_course::instance($course->id);
3006  
3007          $event = \core\event\course_resources_list_viewed::create(array('context' => context_course::instance($course->id)));
3008          $sink = $this->redirectEvents();
3009          $event->trigger();
3010          $events = $sink->get_events();
3011          $sink->close();
3012  
3013          // Validate the event.
3014          $event = $events[0];
3015          $this->assertInstanceOf('\core\event\course_resources_list_viewed', $event);
3016          $this->assertEquals(null, $event->objecttable);
3017          $this->assertEquals(null, $event->objectid);
3018          $this->assertEquals($course->id, $event->courseid);
3019          $this->assertEquals($coursecontext->id, $event->contextid);
3020          $this->assertEventContextNotUsed($event);
3021      }
3022  
3023      /**
3024       * Test duplicate_module()
3025       */
3026      public function test_duplicate_module() {
3027          $this->setAdminUser();
3028          $this->resetAfterTest();
3029          $course = self::getDataGenerator()->create_course();
3030          $res = self::getDataGenerator()->create_module('resource', array('course' => $course));
3031          $cm = get_coursemodule_from_id('resource', $res->cmid, 0, false, MUST_EXIST);
3032  
3033          $newcm = duplicate_module($course, $cm);
3034  
3035          // Make sure they are the same, except obvious id changes.
3036          foreach ($cm as $prop => $value) {
3037              if ($prop == 'id' || $prop == 'url' || $prop == 'instance' || $prop == 'added') {
3038                  // Ignore obviously different properties.
3039                  continue;
3040              }
3041              if ($prop == 'name') {
3042                  // We expect ' (copy)' to be added to the original name since MDL-59227.
3043                  $value = get_string('duplicatedmodule', 'moodle', $value);
3044              }
3045              $this->assertEquals($value, $newcm->$prop);
3046          }
3047      }
3048  
3049      /**
3050       * Tests that when creating or updating a module, if the availability settings
3051       * are present but set to an empty tree, availability is set to null in
3052       * database.
3053       */
3054      public function test_empty_availability_settings() {
3055          global $DB;
3056          $this->setAdminUser();
3057          $this->resetAfterTest();
3058  
3059          // Enable availability.
3060          set_config('enableavailability', 1);
3061  
3062          // Test add.
3063          $emptyavailability = json_encode(\core_availability\tree::get_root_json(array()));
3064          $course = self::getDataGenerator()->create_course();
3065          $label = self::getDataGenerator()->create_module('label', array(
3066                  'course' => $course, 'availability' => $emptyavailability));
3067          $this->assertNull($DB->get_field('course_modules', 'availability',
3068                  array('id' => $label->cmid)));
3069  
3070          // Test update.
3071          $formdata = $DB->get_record('course_modules', array('id' => $label->cmid));
3072          unset($formdata->availability);
3073          $formdata->availabilityconditionsjson = $emptyavailability;
3074          $formdata->modulename = 'label';
3075          $formdata->coursemodule = $label->cmid;
3076          $draftid = 0;
3077          file_prepare_draft_area($draftid, context_module::instance($label->cmid)->id,
3078                  'mod_label', 'intro', 0);
3079          $formdata->introeditor = array(
3080              'itemid' => $draftid,
3081              'text' => '<p>Yo</p>',
3082              'format' => FORMAT_HTML);
3083          update_module($formdata);
3084          $this->assertNull($DB->get_field('course_modules', 'availability',
3085                  array('id' => $label->cmid)));
3086      }
3087  
3088      /**
3089       * Test update_inplace_editable()
3090       */
3091      public function test_update_module_name_inplace() {
3092          global $CFG, $DB, $PAGE;
3093          require_once($CFG->dirroot . '/lib/external/externallib.php');
3094  
3095          $this->setUser($this->getDataGenerator()->create_user());
3096  
3097          $this->resetAfterTest(true);
3098          $course = $this->getDataGenerator()->create_course();
3099          $forum = self::getDataGenerator()->create_module('forum', array('course' => $course->id, 'name' => 'forum name'));
3100  
3101          // Call service for core_course component without necessary permissions.
3102          try {
3103              core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, 'New forum name');
3104              $this->fail('Exception expected');
3105          } catch (moodle_exception $e) {
3106              $this->assertEquals('Course or activity not accessible. (Not enrolled)',
3107                  $e->getMessage());
3108          }
3109  
3110          // Change to admin user and make sure that cm name can be updated using web service update_inplace_editable().
3111          $this->setAdminUser();
3112          $res = core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, 'New forum name');
3113          $res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
3114          $this->assertEquals('New forum name', $res['value']);
3115          $this->assertEquals('New forum name', $DB->get_field('forum', 'name', array('id' => $forum->id)));
3116      }
3117  
3118      /**
3119       * Testing function course_get_tagged_course_modules - search tagged course modules
3120       */
3121      public function test_course_get_tagged_course_modules() {
3122          global $DB;
3123          $this->resetAfterTest();
3124          $course3 = $this->getDataGenerator()->create_course();
3125          $course2 = $this->getDataGenerator()->create_course();
3126          $course1 = $this->getDataGenerator()->create_course();
3127          $cm11 = $this->getDataGenerator()->create_module('assign', array('course' => $course1->id,
3128              'tags' => 'Cat, Dog'));
3129          $cm12 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3130              'tags' => 'Cat, Mouse', 'visible' => 0));
3131          $cm13 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3132              'tags' => 'Cat, Mouse, Dog'));
3133          $cm21 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id,
3134              'tags' => 'Cat, Mouse'));
3135          $cm31 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id,
3136              'tags' => 'Cat, Mouse'));
3137  
3138          // Admin is able to view everything.
3139          $this->setAdminUser();
3140          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3141                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3142          $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3143          $this->assertMatchesRegularExpression('/'.$cm12->name.'/', $res->content);
3144          $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3145          $this->assertMatchesRegularExpression('/'.$cm21->name.'/', $res->content);
3146          $this->assertMatchesRegularExpression('/'.$cm31->name.'/', $res->content);
3147          // Results from course1 are returned before results from course2.
3148          $this->assertTrue(strpos($res->content, $cm11->name) < strpos($res->content, $cm21->name));
3149  
3150          // Ordinary user is not able to see anything.
3151          $user = $this->getDataGenerator()->create_user();
3152          $this->setUser($user);
3153  
3154          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3155                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3156          $this->assertNull($res);
3157  
3158          // Enrol user as student in course1 and course2.
3159          $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
3160          $this->getDataGenerator()->enrol_user($user->id, $course1->id, $roleids['student']);
3161          $this->getDataGenerator()->enrol_user($user->id, $course2->id, $roleids['student']);
3162          core_tag_index_builder::reset_caches();
3163  
3164          // Searching in the course context returns visible modules in this course.
3165          $context = context_course::instance($course1->id);
3166          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3167                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */0);
3168          $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3169          $this->assertDoesNotMatchRegularExpression('/'.$cm12->name.'/', $res->content);
3170          $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3171          $this->assertDoesNotMatchRegularExpression('/'.$cm21->name.'/', $res->content);
3172          $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content);
3173  
3174          // Searching FROM the course context returns visible modules in all courses.
3175          $context = context_course::instance($course2->id);
3176          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3177                  /*$exclusivemode = */false, /*$fromctx = */$context->id, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3178          $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3179          $this->assertDoesNotMatchRegularExpression('/'.$cm12->name.'/', $res->content);
3180          $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3181          $this->assertMatchesRegularExpression('/'.$cm21->name.'/', $res->content);
3182          $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content); // No access to course3.
3183          // Results from course2 are returned before results from course1.
3184          $this->assertTrue(strpos($res->content, $cm21->name) < strpos($res->content, $cm11->name));
3185  
3186          // Enrol user in course1 as a teacher - now he should be able to see hidden module.
3187          $this->getDataGenerator()->enrol_user($user->id, $course1->id, $roleids['editingteacher']);
3188          get_fast_modinfo(0,0,true);
3189  
3190          $context = context_course::instance($course1->id);
3191          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3192                  /*$exclusivemode = */false, /*$fromctx = */$context->id, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3193          $this->assertMatchesRegularExpression('/'.$cm12->name.'/', $res->content);
3194  
3195          // Create more modules and try pagination.
3196          $cm14 = $this->getDataGenerator()->create_module('assign', array('course' => $course1->id,
3197              'tags' => 'Cat, Dog'));
3198          $cm15 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3199              'tags' => 'Cat, Mouse', 'visible' => 0));
3200          $cm16 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3201              'tags' => 'Cat, Mouse, Dog'));
3202  
3203          $context = context_course::instance($course1->id);
3204          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3205                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */0);
3206          $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3207          $this->assertMatchesRegularExpression('/'.$cm12->name.'/', $res->content);
3208          $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3209          $this->assertDoesNotMatchRegularExpression('/'.$cm21->name.'/', $res->content);
3210          $this->assertMatchesRegularExpression('/'.$cm14->name.'/', $res->content);
3211          $this->assertMatchesRegularExpression('/'.$cm15->name.'/', $res->content);
3212          $this->assertDoesNotMatchRegularExpression('/'.$cm16->name.'/', $res->content);
3213          $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content); // No access to course3.
3214          $this->assertEmpty($res->prevpageurl);
3215          $this->assertNotEmpty($res->nextpageurl);
3216  
3217          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3218                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */1);
3219          $this->assertDoesNotMatchRegularExpression('/'.$cm11->name.'/', $res->content);
3220          $this->assertDoesNotMatchRegularExpression('/'.$cm12->name.'/', $res->content);
3221          $this->assertDoesNotMatchRegularExpression('/'.$cm13->name.'/', $res->content);
3222          $this->assertDoesNotMatchRegularExpression('/'.$cm21->name.'/', $res->content);
3223          $this->assertDoesNotMatchRegularExpression('/'.$cm14->name.'/', $res->content);
3224          $this->assertDoesNotMatchRegularExpression('/'.$cm15->name.'/', $res->content);
3225          $this->assertMatchesRegularExpression('/'.$cm16->name.'/', $res->content);
3226          $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content); // No access to course3.
3227          $this->assertNotEmpty($res->prevpageurl);
3228          $this->assertEmpty($res->nextpageurl);
3229      }
3230  
3231      /**
3232       * Test course_get_user_navigation_options for frontpage.
3233       */
3234      public function test_course_get_user_navigation_options_for_frontpage() {
3235          global $CFG, $SITE, $DB;
3236          $this->resetAfterTest();
3237          $context = context_system::instance();
3238          $course = clone $SITE;
3239          $this->setAdminUser();
3240  
3241          $navoptions = course_get_user_navigation_options($context, $course);
3242          $this->assertTrue($navoptions->blogs);
3243          $this->assertTrue($navoptions->notes);
3244          $this->assertTrue($navoptions->participants);
3245          $this->assertTrue($navoptions->badges);
3246          $this->assertTrue($navoptions->tags);
3247          $this->assertFalse($navoptions->search);
3248          $this->assertTrue($navoptions->competencies);
3249  
3250          // Enable global search now.
3251          $CFG->enableglobalsearch = 1;
3252          $navoptions = course_get_user_navigation_options($context, $course);
3253          $this->assertTrue($navoptions->search);
3254  
3255          // Disable competencies.
3256          $oldcompetencies = get_config('core_competency', 'enabled');
3257          set_config('enabled', false, 'core_competency');
3258          $navoptions = course_get_user_navigation_options($context, $course);
3259          $this->assertFalse($navoptions->competencies);
3260          set_config('enabled', $oldcompetencies, 'core_competency');
3261  
3262          // Now try with a standard user.
3263          $user = $this->getDataGenerator()->create_user();
3264          $this->setUser($user);
3265          $navoptions = course_get_user_navigation_options($context, $course);
3266          $this->assertTrue($navoptions->blogs);
3267          $this->assertFalse($navoptions->notes);
3268          $this->assertFalse($navoptions->participants);
3269          $this->assertTrue($navoptions->badges);
3270          $this->assertTrue($navoptions->tags);
3271          $this->assertTrue($navoptions->search);
3272      }
3273  
3274      /**
3275       * Test course_get_user_navigation_options for managers in a normal course.
3276       */
3277      public function test_course_get_user_navigation_options_for_managers() {
3278          global $CFG;
3279          $this->resetAfterTest();
3280          $course = $this->getDataGenerator()->create_course();
3281          $context = context_course::instance($course->id);
3282          $this->setAdminUser();
3283  
3284          $navoptions = course_get_user_navigation_options($context);
3285          $this->assertTrue($navoptions->blogs);
3286          $this->assertTrue($navoptions->notes);
3287          $this->assertTrue($navoptions->participants);
3288          $this->assertTrue($navoptions->badges);
3289      }
3290  
3291      /**
3292       * Test course_get_user_navigation_options for students in a normal course.
3293       */
3294      public function test_course_get_user_navigation_options_for_students() {
3295          global $DB, $CFG;
3296          $this->resetAfterTest();
3297          $course = $this->getDataGenerator()->create_course();
3298          $context = context_course::instance($course->id);
3299  
3300          $user = $this->getDataGenerator()->create_user();
3301          $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
3302          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
3303  
3304          $this->setUser($user);
3305  
3306          $navoptions = course_get_user_navigation_options($context);
3307          $this->assertTrue($navoptions->blogs);
3308          $this->assertFalse($navoptions->notes);
3309          $this->assertTrue($navoptions->participants);
3310          $this->assertFalse($navoptions->badges);
3311  
3312          // Disable some options.
3313          $CFG->badges_allowcoursebadges = 0;
3314          $CFG->enableblogs = 0;
3315          // Disable view participants capability.
3316          assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $context);
3317  
3318          $navoptions = course_get_user_navigation_options($context);
3319          $this->assertFalse($navoptions->blogs);
3320          $this->assertFalse($navoptions->notes);
3321          $this->assertFalse($navoptions->participants);
3322          $this->assertFalse($navoptions->badges);
3323  
3324          // Re-enable some options to check badges are displayed as expected.
3325          $CFG->badges_allowcoursebadges = 1;
3326          assign_capability('moodle/badges:createbadge', CAP_ALLOW, $roleid, $context);
3327  
3328          $navoptions = course_get_user_navigation_options($context);
3329          $this->assertTrue($navoptions->badges);
3330      }
3331  
3332      /**
3333       * Test course_get_user_administration_options for frontpage.
3334       */
3335      public function test_course_get_user_administration_options_for_frontpage() {
3336          global $CFG, $SITE;
3337          $this->resetAfterTest();
3338          $course = clone $SITE;
3339          $context = context_course::instance($course->id);
3340          $this->setAdminUser();
3341  
3342          $adminoptions = course_get_user_administration_options($course, $context);
3343          $this->assertTrue($adminoptions->update);
3344          $this->assertTrue($adminoptions->filters);
3345          $this->assertTrue($adminoptions->reports);
3346          $this->assertTrue($adminoptions->backup);
3347          $this->assertTrue($adminoptions->restore);
3348          $this->assertFalse($adminoptions->files);
3349          $this->assertFalse($adminoptions->tags);
3350  
3351          // Now try with a standard user.
3352          $user = $this->getDataGenerator()->create_user();
3353          $this->setUser($user);
3354          $adminoptions = course_get_user_administration_options($course, $context);
3355          $this->assertFalse($adminoptions->update);
3356          $this->assertFalse($adminoptions->filters);
3357          $this->assertFalse($adminoptions->reports);
3358          $this->assertFalse($adminoptions->backup);
3359          $this->assertFalse($adminoptions->restore);
3360          $this->assertFalse($adminoptions->files);
3361          $this->assertFalse($adminoptions->tags);
3362  
3363      }
3364  
3365      /**
3366       * Test course_get_user_administration_options for managers in a normal course.
3367       */
3368      public function test_course_get_user_administration_options_for_managers() {
3369          global $CFG;
3370          $this->resetAfterTest();
3371          $course = $this->getDataGenerator()->create_course();
3372          $context = context_course::instance($course->id);
3373          $this->setAdminUser();
3374  
3375          $adminoptions = course_get_user_administration_options($course, $context);
3376          $this->assertTrue($adminoptions->update);
3377          $this->assertTrue($adminoptions->filters);
3378          $this->assertTrue($adminoptions->reports);
3379          $this->assertTrue($adminoptions->backup);
3380          $this->assertTrue($adminoptions->restore);
3381          $this->assertFalse($adminoptions->files);
3382          $this->assertTrue($adminoptions->tags);
3383          $this->assertTrue($adminoptions->gradebook);
3384          $this->assertFalse($adminoptions->outcomes);
3385          $this->assertTrue($adminoptions->badges);
3386          $this->assertTrue($adminoptions->import);
3387          $this->assertTrue($adminoptions->reset);
3388          $this->assertTrue($adminoptions->roles);
3389      }
3390  
3391      /**
3392       * Test course_get_user_administration_options for students in a normal course.
3393       */
3394      public function test_course_get_user_administration_options_for_students() {
3395          global $DB, $CFG;
3396          $this->resetAfterTest();
3397          $course = $this->getDataGenerator()->create_course();
3398          $context = context_course::instance($course->id);
3399  
3400          $user = $this->getDataGenerator()->create_user();
3401          $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
3402          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
3403  
3404          $this->setUser($user);
3405          $adminoptions = course_get_user_administration_options($course, $context);
3406  
3407          $this->assertFalse($adminoptions->update);
3408          $this->assertFalse($adminoptions->filters);
3409          $this->assertFalse($adminoptions->reports);
3410          $this->assertFalse($adminoptions->backup);
3411          $this->assertFalse($adminoptions->restore);
3412          $this->assertFalse($adminoptions->files);
3413          $this->assertFalse($adminoptions->tags);
3414          $this->assertFalse($adminoptions->gradebook);
3415          $this->assertFalse($adminoptions->outcomes);
3416          $this->assertTrue($adminoptions->badges);
3417          $this->assertFalse($adminoptions->import);
3418          $this->assertFalse($adminoptions->reset);
3419          $this->assertFalse($adminoptions->roles);
3420  
3421          $CFG->enablebadges = false;
3422          $adminoptions = course_get_user_administration_options($course, $context);
3423          $this->assertFalse($adminoptions->badges);
3424      }
3425  
3426      /**
3427       * Test test_update_course_frontpage_category.
3428       */
3429      public function test_update_course_frontpage_category() {
3430          // Fetch front page course.
3431          $course = get_course(SITEID);
3432          // Test update information on front page course.
3433          $course->category = 99;
3434          $this->expectException('moodle_exception');
3435          $this->expectExceptionMessage(get_string('invalidcourse', 'error'));
3436          update_course($course);
3437      }
3438  
3439      /**
3440       * test_course_enddate
3441       *
3442       * @dataProvider course_enddate_provider
3443       * @param int $startdate
3444       * @param int $enddate
3445       * @param string $errorcode
3446       */
3447      public function test_course_enddate($startdate, $enddate, $errorcode) {
3448  
3449          $this->resetAfterTest(true);
3450  
3451          $record = array('startdate' => $startdate, 'enddate' => $enddate);
3452          try {
3453              $course1 = $this->getDataGenerator()->create_course($record);
3454              if ($errorcode !== false) {
3455                  $this->fail('Expected exception with "' . $errorcode . '" error code in create_create');
3456              }
3457          } catch (moodle_exception $e) {
3458              if ($errorcode === false) {
3459                  $this->fail('Got "' . $errorcode . '" exception error code and no exception was expected');
3460              }
3461              if ($e->errorcode != $errorcode) {
3462                  $this->fail('Got "' . $e->errorcode. '" exception error code and "' . $errorcode . '" was expected');
3463              }
3464              return;
3465          }
3466  
3467          $this->assertEquals($startdate, $course1->startdate);
3468          $this->assertEquals($enddate, $course1->enddate);
3469      }
3470  
3471      /**
3472       * Provider for test_course_enddate.
3473       *
3474       * @return array
3475       */
3476      public function course_enddate_provider() {
3477          // Each provided example contains startdate, enddate and the expected exception error code if there is any.
3478          return [
3479              [
3480                  111,
3481                  222,
3482                  false
3483              ], [
3484                  222,
3485                  111,
3486                  'enddatebeforestartdate'
3487              ], [
3488                  111,
3489                  0,
3490                  false
3491              ], [
3492                  0,
3493                  222,
3494                  'nostartdatenoenddate'
3495              ]
3496          ];
3497      }
3498  
3499  
3500      /**
3501       * test_course_dates_reset
3502       *
3503       * @dataProvider course_dates_reset_provider
3504       * @param int $startdate
3505       * @param int $enddate
3506       * @param int $resetstartdate
3507       * @param int $resetenddate
3508       * @param int $resultingstartdate
3509       * @param int $resultingenddate
3510       */
3511      public function test_course_dates_reset($startdate, $enddate, $resetstartdate, $resetenddate, $resultingstartdate, $resultingenddate) {
3512          global $CFG, $DB;
3513  
3514          require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
3515  
3516          $this->resetAfterTest(true);
3517  
3518          $this->setAdminUser();
3519  
3520          $CFG->enablecompletion = true;
3521  
3522          $this->setTimezone('UTC');
3523  
3524          $record = array('startdate' => $startdate, 'enddate' => $enddate, 'enablecompletion' => 1);
3525          $originalcourse = $this->getDataGenerator()->create_course($record);
3526          $coursecriteria = new completion_criteria_date(array('course' => $originalcourse->id, 'timeend' => $startdate + DAYSECS));
3527          $coursecriteria->insert();
3528  
3529          $activitycompletiondate = $startdate + DAYSECS;
3530          $data = $this->getDataGenerator()->create_module('data', array('course' => $originalcourse->id),
3531                          array('completion' => 1, 'completionexpected' => $activitycompletiondate));
3532  
3533          $resetdata = new stdClass();
3534          $resetdata->id = $originalcourse->id;
3535          $resetdata->reset_start_date_old = $originalcourse->startdate;
3536          $resetdata->reset_start_date = $resetstartdate;
3537          $resetdata->reset_end_date = $resetenddate;
3538          $resetdata->reset_end_date_old = $record['enddate'];
3539          reset_course_userdata($resetdata);
3540  
3541          $course = $DB->get_record('course', array('id' => $originalcourse->id));
3542  
3543          $this->assertEquals($resultingstartdate, $course->startdate);
3544          $this->assertEquals($resultingenddate, $course->enddate);
3545  
3546          $coursecompletioncriteria = completion_criteria_date::fetch(array('course' => $originalcourse->id));
3547          $this->assertEquals($resultingstartdate + DAYSECS, $coursecompletioncriteria->timeend);
3548  
3549          $this->assertEquals($resultingstartdate + DAYSECS, $DB->get_field('course_modules', 'completionexpected',
3550              array('id' => $data->cmid)));
3551      }
3552  
3553      /**
3554       * Provider for test_course_dates_reset.
3555       *
3556       * @return array
3557       */
3558      public function course_dates_reset_provider() {
3559  
3560          // Each example contains the following:
3561          // - course startdate
3562          // - course enddate
3563          // - startdate to reset to (false if not reset)
3564          // - enddate to reset to (false if not reset)
3565          // - resulting startdate
3566          // - resulting enddate
3567          $time = 1445644800;
3568          return [
3569              // No date changes.
3570              [
3571                  $time,
3572                  $time + DAYSECS,
3573                  false,
3574                  false,
3575                  $time,
3576                  $time + DAYSECS
3577              ],
3578              // End date changes to a valid value.
3579              [
3580                  $time,
3581                  $time + DAYSECS,
3582                  false,
3583                  $time + DAYSECS + 111,
3584                  $time,
3585                  $time + DAYSECS + 111
3586              ],
3587              // Start date changes to a valid value. End date does not get updated because it does not have value.
3588              [
3589                  $time,
3590                  0,
3591                  $time + DAYSECS,
3592                  false,
3593                  $time + DAYSECS,
3594                  0
3595              ],
3596              // Start date changes to a valid value. End date gets updated accordingly.
3597              [
3598                  $time,
3599                  $time + DAYSECS,
3600                  $time + WEEKSECS,
3601                  false,
3602                  $time + WEEKSECS,
3603                  $time + WEEKSECS + DAYSECS
3604              ],
3605              // Start date and end date change to a valid value.
3606              [
3607                  $time,
3608                  $time + DAYSECS,
3609                  $time + WEEKSECS,
3610                  $time + YEARSECS,
3611                  $time + WEEKSECS,
3612                  $time + YEARSECS
3613              ]
3614          ];
3615      }
3616  
3617      /**
3618       * Test reset_course_userdata()
3619       *    - with reset_roles_overrides enabled
3620       *    - with selective role unenrolments
3621       */
3622      public function test_course_roles_reset() {
3623          global $DB;
3624  
3625          $this->resetAfterTest(true);
3626  
3627          $generator = $this->getDataGenerator();
3628  
3629          // Create test course and user, enrol one in the other.
3630          $course = $generator->create_course();
3631          $user = $generator->create_user();
3632          $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'), MUST_EXIST);
3633          $generator->enrol_user($user->id, $course->id, $roleid);
3634  
3635          // Test case with reset_roles_overrides enabled.
3636          // Override course so it does NOT allow students 'mod/forum:viewdiscussion'.
3637          $coursecontext = context_course::instance($course->id);
3638          assign_capability('mod/forum:viewdiscussion', CAP_PREVENT, $roleid, $coursecontext->id);
3639  
3640          // Check expected capabilities so far.
3641          $this->assertFalse(has_capability('mod/forum:viewdiscussion', $coursecontext, $user));
3642  
3643          // Oops, preventing student from viewing forums was a mistake, let's reset the course.
3644          $resetdata = new stdClass();
3645          $resetdata->id = $course->id;
3646          $resetdata->reset_roles_overrides = true;
3647          reset_course_userdata($resetdata);
3648  
3649          // Check new expected capabilities - override at the course level should be reset.
3650          $this->assertTrue(has_capability('mod/forum:viewdiscussion', $coursecontext, $user));
3651  
3652          // Test case with selective role unenrolments.
3653          $roles = array();
3654          $roles['student'] = $DB->get_field('role', 'id', array('shortname' => 'student'), MUST_EXIST);
3655          $roles['teacher'] = $DB->get_field('role', 'id', array('shortname' => 'teacher'), MUST_EXIST);
3656  
3657          // We enrol a user with student and teacher roles.
3658          $generator->enrol_user($user->id, $course->id, $roles['student']);
3659          $generator->enrol_user($user->id, $course->id, $roles['teacher']);
3660  
3661          // When we reset only student role, we expect to keep teacher role.
3662          $resetdata = new stdClass();
3663          $resetdata->id = $course->id;
3664          $resetdata->unenrol_users = array($roles['student']);
3665          reset_course_userdata($resetdata);
3666  
3667          $usersroles = enrol_get_course_users_roles($course->id);
3668          $this->assertArrayHasKey($user->id, $usersroles);
3669          $this->assertArrayHasKey($roles['teacher'], $usersroles[$user->id]);
3670          $this->assertArrayNotHasKey($roles['student'], $usersroles[$user->id]);
3671          $this->assertCount(1, $usersroles[$user->id]);
3672  
3673          // We reenrol user as student.
3674          $generator->enrol_user($user->id, $course->id, $roles['student']);
3675  
3676          // When we reset student and teacher roles, we expect no roles left.
3677          $resetdata = new stdClass();
3678          $resetdata->id = $course->id;
3679          $resetdata->unenrol_users = array($roles['student'], $roles['teacher']);
3680          reset_course_userdata($resetdata);
3681  
3682          $usersroles = enrol_get_course_users_roles($course->id);
3683          $this->assertEmpty($usersroles);
3684      }
3685  
3686      public function test_course_check_module_updates_since() {
3687          global $CFG, $DB, $USER;
3688          require_once($CFG->dirroot . '/mod/glossary/lib.php');
3689          require_once($CFG->dirroot . '/rating/lib.php');
3690          require_once($CFG->dirroot . '/comment/lib.php');
3691  
3692          $this->resetAfterTest(true);
3693  
3694          $CFG->enablecompletion = true;
3695          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
3696          $glossary = $this->getDataGenerator()->create_module('glossary', array(
3697              'course' => $course->id,
3698              'completion' => COMPLETION_TRACKING_AUTOMATIC,
3699              'completionview' => 1,
3700              'allowcomments' => 1,
3701              'assessed' => RATING_AGGREGATE_AVERAGE,
3702              'scale' => 100
3703          ));
3704          $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
3705          $context = context_module::instance($glossary->cmid);
3706          $modinfo = get_fast_modinfo($course);
3707          $cm = $modinfo->get_cm($glossary->cmid);
3708          $user = $this->getDataGenerator()->create_user();
3709          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
3710          $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
3711          $from = time();
3712  
3713          $teacher = $this->getDataGenerator()->create_user();
3714          $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
3715          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
3716  
3717          assign_capability('mod/glossary:viewanyrating', CAP_ALLOW, $studentrole->id, $context->id, true);
3718  
3719          // Check nothing changed right now.
3720          $updates = course_check_module_updates_since($cm, $from);
3721          $this->assertFalse($updates->configuration->updated);
3722          $this->assertFalse($updates->completion->updated);
3723          $this->assertFalse($updates->gradeitems->updated);
3724          $this->assertFalse($updates->comments->updated);
3725          $this->assertFalse($updates->ratings->updated);
3726          $this->assertFalse($updates->introfiles->updated);
3727          $this->assertFalse($updates->outcomes->updated);
3728  
3729          $this->waitForSecond();
3730  
3731          // Do some changes.
3732          $this->setUser($user);
3733          $entry = $glossarygenerator->create_content($glossary);
3734  
3735          $this->setUser($teacher);
3736          // Name.
3737          set_coursemodule_name($glossary->cmid, 'New name');
3738  
3739          // Add some ratings.
3740          $rm = new rating_manager();
3741          $result = $rm->add_rating($cm, $context, 'mod_glossary', 'entry', $entry->id, 100, 50, $user->id, RATING_AGGREGATE_AVERAGE);
3742  
3743          // Change grades.
3744          $glossary->cmidnumber = $glossary->cmid;
3745          glossary_update_grades($glossary, $user->id);
3746  
3747          $this->setUser($user);
3748          // Completion status.
3749          glossary_view($glossary, $course, $cm, $context, 'letter');
3750  
3751          // Add one comment.
3752          $args = new stdClass;
3753          $args->context   = $context;
3754          $args->course    = $course;
3755          $args->cm        = $cm;
3756          $args->area      = 'glossary_entry';
3757          $args->itemid    = $entry->id;
3758          $args->client_id = 1;
3759          $args->component = 'mod_glossary';
3760          $manager = new comment($args);
3761          $manager->add('blah blah blah');
3762  
3763          // Check upgrade status.
3764          $updates = course_check_module_updates_since($cm, $from);
3765          $this->assertTrue($updates->configuration->updated);
3766          $this->assertTrue($updates->completion->updated);
3767          $this->assertTrue($updates->gradeitems->updated);
3768          $this->assertTrue($updates->comments->updated);
3769          $this->assertTrue($updates->ratings->updated);
3770          $this->assertFalse($updates->introfiles->updated);
3771          $this->assertFalse($updates->outcomes->updated);
3772      }
3773  
3774      public function test_async_module_deletion_hook_implemented() {
3775          // Async module deletion depends on the 'true' being returned by at least one plugin implementing the hook,
3776          // 'course_module_adhoc_deletion_recommended'. In core, is implemented by the course recyclebin, which will only return
3777          // true if the recyclebin plugin is enabled. To make sure async deletion occurs, this test force-enables the recyclebin.
3778          global $DB, $USER;
3779          $this->resetAfterTest(true);
3780          $this->setAdminUser();
3781  
3782          // Ensure recyclebin is enabled.
3783          set_config('coursebinenable', true, 'tool_recyclebin');
3784  
3785          // Create course, module and context.
3786          $course = $this->getDataGenerator()->create_course(['numsections' => 5]);
3787          $module = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
3788          $modcontext = context_module::instance($module->cmid);
3789  
3790          // Verify context exists.
3791          $this->assertInstanceOf('context_module', $modcontext);
3792  
3793          // Check events generated on the course_delete_module call.
3794          $sink = $this->redirectEvents();
3795  
3796          // Try to delete the module using the async flag.
3797          course_delete_module($module->cmid, true); // Try to delete the module asynchronously.
3798  
3799          // Verify that no event has been generated yet.
3800          $events = $sink->get_events();
3801          $event = array_pop($events);
3802          $sink->close();
3803          $this->assertEmpty($event);
3804  
3805          // Grab the record, in it's final state before hard deletion, for comparison with the event snapshot.
3806          // We need to do this because the 'deletioninprogress' flag has changed from '0' to '1'.
3807          $cm = $DB->get_record('course_modules', ['id' => $module->cmid], '*', MUST_EXIST);
3808  
3809          // Verify the course_module is marked as 'deletioninprogress'.
3810          $this->assertNotEquals($cm, false);
3811          $this->assertEquals($cm->deletioninprogress, '1');
3812  
3813          // Verify the context has not yet been removed.
3814          $this->assertEquals($modcontext, context_module::instance($module->cmid, IGNORE_MISSING));
3815  
3816          // Set up a sink to catch the 'course_module_deleted' event.
3817          $sink = $this->redirectEvents();
3818  
3819          // Now, run the adhoc task which performs the hard deletion.
3820          phpunit_util::run_all_adhoc_tasks();
3821  
3822          // Fetch and validate the event data.
3823          $events = $sink->get_events();
3824          $event = array_pop($events);
3825          $sink->close();
3826          $this->assertInstanceOf('\core\event\course_module_deleted', $event);
3827          $this->assertEquals($module->cmid, $event->objectid);
3828          $this->assertEquals($USER->id, $event->userid);
3829          $this->assertEquals('course_modules', $event->objecttable);
3830          $this->assertEquals(null, $event->get_url());
3831          $this->assertEquals($cm, $event->get_record_snapshot('course_modules', $module->cmid));
3832  
3833          // Verify the context has been removed.
3834          $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
3835  
3836          // Verify the course_module record has been deleted.
3837          $cmcount = $DB->count_records('course_modules', ['id' => $module->cmid]);
3838          $this->assertEmpty($cmcount);
3839      }
3840  
3841      public function test_async_module_deletion_hook_not_implemented() {
3842          // Only proceed if we are sure that no plugin is going to advocate async removal of a module. I.e. no plugin returns
3843          // 'true' from the 'course_module_adhoc_deletion_recommended' hook.
3844          // In the case of core, only recyclebin implements this hook, and it will only return true if enabled, so disable it.
3845          global $DB, $USER;
3846          $this->resetAfterTest(true);
3847          $this->setAdminUser();
3848          set_config('coursebinenable', false, 'tool_recyclebin');
3849  
3850          // Non-core plugins might implement the 'course_module_adhoc_deletion_recommended' hook and spoil this test.
3851          // If at least one plugin still returns true, then skip this test.
3852          if ($pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
3853              foreach ($pluginsfunction as $plugintype => $plugins) {
3854                  foreach ($plugins as $pluginfunction) {
3855                      if ($pluginfunction()) {
3856                          $this->markTestSkipped();
3857                      }
3858                  }
3859              }
3860          }
3861  
3862          // Create course, module and context.
3863          $course = $this->getDataGenerator()->create_course(['numsections' => 5]);
3864          $module = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
3865          $modcontext = context_module::instance($module->cmid);
3866          $cm = $DB->get_record('course_modules', ['id' => $module->cmid], '*', MUST_EXIST);
3867  
3868          // Verify context exists.
3869          $this->assertInstanceOf('context_module', $modcontext);
3870  
3871          // Check events generated on the course_delete_module call.
3872          $sink = $this->redirectEvents();
3873  
3874          // Try to delete the module using the async flag.
3875          course_delete_module($module->cmid, true); // Try to delete the module asynchronously.
3876  
3877          // Fetch and validate the event data.
3878          $events = $sink->get_events();
3879          $event = array_pop($events);
3880          $sink->close();
3881          $this->assertInstanceOf('\core\event\course_module_deleted', $event);
3882          $this->assertEquals($module->cmid, $event->objectid);
3883          $this->assertEquals($USER->id, $event->userid);
3884          $this->assertEquals('course_modules', $event->objecttable);
3885          $this->assertEquals(null, $event->get_url());
3886          $this->assertEquals($cm, $event->get_record_snapshot('course_modules', $module->cmid));
3887  
3888          // Verify the context has been removed.
3889          $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
3890  
3891          // Verify the course_module record has been deleted.
3892          $cmcount = $DB->count_records('course_modules', ['id' => $module->cmid]);
3893          $this->assertEmpty($cmcount);
3894      }
3895  
3896      public function test_async_section_deletion_hook_implemented() {
3897          // Async section deletion (provided section contains modules), depends on the 'true' being returned by at least one plugin
3898          // implementing the 'course_module_adhoc_deletion_recommended' hook. In core, is implemented by the course recyclebin,
3899          // which will only return true if the plugin is enabled. To make sure async deletion occurs, this test enables recyclebin.
3900          global $DB, $USER;
3901          $this->resetAfterTest(true);
3902          $this->setAdminUser();
3903  
3904          // Ensure recyclebin is enabled.
3905          set_config('coursebinenable', true, 'tool_recyclebin');
3906  
3907          // Create course, module and context.
3908          $generator = $this->getDataGenerator();
3909          $course = $generator->create_course(['numsections' => 4, 'format' => 'topics'], ['createsections' => true]);
3910          $assign0 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
3911          $assign1 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
3912          $assign2 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
3913          $assign3 = $generator->create_module('assign', ['course' => $course, 'section' => 0]);
3914  
3915          // Delete empty section. No difference from normal, synchronous behaviour.
3916          $this->assertTrue(course_delete_section($course, 4, false, true));
3917          $this->assertEquals(3, course_get_format($course)->get_last_section_number());
3918  
3919          // Delete a module in section 2 (using async). Need to verify this doesn't generate two tasks when we delete
3920          // the section in the next step.
3921          course_delete_module($assign2->cmid, true);
3922  
3923          // Confirm that the module is pending deletion in its current section.
3924          $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => '2']); // For event comparison.
3925          $this->assertEquals(true, $DB->record_exists('course_modules', ['id' => $assign2->cmid, 'deletioninprogress' => 1,
3926                                                       'section' => $section->id]));
3927  
3928          // Now, delete section 2.
3929          $this->assertFalse(course_delete_section($course, 2, false, true)); // Non-empty section, no forcedelete, so no change.
3930  
3931          $sink = $this->redirectEvents(); // To capture the event.
3932          $this->assertTrue(course_delete_section($course, 2, true, true));
3933  
3934          // Now, confirm that:
3935          // a) the section's modules have been flagged for deletion and moved to section 0 and;
3936          // b) the section has been deleted and;
3937          // c) course_section_deleted event has been fired. The course_module_deleted events will only fire once they have been
3938          // removed from section 0 via the adhoc task.
3939  
3940          // Modules should have been flagged for deletion and moved to section 0.
3941          $sectionid = $DB->get_field('course_sections', 'id', ['course' => $course->id, 'section' => 0]);
3942          $this->assertEquals(3, $DB->count_records('course_modules', ['section' => $sectionid, 'deletioninprogress' => 1]));
3943  
3944          // Confirm the section has been deleted.
3945          $this->assertEquals(2, course_get_format($course)->get_last_section_number());
3946  
3947          // Check event fired.
3948          $events = $sink->get_events();
3949          $event = array_pop($events);
3950          $sink->close();
3951          $this->assertInstanceOf('\core\event\course_section_deleted', $event);
3952          $this->assertEquals($section->id, $event->objectid);
3953          $this->assertEquals($USER->id, $event->userid);
3954          $this->assertEquals('course_sections', $event->objecttable);
3955          $this->assertEquals(null, $event->get_url());
3956          $this->assertEquals($section, $event->get_record_snapshot('course_sections', $section->id));
3957  
3958          // Now, run the adhoc task to delete the modules from section 0.
3959          $sink = $this->redirectEvents(); // To capture the events.
3960          phpunit_util::run_all_adhoc_tasks();
3961  
3962          // Confirm the modules have been deleted.
3963          list($insql, $assignids) = $DB->get_in_or_equal([$assign0->cmid, $assign1->cmid, $assign2->cmid]);
3964          $cmcount = $DB->count_records_select('course_modules', 'id ' . $insql,  $assignids);
3965          $this->assertEmpty($cmcount);
3966  
3967          // Confirm other modules in section 0 still remain.
3968          $this->assertEquals(1, $DB->count_records('course_modules', ['id' => $assign3->cmid]));
3969  
3970          // Confirm that events were generated for all 3 of the modules.
3971          $events = $sink->get_events();
3972          $sink->close();
3973          $count = 0;
3974          while (!empty($events)) {
3975              $event = array_pop($events);
3976              if ($event instanceof \core\event\course_module_deleted &&
3977                  in_array($event->objectid, [$assign0->cmid, $assign1->cmid, $assign2->cmid])) {
3978                  $count++;
3979              }
3980          }
3981          $this->assertEquals(3, $count);
3982      }
3983  
3984      public function test_async_section_deletion_hook_not_implemented() {
3985          // If no plugins advocate async removal, then normal synchronous removal will take place.
3986          // Only proceed if we are sure that no plugin is going to advocate async removal of a module. I.e. no plugin returns
3987          // 'true' from the 'course_module_adhoc_deletion_recommended' hook.
3988          // In the case of core, only recyclebin implements this hook, and it will only return true if enabled, so disable it.
3989          global $DB, $USER;
3990          $this->resetAfterTest(true);
3991          $this->setAdminUser();
3992          set_config('coursebinenable', false, 'tool_recyclebin');
3993  
3994          // Non-core plugins might implement the 'course_module_adhoc_deletion_recommended' hook and spoil this test.
3995          // If at least one plugin still returns true, then skip this test.
3996          if ($pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
3997              foreach ($pluginsfunction as $plugintype => $plugins) {
3998                  foreach ($plugins as $pluginfunction) {
3999                      if ($pluginfunction()) {
4000                          $this->markTestSkipped();
4001                      }
4002                  }
4003              }
4004          }
4005  
4006          // Create course, module and context.
4007          $generator = $this->getDataGenerator();
4008          $course = $generator->create_course(['numsections' => 4, 'format' => 'topics'], ['createsections' => true]);
4009          $assign0 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
4010          $assign1 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
4011  
4012          // Delete empty section. No difference from normal, synchronous behaviour.
4013          $this->assertTrue(course_delete_section($course, 4, false, true));
4014          $this->assertEquals(3, course_get_format($course)->get_last_section_number());
4015  
4016          // Delete section in the middle (2).
4017          $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => '2']); // For event comparison.
4018          $this->assertFalse(course_delete_section($course, 2, false, true)); // Non-empty section, no forcedelete, so no change.
4019  
4020          $sink = $this->redirectEvents(); // To capture the event.
4021          $this->assertTrue(course_delete_section($course, 2, true, true));
4022  
4023          // Now, confirm that:
4024          // a) The section's modules have deleted and;
4025          // b) the section has been deleted and;
4026          // c) course_section_deleted event has been fired and;
4027          // d) course_module_deleted events have both been fired.
4028  
4029          // Confirm modules have been deleted.
4030          list($insql, $assignids) = $DB->get_in_or_equal([$assign0->cmid, $assign1->cmid]);
4031          $cmcount = $DB->count_records_select('course_modules', 'id ' . $insql, $assignids);
4032          $this->assertEmpty($cmcount);
4033  
4034          // Confirm the section has been deleted.
4035          $this->assertEquals(2, course_get_format($course)->get_last_section_number());
4036  
4037          // Confirm the course_section_deleted event has been generated.
4038          $events = $sink->get_events();
4039          $event = array_pop($events);
4040          $sink->close();
4041          $this->assertInstanceOf('\core\event\course_section_deleted', $event);
4042          $this->assertEquals($section->id, $event->objectid);
4043          $this->assertEquals($USER->id, $event->userid);
4044          $this->assertEquals('course_sections', $event->objecttable);
4045          $this->assertEquals(null, $event->get_url());
4046          $this->assertEquals($section, $event->get_record_snapshot('course_sections', $section->id));
4047  
4048          // Confirm that the course_module_deleted events have both been generated.
4049          $count = 0;
4050          while (!empty($events)) {
4051              $event = array_pop($events);
4052              if ($event instanceof \core\event\course_module_deleted &&
4053                  in_array($event->objectid, [$assign0->cmid, $assign1->cmid])) {
4054                  $count++;
4055              }
4056          }
4057          $this->assertEquals(2, $count);
4058      }
4059  
4060      public function test_classify_course_for_timeline() {
4061          global $DB, $CFG;
4062  
4063          require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
4064  
4065          set_config('enablecompletion', COMPLETION_ENABLED);
4066          set_config('coursegraceperiodbefore', 0);
4067          set_config('coursegraceperiodafter', 0);
4068  
4069          $this->resetAfterTest(true);
4070          $this->setAdminUser();
4071  
4072          // Create courses for testing.
4073          $generator = $this->getDataGenerator();
4074          $future = time() + 3600;
4075          $past = time() - 3600;
4076          $futurecourse = $generator->create_course(['startdate' => $future]);
4077          $pastcourse = $generator->create_course(['startdate' => $past - 60, 'enddate' => $past]);
4078          $completedcourse = $generator->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4079          $inprogresscourse = $generator->create_course();
4080  
4081          // Set completion rules.
4082          $criteriadata = new stdClass();
4083          $criteriadata->id = $completedcourse->id;
4084  
4085          // Self completion.
4086          $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF;
4087          $class = 'completion_criteria_self';
4088          $criterion = new $class();
4089          $criterion->update_config($criteriadata);
4090  
4091          $user = $this->getDataGenerator()->create_user();
4092          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
4093          $this->getDataGenerator()->enrol_user($user->id, $futurecourse->id, $studentrole->id);
4094          $this->getDataGenerator()->enrol_user($user->id, $pastcourse->id, $studentrole->id);
4095          $this->getDataGenerator()->enrol_user($user->id, $completedcourse->id, $studentrole->id);
4096          $this->getDataGenerator()->enrol_user($user->id, $inprogresscourse->id, $studentrole->id);
4097  
4098          $this->setUser($user);
4099          core_completion_external::mark_course_self_completed($completedcourse->id);
4100          $ccompletion = new completion_completion(array('course' => $completedcourse->id, 'userid' => $user->id));
4101          $ccompletion->mark_complete();
4102  
4103          // Aggregate the completions.
4104          $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($pastcourse));
4105          $this->assertEquals(COURSE_TIMELINE_FUTURE, course_classify_for_timeline($futurecourse));
4106          $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
4107          $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
4108  
4109          // Test grace period.
4110          set_config('coursegraceperiodafter', 1);
4111          set_config('coursegraceperiodbefore', 1);
4112          $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($pastcourse));
4113          $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($futurecourse));
4114          $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
4115          $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
4116      }
4117  
4118      /**
4119       * Test the main function for updating all calendar events for a module.
4120       */
4121      public function test_course_module_calendar_event_update_process() {
4122          global $DB;
4123  
4124          $this->resetAfterTest();
4125          $this->setAdminUser();
4126  
4127          $completionexpected = time();
4128          $duedate = time();
4129  
4130          $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4131          $assign = $this->getDataGenerator()->create_module('assign', [
4132                      'course' => $course,
4133                      'completionexpected' => $completionexpected,
4134                      'duedate' => $duedate
4135                  ]);
4136  
4137          $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
4138          $events = $DB->get_records('event', ['courseid' => $course->id, 'instance' => $assign->id]);
4139          // Check that both events are using the expected dates.
4140          foreach ($events as $event) {
4141              if ($event->eventtype == \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) {
4142                  $this->assertEquals($completionexpected, $event->timestart);
4143              }
4144              if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
4145                  $this->assertEquals($duedate, $event->timestart);
4146              }
4147          }
4148  
4149          // We have to manually update the module and the course module.
4150          $newcompletionexpected = time() + DAYSECS * 60;
4151          $newduedate = time() + DAYSECS * 45;
4152          $newmodulename = 'Assign - new name';
4153  
4154          $moduleobject = (object)array('id' => $assign->id, 'duedate' => $newduedate, 'name' => $newmodulename);
4155          $DB->update_record('assign', $moduleobject);
4156          $cmobject = (object)array('id' => $cm->id, 'completionexpected' => $newcompletionexpected);
4157          $DB->update_record('course_modules', $cmobject);
4158  
4159          $assign = $DB->get_record('assign', ['id' => $assign->id]);
4160          $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
4161  
4162          course_module_calendar_event_update_process($assign, $cm);
4163  
4164          $events = $DB->get_records('event', ['courseid' => $course->id, 'instance' => $assign->id]);
4165          // Now check that the details have been updated properly from the function.
4166          foreach ($events as $event) {
4167              if ($event->eventtype == \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) {
4168                  $this->assertEquals($newcompletionexpected, $event->timestart);
4169                  $this->assertEquals(get_string('completionexpectedfor', 'completion', (object)['instancename' => $newmodulename]),
4170                          $event->name);
4171              }
4172              if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
4173                  $this->assertEquals($newduedate, $event->timestart);
4174                  $this->assertEquals(get_string('calendardue', 'assign', $newmodulename), $event->name);
4175              }
4176          }
4177      }
4178  
4179      /**
4180       * Test the higher level checks for updating calendar events for an instance.
4181       */
4182      public function test_course_module_update_calendar_events() {
4183          $this->resetAfterTest();
4184          $this->setAdminUser();
4185  
4186          $completionexpected = time();
4187          $duedate = time();
4188  
4189          $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4190          $assign = $this->getDataGenerator()->create_module('assign', [
4191                      'course' => $course,
4192                      'completionexpected' => $completionexpected,
4193                      'duedate' => $duedate
4194                  ]);
4195  
4196          $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
4197  
4198          // Both the instance and cm objects are missing.
4199          $this->assertFalse(course_module_update_calendar_events('assign'));
4200          // Just using the assign instance.
4201          $this->assertTrue(course_module_update_calendar_events('assign', $assign));
4202          // Just using the course module object.
4203          $this->assertTrue(course_module_update_calendar_events('assign', null, $cm));
4204          // Using both the assign instance and the course module object.
4205          $this->assertTrue(course_module_update_calendar_events('assign', $assign, $cm));
4206      }
4207  
4208      /**
4209       * Test the higher level checks for updating calendar events for a module.
4210       */
4211      public function test_course_module_bulk_update_calendar_events() {
4212          $this->resetAfterTest();
4213          $this->setAdminUser();
4214  
4215          $completionexpected = time();
4216          $duedate = time();
4217  
4218          $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4219          $course2 = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4220          $assign = $this->getDataGenerator()->create_module('assign', [
4221                      'course' => $course,
4222                      'completionexpected' => $completionexpected,
4223                      'duedate' => $duedate
4224                  ]);
4225  
4226          // No assign instances in this course.
4227          $this->assertFalse(course_module_bulk_update_calendar_events('assign', $course2->id));
4228          // No book instances for the site.
4229          $this->assertFalse(course_module_bulk_update_calendar_events('book'));
4230          // Update all assign instances.
4231          $this->assertTrue(course_module_bulk_update_calendar_events('assign'));
4232          // Update the assign instances for this course.
4233          $this->assertTrue(course_module_bulk_update_calendar_events('assign', $course->id));
4234      }
4235  
4236      /**
4237       * Test that a student can view participants in a course they are enrolled in.
4238       */
4239      public function test_course_can_view_participants_as_student() {
4240          $this->resetAfterTest();
4241  
4242          $course = $this->getDataGenerator()->create_course();
4243          $coursecontext = context_course::instance($course->id);
4244  
4245          $user = $this->getDataGenerator()->create_user();
4246          $this->getDataGenerator()->enrol_user($user->id, $course->id);
4247  
4248          $this->setUser($user);
4249  
4250          $this->assertTrue(course_can_view_participants($coursecontext));
4251      }
4252  
4253      /**
4254       * Test that a student in a course can not view participants on the site.
4255       */
4256      public function test_course_can_view_participants_as_student_on_site() {
4257          $this->resetAfterTest();
4258  
4259          $course = $this->getDataGenerator()->create_course();
4260  
4261          $user = $this->getDataGenerator()->create_user();
4262          $this->getDataGenerator()->enrol_user($user->id, $course->id);
4263  
4264          $this->setUser($user);
4265  
4266          $this->assertFalse(course_can_view_participants(context_system::instance()));
4267      }
4268  
4269      /**
4270       * Test that an admin can view participants on the site.
4271       */
4272      public function test_course_can_view_participants_as_admin_on_site() {
4273          $this->resetAfterTest();
4274  
4275          $this->setAdminUser();
4276  
4277          $this->assertTrue(course_can_view_participants(context_system::instance()));
4278      }
4279  
4280      /**
4281       * Test teachers can view participants in a course they are enrolled in.
4282       */
4283      public function test_course_can_view_participants_as_teacher() {
4284          global $DB;
4285  
4286          $this->resetAfterTest();
4287  
4288          $course = $this->getDataGenerator()->create_course();
4289          $coursecontext = context_course::instance($course->id);
4290  
4291          $user = $this->getDataGenerator()->create_user();
4292          $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4293          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4294  
4295          $this->setUser($user);
4296  
4297          $this->assertTrue(course_can_view_participants($coursecontext));
4298      }
4299  
4300      /**
4301       * Check the teacher can still view the participants page without the 'viewparticipants' cap.
4302       */
4303      public function test_course_can_view_participants_as_teacher_without_view_participants_cap() {
4304          global $DB;
4305  
4306          $this->resetAfterTest();
4307  
4308          $course = $this->getDataGenerator()->create_course();
4309          $coursecontext = context_course::instance($course->id);
4310  
4311          $user = $this->getDataGenerator()->create_user();
4312          $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4313          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4314  
4315          $this->setUser($user);
4316  
4317          // Disable one of the capabilties.
4318          assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
4319  
4320          // Should still be able to view the page as they have the 'moodle/course:enrolreview' cap.
4321          $this->assertTrue(course_can_view_participants($coursecontext));
4322      }
4323  
4324      /**
4325       * Check the teacher can still view the participants page without the 'moodle/course:enrolreview' cap.
4326       */
4327      public function test_course_can_view_participants_as_teacher_without_enrol_review_cap() {
4328          global $DB;
4329  
4330          $this->resetAfterTest();
4331  
4332          $course = $this->getDataGenerator()->create_course();
4333          $coursecontext = context_course::instance($course->id);
4334  
4335          $user = $this->getDataGenerator()->create_user();
4336          $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4337          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4338  
4339          $this->setUser($user);
4340  
4341          // Disable one of the capabilties.
4342          assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
4343  
4344          // Should still be able to view the page as they have the 'moodle/course:viewparticipants' cap.
4345          $this->assertTrue(course_can_view_participants($coursecontext));
4346      }
4347  
4348      /**
4349       * Check the teacher can not view the participants page without the required caps.
4350       */
4351      public function test_course_can_view_participants_as_teacher_without_required_caps() {
4352          global $DB;
4353  
4354          $this->resetAfterTest();
4355  
4356          $course = $this->getDataGenerator()->create_course();
4357          $coursecontext = context_course::instance($course->id);
4358  
4359          $user = $this->getDataGenerator()->create_user();
4360          $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4361          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4362  
4363          $this->setUser($user);
4364  
4365          // Disable the capabilities.
4366          assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
4367          assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
4368  
4369          $this->assertFalse(course_can_view_participants($coursecontext));
4370      }
4371  
4372      /**
4373       * Check that an exception is not thrown if we can view the participants page.
4374       */
4375      public function test_course_require_view_participants() {
4376          $this->resetAfterTest();
4377  
4378          $course = $this->getDataGenerator()->create_course();
4379          $coursecontext = context_course::instance($course->id);
4380  
4381          $user = $this->getDataGenerator()->create_user();
4382          $this->getDataGenerator()->enrol_user($user->id, $course->id);
4383  
4384          $this->setUser($user);
4385  
4386          course_require_view_participants($coursecontext);
4387      }
4388  
4389      /**
4390       * Check that an exception is thrown if we can't view the participants page.
4391       */
4392      public function test_course_require_view_participants_as_student_on_site() {
4393          $this->resetAfterTest();
4394  
4395          $course = $this->getDataGenerator()->create_course();
4396  
4397          $user = $this->getDataGenerator()->create_user();
4398          $this->getDataGenerator()->enrol_user($user->id, $course->id);
4399  
4400          $this->setUser($user);
4401  
4402          $this->expectException('required_capability_exception');
4403          course_require_view_participants(context_system::instance());
4404      }
4405  
4406      /**
4407       *  Testing the can_download_from_backup_filearea fn.
4408       */
4409      public function test_can_download_from_backup_filearea() {
4410          global $DB;
4411          $this->resetAfterTest();
4412          $course = $this->getDataGenerator()->create_course();
4413          $context = context_course::instance($course->id);
4414          $user = $this->getDataGenerator()->create_user();
4415          $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
4416          $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id);
4417  
4418          // The 'automated' backup area. Downloading from this area requires two capabilities.
4419          // If the user has only the 'backup:downloadfile' capability.
4420          unassign_capability('moodle/restore:userinfo', $teacherrole->id, $context);
4421          assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4422          $this->assertFalse(can_download_from_backup_filearea('automated', $context, $user));
4423  
4424          // If the user has only the 'restore:userinfo' capability.
4425          unassign_capability('moodle/backup:downloadfile', $teacherrole->id, $context);
4426          assign_capability('moodle/restore:userinfo', CAP_ALLOW, $teacherrole->id, $context);
4427          $this->assertFalse(can_download_from_backup_filearea('automated', $context, $user));
4428  
4429          // If the user has both capabilities.
4430          assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4431          assign_capability('moodle/restore:userinfo', CAP_ALLOW, $teacherrole->id, $context);
4432          $this->assertTrue(can_download_from_backup_filearea('automated', $context, $user));
4433  
4434          // Is the user has neither of the capabilities.
4435          unassign_capability('moodle/backup:downloadfile', $teacherrole->id, $context);
4436          unassign_capability('moodle/restore:userinfo', $teacherrole->id, $context);
4437          $this->assertFalse(can_download_from_backup_filearea('automated', $context, $user));
4438  
4439          // The 'course ' and 'backup' backup file areas. These are governed by the same download capability.
4440          // User has the capability.
4441          unassign_capability('moodle/restore:userinfo', $teacherrole->id, $context);
4442          assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4443          $this->assertTrue(can_download_from_backup_filearea('course', $context, $user));
4444          $this->assertTrue(can_download_from_backup_filearea('backup', $context, $user));
4445  
4446          // User doesn't have the capability.
4447          unassign_capability('moodle/backup:downloadfile', $teacherrole->id, $context);
4448          $this->assertFalse(can_download_from_backup_filearea('course', $context, $user));
4449          $this->assertFalse(can_download_from_backup_filearea('backup', $context, $user));
4450  
4451          // A file area that doesn't exist. No permissions, regardless of capabilities.
4452          assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4453          $this->assertFalse(can_download_from_backup_filearea('testing', $context, $user));
4454      }
4455  
4456      /**
4457       * Test cases for the course_classify_courses_for_timeline test.
4458       */
4459      public function get_course_classify_courses_for_timeline_test_cases() {
4460          $now = time();
4461          $day = 86400;
4462  
4463          return [
4464              'no courses' => [
4465                  'coursesdata' => [],
4466                  'expected' => [
4467                      COURSE_TIMELINE_PAST => [],
4468                      COURSE_TIMELINE_FUTURE => [],
4469                      COURSE_TIMELINE_INPROGRESS => []
4470                  ]
4471              ],
4472              'only past' => [
4473                  'coursesdata' => [
4474                      [
4475                          'shortname' => 'past1',
4476                          'startdate' => $now - ($day * 2),
4477                          'enddate' => $now - $day
4478                      ],
4479                      [
4480                          'shortname' => 'past2',
4481                          'startdate' => $now - ($day * 2),
4482                          'enddate' => $now - $day
4483                      ]
4484                  ],
4485                  'expected' => [
4486                      COURSE_TIMELINE_PAST => ['past1', 'past2'],
4487                      COURSE_TIMELINE_FUTURE => [],
4488                      COURSE_TIMELINE_INPROGRESS => []
4489                  ]
4490              ],
4491              'only in progress' => [
4492                  'coursesdata' => [
4493                      [
4494                          'shortname' => 'inprogress1',
4495                          'startdate' => $now - $day,
4496                          'enddate' => $now + $day
4497                      ],
4498                      [
4499                          'shortname' => 'inprogress2',
4500                          'startdate' => $now - $day,
4501                          'enddate' => $now + $day
4502                      ]
4503                  ],
4504                  'expected' => [
4505                      COURSE_TIMELINE_PAST => [],
4506                      COURSE_TIMELINE_FUTURE => [],
4507                      COURSE_TIMELINE_INPROGRESS => ['inprogress1', 'inprogress2']
4508                  ]
4509              ],
4510              'only future' => [
4511                  'coursesdata' => [
4512                      [
4513                          'shortname' => 'future1',
4514                          'startdate' => $now + $day
4515                      ],
4516                      [
4517                          'shortname' => 'future2',
4518                          'startdate' => $now + $day
4519                      ]
4520                  ],
4521                  'expected' => [
4522                      COURSE_TIMELINE_PAST => [],
4523                      COURSE_TIMELINE_FUTURE => ['future1', 'future2'],
4524                      COURSE_TIMELINE_INPROGRESS => []
4525                  ]
4526              ],
4527              'combination' => [
4528                  'coursesdata' => [
4529                      [
4530                          'shortname' => 'past1',
4531                          'startdate' => $now - ($day * 2),
4532                          'enddate' => $now - $day
4533                      ],
4534                      [
4535                          'shortname' => 'past2',
4536                          'startdate' => $now - ($day * 2),
4537                          'enddate' => $now - $day
4538                      ],
4539                      [
4540                          'shortname' => 'inprogress1',
4541                          'startdate' => $now - $day,
4542                          'enddate' => $now + $day
4543                      ],
4544                      [
4545                          'shortname' => 'inprogress2',
4546                          'startdate' => $now - $day,
4547                          'enddate' => $now + $day
4548                      ],
4549                      [
4550                          'shortname' => 'future1',
4551                          'startdate' => $now + $day
4552                      ],
4553                      [
4554                          'shortname' => 'future2',
4555                          'startdate' => $now + $day
4556                      ]
4557                  ],
4558                  'expected' => [
4559                      COURSE_TIMELINE_PAST => ['past1', 'past2'],
4560                      COURSE_TIMELINE_FUTURE => ['future1', 'future2'],
4561                      COURSE_TIMELINE_INPROGRESS => ['inprogress1', 'inprogress2']
4562                  ]
4563              ],
4564          ];
4565      }
4566  
4567      /**
4568       * Test the course_classify_courses_for_timeline function.
4569       *
4570       * @dataProvider get_course_classify_courses_for_timeline_test_cases()
4571       * @param array $coursesdata Courses to create
4572       * @param array $expected Expected test results.
4573       */
4574      public function test_course_classify_courses_for_timeline($coursesdata, $expected) {
4575          $this->resetAfterTest();
4576          $generator = $this->getDataGenerator();
4577  
4578          $courses = array_map(function($coursedata) use ($generator) {
4579              return $generator->create_course($coursedata);
4580          }, $coursesdata);
4581  
4582          sort($expected[COURSE_TIMELINE_PAST]);
4583          sort($expected[COURSE_TIMELINE_FUTURE]);
4584          sort($expected[COURSE_TIMELINE_INPROGRESS]);
4585  
4586          $results = course_classify_courses_for_timeline($courses);
4587  
4588          $actualpast = array_map(function($result) {
4589              return $result->shortname;
4590          }, $results[COURSE_TIMELINE_PAST]);
4591  
4592          $actualfuture = array_map(function($result) {
4593              return $result->shortname;
4594          }, $results[COURSE_TIMELINE_FUTURE]);
4595  
4596          $actualinprogress = array_map(function($result) {
4597              return $result->shortname;
4598          }, $results[COURSE_TIMELINE_INPROGRESS]);
4599  
4600          sort($actualpast);
4601          sort($actualfuture);
4602          sort($actualinprogress);
4603  
4604          $this->assertEquals($expected[COURSE_TIMELINE_PAST], $actualpast);
4605          $this->assertEquals($expected[COURSE_TIMELINE_FUTURE], $actualfuture);
4606          $this->assertEquals($expected[COURSE_TIMELINE_INPROGRESS], $actualinprogress);
4607      }
4608  
4609      /**
4610       * Test cases for the course_get_enrolled_courses_for_logged_in_user tests.
4611       */
4612      public function get_course_get_enrolled_courses_for_logged_in_user_test_cases() {
4613          $buildexpectedresult = function($limit, $offset) {
4614              $result = [];
4615              for ($i = $offset; $i < $offset + $limit; $i++) {
4616                  $result[] = "testcourse{$i}";
4617              }
4618              return $result;
4619          };
4620  
4621          return [
4622              'zero records' => [
4623                  'dbquerylimit' => 3,
4624                  'totalcourses' => 0,
4625                  'limit' => 0,
4626                  'offset' => 0,
4627                  'expecteddbqueries' => 4,
4628                  'expectedresult' => $buildexpectedresult(0, 0)
4629              ],
4630              'less than query limit' => [
4631                  'dbquerylimit' => 3,
4632                  'totalcourses' => 2,
4633                  'limit' => 0,
4634                  'offset' => 0,
4635                  'expecteddbqueries' => 2,
4636                  'expectedresult' => $buildexpectedresult(2, 0)
4637              ],
4638              'more than query limit' => [
4639                  'dbquerylimit' => 3,
4640                  'totalcourses' => 7,
4641                  'limit' => 0,
4642                  'offset' => 0,
4643                  'expecteddbqueries' => 4,
4644                  'expectedresult' => $buildexpectedresult(7, 0)
4645              ],
4646              'limit less than query limit' => [
4647                  'dbquerylimit' => 3,
4648                  'totalcourses' => 7,
4649                  'limit' => 2,
4650                  'offset' => 0,
4651                  'expecteddbqueries' => 2,
4652                  'expectedresult' => $buildexpectedresult(2, 0)
4653              ],
4654              'limit less than query limit with offset' => [
4655                  'dbquerylimit' => 3,
4656                  'totalcourses' => 7,
4657                  'limit' => 2,
4658                  'offset' => 2,
4659                  'expecteddbqueries' => 2,
4660                  'expectedresult' => $buildexpectedresult(2, 2)
4661              ],
4662              'limit less than total' => [
4663                  'dbquerylimit' => 3,
4664                  'totalcourses' => 9,
4665                  'limit' => 6,
4666                  'offset' => 0,
4667                  'expecteddbqueries' => 3,
4668                  'expectedresult' => $buildexpectedresult(6, 0)
4669              ],
4670              'less results than limit' => [
4671                  'dbquerylimit' => 4,
4672                  'totalcourses' => 9,
4673                  'limit' => 20,
4674                  'offset' => 0,
4675                  'expecteddbqueries' => 4,
4676                  'expectedresult' => $buildexpectedresult(9, 0)
4677              ],
4678              'less results than limit exact divisible' => [
4679                  'dbquerylimit' => 3,
4680                  'totalcourses' => 9,
4681                  'limit' => 20,
4682                  'offset' => 0,
4683                  'expecteddbqueries' => 5,
4684                  'expectedresult' => $buildexpectedresult(9, 0)
4685              ],
4686              'less results than limit with offset' => [
4687                  'dbquerylimit' => 3,
4688                  'totalcourses' => 9,
4689                  'limit' => 10,
4690                  'offset' => 5,
4691                  'expecteddbqueries' => 3,
4692                  'expectedresult' => $buildexpectedresult(4, 5)
4693              ],
4694          ];
4695      }
4696  
4697      /**
4698       * Test the course_get_enrolled_courses_for_logged_in_user function.
4699       *
4700       * @dataProvider get_course_get_enrolled_courses_for_logged_in_user_test_cases()
4701       * @param int $dbquerylimit Number of records to load per DB request
4702       * @param int $totalcourses Number of courses to create
4703       * @param int $limit Maximum number of results to get.
4704       * @param int $offset Skip this number of results from the start of the result set.
4705       * @param int $expecteddbqueries The number of DB queries expected during the test.
4706       * @param array $expectedresult Expected test results.
4707       */
4708      public function test_course_get_enrolled_courses_for_logged_in_user(
4709          $dbquerylimit,
4710          $totalcourses,
4711          $limit,
4712          $offset,
4713          $expecteddbqueries,
4714          $expectedresult
4715      ) {
4716          global $DB;
4717  
4718          $this->resetAfterTest();
4719          $generator = $this->getDataGenerator();
4720          $student = $generator->create_user();
4721  
4722          for ($i = 0; $i < $totalcourses; $i++) {
4723              $shortname = "testcourse{$i}";
4724              $course = $generator->create_course(['shortname' => $shortname]);
4725              $generator->enrol_user($student->id, $course->id, 'student');
4726          }
4727  
4728          $this->setUser($student);
4729  
4730          $initialquerycount = $DB->perf_get_queries();
4731          $courses = course_get_enrolled_courses_for_logged_in_user($limit, $offset, 'shortname ASC', 'shortname', $dbquerylimit);
4732  
4733          // Loop over the result set to force the lazy loading to kick in so that we can check the
4734          // number of DB queries.
4735          $actualresult = array_map(function($course) {
4736              return $course->shortname;
4737          }, iterator_to_array($courses, false));
4738  
4739          sort($expectedresult);
4740  
4741          $this->assertEquals($expectedresult, $actualresult);
4742          $this->assertLessThanOrEqual($expecteddbqueries, $DB->perf_get_queries() - $initialquerycount);
4743      }
4744  
4745      /**
4746       * Test cases for the course_filter_courses_by_timeline_classification tests.
4747       */
4748      public function get_course_filter_courses_by_timeline_classification_test_cases() {
4749          $now = time();
4750          $day = 86400;
4751  
4752          $coursedata = [
4753              [
4754                  'shortname' => 'apast',
4755                  'startdate' => $now - ($day * 2),
4756                  'enddate' => $now - $day
4757              ],
4758              [
4759                  'shortname' => 'bpast',
4760                  'startdate' => $now - ($day * 2),
4761                  'enddate' => $now - $day
4762              ],
4763              [
4764                  'shortname' => 'cpast',
4765                  'startdate' => $now - ($day * 2),
4766                  'enddate' => $now - $day
4767              ],
4768              [
4769                  'shortname' => 'dpast',
4770                  'startdate' => $now - ($day * 2),
4771                  'enddate' => $now - $day
4772              ],
4773              [
4774                  'shortname' => 'epast',
4775                  'startdate' => $now - ($day * 2),
4776                  'enddate' => $now - $day
4777              ],
4778              [
4779                  'shortname' => 'ainprogress',
4780                  'startdate' => $now - $day,
4781                  'enddate' => $now + $day
4782              ],
4783              [
4784                  'shortname' => 'binprogress',
4785                  'startdate' => $now - $day,
4786                  'enddate' => $now + $day
4787              ],
4788              [
4789                  'shortname' => 'cinprogress',
4790                  'startdate' => $now - $day,
4791                  'enddate' => $now + $day
4792              ],
4793              [
4794                  'shortname' => 'dinprogress',
4795                  'startdate' => $now - $day,
4796                  'enddate' => $now + $day
4797              ],
4798              [
4799                  'shortname' => 'einprogress',
4800                  'startdate' => $now - $day,
4801                  'enddate' => $now + $day
4802              ],
4803              [
4804                  'shortname' => 'afuture',
4805                  'startdate' => $now + $day
4806              ],
4807              [
4808                  'shortname' => 'bfuture',
4809                  'startdate' => $now + $day
4810              ],
4811              [
4812                  'shortname' => 'cfuture',
4813                  'startdate' => $now + $day
4814              ],
4815              [
4816                  'shortname' => 'dfuture',
4817                  'startdate' => $now + $day
4818              ],
4819              [
4820                  'shortname' => 'efuture',
4821                  'startdate' => $now + $day
4822              ]
4823          ];
4824  
4825          // Raw enrolled courses result set should be returned in this order:
4826          // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
4827          // dfuture, dinprogress, dpast, efuture, einprogress, epast
4828          //
4829          // By classification the offset values for each record should be:
4830          // COURSE_TIMELINE_FUTURE
4831          // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
4832          // COURSE_TIMELINE_INPROGRESS
4833          // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
4834          // COURSE_TIMELINE_PAST
4835          // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
4836          return [
4837              'empty set' => [
4838                  'coursedata' => [],
4839                  'classification' => COURSE_TIMELINE_FUTURE,
4840                  'limit' => 2,
4841                  'offset' => 0,
4842                  'expectedcourses' => [],
4843                  'expectedprocessedcount' => 0
4844              ],
4845              // COURSE_TIMELINE_FUTURE.
4846              'future not limit no offset' => [
4847                  'coursedata' => $coursedata,
4848                  'classification' => COURSE_TIMELINE_FUTURE,
4849                  'limit' => 0,
4850                  'offset' => 0,
4851                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
4852                  'expectedprocessedcount' => 15
4853              ],
4854              'future no offset' => [
4855                  'coursedata' => $coursedata,
4856                  'classification' => COURSE_TIMELINE_FUTURE,
4857                  'limit' => 2,
4858                  'offset' => 0,
4859                  'expectedcourses' => ['afuture', 'bfuture'],
4860                  'expectedprocessedcount' => 4
4861              ],
4862              'future offset' => [
4863                  'coursedata' => $coursedata,
4864                  'classification' => COURSE_TIMELINE_FUTURE,
4865                  'limit' => 2,
4866                  'offset' => 2,
4867                  'expectedcourses' => ['bfuture', 'cfuture'],
4868                  'expectedprocessedcount' => 5
4869              ],
4870              'future exact limit' => [
4871                  'coursedata' => $coursedata,
4872                  'classification' => COURSE_TIMELINE_FUTURE,
4873                  'limit' => 5,
4874                  'offset' => 0,
4875                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
4876                  'expectedprocessedcount' => 13
4877              ],
4878              'future limit less results' => [
4879                  'coursedata' => $coursedata,
4880                  'classification' => COURSE_TIMELINE_FUTURE,
4881                  'limit' => 10,
4882                  'offset' => 0,
4883                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
4884                  'expectedprocessedcount' => 15
4885              ],
4886              'future limit less results with offset' => [
4887                  'coursedata' => $coursedata,
4888                  'classification' => COURSE_TIMELINE_FUTURE,
4889                  'limit' => 10,
4890                  'offset' => 5,
4891                  'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
4892                  'expectedprocessedcount' => 10
4893              ],
4894          ];
4895      }
4896  
4897      /**
4898       * Test the course_get_enrolled_courses_for_logged_in_user_from_search function.
4899       */
4900      public function test_course_get_enrolled_courses_for_logged_in_user_from_search() {
4901          global $DB;
4902  
4903          // Set up.
4904  
4905          $this->resetAfterTest();
4906          $generator = $this->getDataGenerator();
4907          $student = $generator->create_user();
4908  
4909          $cat1 = core_course_category::create(['name' => 'Cat1']);
4910          $cat2 = core_course_category::create(['name' => 'Cat2', 'parent' => $cat1->id]);
4911          $c1 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Test 3', 'summary' => 'Magic', 'idnumber' => 'ID3']);
4912          $c2 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Test 1', 'summary' => 'Magic']);
4913          $c3 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Математика', 'summary' => ' Test Magic']);
4914          $c4 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Test 4', 'summary' => 'Magic', 'idnumber' => 'ID4']);
4915  
4916          $c5 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Test 5', 'summary' => 'Magic']);
4917          $c6 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Дискретная Математика', 'summary' => 'Magic']);
4918          $c7 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Test 7', 'summary' => 'Magic']);
4919          $c8 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Test 8', 'summary' => 'Magic']);
4920  
4921          for ($i = 1; $i < 9; $i++) {
4922              $generator->enrol_user($student->id, ${"c$i"}->id, 'student');
4923          }
4924  
4925          $this->setUser($student);
4926  
4927          $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
4928              0,
4929              0,
4930              'id ASC',
4931              null,
4932              COURSE_DB_QUERY_LIMIT,
4933              ['search' => 'test'],
4934              ['idonly' => true]
4935          );
4936  
4937          $actualresult = array_map(function($course) {
4938              return $course->id;
4939          }, iterator_to_array($returnedcourses, false));
4940  
4941          $this->assertEquals([$c1->id, $c2->id, $c3->id, $c4->id, $c5->id, $c7->id, $c8->id], $actualresult);
4942  
4943          // Test no courses matching the search.
4944          $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
4945              0,
4946              0,
4947              'id ASC',
4948              null,
4949              COURSE_DB_QUERY_LIMIT,
4950              ['search' => 'foobar'],
4951              ['idonly' => true]
4952          );
4953  
4954          $actualresult = array_map(function($course) {
4955              return $course->id;
4956          }, iterator_to_array($returnedcourses, false));
4957  
4958          $this->assertEquals([], $actualresult);
4959  
4960          // Test returning all courses that have a mutual summary.
4961          $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
4962              0,
4963              0,
4964              'id ASC',
4965              null,
4966              COURSE_DB_QUERY_LIMIT,
4967              ['search' => 'Magic'],
4968              ['idonly' => true]
4969          );
4970  
4971          $actualresult = array_map(function($course) {
4972              return $course->id;
4973          }, iterator_to_array($returnedcourses, false));
4974  
4975          $this->assertEquals([$c1->id, $c2->id, $c3->id, $c4->id, $c5->id, $c6->id, $c7->id, $c8->id], $actualresult);
4976  
4977          // Test returning a unique course.
4978          $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
4979              0,
4980              0,
4981              'id ASC',
4982              null,
4983              COURSE_DB_QUERY_LIMIT,
4984              ['search' => 'Дискретная'],
4985              ['idonly' => true]
4986          );
4987  
4988          $actualresult = array_map(function($course) {
4989              return $course->id;
4990          }, iterator_to_array($returnedcourses, false));
4991  
4992          $this->assertEquals([$c6->id], $actualresult);
4993      }
4994  
4995      /**
4996       * Test the course_filter_courses_by_timeline_classification function.
4997       *
4998       * @dataProvider get_course_filter_courses_by_timeline_classification_test_cases()
4999       * @param array $coursedata Course test data to create.
5000       * @param string $classification Timeline classification.
5001       * @param int $limit Maximum number of results to return.
5002       * @param int $offset Results to skip at the start of the result set.
5003       * @param string[] $expectedcourses Expected courses in results.
5004       * @param int $expectedprocessedcount Expected number of course records to be processed.
5005       */
5006      public function test_course_filter_courses_by_timeline_classification(
5007          $coursedata,
5008          $classification,
5009          $limit,
5010          $offset,
5011          $expectedcourses,
5012          $expectedprocessedcount
5013      ) {
5014          $this->resetAfterTest();
5015          $generator = $this->getDataGenerator();
5016  
5017          $courses = array_map(function($coursedata) use ($generator) {
5018              return $generator->create_course($coursedata);
5019          }, $coursedata);
5020  
5021          $student = $generator->create_user();
5022  
5023          foreach ($courses as $course) {
5024              $generator->enrol_user($student->id, $course->id, 'student');
5025          }
5026  
5027          $this->setUser($student);
5028  
5029          $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
5030          list($result, $processedcount) = course_filter_courses_by_timeline_classification(
5031              $coursesgenerator,
5032              $classification,
5033              $limit
5034          );
5035  
5036          $actual = array_map(function($course) {
5037              return $course->shortname;
5038          }, $result);
5039  
5040          $this->assertEquals($expectedcourses, $actual);
5041          $this->assertEquals($expectedprocessedcount, $processedcount);
5042      }
5043  
5044      /**
5045       * Test cases for the course_filter_courses_by_timeline_classification tests.
5046       */
5047      public function get_course_filter_courses_by_customfield_test_cases() {
5048          global $CFG;
5049          require_once($CFG->dirroot.'/blocks/myoverview/lib.php');
5050          $coursedata = [
5051              [
5052                  'shortname' => 'C1',
5053                  'customfield_checkboxfield' => 1,
5054                  'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
5055                  'customfield_selectfield' => 1,
5056                  'customfield_textfield' => 'fish',
5057              ],
5058              [
5059                  'shortname' => 'C2',
5060                  'customfield_checkboxfield' => 0,
5061                  'customfield_datefield' => strtotime('1980-08-05T13:00:00Z'),
5062              ],
5063              [
5064                  'shortname' => 'C3',
5065                  'customfield_checkboxfield' => 0,
5066                  'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
5067                  'customfield_selectfield' => 2,
5068                  'customfield_textfield' => 'dog',
5069              ],
5070              [
5071                  'shortname' => 'C4',
5072                  'customfield_checkboxfield' => 1,
5073                  'customfield_selectfield' => 3,
5074                  'customfield_textfield' => 'cat',
5075              ],
5076              [
5077                  'shortname' => 'C5',
5078                  'customfield_datefield' => strtotime('1980-08-06T13:00:00Z'),
5079                  'customfield_selectfield' => 2,
5080                  'customfield_textfield' => 'fish',
5081              ],
5082          ];
5083  
5084          return [
5085              'empty set' => [
5086                  'coursedata' => [],
5087                  'customfield' => 'checkboxfield',
5088                  'customfieldvalue' => 1,
5089                  'limit' => 10,
5090                  'offset' => 0,
5091                  'expectedcourses' => [],
5092                  'expectedprocessedcount' => 0
5093              ],
5094              'checkbox yes' => [
5095                  'coursedata' => $coursedata,
5096                  'customfield' => 'checkboxfield',
5097                  'customfieldvalue' => 1,
5098                  'limit' => 10,
5099                  'offset' => 0,
5100                  'expectedcourses' => ['C1', 'C4'],
5101                  'expectedprocessedcount' => 5
5102              ],
5103              'checkbox no' => [
5104                  'coursedata' => $coursedata,
5105                  'customfield' => 'checkboxfield',
5106                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5107                  'limit' => 10,
5108                  'offset' => 0,
5109                  'expectedcourses' => ['C2', 'C3', 'C5'],
5110                  'expectedprocessedcount' => 5
5111              ],
5112              'date 1 Feb 2001' => [
5113                  'coursedata' => $coursedata,
5114                  'customfield' => 'datefield',
5115                  'customfieldvalue' => strtotime('2001-02-01T12:00:00Z'),
5116                  'limit' => 10,
5117                  'offset' => 0,
5118                  'expectedcourses' => ['C1', 'C3'],
5119                  'expectedprocessedcount' => 5
5120              ],
5121              'date 6 Aug 1980' => [
5122                  'coursedata' => $coursedata,
5123                  'customfield' => 'datefield',
5124                  'customfieldvalue' => strtotime('1980-08-06T13:00:00Z'),
5125                  'limit' => 10,
5126                  'offset' => 0,
5127                  'expectedcourses' => ['C5'],
5128                  'expectedprocessedcount' => 5
5129              ],
5130              'date no date' => [
5131                  'coursedata' => $coursedata,
5132                  'customfield' => 'datefield',
5133                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5134                  'limit' => 10,
5135                  'offset' => 0,
5136                  'expectedcourses' => ['C4'],
5137                  'expectedprocessedcount' => 5
5138              ],
5139              'select Option 1' => [
5140                  'coursedata' => $coursedata,
5141                  'customfield' => 'selectfield',
5142                  'customfieldvalue' => 1,
5143                  'limit' => 10,
5144                  'offset' => 0,
5145                  'expectedcourses' => ['C1'],
5146                  'expectedprocessedcount' => 5
5147              ],
5148              'select Option 2' => [
5149                  'coursedata' => $coursedata,
5150                  'customfield' => 'selectfield',
5151                  'customfieldvalue' => 2,
5152                  'limit' => 10,
5153                  'offset' => 0,
5154                  'expectedcourses' => ['C3', 'C5'],
5155                  'expectedprocessedcount' => 5
5156              ],
5157              'select no select' => [
5158                  'coursedata' => $coursedata,
5159                  'customfield' => 'selectfield',
5160                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5161                  'limit' => 10,
5162                  'offset' => 0,
5163                  'expectedcourses' => ['C2'],
5164                  'expectedprocessedcount' => 5
5165              ],
5166              'text fish' => [
5167                  'coursedata' => $coursedata,
5168                  'customfield' => 'textfield',
5169                  'customfieldvalue' => 'fish',
5170                  'limit' => 10,
5171                  'offset' => 0,
5172                  'expectedcourses' => ['C1', 'C5'],
5173                  'expectedprocessedcount' => 5
5174              ],
5175              'text dog' => [
5176                  'coursedata' => $coursedata,
5177                  'customfield' => 'textfield',
5178                  'customfieldvalue' => 'dog',
5179                  'limit' => 10,
5180                  'offset' => 0,
5181                  'expectedcourses' => ['C3'],
5182                  'expectedprocessedcount' => 5
5183              ],
5184              'text no text' => [
5185                  'coursedata' => $coursedata,
5186                  'customfield' => 'textfield',
5187                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5188                  'limit' => 10,
5189                  'offset' => 0,
5190                  'expectedcourses' => ['C2'],
5191                  'expectedprocessedcount' => 5
5192              ],
5193              'checkbox limit no' => [
5194                  'coursedata' => $coursedata,
5195                  'customfield' => 'checkboxfield',
5196                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5197                  'limit' => 2,
5198                  'offset' => 0,
5199                  'expectedcourses' => ['C2', 'C3'],
5200                  'expectedprocessedcount' => 3
5201              ],
5202              'checkbox limit offset no' => [
5203                  'coursedata' => $coursedata,
5204                  'customfield' => 'checkboxfield',
5205                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5206                  'limit' => 2,
5207                  'offset' => 3,
5208                  'expectedcourses' => ['C5'],
5209                  'expectedprocessedcount' => 2
5210              ],
5211          ];
5212      }
5213  
5214      /**
5215       * Test the course_filter_courses_by_customfield function.
5216       *
5217       * @dataProvider get_course_filter_courses_by_customfield_test_cases()
5218       * @param array $coursedata Course test data to create.
5219       * @param string $customfield Shortname of the customfield.
5220       * @param string $customfieldvalue the value to filter by.
5221       * @param int $limit Maximum number of results to return.
5222       * @param int $offset Results to skip at the start of the result set.
5223       * @param string[] $expectedcourses Expected courses in results.
5224       * @param int $expectedprocessedcount Expected number of course records to be processed.
5225       */
5226      public function test_course_filter_courses_by_customfield(
5227          $coursedata,
5228          $customfield,
5229          $customfieldvalue,
5230          $limit,
5231          $offset,
5232          $expectedcourses,
5233          $expectedprocessedcount
5234      ) {
5235          $this->resetAfterTest();
5236          $generator = $this->getDataGenerator();
5237  
5238          // Create the custom fields.
5239          $generator->create_custom_field_category([
5240              'name' => 'Course fields',
5241              'component' => 'core_course',
5242              'area' => 'course',
5243              'itemid' => 0,
5244          ]);
5245          $generator->create_custom_field([
5246              'name' => 'Checkbox field',
5247              'category' => 'Course fields',
5248              'type' => 'checkbox',
5249              'shortname' => 'checkboxfield',
5250          ]);
5251          $generator->create_custom_field([
5252              'name' => 'Date field',
5253              'category' => 'Course fields',
5254              'type' => 'date',
5255              'shortname' => 'datefield',
5256              'configdata' => '{"mindate":0, "maxdate":0}',
5257          ]);
5258          $generator->create_custom_field([
5259              'name' => 'Select field',
5260              'category' => 'Course fields',
5261              'type' => 'select',
5262              'shortname' => 'selectfield',
5263              'configdata' => '{"options":"Option 1\nOption 2\nOption 3\nOption 4"}',
5264          ]);
5265          $generator->create_custom_field([
5266              'name' => 'Text field',
5267              'category' => 'Course fields',
5268              'type' => 'text',
5269              'shortname' => 'textfield',
5270          ]);
5271  
5272          $courses = array_map(function($coursedata) use ($generator) {
5273              return $generator->create_course($coursedata);
5274          }, $coursedata);
5275  
5276          $student = $generator->create_user();
5277  
5278          foreach ($courses as $course) {
5279              $generator->enrol_user($student->id, $course->id, 'student');
5280          }
5281  
5282          $this->setUser($student);
5283  
5284          $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
5285          list($result, $processedcount) = course_filter_courses_by_customfield(
5286              $coursesgenerator,
5287              $customfield,
5288              $customfieldvalue,
5289              $limit
5290          );
5291  
5292          $actual = array_map(function($course) {
5293              return $course->shortname;
5294          }, $result);
5295  
5296          $this->assertEquals($expectedcourses, $actual);
5297          $this->assertEquals($expectedprocessedcount, $processedcount);
5298      }
5299  
5300      /**
5301       * Test cases for the course_filter_courses_by_timeline_classification w/ hidden courses tests.
5302       */
5303      public function get_course_filter_courses_by_timeline_classification_hidden_courses_test_cases() {
5304          $now = time();
5305          $day = 86400;
5306  
5307          $coursedata = [
5308              [
5309                  'shortname' => 'apast',
5310                  'startdate' => $now - ($day * 2),
5311                  'enddate' => $now - $day
5312              ],
5313              [
5314                  'shortname' => 'bpast',
5315                  'startdate' => $now - ($day * 2),
5316                  'enddate' => $now - $day
5317              ],
5318              [
5319                  'shortname' => 'cpast',
5320                  'startdate' => $now - ($day * 2),
5321                  'enddate' => $now - $day
5322              ],
5323              [
5324                  'shortname' => 'dpast',
5325                  'startdate' => $now - ($day * 2),
5326                  'enddate' => $now - $day
5327              ],
5328              [
5329                  'shortname' => 'epast',
5330                  'startdate' => $now - ($day * 2),
5331                  'enddate' => $now - $day
5332              ],
5333              [
5334                  'shortname' => 'ainprogress',
5335                  'startdate' => $now - $day,
5336                  'enddate' => $now + $day
5337              ],
5338              [
5339                  'shortname' => 'binprogress',
5340                  'startdate' => $now - $day,
5341                  'enddate' => $now + $day
5342              ],
5343              [
5344                  'shortname' => 'cinprogress',
5345                  'startdate' => $now - $day,
5346                  'enddate' => $now + $day
5347              ],
5348              [
5349                  'shortname' => 'dinprogress',
5350                  'startdate' => $now - $day,
5351                  'enddate' => $now + $day
5352              ],
5353              [
5354                  'shortname' => 'einprogress',
5355                  'startdate' => $now - $day,
5356                  'enddate' => $now + $day
5357              ],
5358              [
5359                  'shortname' => 'afuture',
5360                  'startdate' => $now + $day
5361              ],
5362              [
5363                  'shortname' => 'bfuture',
5364                  'startdate' => $now + $day
5365              ],
5366              [
5367                  'shortname' => 'cfuture',
5368                  'startdate' => $now + $day
5369              ],
5370              [
5371                  'shortname' => 'dfuture',
5372                  'startdate' => $now + $day
5373              ],
5374              [
5375                  'shortname' => 'efuture',
5376                  'startdate' => $now + $day
5377              ]
5378          ];
5379  
5380          // Raw enrolled courses result set should be returned in this order:
5381          // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
5382          // dfuture, dinprogress, dpast, efuture, einprogress, epast
5383          //
5384          // By classification the offset values for each record should be:
5385          // COURSE_TIMELINE_FUTURE
5386          // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
5387          // COURSE_TIMELINE_INPROGRESS
5388          // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
5389          // COURSE_TIMELINE_PAST
5390          // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
5391          return [
5392              'empty set' => [
5393                  'coursedata' => [],
5394                  'classification' => COURSE_TIMELINE_FUTURE,
5395                  'limit' => 2,
5396                  'offset' => 0,
5397                  'expectedcourses' => [],
5398                  'expectedprocessedcount' => 0,
5399                  'hiddencourse' => ''
5400              ],
5401              // COURSE_TIMELINE_FUTURE.
5402              'future not limit no offset' => [
5403                  'coursedata' => $coursedata,
5404                  'classification' => COURSE_TIMELINE_FUTURE,
5405                  'limit' => 0,
5406                  'offset' => 0,
5407                  'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
5408                  'expectedprocessedcount' => 15,
5409                  'hiddencourse' => 'bfuture'
5410              ],
5411              'future no offset' => [
5412                  'coursedata' => $coursedata,
5413                  'classification' => COURSE_TIMELINE_FUTURE,
5414                  'limit' => 2,
5415                  'offset' => 0,
5416                  'expectedcourses' => ['afuture', 'cfuture'],
5417                  'expectedprocessedcount' => 7,
5418                  'hiddencourse' => 'bfuture'
5419              ],
5420              'future offset' => [
5421                  'coursedata' => $coursedata,
5422                  'classification' => COURSE_TIMELINE_FUTURE,
5423                  'limit' => 2,
5424                  'offset' => 2,
5425                  'expectedcourses' => ['bfuture', 'dfuture'],
5426                  'expectedprocessedcount' => 8,
5427                  'hiddencourse' => 'cfuture'
5428              ],
5429              'future exact limit' => [
5430                  'coursedata' => $coursedata,
5431                  'classification' => COURSE_TIMELINE_FUTURE,
5432                  'limit' => 5,
5433                  'offset' => 0,
5434                  'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
5435                  'expectedprocessedcount' => 15,
5436                  'hiddencourse' => 'bfuture'
5437              ],
5438              'future limit less results' => [
5439                  'coursedata' => $coursedata,
5440                  'classification' => COURSE_TIMELINE_FUTURE,
5441                  'limit' => 10,
5442                  'offset' => 0,
5443                  'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
5444                  'expectedprocessedcount' => 15,
5445                  'hiddencourse' => 'bfuture'
5446              ],
5447              'future limit less results with offset' => [
5448                  'coursedata' => $coursedata,
5449                  'classification' => COURSE_TIMELINE_FUTURE,
5450                  'limit' => 10,
5451                  'offset' => 5,
5452                  'expectedcourses' => ['cfuture', 'efuture'],
5453                  'expectedprocessedcount' => 10,
5454                  'hiddencourse' => 'dfuture'
5455              ],
5456          ];
5457      }
5458  
5459      /**
5460       * Test the course_filter_courses_by_timeline_classification function hidden courses.
5461       *
5462       * @dataProvider get_course_filter_courses_by_timeline_classification_hidden_courses_test_cases()
5463       * @param array $coursedata Course test data to create.
5464       * @param string $classification Timeline classification.
5465       * @param int $limit Maximum number of results to return.
5466       * @param int $offset Results to skip at the start of the result set.
5467       * @param string[] $expectedcourses Expected courses in results.
5468       * @param int $expectedprocessedcount Expected number of course records to be processed.
5469       * @param int $hiddencourse The course to hide as part of this process
5470       */
5471      public function test_course_filter_courses_by_timeline_classification_with_hidden_courses(
5472          $coursedata,
5473          $classification,
5474          $limit,
5475          $offset,
5476          $expectedcourses,
5477          $expectedprocessedcount,
5478          $hiddencourse
5479      ) {
5480          $this->resetAfterTest();
5481          $generator = $this->getDataGenerator();
5482          $student = $generator->create_user();
5483          $this->setUser($student);
5484  
5485          $courses = array_map(function($coursedata) use ($generator, $hiddencourse) {
5486              $course = $generator->create_course($coursedata);
5487              if ($course->shortname == $hiddencourse) {
5488                  set_user_preference('block_myoverview_hidden_course_' . $course->id, true);
5489              }
5490              return $course;
5491          }, $coursedata);
5492  
5493          foreach ($courses as $course) {
5494              $generator->enrol_user($student->id, $course->id, 'student');
5495          }
5496  
5497          $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
5498          list($result, $processedcount) = course_filter_courses_by_timeline_classification(
5499              $coursesgenerator,
5500              $classification,
5501              $limit
5502          );
5503  
5504          $actual = array_map(function($course) {
5505              return $course->shortname;
5506          }, $result);
5507  
5508          $this->assertEquals($expectedcourses, $actual);
5509          $this->assertEquals($expectedprocessedcount, $processedcount);
5510      }
5511  
5512  
5513      /**
5514       * Testing core_course_core_calendar_get_valid_event_timestart_range when the course has no end date.
5515       */
5516      public function test_core_course_core_calendar_get_valid_event_timestart_range_no_enddate() {
5517          global $CFG;
5518          require_once($CFG->dirroot . "/calendar/lib.php");
5519  
5520          $this->resetAfterTest(true);
5521          $this->setAdminUser();
5522          $generator = $this->getDataGenerator();
5523          $now = time();
5524          $course = $generator->create_course(['startdate' => $now - 86400]);
5525  
5526          // Create a course event.
5527          $event = new \calendar_event([
5528              'name' => 'Test course event',
5529              'eventtype' => 'course',
5530              'courseid' => $course->id,
5531          ]);
5532  
5533          list ($min, $max) = core_course_core_calendar_get_valid_event_timestart_range($event, $course);
5534          $this->assertEquals($course->startdate, $min[0]);
5535          $this->assertNull($max);
5536      }
5537  
5538      /**
5539       * Testing core_course_core_calendar_get_valid_event_timestart_range when the course has end date.
5540       */
5541      public function test_core_course_core_calendar_get_valid_event_timestart_range_with_enddate() {
5542          global $CFG;
5543          require_once($CFG->dirroot . "/calendar/lib.php");
5544  
5545          $this->resetAfterTest(true);
5546          $this->setAdminUser();
5547          $generator = $this->getDataGenerator();
5548          $now = time();
5549          $course = $generator->create_course(['startdate' => $now - 86400, 'enddate' => $now + 86400]);
5550  
5551          // Create a course event.
5552          $event = new \calendar_event([
5553              'name' => 'Test course event',
5554              'eventtype' => 'course',
5555              'courseid' => $course->id,
5556          ]);
5557  
5558          list ($min, $max) = core_course_core_calendar_get_valid_event_timestart_range($event, $course);
5559          $this->assertEquals($course->startdate, $min[0]);
5560          $this->assertNull($max);
5561      }
5562  
5563      /**
5564       * Test the course_get_recent_courses function.
5565       */
5566      public function test_course_get_recent_courses() {
5567          global $DB;
5568  
5569          $this->resetAfterTest();
5570          $generator = $this->getDataGenerator();
5571  
5572          $courses = array();
5573          for ($i = 1; $i < 4; $i++) {
5574              $courses[]  = $generator->create_course();
5575          };
5576  
5577          $student = $generator->create_user();
5578  
5579          foreach ($courses as $course) {
5580              $generator->enrol_user($student->id, $course->id, 'student');
5581          }
5582  
5583          $this->setUser($student);
5584  
5585          $result = course_get_recent_courses($student->id);
5586  
5587          // No course accessed.
5588          $this->assertCount(0, $result);
5589  
5590          $time = time();
5591          foreach ($courses as $course) {
5592              $context = context_course::instance($course->id);
5593              course_view($context);
5594              $DB->set_field('user_lastaccess', 'timeaccess', $time, [
5595                  'userid' => $student->id,
5596                  'courseid' => $course->id,
5597                  ]);
5598              $time++;
5599          }
5600  
5601          // Every course accessed.
5602          $result = course_get_recent_courses($student->id);
5603          $this->assertCount(3, $result);
5604  
5605          // Every course accessed, result limited to 2 courses.
5606          $result = course_get_recent_courses($student->id, 2);
5607          $this->assertCount(2, $result);
5608  
5609          // Every course accessed, with limit and offset should return the first course.
5610          $result = course_get_recent_courses($student->id, 3, 2);
5611          $this->assertCount(1, $result);
5612          $this->assertArrayHasKey($courses[0]->id, $result);
5613  
5614          // Every course accessed, order by shortname DESC. The last create course ($course[2]) should have the greater shortname.
5615          $result = course_get_recent_courses($student->id, 0, 0, 'shortname DESC');
5616          $this->assertCount(3, $result);
5617          $this->assertEquals($courses[2]->id, array_values($result)[0]->id);
5618          $this->assertEquals($courses[1]->id, array_values($result)[1]->id);
5619          $this->assertEquals($courses[0]->id, array_values($result)[2]->id);
5620  
5621          // Every course accessed, order by shortname ASC.
5622          $result = course_get_recent_courses($student->id, 0, 0, 'shortname ASC');
5623          $this->assertCount(3, $result);
5624          $this->assertEquals($courses[0]->id, array_values($result)[0]->id);
5625          $this->assertEquals($courses[1]->id, array_values($result)[1]->id);
5626          $this->assertEquals($courses[2]->id, array_values($result)[2]->id);
5627  
5628          $guestcourse = $generator->create_course(
5629              (object)array('shortname' => 'guestcourse',
5630                  'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
5631                  'enrol_guest_password_0' => ''));
5632          $context = context_course::instance($guestcourse->id);
5633          course_view($context);
5634  
5635          // Every course accessed, even the not enrolled one.
5636          $result = course_get_recent_courses($student->id);
5637          $this->assertCount(4, $result);
5638  
5639          // Suspended student.
5640          $this->getDataGenerator()->enrol_user($student->id, $courses[0]->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
5641  
5642          // The course with suspended enrolment is not returned by the function.
5643          $result = course_get_recent_courses($student->id);
5644          $this->assertCount(3, $result);
5645          $this->assertArrayNotHasKey($courses[0]->id, $result);
5646      }
5647  
5648      /**
5649       * Test the validation of the sort value in course_get_recent_courses().
5650       *
5651       * @dataProvider course_get_recent_courses_sort_validation_provider
5652       * @param string $sort The sort value
5653       * @param string $expectedexceptionmsg The expected exception message
5654       */
5655      public function test_course_get_recent_courses_sort_validation(string $sort, string $expectedexceptionmsg) {
5656          $this->resetAfterTest();
5657  
5658          $user = $this->getDataGenerator()->create_user();
5659  
5660          if (!empty($expectedexceptionmsg)) {
5661              $this->expectException('invalid_parameter_exception');
5662              $this->expectExceptionMessage($expectedexceptionmsg);
5663          }
5664          course_get_recent_courses($user->id, 0, 0, $sort);
5665      }
5666  
5667      /**
5668       * Data provider for test_course_get_recent_courses_sort_validation().
5669       *
5670       * @return array
5671       */
5672      function course_get_recent_courses_sort_validation_provider() {
5673          return [
5674              'Invalid sort format (SQL injection attempt)' =>
5675                  [
5676                      'shortname DESC LIMIT 1--',
5677                      'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].',
5678                  ],
5679              'Sort uses \'sort by\' field that does not exist' =>
5680                  [
5681                      'shortname DESC, xyz ASC',
5682                      'Invalid field in the sort parameter, allowed fields: id, idnumber, summary, summaryformat, ' .
5683                      'startdate, enddate, category, shortname, fullname, timeaccess, component, visible, ' .
5684                      'showactivitydates, showcompletionconditions, pdfexportfont.',
5685              ],
5686              'Sort uses invalid value for the sorting direction' =>
5687                  [
5688                      'shortname xyz, lastaccess',
5689                      'Invalid sort direction in the sort parameter, allowed values: asc, desc.',
5690                  ],
5691              'Valid sort format' =>
5692                  [
5693                      'shortname asc, timeaccess',
5694                      ''
5695                  ]
5696          ];
5697      }
5698  
5699      /**
5700       * Test the course_get_recent_courses function.
5701       */
5702      public function test_course_get_recent_courses_with_guest() {
5703          global $DB;
5704          $this->resetAfterTest(true);
5705  
5706          $student = $this->getDataGenerator()->create_user();
5707  
5708          // Course 1 with guest access and no direct enrolment.
5709          $course1 = $this->getDataGenerator()->create_course();
5710          $context1 = context_course::instance($course1->id);
5711          $record = $DB->get_record('enrol', ['courseid' => $course1->id, 'enrol' => 'guest']);
5712          enrol_get_plugin('guest')->update_status($record, ENROL_INSTANCE_ENABLED);
5713  
5714          // Course 2 where student is enrolled with two enrolment methods.
5715          $course2 = $this->getDataGenerator()->create_course();
5716          $context2 = context_course::instance($course2->id);
5717          $record = $DB->get_record('enrol', ['courseid' => $course2->id, 'enrol' => 'self']);
5718          enrol_get_plugin('guest')->update_status($record, ENROL_INSTANCE_ENABLED);
5719          $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student', 'manual', 0, 0, ENROL_USER_ACTIVE);
5720          $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student', 'self', 0, 0, ENROL_USER_ACTIVE);
5721  
5722          // Course 3.
5723          $course3 = $this->getDataGenerator()->create_course();
5724          $context3 = context_course::instance($course3->id);
5725  
5726          // Student visits first two courses, course_get_recent_courses returns two courses.
5727          $this->setUser($student);
5728          course_view($context1);
5729          course_view($context2);
5730  
5731          $result = course_get_recent_courses($student->id);
5732          $this->assertEqualsCanonicalizing([$course2->id, $course1->id], array_column($result, 'id'));
5733  
5734          // Admin visits all three courses. Only the one with guest access is returned.
5735          $this->setAdminUser();
5736          course_view($context1);
5737          course_view($context2);
5738          course_view($context3);
5739          $result = course_get_recent_courses(get_admin()->id);
5740          $this->assertEqualsCanonicalizing([$course1->id], array_column($result, 'id'));
5741      }
5742  
5743      /**
5744       * Test cases for the course_get_course_dates_for_user_ids tests.
5745       */
5746      public function get_course_get_course_dates_for_user_ids_test_cases() {
5747          $now = time();
5748          $pastcoursestart = $now - 100;
5749          $futurecoursestart = $now + 100;
5750  
5751          return [
5752              'future course start fixed no users enrolled' => [
5753                  'relativedatemode' => false,
5754                  'coursestart' => $futurecoursestart,
5755                  'usercount' => 2,
5756                  'enrolmentmethods' => [
5757                      ['manual', ENROL_INSTANCE_ENABLED],
5758                      ['self', ENROL_INSTANCE_ENABLED]
5759                  ],
5760                  'enrolled' => [[], []],
5761                  'expected' => [
5762                      [
5763                          'start' => $futurecoursestart,
5764                          'startoffset' => 0
5765                      ],
5766                      [
5767                          'start' => $futurecoursestart,
5768                          'startoffset' => 0
5769                      ]
5770                  ]
5771              ],
5772              'future course start fixed 1 users enrolled future' => [
5773                  'relativedatemode' => false,
5774                  'coursestart' => $futurecoursestart,
5775                  'usercount' => 2,
5776                  'enrolmentmethods' => [
5777                      ['manual', ENROL_INSTANCE_ENABLED],
5778                      ['self', ENROL_INSTANCE_ENABLED]
5779                  ],
5780                  'enrolled' => [
5781                      // User 1.
5782                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
5783                      // User 2.
5784                      []
5785                  ],
5786                  'expected' => [
5787                      [
5788                          'start' => $futurecoursestart,
5789                          'startoffset' => 0
5790                      ],
5791                      [
5792                          'start' => $futurecoursestart,
5793                          'startoffset' => 0
5794                      ]
5795                  ]
5796              ],
5797              'future course start fixed 1 users enrolled past' => [
5798                  'relativedatemode' => false,
5799                  'coursestart' => $futurecoursestart,
5800                  'usercount' => 2,
5801                  'enrolmentmethods' => [
5802                      ['manual', ENROL_INSTANCE_ENABLED],
5803                      ['self', ENROL_INSTANCE_ENABLED]
5804                  ],
5805                  'enrolled' => [
5806                      // User 1.
5807                      ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
5808                      // User 2.
5809                      []
5810                  ],
5811                  'expected' => [
5812                      [
5813                          'start' => $futurecoursestart,
5814                          'startoffset' => 0
5815                      ],
5816                      [
5817                          'start' => $futurecoursestart,
5818                          'startoffset' => 0
5819                      ]
5820                  ]
5821              ],
5822              'future course start fixed 2 users enrolled future' => [
5823                  'relativedatemode' => false,
5824                  'coursestart' => $futurecoursestart,
5825                  'usercount' => 2,
5826                  'enrolmentmethods' => [
5827                      ['manual', ENROL_INSTANCE_ENABLED],
5828                      ['self', ENROL_INSTANCE_ENABLED]
5829                  ],
5830                  'enrolled' => [
5831                      // User 1.
5832                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
5833                      // User 2.
5834                      ['manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]]
5835                  ],
5836                  'expected' => [
5837                      [
5838                          'start' => $futurecoursestart,
5839                          'startoffset' => 0
5840                      ],
5841                      [
5842                          'start' => $futurecoursestart,
5843                          'startoffset' => 0
5844                      ]
5845                  ]
5846              ],
5847              'future course start fixed 2 users enrolled past' => [
5848                  'relativedatemode' => false,
5849                  'coursestart' => $futurecoursestart,
5850                  'usercount' => 2,
5851                  'enrolmentmethods' => [
5852                      ['manual', ENROL_INSTANCE_ENABLED],
5853                      ['self', ENROL_INSTANCE_ENABLED]
5854                  ],
5855                  'enrolled' => [
5856                      // User 1.
5857                      ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
5858                      // User 2.
5859                      ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
5860                  ],
5861                  'expected' => [
5862                      [
5863                          'start' => $futurecoursestart,
5864                          'startoffset' => 0
5865                      ],
5866                      [
5867                          'start' => $futurecoursestart,
5868                          'startoffset' => 0
5869                      ]
5870                  ]
5871              ],
5872              'future course start fixed 2 users enrolled mixed' => [
5873                  'relativedatemode' => false,
5874                  'coursestart' => $futurecoursestart,
5875                  'usercount' => 2,
5876                  'enrolmentmethods' => [
5877                      ['manual', ENROL_INSTANCE_ENABLED],
5878                      ['self', ENROL_INSTANCE_ENABLED]
5879                  ],
5880                  'enrolled' => [
5881                      // User 1.
5882                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
5883                      // User 2.
5884                      ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
5885                  ],
5886                  'expected' => [
5887                      [
5888                          'start' => $futurecoursestart,
5889                          'startoffset' => 0
5890                      ],
5891                      [
5892                          'start' => $futurecoursestart,
5893                          'startoffset' => 0
5894                      ]
5895                  ]
5896              ],
5897              'future course start fixed 2 users enrolled 2 methods' => [
5898                  'relativedatemode' => false,
5899                  'coursestart' => $futurecoursestart,
5900                  'usercount' => 2,
5901                  'enrolmentmethods' => [
5902                      ['manual', ENROL_INSTANCE_ENABLED],
5903                      ['self', ENROL_INSTANCE_ENABLED]
5904                  ],
5905                  'enrolled' => [
5906                      // User 1.
5907                      [
5908                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
5909                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
5910                      ],
5911                      // User 2.
5912                      [
5913                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
5914                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
5915                      ]
5916                  ],
5917                  'expected' => [
5918                      [
5919                          'start' => $futurecoursestart,
5920                          'startoffset' => 0
5921                      ],
5922                      [
5923                          'start' => $futurecoursestart,
5924                          'startoffset' => 0
5925                      ]
5926                  ]
5927              ],
5928              'future course start fixed 2 users enrolled 2 methods 1 disabled' => [
5929                  'relativedatemode' => false,
5930                  'coursestart' => $futurecoursestart,
5931                  'usercount' => 2,
5932                  'enrolmentmethods' => [
5933                      ['manual', ENROL_INSTANCE_DISABLED],
5934                      ['self', ENROL_INSTANCE_ENABLED]
5935                  ],
5936                  'enrolled' => [
5937                      // User 1.
5938                      [
5939                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
5940                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
5941                      ],
5942                      // User 2.
5943                      [
5944                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
5945                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
5946                      ]
5947                  ],
5948                  'expected' => [
5949                      [
5950                          'start' => $futurecoursestart,
5951                          'startoffset' => 0
5952                      ],
5953                      [
5954                          'start' => $futurecoursestart,
5955                          'startoffset' => 0
5956                      ]
5957                  ]
5958              ],
5959              'future course start fixed 2 users enrolled 2 methods 2 disabled' => [
5960                  'relativedatemode' => false,
5961                  'coursestart' => $futurecoursestart,
5962                  'usercount' => 2,
5963                  'enrolmentmethods' => [
5964                      ['manual', ENROL_INSTANCE_DISABLED],
5965                      ['self', ENROL_INSTANCE_DISABLED]
5966                  ],
5967                  'enrolled' => [
5968                      // User 1.
5969                      [
5970                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
5971                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
5972                      ],
5973                      // User 2.
5974                      [
5975                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
5976                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
5977                      ]
5978                  ],
5979                  'expected' => [
5980                      [
5981                          'start' => $futurecoursestart,
5982                          'startoffset' => 0
5983                      ],
5984                      [
5985                          'start' => $futurecoursestart,
5986                          'startoffset' => 0
5987                      ]
5988                  ]
5989              ],
5990              'future course start fixed 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
5991                  'relativedatemode' => false,
5992                  'coursestart' => $futurecoursestart,
5993                  'usercount' => 2,
5994                  'enrolmentmethods' => [
5995                      ['manual', ENROL_INSTANCE_ENABLED],
5996                      ['self', ENROL_INSTANCE_ENABLED]
5997                  ],
5998                  'enrolled' => [
5999                      // User 1.
6000                      [
6001                          'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6002                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6003                      ],
6004                      // User 2.
6005                      [
6006                          'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6007                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6008                      ]
6009                  ],
6010                  'expected' => [
6011                      [
6012                          'start' => $futurecoursestart,
6013                          'startoffset' => 0
6014                      ],
6015                      [
6016                          'start' => $futurecoursestart,
6017                          'startoffset' => 0
6018                      ]
6019                  ]
6020              ],
6021              'future course start fixed 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6022                  'relativedatemode' => false,
6023                  'coursestart' => $futurecoursestart,
6024                  'usercount' => 2,
6025                  'enrolmentmethods' => [
6026                      ['manual', ENROL_INSTANCE_ENABLED],
6027                      ['self', ENROL_INSTANCE_ENABLED]
6028                  ],
6029                  'enrolled' => [
6030                      // User 1.
6031                      [
6032                          'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6033                          'self' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED]
6034                      ],
6035                      // User 2.
6036                      [
6037                          'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6038                          'self' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED]
6039                      ]
6040                  ],
6041                  'expected' => [
6042                      [
6043                          'start' => $futurecoursestart,
6044                          'startoffset' => 0
6045                      ],
6046                      [
6047                          'start' => $futurecoursestart,
6048                          'startoffset' => 0
6049                      ]
6050                  ]
6051              ],
6052              'future course start relative no users enrolled' => [
6053                  'relativedatemode' => true,
6054                  'coursestart' => $futurecoursestart,
6055                  'usercount' => 2,
6056                  'enrolmentmethods' => [
6057                      ['manual', ENROL_INSTANCE_ENABLED],
6058                      ['self', ENROL_INSTANCE_ENABLED]
6059                  ],
6060                  'enrolled' => [[], []],
6061                  'expected' => [
6062                      [
6063                          'start' => $futurecoursestart,
6064                          'startoffset' => 0
6065                      ],
6066                      [
6067                          'start' => $futurecoursestart,
6068                          'startoffset' => 0
6069                      ]
6070                  ]
6071              ],
6072              'future course start relative 1 users enrolled future' => [
6073                  'relativedatemode' => true,
6074                  'coursestart' => $futurecoursestart,
6075                  'usercount' => 2,
6076                  'enrolmentmethods' => [
6077                      ['manual', ENROL_INSTANCE_ENABLED],
6078                      ['self', ENROL_INSTANCE_ENABLED]
6079                  ],
6080                  'enrolled' => [
6081                      // User 1.
6082                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
6083                      // User 2.
6084                      []
6085                  ],
6086                  'expected' => [
6087                      [
6088                          'start' => $futurecoursestart + 10,
6089                          'startoffset' => 10
6090                      ],
6091                      [
6092                          'start' => $futurecoursestart,
6093                          'startoffset' => 0
6094                      ]
6095                  ]
6096              ],
6097              'future course start relative 1 users enrolled past' => [
6098                  'relativedatemode' => true,
6099                  'coursestart' => $futurecoursestart,
6100                  'usercount' => 2,
6101                  'enrolmentmethods' => [
6102                      ['manual', ENROL_INSTANCE_ENABLED],
6103                      ['self', ENROL_INSTANCE_ENABLED]
6104                  ],
6105                  'enrolled' => [
6106                      // User 1.
6107                      ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
6108                      // User 2.
6109                      []
6110                  ],
6111                  'expected' => [
6112                      [
6113                          'start' => $futurecoursestart,
6114                          'startoffset' => 0
6115                      ],
6116                      [
6117                          'start' => $futurecoursestart,
6118                          'startoffset' => 0
6119                      ]
6120                  ]
6121              ],
6122              'future course start relative 2 users enrolled future' => [
6123                  'relativedatemode' => true,
6124                  'coursestart' => $futurecoursestart,
6125                  'usercount' => 2,
6126                  'enrolmentmethods' => [
6127                      ['manual', ENROL_INSTANCE_ENABLED],
6128                      ['self', ENROL_INSTANCE_ENABLED]
6129                  ],
6130                  'enrolled' => [
6131                      // User 1.
6132                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
6133                      // User 2.
6134                      ['manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]]
6135                  ],
6136                  'expected' => [
6137                      [
6138                          'start' => $futurecoursestart + 10,
6139                          'startoffset' => 10
6140                      ],
6141                      [
6142                          'start' => $futurecoursestart + 20,
6143                          'startoffset' => 20
6144                      ]
6145                  ]
6146              ],
6147              'future course start relative 2 users enrolled past' => [
6148                  'relativedatemode' => true,
6149                  'coursestart' => $futurecoursestart,
6150                  'usercount' => 2,
6151                  'enrolmentmethods' => [
6152                      ['manual', ENROL_INSTANCE_ENABLED],
6153                      ['self', ENROL_INSTANCE_ENABLED]
6154                  ],
6155                  'enrolled' => [
6156                      // User 1.
6157                      ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
6158                      // User 2.
6159                      ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
6160                  ],
6161                  'expected' => [
6162                      [
6163                          'start' => $futurecoursestart,
6164                          'startoffset' => 0
6165                      ],
6166                      [
6167                          'start' => $futurecoursestart,
6168                          'startoffset' => 0
6169                      ]
6170                  ]
6171              ],
6172              'future course start relative 2 users enrolled mixed' => [
6173                  'relativedatemode' => true,
6174                  'coursestart' => $futurecoursestart,
6175                  'usercount' => 2,
6176                  'enrolmentmethods' => [
6177                      ['manual', ENROL_INSTANCE_ENABLED],
6178                      ['self', ENROL_INSTANCE_ENABLED]
6179                  ],
6180                  'enrolled' => [
6181                      // User 1.
6182                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
6183                      // User 2.
6184                      ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
6185                  ],
6186                  'expected' => [
6187                      [
6188                          'start' => $futurecoursestart + 10,
6189                          'startoffset' => 10
6190                      ],
6191                      [
6192                          'start' => $futurecoursestart,
6193                          'startoffset' => 0
6194                      ]
6195                  ]
6196              ],
6197              'future course start relative 2 users enrolled 2 methods' => [
6198                  'relativedatemode' => true,
6199                  'coursestart' => $futurecoursestart,
6200                  'usercount' => 2,
6201                  'enrolmentmethods' => [
6202                      ['manual', ENROL_INSTANCE_ENABLED],
6203                      ['self', ENROL_INSTANCE_ENABLED]
6204                  ],
6205                  'enrolled' => [
6206                      // User 1.
6207                      [
6208                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6209                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6210                      ],
6211                      // User 2.
6212                      [
6213                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6214                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6215                      ]
6216                  ],
6217                  'expected' => [
6218                      [
6219                          'start' => $futurecoursestart + 10,
6220                          'startoffset' => 10
6221                      ],
6222                      [
6223                          'start' => $futurecoursestart + 10,
6224                          'startoffset' => 10
6225                      ]
6226                  ]
6227              ],
6228              'future course start relative 2 users enrolled 2 methods 1 disabled' => [
6229                  'relativedatemode' => true,
6230                  'coursestart' => $futurecoursestart,
6231                  'usercount' => 2,
6232                  'enrolmentmethods' => [
6233                      ['manual', ENROL_INSTANCE_DISABLED],
6234                      ['self', ENROL_INSTANCE_ENABLED]
6235                  ],
6236                  'enrolled' => [
6237                      // User 1.
6238                      [
6239                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6240                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6241                      ],
6242                      // User 2.
6243                      [
6244                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6245                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6246                      ]
6247                  ],
6248                  'expected' => [
6249                      [
6250                          'start' => $futurecoursestart + 20,
6251                          'startoffset' => 20
6252                      ],
6253                      [
6254                          'start' => $futurecoursestart + 10,
6255                          'startoffset' => 10
6256                      ]
6257                  ]
6258              ],
6259              'future course start relative 2 users enrolled 2 methods 2 disabled' => [
6260                  'relativedatemode' => true,
6261                  'coursestart' => $futurecoursestart,
6262                  'usercount' => 2,
6263                  'enrolmentmethods' => [
6264                      ['manual', ENROL_INSTANCE_DISABLED],
6265                      ['self', ENROL_INSTANCE_DISABLED]
6266                  ],
6267                  'enrolled' => [
6268                      // User 1.
6269                      [
6270                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6271                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6272                      ],
6273                      // User 2.
6274                      [
6275                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6276                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6277                      ]
6278                  ],
6279                  'expected' => [
6280                      [
6281                          'start' => $futurecoursestart,
6282                          'startoffset' => 0
6283                      ],
6284                      [
6285                          'start' => $futurecoursestart,
6286                          'startoffset' => 0
6287                      ]
6288                  ]
6289              ],
6290              'future course start relative 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6291                  'relativedatemode' => true,
6292                  'coursestart' => $futurecoursestart,
6293                  'usercount' => 2,
6294                  'enrolmentmethods' => [
6295                      ['manual', ENROL_INSTANCE_ENABLED],
6296                      ['self', ENROL_INSTANCE_ENABLED]
6297                  ],
6298                  'enrolled' => [
6299                      // User 1.
6300                      [
6301                          'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6302                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6303                      ],
6304                      // User 2.
6305                      [
6306                          'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6307                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6308                      ]
6309                  ],
6310                  'expected' => [
6311                      [
6312                          'start' => $futurecoursestart + 20,
6313                          'startoffset' => 20
6314                      ],
6315                      [
6316                          'start' => $futurecoursestart + 10,
6317                          'startoffset' => 10
6318                      ]
6319                  ]
6320              ],
6321              'future course start relative 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6322                  'relativedatemode' => true,
6323                  'coursestart' => $futurecoursestart,
6324                  'usercount' => 2,
6325                  'enrolmentmethods' => [
6326                      ['manual', ENROL_INSTANCE_ENABLED],
6327                      ['self', ENROL_INSTANCE_ENABLED]
6328                  ],
6329                  'enrolled' => [
6330                      // User 1.
6331                      [
6332                          'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6333                          'self' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED]
6334                      ],
6335                      // User 2.
6336                      [
6337                          'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6338                          'self' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED]
6339                      ]
6340                  ],
6341                  'expected' => [
6342                      [
6343                          'start' => $futurecoursestart,
6344                          'startoffset' => 0
6345                      ],
6346                      [
6347                          'start' => $futurecoursestart,
6348                          'startoffset' => 0
6349                      ]
6350                  ]
6351              ],
6352  
6353              // Course start date in the past.
6354              'past course start fixed no users enrolled' => [
6355                  'relativedatemode' => false,
6356                  'coursestart' => $pastcoursestart,
6357                  'usercount' => 2,
6358                  'enrolmentmethods' => [
6359                      ['manual', ENROL_INSTANCE_ENABLED],
6360                      ['self', ENROL_INSTANCE_ENABLED]
6361                  ],
6362                  'enrolled' => [[], []],
6363                  'expected' => [
6364                      [
6365                          'start' => $pastcoursestart,
6366                          'startoffset' => 0
6367                      ],
6368                      [
6369                          'start' => $pastcoursestart,
6370                          'startoffset' => 0
6371                      ]
6372                  ]
6373              ],
6374              'past course start fixed 1 users enrolled future' => [
6375                  'relativedatemode' => false,
6376                  'coursestart' => $pastcoursestart,
6377                  'usercount' => 2,
6378                  'enrolmentmethods' => [
6379                      ['manual', ENROL_INSTANCE_ENABLED],
6380                      ['self', ENROL_INSTANCE_ENABLED]
6381                  ],
6382                  'enrolled' => [
6383                      // User 1.
6384                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6385                      // User 2.
6386                      []
6387                  ],
6388                  'expected' => [
6389                      [
6390                          'start' => $pastcoursestart,
6391                          'startoffset' => 0
6392                      ],
6393                      [
6394                          'start' => $pastcoursestart,
6395                          'startoffset' => 0
6396                      ]
6397                  ]
6398              ],
6399              'past course start fixed 1 users enrolled past' => [
6400                  'relativedatemode' => false,
6401                  'coursestart' => $pastcoursestart,
6402                  'usercount' => 2,
6403                  'enrolmentmethods' => [
6404                      ['manual', ENROL_INSTANCE_ENABLED],
6405                      ['self', ENROL_INSTANCE_ENABLED]
6406                  ],
6407                  'enrolled' => [
6408                      // User 1.
6409                      ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6410                      // User 2.
6411                      []
6412                  ],
6413                  'expected' => [
6414                      [
6415                          'start' => $pastcoursestart,
6416                          'startoffset' => 0
6417                      ],
6418                      [
6419                          'start' => $pastcoursestart,
6420                          'startoffset' => 0
6421                      ]
6422                  ]
6423              ],
6424              'past course start fixed 2 users enrolled future' => [
6425                  'relativedatemode' => false,
6426                  'coursestart' => $pastcoursestart,
6427                  'usercount' => 2,
6428                  'enrolmentmethods' => [
6429                      ['manual', ENROL_INSTANCE_ENABLED],
6430                      ['self', ENROL_INSTANCE_ENABLED]
6431                  ],
6432                  'enrolled' => [
6433                      // User 1.
6434                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6435                      // User 2.
6436                      ['manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]]
6437                  ],
6438                  'expected' => [
6439                      [
6440                          'start' => $pastcoursestart,
6441                          'startoffset' => 0
6442                      ],
6443                      [
6444                          'start' => $pastcoursestart,
6445                          'startoffset' => 0
6446                      ]
6447                  ]
6448              ],
6449              'past course start fixed 2 users enrolled past' => [
6450                  'relativedatemode' => false,
6451                  'coursestart' => $pastcoursestart,
6452                  'usercount' => 2,
6453                  'enrolmentmethods' => [
6454                      ['manual', ENROL_INSTANCE_ENABLED],
6455                      ['self', ENROL_INSTANCE_ENABLED]
6456                  ],
6457                  'enrolled' => [
6458                      // User 1.
6459                      ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6460                      // User 2.
6461                      ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6462                  ],
6463                  'expected' => [
6464                      [
6465                          'start' => $pastcoursestart,
6466                          'startoffset' => 0
6467                      ],
6468                      [
6469                          'start' => $pastcoursestart,
6470                          'startoffset' => 0
6471                      ]
6472                  ]
6473              ],
6474              'past course start fixed 2 users enrolled mixed' => [
6475                  'relativedatemode' => false,
6476                  'coursestart' => $pastcoursestart,
6477                  'usercount' => 2,
6478                  'enrolmentmethods' => [
6479                      ['manual', ENROL_INSTANCE_ENABLED],
6480                      ['self', ENROL_INSTANCE_ENABLED]
6481                  ],
6482                  'enrolled' => [
6483                      // User 1.
6484                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6485                      // User 2.
6486                      ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6487                  ],
6488                  'expected' => [
6489                      [
6490                          'start' => $pastcoursestart,
6491                          'startoffset' => 0
6492                      ],
6493                      [
6494                          'start' => $pastcoursestart,
6495                          'startoffset' => 0
6496                      ]
6497                  ]
6498              ],
6499              'past course start fixed 2 users enrolled 2 methods' => [
6500                  'relativedatemode' => false,
6501                  'coursestart' => $pastcoursestart,
6502                  'usercount' => 2,
6503                  'enrolmentmethods' => [
6504                      ['manual', ENROL_INSTANCE_ENABLED],
6505                      ['self', ENROL_INSTANCE_ENABLED]
6506                  ],
6507                  'enrolled' => [
6508                      // User 1.
6509                      [
6510                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6511                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6512                      ],
6513                      // User 2.
6514                      [
6515                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6516                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6517                      ]
6518                  ],
6519                  'expected' => [
6520                      [
6521                          'start' => $pastcoursestart,
6522                          'startoffset' => 0
6523                      ],
6524                      [
6525                          'start' => $pastcoursestart,
6526                          'startoffset' => 0
6527                      ]
6528                  ]
6529              ],
6530              'past course start fixed 2 users enrolled 2 methods 1 disabled' => [
6531                  'relativedatemode' => false,
6532                  'coursestart' => $pastcoursestart,
6533                  'usercount' => 2,
6534                  'enrolmentmethods' => [
6535                      ['manual', ENROL_INSTANCE_DISABLED],
6536                      ['self', ENROL_INSTANCE_ENABLED]
6537                  ],
6538                  'enrolled' => [
6539                      // User 1.
6540                      [
6541                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6542                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6543                      ],
6544                      // User 2.
6545                      [
6546                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6547                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6548                      ]
6549                  ],
6550                  'expected' => [
6551                      [
6552                          'start' => $pastcoursestart,
6553                          'startoffset' => 0
6554                      ],
6555                      [
6556                          'start' => $pastcoursestart,
6557                          'startoffset' => 0
6558                      ]
6559                  ]
6560              ],
6561              'past course start fixed 2 users enrolled 2 methods 2 disabled' => [
6562                  'relativedatemode' => false,
6563                  'coursestart' => $pastcoursestart,
6564                  'usercount' => 2,
6565                  'enrolmentmethods' => [
6566                      ['manual', ENROL_INSTANCE_DISABLED],
6567                      ['self', ENROL_INSTANCE_DISABLED]
6568                  ],
6569                  'enrolled' => [
6570                      // User 1.
6571                      [
6572                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6573                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6574                      ],
6575                      // User 2.
6576                      [
6577                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6578                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6579                      ]
6580                  ],
6581                  'expected' => [
6582                      [
6583                          'start' => $pastcoursestart,
6584                          'startoffset' => 0
6585                      ],
6586                      [
6587                          'start' => $pastcoursestart,
6588                          'startoffset' => 0
6589                      ]
6590                  ]
6591              ],
6592              'past course start fixed 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6593                  'relativedatemode' => false,
6594                  'coursestart' => $pastcoursestart,
6595                  'usercount' => 2,
6596                  'enrolmentmethods' => [
6597                      ['manual', ENROL_INSTANCE_ENABLED],
6598                      ['self', ENROL_INSTANCE_ENABLED]
6599                  ],
6600                  'enrolled' => [
6601                      // User 1.
6602                      [
6603                          'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6604                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6605                      ],
6606                      // User 2.
6607                      [
6608                          'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6609                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6610                      ]
6611                  ],
6612                  'expected' => [
6613                      [
6614                          'start' => $pastcoursestart,
6615                          'startoffset' => 0
6616                      ],
6617                      [
6618                          'start' => $pastcoursestart,
6619                          'startoffset' => 0
6620                      ]
6621                  ]
6622              ],
6623              'past course start fixed 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6624                  'relativedatemode' => false,
6625                  'coursestart' => $pastcoursestart,
6626                  'usercount' => 2,
6627                  'enrolmentmethods' => [
6628                      ['manual', ENROL_INSTANCE_ENABLED],
6629                      ['self', ENROL_INSTANCE_ENABLED]
6630                  ],
6631                  'enrolled' => [
6632                      // User 1.
6633                      [
6634                          'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6635                          'self' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED]
6636                      ],
6637                      // User 2.
6638                      [
6639                          'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6640                          'self' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED]
6641                      ]
6642                  ],
6643                  'expected' => [
6644                      [
6645                          'start' => $pastcoursestart,
6646                          'startoffset' => 0
6647                      ],
6648                      [
6649                          'start' => $pastcoursestart,
6650                          'startoffset' => 0
6651                      ]
6652                  ]
6653              ],
6654              'past course start relative no users enrolled' => [
6655                  'relativedatemode' => true,
6656                  'coursestart' => $pastcoursestart,
6657                  'usercount' => 2,
6658                  'enrolmentmethods' => [
6659                      ['manual', ENROL_INSTANCE_ENABLED],
6660                      ['self', ENROL_INSTANCE_ENABLED]
6661                  ],
6662                  'enrolled' => [[], []],
6663                  'expected' => [
6664                      [
6665                          'start' => $pastcoursestart,
6666                          'startoffset' => 0
6667                      ],
6668                      [
6669                          'start' => $pastcoursestart,
6670                          'startoffset' => 0
6671                      ]
6672                  ]
6673              ],
6674              'past course start relative 1 users enrolled future' => [
6675                  'relativedatemode' => true,
6676                  'coursestart' => $pastcoursestart,
6677                  'usercount' => 2,
6678                  'enrolmentmethods' => [
6679                      ['manual', ENROL_INSTANCE_ENABLED],
6680                      ['self', ENROL_INSTANCE_ENABLED]
6681                  ],
6682                  'enrolled' => [
6683                      // User 1.
6684                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6685                      // User 2.
6686                      []
6687                  ],
6688                  'expected' => [
6689                      [
6690                          'start' => $pastcoursestart + 10,
6691                          'startoffset' => 10
6692                      ],
6693                      [
6694                          'start' => $pastcoursestart,
6695                          'startoffset' => 0
6696                      ]
6697                  ]
6698              ],
6699              'past course start relative 1 users enrolled past' => [
6700                  'relativedatemode' => true,
6701                  'coursestart' => $pastcoursestart,
6702                  'usercount' => 2,
6703                  'enrolmentmethods' => [
6704                      ['manual', ENROL_INSTANCE_ENABLED],
6705                      ['self', ENROL_INSTANCE_ENABLED]
6706                  ],
6707                  'enrolled' => [
6708                      // User 1.
6709                      ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6710                      // User 2.
6711                      []
6712                  ],
6713                  'expected' => [
6714                      [
6715                          'start' => $pastcoursestart,
6716                          'startoffset' => 0
6717                      ],
6718                      [
6719                          'start' => $pastcoursestart,
6720                          'startoffset' => 0
6721                      ]
6722                  ]
6723              ],
6724              'past course start relative 2 users enrolled future' => [
6725                  'relativedatemode' => true,
6726                  'coursestart' => $pastcoursestart,
6727                  'usercount' => 2,
6728                  'enrolmentmethods' => [
6729                      ['manual', ENROL_INSTANCE_ENABLED],
6730                      ['self', ENROL_INSTANCE_ENABLED]
6731                  ],
6732                  'enrolled' => [
6733                      // User 1.
6734                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6735                      // User 2.
6736                      ['manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]]
6737                  ],
6738                  'expected' => [
6739                      [
6740                          'start' => $pastcoursestart + 10,
6741                          'startoffset' => 10
6742                      ],
6743                      [
6744                          'start' => $pastcoursestart + 20,
6745                          'startoffset' => 20
6746                      ]
6747                  ]
6748              ],
6749              'past course start relative 2 users enrolled past' => [
6750                  'relativedatemode' => true,
6751                  'coursestart' => $pastcoursestart,
6752                  'usercount' => 2,
6753                  'enrolmentmethods' => [
6754                      ['manual', ENROL_INSTANCE_ENABLED],
6755                      ['self', ENROL_INSTANCE_ENABLED]
6756                  ],
6757                  'enrolled' => [
6758                      // User 1.
6759                      ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6760                      // User 2.
6761                      ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6762                  ],
6763                  'expected' => [
6764                      [
6765                          'start' => $pastcoursestart,
6766                          'startoffset' => 0
6767                      ],
6768                      [
6769                          'start' => $pastcoursestart,
6770                          'startoffset' => 0
6771                      ]
6772                  ]
6773              ],
6774              'past course start relative 2 users enrolled mixed' => [
6775                  'relativedatemode' => true,
6776                  'coursestart' => $pastcoursestart,
6777                  'usercount' => 2,
6778                  'enrolmentmethods' => [
6779                      ['manual', ENROL_INSTANCE_ENABLED],
6780                      ['self', ENROL_INSTANCE_ENABLED]
6781                  ],
6782                  'enrolled' => [
6783                      // User 1.
6784                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6785                      // User 2.
6786                      ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6787                  ],
6788                  'expected' => [
6789                      [
6790                          'start' => $pastcoursestart + 10,
6791                          'startoffset' => 10
6792                      ],
6793                      [
6794                          'start' => $pastcoursestart,
6795                          'startoffset' => 0
6796                      ]
6797                  ]
6798              ],
6799              'past course start relative 2 users enrolled 2 methods' => [
6800                  'relativedatemode' => true,
6801                  'coursestart' => $pastcoursestart,
6802                  'usercount' => 2,
6803                  'enrolmentmethods' => [
6804                      ['manual', ENROL_INSTANCE_ENABLED],
6805                      ['self', ENROL_INSTANCE_ENABLED]
6806                  ],
6807                  'enrolled' => [
6808                      // User 1.
6809                      [
6810                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6811                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6812                      ],
6813                      // User 2.
6814                      [
6815                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6816                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6817                      ]
6818                  ],
6819                  'expected' => [
6820                      [
6821                          'start' => $pastcoursestart + 10,
6822                          'startoffset' => 10
6823                      ],
6824                      [
6825                          'start' => $pastcoursestart + 10,
6826                          'startoffset' => 10
6827                      ]
6828                  ]
6829              ],
6830              'past course start relative 2 users enrolled 2 methods 1 disabled' => [
6831                  'relativedatemode' => true,
6832                  'coursestart' => $pastcoursestart,
6833                  'usercount' => 2,
6834                  'enrolmentmethods' => [
6835                      ['manual', ENROL_INSTANCE_DISABLED],
6836                      ['self', ENROL_INSTANCE_ENABLED]
6837                  ],
6838                  'enrolled' => [
6839                      // User 1.
6840                      [
6841                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6842                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6843                      ],
6844                      // User 2.
6845                      [
6846                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6847                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6848                      ]
6849                  ],
6850                  'expected' => [
6851                      [
6852                          'start' => $pastcoursestart + 20,
6853                          'startoffset' => 20
6854                      ],
6855                      [
6856                          'start' => $pastcoursestart + 10,
6857                          'startoffset' => 10
6858                      ]
6859                  ]
6860              ],
6861              'past course start relative 2 users enrolled 2 methods 2 disabled' => [
6862                  'relativedatemode' => true,
6863                  'coursestart' => $pastcoursestart,
6864                  'usercount' => 2,
6865                  'enrolmentmethods' => [
6866                      ['manual', ENROL_INSTANCE_DISABLED],
6867                      ['self', ENROL_INSTANCE_DISABLED]
6868                  ],
6869                  'enrolled' => [
6870                      // User 1.
6871                      [
6872                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6873                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6874                      ],
6875                      // User 2.
6876                      [
6877                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6878                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6879                      ]
6880                  ],
6881                  'expected' => [
6882                      [
6883                          'start' => $pastcoursestart,
6884                          'startoffset' => 0
6885                      ],
6886                      [
6887                          'start' => $pastcoursestart,
6888                          'startoffset' => 0
6889                      ]
6890                  ]
6891              ],
6892              'past course start relative 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6893                  'relativedatemode' => true,
6894                  'coursestart' => $pastcoursestart,
6895                  'usercount' => 2,
6896                  'enrolmentmethods' => [
6897                      ['manual', ENROL_INSTANCE_ENABLED],
6898                      ['self', ENROL_INSTANCE_ENABLED]
6899                  ],
6900                  'enrolled' => [
6901                      // User 1.
6902                      [
6903                          'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6904                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6905                      ],
6906                      // User 2.
6907                      [
6908                          'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6909                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6910                      ]
6911                  ],
6912                  'expected' => [
6913                      [
6914                          'start' => $pastcoursestart + 20,
6915                          'startoffset' => 20
6916                      ],
6917                      [
6918                          'start' => $pastcoursestart + 10,
6919                          'startoffset' => 10
6920                      ]
6921                  ]
6922              ],
6923              'past course start relative 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6924                  'relativedatemode' => true,
6925                  'coursestart' => $pastcoursestart,
6926                  'usercount' => 2,
6927                  'enrolmentmethods' => [
6928                      ['manual', ENROL_INSTANCE_ENABLED],
6929                      ['self', ENROL_INSTANCE_ENABLED]
6930                  ],
6931                  'enrolled' => [
6932                      // User 1.
6933                      [
6934                          'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6935                          'self' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED]
6936                      ],
6937                      // User 2.
6938                      [
6939                          'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6940                          'self' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED]
6941                      ]
6942                  ],
6943                  'expected' => [
6944                      [
6945                          'start' => $pastcoursestart,
6946                          'startoffset' => 0
6947                      ],
6948                      [
6949                          'start' => $pastcoursestart,
6950                          'startoffset' => 0
6951                      ]
6952                  ]
6953              ]
6954          ];
6955      }
6956  
6957      /**
6958       * Test the course_get_course_dates_for_user_ids function.
6959       *
6960       * @dataProvider get_course_get_course_dates_for_user_ids_test_cases()
6961       * @param bool $relativedatemode Set the course to relative dates mode
6962       * @param int $coursestart Course start date
6963       * @param int $usercount Number of users to create
6964       * @param array $enrolmentmethods Enrolment methods to set for the course
6965       * @param array $enrolled Enrolment config for to set for the users
6966       * @param array $expected Expected output
6967       */
6968      public function test_course_get_course_dates_for_user_ids(
6969          $relativedatemode,
6970          $coursestart,
6971          $usercount,
6972          $enrolmentmethods,
6973          $enrolled,
6974          $expected
6975      ) {
6976          global $DB;
6977          $this->resetAfterTest();
6978  
6979          $generator = $this->getDataGenerator();
6980          $course  = $generator->create_course(['startdate' => $coursestart]);
6981          $course->relativedatesmode = $relativedatemode;
6982          $users = [];
6983  
6984          for ($i = 0; $i < $usercount; $i++) {
6985              $users[] = $generator->create_user();
6986          }
6987  
6988          foreach ($enrolmentmethods as [$type, $status]) {
6989              $record = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => $type]);
6990              $plugin = enrol_get_plugin($type);
6991              if ($record->status != $status) {
6992                  $plugin->update_status($record, $status);
6993              }
6994          }
6995  
6996          foreach ($enrolled as $index => $enrolconfig) {
6997              $user = $users[$index];
6998              foreach ($enrolconfig as $type => [$starttime, $status]) {
6999                  $generator->enrol_user($user->id, $course->id, 'student', $type, $starttime, 0, $status);
7000              }
7001          }
7002  
7003          $userids = array_map(function($user) {
7004              return $user->id;
7005          }, $users);
7006          $actual = course_get_course_dates_for_user_ids($course, $userids);
7007  
7008          foreach ($expected as $index => $exp) {
7009              $userid = $userids[$index];
7010              $act = $actual[$userid];
7011  
7012              $this->assertEquals($exp['start'], $act['start']);
7013              $this->assertEquals($exp['startoffset'], $act['startoffset']);
7014          }
7015      }
7016  
7017      /**
7018       * Test that calling course_get_course_dates_for_user_ids multiple times in the
7019       * same request fill fetch the correct data for the user.
7020       */
7021      public function test_course_get_course_dates_for_user_ids_multiple_calls() {
7022          $this->resetAfterTest();
7023  
7024          $generator = $this->getDataGenerator();
7025          $now = time();
7026          $coursestart = $now - 1000;
7027          $course  = $generator->create_course(['startdate' => $coursestart]);
7028          $course->relativedatesmode = true;
7029          $user1 = $generator->create_user();
7030          $user2 = $generator->create_user();
7031          $user1start = $coursestart + 100;
7032          $user2start = $coursestart + 200;
7033  
7034          $generator->enrol_user($user1->id, $course->id, 'student', 'manual', $user1start);
7035          $generator->enrol_user($user2->id, $course->id, 'student', 'manual', $user2start);
7036  
7037          $result = course_get_course_dates_for_user_ids($course, [$user1->id]);
7038          $this->assertEquals($user1start, $result[$user1->id]['start']);
7039  
7040          $result = course_get_course_dates_for_user_ids($course, [$user1->id, $user2->id]);
7041          $this->assertEquals($user1start, $result[$user1->id]['start']);
7042          $this->assertEquals($user2start, $result[$user2->id]['start']);
7043  
7044          $result = course_get_course_dates_for_user_ids($course, [$user2->id]);
7045          $this->assertEquals($user2start, $result[$user2->id]['start']);
7046      }
7047  
7048      /**
7049       * Data provider for test_course_modules_pending_deletion.
7050       *
7051       * @return array An array of arrays contain test data
7052       */
7053      public function provider_course_modules_pending_deletion() {
7054          return [
7055              'Non-gradable activity, check all'              => [['forum'], 0, false, true],
7056              'Gradable activity, check all'                  => [['assign'], 0, false, true],
7057              'Non-gradable activity, check gradables'        => [['forum'], 0, true, false],
7058              'Gradable activity, check gradables'            => [['assign'], 0, true, true],
7059              'Non-gradable within multiple, check all'       => [['quiz', 'forum', 'assign'], 1, false, true],
7060              'Non-gradable within multiple, check gradables' => [['quiz', 'forum', 'assign'], 1, true, false],
7061              'Gradable within multiple, check all'           => [['quiz', 'forum', 'assign'], 2, false, true],
7062              'Gradable within multiple, check gradables'     => [['quiz', 'forum', 'assign'], 2, true, true],
7063          ];
7064      }
7065  
7066      /**
7067       * Tests the function course_modules_pending_deletion.
7068       *
7069       * @param string[] $modules A complete list aff all available modules before deletion
7070       * @param int $indextodelete The index of the module in the $modules array that we want to test with
7071       * @param bool $gradable The value to pass to the gradable argument of the course_modules_pending_deletion function
7072       * @param bool $expected The expected result
7073       * @dataProvider provider_course_modules_pending_deletion
7074       */
7075      public function test_course_modules_pending_deletion(array $modules, int $indextodelete, bool $gradable, bool $expected) {
7076          $this->resetAfterTest();
7077  
7078          // Ensure recyclebin is enabled.
7079          set_config('coursebinenable', true, 'tool_recyclebin');
7080  
7081          // Create course and modules.
7082          $generator = $this->getDataGenerator();
7083          $course = $generator->create_course();
7084  
7085          $moduleinstances = [];
7086          foreach ($modules as $module) {
7087              $moduleinstances[] = $generator->create_module($module, array('course' => $course->id));
7088          }
7089  
7090          course_delete_module($moduleinstances[$indextodelete]->cmid, true); // Try to delete the instance asynchronously.
7091          $this->assertEquals($expected, course_modules_pending_deletion($course->id, $gradable));
7092      }
7093  
7094      /**
7095       * Tests for the course_request::can_request
7096       */
7097      public function test_can_request_course() {
7098          global $CFG, $DB;
7099          $this->resetAfterTest();
7100  
7101          $user = $this->getDataGenerator()->create_user();
7102          $cat1 = $CFG->defaultrequestcategory;
7103          $cat2 = $this->getDataGenerator()->create_category()->id;
7104          $cat3 = $this->getDataGenerator()->create_category()->id;
7105          $context1 = context_coursecat::instance($cat1);
7106          $context2 = context_coursecat::instance($cat2);
7107          $context3 = context_coursecat::instance($cat3);
7108          $this->setUser($user);
7109  
7110          // By default users don't have capability to request courses.
7111          $this->assertFalse(course_request::can_request(context_system::instance()));
7112          $this->assertFalse(course_request::can_request($context1));
7113          $this->assertFalse(course_request::can_request($context2));
7114          $this->assertFalse(course_request::can_request($context3));
7115  
7116          // Allow for the 'user' role the capability to request courses.
7117          $userroleid = $DB->get_field('role', 'id', ['shortname' => 'user']);
7118          assign_capability('moodle/course:request', CAP_ALLOW, $userroleid,
7119              context_system::instance()->id);
7120          accesslib_clear_all_caches_for_unit_testing();
7121  
7122          // Lock category selection.
7123          $CFG->lockrequestcategory = 1;
7124  
7125          // Now user can only request course in the default category or in system context.
7126          $this->assertTrue(course_request::can_request(context_system::instance()));
7127          $this->assertTrue(course_request::can_request($context1));
7128          $this->assertFalse(course_request::can_request($context2));
7129          $this->assertFalse(course_request::can_request($context3));
7130  
7131          // Enable category selection. User can request course anywhere.
7132          $CFG->lockrequestcategory = 0;
7133          $this->assertTrue(course_request::can_request(context_system::instance()));
7134          $this->assertTrue(course_request::can_request($context1));
7135          $this->assertTrue(course_request::can_request($context2));
7136          $this->assertTrue(course_request::can_request($context3));
7137  
7138          // Remove cap from cat2.
7139          $roleid = create_role('Test role', 'testrole', 'Test role description');
7140          assign_capability('moodle/course:request', CAP_PROHIBIT, $roleid,
7141              $context2->id, true);
7142          role_assign($roleid, $user->id, $context2->id);
7143          accesslib_clear_all_caches_for_unit_testing();
7144  
7145          $this->assertTrue(course_request::can_request(context_system::instance()));
7146          $this->assertTrue(course_request::can_request($context1));
7147          $this->assertFalse(course_request::can_request($context2));
7148          $this->assertTrue(course_request::can_request($context3));
7149  
7150          // Disable course request functionality.
7151          $CFG->enablecourserequests = false;
7152          $this->assertFalse(course_request::can_request(context_system::instance()));
7153          $this->assertFalse(course_request::can_request($context1));
7154          $this->assertFalse(course_request::can_request($context2));
7155          $this->assertFalse(course_request::can_request($context3));
7156      }
7157  
7158      /**
7159       * Tests for the course_request::can_approve
7160       */
7161      public function test_can_approve_course_request() {
7162          global $CFG;
7163          $this->resetAfterTest();
7164  
7165          $requestor = $this->getDataGenerator()->create_user();
7166          $user = $this->getDataGenerator()->create_user();
7167          $cat1 = $CFG->defaultrequestcategory;
7168          $cat2 = $this->getDataGenerator()->create_category()->id;
7169          $cat3 = $this->getDataGenerator()->create_category()->id;
7170  
7171          // Enable course requests. Default 'user' role has capability to request courses.
7172          $CFG->enablecourserequests = true;
7173          $CFG->lockrequestcategory = 0;
7174          $this->setUser($requestor);
7175          $requestdata = ['summary_editor' => ['text' => '', 'format' => 0], 'name' => 'Req', 'reason' => 'test'];
7176          $request1 = course_request::create((object)($requestdata));
7177          $request2 = course_request::create((object)($requestdata + ['category' => $cat2]));
7178          $request3 = course_request::create((object)($requestdata + ['category' => $cat3]));
7179  
7180          $this->setUser($user);
7181          // Add capability to approve courses.
7182          $roleid = create_role('Test role', 'testrole', 'Test role description');
7183          assign_capability('moodle/site:approvecourse', CAP_ALLOW, $roleid,
7184              context_system::instance()->id, true);
7185          role_assign($roleid, $user->id, context_coursecat::instance($cat2)->id);
7186          accesslib_clear_all_caches_for_unit_testing();
7187  
7188          $this->assertFalse($request1->can_approve());
7189          $this->assertTrue($request2->can_approve());
7190          $this->assertFalse($request3->can_approve());
7191  
7192          // Delete category where course was requested. Now only site-wide manager can approve it.
7193          core_course_category::get($cat2, MUST_EXIST, true)->delete_full(false);
7194          $this->assertFalse($request2->can_approve());
7195  
7196          $this->setAdminUser();
7197          $this->assertTrue($request2->can_approve());
7198      }
7199  
7200      /**
7201       * Test the course allowed module method.
7202       */
7203      public function test_course_allowed_module() {
7204          $this->resetAfterTest();
7205          global $DB;
7206  
7207          $course = $this->getDataGenerator()->create_course();
7208          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
7209          $manager = $this->getDataGenerator()->create_and_enrol($course, 'manager');
7210  
7211          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
7212          assign_capability('mod/assign:addinstance', CAP_PROHIBIT, $teacherrole->id, \context_course::instance($course->id));
7213  
7214          // Global user (teacher) has no permissions in this course.
7215          $this->setUser($teacher);
7216          $this->assertFalse(course_allowed_module($course, 'assign'));
7217  
7218          // Manager has permissions.
7219          $this->assertTrue(course_allowed_module($course, 'assign', $manager));
7220      }
7221  
7222      /**
7223       * Test the {@link average_number_of_participants()} function.
7224       */
7225      public function test_average_number_of_participants() {
7226          global $DB;
7227          $this->resetAfterTest(true);
7228  
7229          $generator = $this->getDataGenerator();
7230          $now = time();
7231  
7232          // If there are no courses, expect zero number of participants per course.
7233          $this->assertEquals(0, average_number_of_participants());
7234  
7235          $c1 = $generator->create_course();
7236          $c2 = $generator->create_course();
7237  
7238          // If there are no users, expect zero number of participants per course.
7239          $this->assertEquals(0, average_number_of_participants());
7240  
7241          $t1 = $generator->create_user(['lastlogin' => $now]);
7242          $s1 = $generator->create_user(['lastlogin' => $now]);
7243          $s2 = $generator->create_user(['lastlogin' => $now - WEEKSECS]);
7244          $s3 = $generator->create_user(['lastlogin' => $now - WEEKSECS]);
7245          $s4 = $generator->create_user(['lastlogin' => $now - YEARSECS]);
7246  
7247          // We have courses, we have users, but no enrolments yet.
7248          $this->assertEquals(0, average_number_of_participants());
7249  
7250          // Front page enrolments are ignored.
7251          $generator->enrol_user($t1->id, SITEID, 'teacher');
7252          $this->assertEquals(0, average_number_of_participants());
7253  
7254          // The teacher enrolled into one of the two courses.
7255          $generator->enrol_user($t1->id, $c1->id, 'editingteacher');
7256          $this->assertEquals(0.5, average_number_of_participants());
7257  
7258          // The teacher enrolled into both courses.
7259          $generator->enrol_user($t1->id, $c2->id, 'editingteacher');
7260          $this->assertEquals(1, average_number_of_participants());
7261  
7262          // Student 1 enrolled in the Course 1 only.
7263          $generator->enrol_user($s1->id, $c1->id, 'student');
7264          $this->assertEquals(1.5, average_number_of_participants());
7265  
7266          // Student 2 enrolled in both courses, but the enrolment in the Course 2 not active yet (enrolment starts in the future).
7267          $generator->enrol_user($s2->id, $c1->id, 'student');
7268          $generator->enrol_user($s2->id, $c2->id, 'student', 'manual', $now + WEEKSECS);
7269          $this->assertEquals(2.5, average_number_of_participants());
7270          $this->assertEquals(2, average_number_of_participants(true));
7271  
7272          // Student 3 enrolled in the Course 1, but the enrolment already expired.
7273          $generator->enrol_user($s3->id, $c1->id, 'student', 'manual', 0, $now - YEARSECS);
7274          $this->assertEquals(3, average_number_of_participants());
7275          $this->assertEquals(2, average_number_of_participants(true));
7276  
7277          // Student 4 enrolled in both courses, but the enrolment has been suspended.
7278          $generator->enrol_user($s4->id, $c1->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
7279          $generator->enrol_user($s4->id, $c2->id, 'student', 'manual', $now - DAYSECS, $now + YEARSECS, ENROL_USER_SUSPENDED);
7280          $this->assertEquals(4, average_number_of_participants());
7281          $this->assertEquals(2, average_number_of_participants(true));
7282  
7283          // Consider only t1 and s1 who logged in recently.
7284          $this->assertEquals(1.5, average_number_of_participants(false, $now - DAYSECS));
7285  
7286          // Consider only t1, s1, s2 and s3 who logged in in recent weeks.
7287          $this->assertEquals(3, average_number_of_participants(false, $now - 4 * WEEKSECS));
7288  
7289          // Hidden courses are excluded from stats.
7290          $DB->set_field('course', 'visible', 0, ['id' => $c1->id]);
7291          $this->assertEquals(3, average_number_of_participants());
7292          $this->assertEquals(1, average_number_of_participants(true));
7293      }
7294  
7295      /**
7296       * Test the set_downloadcontent() function.
7297       */
7298      public function test_set_downloadcontent() {
7299          $this->resetAfterTest();
7300  
7301          $generator = $this->getDataGenerator();
7302          $course = $generator->create_course();
7303          $page = $generator->create_module('page', ['course' => $course]);
7304  
7305          // Test the module 'downloadcontent' field is set to enabled.
7306          set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_ENABLED);
7307          $modinfo = get_fast_modinfo($course)->get_cm($page->cmid);
7308          $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $modinfo->downloadcontent);
7309  
7310          // Now let's test the 'downloadcontent' value is updated to disabled.
7311          set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_DISABLED);
7312          $modinfo = get_fast_modinfo($course)->get_cm($page->cmid);
7313          $this->assertEquals(DOWNLOAD_COURSE_CONTENT_DISABLED, $modinfo->downloadcontent);
7314  
7315          // Nothing to update, the download course content value is the same, it should return false.
7316          $this->assertFalse(set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_DISABLED));
7317  
7318          // The download course content value has changed, it should return true in this case.
7319          $this->assertTrue(set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_ENABLED));
7320      }
7321  
7322      /**
7323       * Test for course_get_courseimage.
7324       *
7325       * @covers ::course_get_courseimage
7326       */
7327      public function test_course_get_courseimage(): void {
7328          global $CFG;
7329  
7330          $this->resetAfterTest();
7331          $generator = $this->getDataGenerator();
7332          $course = $generator->create_course();
7333  
7334          $this->assertNull(course_get_courseimage($course));
7335  
7336          $fs = get_file_storage();
7337          $file = $fs->create_file_from_pathname((object) [
7338              'contextid' => \core\context\course::instance($course->id)->id,
7339              'component' => 'course',
7340              'filearea' => 'overviewfiles',
7341              'itemid' => 0,
7342              'filepath' => '/',
7343              'filename' => 'logo.png',
7344          ], "{$CFG->dirroot}/lib/tests/fixtures/gd-logo.png");
7345  
7346          $image = course_get_courseimage($course);
7347          $this->assertInstanceOf(\stored_file::class, $image);
7348          $this->assertEquals(
7349              $file->get_id(),
7350              $image->get_id(),
7351          );
7352      }
7353  
7354      /**
7355       * Test the course_get_communication_instance_data() function.
7356       *
7357       * @covers ::course_get_communication_instance_data
7358       */
7359      public function test_course_get_communication_instance_data(): void {
7360          $this->resetAfterTest();
7361          $course = $this->getDataGenerator()->create_course();
7362  
7363          // Set admin user as a valid enrolment will be checked in the callback function.
7364          $this->setAdminUser();
7365  
7366          // Use the callback function and return the data.
7367          list($instance, $context, $heading, $returnurl) = component_callback(
7368              'core_course',
7369              'get_communication_instance_data',
7370              [$course->id]
7371          );
7372  
7373          // Check the url is as expected.
7374          $expectedreturnurl = new moodle_url('/course/view.php', ['id' => $course->id]);
7375          $this->assertEquals($expectedreturnurl, $returnurl);
7376  
7377          // Check the context is as expected.
7378          $expectedcontext = context_course::instance($course->id);
7379          $this->assertEquals($expectedcontext, $context);
7380  
7381          // Check the instance id is as expected.
7382          $this->assertEquals($course->id, $instance->id);
7383  
7384          // Check the heading is as expected.
7385          $this->assertEquals($course->fullname, $heading);
7386      }
7387  
7388      /**
7389       * Test the course_update_communication_instance_data() function.
7390       *
7391       * @covers ::course_update_communication_instance_data
7392       */
7393      public function test_course_update_communication_instance_data(): void {
7394          $this->resetAfterTest();
7395          $course = $this->getDataGenerator()->create_course();
7396  
7397          // Set some data to update with.
7398          $data = new stdClass();
7399          $data->instanceid = $course->id;
7400          $data->fullname = 'newname';
7401  
7402          // These should not be the same yet.
7403          $this->assertNotEquals($course->fullname, $data->fullname);
7404  
7405          // Use the callback function to update the course with the data.
7406          component_callback(
7407              'core_course',
7408              'update_communication_instance_data',
7409              [$data]
7410          );
7411  
7412          // Get the course and check it was updated.
7413          $course = get_course($course->id);
7414          $this->assertEquals($course->fullname, $data->fullname);
7415      }
7416  }