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  /**
  18   * H5P activity manager class
  19   *
  20   * @package    mod_h5pactivity
  21   * @since      Moodle 3.9
  22   * @copyright  2020 Ferran Recio <ferran@moodle.com>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace mod_h5pactivity\local;
  27  
  28  use mod_h5pactivity\local\report\participants;
  29  use mod_h5pactivity\local\report\attempts;
  30  use mod_h5pactivity\local\report\results;
  31  use context_module;
  32  use cm_info;
  33  use moodle_recordset;
  34  use core_user;
  35  use stdClass;
  36  use core\dml\sql_join;
  37  use mod_h5pactivity\event\course_module_viewed;
  38  
  39  /**
  40   * Class manager for H5P activity
  41   *
  42   * @package    mod_h5pactivity
  43   * @since      Moodle 3.9
  44   * @copyright  2020 Ferran Recio <ferran@moodle.com>
  45   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  46   */
  47  class manager {
  48  
  49      /** No automathic grading using attempt results. */
  50      const GRADEMANUAL = 0;
  51  
  52      /** Use highest attempt results for grading. */
  53      const GRADEHIGHESTATTEMPT = 1;
  54  
  55      /** Use average attempt results for grading. */
  56      const GRADEAVERAGEATTEMPT = 2;
  57  
  58      /** Use last attempt results for grading. */
  59      const GRADELASTATTEMPT = 3;
  60  
  61      /** Use first attempt results for grading. */
  62      const GRADEFIRSTATTEMPT = 4;
  63  
  64      /** Participants cannot review their own attempts. */
  65      const REVIEWNONE = 0;
  66  
  67      /** Participants can review their own attempts when have one attempt completed. */
  68      const REVIEWCOMPLETION = 1;
  69  
  70      /** @var stdClass course_module record. */
  71      private $instance;
  72  
  73      /** @var context_module the current context. */
  74      private $context;
  75  
  76      /** @var cm_info course_modules record. */
  77      private $coursemodule;
  78  
  79      /**
  80       * Class contructor.
  81       *
  82       * @param cm_info $coursemodule course module info object
  83       * @param stdClass $instance H5Pactivity instance object.
  84       */
  85      public function __construct(cm_info $coursemodule, stdClass $instance) {
  86          $this->coursemodule = $coursemodule;
  87          $this->instance = $instance;
  88          $this->context = context_module::instance($coursemodule->id);
  89          $this->instance->cmidnumber = $coursemodule->idnumber;
  90      }
  91  
  92      /**
  93       * Create a manager instance from an instance record.
  94       *
  95       * @param stdClass $instance a h5pactivity record
  96       * @return manager
  97       */
  98      public static function create_from_instance(stdClass $instance): self {
  99          $coursemodule = get_coursemodule_from_instance('h5pactivity', $instance->id);
 100          // Ensure that $this->coursemodule is a cm_info object.
 101          $coursemodule = cm_info::create($coursemodule);
 102          return new self($coursemodule, $instance);
 103      }
 104  
 105      /**
 106       * Create a manager instance from an course_modules record.
 107       *
 108       * @param stdClass|cm_info $coursemodule a h5pactivity record
 109       * @return manager
 110       */
 111      public static function create_from_coursemodule($coursemodule): self {
 112          global $DB;
 113          // Ensure that $this->coursemodule is a cm_info object.
 114          $coursemodule = cm_info::create($coursemodule);
 115          $instance = $DB->get_record('h5pactivity', ['id' => $coursemodule->instance], '*', MUST_EXIST);
 116          return new self($coursemodule, $instance);
 117      }
 118  
 119      /**
 120       * Return the available grading methods.
 121       * @return string[] an array "option value" => "option description"
 122       */
 123      public static function get_grading_methods(): array {
 124          return [
 125              self::GRADEHIGHESTATTEMPT => get_string('grade_highest_attempt', 'mod_h5pactivity'),
 126              self::GRADEAVERAGEATTEMPT => get_string('grade_average_attempt', 'mod_h5pactivity'),
 127              self::GRADELASTATTEMPT => get_string('grade_last_attempt', 'mod_h5pactivity'),
 128              self::GRADEFIRSTATTEMPT => get_string('grade_first_attempt', 'mod_h5pactivity'),
 129              self::GRADEMANUAL => get_string('grade_manual', 'mod_h5pactivity'),
 130          ];
 131      }
 132  
 133      /**
 134       * Return the selected attempt criteria.
 135       * @return string[] an array "grademethod value", "attempt description"
 136       */
 137      public function get_selected_attempt(): array {
 138          $types = [
 139              self::GRADEHIGHESTATTEMPT => get_string('attempt_highest', 'mod_h5pactivity'),
 140              self::GRADEAVERAGEATTEMPT => get_string('attempt_average', 'mod_h5pactivity'),
 141              self::GRADELASTATTEMPT => get_string('attempt_last', 'mod_h5pactivity'),
 142              self::GRADEFIRSTATTEMPT => get_string('attempt_first', 'mod_h5pactivity'),
 143              self::GRADEMANUAL => get_string('attempt_none', 'mod_h5pactivity'),
 144          ];
 145          if ($this->instance->enabletracking) {
 146              $key = $this->instance->grademethod;
 147          } else {
 148              $key = self::GRADEMANUAL;
 149          }
 150          return [$key, $types[$key]];
 151      }
 152  
 153      /**
 154       * Return the available review modes.
 155       *
 156       * @return string[] an array "option value" => "option description"
 157       */
 158      public static function get_review_modes(): array {
 159          return [
 160              self::REVIEWCOMPLETION => get_string('review_on_completion', 'mod_h5pactivity'),
 161              self::REVIEWNONE => get_string('review_none', 'mod_h5pactivity'),
 162          ];
 163      }
 164  
 165      /**
 166       * Check if tracking is enabled in a particular h5pactivity for a specific user.
 167       *
 168       * @param stdClass|null $user user record (default $USER)
 169       * @return bool if tracking is enabled in this activity
 170       */
 171      public function is_tracking_enabled(stdClass $user = null): bool {
 172          global $USER;
 173          if (!$this->instance->enabletracking) {
 174              return false;
 175          }
 176          if (empty($user)) {
 177              $user = $USER;
 178          }
 179          return has_capability('mod/h5pactivity:submit', $this->context, $user, false);
 180      }
 181  
 182      /**
 183       * Check if a user can see the activity attempts list.
 184       *
 185       * @param stdClass|null $user user record (default $USER)
 186       * @return bool if the user can see the attempts link
 187       */
 188      public function can_view_all_attempts(stdClass $user = null): bool {
 189          global $USER;
 190          if (!$this->instance->enabletracking) {
 191              return false;
 192          }
 193          if (empty($user)) {
 194              $user = $USER;
 195          }
 196          return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user);
 197      }
 198  
 199      /**
 200       * Check if a user can see own attempts.
 201       *
 202       * @param stdClass|null $user user record (default $USER)
 203       * @return bool if the user can see the own attempts link
 204       */
 205      public function can_view_own_attempts(stdClass $user = null): bool {
 206          global $USER;
 207          if (!$this->instance->enabletracking) {
 208              return false;
 209          }
 210          if (empty($user)) {
 211              $user = $USER;
 212          }
 213          if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) {
 214              return true;
 215          }
 216          if ($this->instance->reviewmode == self::REVIEWNONE) {
 217              return false;
 218          }
 219          if ($this->instance->reviewmode == self::REVIEWCOMPLETION) {
 220              return true;
 221          }
 222          return false;
 223  
 224      }
 225  
 226      /**
 227       * Return a relation of userid and the valid attempt's scaled score.
 228       *
 229       * The returned elements contain a record
 230       * of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT"
 231       * the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL"
 232       * the method will return null.
 233       *
 234       * @param int $userid a specific userid or 0 for all user attempts.
 235       * @return array|null of userid, scaled value and, if exists, the attempt id
 236       */
 237      public function get_users_scaled_score(int $userid = 0): ?array {
 238          global $DB;
 239  
 240          $scaled = [];
 241          if (!$this->instance->enabletracking) {
 242              return null;
 243          }
 244  
 245          if ($this->instance->grademethod == self::GRADEMANUAL) {
 246              return null;
 247          }
 248  
 249          $sql = '';
 250  
 251          // General filter.
 252          $where = 'a.h5pactivityid = :h5pactivityid';
 253          $params['h5pactivityid'] = $this->instance->id;
 254  
 255          if ($userid) {
 256              $where .= ' AND a.userid = :userid';
 257              $params['userid'] = $userid;
 258          }
 259  
 260          // Average grading needs aggregation query.
 261          if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) {
 262              $sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified
 263                        FROM {h5pactivity_attempts} a
 264                       WHERE $where AND a.completion = 1
 265                    GROUP BY a.userid";
 266          }
 267  
 268          if (empty($sql)) {
 269              // Decide which attempt is used for the calculation.
 270              $condition = [
 271                  self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled",
 272                  self::GRADELASTATTEMPT => "a.attempt < b.attempt",
 273                  self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt",
 274              ];
 275              $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT];
 276  
 277              $sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified
 278                        FROM {h5pactivity_attempts} a
 279                   LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid
 280                             AND a.userid = b.userid AND b.completion = 1
 281                             AND $join
 282                       WHERE $where AND b.id IS NULL AND a.completion = 1
 283                    GROUP BY a.userid, a.scaled";
 284          }
 285  
 286          return $DB->get_records_sql($sql, $params);
 287      }
 288  
 289      /**
 290       * Count the activity completed attempts.
 291       *
 292       * If no user is provided the method will count all active users attempts.
 293       * Check get_active_users_join PHPdoc to a more detailed description of "active users".
 294       *
 295       * @param int|null $userid optional user id (default null)
 296       * @return int the total amount of attempts
 297       */
 298      public function count_attempts(int $userid = null): int {
 299          global $DB;
 300  
 301          // Counting records is enough for one user.
 302          if ($userid) {
 303              $params['userid'] = $userid;
 304              $params = [
 305                  'h5pactivityid' => $this->instance->id,
 306                  'userid' => $userid,
 307                  'completion' => 1,
 308              ];
 309              return $DB->count_records('h5pactivity_attempts', $params);
 310          }
 311  
 312          $usersjoin = $this->get_active_users_join();
 313  
 314          // Final SQL.
 315          return $DB->count_records_sql(
 316              "SELECT COUNT(*)
 317                 FROM {user} u $usersjoin->joins
 318                WHERE $usersjoin->wheres",
 319              array_merge($usersjoin->params)
 320          );
 321      }
 322  
 323      /**
 324       * Return the join to collect all activity active users.
 325       *
 326       * The concept of active user is relative to the activity permissions. All users with
 327       * "mod/h5pactivity:view" are potential users but those with "mod/h5pactivity:reviewattempts"
 328       * are evaluators and they don't count as valid submitters.
 329       *
 330       * Note that, in general, the active list has the same effect as checking for "mod/h5pactivity:submit"
 331       * but submit capability cannot be used because is a write capability and does not apply to frozen contexts.
 332       *
 333       * @since Moodle 3.11
 334       * @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
 335       * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
 336       * @return sql_join the active users attempts join
 337       */
 338      public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join {
 339  
 340          // Only valid users counts. By default, all users with submit capability are considered potential ones.
 341          $context = $this->get_context();
 342          $coursemodule = $this->get_coursemodule();
 343  
 344          // Ensure user can view users from all groups.
 345          if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS
 346                  && !has_capability('moodle/site:accessallgroups', $context)) {
 347  
 348              return new sql_join('', '1=2', [], true);
 349          }
 350  
 351          // We want to present all potential users.
 352          $capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup);
 353  
 354          if ($capjoin->cannotmatchanyrows) {
 355              return $capjoin;
 356          }
 357  
 358          // But excluding all reviewattempts users converting a capabilities join into left join.
 359          $reviewersjoin = get_with_capability_join($context, 'mod/h5pactivity:reviewattempts', 'u.id');
 360  
 361          $capjoin = new sql_join(
 362              $capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins),
 363              $capjoin->wheres . " AND reviewer.userid IS NULL",
 364              $capjoin->params
 365          );
 366  
 367          if ($allpotentialusers) {
 368              return $capjoin;
 369          }
 370  
 371          // Add attempts join.
 372          $where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion";
 373          $params = [
 374              'h5pactivityid' => $this->instance->id,
 375              'completion' => 1,
 376          ];
 377  
 378          return new sql_join(
 379              $capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id",
 380              $capjoin->wheres . " AND $where",
 381              array_merge($capjoin->params, $params)
 382          );
 383      }
 384  
 385      /**
 386       * Return an array of all users and it's total attempts.
 387       *
 388       * Note: this funciton only returns the list of users with attempts,
 389       * it does not check all participants.
 390       *
 391       * @return array indexed count userid => total number of attempts
 392       */
 393      public function count_users_attempts(): array {
 394          global $DB;
 395          $params = [
 396              'h5pactivityid' => $this->instance->id,
 397          ];
 398          $sql = "SELECT userid, count(*)
 399                    FROM {h5pactivity_attempts}
 400                   WHERE h5pactivityid = :h5pactivityid
 401                   GROUP BY userid";
 402          return $DB->get_records_sql_menu($sql, $params);
 403      }
 404  
 405      /**
 406       * Return the current context.
 407       *
 408       * @return context_module
 409       */
 410      public function get_context(): context_module {
 411          return $this->context;
 412      }
 413  
 414      /**
 415       * Return the current instance.
 416       *
 417       * @return stdClass the instance record
 418       */
 419      public function get_instance(): stdClass {
 420          return $this->instance;
 421      }
 422  
 423      /**
 424       * Return the current cm_info.
 425       *
 426       * @return cm_info the course module
 427       */
 428      public function get_coursemodule(): cm_info {
 429          return $this->coursemodule;
 430      }
 431  
 432      /**
 433       * Return the specific grader object for this activity.
 434       *
 435       * @return grader
 436       */
 437      public function get_grader(): grader {
 438          $idnumber = $this->coursemodule->idnumber ?? '';
 439          return new grader($this->instance, $idnumber);
 440      }
 441  
 442      /**
 443       * Return the suitable report to show the attempts.
 444       *
 445       * This method controls the access to the different reports
 446       * the activity have.
 447       *
 448       * @param int $userid an opional userid to show
 449       * @param int $attemptid an optional $attemptid to show
 450       * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
 451       * @return report|null available report (or null if no report available)
 452       */
 453      public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report {
 454          global $USER, $CFG;
 455  
 456          require_once("{$CFG->dirroot}/user/lib.php");
 457  
 458          // If tracking is disabled, no reports are available.
 459          if (!$this->instance->enabletracking) {
 460              return null;
 461          }
 462  
 463          $attempt = null;
 464          if ($attemptid) {
 465              $attempt = $this->get_attempt($attemptid);
 466              if (!$attempt) {
 467                  return null;
 468              }
 469              // If we have and attempt we can ignore the provided $userid.
 470              $userid = $attempt->get_userid();
 471          }
 472  
 473          if ($this->can_view_all_attempts()) {
 474              $user = core_user::get_user($userid);
 475  
 476              // Ensure user can view the attempt of specific userid, respecting access checks.
 477              if ($user && $user->id != $USER->id) {
 478                  $course = get_course($this->coursemodule->course);
 479                  if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) {
 480                      return null;
 481                  }
 482              }
 483          } else if ($this->can_view_own_attempts()) {
 484              $user = core_user::get_user($USER->id);
 485              if ($userid && $user->id != $userid) {
 486                  return null;
 487              }
 488          } else {
 489              return null;
 490          }
 491  
 492          // Only enrolled users has reports.
 493          if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) {
 494              return null;
 495          }
 496  
 497          // Create the proper report.
 498          if ($user && $attempt) {
 499              return new results($this, $user, $attempt);
 500          } else if ($user) {
 501              return new attempts($this, $user);
 502          }
 503          return new participants($this, $currentgroup);
 504      }
 505  
 506      /**
 507       * Return a single attempt.
 508       *
 509       * @param int $attemptid the attempt id
 510       * @return attempt
 511       */
 512      public function get_attempt(int $attemptid): ?attempt {
 513          global $DB;
 514          $record = $DB->get_record('h5pactivity_attempts', [
 515              'id' => $attemptid,
 516              'h5pactivityid' => $this->instance->id,
 517          ]);
 518          if (!$record) {
 519              return null;
 520          }
 521          return new attempt($record);
 522      }
 523  
 524      /**
 525       * Return an array of all user attempts (including incompleted)
 526       *
 527       * @param int $userid the user id
 528       * @return attempt[]
 529       */
 530      public function get_user_attempts(int $userid): array {
 531          global $DB;
 532          $records = $DB->get_records(
 533              'h5pactivity_attempts',
 534              ['userid' => $userid, 'h5pactivityid' => $this->instance->id],
 535              'id ASC'
 536          );
 537          if (!$records) {
 538              return [];
 539          }
 540          $result = [];
 541          foreach ($records as $record) {
 542              $result[] = new attempt($record);
 543          }
 544          return $result;
 545      }
 546  
 547      /**
 548       * Trigger module viewed event and set the module viewed for completion.
 549       *
 550       * @param stdClass $course course object
 551       * @return void
 552       */
 553      public function set_module_viewed(stdClass $course): void {
 554          global $CFG;
 555          require_once($CFG->libdir . '/completionlib.php');
 556  
 557          // Trigger module viewed event.
 558          $event = course_module_viewed::create([
 559              'objectid' => $this->instance->id,
 560              'context' => $this->context
 561          ]);
 562          $event->add_record_snapshot('course', $course);
 563          $event->add_record_snapshot('course_modules', $this->coursemodule);
 564          $event->add_record_snapshot('h5pactivity', $this->instance);
 565          $event->trigger();
 566  
 567          // Completion.
 568          $completion = new \completion_info($course);
 569          $completion->set_module_viewed($this->coursemodule);
 570      }
 571  }