MDL-68619 mod_h5pactivity: add activity check to get_attempt method
[moodle.git] / mod / h5pactivity / classes / local / manager.php
CommitLineData
e28b4069
FR
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
26namespace mod_h5pactivity\local;
27
9a4307dd
FR
28use mod_h5pactivity\local\report\participants;
29use mod_h5pactivity\local\report\attempts;
30use mod_h5pactivity\local\report\results;
e28b4069
FR
31use context_module;
32use cm_info;
33use moodle_recordset;
9a4307dd 34use core_user;
e28b4069 35use stdClass;
a9ed34a9 36use mod_h5pactivity\event\course_module_viewed;
e28b4069
FR
37
38/**
39 * Class manager for H5P activity
40 *
41 * @package mod_h5pactivity
42 * @since Moodle 3.9
43 * @copyright 2020 Ferran Recio <ferran@moodle.com>
44 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
45 */
46class manager {
47
48 /** No automathic grading using attempt results. */
49 const GRADEMANUAL = 0;
50
51 /** Use highest attempt results for grading. */
52 const GRADEHIGHESTATTEMPT = 1;
53
54 /** Use average attempt results for grading. */
55 const GRADEAVERAGEATTEMPT = 2;
56
57 /** Use last attempt results for grading. */
58 const GRADELASTATTEMPT = 3;
59
60 /** Use first attempt results for grading. */
61 const GRADEFIRSTATTEMPT = 4;
62
9a4307dd
FR
63 /** Participants cannot review their own attempts. */
64 const REVIEWNONE = 0;
65
66 /** Participants can review their own attempts when have one attempt completed. */
67 const REVIEWCOMPLETION = 1;
68
e28b4069
FR
69 /** @var stdClass course_module record. */
70 private $instance;
71
72 /** @var context_module the current context. */
73 private $context;
74
75 /** @var cm_info course_modules record. */
76 private $coursemodule;
77
78 /**
79 * Class contructor.
80 *
81 * @param cm_info $coursemodule course module info object
82 * @param stdClass $instance H5Pactivity instance object.
83 */
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;
89 }
90
91 /**
92 * Create a manager instance from an instance record.
93 *
94 * @param stdClass $instance a h5pactivity record
95 * @return manager
96 */
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);
102 }
103
104 /**
105 * Create a manager instance from an course_modules record.
106 *
107 * @param stdClass|cm_info $coursemodule a h5pactivity record
108 * @return manager
109 */
110 public static function create_from_coursemodule($coursemodule): self {
111 global $DB;
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);
116 }
117
118 /**
119 * Return the available grading methods.
120 * @return string[] an array "option value" => "option description"
121 */
122 public static function get_grading_methods(): array {
123 return [
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'),
129 ];
130 }
131
9a4307dd
FR
132 /**
133 * Return the selected attempt criteria.
134 * @return string[] an array "grademethod value", "attempt description"
135 */
136 public function get_selected_attempt(): array {
137 $types = [
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'),
143 ];
144 if ($this->instance->enabletracking) {
145 $key = $this->instance->grademethod;
146 } else {
147 $key = self::GRADEMANUAL;
148 }
149 return [$key, $types[$key]];
150 }
151
152 /**
153 * Return the available review modes.
154 *
155 * @return string[] an array "option value" => "option description"
156 */
157 public static function get_review_modes(): array {
158 return [
159 self::REVIEWCOMPLETION => get_string('review_on_completion', 'mod_h5pactivity'),
160 self::REVIEWNONE => get_string('review_none', 'mod_h5pactivity'),
161 ];
162 }
163
e28b4069
FR
164 /**
165 * Check if tracking is enabled in a particular h5pactivity for a specific user.
166 *
167 * @param stdClass|null $user user record (default $USER)
168 * @return bool if tracking is enabled in this activity
169 */
170 public function is_tracking_enabled(stdClass $user = null): bool {
171 global $USER;
172 if (!$this->instance->enabletracking) {
173 return false;
174 }
175 if (empty($user)) {
176 $user = $USER;
177 }
178 return has_capability('mod/h5pactivity:submit', $this->context, $user, false);
179 }
180
9a4307dd
FR
181 /**
182 * Check if a user can see the activity attempts list.
183 *
184 * @param stdClass|null $user user record (default $USER)
185 * @return bool if the user can see the attempts link
186 */
187 public function can_view_all_attempts (stdClass $user = null): bool {
188 global $USER;
189 if (!$this->instance->enabletracking) {
190 return false;
191 }
192 if (empty($user)) {
193 $user = $USER;
194 }
195 return has_capability('mod/h5pactivity:reviewattempts', $this->context, $user);
196 }
197
198 /**
199 * Check if a user can see own attempts.
200 *
201 * @param stdClass|null $user user record (default $USER)
202 * @return bool if the user can see the own attempts link
203 */
204 public function can_view_own_attempts (stdClass $user = null): bool {
205 global $USER;
206 if (!$this->instance->enabletracking) {
207 return false;
208 }
209 if (empty($user)) {
210 $user = $USER;
211 }
212 if (has_capability('mod/h5pactivity:reviewattempts', $this->context, $user, false)) {
213 return true;
214 }
215 if ($this->instance->reviewmode == self::REVIEWNONE) {
216 return false;
217 }
218 if ($this->instance->reviewmode == self::REVIEWCOMPLETION) {
219 return true;
220 }
221 return false;
222
223 }
224
e28b4069
FR
225 /**
226 * Return a relation of userid and the valid attempt's scaled score.
227 *
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.
232 *
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
235 */
236 public function get_users_scaled_score(int $userid = 0): ?array {
237 global $DB;
238
239 $scaled = [];
240 if (!$this->instance->enabletracking) {
241 return null;
242 }
243
244 if ($this->instance->grademethod == self::GRADEMANUAL) {
245 return null;
246 }
247
248 $sql = '';
249
250 // General filter.
251 $where = 'a.h5pactivityid = :h5pactivityid';
252 $params['h5pactivityid'] = $this->instance->id;
253
254 if ($userid) {
255 $where .= ' AND a.userid = :userid';
256 $params['userid'] = $userid;
257 }
258
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
264 GROUP BY a.userid";
265 }
266
267 if (empty($sql)) {
268 // Decide which attempt is used for the calculation.
269 $condition = [
270 self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled",
271 self::GRADELASTATTEMPT => "a.attempt < b.attempt",
272 self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt",
273 ];
274 $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT];
275
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
280 AND $join
281 WHERE $where AND b.id IS NULL AND a.completion = 1
282 GROUP BY a.userid, a.scaled";
283 }
284
285 return $DB->get_records_sql($sql, $params);
286 }
287
9a4307dd
FR
288 /**
289 * Count the activity completed attempts.
290 *
291 * If no user is provided will count all activity attempts.
292 *
293 * @param int|null $userid optional user id (default null)
294 * @return int the total amount of attempts
295 */
296 public function count_attempts(int $userid = null): int {
297 global $DB;
298 $params = [
299 'h5pactivityid' => $this->instance->id,
300 'completion' => 1
301 ];
302 if ($userid) {
303 $params['userid'] = $userid;
304 }
305 return $DB->count_records('h5pactivity_attempts', $params);
306 }
307
308 /**
309 * Return an array of all users and it's total attempts.
310 *
311 * Note: this funciton only returns the list of users with attempts,
312 * it does not check all participants.
313 *
314 * @return array indexed count userid => total number of attempts
315 */
316 public function count_users_attempts(): array {
317 global $DB;
318 $params = [
319 'h5pactivityid' => $this->instance->id,
320 ];
321 $sql = "SELECT userid, count(*)
322 FROM {h5pactivity_attempts}
323 WHERE h5pactivityid = :h5pactivityid
324 GROUP BY userid";
325 return $DB->get_records_sql_menu($sql, $params);
326 }
327
e28b4069
FR
328 /**
329 * Return the current context.
330 *
331 * @return context_module
332 */
333 public function get_context(): context_module {
334 return $this->context;
335 }
336
337 /**
9a4307dd 338 * Return the current instance.
e28b4069
FR
339 *
340 * @return stdClass the instance record
341 */
342 public function get_instance(): stdClass {
343 return $this->instance;
344 }
345
346 /**
347 * Return the current cm_info.
348 *
349 * @return cm_info the course module
350 */
351 public function get_coursemodule(): cm_info {
352 return $this->coursemodule;
353 }
354
355 /**
356 * Return the specific grader object for this activity.
357 *
358 * @return grader
359 */
360 public function get_grader(): grader {
361 $idnumber = $this->coursemodule->idnumber ?? '';
362 return new grader($this->instance, $idnumber);
363 }
9a4307dd
FR
364
365 /**
366 * Return the suitable report to show the attempts.
367 *
368 * This method controls the access to the different reports
369 * the activity have.
370 *
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)
374 */
375 public function get_report(int $userid = null, int $attemptid = null): ?report {
376 global $USER;
377 $attempt = null;
378 if ($attemptid) {
379 $attempt = $this->get_attempt($attemptid);
380 if (!$attempt) {
381 return null;
382 }
383 // If we have and attempt we can ignore the provided $userid.
384 $userid = $attempt->get_userid();
385 }
386
387 if ($this->can_view_all_attempts()) {
388 $user = core_user::get_user($userid);
389 } else if ($this->can_view_own_attempts()) {
390 $user = $USER;
391 if ($userid && $user->id != $userid) {
392 return null;
393 }
394 } else {
395 return null;
396 }
397
398 // Check if that user can be tracked.
399 if ($user && !$this->is_tracking_enabled($user)) {
400 return null;
401 }
402
403 // Create the proper report.
404 if ($user && $attempt) {
405 return new results($this, $user, $attempt);
406 } else if ($user) {
407 return new attempts($this, $user);
408 }
409 return new participants($this);
410 }
411
412 /**
413 * Return a single attempt.
414 *
415 * @param int $attemptid the attempt id
416 * @return attempt
417 */
418 public function get_attempt(int $attemptid): ?attempt {
419 global $DB;
42c48b66
FR
420 $record = $DB->get_record('h5pactivity_attempts', [
421 'id' => $attemptid,
422 'h5pactivityid' => $this->instance->id,
423 ]);
9a4307dd
FR
424 if (!$record) {
425 return null;
426 }
427 return new attempt($record);
428 }
429
430 /**
431 * Return an array of all user attempts (including incompleted)
432 *
433 * @param int $userid the user id
434 * @return attempt[]
435 */
436 public function get_user_attempts(int $userid): array {
437 global $DB;
438 $records = $DB->get_records(
439 'h5pactivity_attempts',
440 ['userid' => $userid, 'h5pactivityid' => $this->instance->id],
441 'id ASC'
442 );
443 if (!$records) {
444 return [];
445 }
446 $result = [];
447 foreach ($records as $record) {
448 $result[] = new attempt($record);
449 }
450 return $result;
451 }
a9ed34a9 452
453 /**
454 * Trigger module viewed event and set the module viewed for completion.
455 *
456 * @param stdClass $course course object
457 * @return void
458 */
459 public function set_module_viewed(stdClass $course): void {
460 global $CFG;
461 require_once($CFG->libdir . '/completionlib.php');
462
463 // Trigger module viewed event.
464 $event = course_module_viewed::create([
465 'objectid' => $this->instance->id,
466 'context' => $this->context
467 ]);
468 $event->add_record_snapshot('course', $course);
469 $event->add_record_snapshot('course_modules', $this->coursemodule);
470 $event->add_record_snapshot('h5pactivity', $this->instance);
471 $event->trigger();
472
473 // Completion.
474 $completion = new \completion_info($course);
475 $completion->set_module_viewed($this->coursemodule);
476 }
e28b4069 477}