MDL-69225 h5pactivity: Review when and which banners display
[moodle.git] / mod / h5pactivity / classes / local / manager.php
blob398af2203e2d15776c2c1a0462f47857e0b3a824
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.
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/>.
17 /**
18 * H5P activity manager class
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
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;
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;
39 /**
40 * Class manager for H5P activity
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
47 class manager {
49 /** No automathic grading using attempt results. */
50 const GRADEMANUAL = 0;
52 /** Use highest attempt results for grading. */
53 const GRADEHIGHESTATTEMPT = 1;
55 /** Use average attempt results for grading. */
56 const GRADEAVERAGEATTEMPT = 2;
58 /** Use last attempt results for grading. */
59 const GRADELASTATTEMPT = 3;
61 /** Use first attempt results for grading. */
62 const GRADEFIRSTATTEMPT = 4;
64 /** Participants cannot review their own attempts. */
65 const REVIEWNONE = 0;
67 /** Participants can review their own attempts when have one attempt completed. */
68 const REVIEWCOMPLETION = 1;
70 /** @var stdClass course_module record. */
71 private $instance;
73 /** @var context_module the current context. */
74 private $context;
76 /** @var cm_info course_modules record. */
77 private $coursemodule;
79 /**
80 * Class contructor.
82 * @param cm_info $coursemodule course module info object
83 * @param stdClass $instance H5Pactivity instance object.
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;
92 /**
93 * Create a manager instance from an instance record.
95 * @param stdClass $instance a h5pactivity record
96 * @return manager
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);
106 * Create a manager instance from an course_modules record.
108 * @param stdClass|cm_info $coursemodule a h5pactivity record
109 * @return manager
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);
120 * Return the available grading methods.
121 * @return string[] an array "option value" => "option description"
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'),
134 * Return the selected attempt criteria.
135 * @return string[] an array "grademethod value", "attempt description"
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'),
145 if ($this->instance->enabletracking) {
146 $key = $this->instance->grademethod;
147 } else {
148 $key = self::GRADEMANUAL;
150 return [$key, $types[$key]];
154 * Return the available review modes.
156 * @return string[] an array "option value" => "option description"
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'),
166 * Check if tracking is enabled in a particular h5pactivity for a specific user.
168 * @return bool if tracking is enabled in this activity
170 public function is_tracking_enabled(): bool {
171 return $this->instance->enabletracking;
175 * Check if the user has permission to submit a particular h5pactivity for a specific user.
177 * @param stdClass|null $user user record (default $USER)
178 * @return bool if the user has permission to submit in this activity
180 public function can_submit(stdClass $user = null): bool {
181 global $USER;
183 if (empty($user)) {
184 $user = $USER;
186 return has_capability('mod/h5pactivity:submit', $this->context, $user, false);
190 * Check if a user can see the activity attempts list.
192 * @param stdClass|null $user user record (default $USER)
193 * @return bool if the user can see the attempts link
195 public function can_view_all_attempts(stdClass $user = null): bool {
196 global $USER;
197 if (!$this->instance->enabletracking) {
198 return false;
200 if (empty($user)) {
201 $user = $USER;
203 return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user);
207 * Check if a user can see own attempts.
209 * @param stdClass|null $user user record (default $USER)
210 * @return bool if the user can see the own attempts link
212 public function can_view_own_attempts(stdClass $user = null): bool {
213 global $USER;
214 if (!$this->instance->enabletracking) {
215 return false;
217 if (empty($user)) {
218 $user = $USER;
220 if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) {
221 return true;
223 if ($this->instance->reviewmode == self::REVIEWNONE) {
224 return false;
226 if ($this->instance->reviewmode == self::REVIEWCOMPLETION) {
227 return true;
229 return false;
234 * Return a relation of userid and the valid attempt's scaled score.
236 * The returned elements contain a record
237 * of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT"
238 * the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL"
239 * the method will return null.
241 * @param int $userid a specific userid or 0 for all user attempts.
242 * @return array|null of userid, scaled value and, if exists, the attempt id
244 public function get_users_scaled_score(int $userid = 0): ?array {
245 global $DB;
247 $scaled = [];
248 if (!$this->instance->enabletracking) {
249 return null;
252 if ($this->instance->grademethod == self::GRADEMANUAL) {
253 return null;
256 $sql = '';
258 // General filter.
259 $where = 'a.h5pactivityid = :h5pactivityid';
260 $params['h5pactivityid'] = $this->instance->id;
262 if ($userid) {
263 $where .= ' AND a.userid = :userid';
264 $params['userid'] = $userid;
267 // Average grading needs aggregation query.
268 if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) {
269 $sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified
270 FROM {h5pactivity_attempts} a
271 WHERE $where AND a.completion = 1
272 GROUP BY a.userid";
275 if (empty($sql)) {
276 // Decide which attempt is used for the calculation.
277 $condition = [
278 self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled",
279 self::GRADELASTATTEMPT => "a.attempt < b.attempt",
280 self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt",
282 $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT];
284 $sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified
285 FROM {h5pactivity_attempts} a
286 LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid
287 AND a.userid = b.userid AND b.completion = 1
288 AND $join
289 WHERE $where AND b.id IS NULL AND a.completion = 1
290 GROUP BY a.userid, a.scaled";
293 return $DB->get_records_sql($sql, $params);
297 * Count the activity completed attempts.
299 * If no user is provided the method will count all active users attempts.
300 * Check get_active_users_join PHPdoc to a more detailed description of "active users".
302 * @param int|null $userid optional user id (default null)
303 * @return int the total amount of attempts
305 public function count_attempts(int $userid = null): int {
306 global $DB;
308 // Counting records is enough for one user.
309 if ($userid) {
310 $params['userid'] = $userid;
311 $params = [
312 'h5pactivityid' => $this->instance->id,
313 'userid' => $userid,
314 'completion' => 1,
316 return $DB->count_records('h5pactivity_attempts', $params);
319 $usersjoin = $this->get_active_users_join();
321 // Final SQL.
322 return $DB->count_records_sql(
323 "SELECT COUNT(*)
324 FROM {user} u $usersjoin->joins
325 WHERE $usersjoin->wheres",
326 array_merge($usersjoin->params)
331 * Return the join to collect all activity active users.
333 * The concept of active user is relative to the activity permissions. All users with
334 * "mod/h5pactivity:view" are potential users but those with "mod/h5pactivity:reviewattempts"
335 * are evaluators and they don't count as valid submitters.
337 * Note that, in general, the active list has the same effect as checking for "mod/h5pactivity:submit"
338 * but submit capability cannot be used because is a write capability and does not apply to frozen contexts.
340 * @since Moodle 3.11
341 * @param bool $allpotentialusers if true, the join will return all active users, not only the ones with attempts.
342 * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
343 * @return sql_join the active users attempts join
345 public function get_active_users_join(bool $allpotentialusers = false, $currentgroup = false): sql_join {
347 // Only valid users counts. By default, all users with submit capability are considered potential ones.
348 $context = $this->get_context();
349 $coursemodule = $this->get_coursemodule();
351 // Ensure user can view users from all groups.
352 if ($currentgroup === 0 && $coursemodule->effectivegroupmode == SEPARATEGROUPS
353 && !has_capability('moodle/site:accessallgroups', $context)) {
355 return new sql_join('', '1=2', [], true);
358 // We want to present all potential users.
359 $capjoin = get_enrolled_with_capabilities_join($context, '', 'mod/h5pactivity:view', $currentgroup);
361 if ($capjoin->cannotmatchanyrows) {
362 return $capjoin;
365 // But excluding all reviewattempts users converting a capabilities join into left join.
366 $reviewersjoin = get_with_capability_join($context, 'mod/h5pactivity:reviewattempts', 'u.id');
367 if ($reviewersjoin->cannotmatchanyrows) {
368 return $capjoin;
371 $capjoin = new sql_join(
372 $capjoin->joins . "\n LEFT " . str_replace('ra', 'reviewer', $reviewersjoin->joins),
373 $capjoin->wheres . " AND reviewer.userid IS NULL",
374 $capjoin->params
377 if ($allpotentialusers) {
378 return $capjoin;
381 // Add attempts join.
382 $where = "ha.h5pactivityid = :h5pactivityid AND ha.completion = :completion";
383 $params = [
384 'h5pactivityid' => $this->instance->id,
385 'completion' => 1,
388 return new sql_join(
389 $capjoin->joins . "\n JOIN {h5pactivity_attempts} ha ON ha.userid = u.id",
390 $capjoin->wheres . " AND $where",
391 array_merge($capjoin->params, $params)
396 * Return an array of all users and it's total attempts.
398 * Note: this funciton only returns the list of users with attempts,
399 * it does not check all participants.
401 * @return array indexed count userid => total number of attempts
403 public function count_users_attempts(): array {
404 global $DB;
405 $params = [
406 'h5pactivityid' => $this->instance->id,
408 $sql = "SELECT userid, count(*)
409 FROM {h5pactivity_attempts}
410 WHERE h5pactivityid = :h5pactivityid
411 GROUP BY userid";
412 return $DB->get_records_sql_menu($sql, $params);
416 * Return the current context.
418 * @return context_module
420 public function get_context(): context_module {
421 return $this->context;
425 * Return the current instance.
427 * @return stdClass the instance record
429 public function get_instance(): stdClass {
430 return $this->instance;
434 * Return the current cm_info.
436 * @return cm_info the course module
438 public function get_coursemodule(): cm_info {
439 return $this->coursemodule;
443 * Return the specific grader object for this activity.
445 * @return grader
447 public function get_grader(): grader {
448 $idnumber = $this->coursemodule->idnumber ?? '';
449 return new grader($this->instance, $idnumber);
453 * Return the suitable report to show the attempts.
455 * This method controls the access to the different reports
456 * the activity have.
458 * @param int $userid an opional userid to show
459 * @param int $attemptid an optional $attemptid to show
460 * @param int|bool $currentgroup False if groups not used, 0 for all groups, group id (int) to filter by specific group
461 * @return report|null available report (or null if no report available)
463 public function get_report(int $userid = null, int $attemptid = null, $currentgroup = false): ?report {
464 global $USER, $CFG;
466 require_once("{$CFG->dirroot}/user/lib.php");
468 // If tracking is disabled, no reports are available.
469 if (!$this->instance->enabletracking) {
470 return null;
473 $attempt = null;
474 if ($attemptid) {
475 $attempt = $this->get_attempt($attemptid);
476 if (!$attempt) {
477 return null;
479 // If we have and attempt we can ignore the provided $userid.
480 $userid = $attempt->get_userid();
483 if ($this->can_view_all_attempts()) {
484 $user = core_user::get_user($userid);
486 // Ensure user can view the attempt of specific userid, respecting access checks.
487 if ($user && $user->id != $USER->id) {
488 $course = get_course($this->coursemodule->course);
489 if ($this->coursemodule->effectivegroupmode == SEPARATEGROUPS && !user_can_view_profile($user, $course)) {
490 return null;
493 } else if ($this->can_view_own_attempts()) {
494 $user = core_user::get_user($USER->id);
495 if ($userid && $user->id != $userid) {
496 return null;
498 } else {
499 return null;
502 // Only enrolled users has reports.
503 if ($user && !is_enrolled($this->context, $user, 'mod/h5pactivity:view')) {
504 return null;
507 // Create the proper report.
508 if ($user && $attempt) {
509 return new results($this, $user, $attempt);
510 } else if ($user) {
511 return new attempts($this, $user);
513 return new participants($this, $currentgroup);
517 * Return a single attempt.
519 * @param int $attemptid the attempt id
520 * @return attempt
522 public function get_attempt(int $attemptid): ?attempt {
523 global $DB;
524 $record = $DB->get_record('h5pactivity_attempts', [
525 'id' => $attemptid,
526 'h5pactivityid' => $this->instance->id,
528 if (!$record) {
529 return null;
531 return new attempt($record);
535 * Return an array of all user attempts (including incompleted)
537 * @param int $userid the user id
538 * @return attempt[]
540 public function get_user_attempts(int $userid): array {
541 global $DB;
542 $records = $DB->get_records(
543 'h5pactivity_attempts',
544 ['userid' => $userid, 'h5pactivityid' => $this->instance->id],
545 'id ASC'
547 if (!$records) {
548 return [];
550 $result = [];
551 foreach ($records as $record) {
552 $result[] = new attempt($record);
554 return $result;
558 * Trigger module viewed event and set the module viewed for completion.
560 * @param stdClass $course course object
561 * @return void
563 public function set_module_viewed(stdClass $course): void {
564 global $CFG;
565 require_once($CFG->libdir . '/completionlib.php');
567 // Trigger module viewed event.
568 $event = course_module_viewed::create([
569 'objectid' => $this->instance->id,
570 'context' => $this->context
572 $event->add_record_snapshot('course', $course);
573 $event->add_record_snapshot('course_modules', $this->coursemodule);
574 $event->add_record_snapshot('h5pactivity', $this->instance);
575 $event->trigger();
577 // Completion.
578 $completion = new \completion_info($course);
579 $completion->set_module_viewed($this->coursemodule);