MDL-62487 quiz manual grading: implement suggestions from int review
[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 context_module;
29 use cm_info;
30 use moodle_recordset;
31 use stdClass;
33 /**
34  * Class manager for H5P activity
35  *
36  * @package    mod_h5pactivity
37  * @since      Moodle 3.9
38  * @copyright  2020 Ferran Recio <ferran@moodle.com>
39  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40  */
41 class manager {
43     /** No automathic grading using attempt results. */
44     const GRADEMANUAL = 0;
46     /** Use highest attempt results for grading. */
47     const GRADEHIGHESTATTEMPT = 1;
49     /** Use average attempt results for grading. */
50     const GRADEAVERAGEATTEMPT = 2;
52     /** Use last attempt results for grading. */
53     const GRADELASTATTEMPT = 3;
55     /** Use first attempt results for grading. */
56     const GRADEFIRSTATTEMPT = 4;
58     /** @var stdClass course_module record. */
59     private $instance;
61     /** @var context_module the current context. */
62     private $context;
64     /** @var cm_info course_modules record. */
65     private $coursemodule;
67     /**
68      * Class contructor.
69      *
70      * @param cm_info $coursemodule course module info object
71      * @param stdClass $instance H5Pactivity instance object.
72      */
73     public function __construct(cm_info $coursemodule, stdClass $instance) {
74         $this->coursemodule = $coursemodule;
75         $this->instance = $instance;
76         $this->context = context_module::instance($coursemodule->id);
77         $this->instance->cmidnumber = $coursemodule->idnumber;
78     }
80     /**
81      * Create a manager instance from an instance record.
82      *
83      * @param stdClass $instance a h5pactivity record
84      * @return manager
85      */
86     public static function create_from_instance(stdClass $instance): self {
87         $coursemodule = get_coursemodule_from_instance('h5pactivity', $instance->id);
88         // Ensure that $this->coursemodule is a cm_info object.
89         $coursemodule = cm_info::create($coursemodule);
90         return new self($coursemodule, $instance);
91     }
93     /**
94      * Create a manager instance from an course_modules record.
95      *
96      * @param stdClass|cm_info $coursemodule a h5pactivity record
97      * @return manager
98      */
99     public static function create_from_coursemodule($coursemodule): self {
100         global $DB;
101         // Ensure that $this->coursemodule is a cm_info object.
102         $coursemodule = cm_info::create($coursemodule);
103         $instance = $DB->get_record('h5pactivity', ['id' => $coursemodule->instance], '*', MUST_EXIST);
104         return new self($coursemodule, $instance);
105     }
107     /**
108      * Return the available grading methods.
109      * @return string[] an array "option value" => "option description"
110      */
111     public static function get_grading_methods(): array {
112         return [
113             self::GRADEHIGHESTATTEMPT => get_string('grade_highest_attempt', 'mod_h5pactivity'),
114             self::GRADEAVERAGEATTEMPT => get_string('grade_average_attempt', 'mod_h5pactivity'),
115             self::GRADELASTATTEMPT => get_string('grade_last_attempt', 'mod_h5pactivity'),
116             self::GRADEFIRSTATTEMPT => get_string('grade_first_attempt', 'mod_h5pactivity'),
117             self::GRADEMANUAL => get_string('grade_manual', 'mod_h5pactivity'),
118         ];
119     }
121     /**
122      * Check if tracking is enabled in a particular h5pactivity for a specific user.
123      *
124      * @param stdClass|null $user user record (default $USER)
125      * @return bool if tracking is enabled in this activity
126      */
127     public function is_tracking_enabled(stdClass $user = null): bool {
128         global $USER;
129         if (!$this->instance->enabletracking) {
130             return false;
131         }
132         if (empty($user)) {
133             $user = $USER;
134         }
135         return has_capability('mod/h5pactivity:submit', $this->context, $user, false);
136     }
138     /**
139      * Return a relation of userid and the valid attempt's scaled score.
140      *
141      * The returned elements contain a record
142      * of userid, scaled value, attemptid and timemodified. In case the grading method is "GRADEAVERAGEATTEMPT"
143      * the attemptid will be zero. In case that tracking is disabled or grading method is "GRADEMANUAL"
144      * the method will return null.
145      *
146      * @param int $userid a specific userid or 0 for all user attempts.
147      * @return array|null of userid, scaled value and, if exists, the attempt id
148      */
149     public function get_users_scaled_score(int $userid = 0): ?array {
150         global $DB;
152         $scaled = [];
153         if (!$this->instance->enabletracking) {
154             return null;
155         }
157         if ($this->instance->grademethod == self::GRADEMANUAL) {
158             return null;
159         }
161         $sql = '';
163         // General filter.
164         $where = 'a.h5pactivityid = :h5pactivityid';
165         $params['h5pactivityid'] = $this->instance->id;
167         if ($userid) {
168             $where .= ' AND a.userid = :userid';
169             $params['userid'] = $userid;
170         }
172         // Average grading needs aggregation query.
173         if ($this->instance->grademethod == self::GRADEAVERAGEATTEMPT) {
174             $sql = "SELECT a.userid, AVG(a.scaled) AS scaled, 0 AS attemptid, MAX(timemodified) AS timemodified
175                       FROM {h5pactivity_attempts} a
176                      WHERE $where AND a.completion = 1
177                   GROUP BY a.userid";
178         }
180         if (empty($sql)) {
181             // Decide which attempt is used for the calculation.
182             $condition = [
183                 self::GRADEHIGHESTATTEMPT => "a.scaled < b.scaled",
184                 self::GRADELASTATTEMPT => "a.attempt < b.attempt",
185                 self::GRADEFIRSTATTEMPT => "a.attempt > b.attempt",
186             ];
187             $join = $condition[$this->instance->grademethod] ?? $condition[self::GRADEHIGHESTATTEMPT];
189             $sql = "SELECT a.userid, a.scaled, MAX(a.id) AS attemptid, MAX(a.timemodified) AS timemodified
190                       FROM {h5pactivity_attempts} a
191                  LEFT JOIN {h5pactivity_attempts} b ON a.h5pactivityid = b.h5pactivityid
192                            AND a.userid = b.userid AND b.completion = 1
193                            AND $join
194                      WHERE $where AND b.id IS NULL AND a.completion = 1
195                   GROUP BY a.userid, a.scaled";
196         }
198         return $DB->get_records_sql($sql, $params);
199     }
201     /**
202      * Return the current context.
203      *
204      * @return context_module
205      */
206     public function get_context(): context_module {
207         return $this->context;
208     }
210     /**
211      * Return the current context.
212      *
213      * @return stdClass the instance record
214      */
215     public function get_instance(): stdClass {
216         return $this->instance;
217     }
219     /**
220      * Return the current cm_info.
221      *
222      * @return cm_info the course module
223      */
224     public function get_coursemodule(): cm_info {
225         return $this->coursemodule;
226     }
228     /**
229      * Return the specific grader object for this activity.
230      *
231      * @return grader
232      */
233     public function get_grader(): grader {
234         $idnumber = $this->coursemodule->idnumber ?? '';
235         return new grader($this->instance, $idnumber);
236     }