Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 401 and 403] [Versions 402 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  declare(strict_types=1);
  18  
  19  namespace core_course\reportbuilder\datasource;
  20  
  21  use completion_completion;
  22  use completion_criteria_self;
  23  use core_reportbuilder\local\filters\boolean_select;
  24  use core_reportbuilder\local\filters\date;
  25  use core_reportbuilder\local\filters\duration;
  26  use core_reportbuilder\local\filters\select;
  27  use core_reportbuilder\local\filters\text;
  28  use core_reportbuilder_generator;
  29  use core_reportbuilder_testcase;
  30  use core_user;
  31  use grade_item;
  32  
  33  defined('MOODLE_INTERNAL') || die();
  34  
  35  global $CFG;
  36  require_once("{$CFG->dirroot}/reportbuilder/tests/helpers.php");
  37  
  38  /**
  39   * Course participants datasource tests
  40   *
  41   * @package     core_course
  42   * @covers      \core_course\reportbuilder\datasource\participants
  43   * @copyright   2022 David Matamoros <davidmc@moodle.com>
  44   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  45   */
  46  class participants_test extends core_reportbuilder_testcase {
  47  
  48      /**
  49       * Load required test libraries
  50       */
  51      public static function setUpBeforeClass(): void {
  52          global $CFG;
  53  
  54          require_once("{$CFG->libdir}/gradelib.php");
  55          require_once("{$CFG->dirroot}/completion/criteria/completion_criteria_self.php");
  56      }
  57  
  58      /**
  59       * Test default datasource
  60       */
  61      public function test_datasource_default(): void {
  62          global $DB;
  63  
  64          $this->resetAfterTest();
  65  
  66          // Course one, two manually enrolled users.
  67          $courseone = $this->getDataGenerator()->create_course(['fullname' => 'Zebras']);
  68          $userone = $this->getDataGenerator()->create_and_enrol($courseone, 'student', ['firstname' => 'Zoe']);
  69          $usertwo = $this->getDataGenerator()->create_and_enrol($courseone, 'student', ['firstname' => 'Amy']);
  70  
  71          // Course two, two self enrolled users (one inactive).
  72          $coursetwo = $this->getDataGenerator()->create_course(['fullname' => 'Aardvarks']);
  73  
  74          $enrol = $DB->get_record('enrol', ['courseid' => $coursetwo->id, 'enrol' => 'self']);
  75          enrol_get_plugin($enrol->enrol)->update_status($enrol, ENROL_INSTANCE_ENABLED);
  76  
  77          $this->getDataGenerator()->enrol_user($userone->id, $coursetwo->id, null, 'self');
  78          $this->getDataGenerator()->enrol_user($usertwo->id, $coursetwo->id, null, 'self', 0, 0, ENROL_USER_SUSPENDED);
  79  
  80          /** @var core_reportbuilder_generator $generator */
  81          $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
  82          $report = $generator->create_report(['name' => 'Participants', 'source' => participants::class, 'default' => 1]);
  83  
  84          $content = $this->get_custom_report_content($report->get('id'));
  85  
  86          // Default columns are course, user, method. Sorted by each.
  87          $courseoneurl = course_get_url($courseone);
  88          $coursetwourl = course_get_url($coursetwo);
  89  
  90          $useroneurl = core_user::get_profile_url($userone);
  91          $usertwourl = core_user::get_profile_url($usertwo);
  92  
  93          $this->assertEquals([
  94              ["<a href=\"{$coursetwourl}\">{$coursetwo->fullname}</a>",
  95                  "<a href=\"{$useroneurl}\">" . fullname($userone) . "</a>", 'Self enrolment (Student)'],
  96              ["<a href=\"{$courseoneurl}\">{$courseone->fullname}</a>",
  97                  "<a href=\"{$usertwourl}\">" . fullname($usertwo) . "</a>", 'Manual enrolments'],
  98              ["<a href=\"{$courseoneurl}\">{$courseone->fullname}</a>",
  99                  "<a href=\"{$useroneurl}\">" . fullname($userone) . "</a>", 'Manual enrolments'],
 100          ], array_map('array_values', $content));
 101      }
 102  
 103      /**
 104       * Test datasource columns that aren't added by default
 105       */
 106      public function test_datasource_non_default_columns(): void {
 107          global $DB;
 108          $this->resetAfterTest();
 109  
 110          $timestart = time() - DAYSECS;
 111          $timeend = $timestart + 3 * DAYSECS;
 112          $timecompleted = $timestart + 2 * DAYSECS;
 113          $timelastaccess = time() + 4 * DAYSECS;
 114  
 115          $category = $this->getDataGenerator()->create_category(['name' => 'Music']);
 116          $course = $this->getDataGenerator()->create_course([
 117              'category' => $category->id,
 118              'fullname' => 'All about Lionel at the work place',
 119              'enablecompletion' => true,
 120              'startdate' => $timestart,
 121              'enddate' => $timeend,
 122          ]);
 123  
 124          $user1 = self::getDataGenerator()->create_user();
 125          $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student',
 126              'manual', $timestart, $timeend, ENROL_USER_ACTIVE);
 127  
 128          // Add them to a group.
 129          $group = self::getDataGenerator()->create_group(['courseid' => $course->id]);
 130          self::getDataGenerator()->create_group_member(['groupid' => $group->id, 'userid' => $user1->id]);
 131  
 132          // Create self completion, mark as complete for the user.
 133          $criteriaconfig = (object) ['id' => $course->id, 'criteria_self' => true];
 134          (new completion_criteria_self())->update_config($criteriaconfig);
 135  
 136          $ccompletion = new completion_completion(['course' => $course->id, 'userid' => $user1->id]);
 137          $ccompletion->mark_enrolled($timestart);
 138          $ccompletion->mark_complete($timecompleted);
 139  
 140          // Update final grade for the user.
 141          $courseitem = grade_item::fetch_course_item($course->id);
 142          $courseitem->update_final_grade($user1->id, 42.5);
 143  
 144          // Set some last access value for the user in the course.
 145          $DB->insert_record('user_lastaccess',
 146              ['userid' => $user1->id, 'courseid' => $course->id, 'timeaccess' => $timelastaccess]);
 147  
 148          /** @var core_reportbuilder_generator $generator */
 149          $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
 150          $report = $generator->create_report(['name' => 'Participants', 'source' => participants::class, 'default' => false]);
 151  
 152          $generator->create_column(['reportid' => $report->get('id'),
 153              'uniqueidentifier' => 'course:fullname']);
 154          $generator->create_column(['reportid' => $report->get('id'),
 155              'uniqueidentifier' => 'course_category:name']);
 156          $generator->create_column(['reportid' => $report->get('id'),
 157              'uniqueidentifier' => 'user:fullname']);
 158  
 159          // Enrol entity (report ordering by enrolment name).
 160          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:name', 'sortenabled' => 1]);
 161          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:plugin']);
 162          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:enabled']);
 163          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:period']);
 164          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:startdate']);
 165          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:enddate']);
 166  
 167          // Role entity.
 168          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'role:name']);
 169          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'role:shortname']);
 170          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'role:description']);
 171  
 172          $generator->create_column(['reportid' => $report->get('id'),
 173              'uniqueidentifier' => 'group:name']);
 174          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'completion:criteria']);
 175          $generator->create_column(['reportid' => $report->get('id'),
 176              'uniqueidentifier' => 'completion:completed']);
 177          $generator->create_column(['reportid' => $report->get('id'),
 178              'uniqueidentifier' => 'access:timeaccess']);
 179          $generator->create_column(['reportid' => $report->get('id'),
 180              'uniqueidentifier' => 'completion:progresspercent']);
 181          $generator->create_column(['reportid' => $report->get('id'),
 182              'uniqueidentifier' => 'completion:timeenrolled']);
 183          $generator->create_column(['reportid' => $report->get('id'),
 184              'uniqueidentifier' => 'completion:timestarted']);
 185          $generator->create_column(['reportid' => $report->get('id'),
 186              'uniqueidentifier' => 'completion:timecompleted']);
 187          $generator->create_column(['reportid' => $report->get('id'),
 188              'uniqueidentifier' => 'completion:reaggregate']);
 189          $generator->create_column(['reportid' => $report->get('id'),
 190              'uniqueidentifier' => 'completion:dayscourse']);
 191          $generator->create_column(['reportid' => $report->get('id'),
 192              'uniqueidentifier' => 'completion:daysuntilcompletion']);
 193          $generator->create_column(['reportid' => $report->get('id'),
 194              'uniqueidentifier' => 'completion:grade']);
 195  
 196          // Add filter to the report.
 197          $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:plugin']);
 198  
 199          $content = $this->get_custom_report_content($report->get('id'));
 200  
 201          // It should get 3 records (manual enrolment, self and guest).
 202          $this->assertCount(3, $content);
 203  
 204          // Filter by Manual enrolment method.
 205          $content = $this->get_custom_report_content($report->get('id'), 30, [
 206              'enrol:plugin_operator' => select::EQUAL_TO,
 207              'enrol:plugin_value' => 'manual',
 208          ]);
 209  
 210          $this->assertCount(1, $content);
 211  
 212          $this->assertEquals([
 213              'All about Lionel at the work place', // Course name.
 214              'Music', // Course category name.
 215              fullname($user1), // User fullname.
 216              'Manual enrolments', // Enrolment method.
 217              'Manual enrolments', // Enrolment plugin.
 218              'Yes', // Enrolment enabled.
 219              '', // Enrolment period.
 220              '', // Enrolment start date.
 221              '', // Enrolment end date.
 222              'Student', // Role name.
 223              'student', // Role shortname.
 224              'Students generally have fewer privileges within a course.', // Role description.
 225              $group->name, // Group name.
 226              "All criteria below are required<ul>\n<li>Self completion: Self completion</li>\n</ul>", // Completion criteria.
 227              'Yes', // Course completed.
 228              userdate($timelastaccess), // Time last access.
 229              '100.0%', // Progress percentage.
 230              userdate($timestart), // Time enrolled.
 231              '', // Time started.
 232              userdate($timecompleted), // Time completed.
 233              '', // Reagreggate.
 234              2, // Days taking course.
 235              2, // Days until completion.
 236              '42.50', // Grade.
 237          ], array_values($content[0]));
 238      }
 239  
 240      /**
 241       * Data provider for {@see test_datasource_filters}
 242       *
 243       * @return array
 244       */
 245      public function datasource_filters_provider(): array {
 246          global $DB;
 247  
 248          return [
 249              [
 250                  'enrolment:status',
 251                  [
 252                      'enrolment:status_operator' => select::EQUAL_TO,
 253                      'enrolment:status_value' => 1,
 254                  ],
 255                  ['Luna'],
 256              ],
 257              [
 258                  'enrolment:timecreated',
 259                  [
 260                      'enrolment:timecreated_operator' => date::DATE_CURRENT,
 261                      'enrolment:timecreated_unit' => date::DATE_UNIT_DAY,
 262                  ],
 263                  ['Kira'],
 264              ],
 265              [
 266                  'enrolment:timestarted',
 267                  [
 268                      'enrolment:timestarted_operator' => date::DATE_CURRENT,
 269                      'enrolment:timecreated_unit' => date::DATE_UNIT_DAY,
 270                  ],
 271                  ['Luna'],
 272              ],
 273              [
 274                  'enrolment:timeended',
 275                  [
 276                      'enrolment:timeended_operator' => date::DATE_CURRENT,
 277                      'enrolment:timeended_unit' => date::DATE_UNIT_DAY,
 278                  ],
 279                  ['Luna'],
 280              ],
 281              [
 282                  'enrol:enabled',
 283                  [
 284                      'completion:enabled_operator' => boolean_select::CHECKED,
 285                  ],
 286                  ['Lionel', 'Kira', 'Luna'],
 287              ],
 288              [
 289                  'enrol:period',
 290                  [
 291                      'enrol:period_operator' => duration::DURATION_MAXIMUM,
 292                      'enrol:period_unit' => MINSECS,
 293                      'enrol:period_value' => 2,
 294                  ],
 295                  ['Lionel', 'Kira', 'Luna'],
 296              ],
 297              [
 298                  'enrol:startdate',
 299                  [
 300                      'enrol:startdate_operator' => date::DATE_EMPTY,
 301                  ],
 302                  ['Lionel', 'Kira', 'Luna'],
 303              ],
 304              [
 305                  'enrol:enddate',
 306                  [
 307                      'enrol:enddate_operator' => date::DATE_EMPTY,
 308                  ],
 309                  ['Lionel', 'Kira', 'Luna'],
 310              ],
 311              [
 312                  'enrol:customname',
 313                  [
 314                      'enrol:customname_operator' => text::IS_EMPTY,
 315                  ],
 316                  ['Luna', 'Kira', 'Lionel'],
 317              ],
 318              [
 319                  'enrol:customname',
 320                  [
 321                      'enrol:customname_operator' => text::IS_EQUAL_TO,
 322                      'enrol:customname_value' => 'All night long'
 323                  ],
 324                  [],
 325              ],
 326              [
 327                  'role:name',
 328                  [
 329                      'role:name_operator' => select::EQUAL_TO,
 330                      'role:name_value' => $DB->get_field('role', 'id', ['shortname' => 'editingteacher']),
 331                  ],
 332                  ['Luna'],
 333              ],
 334              [
 335                  'completion:completed',
 336                  [
 337                      'completion:completed_operator' => boolean_select::CHECKED,
 338                  ],
 339                  ['Lionel'],
 340              ],
 341              [
 342                  'completion:timecompleted',
 343                  [
 344                      'completion:timecompleted_operator' => date::DATE_NOT_EMPTY,
 345                  ],
 346                  ['Lionel'],
 347              ],
 348              [
 349                  'completion:timeenrolled',
 350                  [
 351                      'completion:timeenrolled_operator' => date::DATE_NOT_EMPTY,
 352                  ],
 353                  ['Lionel'],
 354              ],
 355              [
 356                  'completion:timestarted',
 357                  [
 358                      'completion:timestarted_operator' => date::DATE_NOT_EMPTY,
 359                  ],
 360                  ['Lionel'],
 361              ],
 362              [
 363                  'completion:reaggregate',
 364                  [
 365                      'completion:reaggregate_operator' => date::DATE_NOT_EMPTY,
 366                  ],
 367                  ['Lionel'],
 368              ],
 369          ];
 370      }
 371  
 372      /**
 373       * Test getting filter SQL
 374       *
 375       * @param string $filter
 376       * @param array $filtervalues
 377       * @param string[] $expected
 378       *
 379       * @dataProvider datasource_filters_provider
 380       */
 381      public function test_datasource_filters(string $filter, array $filtervalues, array $expected): void {
 382          global $DB;
 383          $this->resetAfterTest();
 384  
 385          $timestart = time() - DAYSECS;
 386          $timeend = $timestart + 3 * DAYSECS;
 387          $timecompleted = $timestart + 2 * DAYSECS;
 388          $timelastaccess = time() + 4 * DAYSECS;
 389  
 390          $category = $this->getDataGenerator()->create_category(['name' => 'Music']);
 391          $course = $this->getDataGenerator()->create_course([
 392              'category' => $category->id,
 393              'fullname' => 'All about Lionel at the work place',
 394              'enablecompletion' => true,
 395              'startdate' => $timestart,
 396              'enddate' => $timeend,
 397          ]);
 398  
 399          $user1 = self::getDataGenerator()->create_user(['firstname' => 'Lionel']);
 400          $user2 = self::getDataGenerator()->create_user(['firstname' => 'Kira']);
 401          $user3 = self::getDataGenerator()->create_user(['firstname' => 'Luna']);
 402  
 403          $this->getDataGenerator()->enrol_user($user1->id, $course->id, 'student',
 404              'manual', $timestart - 8 * DAYSECS, $timeend, ENROL_USER_ACTIVE);
 405          $this->getDataGenerator()->enrol_user($user2->id, $course->id, 'student',
 406              'manual', $timestart, $timeend, ENROL_USER_ACTIVE);
 407          $this->getDataGenerator()->enrol_user($user3->id, $course->id, 'editingteacher',
 408              'manual', time(), time(), ENROL_USER_SUSPENDED);
 409  
 410          // Mark course as completed for the user.
 411          $ccompletion = new completion_completion(array('course' => $course->id, 'userid' => $user1->id));
 412          $ccompletion->mark_enrolled($timestart);
 413          $ccompletion->mark_inprogress($timestart);
 414          $ccompletion->mark_complete($timecompleted);
 415  
 416          // Set some last access value for the user in the course.
 417          $DB->insert_record('user_lastaccess',
 418              ['userid' => $user1->id, 'courseid' => $course->id, 'timeaccess' => $timelastaccess]);
 419  
 420          /** @var core_reportbuilder_generator $generator */
 421          $generator = $this->getDataGenerator()->get_plugin_generator('core_reportbuilder');
 422          $report = $generator->create_report(['name' => 'Participants', 'source' => participants::class, 'default' => false]);
 423  
 424          // Add user firstname column to the report.
 425          $generator->create_column(['reportid' => $report->get('id'), 'uniqueidentifier' => 'user:firstname']);
 426  
 427          $DB->set_field('user_enrolments', 'timecreated', 0, ['userid' => $user1->id]);
 428          $DB->set_field('user_enrolments', 'timecreated', 0, ['userid' => $user3->id]);
 429  
 430          // Add filters to the report.
 431          $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => 'enrol:plugin']);
 432          $generator->create_filter(['reportid' => $report->get('id'), 'uniqueidentifier' => $filter]);
 433  
 434          // Apply filters.
 435          $filtermanual = ['enrol:plugin_operator' => select::EQUAL_TO, 'enrol:plugin_value' => 'manual'];
 436          $content = $this->get_custom_report_content($report->get('id'), 30, $filtermanual + $filtervalues);
 437  
 438          $this->assertEqualsCanonicalizing($expected, array_column($content, 'c0_firstname'));
 439      }
 440  
 441      /**
 442       * Stress test datasource
 443       *
 444       * In order to execute this test PHPUNIT_LONGTEST should be defined as true in phpunit.xml or directly in config.php
 445       */
 446      public function test_stress_datasource(): void {
 447          if (!PHPUNIT_LONGTEST) {
 448              $this->markTestSkipped('PHPUNIT_LONGTEST is not defined');
 449          }
 450  
 451          $this->resetAfterTest();
 452  
 453          $course = $this->getDataGenerator()->create_course();
 454          $this->getDataGenerator()->create_and_enrol($course);
 455  
 456          $this->datasource_stress_test_columns(participants::class);
 457          $this->datasource_stress_test_columns_aggregation(participants::class);
 458          $this->datasource_stress_test_conditions(participants::class, 'course:idnumber');
 459      }
 460  }