Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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