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