MDL-68241 mod_h5pactivity: add grading attempts options
[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
28use context_module;
29use cm_info;
30use moodle_recordset;
31use stdClass;
32
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 */
41class manager {
42
43 /** No automathic grading using attempt results. */
44 const GRADEMANUAL = 0;
45
46 /** Use highest attempt results for grading. */
47 const GRADEHIGHESTATTEMPT = 1;
48
49 /** Use average attempt results for grading. */
50 const GRADEAVERAGEATTEMPT = 2;
51
52 /** Use last attempt results for grading. */
53 const GRADELASTATTEMPT = 3;
54
55 /** Use first attempt results for grading. */
56 const GRADEFIRSTATTEMPT = 4;
57
58 /** @var stdClass course_module record. */
59 private $instance;
60
61 /** @var context_module the current context. */
62 private $context;
63
64 /** @var cm_info course_modules record. */
65 private $coursemodule;
66
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 }
79
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 }
92
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 }
106
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 }
120
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 }
137
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;
151
152 $scaled = [];
153 if (!$this->instance->enabletracking) {
154 return null;
155 }
156
157 if ($this->instance->grademethod == self::GRADEMANUAL) {
158 return null;
159 }
160
161 $sql = '';
162
163 // General filter.
164 $where = 'a.h5pactivityid = :h5pactivityid';
165 $params['h5pactivityid'] = $this->instance->id;
166
167 if ($userid) {
168 $where .= ' AND a.userid = :userid';
169 $params['userid'] = $userid;
170 }
171
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 }
179
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];
188
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 }
197
198 return $DB->get_records_sql($sql, $params);
199 }
200
201 /**
202 * Return the current context.
203 *
204 * @return context_module
205 */
206 public function get_context(): context_module {
207 return $this->context;
208 }
209
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 }
218
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 }
227
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 }
237}