weekly release 4.0dev
[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;
36 use mod_h5pactivity\event\course_module_viewed;
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 {
48     /** No automathic grading using attempt results. */
49     const GRADEMANUAL = 0;
51     /** Use highest attempt results for grading. */
52     const GRADEHIGHESTATTEMPT = 1;
54     /** Use average attempt results for grading. */
55     const GRADEAVERAGEATTEMPT = 2;
57     /** Use last attempt results for grading. */
58     const GRADELASTATTEMPT = 3;
60     /** Use first attempt results for grading. */
61     const GRADEFIRSTATTEMPT = 4;
63     /** Participants cannot review their own attempts. */
64     const REVIEWNONE = 0;
66     /** Participants can review their own attempts when have one attempt completed. */
67     const REVIEWCOMPLETION = 1;
69     /** @var stdClass course_module record. */
70     private $instance;
72     /** @var context_module the current context. */
73     private $context;
75     /** @var cm_info course_modules record. */
76     private $coursemodule;
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     }
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     }
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     }
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     }
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     }
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     }
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     }
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     }
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;
223     }
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;
239         $scaled = [];
240         if (!$this->instance->enabletracking) {
241             return null;
242         }
244         if ($this->instance->grademethod == self::GRADEMANUAL) {
245             return null;
246         }
248         $sql = '';
250         // General filter.
251         $where = 'a.h5pactivityid = :h5pactivityid';
252         $params['h5pactivityid'] = $this->instance->id;
254         if ($userid) {
255             $where .= ' AND a.userid = :userid';
256             $params['userid'] = $userid;
257         }
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         }
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];
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         }
285         return $DB->get_records_sql($sql, $params);
286     }
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     }
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     }
328     /**
329      * Return the current context.
330      *
331      * @return context_module
332      */
333     public function get_context(): context_module {
334         return $this->context;
335     }
337     /**
338      * Return the current instance.
339      *
340      * @return stdClass the instance record
341      */
342     public function get_instance(): stdClass {
343         return $this->instance;
344     }
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     }
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     }
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         }
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         }
398         // Check if that user can be tracked.
399         if ($user && !$this->is_tracking_enabled($user)) {
400             return null;
401         }
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     }
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;
420         $record = $DB->get_record('h5pactivity_attempts', [
421             'id' => $attemptid,
422             'h5pactivityid' => $this->instance->id,
423         ]);
424         if (!$record) {
425             return null;
426         }
427         return new attempt($record);
428     }
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     }
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');
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();
473         // Completion.
474         $completion = new \completion_info($course);
475         $completion->set_module_viewed($this->coursemodule);
476     }