Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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