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 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [Versions 401 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  /**
  18   * Library of interface functions and constants.
  19   *
  20   * @package     mod_h5pactivity
  21   * @copyright   2020 Ferran Recio <ferran@moodle.com>
  22   * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  use mod_h5pactivity\local\manager;
  28  use mod_h5pactivity\local\grader;
  29  use mod_h5pactivity\xapi\handler;
  30  
  31  /**
  32   * Checks if H5P activity supports a specific feature.
  33   *
  34   * @uses FEATURE_GROUPS
  35   * @uses FEATURE_GROUPINGS
  36   * @uses FEATURE_MOD_INTRO
  37   * @uses FEATURE_SHOW_DESCRIPTION
  38   * @uses FEATURE_COMPLETION_TRACKS_VIEWS
  39   * @uses FEATURE_COMPLETION_HAS_RULES
  40   * @uses FEATURE_MODEDIT_DEFAULT_COMPLETION
  41   * @uses FEATURE_GRADE_HAS_GRADE
  42   * @uses FEATURE_GRADE_OUTCOMES
  43   * @uses FEATURE_BACKUP_MOODLE2
  44   * @param string $feature FEATURE_xx constant for requested feature
  45   * @return mixed True if module supports feature, false if not, null if doesn't know or string for the module purpose.
  46   */
  47  function h5pactivity_supports(string $feature) {
  48      switch($feature) {
  49          case FEATURE_GROUPS:
  50              return true;
  51          case FEATURE_GROUPINGS:
  52              return true;
  53          case FEATURE_MOD_INTRO:
  54              return true;
  55          case FEATURE_SHOW_DESCRIPTION:
  56              return true;
  57          case FEATURE_COMPLETION_TRACKS_VIEWS:
  58              return true;
  59          case FEATURE_MODEDIT_DEFAULT_COMPLETION:
  60              return true;
  61          case FEATURE_GRADE_HAS_GRADE:
  62              return true;
  63          case FEATURE_GRADE_OUTCOMES:
  64              return true;
  65          case FEATURE_BACKUP_MOODLE2:
  66              return true;
  67          case FEATURE_MOD_PURPOSE:
  68              return MOD_PURPOSE_CONTENT;
  69          default:
  70              return null;
  71      }
  72  }
  73  
  74  /**
  75   * Saves a new instance of the mod_h5pactivity into the database.
  76   *
  77   * Given an object containing all the necessary data, (defined by the form
  78   * in mod_form.php) this function will create a new instance and return the id
  79   * number of the instance.
  80   *
  81   * @param stdClass $data An object from the form.
  82   * @param mod_h5pactivity_mod_form $mform The form.
  83   * @return int The id of the newly inserted record.
  84   */
  85  function h5pactivity_add_instance(stdClass $data, mod_h5pactivity_mod_form $mform = null): int {
  86      global $DB;
  87  
  88      $data->timecreated = time();
  89      $data->timemodified = $data->timecreated;
  90      $cmid = $data->coursemodule;
  91  
  92      $data->id = $DB->insert_record('h5pactivity', $data);
  93  
  94      // We need to use context now, so we need to make sure all needed info is already in db.
  95      $DB->set_field('course_modules', 'instance', $data->id, ['id' => $cmid]);
  96      h5pactivity_set_mainfile($data);
  97  
  98      // Extra fields required in grade related functions.
  99      $data->cmid = $data->coursemodule;
 100      h5pactivity_grade_item_update($data);
 101      return $data->id;
 102  }
 103  
 104  /**
 105   * Updates an instance of the mod_h5pactivity in the database.
 106   *
 107   * Given an object containing all the necessary data (defined in mod_form.php),
 108   * this function will update an existing instance with new data.
 109   *
 110   * @param stdClass $data An object from the form in mod_form.php.
 111   * @param mod_h5pactivity_mod_form $mform The form.
 112   * @return bool True if successful, false otherwise.
 113   */
 114  function h5pactivity_update_instance(stdClass $data, mod_h5pactivity_mod_form $mform = null): bool {
 115      global $DB;
 116  
 117      $data->timemodified = time();
 118      $data->id = $data->instance;
 119  
 120      h5pactivity_set_mainfile($data);
 121  
 122      // Update gradings if grading method or tracking are modified.
 123      $data->cmid = $data->coursemodule;
 124      $moduleinstance = $DB->get_record('h5pactivity', ['id' => $data->id]);
 125      if (($moduleinstance->grademethod != $data->grademethod)
 126              || $data->enabletracking != $moduleinstance->enabletracking) {
 127          h5pactivity_update_grades($data);
 128      } else {
 129          h5pactivity_grade_item_update($data);
 130      }
 131  
 132      return $DB->update_record('h5pactivity', $data);
 133  }
 134  
 135  /**
 136   * Removes an instance of the mod_h5pactivity from the database.
 137   *
 138   * @param int $id Id of the module instance.
 139   * @return bool True if successful, false on failure.
 140   */
 141  function h5pactivity_delete_instance(int $id): bool {
 142      global $DB;
 143  
 144      $activity = $DB->get_record('h5pactivity', ['id' => $id]);
 145      if (!$activity) {
 146          return false;
 147      }
 148  
 149      if ($cm = get_coursemodule_from_instance('h5pactivity', $activity->id)) {
 150          $context = context_module::instance($cm->id);
 151          $xapihandler = handler::create('mod_h5pactivity');
 152          $xapihandler->wipe_states($context->id);
 153      }
 154  
 155      // Remove activity record, and all associated attempt data.
 156      $attemptids = $DB->get_fieldset_select('h5pactivity_attempts', 'id', 'h5pactivityid = ?', [$id]);
 157      if ($attemptids) {
 158          $DB->delete_records_list('h5pactivity_attempts_results', 'attemptid', $attemptids);
 159          $DB->delete_records_list('h5pactivity_attempts', 'id', $attemptids);
 160      }
 161  
 162      $DB->delete_records('h5pactivity', ['id' => $id]);
 163  
 164      h5pactivity_grade_item_delete($activity);
 165  
 166      return true;
 167  }
 168  
 169  /**
 170   * Checks if scale is being used by any instance of mod_h5pactivity.
 171   *
 172   * This is used to find out if scale used anywhere.
 173   *
 174   * @param int $scaleid ID of the scale.
 175   * @return bool True if the scale is used by any mod_h5pactivity instance.
 176   */
 177  function h5pactivity_scale_used_anywhere(int $scaleid): bool {
 178      global $DB;
 179  
 180      if ($scaleid and $DB->record_exists('h5pactivity', ['grade' => -$scaleid])) {
 181          return true;
 182      } else {
 183          return false;
 184      }
 185  }
 186  
 187  /**
 188   * Creates or updates grade item for the given mod_h5pactivity instance.
 189   *
 190   * Needed by {@link grade_update_mod_grades()}.
 191   *
 192   * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property.
 193   * @param mixed $grades optional array/object of grade(s); 'reset' means reset grades in gradebook
 194   * @return int int 0 if ok, error code otherwise
 195   */
 196  function h5pactivity_grade_item_update(stdClass $moduleinstance, $grades = null): int {
 197      $idnumber = $moduleinstance->idnumber ?? '';
 198      $grader = new grader($moduleinstance, $idnumber);
 199      return $grader->grade_item_update($grades);
 200  }
 201  
 202  /**
 203   * Delete grade item for given mod_h5pactivity instance.
 204   *
 205   * @param stdClass $moduleinstance Instance object.
 206   * @return int Returns GRADE_UPDATE_OK, GRADE_UPDATE_FAILED, GRADE_UPDATE_MULTIPLE or GRADE_UPDATE_ITEM_LOCKED
 207   */
 208  function h5pactivity_grade_item_delete(stdClass $moduleinstance): ?int {
 209      $idnumber = $moduleinstance->idnumber ?? '';
 210      $grader = new grader($moduleinstance, $idnumber);
 211      return $grader->grade_item_delete();
 212  }
 213  
 214  /**
 215   * Update mod_h5pactivity grades in the gradebook.
 216   *
 217   * Needed by {@link grade_update_mod_grades()}.
 218   *
 219   * @param stdClass $moduleinstance Instance object with extra cmidnumber and modname property.
 220   * @param int $userid Update grade of specific user only, 0 means all participants.
 221   */
 222  function h5pactivity_update_grades(stdClass $moduleinstance, int $userid = 0): void {
 223      $idnumber = $moduleinstance->idnumber ?? '';
 224      $grader = new grader($moduleinstance, $idnumber);
 225      $grader->update_grades($userid);
 226  }
 227  
 228  /**
 229   * Rescale all grades for this activity and push the new grades to the gradebook.
 230   *
 231   * @param stdClass $course Course db record
 232   * @param stdClass $cm Course module db record
 233   * @param float $oldmin
 234   * @param float $oldmax
 235   * @param float $newmin
 236   * @param float $newmax
 237   * @return bool true if reescale is successful
 238   */
 239  function h5pactivity_rescale_activity_grades(stdClass $course, stdClass $cm, float $oldmin,
 240          float $oldmax, float $newmin, float $newmax): bool {
 241  
 242      $manager = manager::create_from_coursemodule($cm);
 243      $grader = $manager->get_grader();
 244      $grader->update_grades();
 245      return true;
 246  }
 247  
 248  /**
 249   * Implementation of the function for printing the form elements that control
 250   * whether the course reset functionality affects the H5P activity.
 251   *
 252   * @param MoodleQuickForm $mform form passed by reference
 253   */
 254  function h5pactivity_reset_course_form_definition(&$mform): void {
 255      $mform->addElement('header', 'h5pactivityheader', get_string('modulenameplural', 'mod_h5pactivity'));
 256      $mform->addElement('advcheckbox', 'reset_h5pactivity', get_string('deleteallattempts', 'mod_h5pactivity'));
 257  }
 258  
 259  /**
 260   * Course reset form defaults.
 261   *
 262   * @param stdClass $course the course object
 263   * @return array
 264   */
 265  function h5pactivity_reset_course_form_defaults(stdClass $course): array {
 266      return ['reset_h5pactivity' => 1];
 267  }
 268  
 269  
 270  /**
 271   * This function is used by the reset_course_userdata function in moodlelib.
 272   *
 273   * This function will remove all H5P attempts in the database
 274   * and clean up any related data.
 275   *
 276   * @param stdClass $data the data submitted from the reset course.
 277   * @return array of reseting status
 278   */
 279  function h5pactivity_reset_userdata(stdClass $data): array {
 280      global $DB;
 281      $componentstr = get_string('modulenameplural', 'mod_h5pactivity');
 282      $status = [];
 283      if (!empty($data->reset_h5pactivity)) {
 284          $params = ['courseid' => $data->courseid];
 285          $sql = "SELECT a.id FROM {h5pactivity} a WHERE a.course=:courseid";
 286          if ($activities = $DB->get_records_sql($sql, $params)) {
 287              $xapihandler = handler::create('mod_h5pactivity');
 288              foreach ($activities as $activity) {
 289                  $cm = get_coursemodule_from_instance('h5pactivity',
 290                                                       $activity->id,
 291                                                       $data->courseid,
 292                                                       false,
 293                                                       MUST_EXIST);
 294                  mod_h5pactivity\local\attempt::delete_all_attempts ($cm);
 295                  $context = context_module::instance($cm->id);
 296                  $xapihandler->wipe_states($context->id);
 297              }
 298          }
 299          // Remove all grades from gradebook.
 300          if (empty($data->reset_gradebook_grades)) {
 301              h5pactivity_reset_gradebook($data->courseid, 'reset');
 302          }
 303          $status[] = [
 304              'component' => $componentstr,
 305              'item' => get_string('deleteallattempts', 'mod_h5pactivity'),
 306              'error' => false,
 307          ];
 308      }
 309      return $status;
 310  }
 311  
 312  /**
 313   * Removes all grades from gradebook
 314   *
 315   * @param int $courseid Coude ID
 316   * @param string $type optional type (default '')
 317   */
 318  function h5pactivity_reset_gradebook(int $courseid, string $type=''): void {
 319      global $DB;
 320  
 321      $sql = "SELECT a.*, cm.idnumber as cmidnumber, a.course as courseid
 322                FROM {h5pactivity} a, {course_modules} cm, {modules} m
 323               WHERE m.name='h5pactivity' AND m.id=cm.module AND cm.instance=a.id AND a.course=?";
 324  
 325      if ($activities = $DB->get_records_sql($sql, [$courseid])) {
 326          foreach ($activities as $activity) {
 327              h5pactivity_grade_item_update($activity, 'reset');
 328          }
 329      }
 330  }
 331  
 332  /**
 333   * Return a list of page types
 334   *
 335   * @param string $pagetype current page type
 336   * @param stdClass|null $parentcontext Block's parent context
 337   * @param stdClass $currentcontext Current context of block
 338   * @return array array of page types and it's names
 339   */
 340  function h5pactivity_page_type_list(string $pagetype, ?stdClass $parentcontext, stdClass $currentcontext): array {
 341      $modulepagetype = [
 342          'mod-h5pactivity-*' => get_string('page-mod-h5pactivity-x', 'h5pactivity'),
 343      ];
 344      return $modulepagetype;
 345  }
 346  
 347  /**
 348   * Check if the module has any update that affects the current user since a given time.
 349   *
 350   * @param  cm_info $cm course module data
 351   * @param  int $from the time to check updates from
 352   * @param  array $filter  if we need to check only specific updates
 353   * @return stdClass an object with the different type of areas indicating if they were updated or not
 354   */
 355  function h5pactivity_check_updates_since(cm_info $cm, int $from, array $filter = []): stdClass {
 356      global $DB, $USER;
 357  
 358      $updates = course_check_module_updates_since($cm, $from, ['package'], $filter);
 359  
 360      $updates->tracks = (object) ['updated' => false];
 361      $select = 'h5pactivityid = ? AND userid = ? AND timemodified > ?';
 362      $params = [$cm->instance, $USER->id, $from];
 363      $tracks = $DB->get_records_select('h5pactivity_attempts', $select, $params, '', 'id');
 364      if (!empty($tracks)) {
 365          $updates->tracks->updated = true;
 366          $updates->tracks->itemids = array_keys($tracks);
 367      }
 368  
 369      // Now, teachers should see other students updates.
 370      if (has_capability('mod/h5pactivity:reviewattempts', $cm->context)) {
 371          $select = 'h5pactivityid = ? AND timemodified > ?';
 372          $params = [$cm->instance, $from];
 373  
 374          if (groups_get_activity_groupmode($cm) == SEPARATEGROUPS) {
 375              $groupusers = array_keys(groups_get_activity_shared_group_members($cm));
 376              if (empty($groupusers)) {
 377                  return $updates;
 378              }
 379              list($insql, $inparams) = $DB->get_in_or_equal($groupusers);
 380              $select .= ' AND userid ' . $insql;
 381              $params = array_merge($params, $inparams);
 382          }
 383  
 384          $updates->usertracks = (object) ['updated' => false];
 385          $tracks = $DB->get_records_select('h5pactivity_attempts', $select, $params, '', 'id');
 386          if (!empty($tracks)) {
 387              $updates->usertracks->updated = true;
 388              $updates->usertracks->itemids = array_keys($tracks);
 389          }
 390      }
 391      return $updates;
 392  }
 393  
 394  /**
 395   * Returns the lists of all browsable file areas within the given module context.
 396   *
 397   * The file area 'intro' for the activity introduction field is added automatically
 398   * by {@link file_browser::get_file_info_context_module()}.
 399   *
 400   * @param stdClass $course course object
 401   * @param stdClass $cm course module object
 402   * @param stdClass $context context object
 403   * @return string[] array of pair file area => human file area name
 404   */
 405  function h5pactivity_get_file_areas(stdClass $course, stdClass $cm, stdClass $context): array {
 406      $areas = [];
 407      $areas['package'] = get_string('areapackage', 'mod_h5pactivity');
 408      return $areas;
 409  }
 410  
 411  /**
 412   * File browsing support for data module.
 413   *
 414   * @param file_browser $browser
 415   * @param array $areas
 416   * @param stdClass $course
 417   * @param stdClass $cm
 418   * @param context $context
 419   * @param string $filearea
 420   * @param int|null $itemid
 421   * @param string|null $filepath
 422   * @param string|null $filename
 423   * @return file_info_stored|null file_info_stored instance or null if not found
 424   */
 425  function h5pactivity_get_file_info(file_browser $browser, array $areas, stdClass $course,
 426              stdClass $cm, context $context, string $filearea, ?int $itemid = null,
 427              ?string $filepath = null, ?string $filename = null): ?file_info_stored {
 428      global $CFG;
 429  
 430      if (!has_capability('moodle/course:managefiles', $context)) {
 431          return null;
 432      }
 433  
 434      $fs = get_file_storage();
 435  
 436      if ($filearea === 'package') {
 437          $filepath = is_null($filepath) ? '/' : $filepath;
 438          $filename = is_null($filename) ? '.' : $filename;
 439  
 440          $urlbase = $CFG->wwwroot.'/pluginfile.php';
 441          if (!$storedfile = $fs->get_file($context->id, 'mod_h5pactivity', 'package', 0, $filepath, $filename)) {
 442              if ($filepath === '/' and $filename === '.') {
 443                  $storedfile = new virtual_root_file($context->id, 'mod_h5pactivity', 'package', 0);
 444              } else {
 445                  // Not found.
 446                  return null;
 447              }
 448          }
 449          return new file_info_stored($browser, $context, $storedfile, $urlbase, $areas[$filearea], false, true, false, false);
 450      }
 451      return null;
 452  }
 453  
 454  /**
 455   * Serves the files from the mod_h5pactivity file areas.
 456   *
 457   * @param mixed $course course or id of the course
 458   * @param mixed $cm course module or id of the course module
 459   * @param context $context
 460   * @param string $filearea
 461   * @param array $args
 462   * @param bool $forcedownload
 463   * @param array $options additional options affecting the file serving
 464   * @return bool false if file not found, does not return if found - just send the file
 465   */
 466  function h5pactivity_pluginfile($course, $cm, context $context,
 467              string $filearea, array $args, bool $forcedownload, array $options = []): bool {
 468      if ($context->contextlevel != CONTEXT_MODULE) {
 469          return false;
 470      }
 471  
 472      require_login($course, true, $cm);
 473  
 474      $fullpath = '';
 475  
 476      if ($filearea === 'package') {
 477          $revision = (int)array_shift($args); // Prevents caching problems - ignored here.
 478          $relativepath = implode('/', $args);
 479          $fullpath = "/$context->id/mod_h5pactivity/package/0/$relativepath";
 480      }
 481      if (empty($fullpath)) {
 482          return false;
 483      }
 484      $fs = get_file_storage();
 485      $file = $fs->get_file_by_hash(sha1($fullpath));
 486      if (empty($file)) {
 487          return false;
 488      }
 489      send_stored_file($file, $lifetime, 0, false, $options);
 490  }
 491  
 492  /**
 493   * Saves draft files as the activity package.
 494   *
 495   * @param stdClass $data an object from the form
 496   */
 497  function h5pactivity_set_mainfile(stdClass $data): void {
 498      $fs = get_file_storage();
 499      $cmid = $data->coursemodule;
 500      $context = context_module::instance($cmid);
 501  
 502      if (!empty($data->packagefile)) {
 503          $fs = get_file_storage();
 504          $fs->delete_area_files($context->id, 'mod_h5pactivity', 'package');
 505          file_save_draft_area_files($data->packagefile, $context->id, 'mod_h5pactivity', 'package',
 506              0, ['subdirs' => 0, 'maxfiles' => 1]);
 507      }
 508  }
 509  
 510  /**
 511   * Register the ability to handle drag and drop file uploads
 512   * @return array containing details of the files / types the mod can handle
 513   */
 514  function h5pactivity_dndupload_register(): array {
 515      return [
 516          'files' => [
 517              [
 518                  'extension' => 'h5p',
 519                  'message' => get_string('dnduploadh5pactivity', 'h5pactivity')
 520              ]
 521          ]
 522      ];
 523  }
 524  
 525  /**
 526   * Handle a file that has been uploaded
 527   * @param object $uploadinfo details of the file / content that has been uploaded
 528   * @return int instance id of the newly created mod
 529   */
 530  function h5pactivity_dndupload_handle($uploadinfo): int {
 531      global $CFG;
 532  
 533      $context = context_module::instance($uploadinfo->coursemodule);
 534      file_save_draft_area_files($uploadinfo->draftitemid, $context->id, 'mod_h5pactivity', 'package', 0);
 535      $fs = get_file_storage();
 536      $files = $fs->get_area_files($context->id, 'mod_h5pactivity', 'package', 0, 'sortorder, itemid, filepath, filename', false);
 537      $file = reset($files);
 538  
 539      // Create a default h5pactivity object to pass to h5pactivity_add_instance()!
 540      $h5p = get_config('h5pactivity');
 541      $h5p->intro = '';
 542      $h5p->introformat = FORMAT_HTML;
 543      $h5p->course = $uploadinfo->course->id;
 544      $h5p->coursemodule = $uploadinfo->coursemodule;
 545      $h5p->grade = $CFG->gradepointdefault;
 546  
 547      // Add some special handling for the H5P options checkboxes.
 548      $factory = new \core_h5p\factory();
 549      $core = $factory->get_core();
 550      if (isset($uploadinfo->displayopt)) {
 551          $config = (object) $uploadinfo->displayopt;
 552      } else {
 553          $config = \core_h5p\helper::decode_display_options($core);
 554      }
 555      $h5p->displayoptions = \core_h5p\helper::get_display_options($core, $config);
 556  
 557      $h5p->cmidnumber = '';
 558      $h5p->name = $uploadinfo->displayname;
 559      $h5p->reference = $file->get_filename();
 560  
 561      return h5pactivity_add_instance($h5p, null);
 562  }
 563  
 564  /**
 565   * Print recent activity from all h5pactivities in a given course
 566   *
 567   * This is used by the recent activity block
 568   * @param mixed $course the course to print activity for
 569   * @param bool $viewfullnames boolean to determine whether to show full names or not
 570   * @param int $timestart the time the rendering started
 571   * @return bool true if activity was printed, false otherwise.
 572   */
 573  function h5pactivity_print_recent_activity($course, bool $viewfullnames, int $timestart): bool {
 574      global $CFG, $DB, $OUTPUT, $USER;
 575  
 576      $dbparams = [$timestart, $course->id, 'h5pactivity'];
 577  
 578      $userfieldsapi = \core_user\fields::for_userpic();
 579      $namefields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;;
 580  
 581      $sql = "SELECT h5pa.id, h5pa.timemodified, cm.id as cmid, $namefields
 582                FROM {h5pactivity_attempts} h5pa
 583                JOIN {h5pactivity} h5p ON h5p.id = h5pa.h5pactivityid
 584                JOIN {course_modules} cm ON cm.instance = h5p.id
 585                JOIN {modules} md ON md.id = cm.module
 586                JOIN {user} u ON u.id = h5pa.userid
 587               WHERE h5pa.timemodified > ?
 588                 AND h5p.course = ?
 589                 AND md.name = ?
 590            ORDER BY h5pa.timemodified ASC";
 591  
 592      if (!$submissions = $DB->get_records_sql($sql, $dbparams)) {
 593          return false;
 594      }
 595  
 596      $modinfo = get_fast_modinfo($course);
 597  
 598      $recentactivity = h5pactivity_fetch_recent_activity($submissions, $course->id);
 599  
 600      if (empty($recentactivity)) {
 601          return false;
 602      }
 603  
 604      $cms = $modinfo->get_cms();
 605  
 606      echo $OUTPUT->heading(get_string('newsubmissions', 'h5pactivity') . ':', 6);
 607  
 608      foreach ($recentactivity as $submission) {
 609          $cm = $cms[$submission->cmid];
 610          $link = $CFG->wwwroot.'/mod/h5pactivity/view.php?id='.$cm->id;
 611          print_recent_activity_note($submission->timemodified,
 612              $submission,
 613              $cm->name,
 614              $link,
 615              false,
 616              $viewfullnames);
 617      }
 618  
 619      return true;
 620  }
 621  
 622  /**
 623   * Returns all h5pactivities since a given time.
 624   *
 625   * @param array $activities The activity information is returned in this array
 626   * @param int $index The current index in the activities array
 627   * @param int $timestart The earliest activity to show
 628   * @param int $courseid Limit the search to this course
 629   * @param int $cmid The course module id
 630   * @param int $userid Optional user id
 631   * @param int $groupid Optional group id
 632   * @return void
 633   */
 634  function h5pactivity_get_recent_mod_activity(array &$activities, int &$index, int $timestart, int $courseid,
 635              int $cmid, int $userid=0, int $groupid=0) {
 636      global $CFG, $DB, $USER;
 637  
 638      $course = get_course($courseid);
 639      $modinfo = get_fast_modinfo($course);
 640  
 641      $cm = $modinfo->get_cm($cmid);
 642      $params = [];
 643      if ($userid) {
 644          $userselect = 'AND u.id = :userid';
 645          $params['userid'] = $userid;
 646      } else {
 647          $userselect = '';
 648      }
 649  
 650      if ($groupid) {
 651          $groupselect = 'AND gm.groupid = :groupid';
 652          $groupjoin = 'JOIN {groups_members} gm ON gm.userid=u.id';
 653          $params['groupid'] = $groupid;
 654      } else {
 655          $groupselect = '';
 656          $groupjoin = '';
 657      }
 658  
 659      $params['cminstance'] = $cm->instance;
 660      $params['timestart'] = $timestart;
 661      $params['cmid'] = $cmid;
 662  
 663      $userfieldsapi = \core_user\fields::for_userpic();
 664      $userfields = $userfieldsapi->get_sql('u', false, '', 'userid', false)->selects;
 665  
 666      $sql = "SELECT h5pa.id, h5pa.timemodified, cm.id as cmid, $userfields
 667                FROM {h5pactivity_attempts} h5pa
 668                JOIN {h5pactivity} h5p ON h5p.id = h5pa.h5pactivityid
 669                JOIN {course_modules} cm ON cm.instance = h5p.id
 670                JOIN {modules} md ON md.id = cm.module
 671                JOIN {user} u ON u.id = h5pa.userid $groupjoin
 672               WHERE h5pa.timemodified > :timestart
 673                 AND h5p.id = :cminstance $userselect $groupselect
 674                 AND cm.id = :cmid
 675            ORDER BY h5pa.timemodified ASC";
 676  
 677      if (!$submissions = $DB->get_records_sql($sql, $params)) {
 678          return;
 679      }
 680  
 681      $cmcontext = context_module::instance($cm->id);
 682      $grader = has_capability('mod/h5pactivity:reviewattempts', $cmcontext);
 683      $viewfullnames = has_capability('moodle/site:viewfullnames', $cmcontext);
 684  
 685      $recentactivity = h5pactivity_fetch_recent_activity($submissions, $courseid);
 686  
 687      if (empty($recentactivity)) {
 688          return;
 689      }
 690  
 691      if ($grader) {
 692          require_once($CFG->libdir.'/gradelib.php');
 693          $userids = [];
 694          foreach ($recentactivity as $id => $submission) {
 695              $userids[] = $submission->userid;
 696          }
 697          $grades = grade_get_grades($courseid, 'mod', 'h5pactivity', $cm->instance, $userids);
 698      }
 699  
 700      $aname = format_string($cm->name, true);
 701      foreach ($recentactivity as $submission) {
 702          $activity = new stdClass();
 703  
 704          $activity->type = 'h5pactivity';
 705          $activity->cmid = $cm->id;
 706          $activity->name = $aname;
 707          $activity->sectionnum = $cm->sectionnum;
 708          $activity->timestamp = $submission->timemodified;
 709          $activity->user = new stdClass();
 710          if ($grader) {
 711              $activity->grade = $grades->items[0]->grades[$submission->userid]->str_long_grade;
 712          }
 713  
 714          $userfields = explode(',', implode(',', \core_user\fields::get_picture_fields()));
 715          foreach ($userfields as $userfield) {
 716              if ($userfield == 'id') {
 717                  // Aliased in SQL above.
 718                  $activity->user->{$userfield} = $submission->userid;
 719              } else {
 720                  $activity->user->{$userfield} = $submission->{$userfield};
 721              }
 722          }
 723          $activity->user->fullname = fullname($submission, $viewfullnames);
 724  
 725          $activities[$index++] = $activity;
 726      }
 727  
 728      return;
 729  }
 730  
 731  /**
 732   * Print recent activity from all h5pactivities in a given course
 733   *
 734   * This is used by course/recent.php
 735   * @param stdClass $activity
 736   * @param int $courseid
 737   * @param bool $detail
 738   * @param array $modnames
 739   */
 740  function h5pactivity_print_recent_mod_activity(stdClass $activity, int $courseid, bool $detail, array $modnames) {
 741      global $OUTPUT;
 742  
 743      $modinfo = [];
 744      if ($detail) {
 745          $modinfo['modname'] = $activity->name;
 746          $modinfo['modurl'] = new moodle_url('/mod/h5pactivity/view.php', ['id' => $activity->cmid]);
 747          $modinfo['modicon'] = $OUTPUT->image_icon('monologo', $modnames[$activity->type], 'h5pactivity');
 748      }
 749  
 750      $userpicture = $OUTPUT->user_picture($activity->user);
 751  
 752      $template = ['userpicture' => $userpicture,
 753          'submissiontimestamp' => $activity->timestamp,
 754          'modinfo' => $modinfo,
 755          'userurl' => new moodle_url('/user/view.php', array('id' => $activity->user->id, 'course' => $courseid)),
 756          'fullname' => $activity->user->fullname];
 757      if (isset($activity->grade)) {
 758          $template['grade'] = get_string('grade_h5p', 'h5pactivity', $activity->grade);
 759      }
 760  
 761      echo $OUTPUT->render_from_template('mod_h5pactivity/reviewattempts', $template);
 762  }
 763  
 764  /**
 765   * Fetches recent activity for course module.
 766   *
 767   * @param array $submissions The activity submissions
 768   * @param int $courseid Limit the search to this course
 769   * @return array $recentactivity recent activity in a course.
 770   */
 771  function h5pactivity_fetch_recent_activity(array $submissions, int $courseid) : array {
 772      global $USER;
 773  
 774      $course = get_course($courseid);
 775      $modinfo = get_fast_modinfo($course);
 776  
 777      $recentactivity = [];
 778      $grader = [];
 779  
 780      $cms = $modinfo->get_cms();
 781  
 782      foreach ($submissions as $submission) {
 783          if (!array_key_exists($submission->cmid, $cms)) {
 784              continue;
 785          }
 786          $cm = $cms[$submission->cmid];
 787          if (!$cm->uservisible) {
 788              continue;
 789          }
 790  
 791          if ($USER->id == $submission->userid) {
 792              $recentactivity[$submission->userid] = $submission;
 793              continue;
 794          }
 795  
 796          $cmcontext = context_module::instance($cm->id);
 797          // The act of submitting of attempt may be considered private -
 798          // only graders will see it if specified.
 799          if (!array_key_exists($cm->id, $grader)) {
 800              $grader[$cm->id] = has_capability('mod/h5pactivity:reviewattempts', $cmcontext);
 801          }
 802          if (!$grader[$cm->id]) {
 803              continue;
 804          }
 805  
 806          $groups = [];
 807          $usersgroups = [];
 808  
 809          $groupmode = groups_get_activity_groupmode($cm, $course);
 810          $accessallgroups = has_capability('moodle/site:accessallgroups', $cmcontext);
 811  
 812          if ($groupmode == SEPARATEGROUPS && !$accessallgroups) {
 813  
 814              if (isguestuser()) {
 815                  // Shortcut - guest user does not belong into any group.
 816                  continue;
 817              }
 818  
 819              if (!isset($groups[$cm->groupingid])) {
 820                  $groups[$cm->groupingid] = $modinfo->get_groups($cm->groupingid);
 821                  if (!$groups[$cm->groupingid]) {
 822                      continue;
 823                  }
 824              }
 825  
 826              if (!isset($usersgroups[$cm->groupingid][$submission->userid])) {
 827                  $usersgroups[$cm->groupingid][$submission->userid] =
 828                      groups_get_all_groups($course->id, $submission->userid, $cm->groupingid, 'g.*', false, true);
 829              }
 830  
 831              if (is_array($usersgroups[$cm->groupingid][$submission->userid])) {
 832                  $usersgroupstmp = array_keys($usersgroups[$cm->groupingid][$submission->userid]);
 833                  $intersect = array_intersect($usersgroupstmp, $groups[$cm->groupingid]);
 834                  if (empty($intersect)) {
 835                      continue;
 836                  }
 837              }
 838          }
 839  
 840          $recentactivity[$submission->userid] = $submission;
 841      }
 842  
 843      return $recentactivity;
 844  }
 845  
 846  /**
 847   * Extends the settings navigation with the H5P activity settings
 848  
 849   * This function is called when the context for the page is an H5P activity. This is not called by AJAX
 850   * so it is safe to rely on the $PAGE.
 851   *
 852   * @param settings_navigation $settingsnav The settings navigation object
 853   * @param navigation_node $h5pactivitynode The node to add module settings to
 854   */
 855  function h5pactivity_extend_settings_navigation(settings_navigation $settingsnav,
 856          navigation_node $h5pactivitynode = null) {
 857      global $USER;
 858  
 859      $manager = manager::create_from_coursemodule($settingsnav->get_page()->cm);
 860  
 861      // Attempts report.
 862      if ($manager->can_view_all_attempts()) {
 863          $attemptsreporturl = new moodle_url('/mod/h5pactivity/report.php',
 864              ['a' => $settingsnav->get_page()->cm->instance]);
 865          $h5pactivitynode->add(get_string('attempts_report', 'h5pactivity'), $attemptsreporturl,
 866              settings_navigation::TYPE_SETTING, '', 'attemptsreport');
 867      } else if ($manager->can_view_own_attempts() && $manager->count_attempts($USER->id)) {
 868          $attemptsreporturl = new moodle_url('/mod/h5pactivity/report.php',
 869              ['a' => $settingsnav->get_page()->cm->instance, 'userid' => $USER->id]);
 870          $h5pactivitynode->add(get_string('attempts_report', 'h5pactivity'), $attemptsreporturl,
 871              settings_navigation::TYPE_SETTING, '', 'attemptsreport');
 872      }
 873  }