2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * H5P activity manager class
20 * @package mod_h5pactivity
22 * @copyright 2020 Ferran Recio <ferran@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 namespace mod_h5pactivity\local;
28 use mod_h5pactivity\local\report\participants;
29 use mod_h5pactivity\local\report\attempts;
30 use mod_h5pactivity\local\report\results;
36 use mod_h5pactivity\event\course_module_viewed;
39 * Class manager for H5P activity
41 * @package mod_h5pactivity
43 * @copyright 2020 Ferran Recio <ferran@moodle.com>
44 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
48 /** No automathic grading using attempt results. */
49 const GRADEMANUAL = 0;
51 /** Use highest attempt results for grading. */
52 const GRADEHIGHESTATTEMPT = 1;
54 /** Use average attempt results for grading. */
55 const GRADEAVERAGEATTEMPT = 2;
57 /** Use last attempt results for grading. */
58 const GRADELASTATTEMPT = 3;
60 /** Use first attempt results for grading. */
61 const GRADEFIRSTATTEMPT = 4;
63 /** Participants cannot review their own attempts. */
66 /** Participants can review their own attempts when have one attempt completed. */
67 const REVIEWCOMPLETION = 1;
69 /** @var stdClass course_module record. */
72 /** @var context_module the current context. */
75 /** @var cm_info course_modules record. */
76 private $coursemodule;
81 * @param cm_info $coursemodule course module info object
82 * @param stdClass $instance H5Pactivity instance object.
84 public function __construct(cm_info $coursemodule, stdClass $instance) {
85 $this->coursemodule = $coursemodule;
86 $this->instance = $instance;
87 $this->context = context_module::instance($coursemodule->id);
88 $this->instance->cmidnumber = $coursemodule->idnumber;
92 * Create a manager instance from an instance record.
94 * @param stdClass $instance a h5pactivity record
97 public static function create_from_instance(stdClass $instance): self {
98 $coursemodule = get_coursemodule_from_instance('h5pactivity', $instance->id);
99 // Ensure that $this->coursemodule is a cm_info object.
100 $coursemodule = cm_info::create($coursemodule);
101 return new self($coursemodule, $instance);
105 * Create a manager instance from an course_modules record.
107 * @param stdClass|cm_info $coursemodule a h5pactivity record
110 public static function create_from_coursemodule($coursemodule): self {
112 // Ensure that $this->coursemodule is a cm_info object.
113 $coursemodule = cm_info::create($coursemodule);
114 $instance = $DB->get_record('h5pactivity', ['id' => $coursemodule->instance], '*', MUST_EXIST);
115 return new self($coursemodule, $instance);
119 * Return the available grading methods.
120 * @return string[] an array "option value" => "option description"
122 public static function get_grading_methods(): array {
124 self::GRADEHIGHESTATTEMPT => get_string('grade_highest_attempt', 'mod_h5pactivity'),
125 self::GRADEAVERAGEATTEMPT => get_string('grade_average_attempt', 'mod_h5pactivity'),
126 self::GRADELASTATTEMPT => get_string('grade_last_attempt', 'mod_h5pactivity'),
127 self::GRADEFIRSTATTEMPT => get_string('grade_first_attempt', 'mod_h5pactivity'),
128 self::GRADEMANUAL => get_string('grade_manual', 'mod_h5pactivity'),
133 * Return the selected attempt criteria.
134 * @return string[] an array "grademethod value", "attempt description"
136 public function get_selected_attempt(): array {
138 self::GRADEHIGHESTATTEMPT => get_string('attempt_highest', 'mod_h5pactivity'),
139 self::GRADEAVERAGEATTEMPT => get_string('attempt_average', 'mod_h5pactivity'),
140 self::GRADELASTATTEMPT => get_string('attempt_last', 'mod_h5pactivity'),
141 self::GRADEFIRSTATTEMPT => get_string('attempt_first', 'mod_h5pactivity'),
142 self::GRADEMANUAL => get_string('attempt_none', 'mod_h5pactivity'),
144 if ($this->instance->enabletracking) {
145 $key = $this->instance->grademethod;
147 $key = self::GRADEMANUAL;
149 return [$key, $types[$key]];
153 * Return the available review modes.
155 * @return string[] an array "option value" => "option description"
157 public static function get_review_modes(): array {
159 self::REVIEWCOMPLETION => get_string('review_on_completion', 'mod_h5pactivity'),
160 self::REVIEWNONE => get_string('review_none', 'mod_h5pactivity'),
165 * Check if tracking is enabled in a particular h5pactivity for a specific user.
167 * @param stdClass|null $user user record (default $USER)
168 * @return bool if tracking is enabled in this activity
170 public function is_tracking_enabled(stdClass $user = null): bool {
172 if (!$this->instance->enabletracking) {
178 return has_capability('mod/h5pactivity:submit', $this->context, $user, false);
182 * Check if a user can see the activity attempts list.
184 * @param stdClass|null $user user record (default $USER)
185 * @return bool if the user can see the attempts link
187 public function can_view_all_attempts (stdClass $user = null): bool {
189 if (!$this->instance->enabletracking) {
195 return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user);
199 * Check if a user can see own attempts.
201 * @param stdClass|null $user user record (default $USER)
202 * @return bool if the user can see the own attempts link
204 public function can_view_own_attempts (stdClass $user = null): bool {
206 if (!$this->instance->enabletracking) {
212 if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) {
215 if ($this->instance->reviewmode == self::REVIEWNONE) {
218 if ($this->instance->reviewmode == self::REVIEWCOMPLETION) {
226 * Return a relation of userid and the valid attempt's scaled score.
228 * The returned elements contain a record
229 * of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT"
230 * the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL"
231 * the method will return null.
233 * @param int $userid a specific userid or 0 for all user attempts.
234 * @return array|null of userid, scaled value and, if exists, the attempt id
236 public function get_users_scaled_score(int $userid = 0): ?array {
240 if (!$this->instance->enabletracking) {
244 if ($this->instance->grademethod == self::GRADEMANUAL) {
251 $where = 'a.h5pactivityid = :h5pactivityid';
252 $params['h5pactivityid'] = $this->instance->id;
255 $where .= ' AND a.userid = :userid';
256 $params['userid'] = $userid;
259 // Average grading needs aggregation query.
260 if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) {
261 $sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified
262 FROM {h5pactivity_attempts} a
263 WHERE $where AND a.completion = 1
268 // Decide which attempt is used for the calculation.
270 self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled",
271 self::GRADELASTATTEMPT => "a.attempt < b.attempt",
272 self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt",
274 $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT];
276 $sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified
277 FROM {h5pactivity_attempts} a
278 LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid
279 AND a.userid = b.userid AND b.completion = 1
281 WHERE $where AND b.id IS NULL AND a.completion = 1
282 GROUP BY a.userid, a.scaled";
285 return $DB->get_records_sql($sql, $params);
289 * Count the activity completed attempts.
291 * If no user is provided will count all activity attempts.
293 * @param int|null $userid optional user id (default null)
294 * @return int the total amount of attempts
296 public function count_attempts(int $userid = null): int {
299 'h5pactivityid' => $this->instance->id,
303 $params['userid'] = $userid;
305 return $DB->count_records('h5pactivity_attempts', $params);
309 * Return an array of all users and it's total attempts.
311 * Note: this funciton only returns the list of users with attempts,
312 * it does not check all participants.
314 * @return array indexed count userid => total number of attempts
316 public function count_users_attempts(): array {
319 'h5pactivityid' => $this->instance->id,
321 $sql = "SELECT userid, count(*)
322 FROM {h5pactivity_attempts}
323 WHERE h5pactivityid = :h5pactivityid
325 return $DB->get_records_sql_menu($sql, $params);
329 * Return the current context.
331 * @return context_module
333 public function get_context(): context_module {
334 return $this->context;
338 * Return the current instance.
340 * @return stdClass the instance record
342 public function get_instance(): stdClass {
343 return $this->instance;
347 * Return the current cm_info.
349 * @return cm_info the course module
351 public function get_coursemodule(): cm_info {
352 return $this->coursemodule;
356 * Return the specific grader object for this activity.
360 public function get_grader(): grader {
361 $idnumber = $this->coursemodule->idnumber ?? '';
362 return new grader($this->instance, $idnumber);
366 * Return the suitable report to show the attempts.
368 * This method controls the access to the different reports
371 * @param int $userid an opional userid to show
372 * @param int $attemptid an optional $attemptid to show
373 * @return report|null available report (or null if no report available)
375 public function get_report(int $userid = null, int $attemptid = null): ?report {
379 $attempt = $this->get_attempt($attemptid);
383 // If we have and attempt we can ignore the provided $userid.
384 $userid = $attempt->get_userid();
387 if ($this->can_view_all_attempts()) {
388 $user = core_user::get_user($userid);
389 } else if ($this->can_view_own_attempts()) {
390 $user = core_user::get_user($USER->id);
391 if ($userid && $user->id != $userid) {
398 // Check if that user can be tracked.
399 if ($user && !$this->is_tracking_enabled($user)) {
403 // Create the proper report.
404 if ($user && $attempt) {
405 return new results($this, $user, $attempt);
407 return new attempts($this, $user);
409 return new participants($this);
413 * Return a single attempt.
415 * @param int $attemptid the attempt id
418 public function get_attempt(int $attemptid): ?attempt {
420 $record = $DB->get_record('h5pactivity_attempts', [
422 'h5pactivityid' => $this->instance->id,
427 return new attempt($record);
431 * Return an array of all user attempts (including incompleted)
433 * @param int $userid the user id
436 public function get_user_attempts(int $userid): array {
438 $records = $DB->get_records(
439 'h5pactivity_attempts',
440 ['userid' => $userid, 'h5pactivityid' => $this->instance->id],
447 foreach ($records as $record) {
448 $result[] = new attempt($record);
454 * Trigger module viewed event and set the module viewed for completion.
456 * @param stdClass $course course object
459 public function set_module_viewed(stdClass $course): void {
461 require_once($CFG->libdir . '/completionlib.php');
463 // Trigger module viewed event.
464 $event = course_module_viewed::create([
465 'objectid' => $this->instance->id,
466 'context' => $this->context
468 $event->add_record_snapshot('course', $course);
469 $event->add_record_snapshot('course_modules', $this->coursemodule);
470 $event->add_record_snapshot('h5pactivity', $this->instance);
474 $completion = new \completion_info($course);
475 $completion->set_module_viewed($this->coursemodule);