1 |
efrain |
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 |
* Definition of a class to represent a grade item
|
|
|
19 |
*
|
|
|
20 |
* @package core_grades
|
|
|
21 |
* @category grade
|
|
|
22 |
* @copyright 2006 Nicolas Connault
|
|
|
23 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
24 |
*/
|
|
|
25 |
|
|
|
26 |
defined('MOODLE_INTERNAL') || die();
|
|
|
27 |
require_once('grade_object.php');
|
|
|
28 |
|
|
|
29 |
/**
|
|
|
30 |
* Class representing a grade item.
|
|
|
31 |
*
|
|
|
32 |
* It is responsible for handling its DB representation, modifying and returning its metadata.
|
|
|
33 |
*
|
|
|
34 |
* @package core_grades
|
|
|
35 |
* @category grade
|
|
|
36 |
* @copyright 2006 Nicolas Connault
|
|
|
37 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
38 |
*/
|
|
|
39 |
class grade_item extends grade_object {
|
|
|
40 |
/**
|
|
|
41 |
* DB Table (used by grade_object).
|
|
|
42 |
* @var string $table
|
|
|
43 |
*/
|
|
|
44 |
public $table = 'grade_items';
|
|
|
45 |
|
|
|
46 |
/**
|
|
|
47 |
* Array of required table fields, must start with 'id'.
|
|
|
48 |
* @var array $required_fields
|
|
|
49 |
*/
|
|
|
50 |
public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
|
|
|
51 |
'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
|
|
|
52 |
'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
|
|
|
53 |
'aggregationcoef2', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
|
|
|
54 |
'needsupdate', 'weightoverride', 'timecreated', 'timemodified');
|
|
|
55 |
|
|
|
56 |
/**
|
|
|
57 |
* The course this grade_item belongs to.
|
|
|
58 |
* @var int $courseid
|
|
|
59 |
*/
|
|
|
60 |
public $courseid;
|
|
|
61 |
|
|
|
62 |
/**
|
|
|
63 |
* The category this grade_item belongs to (optional).
|
|
|
64 |
* @var int $categoryid
|
|
|
65 |
*/
|
|
|
66 |
public $categoryid;
|
|
|
67 |
|
|
|
68 |
/**
|
|
|
69 |
* The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'.
|
|
|
70 |
* @var grade_category $item_category
|
|
|
71 |
*/
|
|
|
72 |
public $item_category;
|
|
|
73 |
|
|
|
74 |
/**
|
|
|
75 |
* The grade_category object referenced by $this->categoryid.
|
|
|
76 |
* @var grade_category $parent_category
|
|
|
77 |
*/
|
|
|
78 |
public $parent_category;
|
|
|
79 |
|
|
|
80 |
|
|
|
81 |
/**
|
|
|
82 |
* The name of this grade_item (pushed by the module).
|
|
|
83 |
* @var string $itemname
|
|
|
84 |
*/
|
|
|
85 |
public $itemname;
|
|
|
86 |
|
|
|
87 |
/**
|
|
|
88 |
* e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
|
|
|
89 |
* @var string $itemtype
|
|
|
90 |
*/
|
|
|
91 |
public $itemtype;
|
|
|
92 |
|
|
|
93 |
/**
|
|
|
94 |
* The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
|
|
|
95 |
* @var string $itemmodule
|
|
|
96 |
*/
|
|
|
97 |
public $itemmodule;
|
|
|
98 |
|
|
|
99 |
/**
|
|
|
100 |
* ID of the item module
|
|
|
101 |
* @var int $iteminstance
|
|
|
102 |
*/
|
|
|
103 |
public $iteminstance;
|
|
|
104 |
|
|
|
105 |
/**
|
|
|
106 |
* Number of the item in a series of multiple grades pushed by an activity.
|
|
|
107 |
* @var int $itemnumber
|
|
|
108 |
*/
|
|
|
109 |
public $itemnumber;
|
|
|
110 |
|
|
|
111 |
/**
|
|
|
112 |
* Info and notes about this item.
|
|
|
113 |
* @var string $iteminfo
|
|
|
114 |
*/
|
|
|
115 |
public $iteminfo;
|
|
|
116 |
|
|
|
117 |
/**
|
|
|
118 |
* Arbitrary idnumber provided by the module responsible.
|
|
|
119 |
* @var string $idnumber
|
|
|
120 |
*/
|
|
|
121 |
public $idnumber;
|
|
|
122 |
|
|
|
123 |
/**
|
|
|
124 |
* Calculation string used for this item.
|
|
|
125 |
* @var string $calculation
|
|
|
126 |
*/
|
|
|
127 |
public $calculation;
|
|
|
128 |
|
|
|
129 |
/**
|
|
|
130 |
* Indicates if we already tried to normalize the grade calculation formula.
|
|
|
131 |
* This flag helps to minimize db access when broken formulas used in calculation.
|
|
|
132 |
* @var bool
|
|
|
133 |
*/
|
|
|
134 |
public $calculation_normalized;
|
|
|
135 |
/**
|
|
|
136 |
* Math evaluation object
|
|
|
137 |
* @var calc_formula A formula object
|
|
|
138 |
*/
|
|
|
139 |
public $formula;
|
|
|
140 |
|
|
|
141 |
/**
|
|
|
142 |
* The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
|
|
|
143 |
* @var int $gradetype
|
|
|
144 |
*/
|
|
|
145 |
public $gradetype = GRADE_TYPE_VALUE;
|
|
|
146 |
|
|
|
147 |
/**
|
|
|
148 |
* Maximum allowable grade.
|
|
|
149 |
* @var float $grademax
|
|
|
150 |
*/
|
|
|
151 |
public $grademax = 100;
|
|
|
152 |
|
|
|
153 |
/**
|
|
|
154 |
* Minimum allowable grade.
|
|
|
155 |
* @var float $grademin
|
|
|
156 |
*/
|
|
|
157 |
public $grademin = 0;
|
|
|
158 |
|
|
|
159 |
/**
|
|
|
160 |
* id of the scale, if this grade is based on a scale.
|
|
|
161 |
* @var int $scaleid
|
|
|
162 |
*/
|
|
|
163 |
public $scaleid;
|
|
|
164 |
|
|
|
165 |
/**
|
|
|
166 |
* The grade_scale object referenced by $this->scaleid.
|
|
|
167 |
* @var grade_scale $scale
|
|
|
168 |
*/
|
|
|
169 |
public $scale;
|
|
|
170 |
|
|
|
171 |
/**
|
|
|
172 |
* The id of the optional grade_outcome associated with this grade_item.
|
|
|
173 |
* @var int $outcomeid
|
|
|
174 |
*/
|
|
|
175 |
public $outcomeid;
|
|
|
176 |
|
|
|
177 |
/**
|
|
|
178 |
* The grade_outcome this grade is associated with, if applicable.
|
|
|
179 |
* @var grade_outcome $outcome
|
|
|
180 |
*/
|
|
|
181 |
public $outcome;
|
|
|
182 |
|
|
|
183 |
/**
|
|
|
184 |
* grade required to pass. (grademin <= gradepass <= grademax)
|
|
|
185 |
* @var float $gradepass
|
|
|
186 |
*/
|
|
|
187 |
public $gradepass = 0;
|
|
|
188 |
|
|
|
189 |
/**
|
|
|
190 |
* Multiply all grades by this number.
|
|
|
191 |
* @var float $multfactor
|
|
|
192 |
*/
|
|
|
193 |
public $multfactor = 1.0;
|
|
|
194 |
|
|
|
195 |
/**
|
|
|
196 |
* Add this to all grades.
|
|
|
197 |
* @var float $plusfactor
|
|
|
198 |
*/
|
|
|
199 |
public $plusfactor = 0;
|
|
|
200 |
|
|
|
201 |
/**
|
|
|
202 |
* Aggregation coeficient used for weighted averages or extra credit
|
|
|
203 |
* @var float $aggregationcoef
|
|
|
204 |
*/
|
|
|
205 |
public $aggregationcoef = 0;
|
|
|
206 |
|
|
|
207 |
/**
|
|
|
208 |
* Aggregation coeficient used for weighted averages only
|
|
|
209 |
* @var float $aggregationcoef2
|
|
|
210 |
*/
|
|
|
211 |
public $aggregationcoef2 = 0;
|
|
|
212 |
|
|
|
213 |
/**
|
|
|
214 |
* Sorting order of the columns.
|
|
|
215 |
* @var int $sortorder
|
|
|
216 |
*/
|
|
|
217 |
public $sortorder = 0;
|
|
|
218 |
|
|
|
219 |
/**
|
|
|
220 |
* Display type of the grades (Real, Percentage, Letter, or default).
|
|
|
221 |
* @var int $display
|
|
|
222 |
*/
|
|
|
223 |
public $display = GRADE_DISPLAY_TYPE_DEFAULT;
|
|
|
224 |
|
|
|
225 |
/**
|
|
|
226 |
* The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
|
|
|
227 |
* @var int $decimals
|
|
|
228 |
*/
|
|
|
229 |
public $decimals = null;
|
|
|
230 |
|
|
|
231 |
/**
|
|
|
232 |
* Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
|
|
|
233 |
* @var int $locked
|
|
|
234 |
*/
|
|
|
235 |
public $locked = 0;
|
|
|
236 |
|
|
|
237 |
/**
|
|
|
238 |
* Date after which the grade will be locked. Empty means no automatic locking.
|
|
|
239 |
* @var int $locktime
|
|
|
240 |
*/
|
|
|
241 |
public $locktime = 0;
|
|
|
242 |
|
|
|
243 |
/**
|
|
|
244 |
* If set, the whole column will be recalculated, then this flag will be switched off.
|
|
|
245 |
* @var bool $needsupdate
|
|
|
246 |
*/
|
|
|
247 |
public $needsupdate = 1;
|
|
|
248 |
|
|
|
249 |
/**
|
|
|
250 |
* If set, the grade item's weight has been overridden by a user and should not be automatically adjusted.
|
|
|
251 |
*/
|
|
|
252 |
public $weightoverride = 0;
|
|
|
253 |
|
|
|
254 |
/**
|
|
|
255 |
* Cached dependson array
|
|
|
256 |
* @var array An array of cached grade item dependencies.
|
|
|
257 |
*/
|
|
|
258 |
public $dependson_cache = null;
|
|
|
259 |
|
|
|
260 |
/**
|
|
|
261 |
* @var bool If we regrade this item should we mark it as overridden?
|
|
|
262 |
*/
|
|
|
263 |
public $markasoverriddenwhengraded = true;
|
|
|
264 |
|
|
|
265 |
/**
|
|
|
266 |
* @var int course module ID
|
|
|
267 |
*/
|
|
|
268 |
public $cmid;
|
|
|
269 |
|
|
|
270 |
/**
|
|
|
271 |
* @var string average information.
|
|
|
272 |
*/
|
|
|
273 |
public $avg;
|
|
|
274 |
|
|
|
275 |
/**
|
|
|
276 |
* Category name.
|
|
|
277 |
* @var string
|
|
|
278 |
*/
|
|
|
279 |
public $category;
|
|
|
280 |
|
|
|
281 |
/**
|
|
|
282 |
* Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
|
|
|
283 |
*
|
|
|
284 |
* @param array $params An array with required parameters for this grade object.
|
|
|
285 |
* @param bool $fetch Whether to fetch corresponding row from the database or not,
|
|
|
286 |
* optional fields might not be defined if false used
|
|
|
287 |
*/
|
|
|
288 |
public function __construct($params = null, $fetch = true) {
|
|
|
289 |
global $CFG;
|
|
|
290 |
// Set grademax from $CFG->gradepointdefault .
|
|
|
291 |
self::set_properties($this, array('grademax' => $CFG->gradepointdefault));
|
|
|
292 |
parent::__construct($params, $fetch);
|
|
|
293 |
}
|
|
|
294 |
|
|
|
295 |
/**
|
|
|
296 |
* In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
|
|
|
297 |
* Force regrading if necessary, rounds the float numbers using php function,
|
|
|
298 |
* the reason is we need to compare the db value with computed number to skip regrading if possible.
|
|
|
299 |
*
|
|
|
300 |
* @param string $source from where was the object inserted (mod/forum, manual, etc.)
|
|
|
301 |
* @param bool $isbulkupdate If bulk grade update is happening.
|
|
|
302 |
* @return bool success
|
|
|
303 |
*/
|
|
|
304 |
public function update($source = null, $isbulkupdate = false) {
|
|
|
305 |
// reset caches
|
|
|
306 |
$this->dependson_cache = null;
|
|
|
307 |
|
|
|
308 |
// Retrieve scale and infer grademax/min from it if needed
|
|
|
309 |
$this->load_scale();
|
|
|
310 |
|
|
|
311 |
// make sure there is not 0 in outcomeid
|
|
|
312 |
if (empty($this->outcomeid)) {
|
|
|
313 |
$this->outcomeid = null;
|
|
|
314 |
}
|
|
|
315 |
|
|
|
316 |
if ($this->qualifies_for_regrading()) {
|
|
|
317 |
$this->force_regrading();
|
|
|
318 |
}
|
|
|
319 |
|
|
|
320 |
$this->timemodified = time();
|
|
|
321 |
|
|
|
322 |
$this->grademin = grade_floatval($this->grademin);
|
|
|
323 |
$this->grademax = grade_floatval($this->grademax);
|
|
|
324 |
$this->multfactor = grade_floatval($this->multfactor);
|
|
|
325 |
$this->plusfactor = grade_floatval($this->plusfactor);
|
|
|
326 |
$this->aggregationcoef = grade_floatval($this->aggregationcoef);
|
|
|
327 |
$this->aggregationcoef2 = grade_floatval($this->aggregationcoef2);
|
|
|
328 |
|
|
|
329 |
$result = parent::update($source, $isbulkupdate);
|
|
|
330 |
|
|
|
331 |
if ($result) {
|
|
|
332 |
$event = \core\event\grade_item_updated::create_from_grade_item($this);
|
|
|
333 |
$event->trigger();
|
|
|
334 |
}
|
|
|
335 |
|
|
|
336 |
return $result;
|
|
|
337 |
}
|
|
|
338 |
|
|
|
339 |
/**
|
|
|
340 |
* Compares the values held by this object with those of the matching record in DB, and returns
|
|
|
341 |
* whether or not these differences are sufficient to justify an update of all parent objects.
|
|
|
342 |
* This assumes that this object has an id number and a matching record in DB. If not, it will return false.
|
|
|
343 |
*
|
|
|
344 |
* @return bool
|
|
|
345 |
*/
|
|
|
346 |
public function qualifies_for_regrading() {
|
|
|
347 |
if (empty($this->id)) {
|
|
|
348 |
return false;
|
|
|
349 |
}
|
|
|
350 |
|
|
|
351 |
$db_item = new grade_item(array('id' => $this->id));
|
|
|
352 |
|
|
|
353 |
$calculationdiff = $db_item->calculation != $this->calculation;
|
|
|
354 |
$categorydiff = $db_item->categoryid != $this->categoryid;
|
|
|
355 |
$gradetypediff = $db_item->gradetype != $this->gradetype;
|
|
|
356 |
$scaleiddiff = $db_item->scaleid != $this->scaleid;
|
|
|
357 |
$outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
|
|
|
358 |
$locktimediff = $db_item->locktime != $this->locktime;
|
|
|
359 |
$grademindiff = grade_floats_different($db_item->grademin, $this->grademin);
|
|
|
360 |
$grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax);
|
|
|
361 |
$multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor);
|
|
|
362 |
$plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor);
|
|
|
363 |
$acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
|
|
|
364 |
$acoefdiff2 = grade_floats_different($db_item->aggregationcoef2, $this->aggregationcoef2);
|
|
|
365 |
$weightoverride = grade_floats_different($db_item->weightoverride, $this->weightoverride);
|
|
|
366 |
|
|
|
367 |
$needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
|
|
|
368 |
$lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
|
|
|
369 |
|
|
|
370 |
return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
|
|
|
371 |
|| $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
|
|
|
372 |
|| $lockeddiff || $acoefdiff || $acoefdiff2 || $weightoverride || $locktimediff);
|
|
|
373 |
}
|
|
|
374 |
|
|
|
375 |
/**
|
|
|
376 |
* Finds and returns a grade_item instance based on params.
|
|
|
377 |
*
|
|
|
378 |
* @static
|
|
|
379 |
* @param array $params associative arrays varname=>value
|
|
|
380 |
* @return grade_item|bool Returns a grade_item instance or false if none found
|
|
|
381 |
*/
|
|
|
382 |
public static function fetch($params) {
|
|
|
383 |
return grade_object::fetch_helper('grade_items', 'grade_item', $params);
|
|
|
384 |
}
|
|
|
385 |
|
|
|
386 |
/**
|
|
|
387 |
* Check to see if there are any existing grades for this grade_item.
|
|
|
388 |
*
|
|
|
389 |
* @return boolean - true if there are valid grades for this grade_item.
|
|
|
390 |
*/
|
|
|
391 |
public function has_grades() {
|
|
|
392 |
global $DB;
|
|
|
393 |
|
|
|
394 |
$count = $DB->count_records_select('grade_grades',
|
|
|
395 |
'itemid = :gradeitemid AND finalgrade IS NOT NULL',
|
|
|
396 |
array('gradeitemid' => $this->id));
|
|
|
397 |
return $count > 0;
|
|
|
398 |
}
|
|
|
399 |
|
|
|
400 |
/**
|
|
|
401 |
* Check to see if there are existing overridden grades for this grade_item.
|
|
|
402 |
*
|
|
|
403 |
* @return boolean - true if there are overridden grades for this grade_item.
|
|
|
404 |
*/
|
|
|
405 |
public function has_overridden_grades() {
|
|
|
406 |
global $DB;
|
|
|
407 |
|
|
|
408 |
$count = $DB->count_records_select('grade_grades',
|
|
|
409 |
'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
|
|
|
410 |
array('gradeitemid' => $this->id));
|
|
|
411 |
return $count > 0;
|
|
|
412 |
}
|
|
|
413 |
|
|
|
414 |
/**
|
|
|
415 |
* Finds and returns all grade_item instances based on params.
|
|
|
416 |
*
|
|
|
417 |
* @static
|
|
|
418 |
* @param array $params associative arrays varname=>value
|
|
|
419 |
* @return array array of grade_item instances or false if none found.
|
|
|
420 |
*/
|
|
|
421 |
public static function fetch_all($params) {
|
|
|
422 |
return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
|
|
|
423 |
}
|
|
|
424 |
|
|
|
425 |
/**
|
|
|
426 |
* Delete all grades and force_regrading of parent category.
|
|
|
427 |
*
|
|
|
428 |
* @param string $source from where was the object deleted (mod/forum, manual, etc.)
|
|
|
429 |
* @return bool success
|
|
|
430 |
*/
|
|
|
431 |
public function delete($source=null) {
|
|
|
432 |
global $DB;
|
|
|
433 |
|
|
|
434 |
try {
|
|
|
435 |
$transaction = $DB->start_delegated_transaction();
|
|
|
436 |
$this->delete_all_grades($source);
|
|
|
437 |
$success = parent::delete($source);
|
|
|
438 |
if ($success) {
|
|
|
439 |
$event = \core\event\grade_item_deleted::create_from_grade_item($this);
|
|
|
440 |
$event->trigger();
|
|
|
441 |
}
|
|
|
442 |
$transaction->allow_commit();
|
|
|
443 |
} catch (Exception $e) {
|
|
|
444 |
$transaction->rollback($e);
|
|
|
445 |
}
|
|
|
446 |
return $success;
|
|
|
447 |
}
|
|
|
448 |
|
|
|
449 |
/**
|
|
|
450 |
* Delete all grades
|
|
|
451 |
*
|
|
|
452 |
* @param string $source from where was the object deleted (mod/forum, manual, etc.)
|
|
|
453 |
* @return bool
|
|
|
454 |
*/
|
|
|
455 |
public function delete_all_grades($source=null) {
|
|
|
456 |
global $DB;
|
|
|
457 |
|
|
|
458 |
try {
|
|
|
459 |
$transaction = $DB->start_delegated_transaction();
|
|
|
460 |
|
|
|
461 |
if (!$this->is_course_item()) {
|
|
|
462 |
$this->force_regrading();
|
|
|
463 |
}
|
|
|
464 |
|
|
|
465 |
if ($grades = grade_grade::fetch_all(['itemid' => $this->id])) {
|
|
|
466 |
foreach ($grades as $grade) {
|
|
|
467 |
$grade->delete($source);
|
|
|
468 |
}
|
|
|
469 |
}
|
|
|
470 |
|
|
|
471 |
// Delete all the historical files.
|
|
|
472 |
// We only support feedback files for modules atm.
|
|
|
473 |
if ($this->is_external_item()) {
|
|
|
474 |
$fs = new file_storage();
|
|
|
475 |
$fs->delete_area_files($this->get_context()->id, GRADE_FILE_COMPONENT, GRADE_HISTORY_FEEDBACK_FILEAREA);
|
|
|
476 |
}
|
|
|
477 |
|
|
|
478 |
$transaction->allow_commit();
|
|
|
479 |
} catch (Exception $e) {
|
|
|
480 |
$transaction->rollback($e);
|
|
|
481 |
}
|
|
|
482 |
return true;
|
|
|
483 |
}
|
|
|
484 |
|
|
|
485 |
/**
|
|
|
486 |
* Duplicate grade item.
|
|
|
487 |
*
|
|
|
488 |
* @return grade_item The duplicate grade item
|
|
|
489 |
*/
|
|
|
490 |
public function duplicate() {
|
|
|
491 |
// Convert current object to array.
|
|
|
492 |
$copy = (array) $this;
|
|
|
493 |
|
|
|
494 |
if (empty($copy["id"])) {
|
|
|
495 |
throw new moodle_exception('invalidgradeitemid');
|
|
|
496 |
}
|
|
|
497 |
|
|
|
498 |
// Remove fields that will be either unique or automatically filled.
|
|
|
499 |
$removekeys = array();
|
|
|
500 |
$removekeys[] = 'id';
|
|
|
501 |
$removekeys[] = 'idnumber';
|
|
|
502 |
$removekeys[] = 'timecreated';
|
|
|
503 |
$removekeys[] = 'sortorder';
|
|
|
504 |
foreach ($removekeys as $key) {
|
|
|
505 |
unset($copy[$key]);
|
|
|
506 |
}
|
|
|
507 |
|
|
|
508 |
// Addendum to name.
|
|
|
509 |
$copy["itemname"] = get_string('duplicatedgradeitem', 'grades', $copy["itemname"]);
|
|
|
510 |
|
|
|
511 |
// Create new grade item.
|
|
|
512 |
$gradeitem = new grade_item($copy);
|
|
|
513 |
|
|
|
514 |
// Insert grade item into database.
|
|
|
515 |
$gradeitem->insert();
|
|
|
516 |
|
|
|
517 |
return $gradeitem;
|
|
|
518 |
}
|
|
|
519 |
|
|
|
520 |
/**
|
|
|
521 |
* In addition to perform parent::insert(), calls force_regrading() method too.
|
|
|
522 |
*
|
|
|
523 |
* @param string $source From where was the object inserted (mod/forum, manual, etc.)
|
|
|
524 |
* @param string $isbulkupdate If bulk grade update is happening.
|
|
|
525 |
* @return int PK ID if successful, false otherwise
|
|
|
526 |
*/
|
|
|
527 |
public function insert($source = null, $isbulkupdate = false) {
|
|
|
528 |
global $CFG, $DB;
|
|
|
529 |
|
|
|
530 |
if (empty($this->courseid)) {
|
|
|
531 |
throw new \moodle_exception('cannotinsertgrade');
|
|
|
532 |
}
|
|
|
533 |
|
|
|
534 |
// load scale if needed
|
|
|
535 |
$this->load_scale();
|
|
|
536 |
|
|
|
537 |
// add parent category if needed
|
|
|
538 |
if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
|
|
|
539 |
$course_category = grade_category::fetch_course_category($this->courseid);
|
|
|
540 |
$this->categoryid = $course_category->id;
|
|
|
541 |
|
|
|
542 |
}
|
|
|
543 |
|
|
|
544 |
// always place the new items at the end, move them after insert if needed
|
|
|
545 |
$last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
|
|
|
546 |
if (!empty($last_sortorder)) {
|
|
|
547 |
$this->sortorder = $last_sortorder + 1;
|
|
|
548 |
} else {
|
|
|
549 |
$this->sortorder = 1;
|
|
|
550 |
}
|
|
|
551 |
|
|
|
552 |
// add proper item numbers to manual items
|
|
|
553 |
if ($this->itemtype == 'manual') {
|
|
|
554 |
if (empty($this->itemnumber)) {
|
|
|
555 |
$this->itemnumber = 0;
|
|
|
556 |
}
|
|
|
557 |
}
|
|
|
558 |
|
|
|
559 |
// make sure there is not 0 in outcomeid
|
|
|
560 |
if (empty($this->outcomeid)) {
|
|
|
561 |
$this->outcomeid = null;
|
|
|
562 |
}
|
|
|
563 |
|
|
|
564 |
$this->timecreated = $this->timemodified = time();
|
|
|
565 |
|
|
|
566 |
if (parent::insert($source, $isbulkupdate)) {
|
|
|
567 |
// force regrading of items if needed
|
|
|
568 |
$this->force_regrading();
|
|
|
569 |
|
|
|
570 |
$event = \core\event\grade_item_created::create_from_grade_item($this);
|
|
|
571 |
$event->trigger();
|
|
|
572 |
|
|
|
573 |
return $this->id;
|
|
|
574 |
|
|
|
575 |
} else {
|
|
|
576 |
debugging("Could not insert this grade_item in the database!");
|
|
|
577 |
return false;
|
|
|
578 |
}
|
|
|
579 |
}
|
|
|
580 |
|
|
|
581 |
/**
|
|
|
582 |
* Set idnumber of grade item, updates also course_modules table
|
|
|
583 |
*
|
|
|
584 |
* @param string $idnumber (without magic quotes)
|
|
|
585 |
* @return bool success
|
|
|
586 |
*/
|
|
|
587 |
public function add_idnumber($idnumber) {
|
|
|
588 |
global $DB;
|
|
|
589 |
if (!empty($this->idnumber)) {
|
|
|
590 |
return false;
|
|
|
591 |
}
|
|
|
592 |
|
|
|
593 |
if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
|
|
|
594 |
if ($this->itemnumber == 0) {
|
|
|
595 |
// for activity modules, itemnumber 0 is synced with the course_modules
|
|
|
596 |
if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
|
|
|
597 |
return false;
|
|
|
598 |
}
|
|
|
599 |
if (!empty($cm->idnumber)) {
|
|
|
600 |
return false;
|
|
|
601 |
}
|
|
|
602 |
$DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id));
|
|
|
603 |
$this->idnumber = $idnumber;
|
|
|
604 |
return $this->update();
|
|
|
605 |
} else {
|
|
|
606 |
$this->idnumber = $idnumber;
|
|
|
607 |
return $this->update();
|
|
|
608 |
}
|
|
|
609 |
|
|
|
610 |
} else {
|
|
|
611 |
$this->idnumber = $idnumber;
|
|
|
612 |
return $this->update();
|
|
|
613 |
}
|
|
|
614 |
}
|
|
|
615 |
|
|
|
616 |
/**
|
|
|
617 |
* Returns the locked state of this grade_item (if the grade_item is locked OR no specific
|
|
|
618 |
* $userid is given) or the locked state of a specific grade within this item if a specific
|
|
|
619 |
* $userid is given and the grade_item is unlocked.
|
|
|
620 |
*
|
|
|
621 |
* @param int $userid The user's ID
|
|
|
622 |
* @return bool Locked state
|
|
|
623 |
*/
|
|
|
624 |
public function is_locked($userid=NULL) {
|
|
|
625 |
global $CFG;
|
|
|
626 |
|
|
|
627 |
// Override for any grade items belonging to activities which are in the process of being deleted.
|
|
|
628 |
require_once($CFG->dirroot . '/course/lib.php');
|
|
|
629 |
if (course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance)) {
|
|
|
630 |
return true;
|
|
|
631 |
}
|
|
|
632 |
|
|
|
633 |
if (!empty($this->locked)) {
|
|
|
634 |
return true;
|
|
|
635 |
}
|
|
|
636 |
|
|
|
637 |
if (!empty($userid)) {
|
|
|
638 |
if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
|
|
|
639 |
$grade->grade_item =& $this; // prevent db fetching of cached grade_item
|
|
|
640 |
return $grade->is_locked();
|
|
|
641 |
}
|
|
|
642 |
}
|
|
|
643 |
|
|
|
644 |
return false;
|
|
|
645 |
}
|
|
|
646 |
|
|
|
647 |
/**
|
|
|
648 |
* Locks or unlocks this grade_item and (optionally) all its associated final grades.
|
|
|
649 |
*
|
|
|
650 |
* @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
|
|
|
651 |
* @param bool $cascade Lock/unlock child objects too
|
|
|
652 |
* @param bool $refresh Refresh grades when unlocking
|
|
|
653 |
* @return bool True if grade_item all grades updated, false if at least one update fails
|
|
|
654 |
*/
|
|
|
655 |
public function set_locked($lockedstate, $cascade=false, $refresh=true) {
|
|
|
656 |
if ($lockedstate) {
|
|
|
657 |
// Setting lock.
|
|
|
658 |
if (empty($this->id)) {
|
|
|
659 |
return false;
|
|
|
660 |
} else if ($this->needsupdate) {
|
|
|
661 |
// Can not lock grade without first having final grade,
|
|
|
662 |
// so we schedule it to be locked as soon as regrading is finished.
|
|
|
663 |
$this->locktime = time() - 1;
|
|
|
664 |
} else {
|
|
|
665 |
$this->locked = time();
|
|
|
666 |
}
|
|
|
667 |
$this->update();
|
|
|
668 |
|
|
|
669 |
if ($cascade) {
|
|
|
670 |
$grades = $this->get_final();
|
|
|
671 |
foreach($grades as $g) {
|
|
|
672 |
$grade = new grade_grade($g, false);
|
|
|
673 |
$grade->grade_item =& $this;
|
|
|
674 |
$grade->set_locked(1, null, false);
|
|
|
675 |
}
|
|
|
676 |
}
|
|
|
677 |
|
|
|
678 |
return true;
|
|
|
679 |
|
|
|
680 |
} else {
|
|
|
681 |
/// removing lock
|
|
|
682 |
if (!empty($this->locked) and $this->locktime < time()) {
|
|
|
683 |
//we have to reset locktime or else it would lock up again
|
|
|
684 |
$this->locktime = 0;
|
|
|
685 |
}
|
|
|
686 |
|
|
|
687 |
$this->locked = 0;
|
|
|
688 |
$this->update();
|
|
|
689 |
|
|
|
690 |
if ($cascade) {
|
|
|
691 |
if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
|
|
|
692 |
foreach($grades as $grade) {
|
|
|
693 |
$grade->grade_item =& $this;
|
|
|
694 |
$grade->set_locked(0, null, false);
|
|
|
695 |
}
|
|
|
696 |
}
|
|
|
697 |
}
|
|
|
698 |
|
|
|
699 |
if ($refresh) {
|
|
|
700 |
//refresh when unlocking
|
|
|
701 |
$this->refresh_grades();
|
|
|
702 |
}
|
|
|
703 |
|
|
|
704 |
return true;
|
|
|
705 |
}
|
|
|
706 |
}
|
|
|
707 |
|
|
|
708 |
/**
|
|
|
709 |
* Lock the grade if needed. Make sure this is called only when final grades are valid
|
|
|
710 |
*/
|
|
|
711 |
public function check_locktime() {
|
|
|
712 |
if (!empty($this->locked)) {
|
|
|
713 |
return; // already locked
|
|
|
714 |
}
|
|
|
715 |
|
|
|
716 |
if ($this->locktime and $this->locktime < time()) {
|
|
|
717 |
$this->locked = time();
|
|
|
718 |
$this->update('locktime');
|
|
|
719 |
}
|
|
|
720 |
}
|
|
|
721 |
|
|
|
722 |
/**
|
|
|
723 |
* Set the locktime for this grade item.
|
|
|
724 |
*
|
|
|
725 |
* @param int $locktime timestamp for lock to activate
|
|
|
726 |
* @return void
|
|
|
727 |
*/
|
|
|
728 |
public function set_locktime($locktime) {
|
|
|
729 |
$this->locktime = $locktime;
|
|
|
730 |
$this->update();
|
|
|
731 |
}
|
|
|
732 |
|
|
|
733 |
/**
|
|
|
734 |
* Set the locktime for this grade item.
|
|
|
735 |
*
|
|
|
736 |
* @return int $locktime timestamp for lock to activate
|
|
|
737 |
*/
|
|
|
738 |
public function get_locktime() {
|
|
|
739 |
return $this->locktime;
|
|
|
740 |
}
|
|
|
741 |
|
|
|
742 |
/**
|
|
|
743 |
* Set the hidden status of grade_item and all grades.
|
|
|
744 |
*
|
|
|
745 |
* 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
|
|
|
746 |
*
|
|
|
747 |
* @param int $hidden new hidden status
|
|
|
748 |
* @param bool $cascade apply to child objects too
|
|
|
749 |
*/
|
|
|
750 |
public function set_hidden($hidden, $cascade=false) {
|
|
|
751 |
parent::set_hidden($hidden, $cascade);
|
|
|
752 |
|
|
|
753 |
if ($cascade) {
|
|
|
754 |
if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
|
|
|
755 |
foreach($grades as $grade) {
|
|
|
756 |
$grade->grade_item =& $this;
|
|
|
757 |
$grade->set_hidden($hidden, $cascade);
|
|
|
758 |
}
|
|
|
759 |
}
|
|
|
760 |
}
|
|
|
761 |
|
|
|
762 |
//if marking item visible make sure category is visible MDL-21367
|
|
|
763 |
if( !$hidden ) {
|
|
|
764 |
$category_array = grade_category::fetch_all(array('id'=>$this->categoryid));
|
|
|
765 |
if ($category_array && array_key_exists($this->categoryid, $category_array)) {
|
|
|
766 |
$category = $category_array[$this->categoryid];
|
|
|
767 |
//call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
|
|
|
768 |
$category->set_hidden($hidden, false);
|
|
|
769 |
}
|
|
|
770 |
}
|
|
|
771 |
}
|
|
|
772 |
|
|
|
773 |
/**
|
|
|
774 |
* Returns the number of grades that are hidden
|
|
|
775 |
*
|
|
|
776 |
* @param string $groupsql SQL to limit the query by group
|
|
|
777 |
* @param array $params SQL params for $groupsql
|
|
|
778 |
* @param string $groupwheresql Where conditions for $groupsql
|
|
|
779 |
* @return int The number of hidden grades
|
|
|
780 |
*/
|
|
|
781 |
public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
|
|
|
782 |
global $DB;
|
|
|
783 |
$params = (array)$params;
|
|
|
784 |
$params['itemid'] = $this->id;
|
|
|
785 |
|
|
|
786 |
return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
|
|
|
787 |
."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
|
|
|
788 |
}
|
|
|
789 |
|
|
|
790 |
/**
|
|
|
791 |
* Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades.
|
|
|
792 |
* Situations such as an error being found will still result in the regrading being finished.
|
|
|
793 |
*/
|
|
|
794 |
public function regrading_finished() {
|
|
|
795 |
global $DB;
|
|
|
796 |
$this->needsupdate = 0;
|
|
|
797 |
//do not use $this->update() because we do not want this logged in grade_item_history
|
|
|
798 |
$DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
|
|
|
799 |
}
|
|
|
800 |
|
|
|
801 |
/**
|
|
|
802 |
* Performs the necessary calculations on the grades_final referenced by this grade_item.
|
|
|
803 |
* Also resets the needsupdate flag once successfully performed.
|
|
|
804 |
*
|
|
|
805 |
* This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
|
|
|
806 |
* because the regrading must be done in correct order!!
|
|
|
807 |
*
|
|
|
808 |
* @param int $userid Supply a user ID to limit the regrading to a single user
|
|
|
809 |
* @param \core\progress\base|null $progress Optional progress object, will be updated per user
|
|
|
810 |
* @return bool true if ok, error string otherwise
|
|
|
811 |
*/
|
|
|
812 |
public function regrade_final_grades($userid=null, ?\core\progress\base $progress = null) {
|
|
|
813 |
global $CFG, $DB;
|
|
|
814 |
|
|
|
815 |
// locked grade items already have correct final grades
|
|
|
816 |
if ($this->is_locked()) {
|
|
|
817 |
return true;
|
|
|
818 |
}
|
|
|
819 |
|
|
|
820 |
// calculation produces final value using formula from other final values
|
|
|
821 |
if ($this->is_calculated()) {
|
|
|
822 |
if ($this->compute($userid)) {
|
|
|
823 |
return true;
|
|
|
824 |
} else {
|
|
|
825 |
return "Could not calculate grades for grade item"; // TODO: improve and localize
|
|
|
826 |
}
|
|
|
827 |
|
|
|
828 |
// noncalculated outcomes already have final values - raw grades not used
|
|
|
829 |
} else if ($this->is_outcome_item()) {
|
|
|
830 |
return true;
|
|
|
831 |
|
|
|
832 |
// aggregate the category grade
|
|
|
833 |
} else if ($this->is_category_item() or $this->is_course_item()) {
|
|
|
834 |
// aggregate category grade item
|
|
|
835 |
$category = $this->load_item_category();
|
|
|
836 |
$category->grade_item =& $this;
|
|
|
837 |
if ($category->generate_grades($userid, $progress)) {
|
|
|
838 |
return true;
|
|
|
839 |
} else {
|
|
|
840 |
return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
|
|
|
841 |
}
|
|
|
842 |
|
|
|
843 |
} else if ($this->is_manual_item()) {
|
|
|
844 |
// manual items track only final grades, no raw grades
|
|
|
845 |
return true;
|
|
|
846 |
|
|
|
847 |
} else if (!$this->is_raw_used()) {
|
|
|
848 |
// hmm - raw grades are not used- nothing to regrade
|
|
|
849 |
return true;
|
|
|
850 |
}
|
|
|
851 |
|
|
|
852 |
// normal grade item - just new final grades
|
|
|
853 |
$result = true;
|
|
|
854 |
$grade_inst = new grade_grade();
|
|
|
855 |
$fields = implode(',', $grade_inst->required_fields);
|
|
|
856 |
if ($userid) {
|
|
|
857 |
$params = array($this->id, $userid);
|
|
|
858 |
$rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
|
|
|
859 |
} else {
|
|
|
860 |
$rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
|
|
|
861 |
}
|
|
|
862 |
if ($rs) {
|
|
|
863 |
foreach ($rs as $grade_record) {
|
|
|
864 |
$grade = new grade_grade($grade_record, false);
|
|
|
865 |
|
|
|
866 |
// Incrementing the progress by nothing causes it to send an update (once per second)
|
|
|
867 |
// to the web browser so as to prevent the connection timing out.
|
|
|
868 |
if ($progress) {
|
|
|
869 |
$progress->increment_progress(0);
|
|
|
870 |
}
|
|
|
871 |
|
|
|
872 |
if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
|
|
|
873 |
// this grade is locked - final grade must be ok
|
|
|
874 |
continue;
|
|
|
875 |
}
|
|
|
876 |
|
|
|
877 |
$grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
|
|
|
878 |
|
|
|
879 |
if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
|
|
|
880 |
$success = $grade->update('system');
|
|
|
881 |
|
|
|
882 |
// If successful trigger a user_graded event.
|
|
|
883 |
if ($success) {
|
|
|
884 |
$grade->load_grade_item();
|
|
|
885 |
\core\event\user_graded::create_from_grade($grade, \core\event\base::USER_OTHER)->trigger();
|
|
|
886 |
} else {
|
|
|
887 |
$result = "Internal error updating final grade";
|
|
|
888 |
}
|
|
|
889 |
}
|
|
|
890 |
}
|
|
|
891 |
$rs->close();
|
|
|
892 |
}
|
|
|
893 |
|
|
|
894 |
return $result;
|
|
|
895 |
}
|
|
|
896 |
|
|
|
897 |
/**
|
|
|
898 |
* Given a float grade value or integer grade scale, applies a number of adjustment based on
|
|
|
899 |
* grade_item variables and returns the result.
|
|
|
900 |
*
|
|
|
901 |
* @param float $rawgrade The raw grade value
|
|
|
902 |
* @param float $rawmin original rawmin
|
|
|
903 |
* @param float $rawmax original rawmax
|
|
|
904 |
* @return mixed
|
|
|
905 |
*/
|
|
|
906 |
public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
|
|
|
907 |
if (is_null($rawgrade)) {
|
|
|
908 |
return null;
|
|
|
909 |
}
|
|
|
910 |
|
|
|
911 |
if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
|
|
|
912 |
|
|
|
913 |
if ($this->grademax < $this->grademin) {
|
|
|
914 |
return null;
|
|
|
915 |
}
|
|
|
916 |
|
|
|
917 |
if ($this->grademax == $this->grademin) {
|
|
|
918 |
return $this->grademax; // no range
|
|
|
919 |
}
|
|
|
920 |
|
|
|
921 |
// Standardise score to the new grade range
|
|
|
922 |
// NOTE: skip if the activity provides a manual rescaling option.
|
|
|
923 |
$manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
|
|
|
924 |
if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
|
|
|
925 |
$rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
|
|
|
926 |
}
|
|
|
927 |
|
|
|
928 |
// Apply other grade_item factors
|
|
|
929 |
$rawgrade *= $this->multfactor;
|
|
|
930 |
$rawgrade += $this->plusfactor;
|
|
|
931 |
|
|
|
932 |
return $this->bounded_grade($rawgrade);
|
|
|
933 |
|
|
|
934 |
} else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
|
|
|
935 |
if (empty($this->scale)) {
|
|
|
936 |
$this->load_scale();
|
|
|
937 |
}
|
|
|
938 |
|
|
|
939 |
if ($this->grademax < 0) {
|
|
|
940 |
return null; // scale not present - no grade
|
|
|
941 |
}
|
|
|
942 |
|
|
|
943 |
if ($this->grademax == 0) {
|
|
|
944 |
return $this->grademax; // only one option
|
|
|
945 |
}
|
|
|
946 |
|
|
|
947 |
// Convert scale if needed
|
|
|
948 |
// NOTE: skip if the activity provides a manual rescaling option.
|
|
|
949 |
$manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule, 'rescale_activity_grades') !== false);
|
|
|
950 |
if (!$manuallyrescale && ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
|
|
|
951 |
// This should never happen because scales are locked if they are in use.
|
|
|
952 |
$rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
|
|
|
953 |
}
|
|
|
954 |
|
|
|
955 |
return $this->bounded_grade($rawgrade);
|
|
|
956 |
|
|
|
957 |
|
|
|
958 |
} else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
|
|
|
959 |
// somebody changed the grading type when grades already existed
|
|
|
960 |
return null;
|
|
|
961 |
|
|
|
962 |
} else {
|
|
|
963 |
debugging("Unknown grade type");
|
|
|
964 |
return null;
|
|
|
965 |
}
|
|
|
966 |
}
|
|
|
967 |
|
|
|
968 |
/**
|
|
|
969 |
* Update the rawgrademax and rawgrademin for all grade_grades records for this item.
|
|
|
970 |
* Scale every rawgrade to maintain the percentage. This function should be called
|
|
|
971 |
* after the gradeitem has been updated to the new min and max values.
|
|
|
972 |
*
|
|
|
973 |
* @param float $oldgrademin The previous grade min value
|
|
|
974 |
* @param float $oldgrademax The previous grade max value
|
|
|
975 |
* @param float $newgrademin The new grade min value
|
|
|
976 |
* @param float $newgrademax The new grade max value
|
|
|
977 |
* @param string $source from where was the object inserted (mod/forum, manual, etc.)
|
|
|
978 |
* @return bool True on success
|
|
|
979 |
*/
|
|
|
980 |
public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
|
|
|
981 |
global $DB;
|
|
|
982 |
|
|
|
983 |
if (empty($this->id)) {
|
|
|
984 |
return false;
|
|
|
985 |
}
|
|
|
986 |
|
|
|
987 |
if ($oldgrademax <= $oldgrademin) {
|
|
|
988 |
// Grades cannot be scaled.
|
|
|
989 |
return false;
|
|
|
990 |
}
|
|
|
991 |
$scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
|
|
|
992 |
if (($newgrademax - $newgrademin) <= 1) {
|
|
|
993 |
// We would lose too much precision, lets bail.
|
|
|
994 |
return false;
|
|
|
995 |
}
|
|
|
996 |
|
|
|
997 |
$rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id));
|
|
|
998 |
|
|
|
999 |
foreach ($rs as $graderecord) {
|
|
|
1000 |
// For each record, create an object to work on.
|
|
|
1001 |
$grade = new grade_grade($graderecord, false);
|
|
|
1002 |
// Set this object in the item so it doesn't re-fetch it.
|
|
|
1003 |
$grade->grade_item = $this;
|
|
|
1004 |
|
|
|
1005 |
if (!$this->is_category_item() || ($this->is_category_item() && $grade->is_overridden())) {
|
|
|
1006 |
// Updating the raw grade automatically updates the min/max.
|
|
|
1007 |
if ($this->is_raw_used()) {
|
|
|
1008 |
$rawgrade = (($grade->rawgrade - $oldgrademin) * $scale) + $newgrademin;
|
|
|
1009 |
$this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE, null, null, null, $grade);
|
|
|
1010 |
} else {
|
|
|
1011 |
$finalgrade = (($grade->finalgrade - $oldgrademin) * $scale) + $newgrademin;
|
|
|
1012 |
$this->update_final_grade($grade->userid, $finalgrade, $source);
|
|
|
1013 |
}
|
|
|
1014 |
}
|
|
|
1015 |
}
|
|
|
1016 |
$rs->close();
|
|
|
1017 |
|
|
|
1018 |
// Mark this item for regrading.
|
|
|
1019 |
$this->force_regrading();
|
|
|
1020 |
|
|
|
1021 |
return true;
|
|
|
1022 |
}
|
|
|
1023 |
|
|
|
1024 |
/**
|
|
|
1025 |
* Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
|
|
|
1026 |
*
|
|
|
1027 |
* @return void
|
|
|
1028 |
*/
|
|
|
1029 |
public function force_regrading() {
|
|
|
1030 |
global $DB;
|
|
|
1031 |
$this->needsupdate = 1;
|
|
|
1032 |
//mark this item and course item only - categories and calculated items are always regraded
|
|
|
1033 |
$wheresql = "(itemtype='course' OR id=?) AND courseid=?";
|
|
|
1034 |
$params = array($this->id, $this->courseid);
|
|
|
1035 |
$DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
|
|
|
1036 |
}
|
|
|
1037 |
|
|
|
1038 |
/**
|
|
|
1039 |
* Instantiates a grade_scale object from the DB if this item's scaleid variable is set
|
|
|
1040 |
*
|
|
|
1041 |
* @return grade_scale Returns a grade_scale object or null if no scale used
|
|
|
1042 |
*/
|
|
|
1043 |
public function load_scale() {
|
|
|
1044 |
if ($this->gradetype != GRADE_TYPE_SCALE) {
|
|
|
1045 |
$this->scaleid = null;
|
|
|
1046 |
}
|
|
|
1047 |
|
|
|
1048 |
if (!empty($this->scaleid)) {
|
|
|
1049 |
//do not load scale if already present
|
|
|
1050 |
if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
|
|
|
1051 |
$this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
|
|
|
1052 |
if (!$this->scale) {
|
|
|
1053 |
debugging('Incorrect scale id: '.$this->scaleid);
|
|
|
1054 |
$this->scale = null;
|
|
|
1055 |
return null;
|
|
|
1056 |
}
|
|
|
1057 |
$this->scale->load_items();
|
|
|
1058 |
}
|
|
|
1059 |
|
|
|
1060 |
// Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
|
|
|
1061 |
// stay with the current min=1 max=count(scaleitems)
|
|
|
1062 |
$this->grademax = count($this->scale->scale_items);
|
|
|
1063 |
$this->grademin = 1;
|
|
|
1064 |
|
|
|
1065 |
} else {
|
|
|
1066 |
$this->scale = null;
|
|
|
1067 |
}
|
|
|
1068 |
|
|
|
1069 |
return $this->scale;
|
|
|
1070 |
}
|
|
|
1071 |
|
|
|
1072 |
/**
|
|
|
1073 |
* Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
|
|
|
1074 |
*
|
|
|
1075 |
* @return grade_outcome This grade item's associated grade_outcome or null
|
|
|
1076 |
*/
|
|
|
1077 |
public function load_outcome() {
|
|
|
1078 |
if (!empty($this->outcomeid)) {
|
|
|
1079 |
$this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
|
|
|
1080 |
}
|
|
|
1081 |
return $this->outcome;
|
|
|
1082 |
}
|
|
|
1083 |
|
|
|
1084 |
/**
|
|
|
1085 |
* Returns the grade_category object this grade_item belongs to (referenced by categoryid)
|
|
|
1086 |
* or category attached to category item.
|
|
|
1087 |
*
|
|
|
1088 |
* @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
|
|
|
1089 |
*/
|
|
|
1090 |
public function get_parent_category() {
|
|
|
1091 |
if ($this->is_category_item() or $this->is_course_item()) {
|
|
|
1092 |
return $this->get_item_category();
|
|
|
1093 |
|
|
|
1094 |
} else {
|
|
|
1095 |
return grade_category::fetch(array('id'=>$this->categoryid));
|
|
|
1096 |
}
|
|
|
1097 |
}
|
|
|
1098 |
|
|
|
1099 |
/**
|
|
|
1100 |
* Calls upon the get_parent_category method to retrieve the grade_category object
|
|
|
1101 |
* from the DB and assigns it to $this->parent_category. It also returns the object.
|
|
|
1102 |
*
|
|
|
1103 |
* @return grade_category This grade item's parent grade_category.
|
|
|
1104 |
*/
|
|
|
1105 |
public function load_parent_category() {
|
|
|
1106 |
if (empty($this->parent_category->id)) {
|
|
|
1107 |
$this->parent_category = $this->get_parent_category();
|
|
|
1108 |
}
|
|
|
1109 |
return $this->parent_category;
|
|
|
1110 |
}
|
|
|
1111 |
|
|
|
1112 |
/**
|
|
|
1113 |
* Returns the grade_category for a grade category grade item
|
|
|
1114 |
*
|
|
|
1115 |
* @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
|
|
|
1116 |
*/
|
|
|
1117 |
public function get_item_category() {
|
|
|
1118 |
if (!$this->is_course_item() and !$this->is_category_item()) {
|
|
|
1119 |
return false;
|
|
|
1120 |
}
|
|
|
1121 |
return grade_category::fetch(array('id'=>$this->iteminstance));
|
|
|
1122 |
}
|
|
|
1123 |
|
|
|
1124 |
/**
|
|
|
1125 |
* Calls upon the get_item_category method to retrieve the grade_category object
|
|
|
1126 |
* from the DB and assigns it to $this->item_category. It also returns the object.
|
|
|
1127 |
*
|
|
|
1128 |
* @return grade_category
|
|
|
1129 |
*/
|
|
|
1130 |
public function load_item_category() {
|
|
|
1131 |
if (empty($this->item_category->id)) {
|
|
|
1132 |
$this->item_category = $this->get_item_category();
|
|
|
1133 |
}
|
|
|
1134 |
return $this->item_category;
|
|
|
1135 |
}
|
|
|
1136 |
|
|
|
1137 |
/**
|
|
|
1138 |
* Is the grade item associated with category?
|
|
|
1139 |
*
|
|
|
1140 |
* @return bool
|
|
|
1141 |
*/
|
|
|
1142 |
public function is_category_item() {
|
|
|
1143 |
return ($this->itemtype == 'category');
|
|
|
1144 |
}
|
|
|
1145 |
|
|
|
1146 |
/**
|
|
|
1147 |
* Is the grade item associated with course?
|
|
|
1148 |
*
|
|
|
1149 |
* @return bool
|
|
|
1150 |
*/
|
|
|
1151 |
public function is_course_item() {
|
|
|
1152 |
return ($this->itemtype == 'course');
|
|
|
1153 |
}
|
|
|
1154 |
|
|
|
1155 |
/**
|
|
|
1156 |
* Is this a manually graded item?
|
|
|
1157 |
*
|
|
|
1158 |
* @return bool
|
|
|
1159 |
*/
|
|
|
1160 |
public function is_manual_item() {
|
|
|
1161 |
return ($this->itemtype == 'manual');
|
|
|
1162 |
}
|
|
|
1163 |
|
|
|
1164 |
/**
|
|
|
1165 |
* Is this an outcome item?
|
|
|
1166 |
*
|
|
|
1167 |
* @return bool
|
|
|
1168 |
*/
|
|
|
1169 |
public function is_outcome_item() {
|
|
|
1170 |
return !empty($this->outcomeid);
|
|
|
1171 |
}
|
|
|
1172 |
|
|
|
1173 |
/**
|
|
|
1174 |
* Is the grade item external - associated with module, plugin or something else?
|
|
|
1175 |
*
|
|
|
1176 |
* @return bool
|
|
|
1177 |
*/
|
|
|
1178 |
public function is_external_item() {
|
|
|
1179 |
return ($this->itemtype == 'mod');
|
|
|
1180 |
}
|
|
|
1181 |
|
|
|
1182 |
/**
|
|
|
1183 |
* Is the grade item overridable
|
|
|
1184 |
*
|
|
|
1185 |
* @return bool
|
|
|
1186 |
*/
|
|
|
1187 |
public function is_overridable_item() {
|
|
|
1188 |
if ($this->is_course_item() or $this->is_category_item()) {
|
|
|
1189 |
$overridable = (bool) get_config('moodle', 'grade_overridecat');
|
|
|
1190 |
} else {
|
|
|
1191 |
$overridable = false;
|
|
|
1192 |
}
|
|
|
1193 |
|
|
|
1194 |
return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
|
|
|
1195 |
}
|
|
|
1196 |
|
|
|
1197 |
/**
|
|
|
1198 |
* Is the grade item feedback overridable
|
|
|
1199 |
*
|
|
|
1200 |
* @return bool
|
|
|
1201 |
*/
|
|
|
1202 |
public function is_overridable_item_feedback() {
|
|
|
1203 |
return !$this->is_outcome_item() and $this->is_external_item();
|
|
|
1204 |
}
|
|
|
1205 |
|
|
|
1206 |
/**
|
|
|
1207 |
* Returns true if grade items uses raw grades
|
|
|
1208 |
*
|
|
|
1209 |
* @return bool
|
|
|
1210 |
*/
|
|
|
1211 |
public function is_raw_used() {
|
|
|
1212 |
return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
|
|
|
1213 |
}
|
|
|
1214 |
|
|
|
1215 |
/**
|
|
|
1216 |
* Returns true if the grade item is an aggreggated type grade.
|
|
|
1217 |
*
|
|
|
1218 |
* @since Moodle 2.8.7, 2.9.1
|
|
|
1219 |
* @return bool
|
|
|
1220 |
*/
|
|
|
1221 |
public function is_aggregate_item() {
|
|
|
1222 |
return ($this->is_category_item() || $this->is_course_item());
|
|
|
1223 |
}
|
|
|
1224 |
|
|
|
1225 |
/**
|
|
|
1226 |
* Returns the grade item associated with the course
|
|
|
1227 |
*
|
|
|
1228 |
* @param int $courseid
|
|
|
1229 |
* @return grade_item Course level grade item object
|
|
|
1230 |
*/
|
|
|
1231 |
public static function fetch_course_item($courseid) {
|
|
|
1232 |
if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
|
|
|
1233 |
return $course_item;
|
|
|
1234 |
}
|
|
|
1235 |
|
|
|
1236 |
// first get category - it creates the associated grade item
|
|
|
1237 |
$course_category = grade_category::fetch_course_category($courseid);
|
|
|
1238 |
return $course_category->get_grade_item();
|
|
|
1239 |
}
|
|
|
1240 |
|
|
|
1241 |
/**
|
|
|
1242 |
* Is grading object editable?
|
|
|
1243 |
*
|
|
|
1244 |
* @return bool
|
|
|
1245 |
*/
|
|
|
1246 |
public function is_editable() {
|
|
|
1247 |
return true;
|
|
|
1248 |
}
|
|
|
1249 |
|
|
|
1250 |
/**
|
|
|
1251 |
* Checks if grade calculated. Returns this object's calculation.
|
|
|
1252 |
*
|
|
|
1253 |
* @return bool true if grade item calculated.
|
|
|
1254 |
*/
|
|
|
1255 |
public function is_calculated() {
|
|
|
1256 |
if (empty($this->calculation)) {
|
|
|
1257 |
return false;
|
|
|
1258 |
}
|
|
|
1259 |
|
|
|
1260 |
/*
|
|
|
1261 |
* The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
|
|
|
1262 |
* we would have to fetch all course grade items to find out the ids.
|
|
|
1263 |
* Also if user changes the idnumber the formula does not need to be updated.
|
|
|
1264 |
*/
|
|
|
1265 |
|
|
|
1266 |
// first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
|
|
|
1267 |
if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
|
|
|
1268 |
$this->set_calculation($this->calculation);
|
|
|
1269 |
}
|
|
|
1270 |
|
|
|
1271 |
return !empty($this->calculation);
|
|
|
1272 |
}
|
|
|
1273 |
|
|
|
1274 |
/**
|
|
|
1275 |
* Returns calculation string if grade calculated.
|
|
|
1276 |
*
|
|
|
1277 |
* @return string Returns the grade item's calculation if calculation is used, null if not
|
|
|
1278 |
*/
|
|
|
1279 |
public function get_calculation() {
|
|
|
1280 |
if ($this->is_calculated()) {
|
|
|
1281 |
return grade_item::denormalize_formula($this->calculation, $this->courseid);
|
|
|
1282 |
|
|
|
1283 |
} else {
|
|
|
1284 |
return NULL;
|
|
|
1285 |
}
|
|
|
1286 |
}
|
|
|
1287 |
|
|
|
1288 |
/**
|
|
|
1289 |
* Sets this item's calculation (creates it) if not yet set, or
|
|
|
1290 |
* updates it if already set (in the DB). If no calculation is given,
|
|
|
1291 |
* the calculation is removed.
|
|
|
1292 |
*
|
|
|
1293 |
* @param string $formula string representation of formula used for calculation
|
|
|
1294 |
* @return bool success
|
|
|
1295 |
*/
|
|
|
1296 |
public function set_calculation($formula) {
|
|
|
1297 |
$this->calculation = grade_item::normalize_formula($formula, $this->courseid);
|
|
|
1298 |
$this->calculation_normalized = true;
|
|
|
1299 |
return $this->update();
|
|
|
1300 |
}
|
|
|
1301 |
|
|
|
1302 |
/**
|
|
|
1303 |
* Denormalizes the calculation formula to [idnumber] form
|
|
|
1304 |
*
|
|
|
1305 |
* @param string $formula A string representation of the formula
|
|
|
1306 |
* @param int $courseid The course ID
|
|
|
1307 |
* @return string The denormalized formula as a string
|
|
|
1308 |
*/
|
|
|
1309 |
public static function denormalize_formula($formula, $courseid) {
|
|
|
1310 |
if (empty($formula)) {
|
|
|
1311 |
return '';
|
|
|
1312 |
}
|
|
|
1313 |
|
|
|
1314 |
// denormalize formula - convert ##giXX## to [[idnumber]]
|
|
|
1315 |
if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
|
|
|
1316 |
foreach ($matches[1] as $id) {
|
|
|
1317 |
if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
|
|
|
1318 |
if (!empty($grade_item->idnumber)) {
|
|
|
1319 |
$formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
|
|
|
1320 |
}
|
|
|
1321 |
}
|
|
|
1322 |
}
|
|
|
1323 |
}
|
|
|
1324 |
|
|
|
1325 |
return $formula;
|
|
|
1326 |
|
|
|
1327 |
}
|
|
|
1328 |
|
|
|
1329 |
/**
|
|
|
1330 |
* Normalizes the calculation formula to [#giXX#] form
|
|
|
1331 |
*
|
|
|
1332 |
* @param string $formula The formula
|
|
|
1333 |
* @param int $courseid The course ID
|
|
|
1334 |
* @return string The normalized formula as a string
|
|
|
1335 |
*/
|
|
|
1336 |
public static function normalize_formula($formula, $courseid) {
|
|
|
1337 |
$formula = trim($formula);
|
|
|
1338 |
|
|
|
1339 |
if (empty($formula)) {
|
|
|
1340 |
return NULL;
|
|
|
1341 |
|
|
|
1342 |
}
|
|
|
1343 |
|
|
|
1344 |
// normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
|
|
|
1345 |
if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
|
|
|
1346 |
foreach ($grade_items as $grade_item) {
|
|
|
1347 |
$formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
|
|
|
1348 |
}
|
|
|
1349 |
}
|
|
|
1350 |
|
|
|
1351 |
return $formula;
|
|
|
1352 |
}
|
|
|
1353 |
|
|
|
1354 |
/**
|
|
|
1355 |
* Returns the final values for this grade item (as imported by module or other source).
|
|
|
1356 |
*
|
|
|
1357 |
* @param int $userid Optional: to retrieve a single user's final grade
|
|
|
1358 |
* @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
|
|
|
1359 |
*/
|
|
|
1360 |
public function get_final($userid=NULL) {
|
|
|
1361 |
global $DB;
|
|
|
1362 |
if ($userid) {
|
|
|
1363 |
if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
|
|
|
1364 |
return $user;
|
|
|
1365 |
}
|
|
|
1366 |
|
|
|
1367 |
} else {
|
|
|
1368 |
if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
|
|
|
1369 |
//TODO: speed up with better SQL (MDL-31380)
|
|
|
1370 |
$result = array();
|
|
|
1371 |
foreach ($grades as $grade) {
|
|
|
1372 |
$result[$grade->userid] = $grade;
|
|
|
1373 |
}
|
|
|
1374 |
return $result;
|
|
|
1375 |
} else {
|
|
|
1376 |
return array();
|
|
|
1377 |
}
|
|
|
1378 |
}
|
|
|
1379 |
}
|
|
|
1380 |
|
|
|
1381 |
/**
|
|
|
1382 |
* Get (or create if not exist yet) grade for this user
|
|
|
1383 |
*
|
|
|
1384 |
* @param int $userid The user ID
|
|
|
1385 |
* @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
|
|
|
1386 |
* @return grade_grade The grade_grade instance for the user for this grade item
|
|
|
1387 |
*/
|
|
|
1388 |
public function get_grade($userid, $create=true) {
|
|
|
1389 |
if (empty($this->id)) {
|
|
|
1390 |
debugging('Can not use before insert');
|
|
|
1391 |
return false;
|
|
|
1392 |
}
|
|
|
1393 |
|
|
|
1394 |
$grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
|
|
|
1395 |
if (empty($grade->id) and $create) {
|
|
|
1396 |
$grade->insert();
|
|
|
1397 |
}
|
|
|
1398 |
|
|
|
1399 |
return $grade;
|
|
|
1400 |
}
|
|
|
1401 |
|
|
|
1402 |
/**
|
|
|
1403 |
* Returns the sortorder of this grade_item. This method is also available in
|
|
|
1404 |
* grade_category, for cases where the object type is not know.
|
|
|
1405 |
*
|
|
|
1406 |
* @return int Sort order
|
|
|
1407 |
*/
|
|
|
1408 |
public function get_sortorder() {
|
|
|
1409 |
return $this->sortorder;
|
|
|
1410 |
}
|
|
|
1411 |
|
|
|
1412 |
/**
|
|
|
1413 |
* Returns the idnumber of this grade_item. This method is also available in
|
|
|
1414 |
* grade_category, for cases where the object type is not know.
|
|
|
1415 |
*
|
|
|
1416 |
* @return string The grade item idnumber
|
|
|
1417 |
*/
|
|
|
1418 |
public function get_idnumber() {
|
|
|
1419 |
return $this->idnumber;
|
|
|
1420 |
}
|
|
|
1421 |
|
|
|
1422 |
/**
|
|
|
1423 |
* Returns this grade_item. This method is also available in
|
|
|
1424 |
* grade_category, for cases where the object type is not know.
|
|
|
1425 |
*
|
|
|
1426 |
* @return grade_item
|
|
|
1427 |
*/
|
|
|
1428 |
public function get_grade_item() {
|
|
|
1429 |
return $this;
|
|
|
1430 |
}
|
|
|
1431 |
|
|
|
1432 |
/**
|
|
|
1433 |
* Sets the sortorder of this grade_item. This method is also available in
|
|
|
1434 |
* grade_category, for cases where the object type is not know.
|
|
|
1435 |
*
|
|
|
1436 |
* @param int $sortorder
|
|
|
1437 |
*/
|
|
|
1438 |
public function set_sortorder($sortorder) {
|
|
|
1439 |
if ($this->sortorder == $sortorder) {
|
|
|
1440 |
return;
|
|
|
1441 |
}
|
|
|
1442 |
$this->sortorder = $sortorder;
|
|
|
1443 |
$this->update();
|
|
|
1444 |
}
|
|
|
1445 |
|
|
|
1446 |
/**
|
|
|
1447 |
* Update this grade item's sortorder so that it will appear after $sortorder
|
|
|
1448 |
*
|
|
|
1449 |
* @param int $sortorder The sort order to place this grade item after
|
|
|
1450 |
*/
|
|
|
1451 |
public function move_after_sortorder($sortorder) {
|
|
|
1452 |
global $CFG, $DB;
|
|
|
1453 |
|
|
|
1454 |
//make some room first
|
|
|
1455 |
$params = array($sortorder, $this->courseid);
|
|
|
1456 |
$sql = "UPDATE {grade_items}
|
|
|
1457 |
SET sortorder = sortorder + 1
|
|
|
1458 |
WHERE sortorder > ? AND courseid = ?";
|
|
|
1459 |
$DB->execute($sql, $params);
|
|
|
1460 |
|
|
|
1461 |
$this->set_sortorder($sortorder + 1);
|
|
|
1462 |
}
|
|
|
1463 |
|
|
|
1464 |
/**
|
|
|
1465 |
* Detect duplicate grade item's sortorder and re-sort them.
|
|
|
1466 |
* Note: Duplicate sortorder will be introduced while duplicating activities or
|
|
|
1467 |
* merging two courses.
|
|
|
1468 |
*
|
|
|
1469 |
* @param int $courseid id of the course for which grade_items sortorder need to be fixed.
|
|
|
1470 |
*/
|
|
|
1471 |
public static function fix_duplicate_sortorder($courseid) {
|
|
|
1472 |
global $DB;
|
|
|
1473 |
|
|
|
1474 |
$transaction = $DB->start_delegated_transaction();
|
|
|
1475 |
|
|
|
1476 |
$sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
|
|
|
1477 |
FROM {grade_items} g1
|
|
|
1478 |
JOIN {grade_items} g2 ON g1.courseid = g2.courseid
|
|
|
1479 |
WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
|
|
|
1480 |
ORDER BY g1.sortorder DESC, g1.id DESC";
|
|
|
1481 |
|
|
|
1482 |
// Get all duplicates in course highest sort order, and higest id first so that we can make space at the
|
|
|
1483 |
// bottom higher end of the sort orders and work down by id.
|
|
|
1484 |
$rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
|
|
|
1485 |
|
|
|
1486 |
foreach($rs as $duplicate) {
|
|
|
1487 |
$DB->execute("UPDATE {grade_items}
|
|
|
1488 |
SET sortorder = sortorder + 1
|
|
|
1489 |
WHERE courseid = :courseid AND
|
|
|
1490 |
(sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
|
|
|
1491 |
array('courseid' => $duplicate->courseid,
|
|
|
1492 |
'sortorder' => $duplicate->sortorder,
|
|
|
1493 |
'sortorder2' => $duplicate->sortorder,
|
|
|
1494 |
'id' => $duplicate->id));
|
|
|
1495 |
}
|
|
|
1496 |
$rs->close();
|
|
|
1497 |
$transaction->allow_commit();
|
|
|
1498 |
}
|
|
|
1499 |
|
|
|
1500 |
/**
|
|
|
1501 |
* Returns the most descriptive field for this object.
|
|
|
1502 |
*
|
|
|
1503 |
* Determines what type of grade item it is then returns the appropriate string
|
|
|
1504 |
*
|
|
|
1505 |
* @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
|
|
|
1506 |
* @param bool $escape Whether the returned category name is to be HTML escaped or not.
|
|
|
1507 |
* @return string name
|
|
|
1508 |
*/
|
|
|
1509 |
public function get_name($fulltotal=false, $escape = true) {
|
|
|
1510 |
global $CFG;
|
|
|
1511 |
require_once($CFG->dirroot . '/course/lib.php');
|
|
|
1512 |
if (strval($this->itemname) !== '') {
|
|
|
1513 |
// MDL-10557
|
|
|
1514 |
|
|
|
1515 |
// Make it obvious to users if the course module to which this grade item relates, is currently being removed.
|
|
|
1516 |
$deletionpending = course_module_instance_pending_deletion($this->courseid, $this->itemmodule, $this->iteminstance);
|
|
|
1517 |
$deletionnotice = get_string('gradesmoduledeletionprefix', 'grades');
|
|
|
1518 |
|
|
|
1519 |
$options = ['context' => context_course::instance($this->courseid), 'escape' => $escape];
|
|
|
1520 |
return $deletionpending ?
|
|
|
1521 |
format_string($deletionnotice . ' ' . $this->itemname, true, $options) :
|
|
|
1522 |
format_string($this->itemname, true, $options);
|
|
|
1523 |
|
|
|
1524 |
} else if ($this->is_course_item()) {
|
|
|
1525 |
return get_string('coursetotal', 'grades');
|
|
|
1526 |
|
|
|
1527 |
} else if ($this->is_category_item()) {
|
|
|
1528 |
if ($fulltotal) {
|
|
|
1529 |
$category = $this->load_parent_category();
|
|
|
1530 |
$a = new stdClass();
|
|
|
1531 |
$a->category = $category->get_name($escape);
|
|
|
1532 |
return get_string('categorytotalfull', 'grades', $a);
|
|
|
1533 |
} else {
|
|
|
1534 |
return get_string('categorytotal', 'grades');
|
|
|
1535 |
}
|
|
|
1536 |
|
|
|
1537 |
} else {
|
|
|
1538 |
return get_string('gradenoun');
|
|
|
1539 |
}
|
|
|
1540 |
}
|
|
|
1541 |
|
|
|
1542 |
/**
|
|
|
1543 |
* A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
|
|
|
1544 |
*
|
|
|
1545 |
* @return string description
|
|
|
1546 |
*/
|
|
|
1547 |
public function get_description() {
|
|
|
1548 |
if ($this->is_course_item() || $this->is_category_item()) {
|
|
|
1549 |
$categoryitem = $this->load_item_category();
|
|
|
1550 |
return $categoryitem->get_description();
|
|
|
1551 |
}
|
|
|
1552 |
return '';
|
|
|
1553 |
}
|
|
|
1554 |
|
|
|
1555 |
/**
|
|
|
1556 |
* Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
|
|
|
1557 |
*
|
|
|
1558 |
* @param int $parentid The ID of the new parent
|
|
|
1559 |
* @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.
|
|
|
1560 |
* Set this to false when the aggregation fields have been updated in prevision of the new
|
|
|
1561 |
* category, typically when the item is freshly created.
|
|
|
1562 |
* @return bool True if success
|
|
|
1563 |
*/
|
|
|
1564 |
public function set_parent($parentid, $updateaggregationfields = true) {
|
|
|
1565 |
if ($this->is_course_item() or $this->is_category_item()) {
|
|
|
1566 |
throw new \moodle_exception('cannotsetparentforcatoritem');
|
|
|
1567 |
}
|
|
|
1568 |
|
|
|
1569 |
if ($this->categoryid == $parentid) {
|
|
|
1570 |
return true;
|
|
|
1571 |
}
|
|
|
1572 |
|
|
|
1573 |
// find parent and check course id
|
|
|
1574 |
if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
|
|
|
1575 |
return false;
|
|
|
1576 |
}
|
|
|
1577 |
|
|
|
1578 |
$currentparent = $this->load_parent_category();
|
|
|
1579 |
|
|
|
1580 |
if ($updateaggregationfields) {
|
|
|
1581 |
$this->set_aggregation_fields_for_aggregation($currentparent->aggregation, $parent_category->aggregation);
|
|
|
1582 |
}
|
|
|
1583 |
|
|
|
1584 |
$this->force_regrading();
|
|
|
1585 |
|
|
|
1586 |
// set new parent
|
|
|
1587 |
$this->categoryid = $parent_category->id;
|
|
|
1588 |
$this->parent_category =& $parent_category;
|
|
|
1589 |
|
|
|
1590 |
return $this->update();
|
|
|
1591 |
}
|
|
|
1592 |
|
|
|
1593 |
/**
|
|
|
1594 |
* Update the aggregation fields when the aggregation changed.
|
|
|
1595 |
*
|
|
|
1596 |
* This method should always be called when the aggregation has changed, but also when
|
|
|
1597 |
* the item was moved to another category, even it if uses the same aggregation method.
|
|
|
1598 |
*
|
|
|
1599 |
* Some values such as the weight only make sense within a category, once moved the
|
|
|
1600 |
* values should be reset to let the user adapt them accordingly.
|
|
|
1601 |
*
|
|
|
1602 |
* Note that this method does not save the grade item.
|
|
|
1603 |
* {@link grade_item::update()} has to be called manually after using this method.
|
|
|
1604 |
*
|
|
|
1605 |
* @param int $from Aggregation method constant value.
|
|
|
1606 |
* @param int $to Aggregation method constant value.
|
|
|
1607 |
* @return boolean True when at least one field was changed, false otherwise
|
|
|
1608 |
*/
|
|
|
1609 |
public function set_aggregation_fields_for_aggregation($from, $to) {
|
|
|
1610 |
$defaults = grade_category::get_default_aggregation_coefficient_values($to);
|
|
|
1611 |
|
|
|
1612 |
$origaggregationcoef = $this->aggregationcoef;
|
|
|
1613 |
$origaggregationcoef2 = $this->aggregationcoef2;
|
|
|
1614 |
$origweighoverride = $this->weightoverride;
|
|
|
1615 |
|
|
|
1616 |
if ($from == GRADE_AGGREGATE_SUM && $to == GRADE_AGGREGATE_SUM && $this->weightoverride) {
|
|
|
1617 |
// Do nothing. We are switching from SUM to SUM and the weight is overriden,
|
|
|
1618 |
// a teacher would not expect any change in this situation.
|
|
|
1619 |
|
|
|
1620 |
} else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN && $to == GRADE_AGGREGATE_WEIGHTED_MEAN) {
|
|
|
1621 |
// Do nothing. The weights can be kept in this case.
|
|
|
1622 |
|
|
|
1623 |
} else if (in_array($from, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))
|
|
|
1624 |
&& in_array($to, array(GRADE_AGGREGATE_SUM, GRADE_AGGREGATE_EXTRACREDIT_MEAN, GRADE_AGGREGATE_WEIGHTED_MEAN2))) {
|
|
|
1625 |
|
|
|
1626 |
// Reset all but the the extra credit field.
|
|
|
1627 |
$this->aggregationcoef2 = $defaults['aggregationcoef2'];
|
|
|
1628 |
$this->weightoverride = $defaults['weightoverride'];
|
|
|
1629 |
|
|
|
1630 |
if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
|
|
|
1631 |
// Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.
|
|
|
1632 |
$this->aggregationcoef = min(1, $this->aggregationcoef);
|
|
|
1633 |
}
|
|
|
1634 |
} else {
|
|
|
1635 |
// Reset all.
|
|
|
1636 |
$this->aggregationcoef = $defaults['aggregationcoef'];
|
|
|
1637 |
$this->aggregationcoef2 = $defaults['aggregationcoef2'];
|
|
|
1638 |
$this->weightoverride = $defaults['weightoverride'];
|
|
|
1639 |
}
|
|
|
1640 |
|
|
|
1641 |
$acoefdiff = grade_floats_different($origaggregationcoef, $this->aggregationcoef);
|
|
|
1642 |
$acoefdiff2 = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2);
|
|
|
1643 |
$weightoverride = grade_floats_different($origweighoverride, $this->weightoverride);
|
|
|
1644 |
|
|
|
1645 |
return $acoefdiff || $acoefdiff2 || $weightoverride;
|
|
|
1646 |
}
|
|
|
1647 |
|
|
|
1648 |
/**
|
|
|
1649 |
* Makes sure value is a valid grade value.
|
|
|
1650 |
*
|
|
|
1651 |
* @param float $gradevalue
|
|
|
1652 |
* @return mixed float or int fixed grade value
|
|
|
1653 |
*/
|
|
|
1654 |
public function bounded_grade($gradevalue) {
|
|
|
1655 |
global $CFG;
|
|
|
1656 |
|
|
|
1657 |
if (is_null($gradevalue)) {
|
|
|
1658 |
return null;
|
|
|
1659 |
}
|
|
|
1660 |
|
|
|
1661 |
if ($this->gradetype == GRADE_TYPE_SCALE) {
|
|
|
1662 |
// no >100% grades hack for scale grades!
|
|
|
1663 |
// 1.5 is rounded to 2 ;-)
|
|
|
1664 |
return (int)bounded_number($this->grademin, round($gradevalue+0.00001), $this->grademax);
|
|
|
1665 |
}
|
|
|
1666 |
|
|
|
1667 |
$grademax = $this->grademax;
|
|
|
1668 |
|
|
|
1669 |
// NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
|
|
|
1670 |
$maxcoef = isset($CFG->gradeoverhundredprocentmax) ? $CFG->gradeoverhundredprocentmax : 10; // 1000% max by default
|
|
|
1671 |
|
|
|
1672 |
if (!empty($CFG->unlimitedgrades)) {
|
|
|
1673 |
// NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
|
|
|
1674 |
$grademax = $grademax * $maxcoef;
|
|
|
1675 |
} else if ($this->is_category_item() or $this->is_course_item()) {
|
|
|
1676 |
$category = $this->load_item_category();
|
|
|
1677 |
if ($category->aggregation >= 100) {
|
|
|
1678 |
// grade >100% hack
|
|
|
1679 |
$grademax = $grademax * $maxcoef;
|
|
|
1680 |
}
|
|
|
1681 |
}
|
|
|
1682 |
|
|
|
1683 |
return (float)bounded_number($this->grademin, $gradevalue, $grademax);
|
|
|
1684 |
}
|
|
|
1685 |
|
|
|
1686 |
/**
|
|
|
1687 |
* Finds out on which other items does this depend directly when doing calculation or category aggregation
|
|
|
1688 |
*
|
|
|
1689 |
* @param bool $reset_cache
|
|
|
1690 |
* @return array of grade_item IDs this one depends on
|
|
|
1691 |
*/
|
|
|
1692 |
public function depends_on($reset_cache=false) {
|
|
|
1693 |
global $CFG, $DB;
|
|
|
1694 |
|
|
|
1695 |
if ($reset_cache) {
|
|
|
1696 |
$this->dependson_cache = null;
|
|
|
1697 |
} else if (isset($this->dependson_cache)) {
|
|
|
1698 |
return $this->dependson_cache;
|
|
|
1699 |
}
|
|
|
1700 |
|
|
|
1701 |
if ($this->is_locked() && !$this->is_category_item()) {
|
|
|
1702 |
// locked items do not need to be regraded
|
|
|
1703 |
$this->dependson_cache = array();
|
|
|
1704 |
return $this->dependson_cache;
|
|
|
1705 |
}
|
|
|
1706 |
|
|
|
1707 |
if ($this->is_calculated()) {
|
|
|
1708 |
if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
|
|
|
1709 |
$this->dependson_cache = array_unique($matches[1]); // remove duplicates
|
|
|
1710 |
return $this->dependson_cache;
|
|
|
1711 |
} else {
|
|
|
1712 |
$this->dependson_cache = array();
|
|
|
1713 |
return $this->dependson_cache;
|
|
|
1714 |
}
|
|
|
1715 |
|
|
|
1716 |
} else if ($grade_category = $this->load_item_category()) {
|
|
|
1717 |
$params = array();
|
|
|
1718 |
|
|
|
1719 |
//only items with numeric or scale values can be aggregated
|
|
|
1720 |
if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
|
|
|
1721 |
$this->dependson_cache = array();
|
|
|
1722 |
return $this->dependson_cache;
|
|
|
1723 |
}
|
|
|
1724 |
|
|
|
1725 |
$grade_category->apply_forced_settings();
|
|
|
1726 |
|
|
|
1727 |
if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
|
|
|
1728 |
$outcomes_sql = "";
|
|
|
1729 |
} else {
|
|
|
1730 |
$outcomes_sql = "AND gi.outcomeid IS NULL";
|
|
|
1731 |
}
|
|
|
1732 |
|
|
|
1733 |
if (empty($CFG->grade_includescalesinaggregation)) {
|
|
|
1734 |
$gtypes = "gi.gradetype = ?";
|
|
|
1735 |
$params[] = GRADE_TYPE_VALUE;
|
|
|
1736 |
} else {
|
|
|
1737 |
$gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
|
|
|
1738 |
$params[] = GRADE_TYPE_VALUE;
|
|
|
1739 |
$params[] = GRADE_TYPE_SCALE;
|
|
|
1740 |
}
|
|
|
1741 |
|
|
|
1742 |
$params[] = $grade_category->id;
|
|
|
1743 |
$params[] = $this->courseid;
|
|
|
1744 |
$params[] = $grade_category->id;
|
|
|
1745 |
$params[] = $this->courseid;
|
|
|
1746 |
if (empty($CFG->grade_includescalesinaggregation)) {
|
|
|
1747 |
$params[] = GRADE_TYPE_VALUE;
|
|
|
1748 |
} else {
|
|
|
1749 |
$params[] = GRADE_TYPE_VALUE;
|
|
|
1750 |
$params[] = GRADE_TYPE_SCALE;
|
|
|
1751 |
}
|
|
|
1752 |
$sql = "SELECT gi.id
|
|
|
1753 |
FROM {grade_items} gi
|
|
|
1754 |
WHERE $gtypes
|
|
|
1755 |
AND gi.categoryid = ?
|
|
|
1756 |
AND gi.courseid = ?
|
|
|
1757 |
$outcomes_sql
|
|
|
1758 |
UNION
|
|
|
1759 |
|
|
|
1760 |
SELECT gi.id
|
|
|
1761 |
FROM {grade_items} gi, {grade_categories} gc
|
|
|
1762 |
WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
|
|
|
1763 |
AND gc.parent = ?
|
|
|
1764 |
AND gi.courseid = ?
|
|
|
1765 |
AND $gtypes
|
|
|
1766 |
$outcomes_sql";
|
|
|
1767 |
|
|
|
1768 |
if ($children = $DB->get_records_sql($sql, $params)) {
|
|
|
1769 |
$this->dependson_cache = array_keys($children);
|
|
|
1770 |
return $this->dependson_cache;
|
|
|
1771 |
} else {
|
|
|
1772 |
$this->dependson_cache = array();
|
|
|
1773 |
return $this->dependson_cache;
|
|
|
1774 |
}
|
|
|
1775 |
|
|
|
1776 |
} else {
|
|
|
1777 |
$this->dependson_cache = array();
|
|
|
1778 |
return $this->dependson_cache;
|
|
|
1779 |
}
|
|
|
1780 |
}
|
|
|
1781 |
|
|
|
1782 |
/**
|
|
|
1783 |
* Refetch grades from modules, plugins.
|
|
|
1784 |
*
|
|
|
1785 |
* @param int $userid optional, limit the refetch to a single user
|
|
|
1786 |
* @return bool Returns true on success or if there is nothing to do
|
|
|
1787 |
*/
|
|
|
1788 |
public function refresh_grades($userid=0) {
|
|
|
1789 |
global $DB;
|
|
|
1790 |
if ($this->itemtype == 'mod') {
|
|
|
1791 |
if ($this->is_outcome_item()) {
|
|
|
1792 |
//nothing to do
|
|
|
1793 |
return true;
|
|
|
1794 |
}
|
|
|
1795 |
|
|
|
1796 |
if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
|
|
|
1797 |
debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
|
|
|
1798 |
return false;
|
|
|
1799 |
}
|
|
|
1800 |
|
|
|
1801 |
if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
|
|
|
1802 |
debugging('Can not find course module');
|
|
|
1803 |
return false;
|
|
|
1804 |
}
|
|
|
1805 |
|
|
|
1806 |
$activity->modname = $this->itemmodule;
|
|
|
1807 |
$activity->cmidnumber = $cm->idnumber;
|
|
|
1808 |
|
|
|
1809 |
return grade_update_mod_grades($activity, $userid);
|
|
|
1810 |
}
|
|
|
1811 |
|
|
|
1812 |
return true;
|
|
|
1813 |
}
|
|
|
1814 |
|
|
|
1815 |
/**
|
|
|
1816 |
* Updates final grade value for given user, this is a only way to update final
|
|
|
1817 |
* grades from gradebook and import because it logs the change in history table
|
|
|
1818 |
* and deals with overridden flag. This flag is set to prevent later overriding
|
|
|
1819 |
* from raw grades submitted from modules.
|
|
|
1820 |
*
|
|
|
1821 |
* @param int $userid The graded user
|
|
|
1822 |
* @param float|false $finalgrade The float value of final grade, false means do not change
|
|
|
1823 |
* @param string $source The modification source
|
|
|
1824 |
* @param string $feedback Optional teacher feedback
|
|
|
1825 |
* @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
|
|
|
1826 |
* @param int $usermodified The ID of the user making the modification
|
|
|
1827 |
* @param int $timemodified Optional parameter to set the time modified, if not present current time.
|
|
|
1828 |
* @param bool $isbulkupdate If bulk grade update is happening.
|
|
|
1829 |
* @return bool success
|
|
|
1830 |
*/
|
|
|
1831 |
public function update_final_grade($userid, $finalgrade = false, $source = null, $feedback = false,
|
|
|
1832 |
$feedbackformat = FORMAT_MOODLE, $usermodified = null, $timemodified = null, $isbulkupdate = false) {
|
|
|
1833 |
global $USER, $CFG;
|
|
|
1834 |
|
|
|
1835 |
$result = true;
|
|
|
1836 |
|
|
|
1837 |
// no grading used or locked
|
|
|
1838 |
if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
|
|
|
1839 |
return false;
|
|
|
1840 |
}
|
|
|
1841 |
|
|
|
1842 |
$grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
|
|
|
1843 |
$grade->grade_item =& $this; // prevent db fetching of this grade_item
|
|
|
1844 |
|
|
|
1845 |
if (empty($usermodified)) {
|
|
|
1846 |
$grade->usermodified = $USER->id;
|
|
|
1847 |
} else {
|
|
|
1848 |
$grade->usermodified = $usermodified;
|
|
|
1849 |
}
|
|
|
1850 |
|
|
|
1851 |
if ($grade->is_locked()) {
|
|
|
1852 |
// do not update locked grades at all
|
|
|
1853 |
return false;
|
|
|
1854 |
}
|
|
|
1855 |
|
|
|
1856 |
$locktime = $grade->get_locktime();
|
|
|
1857 |
if ($locktime and $locktime < time()) {
|
|
|
1858 |
// do not update grades that should be already locked, force regrade instead
|
|
|
1859 |
$this->force_regrading();
|
|
|
1860 |
return false;
|
|
|
1861 |
}
|
|
|
1862 |
|
|
|
1863 |
$oldgrade = new stdClass();
|
|
|
1864 |
$oldgrade->finalgrade = $grade->finalgrade;
|
|
|
1865 |
$oldgrade->overridden = $grade->overridden;
|
|
|
1866 |
$oldgrade->feedback = $grade->feedback;
|
|
|
1867 |
$oldgrade->feedbackformat = $grade->feedbackformat;
|
|
|
1868 |
$oldgrade->rawgrademin = $grade->rawgrademin;
|
|
|
1869 |
$oldgrade->rawgrademax = $grade->rawgrademax;
|
|
|
1870 |
|
|
|
1871 |
// MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
|
|
|
1872 |
$grade->rawgrademin = $this->grademin;
|
|
|
1873 |
$grade->rawgrademax = $this->grademax;
|
|
|
1874 |
$grade->rawscaleid = $this->scaleid;
|
|
|
1875 |
|
|
|
1876 |
// changed grade?
|
|
|
1877 |
if ($finalgrade !== false) {
|
|
|
1878 |
if ($this->is_overridable_item() && $this->markasoverriddenwhengraded) {
|
|
|
1879 |
$grade->overridden = time();
|
|
|
1880 |
}
|
|
|
1881 |
|
|
|
1882 |
$grade->finalgrade = $this->bounded_grade($finalgrade);
|
|
|
1883 |
}
|
|
|
1884 |
|
|
|
1885 |
// do we have comment from teacher?
|
|
|
1886 |
if ($feedback !== false) {
|
|
|
1887 |
if ($this->is_overridable_item_feedback()) {
|
|
|
1888 |
// external items (modules, plugins) may have own feedback
|
|
|
1889 |
$grade->overridden = time();
|
|
|
1890 |
}
|
|
|
1891 |
|
|
|
1892 |
$grade->feedback = $feedback;
|
|
|
1893 |
$grade->feedbackformat = $feedbackformat;
|
|
|
1894 |
}
|
|
|
1895 |
|
|
|
1896 |
$gradechanged = false;
|
|
|
1897 |
if (empty($grade->id)) {
|
|
|
1898 |
$grade->timecreated = null; // Hack alert - date submitted - no submission yet.
|
|
|
1899 |
$grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
|
|
|
1900 |
$result = (bool)$grade->insert($source, $isbulkupdate);
|
|
|
1901 |
|
|
|
1902 |
// If the grade insert was successful and the final grade was not null then trigger a user_graded event.
|
|
|
1903 |
if ($result && !is_null($grade->finalgrade)) {
|
|
|
1904 |
\core\event\user_graded::create_from_grade($grade)->trigger();
|
|
|
1905 |
}
|
|
|
1906 |
$gradechanged = true;
|
|
|
1907 |
} else {
|
|
|
1908 |
// Existing grade_grades.
|
|
|
1909 |
|
|
|
1910 |
if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
|
|
|
1911 |
or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
|
|
|
1912 |
or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
|
|
|
1913 |
or ($oldgrade->overridden == 0 and $grade->overridden > 0)) {
|
|
|
1914 |
$gradechanged = true;
|
|
|
1915 |
}
|
|
|
1916 |
|
|
|
1917 |
if ($grade->feedback === $oldgrade->feedback and $grade->feedbackformat == $oldgrade->feedbackformat and
|
|
|
1918 |
$gradechanged === false) {
|
|
|
1919 |
// No grade nor feedback changed.
|
|
|
1920 |
return $result;
|
|
|
1921 |
}
|
|
|
1922 |
|
|
|
1923 |
$grade->timemodified = $timemodified ?? time(); // Hack alert - date graded.
|
|
|
1924 |
$result = $grade->update($source, $isbulkupdate);
|
|
|
1925 |
|
|
|
1926 |
// If the grade update was successful and the actual grade has changed then trigger a user_graded event.
|
|
|
1927 |
if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
|
|
|
1928 |
\core\event\user_graded::create_from_grade($grade)->trigger();
|
|
|
1929 |
}
|
|
|
1930 |
}
|
|
|
1931 |
|
|
|
1932 |
if (!$result) {
|
|
|
1933 |
// Something went wrong - better force final grade recalculation.
|
|
|
1934 |
$this->force_regrading();
|
|
|
1935 |
return $result;
|
|
|
1936 |
}
|
|
|
1937 |
|
|
|
1938 |
// If we are not updating grades we don't need to recalculate the whole course.
|
|
|
1939 |
if (!$gradechanged) {
|
|
|
1940 |
return $result;
|
|
|
1941 |
}
|
|
|
1942 |
|
|
|
1943 |
if ($this->is_course_item() and !$this->needsupdate) {
|
|
|
1944 |
if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
|
|
|
1945 |
$this->force_regrading();
|
|
|
1946 |
}
|
|
|
1947 |
|
|
|
1948 |
} else if (!$this->needsupdate) {
|
|
|
1949 |
|
|
|
1950 |
$course_item = grade_item::fetch_course_item($this->courseid);
|
|
|
1951 |
if (!$course_item->needsupdate) {
|
|
|
1952 |
if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
|
|
|
1953 |
$this->force_regrading();
|
|
|
1954 |
}
|
|
|
1955 |
} else {
|
|
|
1956 |
$this->force_regrading();
|
|
|
1957 |
}
|
|
|
1958 |
}
|
|
|
1959 |
|
|
|
1960 |
return $result;
|
|
|
1961 |
}
|
|
|
1962 |
|
|
|
1963 |
|
|
|
1964 |
/**
|
|
|
1965 |
* Updates raw grade value for given user, this is a only way to update raw
|
|
|
1966 |
* grades from external source (modules, etc.),
|
|
|
1967 |
* because it logs the change in history table and deals with final grade recalculation.
|
|
|
1968 |
*
|
|
|
1969 |
* @param int $userid the graded user
|
|
|
1970 |
* @param mixed $rawgrade float value of raw grade - false means do not change
|
|
|
1971 |
* @param string $source modification source
|
|
|
1972 |
* @param string $feedback optional teacher feedback
|
|
|
1973 |
* @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
|
|
|
1974 |
* @param int $usermodified the ID of the user who did the grading
|
|
|
1975 |
* @param int $dategraded A timestamp of when the student's work was graded
|
|
|
1976 |
* @param int $datesubmitted A timestamp of when the student's work was submitted
|
|
|
1977 |
* @param grade_grade $grade A grade object, useful for bulk upgrades
|
|
|
1978 |
* @param array $feedbackfiles An array identifying the location of files we want to copy to the gradebook feedback area.
|
|
|
1979 |
* Example -
|
|
|
1980 |
* [
|
|
|
1981 |
* 'contextid' => 1,
|
|
|
1982 |
* 'component' => 'mod_xyz',
|
|
|
1983 |
* 'filearea' => 'mod_xyz_feedback',
|
|
|
1984 |
* 'itemid' => 2
|
|
|
1985 |
* ];
|
|
|
1986 |
* @param bool $isbulkupdate If bulk grade update is happening.
|
|
|
1987 |
* @return bool success
|
|
|
1988 |
*/
|
|
|
1989 |
public function update_raw_grade($userid, $rawgrade = false, $source = null, $feedback = false,
|
|
|
1990 |
$feedbackformat = FORMAT_MOODLE, $usermodified = null, $dategraded = null, $datesubmitted=null,
|
|
|
1991 |
$grade = null, array $feedbackfiles = [], $isbulkupdate = false) {
|
|
|
1992 |
global $USER;
|
|
|
1993 |
|
|
|
1994 |
$result = true;
|
|
|
1995 |
|
|
|
1996 |
// calculated grades can not be updated; course and category can not be updated because they are aggregated
|
|
|
1997 |
if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
|
|
|
1998 |
return false;
|
|
|
1999 |
}
|
|
|
2000 |
|
|
|
2001 |
if (is_null($grade)) {
|
|
|
2002 |
//fetch from db
|
|
|
2003 |
$grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
|
|
|
2004 |
}
|
|
|
2005 |
$grade->grade_item =& $this; // prevent db fetching of this grade_item
|
|
|
2006 |
|
|
|
2007 |
if (empty($usermodified)) {
|
|
|
2008 |
$grade->usermodified = $USER->id;
|
|
|
2009 |
} else {
|
|
|
2010 |
$grade->usermodified = $usermodified;
|
|
|
2011 |
}
|
|
|
2012 |
|
|
|
2013 |
if ($grade->is_locked()) {
|
|
|
2014 |
// do not update locked grades at all
|
|
|
2015 |
return false;
|
|
|
2016 |
}
|
|
|
2017 |
|
|
|
2018 |
$locktime = $grade->get_locktime();
|
|
|
2019 |
if ($locktime and $locktime < time()) {
|
|
|
2020 |
// do not update grades that should be already locked and force regrade
|
|
|
2021 |
$this->force_regrading();
|
|
|
2022 |
return false;
|
|
|
2023 |
}
|
|
|
2024 |
|
|
|
2025 |
$oldgrade = new stdClass();
|
|
|
2026 |
$oldgrade->finalgrade = $grade->finalgrade;
|
|
|
2027 |
$oldgrade->rawgrade = $grade->rawgrade;
|
|
|
2028 |
$oldgrade->rawgrademin = $grade->rawgrademin;
|
|
|
2029 |
$oldgrade->rawgrademax = $grade->rawgrademax;
|
|
|
2030 |
$oldgrade->rawscaleid = $grade->rawscaleid;
|
|
|
2031 |
$oldgrade->feedback = $grade->feedback;
|
|
|
2032 |
$oldgrade->feedbackformat = $grade->feedbackformat;
|
|
|
2033 |
|
|
|
2034 |
// use new min and max
|
|
|
2035 |
$grade->rawgrade = $grade->rawgrade;
|
|
|
2036 |
$grade->rawgrademin = $this->grademin;
|
|
|
2037 |
$grade->rawgrademax = $this->grademax;
|
|
|
2038 |
$grade->rawscaleid = $this->scaleid;
|
|
|
2039 |
|
|
|
2040 |
// change raw grade?
|
|
|
2041 |
if ($rawgrade !== false) {
|
|
|
2042 |
$grade->rawgrade = $rawgrade;
|
|
|
2043 |
}
|
|
|
2044 |
|
|
|
2045 |
// empty feedback means no feedback at all
|
|
|
2046 |
if ($feedback === '') {
|
|
|
2047 |
$feedback = null;
|
|
|
2048 |
}
|
|
|
2049 |
|
|
|
2050 |
// do we have comment from teacher?
|
|
|
2051 |
if ($feedback !== false and !$grade->is_overridden()) {
|
|
|
2052 |
$grade->feedback = $feedback;
|
|
|
2053 |
$grade->feedbackformat = $feedbackformat;
|
|
|
2054 |
$grade->feedbackfiles = $feedbackfiles;
|
|
|
2055 |
}
|
|
|
2056 |
|
|
|
2057 |
// update final grade if possible
|
|
|
2058 |
if (!$grade->is_locked() and !$grade->is_overridden()) {
|
|
|
2059 |
$grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
|
|
|
2060 |
}
|
|
|
2061 |
|
|
|
2062 |
// TODO: hack alert - create new fields for these in 2.0
|
|
|
2063 |
$oldgrade->timecreated = $grade->timecreated;
|
|
|
2064 |
$oldgrade->timemodified = $grade->timemodified;
|
|
|
2065 |
|
|
|
2066 |
$grade->timecreated = $datesubmitted;
|
|
|
2067 |
|
|
|
2068 |
if ($grade->is_overridden()) {
|
|
|
2069 |
// keep original graded date - update_final_grade() sets this for overridden grades
|
|
|
2070 |
|
|
|
2071 |
} else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
|
|
|
2072 |
// no grade and feedback means no grading yet
|
|
|
2073 |
$grade->timemodified = null;
|
|
|
2074 |
|
|
|
2075 |
} else if (!empty($dategraded)) {
|
|
|
2076 |
// fine - module sends info when graded (yay!)
|
|
|
2077 |
$grade->timemodified = $dategraded;
|
|
|
2078 |
|
|
|
2079 |
} else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
|
|
|
2080 |
or $grade->feedback !== $oldgrade->feedback) {
|
|
|
2081 |
// guess - if either grade or feedback changed set new graded date
|
|
|
2082 |
$grade->timemodified = time();
|
|
|
2083 |
|
|
|
2084 |
} else {
|
|
|
2085 |
//keep original graded date
|
|
|
2086 |
}
|
|
|
2087 |
// end of hack alert
|
|
|
2088 |
|
|
|
2089 |
$gradechanged = false;
|
|
|
2090 |
if (empty($grade->id)) {
|
|
|
2091 |
$result = (bool)$grade->insert($source, $isbulkupdate);
|
|
|
2092 |
|
|
|
2093 |
// If the grade insert was successful and the final grade was not null then trigger a user_graded event.
|
|
|
2094 |
if ($result && !is_null($grade->finalgrade)) {
|
|
|
2095 |
\core\event\user_graded::create_from_grade($grade)->trigger();
|
|
|
2096 |
}
|
|
|
2097 |
$gradechanged = true;
|
|
|
2098 |
} else {
|
|
|
2099 |
// Existing grade_grades.
|
|
|
2100 |
|
|
|
2101 |
if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
|
|
|
2102 |
or grade_floats_different($grade->rawgrade, $oldgrade->rawgrade)
|
|
|
2103 |
or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
|
|
|
2104 |
or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
|
|
|
2105 |
or $grade->rawscaleid != $oldgrade->rawscaleid) {
|
|
|
2106 |
$gradechanged = true;
|
|
|
2107 |
}
|
|
|
2108 |
|
|
|
2109 |
// The timecreated and timemodified checking is part of the hack above.
|
|
|
2110 |
if ($gradechanged === false and
|
|
|
2111 |
$grade->feedback === $oldgrade->feedback and
|
|
|
2112 |
$grade->feedbackformat == $oldgrade->feedbackformat and
|
|
|
2113 |
$grade->timecreated == $oldgrade->timecreated and
|
|
|
2114 |
$grade->timemodified == $oldgrade->timemodified) {
|
|
|
2115 |
// No changes.
|
|
|
2116 |
return $result;
|
|
|
2117 |
}
|
|
|
2118 |
$result = $grade->update($source, $isbulkupdate);
|
|
|
2119 |
|
|
|
2120 |
// If the grade update was successful and the actual grade has changed then trigger a user_graded event.
|
|
|
2121 |
if ($result && grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)) {
|
|
|
2122 |
\core\event\user_graded::create_from_grade($grade)->trigger();
|
|
|
2123 |
}
|
|
|
2124 |
}
|
|
|
2125 |
|
|
|
2126 |
if (!$result) {
|
|
|
2127 |
// Something went wrong - better force final grade recalculation.
|
|
|
2128 |
$this->force_regrading();
|
|
|
2129 |
return $result;
|
|
|
2130 |
}
|
|
|
2131 |
|
|
|
2132 |
// If we are not updating grades we don't need to recalculate the whole course.
|
|
|
2133 |
if (!$gradechanged) {
|
|
|
2134 |
return $result;
|
|
|
2135 |
}
|
|
|
2136 |
|
|
|
2137 |
if (!$this->needsupdate) {
|
|
|
2138 |
$course_item = grade_item::fetch_course_item($this->courseid);
|
|
|
2139 |
if (!$course_item->needsupdate) {
|
|
|
2140 |
if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
|
|
|
2141 |
$this->force_regrading();
|
|
|
2142 |
}
|
|
|
2143 |
}
|
|
|
2144 |
}
|
|
|
2145 |
|
|
|
2146 |
return $result;
|
|
|
2147 |
}
|
|
|
2148 |
|
|
|
2149 |
/**
|
|
|
2150 |
* Calculates final grade values using the formula in the calculation property.
|
|
|
2151 |
* The parameters are taken from final grades of grade items in current course only.
|
|
|
2152 |
*
|
|
|
2153 |
* @param int $userid Supply a user ID to limit the calculations to the grades of a single user
|
|
|
2154 |
* @return bool false if error
|
|
|
2155 |
*/
|
|
|
2156 |
public function compute($userid=null) {
|
|
|
2157 |
global $CFG, $DB;
|
|
|
2158 |
|
|
|
2159 |
if (!$this->is_calculated()) {
|
|
|
2160 |
return false;
|
|
|
2161 |
}
|
|
|
2162 |
|
|
|
2163 |
require_once($CFG->libdir.'/mathslib.php');
|
|
|
2164 |
|
|
|
2165 |
if ($this->is_locked()) {
|
|
|
2166 |
return true; // no need to recalculate locked items
|
|
|
2167 |
}
|
|
|
2168 |
|
|
|
2169 |
// Precreate grades - we need them to exist
|
|
|
2170 |
if ($userid) {
|
|
|
2171 |
$missing = array();
|
|
|
2172 |
if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id, 'userid'=>$userid))) {
|
|
|
2173 |
$m = new stdClass();
|
|
|
2174 |
$m->userid = $userid;
|
|
|
2175 |
$missing[] = $m;
|
|
|
2176 |
}
|
|
|
2177 |
} else {
|
|
|
2178 |
// Find any users who have grades for some but not all grade items in this course
|
|
|
2179 |
$params = array('gicourseid' => $this->courseid, 'ggitemid' => $this->id);
|
|
|
2180 |
$sql = "SELECT gg.userid
|
|
|
2181 |
FROM {grade_grades} gg
|
|
|
2182 |
JOIN {grade_items} gi
|
|
|
2183 |
ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
|
|
|
2184 |
GROUP BY gg.userid
|
|
|
2185 |
HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
|
|
|
2186 |
$missing = $DB->get_records_sql($sql, $params);
|
|
|
2187 |
}
|
|
|
2188 |
|
|
|
2189 |
if ($missing) {
|
|
|
2190 |
foreach ($missing as $m) {
|
|
|
2191 |
$grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
|
|
|
2192 |
$grade->grade_item =& $this;
|
|
|
2193 |
$grade->insert('system');
|
|
|
2194 |
}
|
|
|
2195 |
}
|
|
|
2196 |
|
|
|
2197 |
// get used items
|
|
|
2198 |
$useditems = $this->depends_on();
|
|
|
2199 |
|
|
|
2200 |
// prepare formula and init maths library
|
|
|
2201 |
$formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
|
|
|
2202 |
if (strpos($formula, '[[') !== false) {
|
|
|
2203 |
// missing item
|
|
|
2204 |
return false;
|
|
|
2205 |
}
|
|
|
2206 |
$this->formula = new calc_formula($formula);
|
|
|
2207 |
|
|
|
2208 |
// where to look for final grades?
|
|
|
2209 |
// this itemid is added so that we use only one query for source and final grades
|
|
|
2210 |
$gis = array_merge($useditems, array($this->id));
|
|
|
2211 |
list($usql, $params) = $DB->get_in_or_equal($gis);
|
|
|
2212 |
|
|
|
2213 |
if ($userid) {
|
|
|
2214 |
$usersql = "AND g.userid=?";
|
|
|
2215 |
$params[] = $userid;
|
|
|
2216 |
} else {
|
|
|
2217 |
$usersql = "";
|
|
|
2218 |
}
|
|
|
2219 |
|
|
|
2220 |
$grade_inst = new grade_grade();
|
|
|
2221 |
$fields = 'g.'.implode(',g.', $grade_inst->required_fields);
|
|
|
2222 |
|
|
|
2223 |
$params[] = $this->courseid;
|
|
|
2224 |
$sql = "SELECT $fields
|
|
|
2225 |
FROM {grade_grades} g, {grade_items} gi
|
|
|
2226 |
WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
|
|
|
2227 |
ORDER BY g.userid";
|
|
|
2228 |
|
|
|
2229 |
$return = true;
|
|
|
2230 |
|
|
|
2231 |
// group the grades by userid and use formula on the group
|
|
|
2232 |
$rs = $DB->get_recordset_sql($sql, $params);
|
|
|
2233 |
if ($rs->valid()) {
|
|
|
2234 |
$prevuser = 0;
|
|
|
2235 |
$grade_records = array();
|
|
|
2236 |
$oldgrade = null;
|
|
|
2237 |
foreach ($rs as $used) {
|
|
|
2238 |
if ($used->userid != $prevuser) {
|
|
|
2239 |
if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
|
|
|
2240 |
$return = false;
|
|
|
2241 |
}
|
|
|
2242 |
$prevuser = $used->userid;
|
|
|
2243 |
$grade_records = array();
|
|
|
2244 |
$oldgrade = null;
|
|
|
2245 |
}
|
|
|
2246 |
if ($used->itemid == $this->id) {
|
|
|
2247 |
$oldgrade = $used;
|
|
|
2248 |
}
|
|
|
2249 |
$grade_records['gi'.$used->itemid] = $used->finalgrade;
|
|
|
2250 |
}
|
|
|
2251 |
if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
|
|
|
2252 |
$return = false;
|
|
|
2253 |
}
|
|
|
2254 |
}
|
|
|
2255 |
$rs->close();
|
|
|
2256 |
|
|
|
2257 |
return $return;
|
|
|
2258 |
}
|
|
|
2259 |
|
|
|
2260 |
/**
|
|
|
2261 |
* Internal function that does the final grade calculation
|
|
|
2262 |
*
|
|
|
2263 |
* @param int $userid The user ID
|
|
|
2264 |
* @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
|
|
|
2265 |
* @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
|
|
|
2266 |
* @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
|
|
|
2267 |
* @return bool False if an error occurred
|
|
|
2268 |
*/
|
|
|
2269 |
public function use_formula($userid, $params, $useditems, $oldgrade) {
|
|
|
2270 |
if (empty($userid)) {
|
|
|
2271 |
return true;
|
|
|
2272 |
}
|
|
|
2273 |
|
|
|
2274 |
// add missing final grade values
|
|
|
2275 |
// not graded (null) is counted as 0 - the spreadsheet way
|
|
|
2276 |
$allinputsnull = true;
|
|
|
2277 |
foreach($useditems as $gi) {
|
|
|
2278 |
if (!array_key_exists('gi'.$gi, $params) || is_null($params['gi'.$gi])) {
|
|
|
2279 |
$params['gi'.$gi] = 0;
|
|
|
2280 |
} else {
|
|
|
2281 |
$params['gi'.$gi] = (float)$params['gi'.$gi];
|
|
|
2282 |
if ($gi != $this->id) {
|
|
|
2283 |
$allinputsnull = false;
|
|
|
2284 |
}
|
|
|
2285 |
}
|
|
|
2286 |
}
|
|
|
2287 |
|
|
|
2288 |
// can not use own final grade during calculation
|
|
|
2289 |
unset($params['gi'.$this->id]);
|
|
|
2290 |
|
|
|
2291 |
// Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
|
|
|
2292 |
// wish to update the grades.
|
|
|
2293 |
$gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid);
|
|
|
2294 |
|
|
|
2295 |
$rawminandmaxchanged = false;
|
|
|
2296 |
// insert final grade - will be needed later anyway
|
|
|
2297 |
if ($oldgrade) {
|
|
|
2298 |
// Only run through this code if the gradebook isn't frozen.
|
|
|
2299 |
if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
|
|
|
2300 |
// Do nothing.
|
|
|
2301 |
} else {
|
|
|
2302 |
// The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
|
|
|
2303 |
// grade_item grade maximum and minimum respectively.
|
|
|
2304 |
if ($oldgrade->rawgrademax != $this->grademax || $oldgrade->rawgrademin != $this->grademin) {
|
|
|
2305 |
$rawminandmaxchanged = true;
|
|
|
2306 |
$oldgrade->rawgrademax = $this->grademax;
|
|
|
2307 |
$oldgrade->rawgrademin = $this->grademin;
|
|
|
2308 |
}
|
|
|
2309 |
}
|
|
|
2310 |
$oldfinalgrade = $oldgrade->finalgrade;
|
|
|
2311 |
$grade = new grade_grade($oldgrade, false); // fetching from db is not needed
|
|
|
2312 |
$grade->grade_item =& $this;
|
|
|
2313 |
|
|
|
2314 |
} else {
|
|
|
2315 |
$grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
|
|
|
2316 |
$grade->grade_item =& $this;
|
|
|
2317 |
$rawminandmaxchanged = false;
|
|
|
2318 |
if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
|
|
|
2319 |
// Do nothing.
|
|
|
2320 |
} else {
|
|
|
2321 |
// The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
|
|
|
2322 |
// grade_item grade maximum and minimum respectively.
|
|
|
2323 |
$rawminandmaxchanged = true;
|
|
|
2324 |
$grade->rawgrademax = $this->grademax;
|
|
|
2325 |
$grade->rawgrademin = $this->grademin;
|
|
|
2326 |
}
|
|
|
2327 |
$grade->insert('system');
|
|
|
2328 |
$oldfinalgrade = null;
|
|
|
2329 |
}
|
|
|
2330 |
|
|
|
2331 |
// no need to recalculate locked or overridden grades
|
|
|
2332 |
if ($grade->is_locked() or $grade->is_overridden()) {
|
|
|
2333 |
return true;
|
|
|
2334 |
}
|
|
|
2335 |
|
|
|
2336 |
if ($allinputsnull) {
|
|
|
2337 |
$grade->finalgrade = null;
|
|
|
2338 |
$result = true;
|
|
|
2339 |
|
|
|
2340 |
} else {
|
|
|
2341 |
|
|
|
2342 |
// do the calculation
|
|
|
2343 |
$this->formula->set_params($params);
|
|
|
2344 |
$result = $this->formula->evaluate();
|
|
|
2345 |
|
|
|
2346 |
if ($result === false) {
|
|
|
2347 |
$grade->finalgrade = null;
|
|
|
2348 |
|
|
|
2349 |
} else {
|
|
|
2350 |
// normalize
|
|
|
2351 |
$grade->finalgrade = $this->bounded_grade($result);
|
|
|
2352 |
}
|
|
|
2353 |
}
|
|
|
2354 |
|
|
|
2355 |
// Only run through this code if the gradebook isn't frozen.
|
|
|
2356 |
if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
|
|
|
2357 |
// Update in db if changed.
|
|
|
2358 |
if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
|
|
|
2359 |
$grade->timemodified = time();
|
|
|
2360 |
$success = $grade->update('compute');
|
|
|
2361 |
|
|
|
2362 |
// If successful trigger a user_graded event.
|
|
|
2363 |
if ($success) {
|
|
|
2364 |
\core\event\user_graded::create_from_grade($grade)->trigger();
|
|
|
2365 |
}
|
|
|
2366 |
}
|
|
|
2367 |
} else {
|
|
|
2368 |
// Update in db if changed.
|
|
|
2369 |
if (grade_floats_different($grade->finalgrade, $oldfinalgrade) || $rawminandmaxchanged) {
|
|
|
2370 |
$grade->timemodified = time();
|
|
|
2371 |
$success = $grade->update('compute');
|
|
|
2372 |
|
|
|
2373 |
// If successful trigger a user_graded event.
|
|
|
2374 |
if ($success) {
|
|
|
2375 |
\core\event\user_graded::create_from_grade($grade)->trigger();
|
|
|
2376 |
}
|
|
|
2377 |
}
|
|
|
2378 |
}
|
|
|
2379 |
|
|
|
2380 |
if ($result !== false) {
|
|
|
2381 |
//lock grade if needed
|
|
|
2382 |
}
|
|
|
2383 |
|
|
|
2384 |
if ($result === false) {
|
|
|
2385 |
return false;
|
|
|
2386 |
} else {
|
|
|
2387 |
return true;
|
|
|
2388 |
}
|
|
|
2389 |
|
|
|
2390 |
}
|
|
|
2391 |
|
|
|
2392 |
/**
|
|
|
2393 |
* Validate the formula.
|
|
|
2394 |
*
|
|
|
2395 |
* @param string $formulastr
|
|
|
2396 |
* @return bool true if calculation possible, false otherwise
|
|
|
2397 |
*/
|
|
|
2398 |
public function validate_formula($formulastr) {
|
|
|
2399 |
global $CFG, $DB;
|
|
|
2400 |
require_once($CFG->libdir.'/mathslib.php');
|
|
|
2401 |
|
|
|
2402 |
$formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
|
|
|
2403 |
|
|
|
2404 |
if (empty($formulastr)) {
|
|
|
2405 |
return true;
|
|
|
2406 |
}
|
|
|
2407 |
|
|
|
2408 |
if (strpos($formulastr, '=') !== 0) {
|
|
|
2409 |
return get_string('errorcalculationnoequal', 'grades');
|
|
|
2410 |
}
|
|
|
2411 |
|
|
|
2412 |
// get used items
|
|
|
2413 |
if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
|
|
|
2414 |
$useditems = array_unique($matches[1]); // remove duplicates
|
|
|
2415 |
} else {
|
|
|
2416 |
$useditems = array();
|
|
|
2417 |
}
|
|
|
2418 |
|
|
|
2419 |
// MDL-11902
|
|
|
2420 |
// unset the value if formula is trying to reference to itself
|
|
|
2421 |
// but array keys does not match itemid
|
|
|
2422 |
if (!empty($this->id)) {
|
|
|
2423 |
$useditems = array_diff($useditems, array($this->id));
|
|
|
2424 |
//unset($useditems[$this->id]);
|
|
|
2425 |
}
|
|
|
2426 |
|
|
|
2427 |
// prepare formula and init maths library
|
|
|
2428 |
$formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
|
|
|
2429 |
$formula = new calc_formula($formula);
|
|
|
2430 |
|
|
|
2431 |
|
|
|
2432 |
if (empty($useditems)) {
|
|
|
2433 |
$grade_items = array();
|
|
|
2434 |
|
|
|
2435 |
} else {
|
|
|
2436 |
list($usql, $params) = $DB->get_in_or_equal($useditems);
|
|
|
2437 |
$params[] = $this->courseid;
|
|
|
2438 |
$sql = "SELECT gi.*
|
|
|
2439 |
FROM {grade_items} gi
|
|
|
2440 |
WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
|
|
|
2441 |
|
|
|
2442 |
if (!$grade_items = $DB->get_records_sql($sql, $params)) {
|
|
|
2443 |
$grade_items = array();
|
|
|
2444 |
}
|
|
|
2445 |
}
|
|
|
2446 |
|
|
|
2447 |
$params = array();
|
|
|
2448 |
foreach ($useditems as $itemid) {
|
|
|
2449 |
// make sure all grade items exist in this course
|
|
|
2450 |
if (!array_key_exists($itemid, $grade_items)) {
|
|
|
2451 |
return false;
|
|
|
2452 |
}
|
|
|
2453 |
// use max grade when testing formula, this should be ok in 99.9%
|
|
|
2454 |
// division by 0 is one of possible problems
|
|
|
2455 |
$params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
|
|
|
2456 |
}
|
|
|
2457 |
|
|
|
2458 |
// do the calculation
|
|
|
2459 |
$formula->set_params($params);
|
|
|
2460 |
$result = $formula->evaluate();
|
|
|
2461 |
|
|
|
2462 |
// false as result indicates some problem
|
|
|
2463 |
if ($result === false) {
|
|
|
2464 |
// TODO: add more error hints
|
|
|
2465 |
return get_string('errorcalculationunknown', 'grades');
|
|
|
2466 |
} else {
|
|
|
2467 |
return true;
|
|
|
2468 |
}
|
|
|
2469 |
}
|
|
|
2470 |
|
|
|
2471 |
/**
|
|
|
2472 |
* Returns the value of the display type
|
|
|
2473 |
*
|
|
|
2474 |
* It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
|
|
|
2475 |
*
|
|
|
2476 |
* @return int Display type
|
|
|
2477 |
*/
|
|
|
2478 |
public function get_displaytype() {
|
|
|
2479 |
global $CFG;
|
|
|
2480 |
|
|
|
2481 |
if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
|
|
|
2482 |
return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
|
|
|
2483 |
|
|
|
2484 |
} else {
|
|
|
2485 |
return $this->display;
|
|
|
2486 |
}
|
|
|
2487 |
}
|
|
|
2488 |
|
|
|
2489 |
/**
|
|
|
2490 |
* Returns the value of the decimals field
|
|
|
2491 |
*
|
|
|
2492 |
* It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
|
|
|
2493 |
*
|
|
|
2494 |
* @return int Decimals (0 - 5)
|
|
|
2495 |
*/
|
|
|
2496 |
public function get_decimals() {
|
|
|
2497 |
global $CFG;
|
|
|
2498 |
|
|
|
2499 |
if (is_null($this->decimals)) {
|
|
|
2500 |
return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
|
|
|
2501 |
|
|
|
2502 |
} else {
|
|
|
2503 |
return $this->decimals;
|
|
|
2504 |
}
|
|
|
2505 |
}
|
|
|
2506 |
|
|
|
2507 |
/**
|
|
|
2508 |
* Returns a string representing the range of grademin - grademax for this grade item.
|
|
|
2509 |
*
|
|
|
2510 |
* @param int $rangesdisplaytype
|
|
|
2511 |
* @param int $rangesdecimalpoints
|
|
|
2512 |
* @return string
|
|
|
2513 |
*/
|
|
|
2514 |
function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
|
|
|
2515 |
|
|
|
2516 |
global $USER;
|
|
|
2517 |
|
|
|
2518 |
// Determine which display type to use for this average
|
|
|
2519 |
if (isset($USER->editing) && $USER->editing) {
|
|
|
2520 |
$displaytype = GRADE_DISPLAY_TYPE_REAL;
|
|
|
2521 |
|
|
|
2522 |
} else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
|
|
|
2523 |
$displaytype = $this->get_displaytype();
|
|
|
2524 |
|
|
|
2525 |
} else {
|
|
|
2526 |
$displaytype = $rangesdisplaytype;
|
|
|
2527 |
}
|
|
|
2528 |
|
|
|
2529 |
// Override grade_item setting if a display preference (not default) was set for the averages
|
|
|
2530 |
if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
|
|
|
2531 |
$decimalpoints = $this->get_decimals();
|
|
|
2532 |
|
|
|
2533 |
} else {
|
|
|
2534 |
$decimalpoints = $rangesdecimalpoints;
|
|
|
2535 |
}
|
|
|
2536 |
|
|
|
2537 |
if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
|
|
|
2538 |
$grademin = "0 %";
|
|
|
2539 |
$grademax = "100 %";
|
|
|
2540 |
|
|
|
2541 |
} else {
|
|
|
2542 |
$grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
|
|
|
2543 |
$grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
|
|
|
2544 |
}
|
|
|
2545 |
|
|
|
2546 |
return $grademin.'–'. $grademax;
|
|
|
2547 |
}
|
|
|
2548 |
|
|
|
2549 |
/**
|
|
|
2550 |
* Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
|
|
|
2551 |
*
|
|
|
2552 |
* @return string|false Returns the coefficient string of false is no coefficient is being used
|
|
|
2553 |
*/
|
|
|
2554 |
public function get_coefstring() {
|
|
|
2555 |
$parent_category = $this->load_parent_category();
|
|
|
2556 |
if ($this->is_category_item()) {
|
|
|
2557 |
$parent_category = $parent_category->load_parent_category();
|
|
|
2558 |
}
|
|
|
2559 |
|
|
|
2560 |
if ($parent_category->is_aggregationcoef_used()) {
|
|
|
2561 |
return $parent_category->get_coefstring();
|
|
|
2562 |
} else {
|
|
|
2563 |
return false;
|
|
|
2564 |
}
|
|
|
2565 |
}
|
|
|
2566 |
|
|
|
2567 |
/**
|
|
|
2568 |
* Returns whether the grade item can control the visibility of the grades
|
|
|
2569 |
*
|
|
|
2570 |
* @return bool
|
|
|
2571 |
*/
|
|
|
2572 |
public function can_control_visibility() {
|
|
|
2573 |
if (core_component::get_plugin_directory($this->itemtype, $this->itemmodule)) {
|
|
|
2574 |
return !plugin_supports($this->itemtype, $this->itemmodule, FEATURE_CONTROLS_GRADE_VISIBILITY, false);
|
|
|
2575 |
}
|
|
|
2576 |
return parent::can_control_visibility();
|
|
|
2577 |
}
|
|
|
2578 |
|
|
|
2579 |
/**
|
|
|
2580 |
* Used to notify the completion system (if necessary) that a user's grade
|
|
|
2581 |
* has changed, and clear up a possible score cache.
|
|
|
2582 |
*
|
|
|
2583 |
* @param bool $deleted True if grade was actually deleted
|
|
|
2584 |
*/
|
|
|
2585 |
protected function notify_changed($deleted) {
|
|
|
2586 |
global $CFG;
|
|
|
2587 |
|
|
|
2588 |
// Condition code may cache the grades for conditional availability of
|
|
|
2589 |
// modules or sections. (This code should use a hook for communication
|
|
|
2590 |
// with plugin, but hooks are not implemented at time of writing.)
|
|
|
2591 |
if (!empty($CFG->enableavailability) && class_exists('\availability_grade\callbacks')) {
|
|
|
2592 |
\availability_grade\callbacks::grade_item_changed($this->courseid);
|
|
|
2593 |
}
|
|
|
2594 |
}
|
|
|
2595 |
|
|
|
2596 |
/**
|
|
|
2597 |
* Helper function to get the accurate context for this grade column.
|
|
|
2598 |
*
|
|
|
2599 |
* @return context
|
|
|
2600 |
*/
|
|
|
2601 |
public function get_context() {
|
|
|
2602 |
if ($this->itemtype == 'mod') {
|
|
|
2603 |
$modinfo = get_fast_modinfo($this->courseid);
|
|
|
2604 |
// Sometimes the course module cache is out of date and needs to be rebuilt.
|
|
|
2605 |
if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
|
|
|
2606 |
rebuild_course_cache($this->courseid, true);
|
|
|
2607 |
$modinfo = get_fast_modinfo($this->courseid);
|
|
|
2608 |
}
|
|
|
2609 |
// Even with a rebuilt cache the module does not exist. This means the
|
|
|
2610 |
// database is in an invalid state - we will log an error and return
|
|
|
2611 |
// the course context but the calling code should be updated.
|
|
|
2612 |
if (!isset($modinfo->instances[$this->itemmodule][$this->iteminstance])) {
|
|
|
2613 |
mtrace(get_string('moduleinstancedoesnotexist', 'error'));
|
|
|
2614 |
$context = \context_course::instance($this->courseid);
|
|
|
2615 |
} else {
|
|
|
2616 |
$cm = $modinfo->instances[$this->itemmodule][$this->iteminstance];
|
|
|
2617 |
$context = \context_module::instance($cm->id);
|
|
|
2618 |
}
|
|
|
2619 |
} else {
|
|
|
2620 |
$context = \context_course::instance($this->courseid);
|
|
|
2621 |
}
|
|
|
2622 |
return $context;
|
|
|
2623 |
}
|
|
|
2624 |
}
|