Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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 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      public function test_section_visibility_events() {
1337          $this->setAdminUser();
1338          $this->resetAfterTest(true);
1339  
1340          $course = $this->getDataGenerator()->create_course(array('numsections' => 1), array('createsections' => true));
1341          $sectionnumber = 1;
1342          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
1343              array('section' => $sectionnumber));
1344          $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(),
1345              'course' => $course->id), array('section' => $sectionnumber));
1346          $sink = $this->redirectEvents();
1347          set_section_visible($course->id, $sectionnumber, 0);
1348          $events = $sink->get_events();
1349  
1350          // Extract the number of events related to what we are testing, other events
1351          // such as course_section_updated could have been triggered.
1352          $count = 0;
1353          foreach ($events as $event) {
1354              if ($event instanceof \core\event\course_module_updated) {
1355                  $count++;
1356              }
1357          }
1358          $this->assertSame(2, $count);
1359          $sink->close();
1360      }
1361  
1362      public function test_section_visibility() {
1363          $this->setAdminUser();
1364          $this->resetAfterTest(true);
1365  
1366          // Create course.
1367          $course = $this->getDataGenerator()->create_course(array('numsections' => 3), array('createsections' => true));
1368  
1369          $sink = $this->redirectEvents();
1370  
1371          // Testing an empty section.
1372          $sectionnumber = 1;
1373          set_section_visible($course->id, $sectionnumber, 0);
1374          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1375          $this->assertEquals($section_info->visible, 0);
1376          set_section_visible($course->id, $sectionnumber, 1);
1377          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1378          $this->assertEquals($section_info->visible, 1);
1379  
1380          // Checking that an event was fired.
1381          $events = $sink->get_events();
1382          $this->assertInstanceOf('\core\event\course_section_updated', $events[0]);
1383          $sink->close();
1384  
1385          // Testing a section with visible modules.
1386          $sectionnumber = 2;
1387          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
1388                  array('section' => $sectionnumber));
1389          $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(),
1390                  'course' => $course->id), array('section' => $sectionnumber));
1391          $modules = compact('forum', 'assign');
1392          set_section_visible($course->id, $sectionnumber, 0);
1393          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1394          $this->assertEquals($section_info->visible, 0);
1395          foreach ($modules as $mod) {
1396              $this->check_module_visibility($mod, 0, 1);
1397          }
1398          set_section_visible($course->id, $sectionnumber, 1);
1399          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1400          $this->assertEquals($section_info->visible, 1);
1401          foreach ($modules as $mod) {
1402              $this->check_module_visibility($mod, 1, 1);
1403          }
1404  
1405          // Testing a section with hidden modules, which should stay hidden.
1406          $sectionnumber = 3;
1407          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
1408                  array('section' => $sectionnumber));
1409          $assign = $this->getDataGenerator()->create_module('assign', array('duedate' => time(),
1410                  'course' => $course->id), array('section' => $sectionnumber));
1411          $modules = compact('forum', 'assign');
1412          foreach ($modules as $mod) {
1413              set_coursemodule_visible($mod->cmid, 0);
1414              $this->check_module_visibility($mod, 0, 0);
1415          }
1416          set_section_visible($course->id, $sectionnumber, 0);
1417          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1418          $this->assertEquals($section_info->visible, 0);
1419          foreach ($modules as $mod) {
1420              $this->check_module_visibility($mod, 0, 0);
1421          }
1422          set_section_visible($course->id, $sectionnumber, 1);
1423          $section_info = get_fast_modinfo($course->id)->get_section_info($sectionnumber);
1424          $this->assertEquals($section_info->visible, 1);
1425          foreach ($modules as $mod) {
1426              $this->check_module_visibility($mod, 0, 0);
1427          }
1428      }
1429  
1430      /**
1431       * Helper function to assert that a module has correctly been made visible, or hidden.
1432       *
1433       * @param stdClass $mod module information
1434       * @param int $visibility the current state of the module
1435       * @param int $visibleold the current state of the visibleold property
1436       * @return void
1437       */
1438      public function check_module_visibility($mod, $visibility, $visibleold) {
1439          global $DB;
1440          $cm = get_fast_modinfo($mod->course)->get_cm($mod->cmid);
1441          $this->assertEquals($visibility, $cm->visible);
1442          $this->assertEquals($visibleold, $cm->visibleold);
1443  
1444          // Check the module grade items.
1445          $grade_items = grade_item::fetch_all(array('itemtype' => 'mod', 'itemmodule' => $cm->modname,
1446                  'iteminstance' => $cm->instance, 'courseid' => $cm->course));
1447          if ($grade_items) {
1448              foreach ($grade_items as $grade_item) {
1449                  if ($visibility) {
1450                      $this->assertFalse($grade_item->is_hidden(), "$cm->modname grade_item not visible");
1451                  } else {
1452                      $this->assertTrue($grade_item->is_hidden(), "$cm->modname grade_item not hidden");
1453                  }
1454              }
1455          }
1456  
1457          // Check the events visibility.
1458          if ($events = $DB->get_records('event', array('instance' => $cm->instance, 'modulename' => $cm->modname))) {
1459              foreach ($events as $event) {
1460                  $calevent = new calendar_event($event);
1461                  $this->assertEquals($visibility, $calevent->visible, "$cm->modname calendar_event visibility");
1462              }
1463          }
1464      }
1465  
1466      public function test_course_page_type_list() {
1467          global $DB;
1468          $this->resetAfterTest(true);
1469  
1470          // Create a category.
1471          $category = new stdClass();
1472          $category->name = 'Test Category';
1473  
1474          $testcategory = $this->getDataGenerator()->create_category($category);
1475  
1476          // Create a course.
1477          $course = new stdClass();
1478          $course->fullname = 'Apu loves Unit Təsts';
1479          $course->shortname = 'Spread the lŭve';
1480          $course->idnumber = '123';
1481          $course->summary = 'Awesome!';
1482          $course->summaryformat = FORMAT_PLAIN;
1483          $course->format = 'topics';
1484          $course->newsitems = 0;
1485          $course->numsections = 5;
1486          $course->category = $testcategory->id;
1487  
1488          $testcourse = $this->getDataGenerator()->create_course($course);
1489  
1490          // Create contexts.
1491          $coursecontext = context_course::instance($testcourse->id);
1492          $parentcontext = $coursecontext->get_parent_context(); // Not actually used.
1493          $pagetype = 'page-course-x'; // Not used either.
1494          $pagetypelist = course_page_type_list($pagetype, $parentcontext, $coursecontext);
1495  
1496          // Page type lists for normal courses.
1497          $testpagetypelist1 = array();
1498          $testpagetypelist1['*'] = 'Any page';
1499          $testpagetypelist1['course-*'] = 'Any course page';
1500          $testpagetypelist1['course-view-*'] = 'Any type of course main page';
1501  
1502          $this->assertEquals($testpagetypelist1, $pagetypelist);
1503  
1504          // Get the context for the front page course.
1505          $sitecoursecontext = context_course::instance(SITEID);
1506          $pagetypelist = course_page_type_list($pagetype, $parentcontext, $sitecoursecontext);
1507  
1508          // Page type list for the front page course.
1509          $testpagetypelist2 = array('*' => 'Any page');
1510          $this->assertEquals($testpagetypelist2, $pagetypelist);
1511  
1512          // Make sure that providing no current context to the function doesn't result in an error.
1513          // Calls made from generate_page_type_patterns() may provide null values.
1514          $pagetypelist = course_page_type_list($pagetype, null, null);
1515          $this->assertEquals($pagetypelist, $testpagetypelist1);
1516      }
1517  
1518      public function test_compare_activities_by_time_desc() {
1519  
1520          // Let's create some test data.
1521          $activitiesivities = array();
1522          $x = new stdClass();
1523          $x->timestamp = null;
1524          $activities[] = $x;
1525  
1526          $x = new stdClass();
1527          $x->timestamp = 1;
1528          $activities[] = $x;
1529  
1530          $x = new stdClass();
1531          $x->timestamp = 3;
1532          $activities[] = $x;
1533  
1534          $x = new stdClass();
1535          $x->timestamp = 0;
1536          $activities[] = $x;
1537  
1538          $x = new stdClass();
1539          $x->timestamp = 5;
1540          $activities[] = $x;
1541  
1542          $x = new stdClass();
1543          $activities[] = $x;
1544  
1545          $x = new stdClass();
1546          $x->timestamp = 5;
1547          $activities[] = $x;
1548  
1549          // Do the sorting.
1550          usort($activities, 'compare_activities_by_time_desc');
1551  
1552          // Let's check the result.
1553          $last = 10;
1554          foreach($activities as $activity) {
1555              if (empty($activity->timestamp)) {
1556                  $activity->timestamp = 0;
1557              }
1558              $this->assertLessThanOrEqual($last, $activity->timestamp);
1559          }
1560      }
1561  
1562      public function test_compare_activities_by_time_asc() {
1563  
1564          // Let's create some test data.
1565          $activities = array();
1566          $x = new stdClass();
1567          $x->timestamp = null;
1568          $activities[] = $x;
1569  
1570          $x = new stdClass();
1571          $x->timestamp = 1;
1572          $activities[] = $x;
1573  
1574          $x = new stdClass();
1575          $x->timestamp = 3;
1576          $activities[] = $x;
1577  
1578          $x = new stdClass();
1579          $x->timestamp = 0;
1580          $activities[] = $x;
1581  
1582          $x = new stdClass();
1583          $x->timestamp = 5;
1584          $activities[] = $x;
1585  
1586          $x = new stdClass();
1587          $activities[] = $x;
1588  
1589          $x = new stdClass();
1590          $x->timestamp = 5;
1591          $activities[] = $x;
1592  
1593          // Do the sorting.
1594          usort($activities, 'compare_activities_by_time_asc');
1595  
1596          // Let's check the result.
1597          $last = 0;
1598          foreach($activities as $activity) {
1599              if (empty($activity->timestamp)) {
1600                  $activity->timestamp = 0;
1601              }
1602              $this->assertGreaterThanOrEqual($last, $activity->timestamp);
1603          }
1604      }
1605  
1606      /**
1607       * Tests moving a module between hidden/visible sections and
1608       * verifies that the course/module visiblity seettings are
1609       * retained.
1610       */
1611      public function test_moveto_module_between_hidden_sections() {
1612          global $DB;
1613  
1614          $this->resetAfterTest(true);
1615  
1616          $course = $this->getDataGenerator()->create_course(array('numsections' => 4), array('createsections' => true));
1617          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
1618          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1619          $quiz= $this->getDataGenerator()->create_module('quiz', array('course' => $course->id));
1620  
1621          // Set the page as hidden
1622          set_coursemodule_visible($page->cmid, 0);
1623  
1624          // Set sections 3 as hidden.
1625          set_section_visible($course->id, 3, 0);
1626  
1627          $modinfo = get_fast_modinfo($course);
1628  
1629          $hiddensection = $modinfo->get_section_info(3);
1630          // New section is definitely not visible:
1631          $this->assertEquals($hiddensection->visible, 0);
1632  
1633          $forumcm = $modinfo->cms[$forum->cmid];
1634          $pagecm = $modinfo->cms[$page->cmid];
1635  
1636          // Move the forum and the page to a hidden section, make sure moveto_module returns 0 as new visibility state.
1637          $this->assertEquals(0, moveto_module($forumcm, $hiddensection));
1638          $this->assertEquals(0, moveto_module($pagecm, $hiddensection));
1639  
1640          $modinfo = get_fast_modinfo($course);
1641  
1642          // Verify that forum and page have been moved to the hidden section and quiz has not.
1643          $this->assertContainsEquals($forum->cmid, $modinfo->sections[3]);
1644          $this->assertContainsEquals($page->cmid, $modinfo->sections[3]);
1645          $this->assertNotContainsEquals($quiz->cmid, $modinfo->sections[3]);
1646  
1647          // Verify that forum has been made invisible.
1648          $forumcm = $modinfo->cms[$forum->cmid];
1649          $this->assertEquals($forumcm->visible, 0);
1650          // Verify that old state has been retained.
1651          $this->assertEquals($forumcm->visibleold, 1);
1652  
1653          // Verify that page has stayed invisible.
1654          $pagecm = $modinfo->cms[$page->cmid];
1655          $this->assertEquals($pagecm->visible, 0);
1656          // Verify that old state has been retained.
1657          $this->assertEquals($pagecm->visibleold, 0);
1658  
1659          // Verify that quiz has been unaffected.
1660          $quizcm = $modinfo->cms[$quiz->cmid];
1661          $this->assertEquals($quizcm->visible, 1);
1662  
1663          // Move forum and page back to visible section.
1664          // Make sure the visibility is restored to the original value (visible for forum and hidden for page).
1665          $visiblesection = $modinfo->get_section_info(2);
1666          $this->assertEquals(1, moveto_module($forumcm, $visiblesection));
1667          $this->assertEquals(0, moveto_module($pagecm, $visiblesection));
1668  
1669          $modinfo = get_fast_modinfo($course);
1670  
1671          // Double check that forum has been made visible.
1672          $forumcm = $modinfo->cms[$forum->cmid];
1673          $this->assertEquals($forumcm->visible, 1);
1674  
1675          // Double check that page has stayed invisible.
1676          $pagecm = $modinfo->cms[$page->cmid];
1677          $this->assertEquals($pagecm->visible, 0);
1678  
1679          // Move the page in the same section (this is what mod duplicate does).
1680          // Visibility of page remains 0.
1681          $this->assertEquals(0, moveto_module($pagecm, $visiblesection, $forumcm));
1682  
1683          // Double check that the the page is still hidden.
1684          $modinfo = get_fast_modinfo($course);
1685          $pagecm = $modinfo->cms[$page->cmid];
1686          $this->assertEquals($pagecm->visible, 0);
1687      }
1688  
1689      /**
1690       * Tests moving a module around in the same section. moveto_module()
1691       * is called this way in modduplicate.
1692       */
1693      public function test_moveto_module_in_same_section() {
1694          global $DB;
1695  
1696          $this->resetAfterTest(true);
1697  
1698          $course = $this->getDataGenerator()->create_course(array('numsections' => 3), array('createsections' => true));
1699          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id));
1700          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id));
1701  
1702          // Simulate inconsistent visible/visibleold values (MDL-38713).
1703          $cm = $DB->get_record('course_modules', array('id' => $page->cmid), '*', MUST_EXIST);
1704          $cm->visible = 0;
1705          $cm->visibleold = 1;
1706          $DB->update_record('course_modules', $cm);
1707  
1708          $modinfo = get_fast_modinfo($course);
1709          $forumcm = $modinfo->cms[$forum->cmid];
1710          $pagecm = $modinfo->cms[$page->cmid];
1711  
1712          // Verify that page is hidden.
1713          $this->assertEquals($pagecm->visible, 0);
1714  
1715          // Verify section 0 is where all mods added.
1716          $section = $modinfo->get_section_info(0);
1717          $this->assertEquals($section->id, $forumcm->section);
1718          $this->assertEquals($section->id, $pagecm->section);
1719  
1720  
1721          // Move the page inside the hidden section. Make sure it is hidden.
1722          $this->assertEquals(0, moveto_module($pagecm, $section, $forumcm));
1723  
1724          // Double check that the the page is still hidden.
1725          $modinfo = get_fast_modinfo($course);
1726          $pagecm = $modinfo->cms[$page->cmid];
1727          $this->assertEquals($pagecm->visible, 0);
1728      }
1729  
1730      /**
1731       * Tests the function that deletes a course module
1732       *
1733       * @param string $type The type of module for the test
1734       * @param array $options The options for the module creation
1735       * @dataProvider provider_course_delete_module
1736       */
1737      public function test_course_delete_module($type, $options) {
1738          global $DB;
1739  
1740          $this->resetAfterTest(true);
1741          $this->setAdminUser();
1742  
1743          // Create course and modules.
1744          $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
1745          $options['course'] = $course->id;
1746  
1747          // Generate an assignment with due date (will generate a course event).
1748          $module = $this->getDataGenerator()->create_module($type, $options);
1749  
1750          // Get the module context.
1751          $modcontext = context_module::instance($module->cmid);
1752  
1753          $assocblog = $this->create_module_asscociated_blog($course, $modcontext);
1754  
1755          // Verify context exists.
1756          $this->assertInstanceOf('context_module', $modcontext);
1757  
1758          // Make module specific messes.
1759          switch ($type) {
1760              case 'assign':
1761                  // Add some tags to this assignment.
1762                  core_tag_tag::set_item_tags('mod_assign', 'assign', $module->id, $modcontext, array('Tag 1', 'Tag 2', 'Tag 3'));
1763                  core_tag_tag::set_item_tags('core', 'course_modules', $module->cmid, $modcontext, array('Tag 3', 'Tag 4', 'Tag 5'));
1764  
1765                  // Confirm the tag instances were added.
1766                  $criteria = array('component' => 'mod_assign', 'itemtype' => 'assign', 'contextid' => $modcontext->id);
1767                  $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
1768                  $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
1769                  $this->assertEquals(3, $DB->count_records('tag_instance', $criteria));
1770  
1771                  // Verify event assignment event has been generated.
1772                  $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type));
1773                  $this->assertEquals(1, $eventcount);
1774  
1775                  break;
1776              case 'quiz':
1777                  $qgen = $this->getDataGenerator()->get_plugin_generator('core_question');
1778                  $qcat = $qgen->create_question_category(array('contextid' => $modcontext->id));
1779                  $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
1780                  $qgen->create_question('shortanswer', null, array('category' => $qcat->id));
1781                  break;
1782              default:
1783                  break;
1784          }
1785  
1786          // Run delete..
1787          course_delete_module($module->cmid);
1788  
1789          // Verify the context has been removed.
1790          $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
1791  
1792          // Verify the course_module record has been deleted.
1793          $cmcount = $DB->count_records('course_modules', array('id' => $module->cmid));
1794          $this->assertEmpty($cmcount);
1795  
1796          // Verify the blog_association record has been deleted.
1797          $this->assertCount(0, $DB->get_records('blog_association',
1798                  array('contextid' => $modcontext->id)));
1799  
1800          // Verify the blog post record has been deleted.
1801          $this->assertCount(0, $DB->get_records('post',
1802                  array('id' => $assocblog->id)));
1803  
1804          // Verify the tag instance record has been deleted.
1805          $this->assertCount(0, $DB->get_records('tag_instance',
1806                  array('itemid' => $assocblog->id)));
1807  
1808          // Test clean up of module specific messes.
1809          switch ($type) {
1810              case 'assign':
1811                  // Verify event assignment events have been removed.
1812                  $eventcount = $DB->count_records('event', array('instance' => $module->id, 'modulename' => $type));
1813                  $this->assertEmpty($eventcount);
1814  
1815                  // Verify the tag instances were deleted.
1816                  $criteria = array('component' => 'mod_assign', 'contextid' => $modcontext->id);
1817                  $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
1818  
1819                  $criteria = array('component' => 'core', 'itemtype' => 'course_modules', 'contextid' => $modcontext->id);
1820                  $this->assertEquals(0, $DB->count_records('tag_instance', $criteria));
1821                  break;
1822              case 'quiz':
1823                  // Verify category deleted.
1824                  $criteria = array('contextid' => $modcontext->id);
1825                  $this->assertEquals(0, $DB->count_records('question_categories', $criteria));
1826  
1827                  // Verify questions deleted.
1828                  $criteria = [$qcat->id];
1829                  $sql = 'SELECT COUNT(q.id)
1830                            FROM {question} q
1831                            JOIN {question_versions} qv ON qv.questionid = q.id
1832                            JOIN {question_bank_entries} qbe ON qbe.id = qv.questionbankentryid
1833                            WHERE qbe.questioncategoryid = ?';
1834                  $this->assertEquals(0, $DB->count_records_sql($sql, $criteria));
1835                  break;
1836              default:
1837                  break;
1838          }
1839      }
1840  
1841      /**
1842       * Test that triggering a course_created event works as expected.
1843       */
1844      public function test_course_created_event() {
1845          global $DB;
1846  
1847          $this->resetAfterTest();
1848  
1849          // Catch the events.
1850          $sink = $this->redirectEvents();
1851  
1852          // Create the course with an id number which is used later when generating a course via the imsenterprise plugin.
1853          $data = new stdClass();
1854          $data->idnumber = 'idnumber';
1855          $course = $this->getDataGenerator()->create_course($data);
1856          // Get course from DB for comparison.
1857          $course = $DB->get_record('course', array('id' => $course->id));
1858  
1859          // Capture the event.
1860          $events = $sink->get_events();
1861          $sink->close();
1862  
1863          // Validate the event.
1864          $event = $events[0];
1865          $this->assertInstanceOf('\core\event\course_created', $event);
1866          $this->assertEquals('course', $event->objecttable);
1867          $this->assertEquals($course->id, $event->objectid);
1868          $this->assertEquals(context_course::instance($course->id), $event->get_context());
1869          $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
1870          $this->assertEquals('course_created', $event->get_legacy_eventname());
1871          $this->assertEventLegacyData($course, $event);
1872          $expectedlog = array(SITEID, 'course', 'new', 'view.php?id=' . $course->id, $course->fullname . ' (ID ' . $course->id . ')');
1873          $this->assertEventLegacyLogData($expectedlog, $event);
1874  
1875          // Now we want to trigger creating a course via the imsenterprise.
1876          // Delete the course we created earlier, as we want the imsenterprise plugin to create this.
1877          // We do not want print out any of the text this function generates while doing this, which is why
1878          // we are using ob_start() and ob_end_clean().
1879          ob_start();
1880          delete_course($course);
1881          ob_end_clean();
1882  
1883          // Create the XML file we want to use.
1884          $course->category = (array)$course->category;
1885          $imstestcase = new imsenterprise_test();
1886          $imstestcase->imsplugin = enrol_get_plugin('imsenterprise');
1887          $imstestcase->set_test_config();
1888          $imstestcase->set_xml_file(false, array($course));
1889  
1890          // Capture the event.
1891          $sink = $this->redirectEvents();
1892          $imstestcase->imsplugin->cron();
1893          $events = $sink->get_events();
1894          $sink->close();
1895          $event = null;
1896          foreach ($events as $eventinfo) {
1897              if ($eventinfo instanceof \core\event\course_created ) {
1898                  $event = $eventinfo;
1899                  break;
1900              }
1901          }
1902  
1903          // Validate the event triggered is \core\event\course_created. There is no need to validate the other values
1904          // as they have already been validated in the previous steps. Here we only want to make sure that when the
1905          // imsenterprise plugin creates a course an event is triggered.
1906          $this->assertInstanceOf('\core\event\course_created', $event);
1907          $this->assertEventContextNotUsed($event);
1908      }
1909  
1910      /**
1911       * Test that triggering a course_updated event works as expected.
1912       */
1913      public function test_course_updated_event() {
1914          global $DB;
1915  
1916          $this->resetAfterTest();
1917  
1918          // Create a course.
1919          $course = $this->getDataGenerator()->create_course();
1920  
1921          // Create a category we are going to move this course to.
1922          $category = $this->getDataGenerator()->create_category();
1923  
1924          // Create a hidden category we are going to move this course to.
1925          $categoryhidden = $this->getDataGenerator()->create_category(array('visible' => 0));
1926  
1927          // Update course and catch course_updated event.
1928          $sink = $this->redirectEvents();
1929          update_course($course);
1930          $events = $sink->get_events();
1931          $sink->close();
1932  
1933          // Get updated course information from the DB.
1934          $updatedcourse = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
1935          // Validate event.
1936          $event = array_shift($events);
1937          $this->assertInstanceOf('\core\event\course_updated', $event);
1938          $this->assertEquals('course', $event->objecttable);
1939          $this->assertEquals($updatedcourse->id, $event->objectid);
1940          $this->assertEquals(context_course::instance($course->id), $event->get_context());
1941          $url = new moodle_url('/course/edit.php', array('id' => $event->objectid));
1942          $this->assertEquals($url, $event->get_url());
1943          $this->assertEquals($updatedcourse, $event->get_record_snapshot('course', $event->objectid));
1944          $this->assertEquals('course_updated', $event->get_legacy_eventname());
1945          $this->assertEventLegacyData($updatedcourse, $event);
1946          $expectedlog = array($updatedcourse->id, 'course', 'update', 'edit.php?id=' . $course->id, $course->id);
1947          $this->assertEventLegacyLogData($expectedlog, $event);
1948  
1949          // Move course and catch course_updated event.
1950          $sink = $this->redirectEvents();
1951          move_courses(array($course->id), $category->id);
1952          $events = $sink->get_events();
1953          $sink->close();
1954  
1955          // Return the moved course information from the DB.
1956          $movedcourse = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
1957          // Validate event.
1958          $event = array_shift($events);
1959          $this->assertInstanceOf('\core\event\course_updated', $event);
1960          $this->assertEquals('course', $event->objecttable);
1961          $this->assertEquals($movedcourse->id, $event->objectid);
1962          $this->assertEquals(context_course::instance($course->id), $event->get_context());
1963          $this->assertEquals($movedcourse, $event->get_record_snapshot('course', $movedcourse->id));
1964          $this->assertEquals('course_updated', $event->get_legacy_eventname());
1965          $this->assertEventLegacyData($movedcourse, $event);
1966          $expectedlog = array($movedcourse->id, 'course', 'move', 'edit.php?id=' . $movedcourse->id, $movedcourse->id);
1967          $this->assertEventLegacyLogData($expectedlog, $event);
1968  
1969          // Move course to hidden category and catch course_updated event.
1970          $sink = $this->redirectEvents();
1971          move_courses(array($course->id), $categoryhidden->id);
1972          $events = $sink->get_events();
1973          $sink->close();
1974  
1975          // Return the moved course information from the DB.
1976          $movedcoursehidden = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
1977          // Validate event.
1978          $event = array_shift($events);
1979          $this->assertInstanceOf('\core\event\course_updated', $event);
1980          $this->assertEquals('course', $event->objecttable);
1981          $this->assertEquals($movedcoursehidden->id, $event->objectid);
1982          $this->assertEquals(context_course::instance($course->id), $event->get_context());
1983          $this->assertEquals($movedcoursehidden, $event->get_record_snapshot('course', $movedcoursehidden->id));
1984          $this->assertEquals('course_updated', $event->get_legacy_eventname());
1985          $this->assertEventLegacyData($movedcoursehidden, $event);
1986          $expectedlog = array($movedcoursehidden->id, 'course', 'move', 'edit.php?id=' . $movedcoursehidden->id, $movedcoursehidden->id);
1987          $this->assertEventLegacyLogData($expectedlog, $event);
1988          $this->assertEventContextNotUsed($event);
1989      }
1990  
1991      /**
1992       * Test that triggering a course_updated event logs changes.
1993       */
1994      public function test_course_updated_event_with_changes() {
1995          global $DB;
1996  
1997          $this->resetAfterTest();
1998  
1999          // Create a course.
2000          $course = $this->getDataGenerator()->create_course((object)['visible' => 1]);
2001  
2002          $editedcourse = $DB->get_record('course', ['id' => $course->id]);
2003          $editedcourse->visible = 0;
2004  
2005          // Update course and catch course_updated event.
2006          $sink = $this->redirectEvents();
2007          update_course($editedcourse);
2008          $events = $sink->get_events();
2009          $sink->close();
2010  
2011          $event = array_shift($events);
2012          $this->assertInstanceOf('\core\event\course_updated', $event);
2013          $otherdata = [
2014              'shortname' => $course->shortname,
2015              'fullname' => $course->fullname,
2016              'updatedfields' => [
2017                  'visible' => 0
2018              ]
2019          ];
2020          $this->assertEquals($otherdata, $event->other);
2021  
2022      }
2023  
2024      /**
2025       * Test that triggering a course_deleted event works as expected.
2026       */
2027      public function test_course_deleted_event() {
2028          $this->resetAfterTest();
2029  
2030          // Create the course.
2031          $course = $this->getDataGenerator()->create_course();
2032  
2033          // Save the course context before we delete the course.
2034          $coursecontext = context_course::instance($course->id);
2035  
2036          // Catch the update event.
2037          $sink = $this->redirectEvents();
2038  
2039          // Call delete_course() which will trigger the course_deleted event and the course_content_deleted
2040          // event. This function prints out data to the screen, which we do not want during a PHPUnit test,
2041          // so use ob_start and ob_end_clean to prevent this.
2042          ob_start();
2043          delete_course($course);
2044          ob_end_clean();
2045  
2046          // Capture the event.
2047          $events = $sink->get_events();
2048          $sink->close();
2049  
2050          // Validate the event.
2051          $event = array_pop($events);
2052          $this->assertInstanceOf('\core\event\course_deleted', $event);
2053          $this->assertEquals('course', $event->objecttable);
2054          $this->assertEquals($course->id, $event->objectid);
2055          $this->assertEquals($coursecontext->id, $event->contextid);
2056          $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
2057          $this->assertEquals('course_deleted', $event->get_legacy_eventname());
2058          $eventdata = $event->get_data();
2059          $this->assertSame($course->idnumber, $eventdata['other']['idnumber']);
2060          $this->assertSame($course->fullname, $eventdata['other']['fullname']);
2061          $this->assertSame($course->shortname, $eventdata['other']['shortname']);
2062  
2063          // The legacy data also passed the context in the course object and substitutes timemodified with the current date.
2064          $expectedlegacy = clone($course);
2065          $expectedlegacy->context = $coursecontext;
2066          $expectedlegacy->timemodified = $event->timecreated;
2067          $this->assertEventLegacyData($expectedlegacy, $event);
2068  
2069          // Validate legacy log data.
2070          $expectedlog = array(SITEID, 'course', 'delete', 'view.php?id=' . $course->id, $course->fullname . '(ID ' . $course->id . ')');
2071          $this->assertEventLegacyLogData($expectedlog, $event);
2072          $this->assertEventContextNotUsed($event);
2073      }
2074  
2075      /**
2076       * Test that triggering a course_content_deleted event works as expected.
2077       */
2078      public function test_course_content_deleted_event() {
2079          global $DB;
2080  
2081          $this->resetAfterTest();
2082  
2083          // Create the course.
2084          $course = $this->getDataGenerator()->create_course();
2085  
2086          // Get the course from the DB. The data generator adds some extra properties, such as
2087          // numsections, to the course object which will fail the assertions later on.
2088          $course = $DB->get_record('course', array('id' => $course->id), '*', MUST_EXIST);
2089  
2090          // Save the course context before we delete the course.
2091          $coursecontext = context_course::instance($course->id);
2092  
2093          // Catch the update event.
2094          $sink = $this->redirectEvents();
2095  
2096          remove_course_contents($course->id, false);
2097  
2098          // Capture the event.
2099          $events = $sink->get_events();
2100          $sink->close();
2101  
2102          // Validate the event.
2103          $event = array_pop($events);
2104          $this->assertInstanceOf('\core\event\course_content_deleted', $event);
2105          $this->assertEquals('course', $event->objecttable);
2106          $this->assertEquals($course->id, $event->objectid);
2107          $this->assertEquals($coursecontext->id, $event->contextid);
2108          $this->assertEquals($course, $event->get_record_snapshot('course', $course->id));
2109          $this->assertEquals('course_content_removed', $event->get_legacy_eventname());
2110          // The legacy data also passed the context and options in the course object.
2111          $course->context = $coursecontext;
2112          $course->options = array();
2113          $this->assertEventLegacyData($course, $event);
2114          $this->assertEventContextNotUsed($event);
2115      }
2116  
2117      /**
2118       * Test that triggering a course_category_deleted event works as expected.
2119       */
2120      public function test_course_category_deleted_event() {
2121          $this->resetAfterTest();
2122  
2123          // Create a category.
2124          $category = $this->getDataGenerator()->create_category();
2125  
2126          // Save the original record/context before it is deleted.
2127          $categoryrecord = $category->get_db_record();
2128          $categorycontext = context_coursecat::instance($category->id);
2129  
2130          // Catch the update event.
2131          $sink = $this->redirectEvents();
2132  
2133          // Delete the category.
2134          $category->delete_full();
2135  
2136          // Capture the event.
2137          $events = $sink->get_events();
2138          $sink->close();
2139  
2140          // Validate the event.
2141          $event = $events[0];
2142          $this->assertInstanceOf('\core\event\course_category_deleted', $event);
2143          $this->assertEquals('course_categories', $event->objecttable);
2144          $this->assertEquals($category->id, $event->objectid);
2145          $this->assertEquals($categorycontext->id, $event->contextid);
2146          $this->assertEquals([
2147              'name' => $category->name,
2148          ], $event->other);
2149          $this->assertEquals($categoryrecord, $event->get_record_snapshot($event->objecttable, $event->objectid));
2150          $this->assertEquals('course_category_deleted', $event->get_legacy_eventname());
2151          $this->assertEquals(null, $event->get_url());
2152          $this->assertEventLegacyData($category, $event);
2153          $expectedlog = array(SITEID, 'category', 'delete', 'index.php', $category->name . '(ID ' . $category->id . ')');
2154          $this->assertEventLegacyLogData($expectedlog, $event);
2155  
2156          // Create two categories.
2157          $category = $this->getDataGenerator()->create_category();
2158          $category2 = $this->getDataGenerator()->create_category();
2159  
2160          // Save the original record/context before it is moved and then deleted.
2161          $category2record = $category2->get_db_record();
2162          $category2context = context_coursecat::instance($category2->id);
2163  
2164          // Catch the update event.
2165          $sink = $this->redirectEvents();
2166  
2167          // Move the category.
2168          $category2->delete_move($category->id);
2169  
2170          // Capture the event.
2171          $events = $sink->get_events();
2172          $sink->close();
2173  
2174          // Validate the event.
2175          $event = $events[0];
2176          $this->assertInstanceOf('\core\event\course_category_deleted', $event);
2177          $this->assertEquals('course_categories', $event->objecttable);
2178          $this->assertEquals($category2->id, $event->objectid);
2179          $this->assertEquals($category2context->id, $event->contextid);
2180          $this->assertEquals([
2181              'name' => $category2->name,
2182              'contentmovedcategoryid' => $category->id,
2183          ], $event->other);
2184          $this->assertEquals($category2record, $event->get_record_snapshot($event->objecttable, $event->objectid));
2185          $this->assertEquals('course_category_deleted', $event->get_legacy_eventname());
2186          $this->assertEventLegacyData($category2, $event);
2187          $expectedlog = array(SITEID, 'category', 'delete', 'index.php', $category2->name . '(ID ' . $category2->id . ')');
2188          $this->assertEventLegacyLogData($expectedlog, $event);
2189          $this->assertEventContextNotUsed($event);
2190      }
2191  
2192      /**
2193       * Test that triggering a course_backup_created event works as expected.
2194       */
2195      public function test_course_backup_created_event() {
2196          global $CFG;
2197  
2198          // Get the necessary files to perform backup and restore.
2199          require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
2200          require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2201  
2202          $this->resetAfterTest();
2203  
2204          // Set to admin user.
2205          $this->setAdminUser();
2206  
2207          // The user id is going to be 2 since we are the admin user.
2208          $userid = 2;
2209  
2210          // Create a course.
2211          $course = $this->getDataGenerator()->create_course();
2212  
2213          // Create backup file and save it to the backup location.
2214          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
2215              backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userid);
2216          $sink = $this->redirectEvents();
2217          $bc->execute_plan();
2218  
2219          // Capture the event.
2220          $events = $sink->get_events();
2221          $sink->close();
2222  
2223          // Validate the event.
2224          $event = array_pop($events);
2225          $this->assertInstanceOf('\core\event\course_backup_created', $event);
2226          $this->assertEquals('course', $event->objecttable);
2227          $this->assertEquals($bc->get_courseid(), $event->objectid);
2228          $this->assertEquals(context_course::instance($bc->get_courseid())->id, $event->contextid);
2229  
2230          $url = new moodle_url('/course/view.php', array('id' => $event->objectid));
2231          $this->assertEquals($url, $event->get_url());
2232          $this->assertEventContextNotUsed($event);
2233  
2234          // Destroy the resource controller since we are done using it.
2235          $bc->destroy();
2236      }
2237  
2238      /**
2239       * Test that triggering a course_restored event works as expected.
2240       */
2241      public function test_course_restored_event() {
2242          global $CFG;
2243  
2244          // Get the necessary files to perform backup and restore.
2245          require_once($CFG->dirroot . '/backup/util/includes/backup_includes.php');
2246          require_once($CFG->dirroot . '/backup/util/includes/restore_includes.php');
2247  
2248          $this->resetAfterTest();
2249  
2250          // Set to admin user.
2251          $this->setAdminUser();
2252  
2253          // The user id is going to be 2 since we are the admin user.
2254          $userid = 2;
2255  
2256          // Create a course.
2257          $course = $this->getDataGenerator()->create_course();
2258  
2259          // Create backup file and save it to the backup location.
2260          $bc = new backup_controller(backup::TYPE_1COURSE, $course->id, backup::FORMAT_MOODLE,
2261              backup::INTERACTIVE_NO, backup::MODE_GENERAL, $userid);
2262          $bc->execute_plan();
2263          $results = $bc->get_results();
2264          $file = $results['backup_destination'];
2265          $fp = get_file_packer('application/vnd.moodle.backup');
2266          $filepath = $CFG->dataroot . '/temp/backup/test-restore-course-event';
2267          $file->extract_to_pathname($fp, $filepath);
2268          $bc->destroy();
2269  
2270          // Now we want to catch the restore course event.
2271          $sink = $this->redirectEvents();
2272  
2273          // Now restore the course to trigger the event.
2274          $rc = new restore_controller('test-restore-course-event', $course->id, backup::INTERACTIVE_NO,
2275              backup::MODE_GENERAL, $userid, backup::TARGET_NEW_COURSE);
2276          $rc->execute_precheck();
2277          $rc->execute_plan();
2278  
2279          // Capture the event.
2280          $events = $sink->get_events();
2281          $sink->close();
2282  
2283          // Validate the event.
2284          $event = array_pop($events);
2285          $this->assertInstanceOf('\core\event\course_restored', $event);
2286          $this->assertEquals('course', $event->objecttable);
2287          $this->assertEquals($rc->get_courseid(), $event->objectid);
2288          $this->assertEquals(context_course::instance($rc->get_courseid())->id, $event->contextid);
2289          $this->assertEquals('course_restored', $event->get_legacy_eventname());
2290          $legacydata = (object) array(
2291              'courseid' => $rc->get_courseid(),
2292              'userid' => $rc->get_userid(),
2293              'type' => $rc->get_type(),
2294              'target' => $rc->get_target(),
2295              'mode' => $rc->get_mode(),
2296              'operation' => $rc->get_operation(),
2297              'samesite' => $rc->is_samesite()
2298          );
2299          $url = new moodle_url('/course/view.php', array('id' => $event->objectid));
2300          $this->assertEquals($url, $event->get_url());
2301          $this->assertEventLegacyData($legacydata, $event);
2302          $this->assertEventContextNotUsed($event);
2303  
2304          // Destroy the resource controller since we are done using it.
2305          $rc->destroy();
2306      }
2307  
2308      /**
2309       * Test that triggering a course_section_updated event works as expected.
2310       */
2311      public function test_course_section_updated_event() {
2312          global $DB;
2313  
2314          $this->resetAfterTest();
2315  
2316          // Create the course with sections.
2317          $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
2318          $sections = $DB->get_records('course_sections', array('course' => $course->id));
2319  
2320          $coursecontext = context_course::instance($course->id);
2321  
2322          $section = array_pop($sections);
2323          $section->name = 'Test section';
2324          $section->summary = 'Test section summary';
2325          $DB->update_record('course_sections', $section);
2326  
2327          // Trigger an event for course section update.
2328          $event = \core\event\course_section_updated::create(
2329                  array(
2330                      'objectid' => $section->id,
2331                      'courseid' => $course->id,
2332                      'context' => context_course::instance($course->id),
2333                      'other' => array(
2334                          'sectionnum' => $section->section
2335                      )
2336                  )
2337              );
2338          $event->add_record_snapshot('course_sections', $section);
2339          // Trigger and catch event.
2340          $sink = $this->redirectEvents();
2341          $event->trigger();
2342          $events = $sink->get_events();
2343          $sink->close();
2344  
2345          // Validate the event.
2346          $event = $events[0];
2347          $this->assertInstanceOf('\core\event\course_section_updated', $event);
2348          $this->assertEquals('course_sections', $event->objecttable);
2349          $this->assertEquals($section->id, $event->objectid);
2350          $this->assertEquals($course->id, $event->courseid);
2351          $this->assertEquals($coursecontext->id, $event->contextid);
2352          $this->assertEquals($section->section, $event->other['sectionnum']);
2353          $expecteddesc = "The user with id '{$event->userid}' updated section number '{$event->other['sectionnum']}' for the course with id '{$event->courseid}'";
2354          $this->assertEquals($expecteddesc, $event->get_description());
2355          $url = new moodle_url('/course/editsection.php', array('id' => $event->objectid));
2356          $this->assertEquals($url, $event->get_url());
2357          $this->assertEquals($section, $event->get_record_snapshot('course_sections', $event->objectid));
2358          $id = $section->id;
2359          $sectionnum = $section->section;
2360          $expectedlegacydata = array($course->id, "course", "editsection", 'editsection.php?id=' . $id, $sectionnum);
2361          $this->assertEventLegacyLogData($expectedlegacydata, $event);
2362          $this->assertEventContextNotUsed($event);
2363      }
2364  
2365      /**
2366       * Test that triggering a course_section_deleted event works as expected.
2367       */
2368      public function test_course_section_deleted_event() {
2369          global $USER, $DB;
2370          $this->resetAfterTest();
2371          $sink = $this->redirectEvents();
2372  
2373          // Create the course with sections.
2374          $course = $this->getDataGenerator()->create_course(array('numsections' => 10), array('createsections' => true));
2375          $sections = $DB->get_records('course_sections', array('course' => $course->id), 'section');
2376          $coursecontext = context_course::instance($course->id);
2377          $section = array_pop($sections);
2378          course_delete_section($course, $section);
2379          $events = $sink->get_events();
2380          $event = array_pop($events); // Delete section event.
2381          $sink->close();
2382  
2383          // Validate event data.
2384          $this->assertInstanceOf('\core\event\course_section_deleted', $event);
2385          $this->assertEquals('course_sections', $event->objecttable);
2386          $this->assertEquals($section->id, $event->objectid);
2387          $this->assertEquals($course->id, $event->courseid);
2388          $this->assertEquals($coursecontext->id, $event->contextid);
2389          $this->assertEquals($section->section, $event->other['sectionnum']);
2390          $expecteddesc = "The user with id '{$event->userid}' deleted section number '{$event->other['sectionnum']}' " .
2391                  "(section name '{$event->other['sectionname']}') for the course with id '{$event->courseid}'";
2392          $this->assertEquals($expecteddesc, $event->get_description());
2393          $this->assertEquals($section, $event->get_record_snapshot('course_sections', $event->objectid));
2394          $this->assertNull($event->get_url());
2395  
2396          // Test legacy data.
2397          $sectionnum = $section->section;
2398          $expectedlegacydata = array($course->id, "course", "delete section", 'view.php?id=' . $course->id, $sectionnum);
2399          $this->assertEventLegacyLogData($expectedlegacydata, $event);
2400          $this->assertEventContextNotUsed($event);
2401      }
2402  
2403      public function test_course_integrity_check() {
2404          global $DB;
2405  
2406          $this->resetAfterTest(true);
2407          $course = $this->getDataGenerator()->create_course(array('numsections' => 1),
2408             array('createsections'=>true));
2409  
2410          $forum = $this->getDataGenerator()->create_module('forum', array('course' => $course->id),
2411                  array('section' => 0));
2412          $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id),
2413                  array('section' => 0));
2414          $quiz = $this->getDataGenerator()->create_module('quiz', array('course' => $course->id),
2415                  array('section' => 0));
2416          $correctseq = join(',', array($forum->cmid, $page->cmid, $quiz->cmid));
2417  
2418          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2419          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2420          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2421          $this->assertEquals($correctseq, $section0->sequence);
2422          $this->assertEmpty($section1->sequence);
2423          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2424          $this->assertEquals($section0->id, $cms[$page->cmid]->section);
2425          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2426          $this->assertEmpty(course_integrity_check($course->id));
2427  
2428          // Now let's make manual change in DB and let course_integrity_check() fix it:
2429  
2430          // 1. Module appears twice in one section.
2431          $DB->update_record('course_sections', array('id' => $section0->id, 'sequence' => $section0->sequence. ','. $page->cmid));
2432          $this->assertEquals(
2433                  array('Failed integrity check for course ['. $course->id.
2434                  ']. Sequence for course section ['. $section0->id. '] is "'.
2435                  $section0->sequence. ','. $page->cmid. '", must be "'.
2436                  $section0->sequence. '"'),
2437                  course_integrity_check($course->id));
2438          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2439          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2440          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2441          $this->assertEquals($correctseq, $section0->sequence);
2442          $this->assertEmpty($section1->sequence);
2443          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2444          $this->assertEquals($section0->id, $cms[$page->cmid]->section);
2445          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2446  
2447          // 2. Module appears in two sections (last section wins).
2448          $DB->update_record('course_sections', array('id' => $section1->id, 'sequence' => ''. $page->cmid));
2449          // First message about double mentioning in sequence, second message about wrong section field for $page.
2450          $this->assertEquals(array(
2451              'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2452              '] must be removed from sequence of section ['. $section0->id.
2453              '] because it is also present in sequence of section ['. $section1->id. ']',
2454              'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2455              '] points to section ['. $section0->id. '] instead of ['. $section1->id. ']'),
2456                  course_integrity_check($course->id));
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->assertEquals(''. $page->cmid, $section1->sequence);
2462          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2463          $this->assertEquals($section1->id, $cms[$page->cmid]->section);
2464          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2465  
2466          // 3. Module id is not present in course_section.sequence (integrity check with $fullcheck = false).
2467          $DB->update_record('course_sections', array('id' => $section1->id, 'sequence' => ''));
2468          $this->assertEmpty(course_integrity_check($course->id)); // Not an error!
2469          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2470          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2471          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2472          $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2473          $this->assertEmpty($section1->sequence);
2474          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2475          $this->assertEquals($section1->id, $cms[$page->cmid]->section); // Not changed.
2476          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2477  
2478          // 4. Module id is not present in course_section.sequence (integrity check with $fullcheck = true).
2479          $this->assertEquals(array('Failed integrity check for course ['. $course->id. ']. Course module ['.
2480                  $page->cmid. '] is missing from sequence of section ['. $section1->id. ']'),
2481                  course_integrity_check($course->id, null, null, true)); // Error!
2482          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2483          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2484          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2485          $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2486          $this->assertEquals(''. $page->cmid, $section1->sequence);  // Yay, module added to section.
2487          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2488          $this->assertEquals($section1->id, $cms[$page->cmid]->section); // Not changed.
2489          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2490  
2491          // 5. Module id is not present in course_section.sequence and it's section is invalid (integrity check with $fullcheck = true).
2492          $DB->update_record('course_modules', array('id' => $page->cmid, 'section' => 8765));
2493          $DB->update_record('course_sections', array('id' => $section1->id, 'sequence' => ''));
2494          $this->assertEquals(array(
2495              'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2496              '] is missing from sequence of section ['. $section0->id. ']',
2497              'Failed integrity check for course ['. $course->id. ']. Course module ['. $page->cmid.
2498              '] points to section [8765] instead of ['. $section0->id. ']'),
2499                  course_integrity_check($course->id, null, null, true));
2500          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2501          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2502          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2503          $this->assertEquals($forum->cmid. ','. $quiz->cmid. ','. $page->cmid, $section0->sequence); // Module added to section.
2504          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2505          $this->assertEquals($section0->id, $cms[$page->cmid]->section); // Section changed to section0.
2506          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2507  
2508          // 6. Module is deleted from course_modules but not deleted in sequence (integrity check with $fullcheck = true).
2509          $DB->delete_records('course_modules', array('id' => $page->cmid));
2510          $this->assertEquals(array('Failed integrity check for course ['. $course->id. ']. Course module ['.
2511                  $page->cmid. '] does not exist but is present in the sequence of section ['. $section0->id. ']'),
2512                  course_integrity_check($course->id, null, null, true));
2513          $section0 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 0));
2514          $section1 = $DB->get_record('course_sections', array('course' => $course->id, 'section' => 1));
2515          $cms = $DB->get_records('course_modules', array('course' => $course->id), 'id', 'id,section');
2516          $this->assertEquals($forum->cmid. ','. $quiz->cmid, $section0->sequence);
2517          $this->assertEmpty($section1->sequence);
2518          $this->assertEquals($section0->id, $cms[$forum->cmid]->section);
2519          $this->assertEquals($section0->id, $cms[$quiz->cmid]->section);
2520          $this->assertEquals(2, count($cms));
2521      }
2522  
2523      /**
2524       * Tests for event related to course module creation.
2525       */
2526      public function test_course_module_created_event() {
2527          global $USER;
2528  
2529          $this->resetAfterTest();
2530          $this->setAdminUser();
2531  
2532          // Create an assign module.
2533          $sink = $this->redirectEvents();
2534          $course = $this->getDataGenerator()->create_course();
2535          $module = $this->getDataGenerator()->create_module('assign', ['course' => $course]);
2536          $events = $sink->get_events();
2537          $eventscount = 0;
2538  
2539          // Validate event data.
2540          foreach ($events as $event) {
2541              if ($event instanceof \core\event\course_module_created) {
2542                  $eventscount++;
2543  
2544                  $this->assertEquals($module->cmid, $event->objectid);
2545                  $this->assertEquals($USER->id, $event->userid);
2546                  $this->assertEquals('course_modules', $event->objecttable);
2547                  $url = new moodle_url('/mod/assign/view.php', array('id' => $module->cmid));
2548                  $this->assertEquals($url, $event->get_url());
2549  
2550                  // Test legacy data.
2551                  $this->assertSame('mod_created', $event->get_legacy_eventname());
2552                  $eventdata = new stdClass();
2553                  $eventdata->modulename = 'assign';
2554                  $eventdata->name       = $module->name;
2555                  $eventdata->cmid       = $module->cmid;
2556                  $eventdata->courseid   = $module->course;
2557                  $eventdata->userid     = $USER->id;
2558                  $this->assertEventLegacyData($eventdata, $event);
2559  
2560                  $arr = array(
2561                      array($module->course, "course", "add mod", "../mod/assign/view.php?id=$module->cmid", "assign $module->id"),
2562                      array($module->course, "assign", "add", "view.php?id=$module->cmid", $module->id, $module->cmid)
2563                  );
2564                  $this->assertEventLegacyLogData($arr, $event);
2565                  $this->assertEventContextNotUsed($event);
2566              }
2567          }
2568          // Only one \core\event\course_module_created event should be triggered.
2569          $this->assertEquals(1, $eventscount);
2570  
2571          // Let us see if duplicating an activity results in a nice course module created event.
2572          $sink->clear();
2573          $course = get_course($module->course);
2574          $cm = get_coursemodule_from_id('assign', $module->cmid, 0, false, MUST_EXIST);
2575          $newcm = duplicate_module($course, $cm);
2576          $events = $sink->get_events();
2577          $eventscount = 0;
2578          $sink->close();
2579  
2580          foreach ($events as $event) {
2581              if ($event instanceof \core\event\course_module_created) {
2582                  $eventscount++;
2583                  // Validate event data.
2584                  $this->assertInstanceOf('\core\event\course_module_created', $event);
2585                  $this->assertEquals($newcm->id, $event->objectid);
2586                  $this->assertEquals($USER->id, $event->userid);
2587                  $this->assertEquals($course->id, $event->courseid);
2588                  $url = new moodle_url('/mod/assign/view.php', array('id' => $newcm->id));
2589                  $this->assertEquals($url, $event->get_url());
2590              }
2591          }
2592  
2593          // Only one \core\event\course_module_created event should be triggered.
2594          $this->assertEquals(1, $eventscount);
2595      }
2596  
2597      /**
2598       * Tests for event validations related to course module creation.
2599       */
2600      public function test_course_module_created_event_exceptions() {
2601  
2602          $this->resetAfterTest();
2603  
2604          // Generate data.
2605          $modinfo = $this->create_specific_module_test('assign');
2606          $context = context_module::instance($modinfo->coursemodule);
2607  
2608          // Test not setting instanceid.
2609          try {
2610              $event = \core\event\course_module_created::create(array(
2611                  'courseid' => $modinfo->course,
2612                  'context'  => $context,
2613                  'objectid' => $modinfo->coursemodule,
2614                  'other'    => array(
2615                      'modulename' => 'assign',
2616                      'name'       => 'My assignment',
2617                  )
2618              ));
2619              $this->fail("Event validation should not allow \\core\\event\\course_module_created to be triggered without
2620                      other['instanceid']");
2621          } catch (coding_exception $e) {
2622              $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage());
2623          }
2624  
2625          // Test not setting modulename.
2626          try {
2627              $event = \core\event\course_module_created::create(array(
2628                  'courseid' => $modinfo->course,
2629                  'context'  => $context,
2630                  'objectid' => $modinfo->coursemodule,
2631                  'other'    => array(
2632                      'instanceid' => $modinfo->instance,
2633                      'name'       => 'My assignment',
2634                  )
2635              ));
2636              $this->fail("Event validation should not allow \\core\\event\\course_module_created to be triggered without
2637                      other['modulename']");
2638          } catch (coding_exception $e) {
2639              $this->assertStringContainsString("The 'modulename' value must be set in other.", $e->getMessage());
2640          }
2641  
2642          // Test not setting name.
2643  
2644          try {
2645              $event = \core\event\course_module_created::create(array(
2646                  'courseid' => $modinfo->course,
2647                  'context'  => $context,
2648                  'objectid' => $modinfo->coursemodule,
2649                  'other'    => array(
2650                      'modulename' => 'assign',
2651                      'instanceid' => $modinfo->instance,
2652                  )
2653              ));
2654              $this->fail("Event validation should not allow \\core\\event\\course_module_created to be triggered without
2655                      other['name']");
2656          } catch (coding_exception $e) {
2657              $this->assertStringContainsString("The 'name' value must be set in other.", $e->getMessage());
2658          }
2659  
2660      }
2661  
2662      /**
2663       * Tests for event related to course module updates.
2664       */
2665      public function test_course_module_updated_event() {
2666          global $USER, $DB;
2667          $this->resetAfterTest();
2668  
2669          // Update a forum module.
2670          $sink = $this->redirectEvents();
2671          $modinfo = $this->update_specific_module_test('forum');
2672          $events = $sink->get_events();
2673          $eventscount = 0;
2674          $sink->close();
2675  
2676          $cm = $DB->get_record('course_modules', array('id' => $modinfo->coursemodule), '*', MUST_EXIST);
2677          $mod = $DB->get_record('forum', array('id' => $cm->instance), '*', MUST_EXIST);
2678  
2679          // Validate event data.
2680          foreach ($events as $event) {
2681              if ($event instanceof \core\event\course_module_updated) {
2682                  $eventscount++;
2683  
2684                  $this->assertEquals($cm->id, $event->objectid);
2685                  $this->assertEquals($USER->id, $event->userid);
2686                  $this->assertEquals('course_modules', $event->objecttable);
2687                  $url = new moodle_url('/mod/forum/view.php', array('id' => $cm->id));
2688                  $this->assertEquals($url, $event->get_url());
2689  
2690                  // Test legacy data.
2691                  $this->assertSame('mod_updated', $event->get_legacy_eventname());
2692                  $eventdata = new stdClass();
2693                  $eventdata->modulename = 'forum';
2694                  $eventdata->name       = $mod->name;
2695                  $eventdata->cmid       = $cm->id;
2696                  $eventdata->courseid   = $cm->course;
2697                  $eventdata->userid     = $USER->id;
2698                  $this->assertEventLegacyData($eventdata, $event);
2699  
2700                  $arr = array(
2701                      array($cm->course, "course", "update mod", "../mod/forum/view.php?id=$cm->id", "forum $cm->instance"),
2702                      array($cm->course, "forum", "update", "view.php?id=$cm->id", $cm->instance, $cm->id)
2703                  );
2704                  $this->assertEventLegacyLogData($arr, $event);
2705                  $this->assertEventContextNotUsed($event);
2706              }
2707          }
2708  
2709          // Only one \core\event\course_module_updated event should be triggered.
2710          $this->assertEquals(1, $eventscount);
2711      }
2712  
2713      /**
2714       * Tests for create_from_cm method.
2715       */
2716      public function test_course_module_create_from_cm() {
2717          $this->resetAfterTest();
2718          $this->setAdminUser();
2719  
2720          // Create course and modules.
2721          $course = $this->getDataGenerator()->create_course(array('numsections' => 5));
2722  
2723          // Generate an assignment.
2724          $assign = $this->getDataGenerator()->create_module('assign', array('course' => $course->id));
2725  
2726          // Get the module context.
2727          $modcontext = context_module::instance($assign->cmid);
2728  
2729          // Get course module.
2730          $cm = get_coursemodule_from_id(null, $assign->cmid, $course->id, false, MUST_EXIST);
2731  
2732          // Create an event from course module.
2733          $event = \core\event\course_module_updated::create_from_cm($cm, $modcontext);
2734  
2735          // Trigger the events.
2736          $sink = $this->redirectEvents();
2737          $event->trigger();
2738          $events = $sink->get_events();
2739          $event2 = array_pop($events);
2740  
2741          // Test event data.
2742          $this->assertInstanceOf('\core\event\course_module_updated', $event);
2743          $this->assertEquals($cm->id, $event2->objectid);
2744          $this->assertEquals($modcontext, $event2->get_context());
2745          $this->assertEquals($cm->modname, $event2->other['modulename']);
2746          $this->assertEquals($cm->instance, $event2->other['instanceid']);
2747          $this->assertEquals($cm->name, $event2->other['name']);
2748          $this->assertEventContextNotUsed($event2);
2749          $this->assertSame('mod_updated', $event2->get_legacy_eventname());
2750          $arr = array(
2751              array($cm->course, "course", "update mod", "../mod/assign/view.php?id=$cm->id", "assign $cm->instance"),
2752              array($cm->course, "assign", "update", "view.php?id=$cm->id", $cm->instance, $cm->id)
2753          );
2754          $this->assertEventLegacyLogData($arr, $event);
2755      }
2756  
2757      /**
2758       * Tests for event validations related to course module update.
2759       */
2760      public function test_course_module_updated_event_exceptions() {
2761  
2762          $this->resetAfterTest();
2763  
2764          // Generate data.
2765          $modinfo = $this->create_specific_module_test('assign');
2766          $context = context_module::instance($modinfo->coursemodule);
2767  
2768          // Test not setting instanceid.
2769          try {
2770              $event = \core\event\course_module_updated::create(array(
2771                  'courseid' => $modinfo->course,
2772                  'context'  => $context,
2773                  'objectid' => $modinfo->coursemodule,
2774                  'other'    => array(
2775                      'modulename' => 'assign',
2776                      'name'       => 'My assignment',
2777                  )
2778              ));
2779              $this->fail("Event validation should not allow \\core\\event\\course_module_updated to be triggered without
2780                      other['instanceid']");
2781          } catch (coding_exception $e) {
2782              $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage());
2783          }
2784  
2785          // Test not setting modulename.
2786          try {
2787              $event = \core\event\course_module_updated::create(array(
2788                  'courseid' => $modinfo->course,
2789                  'context'  => $context,
2790                  'objectid' => $modinfo->coursemodule,
2791                  'other'    => array(
2792                      'instanceid' => $modinfo->instance,
2793                      'name'       => 'My assignment',
2794                  )
2795              ));
2796              $this->fail("Event validation should not allow \\core\\event\\course_module_updated to be triggered without
2797                      other['modulename']");
2798          } catch (coding_exception $e) {
2799              $this->assertStringContainsString("The 'modulename' value must be set in other.", $e->getMessage());
2800          }
2801  
2802          // Test not setting name.
2803  
2804          try {
2805              $event = \core\event\course_module_updated::create(array(
2806                  'courseid' => $modinfo->course,
2807                  'context'  => $context,
2808                  'objectid' => $modinfo->coursemodule,
2809                  'other'    => array(
2810                      'modulename' => 'assign',
2811                      'instanceid' => $modinfo->instance,
2812                  )
2813              ));
2814              $this->fail("Event validation should not allow \\core\\event\\course_module_updated to be triggered without
2815                      other['name']");
2816          } catch (coding_exception $e) {
2817              $this->assertStringContainsString("The 'name' value must be set in other.", $e->getMessage());
2818          }
2819  
2820      }
2821  
2822      /**
2823       * Tests for event related to course module delete.
2824       */
2825      public function test_course_module_deleted_event() {
2826          global $USER, $DB;
2827          $this->resetAfterTest();
2828  
2829          // Create and delete a module.
2830          $sink = $this->redirectEvents();
2831          $modinfo = $this->create_specific_module_test('forum');
2832          $cm = $DB->get_record('course_modules', array('id' => $modinfo->coursemodule), '*', MUST_EXIST);
2833          course_delete_module($modinfo->coursemodule);
2834          $events = $sink->get_events();
2835          $event = array_pop($events); // delete module event.;
2836          $sink->close();
2837  
2838          // Validate event data.
2839          $this->assertInstanceOf('\core\event\course_module_deleted', $event);
2840          $this->assertEquals($cm->id, $event->objectid);
2841          $this->assertEquals($USER->id, $event->userid);
2842          $this->assertEquals('course_modules', $event->objecttable);
2843          $this->assertEquals(null, $event->get_url());
2844          $this->assertEquals($cm, $event->get_record_snapshot('course_modules', $cm->id));
2845  
2846          // Test legacy data.
2847          $this->assertSame('mod_deleted', $event->get_legacy_eventname());
2848          $eventdata = new stdClass();
2849          $eventdata->modulename = 'forum';
2850          $eventdata->cmid       = $cm->id;
2851          $eventdata->courseid   = $cm->course;
2852          $eventdata->userid     = $USER->id;
2853          $this->assertEventLegacyData($eventdata, $event);
2854  
2855          $arr = array($cm->course, 'course', "delete mod", "view.php?id=$cm->course", "forum $cm->instance", $cm->id);
2856          $this->assertEventLegacyLogData($arr, $event);
2857  
2858      }
2859  
2860      /**
2861       * Tests for event validations related to course module deletion.
2862       */
2863      public function test_course_module_deleted_event_exceptions() {
2864  
2865          $this->resetAfterTest();
2866  
2867          // Generate data.
2868          $modinfo = $this->create_specific_module_test('assign');
2869          $context = context_module::instance($modinfo->coursemodule);
2870  
2871          // Test not setting instanceid.
2872          try {
2873              $event = \core\event\course_module_deleted::create(array(
2874                  'courseid' => $modinfo->course,
2875                  'context'  => $context,
2876                  'objectid' => $modinfo->coursemodule,
2877                  'other'    => array(
2878                      'modulename' => 'assign',
2879                      'name'       => 'My assignment',
2880                  )
2881              ));
2882              $this->fail("Event validation should not allow \\core\\event\\course_module_deleted to be triggered without
2883                      other['instanceid']");
2884          } catch (coding_exception $e) {
2885              $this->assertStringContainsString("The 'instanceid' value must be set in other.", $e->getMessage());
2886          }
2887  
2888          // Test not setting modulename.
2889          try {
2890              $event = \core\event\course_module_deleted::create(array(
2891                  'courseid' => $modinfo->course,
2892                  'context'  => $context,
2893                  'objectid' => $modinfo->coursemodule,
2894                  'other'    => array(
2895                      'instanceid' => $modinfo->instance,
2896                      'name'       => 'My assignment',
2897                  )
2898              ));
2899              $this->fail("Event validation should not allow \\core\\event\\course_module_deleted to be triggered without
2900                      other['modulename']");
2901          } catch (coding_exception $e) {
2902              $this->assertStringContainsString("The 'modulename' value must be set in other.", $e->getMessage());
2903          }
2904      }
2905  
2906      /**
2907       * Returns a user object and its assigned new role.
2908       *
2909       * @param testing_data_generator $generator
2910       * @param $contextid
2911       * @return array The user object and the role ID
2912       */
2913      protected function get_user_objects(testing_data_generator $generator, $contextid) {
2914          global $USER;
2915  
2916          if (empty($USER->id)) {
2917              $user  = $generator->create_user();
2918              $this->setUser($user);
2919          }
2920          $roleid = create_role('Test role', 'testrole', 'Test role description');
2921          if (!is_array($contextid)) {
2922              $contextid = array($contextid);
2923          }
2924          foreach ($contextid as $cid) {
2925              $assignid = role_assign($roleid, $user->id, $cid);
2926          }
2927          return array($user, $roleid);
2928      }
2929  
2930      /**
2931       * Test course move after course.
2932       */
2933      public function test_course_change_sortorder_after_course() {
2934          global $DB;
2935  
2936          $this->resetAfterTest(true);
2937  
2938          $generator = $this->getDataGenerator();
2939          $category = $generator->create_category();
2940          $course3 = $generator->create_course(array('category' => $category->id));
2941          $course2 = $generator->create_course(array('category' => $category->id));
2942          $course1 = $generator->create_course(array('category' => $category->id));
2943          $context = $category->get_context();
2944  
2945          list($user, $roleid) = $this->get_user_objects($generator, $context->id);
2946          $caps = course_capability_assignment::allow('moodle/category:manage', $roleid, $context->id);
2947  
2948          $courses = $category->get_courses();
2949          $this->assertIsArray($courses);
2950          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2951          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2952          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2953  
2954          // Test moving down.
2955          $this->assertTrue(course_change_sortorder_after_course($course1->id, $course3->id));
2956          $courses = $category->get_courses();
2957          $this->assertIsArray($courses);
2958          $this->assertEquals(array($course2->id, $course3->id, $course1->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 up.
2963          $this->assertTrue(course_change_sortorder_after_course($course1->id, $course2->id));
2964          $courses = $category->get_courses();
2965          $this->assertIsArray($courses);
2966          $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
2967          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2968          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2969  
2970          // Test moving to the top.
2971          $this->assertTrue(course_change_sortorder_after_course($course1->id, 0));
2972          $courses = $category->get_courses();
2973          $this->assertIsArray($courses);
2974          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
2975          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
2976          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
2977      }
2978  
2979      /**
2980       * Tests changing the visibility of a course.
2981       */
2982      public function test_course_change_visibility() {
2983          global $DB;
2984  
2985          $this->resetAfterTest(true);
2986  
2987          $generator = $this->getDataGenerator();
2988          $category = $generator->create_category();
2989          $course = $generator->create_course(array('category' => $category->id));
2990  
2991          $this->assertEquals('1', $course->visible);
2992          $this->assertEquals('1', $course->visibleold);
2993  
2994          $this->assertTrue(course_change_visibility($course->id, false));
2995          $course = $DB->get_record('course', array('id' => $course->id));
2996          $this->assertEquals('0', $course->visible);
2997          $this->assertEquals('0', $course->visibleold);
2998  
2999          $this->assertTrue(course_change_visibility($course->id, true));
3000          $course = $DB->get_record('course', array('id' => $course->id));
3001          $this->assertEquals('1', $course->visible);
3002          $this->assertEquals('1', $course->visibleold);
3003      }
3004  
3005      /**
3006       * Tests moving the course up and down by one.
3007       */
3008      public function test_course_change_sortorder_by_one() {
3009          global $DB;
3010  
3011          $this->resetAfterTest(true);
3012  
3013          $generator = $this->getDataGenerator();
3014          $category = $generator->create_category();
3015          $course3 = $generator->create_course(array('category' => $category->id));
3016          $course2 = $generator->create_course(array('category' => $category->id));
3017          $course1 = $generator->create_course(array('category' => $category->id));
3018  
3019          $courses = $category->get_courses();
3020          $this->assertIsArray($courses);
3021          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
3022          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
3023          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
3024  
3025          // Test moving down.
3026          $course1 = get_course($course1->id);
3027          $this->assertTrue(course_change_sortorder_by_one($course1, false));
3028          $courses = $category->get_courses();
3029          $this->assertIsArray($courses);
3030          $this->assertEquals(array($course2->id, $course1->id, $course3->id), array_keys($courses));
3031          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
3032          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
3033  
3034          // Test moving up.
3035          $course1 = get_course($course1->id);
3036          $this->assertTrue(course_change_sortorder_by_one($course1, true));
3037          $courses = $category->get_courses();
3038          $this->assertIsArray($courses);
3039          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
3040          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
3041          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
3042  
3043          // Test moving the top course up one.
3044          $course1 = get_course($course1->id);
3045          $this->assertFalse(course_change_sortorder_by_one($course1, true));
3046          // Check nothing changed.
3047          $courses = $category->get_courses();
3048          $this->assertIsArray($courses);
3049          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
3050          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
3051          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
3052  
3053          // Test moving the bottom course up down.
3054          $course3 = get_course($course3->id);
3055          $this->assertFalse(course_change_sortorder_by_one($course3, false));
3056          // Check nothing changed.
3057          $courses = $category->get_courses();
3058          $this->assertIsArray($courses);
3059          $this->assertEquals(array($course1->id, $course2->id, $course3->id), array_keys($courses));
3060          $dbcourses = $DB->get_records('course', array('category' => $category->id), 'sortorder', 'id');
3061          $this->assertEquals(array_keys($dbcourses), array_keys($courses));
3062      }
3063  
3064      public function test_view_resources_list() {
3065          $this->resetAfterTest();
3066  
3067          $course = self::getDataGenerator()->create_course();
3068          $coursecontext = context_course::instance($course->id);
3069  
3070          $event = \core\event\course_resources_list_viewed::create(array('context' => context_course::instance($course->id)));
3071          $event->set_legacy_logdata(array('book', 'page', 'resource'));
3072          $sink = $this->redirectEvents();
3073          $event->trigger();
3074          $events = $sink->get_events();
3075          $sink->close();
3076  
3077          // Validate the event.
3078          $event = $events[0];
3079          $this->assertInstanceOf('\core\event\course_resources_list_viewed', $event);
3080          $this->assertEquals(null, $event->objecttable);
3081          $this->assertEquals(null, $event->objectid);
3082          $this->assertEquals($course->id, $event->courseid);
3083          $this->assertEquals($coursecontext->id, $event->contextid);
3084          $expectedlegacydata = array(
3085              array($course->id, "book", "view all", 'index.php?id=' . $course->id, ''),
3086              array($course->id, "page", "view all", 'index.php?id=' . $course->id, ''),
3087              array($course->id, "resource", "view all", 'index.php?id=' . $course->id, ''),
3088          );
3089          $this->assertEventLegacyLogData($expectedlegacydata, $event);
3090          $this->assertEventContextNotUsed($event);
3091      }
3092  
3093      /**
3094       * Test duplicate_module()
3095       */
3096      public function test_duplicate_module() {
3097          $this->setAdminUser();
3098          $this->resetAfterTest();
3099          $course = self::getDataGenerator()->create_course();
3100          $res = self::getDataGenerator()->create_module('resource', array('course' => $course));
3101          $cm = get_coursemodule_from_id('resource', $res->cmid, 0, false, MUST_EXIST);
3102  
3103          $newcm = duplicate_module($course, $cm);
3104  
3105          // Make sure they are the same, except obvious id changes.
3106          foreach ($cm as $prop => $value) {
3107              if ($prop == 'id' || $prop == 'url' || $prop == 'instance' || $prop == 'added') {
3108                  // Ignore obviously different properties.
3109                  continue;
3110              }
3111              if ($prop == 'name') {
3112                  // We expect ' (copy)' to be added to the original name since MDL-59227.
3113                  $value = get_string('duplicatedmodule', 'moodle', $value);
3114              }
3115              $this->assertEquals($value, $newcm->$prop);
3116          }
3117      }
3118  
3119      /**
3120       * Tests that when creating or updating a module, if the availability settings
3121       * are present but set to an empty tree, availability is set to null in
3122       * database.
3123       */
3124      public function test_empty_availability_settings() {
3125          global $DB;
3126          $this->setAdminUser();
3127          $this->resetAfterTest();
3128  
3129          // Enable availability.
3130          set_config('enableavailability', 1);
3131  
3132          // Test add.
3133          $emptyavailability = json_encode(\core_availability\tree::get_root_json(array()));
3134          $course = self::getDataGenerator()->create_course();
3135          $label = self::getDataGenerator()->create_module('label', array(
3136                  'course' => $course, 'availability' => $emptyavailability));
3137          $this->assertNull($DB->get_field('course_modules', 'availability',
3138                  array('id' => $label->cmid)));
3139  
3140          // Test update.
3141          $formdata = $DB->get_record('course_modules', array('id' => $label->cmid));
3142          unset($formdata->availability);
3143          $formdata->availabilityconditionsjson = $emptyavailability;
3144          $formdata->modulename = 'label';
3145          $formdata->coursemodule = $label->cmid;
3146          $draftid = 0;
3147          file_prepare_draft_area($draftid, context_module::instance($label->cmid)->id,
3148                  'mod_label', 'intro', 0);
3149          $formdata->introeditor = array(
3150              'itemid' => $draftid,
3151              'text' => '<p>Yo</p>',
3152              'format' => FORMAT_HTML);
3153          update_module($formdata);
3154          $this->assertNull($DB->get_field('course_modules', 'availability',
3155                  array('id' => $label->cmid)));
3156      }
3157  
3158      /**
3159       * Test update_inplace_editable()
3160       */
3161      public function test_update_module_name_inplace() {
3162          global $CFG, $DB, $PAGE;
3163          require_once($CFG->dirroot . '/lib/external/externallib.php');
3164  
3165          $this->setUser($this->getDataGenerator()->create_user());
3166  
3167          $this->resetAfterTest(true);
3168          $course = $this->getDataGenerator()->create_course();
3169          $forum = self::getDataGenerator()->create_module('forum', array('course' => $course->id, 'name' => 'forum name'));
3170  
3171          // Call service for core_course component without necessary permissions.
3172          try {
3173              core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, 'New forum name');
3174              $this->fail('Exception expected');
3175          } catch (moodle_exception $e) {
3176              $this->assertEquals('Course or activity not accessible. (Not enrolled)',
3177                  $e->getMessage());
3178          }
3179  
3180          // Change to admin user and make sure that cm name can be updated using web service update_inplace_editable().
3181          $this->setAdminUser();
3182          $res = core_external::update_inplace_editable('core_course', 'activityname', $forum->cmid, 'New forum name');
3183          $res = external_api::clean_returnvalue(core_external::update_inplace_editable_returns(), $res);
3184          $this->assertEquals('New forum name', $res['value']);
3185          $this->assertEquals('New forum name', $DB->get_field('forum', 'name', array('id' => $forum->id)));
3186      }
3187  
3188      /**
3189       * Testing function course_get_tagged_course_modules - search tagged course modules
3190       */
3191      public function test_course_get_tagged_course_modules() {
3192          global $DB;
3193          $this->resetAfterTest();
3194          $course3 = $this->getDataGenerator()->create_course();
3195          $course2 = $this->getDataGenerator()->create_course();
3196          $course1 = $this->getDataGenerator()->create_course();
3197          $cm11 = $this->getDataGenerator()->create_module('assign', array('course' => $course1->id,
3198              'tags' => 'Cat, Dog'));
3199          $cm12 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3200              'tags' => 'Cat, Mouse', 'visible' => 0));
3201          $cm13 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3202              'tags' => 'Cat, Mouse, Dog'));
3203          $cm21 = $this->getDataGenerator()->create_module('forum', array('course' => $course2->id,
3204              'tags' => 'Cat, Mouse'));
3205          $cm31 = $this->getDataGenerator()->create_module('forum', array('course' => $course3->id,
3206              'tags' => 'Cat, Mouse'));
3207  
3208          // Admin is able to view everything.
3209          $this->setAdminUser();
3210          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3211                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3212          $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3213          $this->assertMatchesRegularExpression('/'.$cm12->name.'/', $res->content);
3214          $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3215          $this->assertMatchesRegularExpression('/'.$cm21->name.'/', $res->content);
3216          $this->assertMatchesRegularExpression('/'.$cm31->name.'/', $res->content);
3217          // Results from course1 are returned before results from course2.
3218          $this->assertTrue(strpos($res->content, $cm11->name) < strpos($res->content, $cm21->name));
3219  
3220          // Ordinary user is not able to see anything.
3221          $user = $this->getDataGenerator()->create_user();
3222          $this->setUser($user);
3223  
3224          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3225                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3226          $this->assertNull($res);
3227  
3228          // Enrol user as student in course1 and course2.
3229          $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
3230          $this->getDataGenerator()->enrol_user($user->id, $course1->id, $roleids['student']);
3231          $this->getDataGenerator()->enrol_user($user->id, $course2->id, $roleids['student']);
3232          core_tag_index_builder::reset_caches();
3233  
3234          // Searching in the course context returns visible modules in this course.
3235          $context = context_course::instance($course1->id);
3236          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3237                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */0);
3238          $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3239          $this->assertDoesNotMatchRegularExpression('/'.$cm12->name.'/', $res->content);
3240          $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3241          $this->assertDoesNotMatchRegularExpression('/'.$cm21->name.'/', $res->content);
3242          $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content);
3243  
3244          // Searching FROM the course context returns visible modules in all courses.
3245          $context = context_course::instance($course2->id);
3246          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3247                  /*$exclusivemode = */false, /*$fromctx = */$context->id, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3248          $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3249          $this->assertDoesNotMatchRegularExpression('/'.$cm12->name.'/', $res->content);
3250          $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3251          $this->assertMatchesRegularExpression('/'.$cm21->name.'/', $res->content);
3252          $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content); // No access to course3.
3253          // Results from course2 are returned before results from course1.
3254          $this->assertTrue(strpos($res->content, $cm21->name) < strpos($res->content, $cm11->name));
3255  
3256          // Enrol user in course1 as a teacher - now he should be able to see hidden module.
3257          $this->getDataGenerator()->enrol_user($user->id, $course1->id, $roleids['editingteacher']);
3258          get_fast_modinfo(0,0,true);
3259  
3260          $context = context_course::instance($course1->id);
3261          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3262                  /*$exclusivemode = */false, /*$fromctx = */$context->id, /*$ctx = */0, /*$rec = */1, /*$page = */0);
3263          $this->assertMatchesRegularExpression('/'.$cm12->name.'/', $res->content);
3264  
3265          // Create more modules and try pagination.
3266          $cm14 = $this->getDataGenerator()->create_module('assign', array('course' => $course1->id,
3267              'tags' => 'Cat, Dog'));
3268          $cm15 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3269              'tags' => 'Cat, Mouse', 'visible' => 0));
3270          $cm16 = $this->getDataGenerator()->create_module('page', array('course' => $course1->id,
3271              'tags' => 'Cat, Mouse, Dog'));
3272  
3273          $context = context_course::instance($course1->id);
3274          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3275                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */0);
3276          $this->assertMatchesRegularExpression('/'.$cm11->name.'/', $res->content);
3277          $this->assertMatchesRegularExpression('/'.$cm12->name.'/', $res->content);
3278          $this->assertMatchesRegularExpression('/'.$cm13->name.'/', $res->content);
3279          $this->assertDoesNotMatchRegularExpression('/'.$cm21->name.'/', $res->content);
3280          $this->assertMatchesRegularExpression('/'.$cm14->name.'/', $res->content);
3281          $this->assertMatchesRegularExpression('/'.$cm15->name.'/', $res->content);
3282          $this->assertDoesNotMatchRegularExpression('/'.$cm16->name.'/', $res->content);
3283          $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content); // No access to course3.
3284          $this->assertEmpty($res->prevpageurl);
3285          $this->assertNotEmpty($res->nextpageurl);
3286  
3287          $res = course_get_tagged_course_modules(core_tag_tag::get_by_name(0, 'Cat'),
3288                  /*$exclusivemode = */false, /*$fromctx = */0, /*$ctx = */$context->id, /*$rec = */1, /*$page = */1);
3289          $this->assertDoesNotMatchRegularExpression('/'.$cm11->name.'/', $res->content);
3290          $this->assertDoesNotMatchRegularExpression('/'.$cm12->name.'/', $res->content);
3291          $this->assertDoesNotMatchRegularExpression('/'.$cm13->name.'/', $res->content);
3292          $this->assertDoesNotMatchRegularExpression('/'.$cm21->name.'/', $res->content);
3293          $this->assertDoesNotMatchRegularExpression('/'.$cm14->name.'/', $res->content);
3294          $this->assertDoesNotMatchRegularExpression('/'.$cm15->name.'/', $res->content);
3295          $this->assertMatchesRegularExpression('/'.$cm16->name.'/', $res->content);
3296          $this->assertDoesNotMatchRegularExpression('/'.$cm31->name.'/', $res->content); // No access to course3.
3297          $this->assertNotEmpty($res->prevpageurl);
3298          $this->assertEmpty($res->nextpageurl);
3299      }
3300  
3301      /**
3302       * Test course_get_user_navigation_options for frontpage.
3303       */
3304      public function test_course_get_user_navigation_options_for_frontpage() {
3305          global $CFG, $SITE, $DB;
3306          $this->resetAfterTest();
3307          $context = context_system::instance();
3308          $course = clone $SITE;
3309          $this->setAdminUser();
3310  
3311          $navoptions = course_get_user_navigation_options($context, $course);
3312          $this->assertTrue($navoptions->blogs);
3313          $this->assertTrue($navoptions->notes);
3314          $this->assertTrue($navoptions->participants);
3315          $this->assertTrue($navoptions->badges);
3316          $this->assertTrue($navoptions->tags);
3317          $this->assertFalse($navoptions->search);
3318          $this->assertTrue($navoptions->competencies);
3319  
3320          // Enable global search now.
3321          $CFG->enableglobalsearch = 1;
3322          $navoptions = course_get_user_navigation_options($context, $course);
3323          $this->assertTrue($navoptions->search);
3324  
3325          // Disable competencies.
3326          $oldcompetencies = get_config('core_competency', 'enabled');
3327          set_config('enabled', false, 'core_competency');
3328          $navoptions = course_get_user_navigation_options($context, $course);
3329          $this->assertFalse($navoptions->competencies);
3330          set_config('enabled', $oldcompetencies, 'core_competency');
3331  
3332          // Now try with a standard user.
3333          $user = $this->getDataGenerator()->create_user();
3334          $this->setUser($user);
3335          $navoptions = course_get_user_navigation_options($context, $course);
3336          $this->assertTrue($navoptions->blogs);
3337          $this->assertFalse($navoptions->notes);
3338          $this->assertFalse($navoptions->participants);
3339          $this->assertTrue($navoptions->badges);
3340          $this->assertTrue($navoptions->tags);
3341          $this->assertTrue($navoptions->search);
3342      }
3343  
3344      /**
3345       * Test course_get_user_navigation_options for managers in a normal course.
3346       */
3347      public function test_course_get_user_navigation_options_for_managers() {
3348          global $CFG;
3349          $this->resetAfterTest();
3350          $course = $this->getDataGenerator()->create_course();
3351          $context = context_course::instance($course->id);
3352          $this->setAdminUser();
3353  
3354          $navoptions = course_get_user_navigation_options($context);
3355          $this->assertTrue($navoptions->blogs);
3356          $this->assertTrue($navoptions->notes);
3357          $this->assertTrue($navoptions->participants);
3358          $this->assertTrue($navoptions->badges);
3359      }
3360  
3361      /**
3362       * Test course_get_user_navigation_options for students in a normal course.
3363       */
3364      public function test_course_get_user_navigation_options_for_students() {
3365          global $DB, $CFG;
3366          $this->resetAfterTest();
3367          $course = $this->getDataGenerator()->create_course();
3368          $context = context_course::instance($course->id);
3369  
3370          $user = $this->getDataGenerator()->create_user();
3371          $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
3372          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
3373  
3374          $this->setUser($user);
3375  
3376          $navoptions = course_get_user_navigation_options($context);
3377          $this->assertTrue($navoptions->blogs);
3378          $this->assertFalse($navoptions->notes);
3379          $this->assertTrue($navoptions->participants);
3380          $this->assertFalse($navoptions->badges);
3381  
3382          // Disable some options.
3383          $CFG->badges_allowcoursebadges = 0;
3384          $CFG->enableblogs = 0;
3385          // Disable view participants capability.
3386          assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $context);
3387  
3388          $navoptions = course_get_user_navigation_options($context);
3389          $this->assertFalse($navoptions->blogs);
3390          $this->assertFalse($navoptions->notes);
3391          $this->assertFalse($navoptions->participants);
3392          $this->assertFalse($navoptions->badges);
3393  
3394          // Re-enable some options to check badges are displayed as expected.
3395          $CFG->badges_allowcoursebadges = 1;
3396          assign_capability('moodle/badges:createbadge', CAP_ALLOW, $roleid, $context);
3397  
3398          $navoptions = course_get_user_navigation_options($context);
3399          $this->assertTrue($navoptions->badges);
3400      }
3401  
3402      /**
3403       * Test course_get_user_administration_options for frontpage.
3404       */
3405      public function test_course_get_user_administration_options_for_frontpage() {
3406          global $CFG, $SITE;
3407          $this->resetAfterTest();
3408          $course = clone $SITE;
3409          $context = context_course::instance($course->id);
3410          $this->setAdminUser();
3411  
3412          $adminoptions = course_get_user_administration_options($course, $context);
3413          $this->assertTrue($adminoptions->update);
3414          $this->assertTrue($adminoptions->filters);
3415          $this->assertTrue($adminoptions->reports);
3416          $this->assertTrue($adminoptions->backup);
3417          $this->assertTrue($adminoptions->restore);
3418          $this->assertFalse($adminoptions->files);
3419          $this->assertFalse($adminoptions->tags);
3420  
3421          // Now try with a standard user.
3422          $user = $this->getDataGenerator()->create_user();
3423          $this->setUser($user);
3424          $adminoptions = course_get_user_administration_options($course, $context);
3425          $this->assertFalse($adminoptions->update);
3426          $this->assertFalse($adminoptions->filters);
3427          $this->assertFalse($adminoptions->reports);
3428          $this->assertFalse($adminoptions->backup);
3429          $this->assertFalse($adminoptions->restore);
3430          $this->assertFalse($adminoptions->files);
3431          $this->assertFalse($adminoptions->tags);
3432  
3433      }
3434  
3435      /**
3436       * Test course_get_user_administration_options for managers in a normal course.
3437       */
3438      public function test_course_get_user_administration_options_for_managers() {
3439          global $CFG;
3440          $this->resetAfterTest();
3441          $course = $this->getDataGenerator()->create_course();
3442          $context = context_course::instance($course->id);
3443          $this->setAdminUser();
3444  
3445          $adminoptions = course_get_user_administration_options($course, $context);
3446          $this->assertTrue($adminoptions->update);
3447          $this->assertTrue($adminoptions->filters);
3448          $this->assertTrue($adminoptions->reports);
3449          $this->assertTrue($adminoptions->backup);
3450          $this->assertTrue($adminoptions->restore);
3451          $this->assertFalse($adminoptions->files);
3452          $this->assertTrue($adminoptions->tags);
3453          $this->assertTrue($adminoptions->gradebook);
3454          $this->assertFalse($adminoptions->outcomes);
3455          $this->assertTrue($adminoptions->badges);
3456          $this->assertTrue($adminoptions->import);
3457          $this->assertTrue($adminoptions->reset);
3458          $this->assertTrue($adminoptions->roles);
3459      }
3460  
3461      /**
3462       * Test course_get_user_administration_options for students in a normal course.
3463       */
3464      public function test_course_get_user_administration_options_for_students() {
3465          global $DB, $CFG;
3466          $this->resetAfterTest();
3467          $course = $this->getDataGenerator()->create_course();
3468          $context = context_course::instance($course->id);
3469  
3470          $user = $this->getDataGenerator()->create_user();
3471          $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'));
3472          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
3473  
3474          $this->setUser($user);
3475          $adminoptions = course_get_user_administration_options($course, $context);
3476  
3477          $this->assertFalse($adminoptions->update);
3478          $this->assertFalse($adminoptions->filters);
3479          $this->assertFalse($adminoptions->reports);
3480          $this->assertFalse($adminoptions->backup);
3481          $this->assertFalse($adminoptions->restore);
3482          $this->assertFalse($adminoptions->files);
3483          $this->assertFalse($adminoptions->tags);
3484          $this->assertFalse($adminoptions->gradebook);
3485          $this->assertFalse($adminoptions->outcomes);
3486          $this->assertTrue($adminoptions->badges);
3487          $this->assertFalse($adminoptions->import);
3488          $this->assertFalse($adminoptions->reset);
3489          $this->assertFalse($adminoptions->roles);
3490  
3491          $CFG->enablebadges = false;
3492          $adminoptions = course_get_user_administration_options($course, $context);
3493          $this->assertFalse($adminoptions->badges);
3494      }
3495  
3496      /**
3497       * Test test_update_course_frontpage_category.
3498       */
3499      public function test_update_course_frontpage_category() {
3500          // Fetch front page course.
3501          $course = get_course(SITEID);
3502          // Test update information on front page course.
3503          $course->category = 99;
3504          $this->expectException('moodle_exception');
3505          $this->expectExceptionMessage(get_string('invalidcourse', 'error'));
3506          update_course($course);
3507      }
3508  
3509      /**
3510       * test_course_enddate
3511       *
3512       * @dataProvider course_enddate_provider
3513       * @param int $startdate
3514       * @param int $enddate
3515       * @param string $errorcode
3516       */
3517      public function test_course_enddate($startdate, $enddate, $errorcode) {
3518  
3519          $this->resetAfterTest(true);
3520  
3521          $record = array('startdate' => $startdate, 'enddate' => $enddate);
3522          try {
3523              $course1 = $this->getDataGenerator()->create_course($record);
3524              if ($errorcode !== false) {
3525                  $this->fail('Expected exception with "' . $errorcode . '" error code in create_create');
3526              }
3527          } catch (moodle_exception $e) {
3528              if ($errorcode === false) {
3529                  $this->fail('Got "' . $errorcode . '" exception error code and no exception was expected');
3530              }
3531              if ($e->errorcode != $errorcode) {
3532                  $this->fail('Got "' . $e->errorcode. '" exception error code and "' . $errorcode . '" was expected');
3533              }
3534              return;
3535          }
3536  
3537          $this->assertEquals($startdate, $course1->startdate);
3538          $this->assertEquals($enddate, $course1->enddate);
3539      }
3540  
3541      /**
3542       * Provider for test_course_enddate.
3543       *
3544       * @return array
3545       */
3546      public function course_enddate_provider() {
3547          // Each provided example contains startdate, enddate and the expected exception error code if there is any.
3548          return [
3549              [
3550                  111,
3551                  222,
3552                  false
3553              ], [
3554                  222,
3555                  111,
3556                  'enddatebeforestartdate'
3557              ], [
3558                  111,
3559                  0,
3560                  false
3561              ], [
3562                  0,
3563                  222,
3564                  'nostartdatenoenddate'
3565              ]
3566          ];
3567      }
3568  
3569  
3570      /**
3571       * test_course_dates_reset
3572       *
3573       * @dataProvider course_dates_reset_provider
3574       * @param int $startdate
3575       * @param int $enddate
3576       * @param int $resetstartdate
3577       * @param int $resetenddate
3578       * @param int $resultingstartdate
3579       * @param int $resultingenddate
3580       */
3581      public function test_course_dates_reset($startdate, $enddate, $resetstartdate, $resetenddate, $resultingstartdate, $resultingenddate) {
3582          global $CFG, $DB;
3583  
3584          require_once($CFG->dirroot.'/completion/criteria/completion_criteria_date.php');
3585  
3586          $this->resetAfterTest(true);
3587  
3588          $this->setAdminUser();
3589  
3590          $CFG->enablecompletion = true;
3591  
3592          $this->setTimezone('UTC');
3593  
3594          $record = array('startdate' => $startdate, 'enddate' => $enddate, 'enablecompletion' => 1);
3595          $originalcourse = $this->getDataGenerator()->create_course($record);
3596          $coursecriteria = new completion_criteria_date(array('course' => $originalcourse->id, 'timeend' => $startdate + DAYSECS));
3597          $coursecriteria->insert();
3598  
3599          $activitycompletiondate = $startdate + DAYSECS;
3600          $data = $this->getDataGenerator()->create_module('data', array('course' => $originalcourse->id),
3601                          array('completion' => 1, 'completionexpected' => $activitycompletiondate));
3602  
3603          $resetdata = new stdClass();
3604          $resetdata->id = $originalcourse->id;
3605          $resetdata->reset_start_date_old = $originalcourse->startdate;
3606          $resetdata->reset_start_date = $resetstartdate;
3607          $resetdata->reset_end_date = $resetenddate;
3608          $resetdata->reset_end_date_old = $record['enddate'];
3609          reset_course_userdata($resetdata);
3610  
3611          $course = $DB->get_record('course', array('id' => $originalcourse->id));
3612  
3613          $this->assertEquals($resultingstartdate, $course->startdate);
3614          $this->assertEquals($resultingenddate, $course->enddate);
3615  
3616          $coursecompletioncriteria = completion_criteria_date::fetch(array('course' => $originalcourse->id));
3617          $this->assertEquals($resultingstartdate + DAYSECS, $coursecompletioncriteria->timeend);
3618  
3619          $this->assertEquals($resultingstartdate + DAYSECS, $DB->get_field('course_modules', 'completionexpected',
3620              array('id' => $data->cmid)));
3621      }
3622  
3623      /**
3624       * Provider for test_course_dates_reset.
3625       *
3626       * @return array
3627       */
3628      public function course_dates_reset_provider() {
3629  
3630          // Each example contains the following:
3631          // - course startdate
3632          // - course enddate
3633          // - startdate to reset to (false if not reset)
3634          // - enddate to reset to (false if not reset)
3635          // - resulting startdate
3636          // - resulting enddate
3637          $time = 1445644800;
3638          return [
3639              // No date changes.
3640              [
3641                  $time,
3642                  $time + DAYSECS,
3643                  false,
3644                  false,
3645                  $time,
3646                  $time + DAYSECS
3647              ],
3648              // End date changes to a valid value.
3649              [
3650                  $time,
3651                  $time + DAYSECS,
3652                  false,
3653                  $time + DAYSECS + 111,
3654                  $time,
3655                  $time + DAYSECS + 111
3656              ],
3657              // Start date changes to a valid value. End date does not get updated because it does not have value.
3658              [
3659                  $time,
3660                  0,
3661                  $time + DAYSECS,
3662                  false,
3663                  $time + DAYSECS,
3664                  0
3665              ],
3666              // Start date changes to a valid value. End date gets updated accordingly.
3667              [
3668                  $time,
3669                  $time + DAYSECS,
3670                  $time + WEEKSECS,
3671                  false,
3672                  $time + WEEKSECS,
3673                  $time + WEEKSECS + DAYSECS
3674              ],
3675              // Start date and end date change to a valid value.
3676              [
3677                  $time,
3678                  $time + DAYSECS,
3679                  $time + WEEKSECS,
3680                  $time + YEARSECS,
3681                  $time + WEEKSECS,
3682                  $time + YEARSECS
3683              ]
3684          ];
3685      }
3686  
3687      /**
3688       * Test reset_course_userdata()
3689       *    - with reset_roles_overrides enabled
3690       *    - with selective role unenrolments
3691       */
3692      public function test_course_roles_reset() {
3693          global $DB;
3694  
3695          $this->resetAfterTest(true);
3696  
3697          $generator = $this->getDataGenerator();
3698  
3699          // Create test course and user, enrol one in the other.
3700          $course = $generator->create_course();
3701          $user = $generator->create_user();
3702          $roleid = $DB->get_field('role', 'id', array('shortname' => 'student'), MUST_EXIST);
3703          $generator->enrol_user($user->id, $course->id, $roleid);
3704  
3705          // Test case with reset_roles_overrides enabled.
3706          // Override course so it does NOT allow students 'mod/forum:viewdiscussion'.
3707          $coursecontext = context_course::instance($course->id);
3708          assign_capability('mod/forum:viewdiscussion', CAP_PREVENT, $roleid, $coursecontext->id);
3709  
3710          // Check expected capabilities so far.
3711          $this->assertFalse(has_capability('mod/forum:viewdiscussion', $coursecontext, $user));
3712  
3713          // Oops, preventing student from viewing forums was a mistake, let's reset the course.
3714          $resetdata = new stdClass();
3715          $resetdata->id = $course->id;
3716          $resetdata->reset_roles_overrides = true;
3717          reset_course_userdata($resetdata);
3718  
3719          // Check new expected capabilities - override at the course level should be reset.
3720          $this->assertTrue(has_capability('mod/forum:viewdiscussion', $coursecontext, $user));
3721  
3722          // Test case with selective role unenrolments.
3723          $roles = array();
3724          $roles['student'] = $DB->get_field('role', 'id', array('shortname' => 'student'), MUST_EXIST);
3725          $roles['teacher'] = $DB->get_field('role', 'id', array('shortname' => 'teacher'), MUST_EXIST);
3726  
3727          // We enrol a user with student and teacher roles.
3728          $generator->enrol_user($user->id, $course->id, $roles['student']);
3729          $generator->enrol_user($user->id, $course->id, $roles['teacher']);
3730  
3731          // When we reset only student role, we expect to keep teacher role.
3732          $resetdata = new stdClass();
3733          $resetdata->id = $course->id;
3734          $resetdata->unenrol_users = array($roles['student']);
3735          reset_course_userdata($resetdata);
3736  
3737          $usersroles = enrol_get_course_users_roles($course->id);
3738          $this->assertArrayHasKey($user->id, $usersroles);
3739          $this->assertArrayHasKey($roles['teacher'], $usersroles[$user->id]);
3740          $this->assertArrayNotHasKey($roles['student'], $usersroles[$user->id]);
3741          $this->assertCount(1, $usersroles[$user->id]);
3742  
3743          // We reenrol user as student.
3744          $generator->enrol_user($user->id, $course->id, $roles['student']);
3745  
3746          // When we reset student and teacher roles, we expect no roles left.
3747          $resetdata = new stdClass();
3748          $resetdata->id = $course->id;
3749          $resetdata->unenrol_users = array($roles['student'], $roles['teacher']);
3750          reset_course_userdata($resetdata);
3751  
3752          $usersroles = enrol_get_course_users_roles($course->id);
3753          $this->assertEmpty($usersroles);
3754      }
3755  
3756      public function test_course_check_module_updates_since() {
3757          global $CFG, $DB, $USER;
3758          require_once($CFG->dirroot . '/mod/glossary/lib.php');
3759          require_once($CFG->dirroot . '/rating/lib.php');
3760          require_once($CFG->dirroot . '/comment/lib.php');
3761  
3762          $this->resetAfterTest(true);
3763  
3764          $CFG->enablecompletion = true;
3765          $course = $this->getDataGenerator()->create_course(array('enablecompletion' => 1));
3766          $glossary = $this->getDataGenerator()->create_module('glossary', array(
3767              'course' => $course->id,
3768              'completion' => COMPLETION_TRACKING_AUTOMATIC,
3769              'completionview' => 1,
3770              'allowcomments' => 1,
3771              'assessed' => RATING_AGGREGATE_AVERAGE,
3772              'scale' => 100
3773          ));
3774          $glossarygenerator = $this->getDataGenerator()->get_plugin_generator('mod_glossary');
3775          $context = context_module::instance($glossary->cmid);
3776          $modinfo = get_fast_modinfo($course);
3777          $cm = $modinfo->get_cm($glossary->cmid);
3778          $user = $this->getDataGenerator()->create_user();
3779          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
3780          $this->getDataGenerator()->enrol_user($user->id, $course->id, $studentrole->id);
3781          $from = time();
3782  
3783          $teacher = $this->getDataGenerator()->create_user();
3784          $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
3785          $this->getDataGenerator()->enrol_user($teacher->id, $course->id, $teacherrole->id);
3786  
3787          assign_capability('mod/glossary:viewanyrating', CAP_ALLOW, $studentrole->id, $context->id, true);
3788  
3789          // Check nothing changed right now.
3790          $updates = course_check_module_updates_since($cm, $from);
3791          $this->assertFalse($updates->configuration->updated);
3792          $this->assertFalse($updates->completion->updated);
3793          $this->assertFalse($updates->gradeitems->updated);
3794          $this->assertFalse($updates->comments->updated);
3795          $this->assertFalse($updates->ratings->updated);
3796          $this->assertFalse($updates->introfiles->updated);
3797          $this->assertFalse($updates->outcomes->updated);
3798  
3799          $this->waitForSecond();
3800  
3801          // Do some changes.
3802          $this->setUser($user);
3803          $entry = $glossarygenerator->create_content($glossary);
3804  
3805          $this->setUser($teacher);
3806          // Name.
3807          set_coursemodule_name($glossary->cmid, 'New name');
3808  
3809          // Add some ratings.
3810          $rm = new rating_manager();
3811          $result = $rm->add_rating($cm, $context, 'mod_glossary', 'entry', $entry->id, 100, 50, $user->id, RATING_AGGREGATE_AVERAGE);
3812  
3813          // Change grades.
3814          $glossary->cmidnumber = $glossary->cmid;
3815          glossary_update_grades($glossary, $user->id);
3816  
3817          $this->setUser($user);
3818          // Completion status.
3819          glossary_view($glossary, $course, $cm, $context, 'letter');
3820  
3821          // Add one comment.
3822          $args = new stdClass;
3823          $args->context   = $context;
3824          $args->course    = $course;
3825          $args->cm        = $cm;
3826          $args->area      = 'glossary_entry';
3827          $args->itemid    = $entry->id;
3828          $args->client_id = 1;
3829          $args->component = 'mod_glossary';
3830          $manager = new comment($args);
3831          $manager->add('blah blah blah');
3832  
3833          // Check upgrade status.
3834          $updates = course_check_module_updates_since($cm, $from);
3835          $this->assertTrue($updates->configuration->updated);
3836          $this->assertTrue($updates->completion->updated);
3837          $this->assertTrue($updates->gradeitems->updated);
3838          $this->assertTrue($updates->comments->updated);
3839          $this->assertTrue($updates->ratings->updated);
3840          $this->assertFalse($updates->introfiles->updated);
3841          $this->assertFalse($updates->outcomes->updated);
3842      }
3843  
3844      public function test_async_module_deletion_hook_implemented() {
3845          // Async module deletion depends on the 'true' being returned by at least one plugin implementing the hook,
3846          // 'course_module_adhoc_deletion_recommended'. In core, is implemented by the course recyclebin, which will only return
3847          // true if the recyclebin plugin is enabled. To make sure async deletion occurs, this test force-enables the recyclebin.
3848          global $DB, $USER;
3849          $this->resetAfterTest(true);
3850          $this->setAdminUser();
3851  
3852          // Ensure recyclebin is enabled.
3853          set_config('coursebinenable', true, 'tool_recyclebin');
3854  
3855          // Create course, module and context.
3856          $course = $this->getDataGenerator()->create_course(['numsections' => 5]);
3857          $module = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
3858          $modcontext = context_module::instance($module->cmid);
3859  
3860          // Verify context exists.
3861          $this->assertInstanceOf('context_module', $modcontext);
3862  
3863          // Check events generated on the course_delete_module call.
3864          $sink = $this->redirectEvents();
3865  
3866          // Try to delete the module using the async flag.
3867          course_delete_module($module->cmid, true); // Try to delete the module asynchronously.
3868  
3869          // Verify that no event has been generated yet.
3870          $events = $sink->get_events();
3871          $event = array_pop($events);
3872          $sink->close();
3873          $this->assertEmpty($event);
3874  
3875          // Grab the record, in it's final state before hard deletion, for comparison with the event snapshot.
3876          // We need to do this because the 'deletioninprogress' flag has changed from '0' to '1'.
3877          $cm = $DB->get_record('course_modules', ['id' => $module->cmid], '*', MUST_EXIST);
3878  
3879          // Verify the course_module is marked as 'deletioninprogress'.
3880          $this->assertNotEquals($cm, false);
3881          $this->assertEquals($cm->deletioninprogress, '1');
3882  
3883          // Verify the context has not yet been removed.
3884          $this->assertEquals($modcontext, context_module::instance($module->cmid, IGNORE_MISSING));
3885  
3886          // Set up a sink to catch the 'course_module_deleted' event.
3887          $sink = $this->redirectEvents();
3888  
3889          // Now, run the adhoc task which performs the hard deletion.
3890          phpunit_util::run_all_adhoc_tasks();
3891  
3892          // Fetch and validate the event data.
3893          $events = $sink->get_events();
3894          $event = array_pop($events);
3895          $sink->close();
3896          $this->assertInstanceOf('\core\event\course_module_deleted', $event);
3897          $this->assertEquals($module->cmid, $event->objectid);
3898          $this->assertEquals($USER->id, $event->userid);
3899          $this->assertEquals('course_modules', $event->objecttable);
3900          $this->assertEquals(null, $event->get_url());
3901          $this->assertEquals($cm, $event->get_record_snapshot('course_modules', $module->cmid));
3902  
3903          // Verify the context has been removed.
3904          $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
3905  
3906          // Verify the course_module record has been deleted.
3907          $cmcount = $DB->count_records('course_modules', ['id' => $module->cmid]);
3908          $this->assertEmpty($cmcount);
3909      }
3910  
3911      public function test_async_module_deletion_hook_not_implemented() {
3912          // Only proceed if we are sure that no plugin is going to advocate async removal of a module. I.e. no plugin returns
3913          // 'true' from the 'course_module_adhoc_deletion_recommended' hook.
3914          // In the case of core, only recyclebin implements this hook, and it will only return true if enabled, so disable it.
3915          global $DB, $USER;
3916          $this->resetAfterTest(true);
3917          $this->setAdminUser();
3918          set_config('coursebinenable', false, 'tool_recyclebin');
3919  
3920          // Non-core plugins might implement the 'course_module_adhoc_deletion_recommended' hook and spoil this test.
3921          // If at least one plugin still returns true, then skip this test.
3922          if ($pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
3923              foreach ($pluginsfunction as $plugintype => $plugins) {
3924                  foreach ($plugins as $pluginfunction) {
3925                      if ($pluginfunction()) {
3926                          $this->markTestSkipped();
3927                      }
3928                  }
3929              }
3930          }
3931  
3932          // Create course, module and context.
3933          $course = $this->getDataGenerator()->create_course(['numsections' => 5]);
3934          $module = $this->getDataGenerator()->create_module('assign', ['course' => $course->id]);
3935          $modcontext = context_module::instance($module->cmid);
3936          $cm = $DB->get_record('course_modules', ['id' => $module->cmid], '*', MUST_EXIST);
3937  
3938          // Verify context exists.
3939          $this->assertInstanceOf('context_module', $modcontext);
3940  
3941          // Check events generated on the course_delete_module call.
3942          $sink = $this->redirectEvents();
3943  
3944          // Try to delete the module using the async flag.
3945          course_delete_module($module->cmid, true); // Try to delete the module asynchronously.
3946  
3947          // Fetch and validate the event data.
3948          $events = $sink->get_events();
3949          $event = array_pop($events);
3950          $sink->close();
3951          $this->assertInstanceOf('\core\event\course_module_deleted', $event);
3952          $this->assertEquals($module->cmid, $event->objectid);
3953          $this->assertEquals($USER->id, $event->userid);
3954          $this->assertEquals('course_modules', $event->objecttable);
3955          $this->assertEquals(null, $event->get_url());
3956          $this->assertEquals($cm, $event->get_record_snapshot('course_modules', $module->cmid));
3957  
3958          // Verify the context has been removed.
3959          $this->assertFalse(context_module::instance($module->cmid, IGNORE_MISSING));
3960  
3961          // Verify the course_module record has been deleted.
3962          $cmcount = $DB->count_records('course_modules', ['id' => $module->cmid]);
3963          $this->assertEmpty($cmcount);
3964      }
3965  
3966      public function test_async_section_deletion_hook_implemented() {
3967          // Async section deletion (provided section contains modules), depends on the 'true' being returned by at least one plugin
3968          // implementing the 'course_module_adhoc_deletion_recommended' hook. In core, is implemented by the course recyclebin,
3969          // which will only return true if the plugin is enabled. To make sure async deletion occurs, this test enables recyclebin.
3970          global $DB, $USER;
3971          $this->resetAfterTest(true);
3972          $this->setAdminUser();
3973  
3974          // Ensure recyclebin is enabled.
3975          set_config('coursebinenable', true, 'tool_recyclebin');
3976  
3977          // Create course, module and context.
3978          $generator = $this->getDataGenerator();
3979          $course = $generator->create_course(['numsections' => 4, 'format' => 'topics'], ['createsections' => true]);
3980          $assign0 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
3981          $assign1 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
3982          $assign2 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
3983          $assign3 = $generator->create_module('assign', ['course' => $course, 'section' => 0]);
3984  
3985          // Delete empty section. No difference from normal, synchronous behaviour.
3986          $this->assertTrue(course_delete_section($course, 4, false, true));
3987          $this->assertEquals(3, course_get_format($course)->get_last_section_number());
3988  
3989          // Delete a module in section 2 (using async). Need to verify this doesn't generate two tasks when we delete
3990          // the section in the next step.
3991          course_delete_module($assign2->cmid, true);
3992  
3993          // Confirm that the module is pending deletion in its current section.
3994          $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => '2']); // For event comparison.
3995          $this->assertEquals(true, $DB->record_exists('course_modules', ['id' => $assign2->cmid, 'deletioninprogress' => 1,
3996                                                       'section' => $section->id]));
3997  
3998          // Now, delete section 2.
3999          $this->assertFalse(course_delete_section($course, 2, false, true)); // Non-empty section, no forcedelete, so no change.
4000  
4001          $sink = $this->redirectEvents(); // To capture the event.
4002          $this->assertTrue(course_delete_section($course, 2, true, true));
4003  
4004          // Now, confirm that:
4005          // a) the section's modules have been flagged for deletion and moved to section 0 and;
4006          // b) the section has been deleted and;
4007          // c) course_section_deleted event has been fired. The course_module_deleted events will only fire once they have been
4008          // removed from section 0 via the adhoc task.
4009  
4010          // Modules should have been flagged for deletion and moved to section 0.
4011          $sectionid = $DB->get_field('course_sections', 'id', ['course' => $course->id, 'section' => 0]);
4012          $this->assertEquals(3, $DB->count_records('course_modules', ['section' => $sectionid, 'deletioninprogress' => 1]));
4013  
4014          // Confirm the section has been deleted.
4015          $this->assertEquals(2, course_get_format($course)->get_last_section_number());
4016  
4017          // Check event fired.
4018          $events = $sink->get_events();
4019          $event = array_pop($events);
4020          $sink->close();
4021          $this->assertInstanceOf('\core\event\course_section_deleted', $event);
4022          $this->assertEquals($section->id, $event->objectid);
4023          $this->assertEquals($USER->id, $event->userid);
4024          $this->assertEquals('course_sections', $event->objecttable);
4025          $this->assertEquals(null, $event->get_url());
4026          $this->assertEquals($section, $event->get_record_snapshot('course_sections', $section->id));
4027  
4028          // Now, run the adhoc task to delete the modules from section 0.
4029          $sink = $this->redirectEvents(); // To capture the events.
4030          phpunit_util::run_all_adhoc_tasks();
4031  
4032          // Confirm the modules have been deleted.
4033          list($insql, $assignids) = $DB->get_in_or_equal([$assign0->cmid, $assign1->cmid, $assign2->cmid]);
4034          $cmcount = $DB->count_records_select('course_modules', 'id ' . $insql,  $assignids);
4035          $this->assertEmpty($cmcount);
4036  
4037          // Confirm other modules in section 0 still remain.
4038          $this->assertEquals(1, $DB->count_records('course_modules', ['id' => $assign3->cmid]));
4039  
4040          // Confirm that events were generated for all 3 of the modules.
4041          $events = $sink->get_events();
4042          $sink->close();
4043          $count = 0;
4044          while (!empty($events)) {
4045              $event = array_pop($events);
4046              if ($event instanceof \core\event\course_module_deleted &&
4047                  in_array($event->objectid, [$assign0->cmid, $assign1->cmid, $assign2->cmid])) {
4048                  $count++;
4049              }
4050          }
4051          $this->assertEquals(3, $count);
4052      }
4053  
4054      public function test_async_section_deletion_hook_not_implemented() {
4055          // If no plugins advocate async removal, then normal synchronous removal will take place.
4056          // Only proceed if we are sure that no plugin is going to advocate async removal of a module. I.e. no plugin returns
4057          // 'true' from the 'course_module_adhoc_deletion_recommended' hook.
4058          // In the case of core, only recyclebin implements this hook, and it will only return true if enabled, so disable it.
4059          global $DB, $USER;
4060          $this->resetAfterTest(true);
4061          $this->setAdminUser();
4062          set_config('coursebinenable', false, 'tool_recyclebin');
4063  
4064          // Non-core plugins might implement the 'course_module_adhoc_deletion_recommended' hook and spoil this test.
4065          // If at least one plugin still returns true, then skip this test.
4066          if ($pluginsfunction = get_plugins_with_function('course_module_background_deletion_recommended')) {
4067              foreach ($pluginsfunction as $plugintype => $plugins) {
4068                  foreach ($plugins as $pluginfunction) {
4069                      if ($pluginfunction()) {
4070                          $this->markTestSkipped();
4071                      }
4072                  }
4073              }
4074          }
4075  
4076          // Create course, module and context.
4077          $generator = $this->getDataGenerator();
4078          $course = $generator->create_course(['numsections' => 4, 'format' => 'topics'], ['createsections' => true]);
4079          $assign0 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
4080          $assign1 = $generator->create_module('assign', ['course' => $course, 'section' => 2]);
4081  
4082          // Delete empty section. No difference from normal, synchronous behaviour.
4083          $this->assertTrue(course_delete_section($course, 4, false, true));
4084          $this->assertEquals(3, course_get_format($course)->get_last_section_number());
4085  
4086          // Delete section in the middle (2).
4087          $section = $DB->get_record('course_sections', ['course' => $course->id, 'section' => '2']); // For event comparison.
4088          $this->assertFalse(course_delete_section($course, 2, false, true)); // Non-empty section, no forcedelete, so no change.
4089  
4090          $sink = $this->redirectEvents(); // To capture the event.
4091          $this->assertTrue(course_delete_section($course, 2, true, true));
4092  
4093          // Now, confirm that:
4094          // a) The section's modules have deleted and;
4095          // b) the section has been deleted and;
4096          // c) course_section_deleted event has been fired and;
4097          // d) course_module_deleted events have both been fired.
4098  
4099          // Confirm modules have been deleted.
4100          list($insql, $assignids) = $DB->get_in_or_equal([$assign0->cmid, $assign1->cmid]);
4101          $cmcount = $DB->count_records_select('course_modules', 'id ' . $insql, $assignids);
4102          $this->assertEmpty($cmcount);
4103  
4104          // Confirm the section has been deleted.
4105          $this->assertEquals(2, course_get_format($course)->get_last_section_number());
4106  
4107          // Confirm the course_section_deleted event has been generated.
4108          $events = $sink->get_events();
4109          $event = array_pop($events);
4110          $sink->close();
4111          $this->assertInstanceOf('\core\event\course_section_deleted', $event);
4112          $this->assertEquals($section->id, $event->objectid);
4113          $this->assertEquals($USER->id, $event->userid);
4114          $this->assertEquals('course_sections', $event->objecttable);
4115          $this->assertEquals(null, $event->get_url());
4116          $this->assertEquals($section, $event->get_record_snapshot('course_sections', $section->id));
4117  
4118          // Confirm that the course_module_deleted events have both been generated.
4119          $count = 0;
4120          while (!empty($events)) {
4121              $event = array_pop($events);
4122              if ($event instanceof \core\event\course_module_deleted &&
4123                  in_array($event->objectid, [$assign0->cmid, $assign1->cmid])) {
4124                  $count++;
4125              }
4126          }
4127          $this->assertEquals(2, $count);
4128      }
4129  
4130      public function test_classify_course_for_timeline() {
4131          global $DB, $CFG;
4132  
4133          require_once($CFG->dirroot.'/completion/criteria/completion_criteria_self.php');
4134  
4135          set_config('enablecompletion', COMPLETION_ENABLED);
4136          set_config('coursegraceperiodbefore', 0);
4137          set_config('coursegraceperiodafter', 0);
4138  
4139          $this->resetAfterTest(true);
4140          $this->setAdminUser();
4141  
4142          // Create courses for testing.
4143          $generator = $this->getDataGenerator();
4144          $future = time() + 3600;
4145          $past = time() - 3600;
4146          $futurecourse = $generator->create_course(['startdate' => $future]);
4147          $pastcourse = $generator->create_course(['startdate' => $past - 60, 'enddate' => $past]);
4148          $completedcourse = $generator->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4149          $inprogresscourse = $generator->create_course();
4150  
4151          // Set completion rules.
4152          $criteriadata = new stdClass();
4153          $criteriadata->id = $completedcourse->id;
4154  
4155          // Self completion.
4156          $criteriadata->criteria_self = COMPLETION_CRITERIA_TYPE_SELF;
4157          $class = 'completion_criteria_self';
4158          $criterion = new $class();
4159          $criterion->update_config($criteriadata);
4160  
4161          $user = $this->getDataGenerator()->create_user();
4162          $studentrole = $DB->get_record('role', array('shortname' => 'student'));
4163          $this->getDataGenerator()->enrol_user($user->id, $futurecourse->id, $studentrole->id);
4164          $this->getDataGenerator()->enrol_user($user->id, $pastcourse->id, $studentrole->id);
4165          $this->getDataGenerator()->enrol_user($user->id, $completedcourse->id, $studentrole->id);
4166          $this->getDataGenerator()->enrol_user($user->id, $inprogresscourse->id, $studentrole->id);
4167  
4168          $this->setUser($user);
4169          core_completion_external::mark_course_self_completed($completedcourse->id);
4170          $ccompletion = new completion_completion(array('course' => $completedcourse->id, 'userid' => $user->id));
4171          $ccompletion->mark_complete();
4172  
4173          // Aggregate the completions.
4174          $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($pastcourse));
4175          $this->assertEquals(COURSE_TIMELINE_FUTURE, course_classify_for_timeline($futurecourse));
4176          $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
4177          $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
4178  
4179          // Test grace period.
4180          set_config('coursegraceperiodafter', 1);
4181          set_config('coursegraceperiodbefore', 1);
4182          $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($pastcourse));
4183          $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($futurecourse));
4184          $this->assertEquals(COURSE_TIMELINE_PAST, course_classify_for_timeline($completedcourse));
4185          $this->assertEquals(COURSE_TIMELINE_INPROGRESS, course_classify_for_timeline($inprogresscourse));
4186      }
4187  
4188      /**
4189       * Test the main function for updating all calendar events for a module.
4190       */
4191      public function test_course_module_calendar_event_update_process() {
4192          global $DB;
4193  
4194          $this->resetAfterTest();
4195          $this->setAdminUser();
4196  
4197          $completionexpected = time();
4198          $duedate = time();
4199  
4200          $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4201          $assign = $this->getDataGenerator()->create_module('assign', [
4202                      'course' => $course,
4203                      'completionexpected' => $completionexpected,
4204                      'duedate' => $duedate
4205                  ]);
4206  
4207          $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
4208          $events = $DB->get_records('event', ['courseid' => $course->id, 'instance' => $assign->id]);
4209          // Check that both events are using the expected dates.
4210          foreach ($events as $event) {
4211              if ($event->eventtype == \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) {
4212                  $this->assertEquals($completionexpected, $event->timestart);
4213              }
4214              if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
4215                  $this->assertEquals($duedate, $event->timestart);
4216              }
4217          }
4218  
4219          // We have to manually update the module and the course module.
4220          $newcompletionexpected = time() + DAYSECS * 60;
4221          $newduedate = time() + DAYSECS * 45;
4222          $newmodulename = 'Assign - new name';
4223  
4224          $moduleobject = (object)array('id' => $assign->id, 'duedate' => $newduedate, 'name' => $newmodulename);
4225          $DB->update_record('assign', $moduleobject);
4226          $cmobject = (object)array('id' => $cm->id, 'completionexpected' => $newcompletionexpected);
4227          $DB->update_record('course_modules', $cmobject);
4228  
4229          $assign = $DB->get_record('assign', ['id' => $assign->id]);
4230          $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
4231  
4232          course_module_calendar_event_update_process($assign, $cm);
4233  
4234          $events = $DB->get_records('event', ['courseid' => $course->id, 'instance' => $assign->id]);
4235          // Now check that the details have been updated properly from the function.
4236          foreach ($events as $event) {
4237              if ($event->eventtype == \core_completion\api::COMPLETION_EVENT_TYPE_DATE_COMPLETION_EXPECTED) {
4238                  $this->assertEquals($newcompletionexpected, $event->timestart);
4239                  $this->assertEquals(get_string('completionexpectedfor', 'completion', (object)['instancename' => $newmodulename]),
4240                          $event->name);
4241              }
4242              if ($event->eventtype == ASSIGN_EVENT_TYPE_DUE) {
4243                  $this->assertEquals($newduedate, $event->timestart);
4244                  $this->assertEquals(get_string('calendardue', 'assign', $newmodulename), $event->name);
4245              }
4246          }
4247      }
4248  
4249      /**
4250       * Test the higher level checks for updating calendar events for an instance.
4251       */
4252      public function test_course_module_update_calendar_events() {
4253          $this->resetAfterTest();
4254          $this->setAdminUser();
4255  
4256          $completionexpected = time();
4257          $duedate = time();
4258  
4259          $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4260          $assign = $this->getDataGenerator()->create_module('assign', [
4261                      'course' => $course,
4262                      'completionexpected' => $completionexpected,
4263                      'duedate' => $duedate
4264                  ]);
4265  
4266          $cm = get_coursemodule_from_instance('assign', $assign->id, $course->id);
4267  
4268          // Both the instance and cm objects are missing.
4269          $this->assertFalse(course_module_update_calendar_events('assign'));
4270          // Just using the assign instance.
4271          $this->assertTrue(course_module_update_calendar_events('assign', $assign));
4272          // Just using the course module object.
4273          $this->assertTrue(course_module_update_calendar_events('assign', null, $cm));
4274          // Using both the assign instance and the course module object.
4275          $this->assertTrue(course_module_update_calendar_events('assign', $assign, $cm));
4276      }
4277  
4278      /**
4279       * Test the higher level checks for updating calendar events for a module.
4280       */
4281      public function test_course_module_bulk_update_calendar_events() {
4282          $this->resetAfterTest();
4283          $this->setAdminUser();
4284  
4285          $completionexpected = time();
4286          $duedate = time();
4287  
4288          $course = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4289          $course2 = $this->getDataGenerator()->create_course(['enablecompletion' => COMPLETION_ENABLED]);
4290          $assign = $this->getDataGenerator()->create_module('assign', [
4291                      'course' => $course,
4292                      'completionexpected' => $completionexpected,
4293                      'duedate' => $duedate
4294                  ]);
4295  
4296          // No assign instances in this course.
4297          $this->assertFalse(course_module_bulk_update_calendar_events('assign', $course2->id));
4298          // No book instances for the site.
4299          $this->assertFalse(course_module_bulk_update_calendar_events('book'));
4300          // Update all assign instances.
4301          $this->assertTrue(course_module_bulk_update_calendar_events('assign'));
4302          // Update the assign instances for this course.
4303          $this->assertTrue(course_module_bulk_update_calendar_events('assign', $course->id));
4304      }
4305  
4306      /**
4307       * Test that a student can view participants in a course they are enrolled in.
4308       */
4309      public function test_course_can_view_participants_as_student() {
4310          $this->resetAfterTest();
4311  
4312          $course = $this->getDataGenerator()->create_course();
4313          $coursecontext = context_course::instance($course->id);
4314  
4315          $user = $this->getDataGenerator()->create_user();
4316          $this->getDataGenerator()->enrol_user($user->id, $course->id);
4317  
4318          $this->setUser($user);
4319  
4320          $this->assertTrue(course_can_view_participants($coursecontext));
4321      }
4322  
4323      /**
4324       * Test that a student in a course can not view participants on the site.
4325       */
4326      public function test_course_can_view_participants_as_student_on_site() {
4327          $this->resetAfterTest();
4328  
4329          $course = $this->getDataGenerator()->create_course();
4330  
4331          $user = $this->getDataGenerator()->create_user();
4332          $this->getDataGenerator()->enrol_user($user->id, $course->id);
4333  
4334          $this->setUser($user);
4335  
4336          $this->assertFalse(course_can_view_participants(context_system::instance()));
4337      }
4338  
4339      /**
4340       * Test that an admin can view participants on the site.
4341       */
4342      public function test_course_can_view_participants_as_admin_on_site() {
4343          $this->resetAfterTest();
4344  
4345          $this->setAdminUser();
4346  
4347          $this->assertTrue(course_can_view_participants(context_system::instance()));
4348      }
4349  
4350      /**
4351       * Test teachers can view participants in a course they are enrolled in.
4352       */
4353      public function test_course_can_view_participants_as_teacher() {
4354          global $DB;
4355  
4356          $this->resetAfterTest();
4357  
4358          $course = $this->getDataGenerator()->create_course();
4359          $coursecontext = context_course::instance($course->id);
4360  
4361          $user = $this->getDataGenerator()->create_user();
4362          $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4363          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4364  
4365          $this->setUser($user);
4366  
4367          $this->assertTrue(course_can_view_participants($coursecontext));
4368      }
4369  
4370      /**
4371       * Check the teacher can still view the participants page without the 'viewparticipants' cap.
4372       */
4373      public function test_course_can_view_participants_as_teacher_without_view_participants_cap() {
4374          global $DB;
4375  
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          $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4383          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4384  
4385          $this->setUser($user);
4386  
4387          // Disable one of the capabilties.
4388          assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
4389  
4390          // Should still be able to view the page as they have the 'moodle/course:enrolreview' cap.
4391          $this->assertTrue(course_can_view_participants($coursecontext));
4392      }
4393  
4394      /**
4395       * Check the teacher can still view the participants page without the 'moodle/course:enrolreview' cap.
4396       */
4397      public function test_course_can_view_participants_as_teacher_without_enrol_review_cap() {
4398          global $DB;
4399  
4400          $this->resetAfterTest();
4401  
4402          $course = $this->getDataGenerator()->create_course();
4403          $coursecontext = context_course::instance($course->id);
4404  
4405          $user = $this->getDataGenerator()->create_user();
4406          $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4407          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4408  
4409          $this->setUser($user);
4410  
4411          // Disable one of the capabilties.
4412          assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
4413  
4414          // Should still be able to view the page as they have the 'moodle/course:viewparticipants' cap.
4415          $this->assertTrue(course_can_view_participants($coursecontext));
4416      }
4417  
4418      /**
4419       * Check the teacher can not view the participants page without the required caps.
4420       */
4421      public function test_course_can_view_participants_as_teacher_without_required_caps() {
4422          global $DB;
4423  
4424          $this->resetAfterTest();
4425  
4426          $course = $this->getDataGenerator()->create_course();
4427          $coursecontext = context_course::instance($course->id);
4428  
4429          $user = $this->getDataGenerator()->create_user();
4430          $roleid = $DB->get_field('role', 'id', array('shortname' => 'editingteacher'));
4431          $this->getDataGenerator()->enrol_user($user->id, $course->id, $roleid);
4432  
4433          $this->setUser($user);
4434  
4435          // Disable the capabilities.
4436          assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $roleid, $coursecontext);
4437          assign_capability('moodle/course:enrolreview', CAP_PROHIBIT, $roleid, $coursecontext);
4438  
4439          $this->assertFalse(course_can_view_participants($coursecontext));
4440      }
4441  
4442      /**
4443       * Check that an exception is not thrown if we can view the participants page.
4444       */
4445      public function test_course_require_view_participants() {
4446          $this->resetAfterTest();
4447  
4448          $course = $this->getDataGenerator()->create_course();
4449          $coursecontext = context_course::instance($course->id);
4450  
4451          $user = $this->getDataGenerator()->create_user();
4452          $this->getDataGenerator()->enrol_user($user->id, $course->id);
4453  
4454          $this->setUser($user);
4455  
4456          course_require_view_participants($coursecontext);
4457      }
4458  
4459      /**
4460       * Check that an exception is thrown if we can't view the participants page.
4461       */
4462      public function test_course_require_view_participants_as_student_on_site() {
4463          $this->resetAfterTest();
4464  
4465          $course = $this->getDataGenerator()->create_course();
4466  
4467          $user = $this->getDataGenerator()->create_user();
4468          $this->getDataGenerator()->enrol_user($user->id, $course->id);
4469  
4470          $this->setUser($user);
4471  
4472          $this->expectException('required_capability_exception');
4473          course_require_view_participants(context_system::instance());
4474      }
4475  
4476      /**
4477       *  Testing the can_download_from_backup_filearea fn.
4478       */
4479      public function test_can_download_from_backup_filearea() {
4480          global $DB;
4481          $this->resetAfterTest();
4482          $course = $this->getDataGenerator()->create_course();
4483          $context = context_course::instance($course->id);
4484          $user = $this->getDataGenerator()->create_user();
4485          $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
4486          $this->getDataGenerator()->enrol_user($user->id, $course->id, $teacherrole->id);
4487  
4488          // The 'automated' backup area. Downloading from this area requires two capabilities.
4489          // If the user has only the 'backup:downloadfile' capability.
4490          unassign_capability('moodle/restore:userinfo', $teacherrole->id, $context);
4491          assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4492          $this->assertFalse(can_download_from_backup_filearea('automated', $context, $user));
4493  
4494          // If the user has only the 'restore:userinfo' capability.
4495          unassign_capability('moodle/backup:downloadfile', $teacherrole->id, $context);
4496          assign_capability('moodle/restore:userinfo', CAP_ALLOW, $teacherrole->id, $context);
4497          $this->assertFalse(can_download_from_backup_filearea('automated', $context, $user));
4498  
4499          // If the user has both capabilities.
4500          assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4501          assign_capability('moodle/restore:userinfo', CAP_ALLOW, $teacherrole->id, $context);
4502          $this->assertTrue(can_download_from_backup_filearea('automated', $context, $user));
4503  
4504          // Is the user has neither of the capabilities.
4505          unassign_capability('moodle/backup:downloadfile', $teacherrole->id, $context);
4506          unassign_capability('moodle/restore:userinfo', $teacherrole->id, $context);
4507          $this->assertFalse(can_download_from_backup_filearea('automated', $context, $user));
4508  
4509          // The 'course ' and 'backup' backup file areas. These are governed by the same download capability.
4510          // User has the capability.
4511          unassign_capability('moodle/restore:userinfo', $teacherrole->id, $context);
4512          assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4513          $this->assertTrue(can_download_from_backup_filearea('course', $context, $user));
4514          $this->assertTrue(can_download_from_backup_filearea('backup', $context, $user));
4515  
4516          // User doesn't have the capability.
4517          unassign_capability('moodle/backup:downloadfile', $teacherrole->id, $context);
4518          $this->assertFalse(can_download_from_backup_filearea('course', $context, $user));
4519          $this->assertFalse(can_download_from_backup_filearea('backup', $context, $user));
4520  
4521          // A file area that doesn't exist. No permissions, regardless of capabilities.
4522          assign_capability('moodle/backup:downloadfile', CAP_ALLOW, $teacherrole->id, $context);
4523          $this->assertFalse(can_download_from_backup_filearea('testing', $context, $user));
4524      }
4525  
4526      /**
4527       * Test cases for the course_classify_courses_for_timeline test.
4528       */
4529      public function get_course_classify_courses_for_timeline_test_cases() {
4530          $now = time();
4531          $day = 86400;
4532  
4533          return [
4534              'no courses' => [
4535                  'coursesdata' => [],
4536                  'expected' => [
4537                      COURSE_TIMELINE_PAST => [],
4538                      COURSE_TIMELINE_FUTURE => [],
4539                      COURSE_TIMELINE_INPROGRESS => []
4540                  ]
4541              ],
4542              'only past' => [
4543                  'coursesdata' => [
4544                      [
4545                          'shortname' => 'past1',
4546                          'startdate' => $now - ($day * 2),
4547                          'enddate' => $now - $day
4548                      ],
4549                      [
4550                          'shortname' => 'past2',
4551                          'startdate' => $now - ($day * 2),
4552                          'enddate' => $now - $day
4553                      ]
4554                  ],
4555                  'expected' => [
4556                      COURSE_TIMELINE_PAST => ['past1', 'past2'],
4557                      COURSE_TIMELINE_FUTURE => [],
4558                      COURSE_TIMELINE_INPROGRESS => []
4559                  ]
4560              ],
4561              'only in progress' => [
4562                  'coursesdata' => [
4563                      [
4564                          'shortname' => 'inprogress1',
4565                          'startdate' => $now - $day,
4566                          'enddate' => $now + $day
4567                      ],
4568                      [
4569                          'shortname' => 'inprogress2',
4570                          'startdate' => $now - $day,
4571                          'enddate' => $now + $day
4572                      ]
4573                  ],
4574                  'expected' => [
4575                      COURSE_TIMELINE_PAST => [],
4576                      COURSE_TIMELINE_FUTURE => [],
4577                      COURSE_TIMELINE_INPROGRESS => ['inprogress1', 'inprogress2']
4578                  ]
4579              ],
4580              'only future' => [
4581                  'coursesdata' => [
4582                      [
4583                          'shortname' => 'future1',
4584                          'startdate' => $now + $day
4585                      ],
4586                      [
4587                          'shortname' => 'future2',
4588                          'startdate' => $now + $day
4589                      ]
4590                  ],
4591                  'expected' => [
4592                      COURSE_TIMELINE_PAST => [],
4593                      COURSE_TIMELINE_FUTURE => ['future1', 'future2'],
4594                      COURSE_TIMELINE_INPROGRESS => []
4595                  ]
4596              ],
4597              'combination' => [
4598                  'coursesdata' => [
4599                      [
4600                          'shortname' => 'past1',
4601                          'startdate' => $now - ($day * 2),
4602                          'enddate' => $now - $day
4603                      ],
4604                      [
4605                          'shortname' => 'past2',
4606                          'startdate' => $now - ($day * 2),
4607                          'enddate' => $now - $day
4608                      ],
4609                      [
4610                          'shortname' => 'inprogress1',
4611                          'startdate' => $now - $day,
4612                          'enddate' => $now + $day
4613                      ],
4614                      [
4615                          'shortname' => 'inprogress2',
4616                          'startdate' => $now - $day,
4617                          'enddate' => $now + $day
4618                      ],
4619                      [
4620                          'shortname' => 'future1',
4621                          'startdate' => $now + $day
4622                      ],
4623                      [
4624                          'shortname' => 'future2',
4625                          'startdate' => $now + $day
4626                      ]
4627                  ],
4628                  'expected' => [
4629                      COURSE_TIMELINE_PAST => ['past1', 'past2'],
4630                      COURSE_TIMELINE_FUTURE => ['future1', 'future2'],
4631                      COURSE_TIMELINE_INPROGRESS => ['inprogress1', 'inprogress2']
4632                  ]
4633              ],
4634          ];
4635      }
4636  
4637      /**
4638       * Test the course_classify_courses_for_timeline function.
4639       *
4640       * @dataProvider get_course_classify_courses_for_timeline_test_cases()
4641       * @param array $coursesdata Courses to create
4642       * @param array $expected Expected test results.
4643       */
4644      public function test_course_classify_courses_for_timeline($coursesdata, $expected) {
4645          $this->resetAfterTest();
4646          $generator = $this->getDataGenerator();
4647  
4648          $courses = array_map(function($coursedata) use ($generator) {
4649              return $generator->create_course($coursedata);
4650          }, $coursesdata);
4651  
4652          sort($expected[COURSE_TIMELINE_PAST]);
4653          sort($expected[COURSE_TIMELINE_FUTURE]);
4654          sort($expected[COURSE_TIMELINE_INPROGRESS]);
4655  
4656          $results = course_classify_courses_for_timeline($courses);
4657  
4658          $actualpast = array_map(function($result) {
4659              return $result->shortname;
4660          }, $results[COURSE_TIMELINE_PAST]);
4661  
4662          $actualfuture = array_map(function($result) {
4663              return $result->shortname;
4664          }, $results[COURSE_TIMELINE_FUTURE]);
4665  
4666          $actualinprogress = array_map(function($result) {
4667              return $result->shortname;
4668          }, $results[COURSE_TIMELINE_INPROGRESS]);
4669  
4670          sort($actualpast);
4671          sort($actualfuture);
4672          sort($actualinprogress);
4673  
4674          $this->assertEquals($expected[COURSE_TIMELINE_PAST], $actualpast);
4675          $this->assertEquals($expected[COURSE_TIMELINE_FUTURE], $actualfuture);
4676          $this->assertEquals($expected[COURSE_TIMELINE_INPROGRESS], $actualinprogress);
4677      }
4678  
4679      /**
4680       * Test cases for the course_get_enrolled_courses_for_logged_in_user tests.
4681       */
4682      public function get_course_get_enrolled_courses_for_logged_in_user_test_cases() {
4683          $buildexpectedresult = function($limit, $offset) {
4684              $result = [];
4685              for ($i = $offset; $i < $offset + $limit; $i++) {
4686                  $result[] = "testcourse{$i}";
4687              }
4688              return $result;
4689          };
4690  
4691          return [
4692              'zero records' => [
4693                  'dbquerylimit' => 3,
4694                  'totalcourses' => 0,
4695                  'limit' => 0,
4696                  'offset' => 0,
4697                  'expecteddbqueries' => 4,
4698                  'expectedresult' => $buildexpectedresult(0, 0)
4699              ],
4700              'less than query limit' => [
4701                  'dbquerylimit' => 3,
4702                  'totalcourses' => 2,
4703                  'limit' => 0,
4704                  'offset' => 0,
4705                  'expecteddbqueries' => 2,
4706                  'expectedresult' => $buildexpectedresult(2, 0)
4707              ],
4708              'more than query limit' => [
4709                  'dbquerylimit' => 3,
4710                  'totalcourses' => 7,
4711                  'limit' => 0,
4712                  'offset' => 0,
4713                  'expecteddbqueries' => 4,
4714                  'expectedresult' => $buildexpectedresult(7, 0)
4715              ],
4716              'limit less than query limit' => [
4717                  'dbquerylimit' => 3,
4718                  'totalcourses' => 7,
4719                  'limit' => 2,
4720                  'offset' => 0,
4721                  'expecteddbqueries' => 2,
4722                  'expectedresult' => $buildexpectedresult(2, 0)
4723              ],
4724              'limit less than query limit with offset' => [
4725                  'dbquerylimit' => 3,
4726                  'totalcourses' => 7,
4727                  'limit' => 2,
4728                  'offset' => 2,
4729                  'expecteddbqueries' => 2,
4730                  'expectedresult' => $buildexpectedresult(2, 2)
4731              ],
4732              'limit less than total' => [
4733                  'dbquerylimit' => 3,
4734                  'totalcourses' => 9,
4735                  'limit' => 6,
4736                  'offset' => 0,
4737                  'expecteddbqueries' => 3,
4738                  'expectedresult' => $buildexpectedresult(6, 0)
4739              ],
4740              'less results than limit' => [
4741                  'dbquerylimit' => 4,
4742                  'totalcourses' => 9,
4743                  'limit' => 20,
4744                  'offset' => 0,
4745                  'expecteddbqueries' => 4,
4746                  'expectedresult' => $buildexpectedresult(9, 0)
4747              ],
4748              'less results than limit exact divisible' => [
4749                  'dbquerylimit' => 3,
4750                  'totalcourses' => 9,
4751                  'limit' => 20,
4752                  'offset' => 0,
4753                  'expecteddbqueries' => 5,
4754                  'expectedresult' => $buildexpectedresult(9, 0)
4755              ],
4756              'less results than limit with offset' => [
4757                  'dbquerylimit' => 3,
4758                  'totalcourses' => 9,
4759                  'limit' => 10,
4760                  'offset' => 5,
4761                  'expecteddbqueries' => 3,
4762                  'expectedresult' => $buildexpectedresult(4, 5)
4763              ],
4764          ];
4765      }
4766  
4767      /**
4768       * Test the course_get_enrolled_courses_for_logged_in_user function.
4769       *
4770       * @dataProvider get_course_get_enrolled_courses_for_logged_in_user_test_cases()
4771       * @param int $dbquerylimit Number of records to load per DB request
4772       * @param int $totalcourses Number of courses to create
4773       * @param int $limit Maximum number of results to get.
4774       * @param int $offset Skip this number of results from the start of the result set.
4775       * @param int $expecteddbqueries The number of DB queries expected during the test.
4776       * @param array $expectedresult Expected test results.
4777       */
4778      public function test_course_get_enrolled_courses_for_logged_in_user(
4779          $dbquerylimit,
4780          $totalcourses,
4781          $limit,
4782          $offset,
4783          $expecteddbqueries,
4784          $expectedresult
4785      ) {
4786          global $DB;
4787  
4788          $this->resetAfterTest();
4789          $generator = $this->getDataGenerator();
4790          $student = $generator->create_user();
4791  
4792          for ($i = 0; $i < $totalcourses; $i++) {
4793              $shortname = "testcourse{$i}";
4794              $course = $generator->create_course(['shortname' => $shortname]);
4795              $generator->enrol_user($student->id, $course->id, 'student');
4796          }
4797  
4798          $this->setUser($student);
4799  
4800          $initialquerycount = $DB->perf_get_queries();
4801          $courses = course_get_enrolled_courses_for_logged_in_user($limit, $offset, 'shortname ASC', 'shortname', $dbquerylimit);
4802  
4803          // Loop over the result set to force the lazy loading to kick in so that we can check the
4804          // number of DB queries.
4805          $actualresult = array_map(function($course) {
4806              return $course->shortname;
4807          }, iterator_to_array($courses, false));
4808  
4809          sort($expectedresult);
4810  
4811          $this->assertEquals($expectedresult, $actualresult);
4812          $this->assertLessThanOrEqual($expecteddbqueries, $DB->perf_get_queries() - $initialquerycount);
4813      }
4814  
4815      /**
4816       * Test cases for the course_filter_courses_by_timeline_classification tests.
4817       */
4818      public function get_course_filter_courses_by_timeline_classification_test_cases() {
4819          $now = time();
4820          $day = 86400;
4821  
4822          $coursedata = [
4823              [
4824                  'shortname' => 'apast',
4825                  'startdate' => $now - ($day * 2),
4826                  'enddate' => $now - $day
4827              ],
4828              [
4829                  'shortname' => 'bpast',
4830                  'startdate' => $now - ($day * 2),
4831                  'enddate' => $now - $day
4832              ],
4833              [
4834                  'shortname' => 'cpast',
4835                  'startdate' => $now - ($day * 2),
4836                  'enddate' => $now - $day
4837              ],
4838              [
4839                  'shortname' => 'dpast',
4840                  'startdate' => $now - ($day * 2),
4841                  'enddate' => $now - $day
4842              ],
4843              [
4844                  'shortname' => 'epast',
4845                  'startdate' => $now - ($day * 2),
4846                  'enddate' => $now - $day
4847              ],
4848              [
4849                  'shortname' => 'ainprogress',
4850                  'startdate' => $now - $day,
4851                  'enddate' => $now + $day
4852              ],
4853              [
4854                  'shortname' => 'binprogress',
4855                  'startdate' => $now - $day,
4856                  'enddate' => $now + $day
4857              ],
4858              [
4859                  'shortname' => 'cinprogress',
4860                  'startdate' => $now - $day,
4861                  'enddate' => $now + $day
4862              ],
4863              [
4864                  'shortname' => 'dinprogress',
4865                  'startdate' => $now - $day,
4866                  'enddate' => $now + $day
4867              ],
4868              [
4869                  'shortname' => 'einprogress',
4870                  'startdate' => $now - $day,
4871                  'enddate' => $now + $day
4872              ],
4873              [
4874                  'shortname' => 'afuture',
4875                  'startdate' => $now + $day
4876              ],
4877              [
4878                  'shortname' => 'bfuture',
4879                  'startdate' => $now + $day
4880              ],
4881              [
4882                  'shortname' => 'cfuture',
4883                  'startdate' => $now + $day
4884              ],
4885              [
4886                  'shortname' => 'dfuture',
4887                  'startdate' => $now + $day
4888              ],
4889              [
4890                  'shortname' => 'efuture',
4891                  'startdate' => $now + $day
4892              ]
4893          ];
4894  
4895          // Raw enrolled courses result set should be returned in this order:
4896          // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
4897          // dfuture, dinprogress, dpast, efuture, einprogress, epast
4898          //
4899          // By classification the offset values for each record should be:
4900          // COURSE_TIMELINE_FUTURE
4901          // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
4902          // COURSE_TIMELINE_INPROGRESS
4903          // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
4904          // COURSE_TIMELINE_PAST
4905          // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
4906          return [
4907              'empty set' => [
4908                  'coursedata' => [],
4909                  'classification' => COURSE_TIMELINE_FUTURE,
4910                  'limit' => 2,
4911                  'offset' => 0,
4912                  'expectedcourses' => [],
4913                  'expectedprocessedcount' => 0
4914              ],
4915              // COURSE_TIMELINE_FUTURE.
4916              'future not limit no offset' => [
4917                  'coursedata' => $coursedata,
4918                  'classification' => COURSE_TIMELINE_FUTURE,
4919                  'limit' => 0,
4920                  'offset' => 0,
4921                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
4922                  'expectedprocessedcount' => 15
4923              ],
4924              'future no offset' => [
4925                  'coursedata' => $coursedata,
4926                  'classification' => COURSE_TIMELINE_FUTURE,
4927                  'limit' => 2,
4928                  'offset' => 0,
4929                  'expectedcourses' => ['afuture', 'bfuture'],
4930                  'expectedprocessedcount' => 4
4931              ],
4932              'future offset' => [
4933                  'coursedata' => $coursedata,
4934                  'classification' => COURSE_TIMELINE_FUTURE,
4935                  'limit' => 2,
4936                  'offset' => 2,
4937                  'expectedcourses' => ['bfuture', 'cfuture'],
4938                  'expectedprocessedcount' => 5
4939              ],
4940              'future exact limit' => [
4941                  'coursedata' => $coursedata,
4942                  'classification' => COURSE_TIMELINE_FUTURE,
4943                  'limit' => 5,
4944                  'offset' => 0,
4945                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
4946                  'expectedprocessedcount' => 13
4947              ],
4948              'future limit less results' => [
4949                  'coursedata' => $coursedata,
4950                  'classification' => COURSE_TIMELINE_FUTURE,
4951                  'limit' => 10,
4952                  'offset' => 0,
4953                  'expectedcourses' => ['afuture', 'bfuture', 'cfuture', 'dfuture', 'efuture'],
4954                  'expectedprocessedcount' => 15
4955              ],
4956              'future limit less results with offset' => [
4957                  'coursedata' => $coursedata,
4958                  'classification' => COURSE_TIMELINE_FUTURE,
4959                  'limit' => 10,
4960                  'offset' => 5,
4961                  'expectedcourses' => ['cfuture', 'dfuture', 'efuture'],
4962                  'expectedprocessedcount' => 10
4963              ],
4964          ];
4965      }
4966  
4967      /**
4968       * Test the course_get_enrolled_courses_for_logged_in_user_from_search function.
4969       */
4970      public function test_course_get_enrolled_courses_for_logged_in_user_from_search() {
4971          global $DB;
4972  
4973          // Set up.
4974  
4975          $this->resetAfterTest();
4976          $generator = $this->getDataGenerator();
4977          $student = $generator->create_user();
4978  
4979          $cat1 = core_course_category::create(['name' => 'Cat1']);
4980          $cat2 = core_course_category::create(['name' => 'Cat2', 'parent' => $cat1->id]);
4981          $c1 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Test 3', 'summary' => 'Magic', 'idnumber' => 'ID3']);
4982          $c2 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Test 1', 'summary' => 'Magic']);
4983          $c3 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Математика', 'summary' => ' Test Magic']);
4984          $c4 = $this->getDataGenerator()->create_course(['category' => $cat1->id, 'fullname' => 'Test 4', 'summary' => 'Magic', 'idnumber' => 'ID4']);
4985  
4986          $c5 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Test 5', 'summary' => 'Magic']);
4987          $c6 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Дискретная Математика', 'summary' => 'Magic']);
4988          $c7 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Test 7', 'summary' => 'Magic']);
4989          $c8 = $this->getDataGenerator()->create_course(['category' => $cat2->id, 'fullname' => 'Test 8', 'summary' => 'Magic']);
4990  
4991          for ($i = 1; $i < 9; $i++) {
4992              $generator->enrol_user($student->id, ${"c$i"}->id, 'student');
4993          }
4994  
4995          $this->setUser($student);
4996  
4997          $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
4998              0,
4999              0,
5000              'id ASC',
5001              null,
5002              COURSE_DB_QUERY_LIMIT,
5003              ['search' => 'test'],
5004              ['idonly' => true]
5005          );
5006  
5007          $actualresult = array_map(function($course) {
5008              return $course->id;
5009          }, iterator_to_array($returnedcourses, false));
5010  
5011          $this->assertEquals([$c1->id, $c2->id, $c3->id, $c4->id, $c5->id, $c7->id, $c8->id], $actualresult);
5012  
5013          // Test no courses matching the search.
5014          $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
5015              0,
5016              0,
5017              'id ASC',
5018              null,
5019              COURSE_DB_QUERY_LIMIT,
5020              ['search' => 'foobar'],
5021              ['idonly' => true]
5022          );
5023  
5024          $actualresult = array_map(function($course) {
5025              return $course->id;
5026          }, iterator_to_array($returnedcourses, false));
5027  
5028          $this->assertEquals([], $actualresult);
5029  
5030          // Test returning all courses that have a mutual summary.
5031          $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
5032              0,
5033              0,
5034              'id ASC',
5035              null,
5036              COURSE_DB_QUERY_LIMIT,
5037              ['search' => 'Magic'],
5038              ['idonly' => true]
5039          );
5040  
5041          $actualresult = array_map(function($course) {
5042              return $course->id;
5043          }, iterator_to_array($returnedcourses, false));
5044  
5045          $this->assertEquals([$c1->id, $c2->id, $c3->id, $c4->id, $c5->id, $c6->id, $c7->id, $c8->id], $actualresult);
5046  
5047          // Test returning a unique course.
5048          $returnedcourses = course_get_enrolled_courses_for_logged_in_user_from_search(
5049              0,
5050              0,
5051              'id ASC',
5052              null,
5053              COURSE_DB_QUERY_LIMIT,
5054              ['search' => 'Дискретная'],
5055              ['idonly' => true]
5056          );
5057  
5058          $actualresult = array_map(function($course) {
5059              return $course->id;
5060          }, iterator_to_array($returnedcourses, false));
5061  
5062          $this->assertEquals([$c6->id], $actualresult);
5063      }
5064  
5065      /**
5066       * Test the course_filter_courses_by_timeline_classification function.
5067       *
5068       * @dataProvider get_course_filter_courses_by_timeline_classification_test_cases()
5069       * @param array $coursedata Course test data to create.
5070       * @param string $classification Timeline classification.
5071       * @param int $limit Maximum number of results to return.
5072       * @param int $offset Results to skip at the start of the result set.
5073       * @param string[] $expectedcourses Expected courses in results.
5074       * @param int $expectedprocessedcount Expected number of course records to be processed.
5075       */
5076      public function test_course_filter_courses_by_timeline_classification(
5077          $coursedata,
5078          $classification,
5079          $limit,
5080          $offset,
5081          $expectedcourses,
5082          $expectedprocessedcount
5083      ) {
5084          $this->resetAfterTest();
5085          $generator = $this->getDataGenerator();
5086  
5087          $courses = array_map(function($coursedata) use ($generator) {
5088              return $generator->create_course($coursedata);
5089          }, $coursedata);
5090  
5091          $student = $generator->create_user();
5092  
5093          foreach ($courses as $course) {
5094              $generator->enrol_user($student->id, $course->id, 'student');
5095          }
5096  
5097          $this->setUser($student);
5098  
5099          $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
5100          list($result, $processedcount) = course_filter_courses_by_timeline_classification(
5101              $coursesgenerator,
5102              $classification,
5103              $limit
5104          );
5105  
5106          $actual = array_map(function($course) {
5107              return $course->shortname;
5108          }, $result);
5109  
5110          $this->assertEquals($expectedcourses, $actual);
5111          $this->assertEquals($expectedprocessedcount, $processedcount);
5112      }
5113  
5114      /**
5115       * Test cases for the course_filter_courses_by_timeline_classification tests.
5116       */
5117      public function get_course_filter_courses_by_customfield_test_cases() {
5118          global $CFG;
5119          require_once($CFG->dirroot.'/blocks/myoverview/lib.php');
5120          $coursedata = [
5121              [
5122                  'shortname' => 'C1',
5123                  'customfield_checkboxfield' => 1,
5124                  'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
5125                  'customfield_selectfield' => 1,
5126                  'customfield_textfield' => 'fish',
5127              ],
5128              [
5129                  'shortname' => 'C2',
5130                  'customfield_checkboxfield' => 0,
5131                  'customfield_datefield' => strtotime('1980-08-05T13:00:00Z'),
5132              ],
5133              [
5134                  'shortname' => 'C3',
5135                  'customfield_checkboxfield' => 0,
5136                  'customfield_datefield' => strtotime('2001-02-01T12:00:00Z'),
5137                  'customfield_selectfield' => 2,
5138                  'customfield_textfield' => 'dog',
5139              ],
5140              [
5141                  'shortname' => 'C4',
5142                  'customfield_checkboxfield' => 1,
5143                  'customfield_selectfield' => 3,
5144                  'customfield_textfield' => 'cat',
5145              ],
5146              [
5147                  'shortname' => 'C5',
5148                  'customfield_datefield' => strtotime('1980-08-06T13:00:00Z'),
5149                  'customfield_selectfield' => 2,
5150                  'customfield_textfield' => 'fish',
5151              ],
5152          ];
5153  
5154          return [
5155              'empty set' => [
5156                  'coursedata' => [],
5157                  'customfield' => 'checkboxfield',
5158                  'customfieldvalue' => 1,
5159                  'limit' => 10,
5160                  'offset' => 0,
5161                  'expectedcourses' => [],
5162                  'expectedprocessedcount' => 0
5163              ],
5164              'checkbox yes' => [
5165                  'coursedata' => $coursedata,
5166                  'customfield' => 'checkboxfield',
5167                  'customfieldvalue' => 1,
5168                  'limit' => 10,
5169                  'offset' => 0,
5170                  'expectedcourses' => ['C1', 'C4'],
5171                  'expectedprocessedcount' => 5
5172              ],
5173              'checkbox no' => [
5174                  'coursedata' => $coursedata,
5175                  'customfield' => 'checkboxfield',
5176                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5177                  'limit' => 10,
5178                  'offset' => 0,
5179                  'expectedcourses' => ['C2', 'C3', 'C5'],
5180                  'expectedprocessedcount' => 5
5181              ],
5182              'date 1 Feb 2001' => [
5183                  'coursedata' => $coursedata,
5184                  'customfield' => 'datefield',
5185                  'customfieldvalue' => strtotime('2001-02-01T12:00:00Z'),
5186                  'limit' => 10,
5187                  'offset' => 0,
5188                  'expectedcourses' => ['C1', 'C3'],
5189                  'expectedprocessedcount' => 5
5190              ],
5191              'date 6 Aug 1980' => [
5192                  'coursedata' => $coursedata,
5193                  'customfield' => 'datefield',
5194                  'customfieldvalue' => strtotime('1980-08-06T13:00:00Z'),
5195                  'limit' => 10,
5196                  'offset' => 0,
5197                  'expectedcourses' => ['C5'],
5198                  'expectedprocessedcount' => 5
5199              ],
5200              'date no date' => [
5201                  'coursedata' => $coursedata,
5202                  'customfield' => 'datefield',
5203                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5204                  'limit' => 10,
5205                  'offset' => 0,
5206                  'expectedcourses' => ['C4'],
5207                  'expectedprocessedcount' => 5
5208              ],
5209              'select Option 1' => [
5210                  'coursedata' => $coursedata,
5211                  'customfield' => 'selectfield',
5212                  'customfieldvalue' => 1,
5213                  'limit' => 10,
5214                  'offset' => 0,
5215                  'expectedcourses' => ['C1'],
5216                  'expectedprocessedcount' => 5
5217              ],
5218              'select Option 2' => [
5219                  'coursedata' => $coursedata,
5220                  'customfield' => 'selectfield',
5221                  'customfieldvalue' => 2,
5222                  'limit' => 10,
5223                  'offset' => 0,
5224                  'expectedcourses' => ['C3', 'C5'],
5225                  'expectedprocessedcount' => 5
5226              ],
5227              'select no select' => [
5228                  'coursedata' => $coursedata,
5229                  'customfield' => 'selectfield',
5230                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5231                  'limit' => 10,
5232                  'offset' => 0,
5233                  'expectedcourses' => ['C2'],
5234                  'expectedprocessedcount' => 5
5235              ],
5236              'text fish' => [
5237                  'coursedata' => $coursedata,
5238                  'customfield' => 'textfield',
5239                  'customfieldvalue' => 'fish',
5240                  'limit' => 10,
5241                  'offset' => 0,
5242                  'expectedcourses' => ['C1', 'C5'],
5243                  'expectedprocessedcount' => 5
5244              ],
5245              'text dog' => [
5246                  'coursedata' => $coursedata,
5247                  'customfield' => 'textfield',
5248                  'customfieldvalue' => 'dog',
5249                  'limit' => 10,
5250                  'offset' => 0,
5251                  'expectedcourses' => ['C3'],
5252                  'expectedprocessedcount' => 5
5253              ],
5254              'text no text' => [
5255                  'coursedata' => $coursedata,
5256                  'customfield' => 'textfield',
5257                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5258                  'limit' => 10,
5259                  'offset' => 0,
5260                  'expectedcourses' => ['C2'],
5261                  'expectedprocessedcount' => 5
5262              ],
5263              'checkbox limit no' => [
5264                  'coursedata' => $coursedata,
5265                  'customfield' => 'checkboxfield',
5266                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5267                  'limit' => 2,
5268                  'offset' => 0,
5269                  'expectedcourses' => ['C2', 'C3'],
5270                  'expectedprocessedcount' => 3
5271              ],
5272              'checkbox limit offset no' => [
5273                  'coursedata' => $coursedata,
5274                  'customfield' => 'checkboxfield',
5275                  'customfieldvalue' => BLOCK_MYOVERVIEW_CUSTOMFIELD_EMPTY,
5276                  'limit' => 2,
5277                  'offset' => 3,
5278                  'expectedcourses' => ['C5'],
5279                  'expectedprocessedcount' => 2
5280              ],
5281          ];
5282      }
5283  
5284      /**
5285       * Test the course_filter_courses_by_customfield function.
5286       *
5287       * @dataProvider get_course_filter_courses_by_customfield_test_cases()
5288       * @param array $coursedata Course test data to create.
5289       * @param string $customfield Shortname of the customfield.
5290       * @param string $customfieldvalue the value to filter by.
5291       * @param int $limit Maximum number of results to return.
5292       * @param int $offset Results to skip at the start of the result set.
5293       * @param string[] $expectedcourses Expected courses in results.
5294       * @param int $expectedprocessedcount Expected number of course records to be processed.
5295       */
5296      public function test_course_filter_courses_by_customfield(
5297          $coursedata,
5298          $customfield,
5299          $customfieldvalue,
5300          $limit,
5301          $offset,
5302          $expectedcourses,
5303          $expectedprocessedcount
5304      ) {
5305          $this->resetAfterTest();
5306          $generator = $this->getDataGenerator();
5307  
5308          // Create the custom fields.
5309          $generator->create_custom_field_category([
5310              'name' => 'Course fields',
5311              'component' => 'core_course',
5312              'area' => 'course',
5313              'itemid' => 0,
5314          ]);
5315          $generator->create_custom_field([
5316              'name' => 'Checkbox field',
5317              'category' => 'Course fields',
5318              'type' => 'checkbox',
5319              'shortname' => 'checkboxfield',
5320          ]);
5321          $generator->create_custom_field([
5322              'name' => 'Date field',
5323              'category' => 'Course fields',
5324              'type' => 'date',
5325              'shortname' => 'datefield',
5326              'configdata' => '{"mindate":0, "maxdate":0}',
5327          ]);
5328          $generator->create_custom_field([
5329              'name' => 'Select field',
5330              'category' => 'Course fields',
5331              'type' => 'select',
5332              'shortname' => 'selectfield',
5333              'configdata' => '{"options":"Option 1\nOption 2\nOption 3\nOption 4"}',
5334          ]);
5335          $generator->create_custom_field([
5336              'name' => 'Text field',
5337              'category' => 'Course fields',
5338              'type' => 'text',
5339              'shortname' => 'textfield',
5340          ]);
5341  
5342          $courses = array_map(function($coursedata) use ($generator) {
5343              return $generator->create_course($coursedata);
5344          }, $coursedata);
5345  
5346          $student = $generator->create_user();
5347  
5348          foreach ($courses as $course) {
5349              $generator->enrol_user($student->id, $course->id, 'student');
5350          }
5351  
5352          $this->setUser($student);
5353  
5354          $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
5355          list($result, $processedcount) = course_filter_courses_by_customfield(
5356              $coursesgenerator,
5357              $customfield,
5358              $customfieldvalue,
5359              $limit
5360          );
5361  
5362          $actual = array_map(function($course) {
5363              return $course->shortname;
5364          }, $result);
5365  
5366          $this->assertEquals($expectedcourses, $actual);
5367          $this->assertEquals($expectedprocessedcount, $processedcount);
5368      }
5369  
5370      /**
5371       * Test cases for the course_filter_courses_by_timeline_classification w/ hidden courses tests.
5372       */
5373      public function get_course_filter_courses_by_timeline_classification_hidden_courses_test_cases() {
5374          $now = time();
5375          $day = 86400;
5376  
5377          $coursedata = [
5378              [
5379                  'shortname' => 'apast',
5380                  'startdate' => $now - ($day * 2),
5381                  'enddate' => $now - $day
5382              ],
5383              [
5384                  'shortname' => 'bpast',
5385                  'startdate' => $now - ($day * 2),
5386                  'enddate' => $now - $day
5387              ],
5388              [
5389                  'shortname' => 'cpast',
5390                  'startdate' => $now - ($day * 2),
5391                  'enddate' => $now - $day
5392              ],
5393              [
5394                  'shortname' => 'dpast',
5395                  'startdate' => $now - ($day * 2),
5396                  'enddate' => $now - $day
5397              ],
5398              [
5399                  'shortname' => 'epast',
5400                  'startdate' => $now - ($day * 2),
5401                  'enddate' => $now - $day
5402              ],
5403              [
5404                  'shortname' => 'ainprogress',
5405                  'startdate' => $now - $day,
5406                  'enddate' => $now + $day
5407              ],
5408              [
5409                  'shortname' => 'binprogress',
5410                  'startdate' => $now - $day,
5411                  'enddate' => $now + $day
5412              ],
5413              [
5414                  'shortname' => 'cinprogress',
5415                  'startdate' => $now - $day,
5416                  'enddate' => $now + $day
5417              ],
5418              [
5419                  'shortname' => 'dinprogress',
5420                  'startdate' => $now - $day,
5421                  'enddate' => $now + $day
5422              ],
5423              [
5424                  'shortname' => 'einprogress',
5425                  'startdate' => $now - $day,
5426                  'enddate' => $now + $day
5427              ],
5428              [
5429                  'shortname' => 'afuture',
5430                  'startdate' => $now + $day
5431              ],
5432              [
5433                  'shortname' => 'bfuture',
5434                  'startdate' => $now + $day
5435              ],
5436              [
5437                  'shortname' => 'cfuture',
5438                  'startdate' => $now + $day
5439              ],
5440              [
5441                  'shortname' => 'dfuture',
5442                  'startdate' => $now + $day
5443              ],
5444              [
5445                  'shortname' => 'efuture',
5446                  'startdate' => $now + $day
5447              ]
5448          ];
5449  
5450          // Raw enrolled courses result set should be returned in this order:
5451          // afuture, ainprogress, apast, bfuture, binprogress, bpast, cfuture, cinprogress, cpast,
5452          // dfuture, dinprogress, dpast, efuture, einprogress, epast
5453          //
5454          // By classification the offset values for each record should be:
5455          // COURSE_TIMELINE_FUTURE
5456          // 0 (afuture), 3 (bfuture), 6 (cfuture), 9 (dfuture), 12 (efuture)
5457          // COURSE_TIMELINE_INPROGRESS
5458          // 1 (ainprogress), 4 (binprogress), 7 (cinprogress), 10 (dinprogress), 13 (einprogress)
5459          // COURSE_TIMELINE_PAST
5460          // 2 (apast), 5 (bpast), 8 (cpast), 11 (dpast), 14 (epast).
5461          return [
5462              'empty set' => [
5463                  'coursedata' => [],
5464                  'classification' => COURSE_TIMELINE_FUTURE,
5465                  'limit' => 2,
5466                  'offset' => 0,
5467                  'expectedcourses' => [],
5468                  'expectedprocessedcount' => 0,
5469                  'hiddencourse' => ''
5470              ],
5471              // COURSE_TIMELINE_FUTURE.
5472              'future not limit no offset' => [
5473                  'coursedata' => $coursedata,
5474                  'classification' => COURSE_TIMELINE_FUTURE,
5475                  'limit' => 0,
5476                  'offset' => 0,
5477                  'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
5478                  'expectedprocessedcount' => 15,
5479                  'hiddencourse' => 'bfuture'
5480              ],
5481              'future no offset' => [
5482                  'coursedata' => $coursedata,
5483                  'classification' => COURSE_TIMELINE_FUTURE,
5484                  'limit' => 2,
5485                  'offset' => 0,
5486                  'expectedcourses' => ['afuture', 'cfuture'],
5487                  'expectedprocessedcount' => 7,
5488                  'hiddencourse' => 'bfuture'
5489              ],
5490              'future offset' => [
5491                  'coursedata' => $coursedata,
5492                  'classification' => COURSE_TIMELINE_FUTURE,
5493                  'limit' => 2,
5494                  'offset' => 2,
5495                  'expectedcourses' => ['bfuture', 'dfuture'],
5496                  'expectedprocessedcount' => 8,
5497                  'hiddencourse' => 'cfuture'
5498              ],
5499              'future exact limit' => [
5500                  'coursedata' => $coursedata,
5501                  'classification' => COURSE_TIMELINE_FUTURE,
5502                  'limit' => 5,
5503                  'offset' => 0,
5504                  'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
5505                  'expectedprocessedcount' => 15,
5506                  'hiddencourse' => 'bfuture'
5507              ],
5508              'future limit less results' => [
5509                  'coursedata' => $coursedata,
5510                  'classification' => COURSE_TIMELINE_FUTURE,
5511                  'limit' => 10,
5512                  'offset' => 0,
5513                  'expectedcourses' => ['afuture', 'cfuture', 'dfuture', 'efuture'],
5514                  'expectedprocessedcount' => 15,
5515                  'hiddencourse' => 'bfuture'
5516              ],
5517              'future limit less results with offset' => [
5518                  'coursedata' => $coursedata,
5519                  'classification' => COURSE_TIMELINE_FUTURE,
5520                  'limit' => 10,
5521                  'offset' => 5,
5522                  'expectedcourses' => ['cfuture', 'efuture'],
5523                  'expectedprocessedcount' => 10,
5524                  'hiddencourse' => 'dfuture'
5525              ],
5526          ];
5527      }
5528  
5529      /**
5530       * Test the course_filter_courses_by_timeline_classification function hidden courses.
5531       *
5532       * @dataProvider get_course_filter_courses_by_timeline_classification_hidden_courses_test_cases()
5533       * @param array $coursedata Course test data to create.
5534       * @param string $classification Timeline classification.
5535       * @param int $limit Maximum number of results to return.
5536       * @param int $offset Results to skip at the start of the result set.
5537       * @param string[] $expectedcourses Expected courses in results.
5538       * @param int $expectedprocessedcount Expected number of course records to be processed.
5539       * @param int $hiddencourse The course to hide as part of this process
5540       */
5541      public function test_course_filter_courses_by_timeline_classification_with_hidden_courses(
5542          $coursedata,
5543          $classification,
5544          $limit,
5545          $offset,
5546          $expectedcourses,
5547          $expectedprocessedcount,
5548          $hiddencourse
5549      ) {
5550          $this->resetAfterTest();
5551          $generator = $this->getDataGenerator();
5552          $student = $generator->create_user();
5553          $this->setUser($student);
5554  
5555          $courses = array_map(function($coursedata) use ($generator, $hiddencourse) {
5556              $course = $generator->create_course($coursedata);
5557              if ($course->shortname == $hiddencourse) {
5558                  set_user_preference('block_myoverview_hidden_course_' . $course->id, true);
5559              }
5560              return $course;
5561          }, $coursedata);
5562  
5563          foreach ($courses as $course) {
5564              $generator->enrol_user($student->id, $course->id, 'student');
5565          }
5566  
5567          $coursesgenerator = course_get_enrolled_courses_for_logged_in_user(0, $offset, 'shortname ASC', 'shortname');
5568          list($result, $processedcount) = course_filter_courses_by_timeline_classification(
5569              $coursesgenerator,
5570              $classification,
5571              $limit
5572          );
5573  
5574          $actual = array_map(function($course) {
5575              return $course->shortname;
5576          }, $result);
5577  
5578          $this->assertEquals($expectedcourses, $actual);
5579          $this->assertEquals($expectedprocessedcount, $processedcount);
5580      }
5581  
5582  
5583      /**
5584       * Testing core_course_core_calendar_get_valid_event_timestart_range when the course has no end date.
5585       */
5586      public function test_core_course_core_calendar_get_valid_event_timestart_range_no_enddate() {
5587          global $CFG;
5588          require_once($CFG->dirroot . "/calendar/lib.php");
5589  
5590          $this->resetAfterTest(true);
5591          $this->setAdminUser();
5592          $generator = $this->getDataGenerator();
5593          $now = time();
5594          $course = $generator->create_course(['startdate' => $now - 86400]);
5595  
5596          // Create a course event.
5597          $event = new \calendar_event([
5598              'name' => 'Test course event',
5599              'eventtype' => 'course',
5600              'courseid' => $course->id,
5601          ]);
5602  
5603          list ($min, $max) = core_course_core_calendar_get_valid_event_timestart_range($event, $course);
5604          $this->assertEquals($course->startdate, $min[0]);
5605          $this->assertNull($max);
5606      }
5607  
5608      /**
5609       * Testing core_course_core_calendar_get_valid_event_timestart_range when the course has end date.
5610       */
5611      public function test_core_course_core_calendar_get_valid_event_timestart_range_with_enddate() {
5612          global $CFG;
5613          require_once($CFG->dirroot . "/calendar/lib.php");
5614  
5615          $this->resetAfterTest(true);
5616          $this->setAdminUser();
5617          $generator = $this->getDataGenerator();
5618          $now = time();
5619          $course = $generator->create_course(['startdate' => $now - 86400, 'enddate' => $now + 86400]);
5620  
5621          // Create a course event.
5622          $event = new \calendar_event([
5623              'name' => 'Test course event',
5624              'eventtype' => 'course',
5625              'courseid' => $course->id,
5626          ]);
5627  
5628          list ($min, $max) = core_course_core_calendar_get_valid_event_timestart_range($event, $course);
5629          $this->assertEquals($course->startdate, $min[0]);
5630          $this->assertNull($max);
5631      }
5632  
5633      /**
5634       * Test the course_get_recent_courses function.
5635       */
5636      public function test_course_get_recent_courses() {
5637          global $DB;
5638  
5639          $this->resetAfterTest();
5640          $generator = $this->getDataGenerator();
5641  
5642          $courses = array();
5643          for ($i = 1; $i < 4; $i++) {
5644              $courses[]  = $generator->create_course();
5645          };
5646  
5647          $student = $generator->create_user();
5648  
5649          foreach ($courses as $course) {
5650              $generator->enrol_user($student->id, $course->id, 'student');
5651          }
5652  
5653          $this->setUser($student);
5654  
5655          $result = course_get_recent_courses($student->id);
5656  
5657          // No course accessed.
5658          $this->assertCount(0, $result);
5659  
5660          $time = time();
5661          foreach ($courses as $course) {
5662              $context = context_course::instance($course->id);
5663              course_view($context);
5664              $DB->set_field('user_lastaccess', 'timeaccess', $time, [
5665                  'userid' => $student->id,
5666                  'courseid' => $course->id,
5667                  ]);
5668              $time++;
5669          }
5670  
5671          // Every course accessed.
5672          $result = course_get_recent_courses($student->id);
5673          $this->assertCount(3, $result);
5674  
5675          // Every course accessed, result limited to 2 courses.
5676          $result = course_get_recent_courses($student->id, 2);
5677          $this->assertCount(2, $result);
5678  
5679          // Every course accessed, with limit and offset should return the first course.
5680          $result = course_get_recent_courses($student->id, 3, 2);
5681          $this->assertCount(1, $result);
5682          $this->assertArrayHasKey($courses[0]->id, $result);
5683  
5684          // Every course accessed, order by shortname DESC. The last create course ($course[2]) should have the greater shortname.
5685          $result = course_get_recent_courses($student->id, 0, 0, 'shortname DESC');
5686          $this->assertCount(3, $result);
5687          $this->assertEquals($courses[2]->id, array_values($result)[0]->id);
5688          $this->assertEquals($courses[1]->id, array_values($result)[1]->id);
5689          $this->assertEquals($courses[0]->id, array_values($result)[2]->id);
5690  
5691          // Every course accessed, order by shortname ASC.
5692          $result = course_get_recent_courses($student->id, 0, 0, 'shortname ASC');
5693          $this->assertCount(3, $result);
5694          $this->assertEquals($courses[0]->id, array_values($result)[0]->id);
5695          $this->assertEquals($courses[1]->id, array_values($result)[1]->id);
5696          $this->assertEquals($courses[2]->id, array_values($result)[2]->id);
5697  
5698          $guestcourse = $generator->create_course(
5699              (object)array('shortname' => 'guestcourse',
5700                  'enrol_guest_status_0' => ENROL_INSTANCE_ENABLED,
5701                  'enrol_guest_password_0' => ''));
5702          $context = context_course::instance($guestcourse->id);
5703          course_view($context);
5704  
5705          // Every course accessed, even the not enrolled one.
5706          $result = course_get_recent_courses($student->id);
5707          $this->assertCount(4, $result);
5708  
5709          // Suspended student.
5710          $this->getDataGenerator()->enrol_user($student->id, $courses[0]->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
5711  
5712          // The course with suspended enrolment is not returned by the function.
5713          $result = course_get_recent_courses($student->id);
5714          $this->assertCount(3, $result);
5715          $this->assertArrayNotHasKey($courses[0]->id, $result);
5716      }
5717  
5718      /**
5719       * Test the validation of the sort value in course_get_recent_courses().
5720       *
5721       * @dataProvider course_get_recent_courses_sort_validation_provider
5722       * @param string $sort The sort value
5723       * @param string $expectedexceptionmsg The expected exception message
5724       */
5725      public function test_course_get_recent_courses_sort_validation(string $sort, string $expectedexceptionmsg) {
5726          $this->resetAfterTest();
5727  
5728          $user = $this->getDataGenerator()->create_user();
5729  
5730          if (!empty($expectedexceptionmsg)) {
5731              $this->expectException('invalid_parameter_exception');
5732              $this->expectExceptionMessage($expectedexceptionmsg);
5733          }
5734          course_get_recent_courses($user->id, 0, 0, $sort);
5735      }
5736  
5737      /**
5738       * Data provider for test_course_get_recent_courses_sort_validation().
5739       *
5740       * @return array
5741       */
5742      function course_get_recent_courses_sort_validation_provider() {
5743          return [
5744              'Invalid sort format (SQL injection attempt)' =>
5745                  [
5746                      'shortname DESC LIMIT 1--',
5747                      'Invalid structure of the sort parameter, allowed structure: fieldname [ASC|DESC].',
5748                  ],
5749              'Sort uses \'sort by\' field that does not exist' =>
5750                  [
5751                      'shortname DESC, xyz ASC',
5752                      'Invalid field in the sort parameter, allowed fields: id, idnumber, summary, summaryformat, ' .
5753                      'startdate, enddate, category, shortname, fullname, timeaccess, component, visible, ' .
5754                      'showactivitydates, showcompletionconditions.',
5755              ],
5756              'Sort uses invalid value for the sorting direction' =>
5757                  [
5758                      'shortname xyz, lastaccess',
5759                      'Invalid sort direction in the sort parameter, allowed values: asc, desc.',
5760                  ],
5761              'Valid sort format' =>
5762                  [
5763                      'shortname asc, timeaccess',
5764                      ''
5765                  ]
5766          ];
5767      }
5768  
5769      /**
5770       * Test the course_get_recent_courses function.
5771       */
5772      public function test_course_get_recent_courses_with_guest() {
5773          global $DB;
5774          $this->resetAfterTest(true);
5775  
5776          $student = $this->getDataGenerator()->create_user();
5777  
5778          // Course 1 with guest access and no direct enrolment.
5779          $course1 = $this->getDataGenerator()->create_course();
5780          $context1 = context_course::instance($course1->id);
5781          $record = $DB->get_record('enrol', ['courseid' => $course1->id, 'enrol' => 'guest']);
5782          enrol_get_plugin('guest')->update_status($record, ENROL_INSTANCE_ENABLED);
5783  
5784          // Course 2 where student is enrolled with two enrolment methods.
5785          $course2 = $this->getDataGenerator()->create_course();
5786          $context2 = context_course::instance($course2->id);
5787          $record = $DB->get_record('enrol', ['courseid' => $course2->id, 'enrol' => 'self']);
5788          enrol_get_plugin('guest')->update_status($record, ENROL_INSTANCE_ENABLED);
5789          $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student', 'manual', 0, 0, ENROL_USER_ACTIVE);
5790          $this->getDataGenerator()->enrol_user($student->id, $course2->id, 'student', 'self', 0, 0, ENROL_USER_ACTIVE);
5791  
5792          // Course 3.
5793          $course3 = $this->getDataGenerator()->create_course();
5794          $context3 = context_course::instance($course3->id);
5795  
5796          // Student visits first two courses, course_get_recent_courses returns two courses.
5797          $this->setUser($student);
5798          course_view($context1);
5799          course_view($context2);
5800  
5801          $result = course_get_recent_courses($student->id);
5802          $this->assertEqualsCanonicalizing([$course2->id, $course1->id], array_column($result, 'id'));
5803  
5804          // Admin visits all three courses. Only the one with guest access is returned.
5805          $this->setAdminUser();
5806          course_view($context1);
5807          course_view($context2);
5808          course_view($context3);
5809          $result = course_get_recent_courses(get_admin()->id);
5810          $this->assertEqualsCanonicalizing([$course1->id], array_column($result, 'id'));
5811      }
5812  
5813      /**
5814       * Test cases for the course_get_course_dates_for_user_ids tests.
5815       */
5816      public function get_course_get_course_dates_for_user_ids_test_cases() {
5817          $now = time();
5818          $pastcoursestart = $now - 100;
5819          $futurecoursestart = $now + 100;
5820  
5821          return [
5822              'future course start fixed no users enrolled' => [
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                  'expected' => [
5832                      [
5833                          'start' => $futurecoursestart,
5834                          'startoffset' => 0
5835                      ],
5836                      [
5837                          'start' => $futurecoursestart,
5838                          'startoffset' => 0
5839                      ]
5840                  ]
5841              ],
5842              'future course start fixed 1 users enrolled future' => [
5843                  'relativedatemode' => false,
5844                  'coursestart' => $futurecoursestart,
5845                  'usercount' => 2,
5846                  'enrolmentmethods' => [
5847                      ['manual', ENROL_INSTANCE_ENABLED],
5848                      ['self', ENROL_INSTANCE_ENABLED]
5849                  ],
5850                  'enrolled' => [
5851                      // User 1.
5852                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
5853                      // User 2.
5854                      []
5855                  ],
5856                  'expected' => [
5857                      [
5858                          'start' => $futurecoursestart,
5859                          'startoffset' => 0
5860                      ],
5861                      [
5862                          'start' => $futurecoursestart,
5863                          'startoffset' => 0
5864                      ]
5865                  ]
5866              ],
5867              'future course start fixed 1 users enrolled past' => [
5868                  'relativedatemode' => false,
5869                  'coursestart' => $futurecoursestart,
5870                  'usercount' => 2,
5871                  'enrolmentmethods' => [
5872                      ['manual', ENROL_INSTANCE_ENABLED],
5873                      ['self', ENROL_INSTANCE_ENABLED]
5874                  ],
5875                  'enrolled' => [
5876                      // User 1.
5877                      ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
5878                      // User 2.
5879                      []
5880                  ],
5881                  'expected' => [
5882                      [
5883                          'start' => $futurecoursestart,
5884                          'startoffset' => 0
5885                      ],
5886                      [
5887                          'start' => $futurecoursestart,
5888                          'startoffset' => 0
5889                      ]
5890                  ]
5891              ],
5892              'future course start fixed 2 users enrolled future' => [
5893                  'relativedatemode' => false,
5894                  'coursestart' => $futurecoursestart,
5895                  'usercount' => 2,
5896                  'enrolmentmethods' => [
5897                      ['manual', ENROL_INSTANCE_ENABLED],
5898                      ['self', ENROL_INSTANCE_ENABLED]
5899                  ],
5900                  'enrolled' => [
5901                      // User 1.
5902                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
5903                      // User 2.
5904                      ['manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]]
5905                  ],
5906                  'expected' => [
5907                      [
5908                          'start' => $futurecoursestart,
5909                          'startoffset' => 0
5910                      ],
5911                      [
5912                          'start' => $futurecoursestart,
5913                          'startoffset' => 0
5914                      ]
5915                  ]
5916              ],
5917              'future course start fixed 2 users enrolled past' => [
5918                  'relativedatemode' => false,
5919                  'coursestart' => $futurecoursestart,
5920                  'usercount' => 2,
5921                  'enrolmentmethods' => [
5922                      ['manual', ENROL_INSTANCE_ENABLED],
5923                      ['self', ENROL_INSTANCE_ENABLED]
5924                  ],
5925                  'enrolled' => [
5926                      // User 1.
5927                      ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
5928                      // User 2.
5929                      ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
5930                  ],
5931                  'expected' => [
5932                      [
5933                          'start' => $futurecoursestart,
5934                          'startoffset' => 0
5935                      ],
5936                      [
5937                          'start' => $futurecoursestart,
5938                          'startoffset' => 0
5939                      ]
5940                  ]
5941              ],
5942              'future course start fixed 2 users enrolled mixed' => [
5943                  'relativedatemode' => false,
5944                  'coursestart' => $futurecoursestart,
5945                  'usercount' => 2,
5946                  'enrolmentmethods' => [
5947                      ['manual', ENROL_INSTANCE_ENABLED],
5948                      ['self', ENROL_INSTANCE_ENABLED]
5949                  ],
5950                  'enrolled' => [
5951                      // User 1.
5952                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
5953                      // User 2.
5954                      ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
5955                  ],
5956                  'expected' => [
5957                      [
5958                          'start' => $futurecoursestart,
5959                          'startoffset' => 0
5960                      ],
5961                      [
5962                          'start' => $futurecoursestart,
5963                          'startoffset' => 0
5964                      ]
5965                  ]
5966              ],
5967              'future course start fixed 2 users enrolled 2 methods' => [
5968                  'relativedatemode' => false,
5969                  'coursestart' => $futurecoursestart,
5970                  'usercount' => 2,
5971                  'enrolmentmethods' => [
5972                      ['manual', ENROL_INSTANCE_ENABLED],
5973                      ['self', ENROL_INSTANCE_ENABLED]
5974                  ],
5975                  'enrolled' => [
5976                      // User 1.
5977                      [
5978                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
5979                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
5980                      ],
5981                      // User 2.
5982                      [
5983                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
5984                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
5985                      ]
5986                  ],
5987                  'expected' => [
5988                      [
5989                          'start' => $futurecoursestart,
5990                          'startoffset' => 0
5991                      ],
5992                      [
5993                          'start' => $futurecoursestart,
5994                          'startoffset' => 0
5995                      ]
5996                  ]
5997              ],
5998              'future course start fixed 2 users enrolled 2 methods 1 disabled' => [
5999                  'relativedatemode' => false,
6000                  'coursestart' => $futurecoursestart,
6001                  'usercount' => 2,
6002                  'enrolmentmethods' => [
6003                      ['manual', ENROL_INSTANCE_DISABLED],
6004                      ['self', ENROL_INSTANCE_ENABLED]
6005                  ],
6006                  'enrolled' => [
6007                      // User 1.
6008                      [
6009                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6010                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6011                      ],
6012                      // User 2.
6013                      [
6014                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6015                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6016                      ]
6017                  ],
6018                  'expected' => [
6019                      [
6020                          'start' => $futurecoursestart,
6021                          'startoffset' => 0
6022                      ],
6023                      [
6024                          'start' => $futurecoursestart,
6025                          'startoffset' => 0
6026                      ]
6027                  ]
6028              ],
6029              'future course start fixed 2 users enrolled 2 methods 2 disabled' => [
6030                  'relativedatemode' => false,
6031                  'coursestart' => $futurecoursestart,
6032                  'usercount' => 2,
6033                  'enrolmentmethods' => [
6034                      ['manual', ENROL_INSTANCE_DISABLED],
6035                      ['self', ENROL_INSTANCE_DISABLED]
6036                  ],
6037                  'enrolled' => [
6038                      // User 1.
6039                      [
6040                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6041                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6042                      ],
6043                      // User 2.
6044                      [
6045                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6046                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6047                      ]
6048                  ],
6049                  'expected' => [
6050                      [
6051                          'start' => $futurecoursestart,
6052                          'startoffset' => 0
6053                      ],
6054                      [
6055                          'start' => $futurecoursestart,
6056                          'startoffset' => 0
6057                      ]
6058                  ]
6059              ],
6060              'future course start fixed 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6061                  'relativedatemode' => false,
6062                  'coursestart' => $futurecoursestart,
6063                  'usercount' => 2,
6064                  'enrolmentmethods' => [
6065                      ['manual', ENROL_INSTANCE_ENABLED],
6066                      ['self', ENROL_INSTANCE_ENABLED]
6067                  ],
6068                  'enrolled' => [
6069                      // User 1.
6070                      [
6071                          'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6072                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6073                      ],
6074                      // User 2.
6075                      [
6076                          'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6077                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6078                      ]
6079                  ],
6080                  'expected' => [
6081                      [
6082                          'start' => $futurecoursestart,
6083                          'startoffset' => 0
6084                      ],
6085                      [
6086                          'start' => $futurecoursestart,
6087                          'startoffset' => 0
6088                      ]
6089                  ]
6090              ],
6091              'future course start fixed 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6092                  'relativedatemode' => false,
6093                  'coursestart' => $futurecoursestart,
6094                  'usercount' => 2,
6095                  'enrolmentmethods' => [
6096                      ['manual', ENROL_INSTANCE_ENABLED],
6097                      ['self', ENROL_INSTANCE_ENABLED]
6098                  ],
6099                  'enrolled' => [
6100                      // User 1.
6101                      [
6102                          'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6103                          'self' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED]
6104                      ],
6105                      // User 2.
6106                      [
6107                          'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6108                          'self' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED]
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 no users enrolled' => [
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                  'expected' => [
6132                      [
6133                          'start' => $futurecoursestart,
6134                          'startoffset' => 0
6135                      ],
6136                      [
6137                          'start' => $futurecoursestart,
6138                          'startoffset' => 0
6139                      ]
6140                  ]
6141              ],
6142              'future course start relative 1 users enrolled future' => [
6143                  'relativedatemode' => true,
6144                  'coursestart' => $futurecoursestart,
6145                  'usercount' => 2,
6146                  'enrolmentmethods' => [
6147                      ['manual', ENROL_INSTANCE_ENABLED],
6148                      ['self', ENROL_INSTANCE_ENABLED]
6149                  ],
6150                  'enrolled' => [
6151                      // User 1.
6152                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
6153                      // User 2.
6154                      []
6155                  ],
6156                  'expected' => [
6157                      [
6158                          'start' => $futurecoursestart + 10,
6159                          'startoffset' => 10
6160                      ],
6161                      [
6162                          'start' => $futurecoursestart,
6163                          'startoffset' => 0
6164                      ]
6165                  ]
6166              ],
6167              'future course start relative 1 users enrolled past' => [
6168                  'relativedatemode' => true,
6169                  'coursestart' => $futurecoursestart,
6170                  'usercount' => 2,
6171                  'enrolmentmethods' => [
6172                      ['manual', ENROL_INSTANCE_ENABLED],
6173                      ['self', ENROL_INSTANCE_ENABLED]
6174                  ],
6175                  'enrolled' => [
6176                      // User 1.
6177                      ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
6178                      // User 2.
6179                      []
6180                  ],
6181                  'expected' => [
6182                      [
6183                          'start' => $futurecoursestart,
6184                          'startoffset' => 0
6185                      ],
6186                      [
6187                          'start' => $futurecoursestart,
6188                          'startoffset' => 0
6189                      ]
6190                  ]
6191              ],
6192              'future course start relative 2 users enrolled future' => [
6193                  'relativedatemode' => true,
6194                  'coursestart' => $futurecoursestart,
6195                  'usercount' => 2,
6196                  'enrolmentmethods' => [
6197                      ['manual', ENROL_INSTANCE_ENABLED],
6198                      ['self', ENROL_INSTANCE_ENABLED]
6199                  ],
6200                  'enrolled' => [
6201                      // User 1.
6202                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
6203                      // User 2.
6204                      ['manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]]
6205                  ],
6206                  'expected' => [
6207                      [
6208                          'start' => $futurecoursestart + 10,
6209                          'startoffset' => 10
6210                      ],
6211                      [
6212                          'start' => $futurecoursestart + 20,
6213                          'startoffset' => 20
6214                      ]
6215                  ]
6216              ],
6217              'future course start relative 2 users enrolled past' => [
6218                  'relativedatemode' => true,
6219                  'coursestart' => $futurecoursestart,
6220                  'usercount' => 2,
6221                  'enrolmentmethods' => [
6222                      ['manual', ENROL_INSTANCE_ENABLED],
6223                      ['self', ENROL_INSTANCE_ENABLED]
6224                  ],
6225                  'enrolled' => [
6226                      // User 1.
6227                      ['manual' => [$futurecoursestart - 10, ENROL_USER_ACTIVE]],
6228                      // User 2.
6229                      ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
6230                  ],
6231                  'expected' => [
6232                      [
6233                          'start' => $futurecoursestart,
6234                          'startoffset' => 0
6235                      ],
6236                      [
6237                          'start' => $futurecoursestart,
6238                          'startoffset' => 0
6239                      ]
6240                  ]
6241              ],
6242              'future course start relative 2 users enrolled mixed' => [
6243                  'relativedatemode' => true,
6244                  'coursestart' => $futurecoursestart,
6245                  'usercount' => 2,
6246                  'enrolmentmethods' => [
6247                      ['manual', ENROL_INSTANCE_ENABLED],
6248                      ['self', ENROL_INSTANCE_ENABLED]
6249                  ],
6250                  'enrolled' => [
6251                      // User 1.
6252                      ['manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]],
6253                      // User 2.
6254                      ['manual' => [$futurecoursestart - 20, ENROL_USER_ACTIVE]]
6255                  ],
6256                  'expected' => [
6257                      [
6258                          'start' => $futurecoursestart + 10,
6259                          'startoffset' => 10
6260                      ],
6261                      [
6262                          'start' => $futurecoursestart,
6263                          'startoffset' => 0
6264                      ]
6265                  ]
6266              ],
6267              'future course start relative 2 users enrolled 2 methods' => [
6268                  'relativedatemode' => true,
6269                  'coursestart' => $futurecoursestart,
6270                  'usercount' => 2,
6271                  'enrolmentmethods' => [
6272                      ['manual', ENROL_INSTANCE_ENABLED],
6273                      ['self', ENROL_INSTANCE_ENABLED]
6274                  ],
6275                  'enrolled' => [
6276                      // User 1.
6277                      [
6278                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6279                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6280                      ],
6281                      // User 2.
6282                      [
6283                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6284                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6285                      ]
6286                  ],
6287                  'expected' => [
6288                      [
6289                          'start' => $futurecoursestart + 10,
6290                          'startoffset' => 10
6291                      ],
6292                      [
6293                          'start' => $futurecoursestart + 10,
6294                          'startoffset' => 10
6295                      ]
6296                  ]
6297              ],
6298              'future course start relative 2 users enrolled 2 methods 1 disabled' => [
6299                  'relativedatemode' => true,
6300                  'coursestart' => $futurecoursestart,
6301                  'usercount' => 2,
6302                  'enrolmentmethods' => [
6303                      ['manual', ENROL_INSTANCE_DISABLED],
6304                      ['self', ENROL_INSTANCE_ENABLED]
6305                  ],
6306                  'enrolled' => [
6307                      // User 1.
6308                      [
6309                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6310                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6311                      ],
6312                      // User 2.
6313                      [
6314                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6315                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6316                      ]
6317                  ],
6318                  'expected' => [
6319                      [
6320                          'start' => $futurecoursestart + 20,
6321                          'startoffset' => 20
6322                      ],
6323                      [
6324                          'start' => $futurecoursestart + 10,
6325                          'startoffset' => 10
6326                      ]
6327                  ]
6328              ],
6329              'future course start relative 2 users enrolled 2 methods 2 disabled' => [
6330                  'relativedatemode' => true,
6331                  'coursestart' => $futurecoursestart,
6332                  'usercount' => 2,
6333                  'enrolmentmethods' => [
6334                      ['manual', ENROL_INSTANCE_DISABLED],
6335                      ['self', ENROL_INSTANCE_DISABLED]
6336                  ],
6337                  'enrolled' => [
6338                      // User 1.
6339                      [
6340                          'manual' => [$futurecoursestart + 10, ENROL_USER_ACTIVE],
6341                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6342                      ],
6343                      // User 2.
6344                      [
6345                          'manual' => [$futurecoursestart + 20, ENROL_USER_ACTIVE],
6346                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6347                      ]
6348                  ],
6349                  'expected' => [
6350                      [
6351                          'start' => $futurecoursestart,
6352                          'startoffset' => 0
6353                      ],
6354                      [
6355                          'start' => $futurecoursestart,
6356                          'startoffset' => 0
6357                      ]
6358                  ]
6359              ],
6360              'future course start relative 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6361                  'relativedatemode' => true,
6362                  'coursestart' => $futurecoursestart,
6363                  'usercount' => 2,
6364                  'enrolmentmethods' => [
6365                      ['manual', ENROL_INSTANCE_ENABLED],
6366                      ['self', ENROL_INSTANCE_ENABLED]
6367                  ],
6368                  'enrolled' => [
6369                      // User 1.
6370                      [
6371                          'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6372                          'self' => [$futurecoursestart + 20, ENROL_USER_ACTIVE]
6373                      ],
6374                      // User 2.
6375                      [
6376                          'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6377                          'self' => [$futurecoursestart + 10, ENROL_USER_ACTIVE]
6378                      ]
6379                  ],
6380                  'expected' => [
6381                      [
6382                          'start' => $futurecoursestart + 20,
6383                          'startoffset' => 20
6384                      ],
6385                      [
6386                          'start' => $futurecoursestart + 10,
6387                          'startoffset' => 10
6388                      ]
6389                  ]
6390              ],
6391              'future course start relative 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6392                  'relativedatemode' => true,
6393                  'coursestart' => $futurecoursestart,
6394                  'usercount' => 2,
6395                  'enrolmentmethods' => [
6396                      ['manual', ENROL_INSTANCE_ENABLED],
6397                      ['self', ENROL_INSTANCE_ENABLED]
6398                  ],
6399                  'enrolled' => [
6400                      // User 1.
6401                      [
6402                          'manual' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED],
6403                          'self' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED]
6404                      ],
6405                      // User 2.
6406                      [
6407                          'manual' => [$futurecoursestart + 20, ENROL_USER_SUSPENDED],
6408                          'self' => [$futurecoursestart + 10, ENROL_USER_SUSPENDED]
6409                      ]
6410                  ],
6411                  'expected' => [
6412                      [
6413                          'start' => $futurecoursestart,
6414                          'startoffset' => 0
6415                      ],
6416                      [
6417                          'start' => $futurecoursestart,
6418                          'startoffset' => 0
6419                      ]
6420                  ]
6421              ],
6422  
6423              // Course start date in the past.
6424              'past course start fixed no users enrolled' => [
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                  'expected' => [
6434                      [
6435                          'start' => $pastcoursestart,
6436                          'startoffset' => 0
6437                      ],
6438                      [
6439                          'start' => $pastcoursestart,
6440                          'startoffset' => 0
6441                      ]
6442                  ]
6443              ],
6444              'past course start fixed 1 users enrolled future' => [
6445                  'relativedatemode' => false,
6446                  'coursestart' => $pastcoursestart,
6447                  'usercount' => 2,
6448                  'enrolmentmethods' => [
6449                      ['manual', ENROL_INSTANCE_ENABLED],
6450                      ['self', ENROL_INSTANCE_ENABLED]
6451                  ],
6452                  'enrolled' => [
6453                      // User 1.
6454                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6455                      // User 2.
6456                      []
6457                  ],
6458                  'expected' => [
6459                      [
6460                          'start' => $pastcoursestart,
6461                          'startoffset' => 0
6462                      ],
6463                      [
6464                          'start' => $pastcoursestart,
6465                          'startoffset' => 0
6466                      ]
6467                  ]
6468              ],
6469              'past course start fixed 1 users enrolled past' => [
6470                  'relativedatemode' => false,
6471                  'coursestart' => $pastcoursestart,
6472                  'usercount' => 2,
6473                  'enrolmentmethods' => [
6474                      ['manual', ENROL_INSTANCE_ENABLED],
6475                      ['self', ENROL_INSTANCE_ENABLED]
6476                  ],
6477                  'enrolled' => [
6478                      // User 1.
6479                      ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6480                      // User 2.
6481                      []
6482                  ],
6483                  'expected' => [
6484                      [
6485                          'start' => $pastcoursestart,
6486                          'startoffset' => 0
6487                      ],
6488                      [
6489                          'start' => $pastcoursestart,
6490                          'startoffset' => 0
6491                      ]
6492                  ]
6493              ],
6494              'past course start fixed 2 users enrolled future' => [
6495                  'relativedatemode' => false,
6496                  'coursestart' => $pastcoursestart,
6497                  'usercount' => 2,
6498                  'enrolmentmethods' => [
6499                      ['manual', ENROL_INSTANCE_ENABLED],
6500                      ['self', ENROL_INSTANCE_ENABLED]
6501                  ],
6502                  'enrolled' => [
6503                      // User 1.
6504                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6505                      // User 2.
6506                      ['manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]]
6507                  ],
6508                  'expected' => [
6509                      [
6510                          'start' => $pastcoursestart,
6511                          'startoffset' => 0
6512                      ],
6513                      [
6514                          'start' => $pastcoursestart,
6515                          'startoffset' => 0
6516                      ]
6517                  ]
6518              ],
6519              'past course start fixed 2 users enrolled past' => [
6520                  'relativedatemode' => false,
6521                  'coursestart' => $pastcoursestart,
6522                  'usercount' => 2,
6523                  'enrolmentmethods' => [
6524                      ['manual', ENROL_INSTANCE_ENABLED],
6525                      ['self', ENROL_INSTANCE_ENABLED]
6526                  ],
6527                  'enrolled' => [
6528                      // User 1.
6529                      ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6530                      // User 2.
6531                      ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6532                  ],
6533                  'expected' => [
6534                      [
6535                          'start' => $pastcoursestart,
6536                          'startoffset' => 0
6537                      ],
6538                      [
6539                          'start' => $pastcoursestart,
6540                          'startoffset' => 0
6541                      ]
6542                  ]
6543              ],
6544              'past course start fixed 2 users enrolled mixed' => [
6545                  'relativedatemode' => false,
6546                  'coursestart' => $pastcoursestart,
6547                  'usercount' => 2,
6548                  'enrolmentmethods' => [
6549                      ['manual', ENROL_INSTANCE_ENABLED],
6550                      ['self', ENROL_INSTANCE_ENABLED]
6551                  ],
6552                  'enrolled' => [
6553                      // User 1.
6554                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6555                      // User 2.
6556                      ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6557                  ],
6558                  'expected' => [
6559                      [
6560                          'start' => $pastcoursestart,
6561                          'startoffset' => 0
6562                      ],
6563                      [
6564                          'start' => $pastcoursestart,
6565                          'startoffset' => 0
6566                      ]
6567                  ]
6568              ],
6569              'past course start fixed 2 users enrolled 2 methods' => [
6570                  'relativedatemode' => false,
6571                  'coursestart' => $pastcoursestart,
6572                  'usercount' => 2,
6573                  'enrolmentmethods' => [
6574                      ['manual', ENROL_INSTANCE_ENABLED],
6575                      ['self', ENROL_INSTANCE_ENABLED]
6576                  ],
6577                  'enrolled' => [
6578                      // User 1.
6579                      [
6580                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6581                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6582                      ],
6583                      // User 2.
6584                      [
6585                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6586                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6587                      ]
6588                  ],
6589                  'expected' => [
6590                      [
6591                          'start' => $pastcoursestart,
6592                          'startoffset' => 0
6593                      ],
6594                      [
6595                          'start' => $pastcoursestart,
6596                          'startoffset' => 0
6597                      ]
6598                  ]
6599              ],
6600              'past course start fixed 2 users enrolled 2 methods 1 disabled' => [
6601                  'relativedatemode' => false,
6602                  'coursestart' => $pastcoursestart,
6603                  'usercount' => 2,
6604                  'enrolmentmethods' => [
6605                      ['manual', ENROL_INSTANCE_DISABLED],
6606                      ['self', ENROL_INSTANCE_ENABLED]
6607                  ],
6608                  'enrolled' => [
6609                      // User 1.
6610                      [
6611                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6612                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6613                      ],
6614                      // User 2.
6615                      [
6616                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6617                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6618                      ]
6619                  ],
6620                  'expected' => [
6621                      [
6622                          'start' => $pastcoursestart,
6623                          'startoffset' => 0
6624                      ],
6625                      [
6626                          'start' => $pastcoursestart,
6627                          'startoffset' => 0
6628                      ]
6629                  ]
6630              ],
6631              'past course start fixed 2 users enrolled 2 methods 2 disabled' => [
6632                  'relativedatemode' => false,
6633                  'coursestart' => $pastcoursestart,
6634                  'usercount' => 2,
6635                  'enrolmentmethods' => [
6636                      ['manual', ENROL_INSTANCE_DISABLED],
6637                      ['self', ENROL_INSTANCE_DISABLED]
6638                  ],
6639                  'enrolled' => [
6640                      // User 1.
6641                      [
6642                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6643                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6644                      ],
6645                      // User 2.
6646                      [
6647                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6648                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6649                      ]
6650                  ],
6651                  'expected' => [
6652                      [
6653                          'start' => $pastcoursestart,
6654                          'startoffset' => 0
6655                      ],
6656                      [
6657                          'start' => $pastcoursestart,
6658                          'startoffset' => 0
6659                      ]
6660                  ]
6661              ],
6662              'past course start fixed 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6663                  'relativedatemode' => false,
6664                  'coursestart' => $pastcoursestart,
6665                  'usercount' => 2,
6666                  'enrolmentmethods' => [
6667                      ['manual', ENROL_INSTANCE_ENABLED],
6668                      ['self', ENROL_INSTANCE_ENABLED]
6669                  ],
6670                  'enrolled' => [
6671                      // User 1.
6672                      [
6673                          'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6674                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6675                      ],
6676                      // User 2.
6677                      [
6678                          'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6679                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6680                      ]
6681                  ],
6682                  'expected' => [
6683                      [
6684                          'start' => $pastcoursestart,
6685                          'startoffset' => 0
6686                      ],
6687                      [
6688                          'start' => $pastcoursestart,
6689                          'startoffset' => 0
6690                      ]
6691                  ]
6692              ],
6693              'past course start fixed 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6694                  'relativedatemode' => false,
6695                  'coursestart' => $pastcoursestart,
6696                  'usercount' => 2,
6697                  'enrolmentmethods' => [
6698                      ['manual', ENROL_INSTANCE_ENABLED],
6699                      ['self', ENROL_INSTANCE_ENABLED]
6700                  ],
6701                  'enrolled' => [
6702                      // User 1.
6703                      [
6704                          'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6705                          'self' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED]
6706                      ],
6707                      // User 2.
6708                      [
6709                          'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6710                          'self' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED]
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 no users enrolled' => [
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                  'expected' => [
6734                      [
6735                          'start' => $pastcoursestart,
6736                          'startoffset' => 0
6737                      ],
6738                      [
6739                          'start' => $pastcoursestart,
6740                          'startoffset' => 0
6741                      ]
6742                  ]
6743              ],
6744              'past course start relative 1 users enrolled future' => [
6745                  'relativedatemode' => true,
6746                  'coursestart' => $pastcoursestart,
6747                  'usercount' => 2,
6748                  'enrolmentmethods' => [
6749                      ['manual', ENROL_INSTANCE_ENABLED],
6750                      ['self', ENROL_INSTANCE_ENABLED]
6751                  ],
6752                  'enrolled' => [
6753                      // User 1.
6754                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6755                      // User 2.
6756                      []
6757                  ],
6758                  'expected' => [
6759                      [
6760                          'start' => $pastcoursestart + 10,
6761                          'startoffset' => 10
6762                      ],
6763                      [
6764                          'start' => $pastcoursestart,
6765                          'startoffset' => 0
6766                      ]
6767                  ]
6768              ],
6769              'past course start relative 1 users enrolled past' => [
6770                  'relativedatemode' => true,
6771                  'coursestart' => $pastcoursestart,
6772                  'usercount' => 2,
6773                  'enrolmentmethods' => [
6774                      ['manual', ENROL_INSTANCE_ENABLED],
6775                      ['self', ENROL_INSTANCE_ENABLED]
6776                  ],
6777                  'enrolled' => [
6778                      // User 1.
6779                      ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6780                      // User 2.
6781                      []
6782                  ],
6783                  'expected' => [
6784                      [
6785                          'start' => $pastcoursestart,
6786                          'startoffset' => 0
6787                      ],
6788                      [
6789                          'start' => $pastcoursestart,
6790                          'startoffset' => 0
6791                      ]
6792                  ]
6793              ],
6794              'past course start relative 2 users enrolled future' => [
6795                  'relativedatemode' => true,
6796                  'coursestart' => $pastcoursestart,
6797                  'usercount' => 2,
6798                  'enrolmentmethods' => [
6799                      ['manual', ENROL_INSTANCE_ENABLED],
6800                      ['self', ENROL_INSTANCE_ENABLED]
6801                  ],
6802                  'enrolled' => [
6803                      // User 1.
6804                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6805                      // User 2.
6806                      ['manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]]
6807                  ],
6808                  'expected' => [
6809                      [
6810                          'start' => $pastcoursestart + 10,
6811                          'startoffset' => 10
6812                      ],
6813                      [
6814                          'start' => $pastcoursestart + 20,
6815                          'startoffset' => 20
6816                      ]
6817                  ]
6818              ],
6819              'past course start relative 2 users enrolled past' => [
6820                  'relativedatemode' => true,
6821                  'coursestart' => $pastcoursestart,
6822                  'usercount' => 2,
6823                  'enrolmentmethods' => [
6824                      ['manual', ENROL_INSTANCE_ENABLED],
6825                      ['self', ENROL_INSTANCE_ENABLED]
6826                  ],
6827                  'enrolled' => [
6828                      // User 1.
6829                      ['manual' => [$pastcoursestart - 10, ENROL_USER_ACTIVE]],
6830                      // User 2.
6831                      ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6832                  ],
6833                  'expected' => [
6834                      [
6835                          'start' => $pastcoursestart,
6836                          'startoffset' => 0
6837                      ],
6838                      [
6839                          'start' => $pastcoursestart,
6840                          'startoffset' => 0
6841                      ]
6842                  ]
6843              ],
6844              'past course start relative 2 users enrolled mixed' => [
6845                  'relativedatemode' => true,
6846                  'coursestart' => $pastcoursestart,
6847                  'usercount' => 2,
6848                  'enrolmentmethods' => [
6849                      ['manual', ENROL_INSTANCE_ENABLED],
6850                      ['self', ENROL_INSTANCE_ENABLED]
6851                  ],
6852                  'enrolled' => [
6853                      // User 1.
6854                      ['manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]],
6855                      // User 2.
6856                      ['manual' => [$pastcoursestart - 20, ENROL_USER_ACTIVE]]
6857                  ],
6858                  'expected' => [
6859                      [
6860                          'start' => $pastcoursestart + 10,
6861                          'startoffset' => 10
6862                      ],
6863                      [
6864                          'start' => $pastcoursestart,
6865                          'startoffset' => 0
6866                      ]
6867                  ]
6868              ],
6869              'past course start relative 2 users enrolled 2 methods' => [
6870                  'relativedatemode' => true,
6871                  'coursestart' => $pastcoursestart,
6872                  'usercount' => 2,
6873                  'enrolmentmethods' => [
6874                      ['manual', ENROL_INSTANCE_ENABLED],
6875                      ['self', ENROL_INSTANCE_ENABLED]
6876                  ],
6877                  'enrolled' => [
6878                      // User 1.
6879                      [
6880                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6881                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6882                      ],
6883                      // User 2.
6884                      [
6885                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6886                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6887                      ]
6888                  ],
6889                  'expected' => [
6890                      [
6891                          'start' => $pastcoursestart + 10,
6892                          'startoffset' => 10
6893                      ],
6894                      [
6895                          'start' => $pastcoursestart + 10,
6896                          'startoffset' => 10
6897                      ]
6898                  ]
6899              ],
6900              'past course start relative 2 users enrolled 2 methods 1 disabled' => [
6901                  'relativedatemode' => true,
6902                  'coursestart' => $pastcoursestart,
6903                  'usercount' => 2,
6904                  'enrolmentmethods' => [
6905                      ['manual', ENROL_INSTANCE_DISABLED],
6906                      ['self', ENROL_INSTANCE_ENABLED]
6907                  ],
6908                  'enrolled' => [
6909                      // User 1.
6910                      [
6911                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6912                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6913                      ],
6914                      // User 2.
6915                      [
6916                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6917                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6918                      ]
6919                  ],
6920                  'expected' => [
6921                      [
6922                          'start' => $pastcoursestart + 20,
6923                          'startoffset' => 20
6924                      ],
6925                      [
6926                          'start' => $pastcoursestart + 10,
6927                          'startoffset' => 10
6928                      ]
6929                  ]
6930              ],
6931              'past course start relative 2 users enrolled 2 methods 2 disabled' => [
6932                  'relativedatemode' => true,
6933                  'coursestart' => $pastcoursestart,
6934                  'usercount' => 2,
6935                  'enrolmentmethods' => [
6936                      ['manual', ENROL_INSTANCE_DISABLED],
6937                      ['self', ENROL_INSTANCE_DISABLED]
6938                  ],
6939                  'enrolled' => [
6940                      // User 1.
6941                      [
6942                          'manual' => [$pastcoursestart + 10, ENROL_USER_ACTIVE],
6943                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6944                      ],
6945                      // User 2.
6946                      [
6947                          'manual' => [$pastcoursestart + 20, ENROL_USER_ACTIVE],
6948                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6949                      ]
6950                  ],
6951                  'expected' => [
6952                      [
6953                          'start' => $pastcoursestart,
6954                          'startoffset' => 0
6955                      ],
6956                      [
6957                          'start' => $pastcoursestart,
6958                          'startoffset' => 0
6959                      ]
6960                  ]
6961              ],
6962              'past course start relative 2 users enrolled 2 methods 0 disabled 1 user suspended' => [
6963                  'relativedatemode' => true,
6964                  'coursestart' => $pastcoursestart,
6965                  'usercount' => 2,
6966                  'enrolmentmethods' => [
6967                      ['manual', ENROL_INSTANCE_ENABLED],
6968                      ['self', ENROL_INSTANCE_ENABLED]
6969                  ],
6970                  'enrolled' => [
6971                      // User 1.
6972                      [
6973                          'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
6974                          'self' => [$pastcoursestart + 20, ENROL_USER_ACTIVE]
6975                      ],
6976                      // User 2.
6977                      [
6978                          'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
6979                          'self' => [$pastcoursestart + 10, ENROL_USER_ACTIVE]
6980                      ]
6981                  ],
6982                  'expected' => [
6983                      [
6984                          'start' => $pastcoursestart + 20,
6985                          'startoffset' => 20
6986                      ],
6987                      [
6988                          'start' => $pastcoursestart + 10,
6989                          'startoffset' => 10
6990                      ]
6991                  ]
6992              ],
6993              'past course start relative 2 users enrolled 2 methods 0 disabled 2 user suspended' => [
6994                  'relativedatemode' => true,
6995                  'coursestart' => $pastcoursestart,
6996                  'usercount' => 2,
6997                  'enrolmentmethods' => [
6998                      ['manual', ENROL_INSTANCE_ENABLED],
6999                      ['self', ENROL_INSTANCE_ENABLED]
7000                  ],
7001                  'enrolled' => [
7002                      // User 1.
7003                      [
7004                          'manual' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED],
7005                          'self' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED]
7006                      ],
7007                      // User 2.
7008                      [
7009                          'manual' => [$pastcoursestart + 20, ENROL_USER_SUSPENDED],
7010                          'self' => [$pastcoursestart + 10, ENROL_USER_SUSPENDED]
7011                      ]
7012                  ],
7013                  'expected' => [
7014                      [
7015                          'start' => $pastcoursestart,
7016                          'startoffset' => 0
7017                      ],
7018                      [
7019                          'start' => $pastcoursestart,
7020                          'startoffset' => 0
7021                      ]
7022                  ]
7023              ]
7024          ];
7025      }
7026  
7027      /**
7028       * Test the course_get_course_dates_for_user_ids function.
7029       *
7030       * @dataProvider get_course_get_course_dates_for_user_ids_test_cases()
7031       * @param bool $relativedatemode Set the course to relative dates mode
7032       * @param int $coursestart Course start date
7033       * @param int $usercount Number of users to create
7034       * @param array $enrolmentmethods Enrolment methods to set for the course
7035       * @param array $enrolled Enrolment config for to set for the users
7036       * @param array $expected Expected output
7037       */
7038      public function test_course_get_course_dates_for_user_ids(
7039          $relativedatemode,
7040          $coursestart,
7041          $usercount,
7042          $enrolmentmethods,
7043          $enrolled,
7044          $expected
7045      ) {
7046          global $DB;
7047          $this->resetAfterTest();
7048  
7049          $generator = $this->getDataGenerator();
7050          $course  = $generator->create_course(['startdate' => $coursestart]);
7051          $course->relativedatesmode = $relativedatemode;
7052          $users = [];
7053  
7054          for ($i = 0; $i < $usercount; $i++) {
7055              $users[] = $generator->create_user();
7056          }
7057  
7058          foreach ($enrolmentmethods as [$type, $status]) {
7059              $record = $DB->get_record('enrol', ['courseid' => $course->id, 'enrol' => $type]);
7060              $plugin = enrol_get_plugin($type);
7061              if ($record->status != $status) {
7062                  $plugin->update_status($record, $status);
7063              }
7064          }
7065  
7066          foreach ($enrolled as $index => $enrolconfig) {
7067              $user = $users[$index];
7068              foreach ($enrolconfig as $type => [$starttime, $status]) {
7069                  $generator->enrol_user($user->id, $course->id, 'student', $type, $starttime, 0, $status);
7070              }
7071          }
7072  
7073          $userids = array_map(function($user) {
7074              return $user->id;
7075          }, $users);
7076          $actual = course_get_course_dates_for_user_ids($course, $userids);
7077  
7078          foreach ($expected as $index => $exp) {
7079              $userid = $userids[$index];
7080              $act = $actual[$userid];
7081  
7082              $this->assertEquals($exp['start'], $act['start']);
7083              $this->assertEquals($exp['startoffset'], $act['startoffset']);
7084          }
7085      }
7086  
7087      /**
7088       * Test that calling course_get_course_dates_for_user_ids multiple times in the
7089       * same request fill fetch the correct data for the user.
7090       */
7091      public function test_course_get_course_dates_for_user_ids_multiple_calls() {
7092          $this->resetAfterTest();
7093  
7094          $generator = $this->getDataGenerator();
7095          $now = time();
7096          $coursestart = $now - 1000;
7097          $course  = $generator->create_course(['startdate' => $coursestart]);
7098          $course->relativedatesmode = true;
7099          $user1 = $generator->create_user();
7100          $user2 = $generator->create_user();
7101          $user1start = $coursestart + 100;
7102          $user2start = $coursestart + 200;
7103  
7104          $generator->enrol_user($user1->id, $course->id, 'student', 'manual', $user1start);
7105          $generator->enrol_user($user2->id, $course->id, 'student', 'manual', $user2start);
7106  
7107          $result = course_get_course_dates_for_user_ids($course, [$user1->id]);
7108          $this->assertEquals($user1start, $result[$user1->id]['start']);
7109  
7110          $result = course_get_course_dates_for_user_ids($course, [$user1->id, $user2->id]);
7111          $this->assertEquals($user1start, $result[$user1->id]['start']);
7112          $this->assertEquals($user2start, $result[$user2->id]['start']);
7113  
7114          $result = course_get_course_dates_for_user_ids($course, [$user2->id]);
7115          $this->assertEquals($user2start, $result[$user2->id]['start']);
7116      }
7117  
7118      /**
7119       * Data provider for test_course_modules_pending_deletion.
7120       *
7121       * @return array An array of arrays contain test data
7122       */
7123      public function provider_course_modules_pending_deletion() {
7124          return [
7125              'Non-gradable activity, check all'              => [['forum'], 0, false, true],
7126              'Gradable activity, check all'                  => [['assign'], 0, false, true],
7127              'Non-gradable activity, check gradables'        => [['forum'], 0, true, false],
7128              'Gradable activity, check gradables'            => [['assign'], 0, true, true],
7129              'Non-gradable within multiple, check all'       => [['quiz', 'forum', 'assign'], 1, false, true],
7130              'Non-gradable within multiple, check gradables' => [['quiz', 'forum', 'assign'], 1, true, false],
7131              'Gradable within multiple, check all'           => [['quiz', 'forum', 'assign'], 2, false, true],
7132              'Gradable within multiple, check gradables'     => [['quiz', 'forum', 'assign'], 2, true, true],
7133          ];
7134      }
7135  
7136      /**
7137       * Tests the function course_modules_pending_deletion.
7138       *
7139       * @param string[] $modules A complete list aff all available modules before deletion
7140       * @param int $indextodelete The index of the module in the $modules array that we want to test with
7141       * @param bool $gradable The value to pass to the gradable argument of the course_modules_pending_deletion function
7142       * @param bool $expected The expected result
7143       * @dataProvider provider_course_modules_pending_deletion
7144       */
7145      public function test_course_modules_pending_deletion(array $modules, int $indextodelete, bool $gradable, bool $expected) {
7146          $this->resetAfterTest();
7147  
7148          // Ensure recyclebin is enabled.
7149          set_config('coursebinenable', true, 'tool_recyclebin');
7150  
7151          // Create course and modules.
7152          $generator = $this->getDataGenerator();
7153          $course = $generator->create_course();
7154  
7155          $moduleinstances = [];
7156          foreach ($modules as $module) {
7157              $moduleinstances[] = $generator->create_module($module, array('course' => $course->id));
7158          }
7159  
7160          course_delete_module($moduleinstances[$indextodelete]->cmid, true); // Try to delete the instance asynchronously.
7161          $this->assertEquals($expected, course_modules_pending_deletion($course->id, $gradable));
7162      }
7163  
7164      /**
7165       * Tests for the course_request::can_request
7166       */
7167      public function test_can_request_course() {
7168          global $CFG, $DB;
7169          $this->resetAfterTest();
7170  
7171          $user = $this->getDataGenerator()->create_user();
7172          $cat1 = $CFG->defaultrequestcategory;
7173          $cat2 = $this->getDataGenerator()->create_category()->id;
7174          $cat3 = $this->getDataGenerator()->create_category()->id;
7175          $context1 = context_coursecat::instance($cat1);
7176          $context2 = context_coursecat::instance($cat2);
7177          $context3 = context_coursecat::instance($cat3);
7178          $this->setUser($user);
7179  
7180          // By default users don't have capability to request courses.
7181          $this->assertFalse(course_request::can_request(context_system::instance()));
7182          $this->assertFalse(course_request::can_request($context1));
7183          $this->assertFalse(course_request::can_request($context2));
7184          $this->assertFalse(course_request::can_request($context3));
7185  
7186          // Allow for the 'user' role the capability to request courses.
7187          $userroleid = $DB->get_field('role', 'id', ['shortname' => 'user']);
7188          assign_capability('moodle/course:request', CAP_ALLOW, $userroleid,
7189              context_system::instance()->id);
7190          accesslib_clear_all_caches_for_unit_testing();
7191  
7192          // Lock category selection.
7193          $CFG->lockrequestcategory = 1;
7194  
7195          // Now user can only request course in the default category or in system context.
7196          $this->assertTrue(course_request::can_request(context_system::instance()));
7197          $this->assertTrue(course_request::can_request($context1));
7198          $this->assertFalse(course_request::can_request($context2));
7199          $this->assertFalse(course_request::can_request($context3));
7200  
7201          // Enable category selection. User can request course anywhere.
7202          $CFG->lockrequestcategory = 0;
7203          $this->assertTrue(course_request::can_request(context_system::instance()));
7204          $this->assertTrue(course_request::can_request($context1));
7205          $this->assertTrue(course_request::can_request($context2));
7206          $this->assertTrue(course_request::can_request($context3));
7207  
7208          // Remove cap from cat2.
7209          $roleid = create_role('Test role', 'testrole', 'Test role description');
7210          assign_capability('moodle/course:request', CAP_PROHIBIT, $roleid,
7211              $context2->id, true);
7212          role_assign($roleid, $user->id, $context2->id);
7213          accesslib_clear_all_caches_for_unit_testing();
7214  
7215          $this->assertTrue(course_request::can_request(context_system::instance()));
7216          $this->assertTrue(course_request::can_request($context1));
7217          $this->assertFalse(course_request::can_request($context2));
7218          $this->assertTrue(course_request::can_request($context3));
7219  
7220          // Disable course request functionality.
7221          $CFG->enablecourserequests = false;
7222          $this->assertFalse(course_request::can_request(context_system::instance()));
7223          $this->assertFalse(course_request::can_request($context1));
7224          $this->assertFalse(course_request::can_request($context2));
7225          $this->assertFalse(course_request::can_request($context3));
7226      }
7227  
7228      /**
7229       * Tests for the course_request::can_approve
7230       */
7231      public function test_can_approve_course_request() {
7232          global $CFG;
7233          $this->resetAfterTest();
7234  
7235          $requestor = $this->getDataGenerator()->create_user();
7236          $user = $this->getDataGenerator()->create_user();
7237          $cat1 = $CFG->defaultrequestcategory;
7238          $cat2 = $this->getDataGenerator()->create_category()->id;
7239          $cat3 = $this->getDataGenerator()->create_category()->id;
7240  
7241          // Enable course requests. Default 'user' role has capability to request courses.
7242          $CFG->enablecourserequests = true;
7243          $CFG->lockrequestcategory = 0;
7244          $this->setUser($requestor);
7245          $requestdata = ['summary_editor' => ['text' => '', 'format' => 0], 'name' => 'Req', 'reason' => 'test'];
7246          $request1 = course_request::create((object)($requestdata));
7247          $request2 = course_request::create((object)($requestdata + ['category' => $cat2]));
7248          $request3 = course_request::create((object)($requestdata + ['category' => $cat3]));
7249  
7250          $this->setUser($user);
7251          // Add capability to approve courses.
7252          $roleid = create_role('Test role', 'testrole', 'Test role description');
7253          assign_capability('moodle/site:approvecourse', CAP_ALLOW, $roleid,
7254              context_system::instance()->id, true);
7255          role_assign($roleid, $user->id, context_coursecat::instance($cat2)->id);
7256          accesslib_clear_all_caches_for_unit_testing();
7257  
7258          $this->assertFalse($request1->can_approve());
7259          $this->assertTrue($request2->can_approve());
7260          $this->assertFalse($request3->can_approve());
7261  
7262          // Delete category where course was requested. Now only site-wide manager can approve it.
7263          core_course_category::get($cat2, MUST_EXIST, true)->delete_full(false);
7264          $this->assertFalse($request2->can_approve());
7265  
7266          $this->setAdminUser();
7267          $this->assertTrue($request2->can_approve());
7268      }
7269  
7270      /**
7271       * Test the course allowed module method.
7272       */
7273      public function test_course_allowed_module() {
7274          $this->resetAfterTest();
7275          global $DB;
7276  
7277          $course = $this->getDataGenerator()->create_course();
7278          $teacher = $this->getDataGenerator()->create_and_enrol($course, 'editingteacher');
7279          $manager = $this->getDataGenerator()->create_and_enrol($course, 'manager');
7280  
7281          $teacherrole = $DB->get_record('role', array('shortname' => 'editingteacher'));
7282          assign_capability('mod/assign:addinstance', CAP_PROHIBIT, $teacherrole->id, \context_course::instance($course->id));
7283  
7284          // Global user (teacher) has no permissions in this course.
7285          $this->setUser($teacher);
7286          $this->assertFalse(course_allowed_module($course, 'assign'));
7287  
7288          // Manager has permissions.
7289          $this->assertTrue(course_allowed_module($course, 'assign', $manager));
7290      }
7291  
7292      /**
7293       * Test the {@link average_number_of_participants()} function.
7294       */
7295      public function test_average_number_of_participants() {
7296          global $DB;
7297          $this->resetAfterTest(true);
7298  
7299          $generator = $this->getDataGenerator();
7300          $now = time();
7301  
7302          // If there are no courses, expect zero number of participants per course.
7303          $this->assertEquals(0, average_number_of_participants());
7304  
7305          $c1 = $generator->create_course();
7306          $c2 = $generator->create_course();
7307  
7308          // If there are no users, expect zero number of participants per course.
7309          $this->assertEquals(0, average_number_of_participants());
7310  
7311          $t1 = $generator->create_user(['lastlogin' => $now]);
7312          $s1 = $generator->create_user(['lastlogin' => $now]);
7313          $s2 = $generator->create_user(['lastlogin' => $now - WEEKSECS]);
7314          $s3 = $generator->create_user(['lastlogin' => $now - WEEKSECS]);
7315          $s4 = $generator->create_user(['lastlogin' => $now - YEARSECS]);
7316  
7317          // We have courses, we have users, but no enrolments yet.
7318          $this->assertEquals(0, average_number_of_participants());
7319  
7320          // Front page enrolments are ignored.
7321          $generator->enrol_user($t1->id, SITEID, 'teacher');
7322          $this->assertEquals(0, average_number_of_participants());
7323  
7324          // The teacher enrolled into one of the two courses.
7325          $generator->enrol_user($t1->id, $c1->id, 'editingteacher');
7326          $this->assertEquals(0.5, average_number_of_participants());
7327  
7328          // The teacher enrolled into both courses.
7329          $generator->enrol_user($t1->id, $c2->id, 'editingteacher');
7330          $this->assertEquals(1, average_number_of_participants());
7331  
7332          // Student 1 enrolled in the Course 1 only.
7333          $generator->enrol_user($s1->id, $c1->id, 'student');
7334          $this->assertEquals(1.5, average_number_of_participants());
7335  
7336          // Student 2 enrolled in both courses, but the enrolment in the Course 2 not active yet (enrolment starts in the future).
7337          $generator->enrol_user($s2->id, $c1->id, 'student');
7338          $generator->enrol_user($s2->id, $c2->id, 'student', 'manual', $now + WEEKSECS);
7339          $this->assertEquals(2.5, average_number_of_participants());
7340          $this->assertEquals(2, average_number_of_participants(true));
7341  
7342          // Student 3 enrolled in the Course 1, but the enrolment already expired.
7343          $generator->enrol_user($s3->id, $c1->id, 'student', 'manual', 0, $now - YEARSECS);
7344          $this->assertEquals(3, average_number_of_participants());
7345          $this->assertEquals(2, average_number_of_participants(true));
7346  
7347          // Student 4 enrolled in both courses, but the enrolment has been suspended.
7348          $generator->enrol_user($s4->id, $c1->id, 'student', 'manual', 0, 0, ENROL_USER_SUSPENDED);
7349          $generator->enrol_user($s4->id, $c2->id, 'student', 'manual', $now - DAYSECS, $now + YEARSECS, ENROL_USER_SUSPENDED);
7350          $this->assertEquals(4, average_number_of_participants());
7351          $this->assertEquals(2, average_number_of_participants(true));
7352  
7353          // Consider only t1 and s1 who logged in recently.
7354          $this->assertEquals(1.5, average_number_of_participants(false, $now - DAYSECS));
7355  
7356          // Consider only t1, s1, s2 and s3 who logged in in recent weeks.
7357          $this->assertEquals(3, average_number_of_participants(false, $now - 4 * WEEKSECS));
7358  
7359          // Hidden courses are excluded from stats.
7360          $DB->set_field('course', 'visible', 0, ['id' => $c1->id]);
7361          $this->assertEquals(3, average_number_of_participants());
7362          $this->assertEquals(1, average_number_of_participants(true));
7363      }
7364  
7365      /**
7366       * Test the set_downloadcontent() function.
7367       */
7368      public function test_set_downloadcontent() {
7369          $this->resetAfterTest();
7370  
7371          $generator = $this->getDataGenerator();
7372          $course = $generator->create_course();
7373          $page = $generator->create_module('page', ['course' => $course]);
7374  
7375          // Test the module 'downloadcontent' field is set to enabled.
7376          set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_ENABLED);
7377          $modinfo = get_fast_modinfo($course)->get_cm($page->cmid);
7378          $this->assertEquals(DOWNLOAD_COURSE_CONTENT_ENABLED, $modinfo->downloadcontent);
7379  
7380          // Now let's test the 'downloadcontent' value is updated to disabled.
7381          set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_DISABLED);
7382          $modinfo = get_fast_modinfo($course)->get_cm($page->cmid);
7383          $this->assertEquals(DOWNLOAD_COURSE_CONTENT_DISABLED, $modinfo->downloadcontent);
7384  
7385          // Nothing to update, the download course content value is the same, it should return false.
7386          $this->assertFalse(set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_DISABLED));
7387  
7388          // The download course content value has changed, it should return true in this case.
7389          $this->assertTrue(set_downloadcontent($page->cmid, DOWNLOAD_COURSE_CONTENT_ENABLED));
7390      }
7391  }