Merge branch 'MDL-62487' of https://github.com/timhunt/moodle
[moodle.git] / mod / h5pactivity / classes / local / manager.php
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/>.
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  */
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;
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  */
45 class manager {
47     /** No automathic grading using attempt results. */
48     const GRADEMANUAL = 0;
50     /** Use highest attempt results for grading. */
51     const GRADEHIGHESTATTEMPT = 1;
53     /** Use average attempt results for grading. */
54     const GRADEAVERAGEATTEMPT = 2;
56     /** Use last attempt results for grading. */
57     const GRADELASTATTEMPT = 3;
59     /** Use first attempt results for grading. */
60     const GRADEFIRSTATTEMPT = 4;
62     /** Participants cannot review their own attempts. */
63     const REVIEWNONE = 0;
65     /** Participants can review their own attempts when have one attempt completed. */
66     const REVIEWCOMPLETION = 1;
68     /** @var stdClass course_module record. */
69     private $instance;
71     /** @var context_module the current context. */
72     private $context;
74     /** @var cm_info course_modules record. */
75     private $coursemodule;
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     }
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     }
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     }
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     }
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     }
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     }
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     }
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     }
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;
222     }
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;
238         $scaled = [];
239         if (!$this->instance->enabletracking) {
240             return null;
241         }
243         if ($this->instance->grademethod == self::GRADEMANUAL) {
244             return null;
245         }
247         $sql = '';
249         // General filter.
250         $where = 'a.h5pactivityid = :h5pactivityid';
251         $params['h5pactivityid'] = $this->instance->id;
253         if ($userid) {
254             $where .= ' AND a.userid = :userid';
255             $params['userid'] = $userid;
256         }
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         }
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];
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         }
284         return $DB->get_records_sql($sql, $params);
285     }
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     }
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     }
327     /**
328      * Return the current context.
329      *
330      * @return context_module
331      */
332     public function get_context(): context_module {
333         return $this->context;
334     }
336     /**
337      * Return the current instance.
338      *
339      * @return stdClass the instance record
340      */
341     public function get_instance(): stdClass {
342         return $this->instance;
343     }
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     }
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     }
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         }
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         }
397         // Check if that user can be tracked.
398         if ($user && !$this->is_tracking_enabled($user)) {
399             return null;
400         }
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     }
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     }
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     }