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.
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * H5P activity manager class
 *
 * @package    mod_h5pactivity
 * @since      Moodle 3.9
 * @copyright  2020 Ferran Recio <ferran@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace mod_h5pactivity\local;

use mod_h5pactivity\local\report\participants;
use mod_h5pactivity\local\report\attempts;
use mod_h5pactivity\local\report\results;
use context_module;
use cm_info;
use moodle_recordset;
use core_user;
use stdClass;
use core\dml\sql_join;
use mod_h5pactivity\event\course_module_viewed;

/**
 * Class manager for H5P activity
 *
 * @package    mod_h5pactivity
 * @since      Moodle 3.9
 * @copyright  2020 Ferran Recio <ferran@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class manager {

    /** No automathic grading using attempt results. */
    const GRADEMANUAL = 0;

    /** Use highest attempt results for grading. */
    const GRADEHIGHESTATTEMPT = 1;

    /** Use average attempt results for grading. */
    const GRADEAVERAGEATTEMPT = 2;

    /** Use last attempt results for grading. */
    const GRADELASTATTEMPT = 3;

    /** Use first attempt results for grading. */
    const GRADEFIRSTATTEMPT = 4;

    /** Participants cannot review their own attempts. */
    const REVIEWNONE = 0;

    /** Participants can review their own attempts when have one attempt completed. */
    const REVIEWCOMPLETION = 1;

    /** @var stdClass course_module record. */
    private $instance;

    /** @var context_module the current context. */
    private $context;

    /** @var cm_info course_modules record. */
    private $coursemodule;

    /**
     * Class contructor.
     *
     * @param cm_info $coursemodule course module info object
     * @param stdClass $instance H5Pactivity instance object.
     */
    public function __construct(cm_info $coursemodule, stdClass $instance) {
        $this->coursemodule = $coursemodule;
        $this->instance = $instance;
        $this->context = context_module::instance($coursemodule->id);
        $this->instance->cmidnumber = $coursemodule->idnumber;
    }

    /**
     * Create a manager instance from an instance record.
     *
     * @param stdClass $instance a h5pactivity record
     * @return manager
     */
    public static function create_from_instance(stdClass $instance): self {
        $coursemodule = get_coursemodule_from_instance('h5pactivity', $instance->id);
        // Ensure that $this->coursemodule is a cm_info object.
        $coursemodule = cm_info::create($coursemodule);
        return new self($coursemodule, $instance);
    }

    /**
     * Create a manager instance from an course_modules record.
     *
     * @param stdClass|cm_info $coursemodule a h5pactivity record
     * @return manager
     */
    public static function create_from_coursemodule($coursemodule): self {
        global $DB;
        // Ensure that $this->coursemodule is a cm_info object.
        $coursemodule = cm_info::create($coursemodule);
        $instance = $DB->get_record('h5pactivity', ['id' => $coursemodule->instance], '*', MUST_EXIST);
        return new self($coursemodule, $instance);
    }

    /**
     * Return the available grading methods.
     * @return string[] an array "option value" => "option description"
     */
    public static function get_grading_methods(): array {
        return [
            self::GRADEHIGHESTATTEMPT => get_string('grade_highest_attempt', 'mod_h5pactivity'),
            self::GRADEAVERAGEATTEMPT => get_string('grade_average_attempt', 'mod_h5pactivity'),
            self::GRADELASTATTEMPT => get_string('grade_last_attempt', 'mod_h5pactivity'),
            self::GRADEFIRSTATTEMPT => get_string('grade_first_attempt', 'mod_h5pactivity'),
            self::GRADEMANUAL => get_string('grade_manual', 'mod_h5pactivity'),
        ];
    }

    /**
     * Return the selected attempt criteria.
     * @return string[] an array "grademethod value", "attempt description"
     */
    public function get_selected_attempt(): array {
        $types = [
            self::GRADEHIGHESTATTEMPT => get_string('attempt_highest', 'mod_h5pactivity'),
            self::GRADEAVERAGEATTEMPT => get_string('attempt_average', 'mod_h5pactivity'),
            self::GRADELASTATTEMPT => get_string('attempt_last', 'mod_h5pactivity'),
            self::GRADEFIRSTATTEMPT => get_string('attempt_first', 'mod_h5pactivity'),
            self::GRADEMANUAL => get_string('attempt_none', 'mod_h5pactivity'),
        ];
        if ($this->instance->enabletracking) {
            $key = $this->instance->grademethod;
        } else {
            $key = self::GRADEMANUAL;
        }
        return [$key, $types[$key]];
    }

    /**
     * Return the available review modes.
     *
     * @return string[] an array "option value" => "option description"
     */
    public static function get_review_modes(): array {
        return [
            self::REVIEWCOMPLETION => get_string('review_on_completion', 'mod_h5pactivity'),
            self::REVIEWNONE => get_string('review_none', 'mod_h5pactivity'),
        ];
    }

    /**
     * Check if tracking is enabled in a particular h5pactivity for a specific user.
     *
     * @param stdClass|null $user user record (default $USER)
     * @return bool if tracking is enabled in this activity
     */
    public function is_tracking_enabled(stdClass $user = null): bool {
        global $USER;
        if (!$this->instance->enabletracking) {
            return false;
        }
        if (empty($user)) {
            $user = $USER;
        }
        return has_capability('mod/h5pactivity:submit', $this->context, $user, false);
    }

    /**
     * Check if a user can see the activity attempts list.
     *
     * @param stdClass|null $user user record (default $USER)
     * @return bool if the user can see the attempts link
     */
    public function can_view_all_attempts(stdClass $user = null): bool {
        global $USER;
        if (!$this->instance->enabletracking) {
            return false;
        }
        if (empty($user)) {
            $user = $USER;
        }
        return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user);
    }

    /**
     * Check if a user can see own attempts.
     *
     * @param stdClass|null $user user record (default $USER)
     * @return bool if the user can see the own attempts link
     */
    public function can_view_own_attempts(stdClass $user = null): bool {
        global $USER;
        if (!$this->instance->enabletracking) {
            return false;
        }
        if (empty($user)) {
            $user = $USER;
        }
        if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) {
            return true;
        }
        if ($this->instance->reviewmode == self::REVIEWNONE) {
            return false;
        }
        if ($this->instance->reviewmode == self::REVIEWCOMPLETION) {
            return true;
        }
        return false;

    }

    /**
     * Return a relation of userid and the valid attempt's scaled score.
     *
     * The returned elements contain a record
     * of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT"
     * the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL"
     * the method will return null.
     *
     * @param int $userid a specific userid or 0 for all user attempts.
     * @return array|null of userid, scaled value and, if exists, the attempt id
     */
    public function get_users_scaled_score(int $userid = 0): ?array {
        global $DB;

        $scaled = [];
        if (!$this->instance->enabletracking) {
            return null;
        }

        if ($this->instance->grademethod == self::GRADEMANUAL) {
            return null;
        }

        $sql = '';

        // General filter.
        $where = 'a.h5pactivityid = :h5pactivityid';
        $params['h5pactivityid'] = $this->instance->id;

        if ($userid) {
            $where .= ' AND a.userid = :userid';
            $params['userid'] = $userid;
        }

        // Average grading needs aggregation query.
        if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) {
            $sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified
                      FROM {h5pactivity_attempts} a
                     WHERE $where AND a.completion = 1
                  GROUP BY a.userid";
        }

        if (empty($sql)) {
            // Decide which attempt is used for the calculation.
            $condition = [
                self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled",
                self::GRADELASTATTEMPT => "a.attempt < b.attempt",
                self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt",
            ];
            $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT];

            $sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified
                      FROM {h5pactivity_attempts} a
                 LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid
                           AND a.userid = b.userid AND b.completion = 1
                           AND $join
                     WHERE $where AND b.id IS NULL AND a.completion = 1
                  GROUP BY a.userid, a.scaled";
        }

        return $DB->get_records_sql($sql, $params);
    }

    /**
     * Count the activity completed attempts.
     *
     * If no user is provided the method will count all active users attempts.
     * Check get_active_users_join PHPdoc to a more detailed description of "active users".
     *
     * @param int|null $userid optional user id (default null)
     * @return int the total amount of attempts
     */
    public function count_attempts(int $userid = null): int {
        global $DB;

        // Counting records is enough for one user.
        if ($userid) {
            $params['userid'] = $userid;
            $params = [
                'h5pactivityid' => $this->instance->id,
                'userid' => $userid,
                'completion' => 1,
            ];
            return $DB->count_records('h5pactivity_attempts', $params);
        }

        $usersjoin = $this->get_active_users_join();

        // Final SQL.
        return $DB->count_records_sql(
            "SELECT COUNT(*)
               FROM {user} u $usersjoin->joins
              WHERE $usersjoin->wheres",
            array_merge($usersjoin->params)
        );
    }

    /**
     * Return the join to collect all activity active users.
     *
     * The concept of active user is relative to the activity permissions. All users with
     * "mod/h5pactivity:view" are potential users but those with "mod/h5pactivity:reviewattempts"
     * are evaluators and they don't count as valid submitters.
     *
     * Note that, in general, the active list has the same effect as checking for "mod/h5pactivity:submit"
     * but submit capability cannot be used because is a write capability and does not apply to frozen contexts.
     *
     * @since Moodle 3.11
     * @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
     * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
     * @return sql_join the active users attempts join
     */
    public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join {

        // Only valid users counts. By default, all users with submit capability are considered potential ones.
        $context = $this->get_context();
        $coursemodule = $this->get_coursemodule();

        // Ensure user can view users from all groups.
        if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS
                && !has_capability('moodle/site:accessallgroups', $context)) {

            return new sql_join('', '1=2', [], true);
        }

        // We want to present all potential users.
        $capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup);

        if ($capjoin->cannotmatchanyrows) {
            return $capjoin;
        }

        // But excluding all reviewattempts users converting a capabilities join into left join.
        $reviewersjoin = get_with_capability_join($context, 'mod/h5pactivity:reviewattempts', 'u.id');
> if ($reviewersjoin->cannotmatchanyrows) { > return $capjoin; $capjoin = new sql_join( > }
$capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins), $capjoin->wheres . " AND reviewer.userid IS NULL", $capjoin->params ); if ($allpotentialusers) { return $capjoin; } // Add attempts join. $where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion"; $params = [ 'h5pactivityid' => $this->instance->id, 'completion' => 1, ]; return new sql_join( $capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id", $capjoin->wheres . " AND $where", array_merge($capjoin->params, $params) ); } /** * Return an array of all users and it's total attempts. * * Note: this funciton only returns the list of users with attempts, * it does not check all participants. * * @return array indexed count userid => total number of attempts */ public function count_users_attempts(): array { global $DB; $params = [ 'h5pactivityid' => $this->instance->id, ]; $sql = "SELECT userid, count(*) FROM {h5pactivity_attempts} WHERE h5pactivityid = :h5pactivityid GROUP BY userid"; return $DB->get_records_sql_menu($sql, $params); } /** * Return the current context. * * @return context_module */ public function get_context(): context_module { return $this->context; } /** * Return the current instance. * * @return stdClass the instance record */ public function get_instance(): stdClass { return $this->instance; } /** * Return the current cm_info. * * @return cm_info the course module */ public function get_coursemodule(): cm_info { return $this->coursemodule; } /** * Return the specific grader object for this activity. * * @return grader */ public function get_grader(): grader { $idnumber = $this->coursemodule->idnumber ?? ''; return new grader($this->instance, $idnumber); } /** * Return the suitable report to show the attempts. * * This method controls the access to the different reports * the activity have. * * @param int $userid an opional userid to show * @param int $attemptid an optional $attemptid to show * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group * @return report|null available report (or null if no report available) */ public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report { global $USER, $CFG; require_once("{$CFG->dirroot}/user/lib.php"); // If tracking is disabled, no reports are available. if (!$this->instance->enabletracking) { return null; } $attempt = null; if ($attemptid) { $attempt = $this->get_attempt($attemptid); if (!$attempt) { return null; } // If we have and attempt we can ignore the provided $userid. $userid = $attempt->get_userid(); } if ($this->can_view_all_attempts()) { $user = core_user::get_user($userid); // Ensure user can view the attempt of specific userid, respecting access checks. if ($user && $user->id != $USER->id) { $course = get_course($this->coursemodule->course); if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) { return null; } } } else if ($this->can_view_own_attempts()) { $user = core_user::get_user($USER->id); if ($userid && $user->id != $userid) { return null; } } else { return null; } // Only enrolled users has reports. if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) { return null; } // Create the proper report. if ($user && $attempt) { return new results($this, $user, $attempt); } else if ($user) { return new attempts($this, $user); } return new participants($this, $currentgroup); } /** * Return a single attempt. * * @param int $attemptid the attempt id * @return attempt */ public function get_attempt(int $attemptid): ?attempt { global $DB; $record = $DB->get_record('h5pactivity_attempts', [ 'id' => $attemptid, 'h5pactivityid' => $this->instance->id, ]); if (!$record) { return null; } return new attempt($record); } /** * Return an array of all user attempts (including incompleted) * * @param int $userid the user id * @return attempt[] */ public function get_user_attempts(int $userid): array { global $DB; $records = $DB->get_records( 'h5pactivity_attempts', ['userid' => $userid, 'h5pactivityid' => $this->instance->id], 'id ASC' ); if (!$records) { return []; } $result = []; foreach ($records as $record) { $result[] = new attempt($record); } return $result; } /** * Trigger module viewed event and set the module viewed for completion. * * @param stdClass $course course object * @return void */ public function set_module_viewed(stdClass $course): void { global $CFG; require_once($CFG->libdir . '/completionlib.php'); // Trigger module viewed event. $event = course_module_viewed::create([ 'objectid' => $this->instance->id, 'context' => $this->context ]); $event->add_record_snapshot('course', $course); $event->add_record_snapshot('course_modules', $this->coursemodule); $event->add_record_snapshot('h5pactivity', $this->instance); $event->trigger(); // Completion. $completion = new \completion_info($course); $completion->set_module_viewed($this->coursemodule); } }