Commit | Line | Data |
---|---|---|
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 | ||
26 | namespace mod_h5pactivity\local; | |
27 | ||
9a4307dd FR |
28 | use mod_h5pactivity\local\report\participants; |
29 | use mod_h5pactivity\local\report\attempts; | |
30 | use mod_h5pactivity\local\report\results; | |
e28b4069 FR |
31 | use context_module; |
32 | use cm_info; | |
33 | use moodle_recordset; | |
9a4307dd | 34 | use core_user; |
e28b4069 | 35 | use stdClass; |
a9ed34a9 | 36 | use 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 | */ | |
46 | class 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()) { | |
932e8ea7 | 390 | $user = core_user::get_user($USER->id); |
9a4307dd FR |
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 | } |