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