MDL-16075 adding support for embedded Glossary images
[moodle.git] / lib / grade / grade_item.php
CommitLineData
3c2e81ee 1<?php // $Id$
2
3///////////////////////////////////////////////////////////////////////////
4// //
5// NOTICE OF COPYRIGHT //
6// //
7// Moodle - Modular Object-Oriented Dynamic Learning Environment //
8// http://moodle.com //
9// //
4fc9ec1e 10// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
3c2e81ee 11// //
12// This program is free software; you can redistribute it and/or modify //
13// it under the terms of the GNU General Public License as published by //
14// the Free Software Foundation; either version 2 of the License, or //
15// (at your option) any later version. //
16// //
17// This program is distributed in the hope that it will be useful, //
18// but WITHOUT ANY WARRANTY; without even the implied warranty of //
19// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
20// GNU General Public License for more details: //
21// //
22// http://www.gnu.org/copyleft/gpl.html //
23// //
24///////////////////////////////////////////////////////////////////////////
25
26require_once('grade_object.php');
4ac209d5 27
3c2e81ee 28/**
29 * Class representing a grade item. It is responsible for handling its DB representation,
30 * modifying and returning its metadata.
31 */
32class grade_item extends grade_object {
33 /**
34 * DB Table (used by grade_object).
35 * @var string $table
36 */
da3801e8 37 public $table = 'grade_items';
3c2e81ee 38
39 /**
40 * Array of required table fields, must start with 'id'.
41 * @var array $required_fields
42 */
da3801e8 43 public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
3c2e81ee 44 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
45 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
46 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated',
47 'timemodified');
48
49 /**
50 * The course this grade_item belongs to.
51 * @var int $courseid
52 */
da3801e8 53 public $courseid;
3c2e81ee 54
55 /**
56 * The category this grade_item belongs to (optional).
57 * @var int $categoryid
58 */
da3801e8 59 public $categoryid;
3c2e81ee 60
61 /**
62 * The grade_category object referenced $this->iteminstance (itemtype must be == 'category' or == 'course' in that case).
63 * @var object $item_category
64 */
da3801e8 65 public $item_category;
3c2e81ee 66
67 /**
68 * The grade_category object referenced by $this->categoryid.
69 * @var object $parent_category
70 */
da3801e8 71 public $parent_category;
3c2e81ee 72
73
74 /**
75 * The name of this grade_item (pushed by the module).
76 * @var string $itemname
77 */
da3801e8 78 public $itemname;
3c2e81ee 79
80 /**
81 * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
82 * @var string $itemtype
83 */
da3801e8 84 public $itemtype;
3c2e81ee 85
86 /**
87 * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
88 * @var string $itemmodule
89 */
da3801e8 90 public $itemmodule;
3c2e81ee 91
92 /**
93 * ID of the item module
94 * @var int $iteminstance
95 */
da3801e8 96 public $iteminstance;
3c2e81ee 97
98 /**
99 * Number of the item in a series of multiple grades pushed by an activity.
100 * @var int $itemnumber
101 */
da3801e8 102 public $itemnumber;
3c2e81ee 103
104 /**
105 * Info and notes about this item.
106 * @var string $iteminfo
107 */
da3801e8 108 public $iteminfo;
3c2e81ee 109
110 /**
111 * Arbitrary idnumber provided by the module responsible.
112 * @var string $idnumber
113 */
da3801e8 114 public $idnumber;
3c2e81ee 115
116 /**
117 * Calculation string used for this item.
118 * @var string $calculation
119 */
da3801e8 120 public $calculation;
3c2e81ee 121
122 /**
123 * Indicates if we already tried to normalize the grade calculation formula.
124 * This flag helps to minimize db access when broken formulas used in calculation.
125 * @var boolean
126 */
da3801e8 127 public $calculation_normalized;
3c2e81ee 128 /**
129 * Math evaluation object
130 */
da3801e8 131 public $formula;
3c2e81ee 132
133 /**
134 * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
135 * @var int $gradetype
136 */
da3801e8 137 public $gradetype = GRADE_TYPE_VALUE;
3c2e81ee 138
139 /**
140 * Maximum allowable grade.
141 * @var float $grademax
142 */
da3801e8 143 public $grademax = 100;
3c2e81ee 144
145 /**
146 * Minimum allowable grade.
147 * @var float $grademin
148 */
da3801e8 149 public $grademin = 0;
3c2e81ee 150
151 /**
152 * id of the scale, if this grade is based on a scale.
153 * @var int $scaleid
154 */
da3801e8 155 public $scaleid;
3c2e81ee 156
157 /**
158 * A grade_scale object (referenced by $this->scaleid).
159 * @var object $scale
160 */
da3801e8 161 public $scale;
3c2e81ee 162
163 /**
164 * The id of the optional grade_outcome associated with this grade_item.
165 * @var int $outcomeid
166 */
da3801e8 167 public $outcomeid;
3c2e81ee 168
169 /**
170 * The grade_outcome this grade is associated with, if applicable.
171 * @var object $outcome
172 */
da3801e8 173 public $outcome;
3c2e81ee 174
175 /**
176 * grade required to pass. (grademin <= gradepass <= grademax)
177 * @var float $gradepass
178 */
da3801e8 179 public $gradepass = 0;
3c2e81ee 180
181 /**
182 * Multiply all grades by this number.
183 * @var float $multfactor
184 */
da3801e8 185 public $multfactor = 1.0;
3c2e81ee 186
187 /**
188 * Add this to all grades.
189 * @var float $plusfactor
190 */
da3801e8 191 public $plusfactor = 0;
3c2e81ee 192
193 /**
194 * Aggregation coeficient used for weighted averages
195 * @var float $aggregationcoef
196 */
da3801e8 197 public $aggregationcoef = 0;
3c2e81ee 198
199 /**
200 * Sorting order of the columns.
201 * @var int $sortorder
202 */
da3801e8 203 public $sortorder = 0;
3c2e81ee 204
205 /**
206 * Display type of the grades (Real, Percentage, Letter, or default).
207 * @var int $display
208 */
da3801e8 209 public $display = GRADE_DISPLAY_TYPE_DEFAULT;
3c2e81ee 210
211 /**
212 * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
213 * @var int $decimals
214 */
da3801e8 215 public $decimals = null;
3c2e81ee 216
217 /**
218 * 0 if visible, 1 always hidden or date not visible until
219 * @var int $hidden
220 */
da3801e8 221 public $hidden = 0;
3c2e81ee 222
223 /**
224 * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
225 * @var int $locked
226 */
da3801e8 227 public $locked = 0;
3c2e81ee 228
229 /**
230 * Date after which the grade will be locked. Empty means no automatic locking.
231 * @var int $locktime
232 */
da3801e8 233 public $locktime = 0;
3c2e81ee 234
235 /**
236 * If set, the whole column will be recalculated, then this flag will be switched off.
237 * @var boolean $needsupdate
238 */
da3801e8 239 public $needsupdate = 1;
3c2e81ee 240
241 /**
242 * Cached dependson array
243 */
da3801e8 244 public $dependson_cache = null;
3c2e81ee 245
246 /**
247 * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
25bcd908 248 * Force regrading if necessary, rounds the float numbers using php function,
249 * the reason is we need to compare the db value with computed number to skip regrading if possible.
3c2e81ee 250 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
251 * @return boolean success
252 */
da3801e8 253 public function update($source=null) {
3c2e81ee 254 // reset caches
255 $this->dependson_cache = null;
256
257 // Retrieve scale and infer grademax/min from it if needed
258 $this->load_scale();
259
260 // make sure there is not 0 in outcomeid
261 if (empty($this->outcomeid)) {
262 $this->outcomeid = null;
263 }
264
265 if ($this->qualifies_for_regrading()) {
266 $this->force_regrading();
267 }
268
ced5ee59 269 $this->timemodified = time();
270
25bcd908 271 $this->grademin = grade_floatval($this->grademin);
272 $this->grademax = grade_floatval($this->grademax);
273 $this->multfactor = grade_floatval($this->multfactor);
274 $this->plusfactor = grade_floatval($this->plusfactor);
275 $this->aggregationcoef = grade_floatval($this->aggregationcoef);
276
3c2e81ee 277 return parent::update($source);
278 }
279
280 /**
281 * Compares the values held by this object with those of the matching record in DB, and returns
282 * whether or not these differences are sufficient to justify an update of all parent objects.
283 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
284 * @return boolean
285 */
da3801e8 286 public function qualifies_for_regrading() {
3c2e81ee 287 if (empty($this->id)) {
288 return false;
289 }
290
f3ac8eb4 291 $db_item = new grade_item(array('id' => $this->id));
292
293 $calculationdiff = $db_item->calculation != $this->calculation;
294 $categorydiff = $db_item->categoryid != $this->categoryid;
295 $gradetypediff = $db_item->gradetype != $this->gradetype;
f3ac8eb4 296 $scaleiddiff = $db_item->scaleid != $this->scaleid;
297 $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
f3ac8eb4 298 $locktimediff = $db_item->locktime != $this->locktime;
25bcd908 299 $grademindiff = grade_floats_different($db_item->grademin, $this->grademin);
300 $grademaxdiff = grade_floats_different($db_item->grademax, $this->grademax);
301 $multfactordiff = grade_floats_different($db_item->multfactor, $this->multfactor);
302 $plusfactordiff = grade_floats_different($db_item->plusfactor, $this->plusfactor);
303 $acoefdiff = grade_floats_different($db_item->aggregationcoef, $this->aggregationcoef);
3c2e81ee 304
305 $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
306 $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
307
308 return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
309 || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
310 || $lockeddiff || $acoefdiff || $locktimediff);
311 }
312
313 /**
314 * Finds and returns a grade_item instance based on params.
315 * @static
316 *
317 * @param array $params associative arrays varname=>value
318 * @return object grade_item instance or false if none found.
319 */
da3801e8 320 public static function fetch($params) {
f3ac8eb4 321 return grade_object::fetch_helper('grade_items', 'grade_item', $params);
3c2e81ee 322 }
323
324 /**
325 * Finds and returns all grade_item instances based on params.
326 * @static
327 *
328 * @param array $params associative arrays varname=>value
329 * @return array array of grade_item insatnces or false if none found.
330 */
da3801e8 331 public static function fetch_all($params) {
f3ac8eb4 332 return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
3c2e81ee 333 }
334
335 /**
336 * Delete all grades and force_regrading of parent category.
337 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
338 * @return boolean success
339 */
da3801e8 340 public function delete($source=null) {
f0362b5d 341 $this->delete_all_grades($source);
342 return parent::delete($source);
343 }
344
345 /**
346 * Delete all grades
347 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
348 * @return boolean success
349 */
da3801e8 350 public function delete_all_grades($source=null) {
3c2e81ee 351 if (!$this->is_course_item()) {
352 $this->force_regrading();
353 }
4ac209d5 354
f3ac8eb4 355 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
3c2e81ee 356 foreach ($grades as $grade) {
357 $grade->delete($source);
358 }
359 }
360
f0362b5d 361 return true;
3c2e81ee 362 }
363
364 /**
365 * In addition to perform parent::insert(), calls force_regrading() method too.
366 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
367 * @return int PK ID if successful, false otherwise
368 */
da3801e8 369 public function insert($source=null) {
370 global $CFG, $DB;
3c2e81ee 371
372 if (empty($this->courseid)) {
2f137aa1 373 print_error('cannotinsertgrade');
3c2e81ee 374 }
375
376 // load scale if needed
377 $this->load_scale();
378
379 // add parent category if needed
380 if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
f3ac8eb4 381 $course_category = grade_category::fetch_course_category($this->courseid);
3c2e81ee 382 $this->categoryid = $course_category->id;
383
384 }
385
386 // always place the new items at the end, move them after insert if needed
9718765e 387 $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid));
3c2e81ee 388 if (!empty($last_sortorder)) {
389 $this->sortorder = $last_sortorder + 1;
390 } else {
391 $this->sortorder = 1;
392 }
393
394 // add proper item numbers to manual items
395 if ($this->itemtype == 'manual') {
396 if (empty($this->itemnumber)) {
397 $this->itemnumber = 0;
398 }
399 }
400
401 // make sure there is not 0 in outcomeid
402 if (empty($this->outcomeid)) {
403 $this->outcomeid = null;
404 }
405
ced5ee59 406 $this->timecreated = $this->timemodified = time();
407
3c2e81ee 408 if (parent::insert($source)) {
409 // force regrading of items if needed
410 $this->force_regrading();
411 return $this->id;
412
413 } else {
414 debugging("Could not insert this grade_item in the database!");
415 return false;
416 }
417 }
418
419 /**
420 * Set idnumber of grade item, updates also course_modules table
421 * @param string $idnumber (without magic quotes)
422 * @return boolean success
423 */
da3801e8 424 public function add_idnumber($idnumber) {
425 global $DB;
3c2e81ee 426 if (!empty($this->idnumber)) {
427 return false;
428 }
429
430 if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
f3ac8eb4 431 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
3c2e81ee 432 return false;
433 }
434 if (!empty($cm->idnumber)) {
435 return false;
436 }
5b0af8c5 437 if ($DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id))) {
3c2e81ee 438 $this->idnumber = $idnumber;
439 return $this->update();
440 }
441 return false;
442
443 } else {
444 $this->idnumber = $idnumber;
445 return $this->update();
446 }
447 }
448
449 /**
450 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
451 * $userid is given) or the locked state of a specific grade within this item if a specific
452 * $userid is given and the grade_item is unlocked.
453 *
454 * @param int $userid
455 * @return boolean Locked state
456 */
da3801e8 457 public function is_locked($userid=NULL) {
3c2e81ee 458 if (!empty($this->locked)) {
459 return true;
460 }
461
462 if (!empty($userid)) {
f3ac8eb4 463 if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
3c2e81ee 464 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
465 return $grade->is_locked();
466 }
467 }
468
469 return false;
470 }
471
472 /**
473 * Locks or unlocks this grade_item and (optionally) all its associated final grades.
474 * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
475 * @param boolean $cascade lock/unlock child objects too
476 * @param boolean $refresh refresh grades when unlocking
477 * @return boolean true if grade_item all grades updated, false if at least one update fails
478 */
da3801e8 479 public function set_locked($lockedstate, $cascade=false, $refresh=true) {
3c2e81ee 480 if ($lockedstate) {
481 /// setting lock
482 if ($this->needsupdate) {
483 return false; // can not lock grade without first having final grade
484 }
485
486 $this->locked = time();
487 $this->update();
488
489 if ($cascade) {
490 $grades = $this->get_final();
491 foreach($grades as $g) {
f3ac8eb4 492 $grade = new grade_grade($g, false);
3c2e81ee 493 $grade->grade_item =& $this;
494 $grade->set_locked(1, null, false);
495 }
496 }
497
498 return true;
499
500 } else {
501 /// removing lock
502 if (!empty($this->locked) and $this->locktime < time()) {
503 //we have to reset locktime or else it would lock up again
504 $this->locktime = 0;
505 }
506
507 $this->locked = 0;
508 $this->update();
509
510 if ($cascade) {
f3ac8eb4 511 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
3c2e81ee 512 foreach($grades as $grade) {
513 $grade->grade_item =& $this;
514 $grade->set_locked(0, null, false);
515 }
516 }
517 }
518
519 if ($refresh) {
520 //refresh when unlocking
521 $this->refresh_grades();
522 }
523
524 return true;
525 }
526 }
527
528 /**
529 * Lock the grade if needed - make sure this is called only when final grades are valid
530 */
da3801e8 531 public function check_locktime() {
3c2e81ee 532 if (!empty($this->locked)) {
533 return; // already locked
534 }
535
536 if ($this->locktime and $this->locktime < time()) {
537 $this->locked = time();
538 $this->update('locktime');
539 }
540 }
541
542 /**
543 * Set the locktime for this grade item.
544 *
545 * @param int $locktime timestamp for lock to activate
546 * @return void
547 */
da3801e8 548 public function set_locktime($locktime) {
3c2e81ee 549 $this->locktime = $locktime;
550 $this->update();
551 }
552
553 /**
554 * Set the locktime for this grade item.
555 *
556 * @return int $locktime timestamp for lock to activate
557 */
da3801e8 558 public function get_locktime() {
3c2e81ee 559 return $this->locktime;
560 }
561
562 /**
563 * Returns the hidden state of this grade_item
564 * @return boolean hidden state
565 */
da3801e8 566 public function is_hidden() {
3c2e81ee 567 return ($this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()));
568 }
569
597f50e6 570 /**
571 * Check grade hidden status. Uses data from both grade item and grade.
572 * @return boolean true if hiddenuntil, false if not
573 */
da3801e8 574 public function is_hiddenuntil() {
597f50e6 575 return $this->hidden > 1;
576 }
577
3c2e81ee 578 /**
579 * Check grade item hidden status.
580 * @return int 0 means visible, 1 hidden always, timestamp hidden until
581 */
da3801e8 582 public function get_hidden() {
3c2e81ee 583 return $this->hidden;
584 }
585
586 /**
587 * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.
588 * @param int $hidden new hidden status
589 * @param boolean $cascade apply to child objects too
590 * @return void
591 */
da3801e8 592 public function set_hidden($hidden, $cascade=false) {
3c2e81ee 593 $this->hidden = $hidden;
594 $this->update();
595
596 if ($cascade) {
f3ac8eb4 597 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
3c2e81ee 598 foreach($grades as $grade) {
599 $grade->grade_item =& $this;
600 $grade->set_hidden($hidden, $cascade);
601 }
602 }
603 }
604 }
605
606 /**
607 * Returns the number of grades that are hidden.
9718765e 608 * @param string $groupsql
609 * @param array $params sql params in $groupsql
610 * @param string $groupsqlwhere
611 * @return int Number of hidden grades
3c2e81ee 612 */
9718765e 613 public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
5b0af8c5 614 global $DB;
9718765e 615 $params = (array)$params;
616 $params['itemid'] = $this->id;
617
5b0af8c5 618 return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
9718765e 619 ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
3c2e81ee 620 }
621
622 /**
623 * Mark regrading as finished successfully.
624 */
da3801e8 625 public function regrading_finished() {
626 global $DB;
3c2e81ee 627 $this->needsupdate = 0;
628 //do not use $this->update() because we do not want this logged in grade_item_history
da3801e8 629 $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id));
3c2e81ee 630 }
631
632 /**
633 * Performs the necessary calculations on the grades_final referenced by this grade_item.
634 * Also resets the needsupdate flag once successfully performed.
635 *
636 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
637 * because the regrading must be done in correct order!!
638 *
639 * @return boolean true if ok, error string otherwise
640 */
da3801e8 641 public function regrade_final_grades($userid=null) {
642 global $CFG, $DB;
3c2e81ee 643
644 // locked grade items already have correct final grades
645 if ($this->is_locked()) {
646 return true;
647 }
648
649 // calculation produces final value using formula from other final values
650 if ($this->is_calculated()) {
651 if ($this->compute($userid)) {
652 return true;
653 } else {
654 return "Could not calculate grades for grade item"; // TODO: improve and localize
655 }
656
657 // noncalculated outcomes already have final values - raw grades not used
658 } else if ($this->is_outcome_item()) {
659 return true;
660
661 // aggregate the category grade
662 } else if ($this->is_category_item() or $this->is_course_item()) {
663 // aggregate category grade item
664 $category = $this->get_item_category();
665 $category->grade_item =& $this;
666 if ($category->generate_grades($userid)) {
667 return true;
668 } else {
669 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
670 }
671
672 } else if ($this->is_manual_item()) {
673 // manual items track only final grades, no raw grades
674 return true;
675
676 } else if (!$this->is_raw_used()) {
677 // hmm - raw grades are not used- nothing to regrade
678 return true;
679 }
680
681 // normal grade item - just new final grades
682 $result = true;
f3ac8eb4 683 $grade_inst = new grade_grade();
3c2e81ee 684 $fields = implode(',', $grade_inst->required_fields);
685 if ($userid) {
5b0af8c5 686 $params = array($this->id, $userid);
687 $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
3c2e81ee 688 } else {
da3801e8 689 $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id), '', $fields);
3c2e81ee 690 }
691 if ($rs) {
da3801e8 692 foreach ($rs as $grade_record) {
f3ac8eb4 693 $grade = new grade_grade($grade_record, false);
3c2e81ee 694
695 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
696 // this grade is locked - final grade must be ok
697 continue;
698 }
699
700 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
701
25bcd908 702 if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade)) {
3c2e81ee 703 if (!$grade->update('system')) {
704 $result = "Internal error updating final grade";
705 }
706 }
707 }
da3801e8 708 $rs->close();
3c2e81ee 709 }
710
711 return $result;
712 }
713
714 /**
715 * Given a float grade value or integer grade scale, applies a number of adjustment based on
716 * grade_item variables and returns the result.
b45d8391 717 * @param float $rawgrade The raw grade value.
718 * @param float $rawmin original rawmin
719 * @param float $rawmax original rawmax
3c2e81ee 720 * @return mixed
721 */
da3801e8 722 public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
3c2e81ee 723 if (is_null($rawgrade)) {
724 return null;
725 }
726
727 if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
728
729 if ($this->grademax < $this->grademin) {
730 return null;
731 }
732
733 if ($this->grademax == $this->grademin) {
734 return $this->grademax; // no range
735 }
736
737 // Standardise score to the new grade range
738 // NOTE: this is not compatible with current assignment grading
b45d8391 739 if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
9a68cffc 740 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
3c2e81ee 741 }
742
743 // Apply other grade_item factors
744 $rawgrade *= $this->multfactor;
745 $rawgrade += $this->plusfactor;
746
747 return bounded_number($this->grademin, $rawgrade, $this->grademax);
748
749 } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
750 if (empty($this->scale)) {
751 $this->load_scale();
752 }
753
754 if ($this->grademax < 0) {
755 return null; // scale not present - no grade
756 }
757
758 if ($this->grademax == 0) {
759 return $this->grademax; // only one option
760 }
761
762 // Convert scale if needed
763 // NOTE: this is not compatible with current assignment grading
b45d8391 764 if ($this->itemmodule != 'assignment' and ($rawmin != $this->grademin or $rawmax != $this->grademax)) {
9a68cffc 765 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
3c2e81ee 766 }
767
768 return (int)bounded_number(0, round($rawgrade+0.00001), $this->grademax);
769
770
771 } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
772 // somebody changed the grading type when grades already existed
773 return null;
774
775 } else {
a7bea6c8 776 debugging("Unknown grade type");
f3ac8eb4 777 return null;
3c2e81ee 778 }
779 }
780
781 /**
782 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
783 * @return void
784 */
da3801e8 785 public function force_regrading() {
f33e1ed4 786 global $DB;
3c2e81ee 787 $this->needsupdate = 1;
788 //mark this item and course item only - categories and calculated items are always regraded
f33e1ed4 789 $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
790 $params = array($this->id, $this->courseid);
791 $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
3c2e81ee 792 }
793
794 /**
795 * Instantiates a grade_scale object whose data is retrieved from the DB,
796 * if this item's scaleid variable is set.
797 * @return object grade_scale or null if no scale used
798 */
da3801e8 799 public function load_scale() {
3c2e81ee 800 if ($this->gradetype != GRADE_TYPE_SCALE) {
801 $this->scaleid = null;
802 }
803
804 if (!empty($this->scaleid)) {
805 //do not load scale if already present
806 if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
f3ac8eb4 807 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
92b0d47c 808 if (!$this->scale) {
809 debugging('Incorrect scale id: '.$this->scaleid);
810 $this->scale = null;
811 return null;
812 }
3c2e81ee 813 $this->scale->load_items();
814 }
815
816 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
817 // stay with the current min=1 max=count(scaleitems)
818 $this->grademax = count($this->scale->scale_items);
819 $this->grademin = 1;
820
821 } else {
822 $this->scale = null;
823 }
824
825 return $this->scale;
826 }
827
828 /**
829 * Instantiates a grade_outcome object whose data is retrieved from the DB,
830 * if this item's outcomeid variable is set.
831 * @return object grade_outcome
832 */
da3801e8 833 public function load_outcome() {
3c2e81ee 834 if (!empty($this->outcomeid)) {
f3ac8eb4 835 $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
3c2e81ee 836 }
837 return $this->outcome;
838 }
839
840 /**
841 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
842 * or category attached to category item.
843 *
844 * @return mixed grade_category object if applicable, false if course item
845 */
da3801e8 846 public function get_parent_category() {
3c2e81ee 847 if ($this->is_category_item() or $this->is_course_item()) {
848 return $this->get_item_category();
849
850 } else {
f3ac8eb4 851 return grade_category::fetch(array('id'=>$this->categoryid));
3c2e81ee 852 }
853 }
854
855 /**
856 * Calls upon the get_parent_category method to retrieve the grade_category object
857 * from the DB and assigns it to $this->parent_category. It also returns the object.
858 * @return object Grade_category
859 */
da3801e8 860 public function load_parent_category() {
3c2e81ee 861 if (empty($this->parent_category->id)) {
862 $this->parent_category = $this->get_parent_category();
863 }
864 return $this->parent_category;
865 }
866
867 /**
868 * Returns the grade_category for category item
869 *
870 * @return mixed grade_category object if applicable, false otherwise
871 */
da3801e8 872 public function get_item_category() {
3c2e81ee 873 if (!$this->is_course_item() and !$this->is_category_item()) {
874 return false;
875 }
f3ac8eb4 876 return grade_category::fetch(array('id'=>$this->iteminstance));
3c2e81ee 877 }
878
879 /**
880 * Calls upon the get_item_category method to retrieve the grade_category object
881 * from the DB and assigns it to $this->item_category. It also returns the object.
882 * @return object Grade_category
883 */
da3801e8 884 public function load_item_category() {
3c2e81ee 885 if (empty($this->category->id)) {
886 $this->item_category = $this->get_item_category();
887 }
888 return $this->item_category;
889 }
890
891 /**
892 * Is the grade item associated with category?
893 * @return boolean
894 */
da3801e8 895 public function is_category_item() {
3c2e81ee 896 return ($this->itemtype == 'category');
897 }
898
899 /**
900 * Is the grade item associated with course?
901 * @return boolean
902 */
da3801e8 903 public function is_course_item() {
3c2e81ee 904 return ($this->itemtype == 'course');
905 }
906
907 /**
908 * Is this a manualy graded item?
909 * @return boolean
910 */
da3801e8 911 public function is_manual_item() {
3c2e81ee 912 return ($this->itemtype == 'manual');
913 }
914
915 /**
916 * Is this an outcome item?
917 * @return boolean
918 */
da3801e8 919 public function is_outcome_item() {
3c2e81ee 920 return !empty($this->outcomeid);
921 }
922
923 /**
0f392ff4 924 * Is the grade item external - associated with module, plugin or something else?
3c2e81ee 925 * @return boolean
926 */
da3801e8 927 public function is_external_item() {
0f392ff4 928 return ($this->itemtype == 'mod');
929 }
930
931 /**
932 * Is the grade item overridable
933 * @return boolean
934 */
da3801e8 935 public function is_overridable_item() {
0f392ff4 936 return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $this->is_course_item() or $this->is_category_item());
3c2e81ee 937 }
938
5048575d 939 /**
940 * Is the grade item feedback overridable
941 * @return boolean
942 */
da3801e8 943 public function is_overridable_item_feedback() {
5048575d 944 return !$this->is_outcome_item() and $this->is_external_item();
945 }
946
3c2e81ee 947 /**
948 * Returns true if grade items uses raw grades
949 * @return boolean
950 */
da3801e8 951 public function is_raw_used() {
0f392ff4 952 return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
3c2e81ee 953 }
954
955 /**
956 * Returns grade item associated with the course
957 * @param int $courseid
958 * @return course item object
959 */
da3801e8 960 public function fetch_course_item($courseid) {
f3ac8eb4 961 if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
3c2e81ee 962 return $course_item;
963 }
964
965 // first get category - it creates the associated grade item
f3ac8eb4 966 $course_category = grade_category::fetch_course_category($courseid);
076aeb01 967 return $course_category->get_grade_item();
3c2e81ee 968 }
969
970 /**
971 * Is grading object editable?
972 * @return boolean
973 */
da3801e8 974 public function is_editable() {
3c2e81ee 975 return true;
976 }
977
978 /**
979 * Checks if grade calculated. Returns this object's calculation.
980 * @return boolean true if grade item calculated.
981 */
da3801e8 982 public function is_calculated() {
3c2e81ee 983 if (empty($this->calculation)) {
984 return false;
985 }
986
987 /*
988 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
989 * we would have to fetch all course grade items to find out the ids.
990 * Also if user changes the idnumber the formula does not need to be updated.
991 */
992
993 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
477eec40 994 if (!$this->calculation_normalized and strpos($this->calculation, '[[') !== false) {
3c2e81ee 995 $this->set_calculation($this->calculation);
996 }
997
998 return !empty($this->calculation);
999 }
1000
1001 /**
1002 * Returns calculation string if grade calculated.
1003 * @return mixed string if calculation used, null if not
1004 */
da3801e8 1005 public function get_calculation() {
3c2e81ee 1006 if ($this->is_calculated()) {
f3ac8eb4 1007 return grade_item::denormalize_formula($this->calculation, $this->courseid);
3c2e81ee 1008
1009 } else {
1010 return NULL;
1011 }
1012 }
1013
1014 /**
1015 * Sets this item's calculation (creates it) if not yet set, or
1016 * updates it if already set (in the DB). If no calculation is given,
1017 * the calculation is removed.
1018 * @param string $formula string representation of formula used for calculation
1019 * @return boolean success
1020 */
da3801e8 1021 public function set_calculation($formula) {
f3ac8eb4 1022 $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
3c2e81ee 1023 $this->calculation_normalized = true;
1024 return $this->update();
1025 }
1026
1027 /**
1028 * Denormalizes the calculation formula to [idnumber] form
1029 * @static
1030 * @param string $formula
1031 * @return string denormalized string
1032 */
da3801e8 1033 public static function denormalize_formula($formula, $courseid) {
3c2e81ee 1034 if (empty($formula)) {
1035 return '';
1036 }
1037
1038 // denormalize formula - convert ##giXX## to [[idnumber]]
1039 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1040 foreach ($matches[1] as $id) {
f3ac8eb4 1041 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
3c2e81ee 1042 if (!empty($grade_item->idnumber)) {
1043 $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
1044 }
1045 }
1046 }
1047 }
1048
1049 return $formula;
1050
1051 }
1052
1053 /**
1054 * Normalizes the calculation formula to [#giXX#] form
1055 * @static
1056 * @param string $formula
1057 * @return string normalized string
1058 */
da3801e8 1059 public static function normalize_formula($formula, $courseid) {
3c2e81ee 1060 $formula = trim($formula);
1061
1062 if (empty($formula)) {
1063 return NULL;
1064
1065 }
1066
1067 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
f3ac8eb4 1068 if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
3c2e81ee 1069 foreach ($grade_items as $grade_item) {
1070 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
1071 }
1072 }
1073
1074 return $formula;
1075 }
1076
1077 /**
1078 * Returns the final values for this grade item (as imported by module or other source).
1079 * @param int $userid Optional: to retrieve a single final grade
1080 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
1081 */
da3801e8 1082 public function get_final($userid=NULL) {
1083 global $DB;
3c2e81ee 1084 if ($userid) {
da3801e8 1085 if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id, 'userid' => $userid))) {
3c2e81ee 1086 return $user;
1087 }
1088
1089 } else {
da3801e8 1090 if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id))) {
3c2e81ee 1091 //TODO: speed up with better SQL
1092 $result = array();
1093 foreach ($grades as $grade) {
1094 $result[$grade->userid] = $grade;
1095 }
1096 return $result;
1097 } else {
1098 return array();
1099 }
1100 }
1101 }
1102
1103 /**
1104 * Get (or create if not exist yet) grade for this user
1105 * @param int $userid
1106 * @return object grade_grade object instance
1107 */
da3801e8 1108 public function get_grade($userid, $create=true) {
3c2e81ee 1109 if (empty($this->id)) {
1110 debugging('Can not use before insert');
1111 return false;
1112 }
1113
f3ac8eb4 1114 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
3c2e81ee 1115 if (empty($grade->id) and $create) {
1116 $grade->insert();
1117 }
1118
1119 return $grade;
1120 }
1121
1122 /**
1123 * Returns the sortorder of this grade_item. This method is also available in
1124 * grade_category, for cases where the object type is not know.
1125 * @return int Sort order
1126 */
da3801e8 1127 public function get_sortorder() {
3c2e81ee 1128 return $this->sortorder;
1129 }
1130
1131 /**
1132 * Returns the idnumber of this grade_item. This method is also available in
1133 * grade_category, for cases where the object type is not know.
1134 * @return string idnumber
1135 */
da3801e8 1136 public function get_idnumber() {
3c2e81ee 1137 return $this->idnumber;
1138 }
1139
1140 /**
1141 * Returns this grade_item. This method is also available in
1142 * grade_category, for cases where the object type is not know.
1143 * @return string idnumber
1144 */
da3801e8 1145 public function get_grade_item() {
3c2e81ee 1146 return $this;
1147 }
1148
1149 /**
1150 * Sets the sortorder of this grade_item. This method is also available in
1151 * grade_category, for cases where the object type is not know.
1152 * @param int $sortorder
1153 * @return void
1154 */
da3801e8 1155 public function set_sortorder($sortorder) {
25bcd908 1156 if ($this->sortorder == $sortorder) {
1157 return;
9eeb49b2 1158 }
3c2e81ee 1159 $this->sortorder = $sortorder;
1160 $this->update();
1161 }
1162
da3801e8 1163 public function move_after_sortorder($sortorder) {
5b0af8c5 1164 global $CFG, $DB;
3c2e81ee 1165
1166 //make some room first
5b0af8c5 1167 $params = array($sortorder, $this->courseid);
1168 $sql = "UPDATE {grade_items}
3c2e81ee 1169 SET sortorder = sortorder + 1
5b0af8c5 1170 WHERE sortorder > ? AND courseid = ?";
1171 $DB->execute($sql, $params);
3c2e81ee 1172
1173 $this->set_sortorder($sortorder + 1);
1174 }
1175
1176 /**
1177 * Returns the most descriptive field for this object. This is a standard method used
1178 * when we do not know the exact type of an object.
1179 * @return string name
1180 */
da3801e8 1181 public function get_name() {
3c2e81ee 1182 if (!empty($this->itemname)) {
1183 // MDL-10557
1184 return format_string($this->itemname);
1185
1186 } else if ($this->is_course_item()) {
1187 return get_string('coursetotal', 'grades');
1188
1189 } else if ($this->is_category_item()) {
1190 return get_string('categorytotal', 'grades');
1191
1192 } else {
1193 return get_string('grade');
1194 }
1195 }
1196
1197 /**
1198 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1199 * @param int $parentid
1200 * @return boolean success;
1201 */
da3801e8 1202 public function set_parent($parentid) {
3c2e81ee 1203 if ($this->is_course_item() or $this->is_category_item()) {
2f137aa1 1204 print_error('cannotsetparentforcatoritem');
3c2e81ee 1205 }
1206
1207 if ($this->categoryid == $parentid) {
1208 return true;
1209 }
1210
1211 // find parent and check course id
f3ac8eb4 1212 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
3c2e81ee 1213 return false;
1214 }
1215
1216 $this->force_regrading();
1217
1218 // set new parent
1219 $this->categoryid = $parent_category->id;
1220 $this->parent_category =& $parent_category;
1221
1222 return $this->update();
1223 }
1224
1225 /**
1226 * Finds out on which other items does this depend directly when doing calculation or category agregation
1227 * @param bool $reset_cache
1228 * @return array of grade_item ids this one depends on
1229 */
da3801e8 1230 public function depends_on($reset_cache=false) {
1231 global $CFG, $DB;
3c2e81ee 1232
1233 if ($reset_cache) {
1234 $this->dependson_cache = null;
1235 } else if (isset($this->dependson_cache)) {
1236 return $this->dependson_cache;
1237 }
1238
1239 if ($this->is_locked()) {
1240 // locked items do not need to be regraded
1241 $this->dependson_cache = array();
1242 return $this->dependson_cache;
1243 }
1244
1245 if ($this->is_calculated()) {
1246 if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1247 $this->dependson_cache = array_unique($matches[1]); // remove duplicates
1248 return $this->dependson_cache;
1249 } else {
1250 $this->dependson_cache = array();
1251 return $this->dependson_cache;
1252 }
1253
1254 } else if ($grade_category = $this->load_item_category()) {
5b0af8c5 1255 $params = array();
1256
3c2e81ee 1257 //only items with numeric or scale values can be aggregated
1258 if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1259 $this->dependson_cache = array();
1260 return $this->dependson_cache;
1261 }
1262
a6771652 1263 $grade_category->apply_forced_settings();
3c2e81ee 1264
1265 if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1266 $outcomes_sql = "";
1267 } else {
1268 $outcomes_sql = "AND gi.outcomeid IS NULL";
1269 }
1270
91f9a62c 1271 if (empty($CFG->grade_includescalesinaggregation)) {
5b0af8c5 1272 $gtypes = "gi.gradetype = ?";
1273 $params[] = GRADE_TYPE_VALUE;
91f9a62c 1274 } else {
5b0af8c5 1275 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1276 $params[] = GRADE_TYPE_VALUE;
1277 $params[] = GRADE_TYPE_SCALE;
91f9a62c 1278 }
1279
3c2e81ee 1280 if ($grade_category->aggregatesubcats) {
1281 // return all children excluding category items
5b0af8c5 1282 $params[] = $grade_category->id;
3c2e81ee 1283 $sql = "SELECT gi.id
5b0af8c5 1284 FROM {grade_items} gi
9eeb49b2 1285 WHERE $gtypes
3c2e81ee 1286 $outcomes_sql
1287 AND gi.categoryid IN (
1288 SELECT gc.id
5b0af8c5 1289 FROM {grade_categories} gc
1290 WHERE gc.path LIKE '%?%')";
3c2e81ee 1291
1292 } else {
5b0af8c5 1293 $params[] = $grade_category->id;
1294 $params[] = $grade_category->id;
1295 $params[] = GRADE_TYPE_VALUE;
1296 $params[] = GRADE_TYPE_SCALE;
3c2e81ee 1297 $sql = "SELECT gi.id
5b0af8c5 1298 FROM {grade_items} gi
1299 WHERE $gtypes
1300 AND gi.categoryid = ?
3c2e81ee 1301 $outcomes_sql
3c2e81ee 1302 UNION
1303
1304 SELECT gi.id
5b0af8c5 1305 FROM {grade_items} gi, {grade_categories} gc
3c2e81ee 1306 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
5b0af8c5 1307 AND gc.parent = ?
91f9a62c 1308 AND $gtypes
3c2e81ee 1309 $outcomes_sql";
1310 }
1311
5b0af8c5 1312 if ($children = $DB->get_records_sql($sql, $params)) {
3c2e81ee 1313 $this->dependson_cache = array_keys($children);
1314 return $this->dependson_cache;
1315 } else {
1316 $this->dependson_cache = array();
1317 return $this->dependson_cache;
1318 }
1319
1320 } else {
1321 $this->dependson_cache = array();
1322 return $this->dependson_cache;
1323 }
1324 }
1325
1326 /**
1994d890 1327 * Refetch grades from modules, plugins.
3c2e81ee 1328 * @param int $userid optional, one user only
1329 */
da3801e8 1330 public function refresh_grades($userid=0) {
1331 global $DB;
3c2e81ee 1332 if ($this->itemtype == 'mod') {
1333 if ($this->is_outcome_item()) {
1334 //nothing to do
1335 return;
1336 }
1337
da3801e8 1338 if (!$activity = $DB->get_record($this->itemmodule, array('id' => $this->iteminstance))) {
1994d890 1339 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
3c2e81ee 1340 return;
1341 }
1342
f3ac8eb4 1343 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1994d890 1344 debugging('Can not find course module');
3c2e81ee 1345 return;
1346 }
1347
1348 $activity->modname = $this->itemmodule;
1349 $activity->cmidnumber = $cm->idnumber;
1350
1351 grade_update_mod_grades($activity);
1352 }
1353 }
1354
1355 /**
1356 * Updates final grade value for given user, this is a only way to update final
1357 * grades from gradebook and import because it logs the change in history table
1358 * and deals with overridden flag. This flag is set to prevent later overriding
1359 * from raw grades submitted from modules.
1360 *
1361 * @param int $userid the graded user
1362 * @param mixed $finalgrade float value of final grade - false means do not change
1363 * @param string $howmodified modification source
1364 * @param string $note optional note
1365 * @param mixed $feedback teachers feedback as string - false means do not change
1366 * @param int $feedbackformat
1367 * @return boolean success
1368 */
da3801e8 1369 public function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
3c2e81ee 1370 global $USER, $CFG;
1371
3c2e81ee 1372 $result = true;
1373
1374 // no grading used or locked
1375 if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1376 return false;
1377 }
1378
f3ac8eb4 1379 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
3c2e81ee 1380 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1381
ced5ee59 1382 if (empty($usermodified)) {
1383 $grade->usermodified = $USER->id;
1384 } else {
1385 $grade->usermodified = $usermodified;
1386 }
3c2e81ee 1387
1388 if ($grade->is_locked()) {
1389 // do not update locked grades at all
1390 return false;
1391 }
1392
1393 $locktime = $grade->get_locktime();
1394 if ($locktime and $locktime < time()) {
1395 // do not update grades that should be already locked, force regrade instead
1396 $this->force_regrading();
1397 return false;
1398 }
1399
1400 $oldgrade = new object();
1401 $oldgrade->finalgrade = $grade->finalgrade;
1402 $oldgrade->overridden = $grade->overridden;
1403 $oldgrade->feedback = $grade->feedback;
1404 $oldgrade->feedbackformat = $grade->feedbackformat;
1405
0f392ff4 1406 // changed grade?
1407 if ($finalgrade !== false) {
1408 if ($this->is_overridable_item()) {
3c2e81ee 1409 $grade->overridden = time();
0f392ff4 1410 } else {
1411 $grade->overridden = 0;
3c2e81ee 1412 }
3c2e81ee 1413
717f432f 1414 if (is_null($finalgrade)) {
1415 $grade->finalgrade = null;
3c2e81ee 1416 } else {
66690b69 1417 $grade->finalgrade = bounded_number($this->grademin, $finalgrade, $this->grademax);
3c2e81ee 1418 }
3c2e81ee 1419 }
1420
1421 // do we have comment from teacher?
1422 if ($feedback !== false) {
5048575d 1423 if ($this->is_overridable_item_feedback()) {
0f392ff4 1424 // external items (modules, plugins) may have own feedback
1425 $grade->overridden = time();
1426 }
1427
3c2e81ee 1428 $grade->feedback = $feedback;
1429 $grade->feedbackformat = $feedbackformat;
1430 }
1431
1432 if (empty($grade->id)) {
9eeb49b2 1433 $grade->timecreated = null; // hack alert - date submitted - no submission yet
1434 $grade->timemodified = time(); // hack alert - date graded
3c2e81ee 1435 $result = (boolean)$grade->insert($source);
1436
25bcd908 1437 } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1438 or $grade->feedback !== $oldgrade->feedback
5048575d 1439 or $grade->feedbackformat != $oldgrade->feedbackformat
1440 or $grade->overridden != $oldgrade->overridden) {
9eeb49b2 1441 $grade->timemodified = time(); // hack alert - date graded
3c2e81ee 1442 $result = $grade->update($source);
0f392ff4 1443 } else {
1444 // no grade change
1445 return $result;
3c2e81ee 1446 }
1447
1448 if (!$result) {
1449 // something went wrong - better force final grade recalculation
1450 $this->force_regrading();
1451
1452 } else if ($this->is_course_item() and !$this->needsupdate) {
b45d8391 1453 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
3c2e81ee 1454 $this->force_regrading();
1455 }
1456
1457 } else if (!$this->needsupdate) {
f3ac8eb4 1458 $course_item = grade_item::fetch_course_item($this->courseid);
3c2e81ee 1459 if (!$course_item->needsupdate) {
b45d8391 1460 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
3c2e81ee 1461 $this->force_regrading();
1462 }
1463 } else {
1464 $this->force_regrading();
1465 }
1466 }
1467
1468 return $result;
1469 }
1470
1471
1472 /**
1473 * Updates raw grade value for given user, this is a only way to update raw
1474 * grades from external source (modules, etc.),
1475 * because it logs the change in history table and deals with final grade recalculation.
1476 *
1477 * @param int $userid the graded user
1478 * @param mixed $rawgrade float value of raw grade - false means do not change
1479 * @param string $howmodified modification source
1480 * @param string $note optional note
1481 * @param mixed $feedback teachers feedback as string - false means do not change
1482 * @param int $feedbackformat
ced5ee59 1483 * @param int $usermodified - user which did the grading
1484 * @param int $dategraded
1485 * @param int $datesubmitted
55231be0 1486 * @param object $grade object - usefull for bulk upgrades
3c2e81ee 1487 * @return boolean success
1488 */
da3801e8 1489 public function update_raw_grade($userid, $rawgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null, $dategraded=null, $datesubmitted=null, $grade=null) {
3c2e81ee 1490 global $USER;
1491
3c2e81ee 1492 $result = true;
1493
1494 // calculated grades can not be updated; course and category can not be updated because they are aggregated
0f392ff4 1495 if (!$this->is_raw_used() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
3c2e81ee 1496 return false;
1497 }
1498
55231be0 1499 if (is_null($grade)) {
1500 //fetch from db
1501 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1502 }
3c2e81ee 1503 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1504
ced5ee59 1505 if (empty($usermodified)) {
1506 $grade->usermodified = $USER->id;
1507 } else {
1508 $grade->usermodified = $usermodified;
1509 }
1510
3c2e81ee 1511 if ($grade->is_locked()) {
1512 // do not update locked grades at all
1513 return false;
1514 }
1515
1516 $locktime = $grade->get_locktime();
1517 if ($locktime and $locktime < time()) {
1518 // do not update grades that should be already locked and force regrade
1519 $this->force_regrading();
1520 return false;
1521 }
1522
1523 $oldgrade = new object();
66690b69 1524 $oldgrade->finalgrade = $grade->finalgrade;
1525 $oldgrade->rawgrade = $grade->rawgrade;
1526 $oldgrade->rawgrademin = $grade->rawgrademin;
1527 $oldgrade->rawgrademax = $grade->rawgrademax;
1528 $oldgrade->rawscaleid = $grade->rawscaleid;
3c2e81ee 1529 $oldgrade->feedback = $grade->feedback;
1530 $oldgrade->feedbackformat = $grade->feedbackformat;
1531
b45d8391 1532 // use new min and max
66690b69 1533 $grade->rawgrade = $grade->rawgrade;
1534 $grade->rawgrademin = $this->grademin;
1535 $grade->rawgrademax = $this->grademax;
1536 $grade->rawscaleid = $this->scaleid;
3c2e81ee 1537
1538 // change raw grade?
1539 if ($rawgrade !== false) {
66690b69 1540 $grade->rawgrade = $rawgrade;
3c2e81ee 1541 }
1542
9eeb49b2 1543 // empty feedback means no feedback at all
1544 if ($feedback === '') {
1545 $feedback = null;
1546 }
1547
3c2e81ee 1548 // do we have comment from teacher?
25bcd908 1549 if ($feedback !== false and !$grade->is_overridden()) {
3c2e81ee 1550 $grade->feedback = $feedback;
1551 $grade->feedbackformat = $feedbackformat;
1552 }
1553
b45d8391 1554 // update final grade if possible
1555 if (!$grade->is_locked() and !$grade->is_overridden()) {
66690b69 1556 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
b45d8391 1557 }
1558
9eeb49b2 1559 // TODO: hack alert - create new fields for these in 2.0
1560 $oldgrade->timecreated = $grade->timecreated;
1561 $oldgrade->timemodified = $grade->timemodified;
1562
1563 $grade->timecreated = $datesubmitted;
1564
1565 if ($grade->is_overridden()) {
1566 // keep original graded date - update_final_grade() sets this for overridden grades
1567
1568 } else if (is_null($grade->rawgrade) and is_null($grade->feedback)) {
1569 // no grade and feedback means no grading yet
1570 $grade->timemodified = null;
1571
1572 } else if (!empty($dategraded)) {
1573 // fine - module sends info when graded (yay!)
1574 $grade->timemodified = $dategraded;
1575
1576 } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1577 or $grade->feedback !== $oldgrade->feedback) {
1578 // guess - if either grade or feedback changed set new graded date
1579 $grade->timemodified = time();
1580
1581 } else {
1582 //keep original graded date
717f432f 1583 }
9eeb49b2 1584 // end of hack alert
717f432f 1585
3c2e81ee 1586 if (empty($grade->id)) {
1587 $result = (boolean)$grade->insert($source);
1588
25bcd908 1589 } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
1590 or grade_floats_different($grade->rawgrade, $oldgrade->rawgrade)
1591 or grade_floats_different($grade->rawgrademin, $oldgrade->rawgrademin)
1592 or grade_floats_different($grade->rawgrademax, $oldgrade->rawgrademax)
1593 or $grade->rawscaleid != $oldgrade->rawscaleid
1594 or $grade->feedback !== $oldgrade->feedback
9eeb49b2 1595 or $grade->feedbackformat != $oldgrade->feedbackformat
1596 or $grade->timecreated != $oldgrade->timecreated // part of hack above
1597 or $grade->timemodified != $oldgrade->timemodified // part of hack above
1598 ) {
3c2e81ee 1599 $result = $grade->update($source);
66690b69 1600 } else {
1601 return $result;
3c2e81ee 1602 }
1603
1604 if (!$result) {
1605 // something went wrong - better force final grade recalculation
1606 $this->force_regrading();
1607
1608 } else if (!$this->needsupdate) {
f3ac8eb4 1609 $course_item = grade_item::fetch_course_item($this->courseid);
3c2e81ee 1610 if (!$course_item->needsupdate) {
b45d8391 1611 if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
3c2e81ee 1612 $this->force_regrading();
1613 }
3c2e81ee 1614 }
1615 }
1616
1617 return $result;
1618 }
1619
1620 /**
1621 * Calculates final grade values using the formula in calculation property.
1622 * The parameters are taken from final grades of grade items in current course only.
1623 * @return boolean false if error
1624 */
da3801e8 1625 public function compute($userid=null) {
1626 global $CFG, $DB;
3c2e81ee 1627
1628 if (!$this->is_calculated()) {
1629 return false;
1630 }
1631
1632 require_once($CFG->libdir.'/mathslib.php');
1633
1634 if ($this->is_locked()) {
1635 return true; // no need to recalculate locked items
1636 }
1637
52e95845 1638 // precreate grades - we need them to exist
f53db007 1639 $params = array($this->courseid, $this->id, $this->id);
52e95845 1640 $sql = "SELECT DISTINCT go.userid
5b0af8c5 1641 FROM {grade_grades} go
1642 JOIN {grade_items} gi
f53db007 1643 ON (gi.id = go.itemid AND gi.courseid = ?)
5b0af8c5 1644 LEFT OUTER JOIN {grade_grades} g
1645 ON (g.userid = go.userid AND g.itemid = ?)
f53db007 1646 WHERE gi.id <> ? AND g.id IS NULL";
5b0af8c5 1647 if ($missing = $DB->get_records_sql($sql, $params)) {
52e95845 1648 foreach ($missing as $m) {
1649 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$m->userid), false);
1650 $grade->grade_item =& $this;
1651 $grade->insert('system');
1652 }
1653 }
1654
3c2e81ee 1655 // get used items
1656 $useditems = $this->depends_on();
1657
1658 // prepare formula and init maths library
1659 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
9c9a3259 1660 if (strpos($formula, '[[') !== false) {
1661 // missing item
1662 return false;
1663 }
3c2e81ee 1664 $this->formula = new calc_formula($formula);
1665
1666 // where to look for final grades?
1667 // this itemid is added so that we use only one query for source and final grades
5b0af8c5 1668 $gis = array_merge($useditems, array($this->id));
1669 list($usql, $params) = $DB->get_in_or_equal($gis);
3c2e81ee 1670
1671 if ($userid) {
5b0af8c5 1672 $usersql = "AND g.userid=?";
1673 $params[] = $userid;
3c2e81ee 1674 } else {
1675 $usersql = "";
1676 }
1677
f3ac8eb4 1678 $grade_inst = new grade_grade();
3c2e81ee 1679 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1680
5b0af8c5 1681 $params[] = $this->courseid;
3c2e81ee 1682 $sql = "SELECT $fields
5b0af8c5 1683 FROM {grade_grades} g, {grade_items} gi
1684 WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
1685 ORDER BY g.userid";
3c2e81ee 1686
1687 $return = true;
1688
1689 // group the grades by userid and use formula on the group
5b0af8c5 1690 if ($rs = $DB->get_recordset_sql($sql, $params)) {
3c2e81ee 1691 $prevuser = 0;
1692 $grade_records = array();
1693 $oldgrade = null;
da3801e8 1694 foreach ($rs as $used) {
3c2e81ee 1695 if ($used->userid != $prevuser) {
1696 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1697 $return = false;
1698 }
1699 $prevuser = $used->userid;
1700 $grade_records = array();
1701 $oldgrade = null;
1702 }
1703 if ($used->itemid == $this->id) {
1704 $oldgrade = $used;
1705 }
1706 $grade_records['gi'.$used->itemid] = $used->finalgrade;
1707 }
da3801e8 1708 $rs->close();
3c2e81ee 1709 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1710 $return = false;
1711 }
1712 }
3c2e81ee 1713
1714 return $return;
1715 }
1716
1717 /**
1718 * internal function - does the final grade calculation
1719 */
da3801e8 1720 public function use_formula($userid, $params, $useditems, $oldgrade) {
3c2e81ee 1721 if (empty($userid)) {
1722 return true;
1723 }
1724
1725 // add missing final grade values
1726 // not graded (null) is counted as 0 - the spreadsheet way
1727 foreach($useditems as $gi) {
1728 if (!array_key_exists('gi'.$gi, $params)) {
1729 $params['gi'.$gi] = 0;
1730 } else {
1731 $params['gi'.$gi] = (float)$params['gi'.$gi];
1732 }
1733 }
1734
1735 // can not use own final grade during calculation
1736 unset($params['gi'.$this->id]);
1737
1738 // insert final grade - will be needed later anyway
1739 if ($oldgrade) {
66690b69 1740 $oldfinalgrade = $oldgrade->finalgrade;
f3ac8eb4 1741 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
3c2e81ee 1742 $grade->grade_item =& $this;
1743
1744 } else {
f3ac8eb4 1745 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
3c2e81ee 1746 $grade->grade_item =& $this;
4ac209d5 1747 $grade->insert('system');
1748 $oldfinalgrade = null;
3c2e81ee 1749 }
1750
1751 // no need to recalculate locked or overridden grades
1752 if ($grade->is_locked() or $grade->is_overridden()) {
1753 return true;
1754 }
1755
1756 // do the calculation
1757 $this->formula->set_params($params);
1758 $result = $this->formula->evaluate();
1759
3c2e81ee 1760 if ($result === false) {
1761 $grade->finalgrade = null;
1762
1763 } else {
1764 // normalize
1765 $result = bounded_number($this->grademin, $result, $this->grademax);
1766 if ($this->gradetype == GRADE_TYPE_SCALE) {
1767 $result = round($result+0.00001); // round scales upwards
1768 }
66690b69 1769 $grade->finalgrade = $result;
3c2e81ee 1770 }
1771
1772 // update in db if changed
25bcd908 1773 if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
4ac209d5 1774 $grade->update('compute');
3c2e81ee 1775 }
1776
1777 if ($result !== false) {
1778 //lock grade if needed
1779 }
1780
1781 if ($result === false) {
1782 return false;
1783 } else {
1784 return true;
1785 }
1786
1787 }
1788
1789 /**
1790 * Validate the formula.
1791 * @param string $formula
1792 * @return boolean true if calculation possible, false otherwise
1793 */
da3801e8 1794 public function validate_formula($formulastr) {
1795 global $CFG, $DB;
3c2e81ee 1796 require_once($CFG->libdir.'/mathslib.php');
1797
f3ac8eb4 1798 $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
3c2e81ee 1799
1800 if (empty($formulastr)) {
1801 return true;
1802 }
1803
1804 if (strpos($formulastr, '=') !== 0) {
1805 return get_string('errorcalculationnoequal', 'grades');
1806 }
1807
1808 // get used items
1809 if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
1810 $useditems = array_unique($matches[1]); // remove duplicates
1811 } else {
1812 $useditems = array();
1813 }
ced5ee59 1814
26d7de8b 1815 // MDL-11902
1816 // unset the value if formula is trying to reference to itself
1817 // but array keys does not match itemid
3c2e81ee 1818 if (!empty($this->id)) {
26d7de8b 1819 $useditems = array_diff($useditems, array($this->id));
1820 //unset($useditems[$this->id]);
3c2e81ee 1821 }
1822
1823 // prepare formula and init maths library
1824 $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
1825 $formula = new calc_formula($formula);
1826
1827
1828 if (empty($useditems)) {
1829 $grade_items = array();
1830
1831 } else {
5b0af8c5 1832 list($usql, $params) = $DB->get_in_or_equal($useditems);
1833 $params[] = $this->courseid;
3c2e81ee 1834 $sql = "SELECT gi.*
5b0af8c5 1835 FROM {grade_items} gi
1836 WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
3c2e81ee 1837
5b0af8c5 1838 if (!$grade_items = $DB->get_records_sql($sql, $params)) {
3c2e81ee 1839 $grade_items = array();
1840 }
1841 }
1842
1843 $params = array();
1844 foreach ($useditems as $itemid) {
1845 // make sure all grade items exist in this course
1846 if (!array_key_exists($itemid, $grade_items)) {
1847 return false;
1848 }
1849 // use max grade when testing formula, this should be ok in 99.9%
1850 // division by 0 is one of possible problems
1851 $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
1852 }
1853
1854 // do the calculation
1855 $formula->set_params($params);
1856 $result = $formula->evaluate();
1857
1858 // false as result indicates some problem
1859 if ($result === false) {
1860 // TODO: add more error hints
1861 return get_string('errorcalculationunknown', 'grades');
1862 } else {
1863 return true;
1864 }
1865 }
1866
1867 /**
1868 * Returns the value of the display type. It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
1869 * @return int Display type
1870 */
da3801e8 1871 public function get_displaytype() {
3c2e81ee 1872 global $CFG;
1873
1874 if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
1875 return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);
1876
1877 } else {
1878 return $this->display;
1879 }
1880 }
1881
1882 /**
1883 * Returns the value of the decimals field. It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
1884 * @return int Decimals (0 - 5)
1885 */
da3801e8 1886 public function get_decimals() {
3c2e81ee 1887 global $CFG;
1888
1889 if (is_null($this->decimals)) {
1890 return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);
1891
1892 } else {
1893 return $this->decimals;
1894 }
1895 }
4dc81cc7 1896
9fb16349 1897 /**
1898 * Returns a string representing the range of grademin - grademax for this grade item.
1899 * @param int $rangesdisplaytype
1900 * @param int $rangesdecimalpoints
1901 * @return string
1902 */
4dc81cc7 1903 function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
1904
1905 global $USER;
1906
1907 // Determine which display type to use for this average
1908 if (isset($USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
1909 $displaytype = GRADE_DISPLAY_TYPE_REAL;
1910
1911 } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
1912 $displaytype = $this->get_displaytype();
1913
1914 } else {
1915 $displaytype = $rangesdisplaytype;
1916 }
1917
1918 // Override grade_item setting if a display preference (not default) was set for the averages
1919 if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
1920 $decimalpoints = $this->get_decimals();
1921
1922 } else {
1923 $decimalpoints = $rangesdecimalpoints;
1924 }
1925
1926 if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
1927 $grademin = "0 %";
1928 $grademax = "100 %";
1929
1930 } else {
1931 $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
1932 $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
1933 }
1934
1935 return $grademin.'&ndash;'. $grademax;
1936 }
3c2e81ee 1937}
1938?>