Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401]

   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          if ($reviewersjoin->cannotmatchanyrows) {
 361              return $capjoin;
 362          }
 363  
 364          $capjoin = new sql_join(
 365              $capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins),
 366              $capjoin->wheres . " AND reviewer.userid IS NULL",
 367              $capjoin->params
 368          );
 369  
 370          if ($allpotentialusers) {
 371              return $capjoin;
 372          }
 373  
 374          // Add attempts join.
 375          $where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion";
 376          $params = [
 377              'h5pactivityid' => $this->instance->id,
 378              'completion' => 1,
 379          ];
 380  
 381          return new sql_join(
 382              $capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id",
 383              $capjoin->wheres . " AND $where",
 384              array_merge($capjoin->params, $params)
 385          );
 386      }
 387  
 388      /**
 389       * Return an array of all users and it's total attempts.
 390       *
 391       * Note: this funciton only returns the list of users with attempts,
 392       * it does not check all participants.
 393       *
 394       * @return array indexed count userid => total number of attempts
 395       */
 396      public function count_users_attempts(): array {
 397          global $DB;
 398          $params = [
 399              'h5pactivityid' => $this->instance->id,
 400          ];
 401          $sql = "SELECT userid, count(*)
 402                    FROM {h5pactivity_attempts}
 403                   WHERE h5pactivityid = :h5pactivityid
 404                   GROUP BY userid";
 405          return $DB->get_records_sql_menu($sql, $params);
 406      }
 407  
 408      /**
 409       * Return the current context.
 410       *
 411       * @return context_module
 412       */
 413      public function get_context(): context_module {
 414          return $this->context;
 415      }
 416  
 417      /**
 418       * Return the current instance.
 419       *
 420       * @return stdClass the instance record
 421       */
 422      public function get_instance(): stdClass {
 423          return $this->instance;
 424      }
 425  
 426      /**
 427       * Return the current cm_info.
 428       *
 429       * @return cm_info the course module
 430       */
 431      public function get_coursemodule(): cm_info {
 432          return $this->coursemodule;
 433      }
 434  
 435      /**
 436       * Return the specific grader object for this activity.
 437       *
 438       * @return grader
 439       */
 440      public function get_grader(): grader {
 441          $idnumber = $this->coursemodule->idnumber ?? '';
 442          return new grader($this->instance, $idnumber);
 443      }
 444  
 445      /**
 446       * Return the suitable report to show the attempts.
 447       *
 448       * This method controls the access to the different reports
 449       * the activity have.
 450       *
 451       * @param int $userid an opional userid to show
 452       * @param int $attemptid an optional $attemptid to show
 453       * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
 454       * @return report|null available report (or null if no report available)
 455       */
 456      public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report {
 457          global $USER, $CFG;
 458  
 459          require_once("{$CFG->dirroot}/user/lib.php");
 460  
 461          // If tracking is disabled, no reports are available.
 462          if (!$this->instance->enabletracking) {
 463              return null;
 464          }
 465  
 466          $attempt = null;
 467          if ($attemptid) {
 468              $attempt = $this->get_attempt($attemptid);
 469              if (!$attempt) {
 470                  return null;
 471              }
 472              // If we have and attempt we can ignore the provided $userid.
 473              $userid = $attempt->get_userid();
 474          }
 475  
 476          if ($this->can_view_all_attempts()) {
 477              $user = core_user::get_user($userid);
 478  
 479              // Ensure user can view the attempt of specific userid, respecting access checks.
 480              if ($user && $user->id != $USER->id) {
 481                  $course = get_course($this->coursemodule->course);
 482                  if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) {
 483                      return null;
 484                  }
 485              }
 486          } else if ($this->can_view_own_attempts()) {
 487              $user = core_user::get_user($USER->id);
 488              if ($userid && $user->id != $userid) {
 489                  return null;
 490              }
 491          } else {
 492              return null;
 493          }
 494  
 495          // Only enrolled users has reports.
 496          if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) {
 497              return null;
 498          }
 499  
 500          // Create the proper report.
 501          if ($user && $attempt) {
 502              return new results($this, $user, $attempt);
 503          } else if ($user) {
 504              return new attempts($this, $user);
 505          }
 506          return new participants($this, $currentgroup);
 507      }
 508  
 509      /**
 510       * Return a single attempt.
 511       *
 512       * @param int $attemptid the attempt id
 513       * @return attempt
 514       */
 515      public function get_attempt(int $attemptid): ?attempt {
 516          global $DB;
 517          $record = $DB->get_record('h5pactivity_attempts', [
 518              'id' => $attemptid,
 519              'h5pactivityid' => $this->instance->id,
 520          ]);
 521          if (!$record) {
 522              return null;
 523          }
 524          return new attempt($record);
 525      }
 526  
 527      /**
 528       * Return an array of all user attempts (including incompleted)
 529       *
 530       * @param int $userid the user id
 531       * @return attempt[]
 532       */
 533      public function get_user_attempts(int $userid): array {
 534          global $DB;
 535          $records = $DB->get_records(
 536              'h5pactivity_attempts',
 537              ['userid' => $userid, 'h5pactivityid' => $this->instance->id],
 538              'id ASC'
 539          );
 540          if (!$records) {
 541              return [];
 542          }
 543          $result = [];
 544          foreach ($records as $record) {
 545              $result[] = new attempt($record);
 546          }
 547          return $result;
 548      }
 549  
 550      /**
 551       * Trigger module viewed event and set the module viewed for completion.
 552       *
 553       * @param stdClass $course course object
 554       * @return void
 555       */
 556      public function set_module_viewed(stdClass $course): void {
 557          global $CFG;
 558          require_once($CFG->libdir . '/completionlib.php');
 559  
 560          // Trigger module viewed event.
 561          $event = course_module_viewed::create([
 562              'objectid' => $this->instance->id,
 563              'context' => $this->context
 564          ]);
 565          $event->add_record_snapshot('course', $course);
 566          $event->add_record_snapshot('course_modules', $this->coursemodule);
 567          $event->add_record_snapshot('h5pactivity', $this->instance);
 568          $event->trigger();
 569  
 570          // Completion.
 571          $completion = new \completion_info($course);
 572          $completion->set_module_viewed($this->coursemodule);
 573      }
 574  }