MDL-11517 reserved word MOD used in table alias in questions backup code
[moodle-pu.git] / lib / grade / grade_item.php
blobc3e4b464314738765551bd96e34c3b4d1498d484
1 <?php // $Id$
3 ///////////////////////////////////////////////////////////////////////////
4 // //
5 // NOTICE OF COPYRIGHT //
6 // //
7 // Moodle - Modular Object-Oriented Dynamic Learning Environment //
8 // http://moodle.com //
9 // //
10 // Copyright (C) 2001-2003 Martin Dougiamas http://dougiamas.com //
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 ///////////////////////////////////////////////////////////////////////////
26 require_once('grade_object.php');
28 /**
29 * Class representing a grade item. It is responsible for handling its DB representation,
30 * modifying and returning its metadata.
32 class grade_item extends grade_object {
33 /**
34 * DB Table (used by grade_object).
35 * @var string $table
37 var $table = 'grade_items';
39 /**
40 * Array of required table fields, must start with 'id'.
41 * @var array $required_fields
43 var $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
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');
49 /**
50 * The course this grade_item belongs to.
51 * @var int $courseid
53 var $courseid;
55 /**
56 * The category this grade_item belongs to (optional).
57 * @var int $categoryid
59 var $categoryid;
61 /**
62 * The grade_category object referenced $this->iteminstance (itemtype must be == 'category' or == 'course' in that case).
63 * @var object $item_category
65 var $item_category;
67 /**
68 * The grade_category object referenced by $this->categoryid.
69 * @var object $parent_category
71 var $parent_category;
74 /**
75 * The name of this grade_item (pushed by the module).
76 * @var string $itemname
78 var $itemname;
80 /**
81 * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
82 * @var string $itemtype
84 var $itemtype;
86 /**
87 * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
88 * @var string $itemmodule
90 var $itemmodule;
92 /**
93 * ID of the item module
94 * @var int $iteminstance
96 var $iteminstance;
98 /**
99 * Number of the item in a series of multiple grades pushed by an activity.
100 * @var int $itemnumber
102 var $itemnumber;
105 * Info and notes about this item.
106 * @var string $iteminfo
108 var $iteminfo;
111 * Arbitrary idnumber provided by the module responsible.
112 * @var string $idnumber
114 var $idnumber;
117 * Calculation string used for this item.
118 * @var string $calculation
120 var $calculation;
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
127 var $calculation_normalized;
129 * Math evaluation object
131 var $formula;
134 * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
135 * @var int $gradetype
137 var $gradetype = GRADE_TYPE_VALUE;
140 * Maximum allowable grade.
141 * @var float $grademax
143 var $grademax = 100;
146 * Minimum allowable grade.
147 * @var float $grademin
149 var $grademin = 0;
152 * id of the scale, if this grade is based on a scale.
153 * @var int $scaleid
155 var $scaleid;
158 * A grade_scale object (referenced by $this->scaleid).
159 * @var object $scale
161 var $scale;
164 * The id of the optional grade_outcome associated with this grade_item.
165 * @var int $outcomeid
167 var $outcomeid;
170 * The grade_outcome this grade is associated with, if applicable.
171 * @var object $outcome
173 var $outcome;
176 * grade required to pass. (grademin <= gradepass <= grademax)
177 * @var float $gradepass
179 var $gradepass = 0;
182 * Multiply all grades by this number.
183 * @var float $multfactor
185 var $multfactor = 1.0;
188 * Add this to all grades.
189 * @var float $plusfactor
191 var $plusfactor = 0;
194 * Aggregation coeficient used for weighted averages
195 * @var float $aggregationcoef
197 var $aggregationcoef = 0;
200 * Sorting order of the columns.
201 * @var int $sortorder
203 var $sortorder = 0;
206 * Display type of the grades (Real, Percentage, Letter, or default).
207 * @var int $display
209 var $display = GRADE_DISPLAY_TYPE_DEFAULT;
212 * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
213 * @var int $decimals
215 var $decimals = null;
218 * 0 if visible, 1 always hidden or date not visible until
219 * @var int $hidden
221 var $hidden = 0;
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
227 var $locked = 0;
230 * Date after which the grade will be locked. Empty means no automatic locking.
231 * @var int $locktime
233 var $locktime = 0;
236 * If set, the whole column will be recalculated, then this flag will be switched off.
237 * @var boolean $needsupdate
239 var $needsupdate = 1;
242 * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
243 * Force regrading if necessary
244 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
245 * @return boolean success
247 function update($source=null) {
248 // Retrieve scale and infer grademax/min from it if needed
249 $this->load_scale();
251 // make sure there is not 0 in outcomeid
252 if (empty($this->outcomeid)) {
253 $this->outcomeid = null;
256 if ($this->qualifies_for_regrading()) {
257 $this->force_regrading();
260 return parent::update($source);
264 * Compares the values held by this object with those of the matching record in DB, and returns
265 * whether or not these differences are sufficient to justify an update of all parent objects.
266 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
267 * @return boolean
269 function qualifies_for_regrading() {
270 if (empty($this->id)) {
271 return false;
274 $db_item = new grade_item(array('id' => $this->id));
276 $calculationdiff = $db_item->calculation != $this->calculation;
277 $categorydiff = $db_item->categoryid != $this->categoryid;
278 $gradetypediff = $db_item->gradetype != $this->gradetype;
279 $grademaxdiff = $db_item->grademax != $this->grademax;
280 $grademindiff = $db_item->grademin != $this->grademin;
281 $scaleiddiff = $db_item->scaleid != $this->scaleid;
282 $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
283 $multfactordiff = $db_item->multfactor != $this->multfactor;
284 $plusfactordiff = $db_item->plusfactor != $this->plusfactor;
285 $locktimediff = $db_item->locktime != $this->locktime;
286 $acoefdiff = $db_item->aggregationcoef != $this->aggregationcoef;
288 $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
289 $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
291 return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
292 || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
293 || $lockeddiff || $acoefdiff || $locktimediff);
297 * Finds and returns a grade_item instance based on params.
298 * @static
300 * @param array $params associative arrays varname=>value
301 * @return object grade_item instance or false if none found.
303 function fetch($params) {
304 return grade_object::fetch_helper('grade_items', 'grade_item', $params);
308 * Finds and returns all grade_item instances based on params.
309 * @static
311 * @param array $params associative arrays varname=>value
312 * @return array array of grade_item insatnces or false if none found.
314 function fetch_all($params) {
315 return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
319 * Delete all grades and force_regrading of parent category.
320 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
321 * @return boolean success
323 function delete($source=null) {
324 if (!$this->is_course_item()) {
325 $this->force_regrading();
328 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
329 foreach ($grades as $grade) {
330 $grade->delete($source);
334 return parent::delete($source);
338 * In addition to perform parent::insert(), calls force_regrading() method too.
339 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
340 * @return int PK ID if successful, false otherwise
342 function insert($source=null) {
343 global $CFG;
345 if (empty($this->courseid)) {
346 error('Can not insert grade item without course id!');
349 // load scale if needed
350 $this->load_scale();
352 // add parent category if needed
353 if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
354 $course_category = grade_category::fetch_course_category($this->courseid);
355 $this->categoryid = $course_category->id;
359 // always place the new items at the end, move them after insert if needed
360 $last_sortorder = get_field_select('grade_items', 'MAX(sortorder)', "courseid = {$this->courseid}");
361 if (!empty($last_sortorder)) {
362 $this->sortorder = $last_sortorder + 1;
363 } else {
364 $this->sortorder = 1;
367 // add proper item numbers to manual items
368 if ($this->itemtype == 'manual') {
369 if (empty($this->itemnumber)) {
370 $this->itemnumber = 0;
374 // make sure there is not 0 in outcomeid
375 if (empty($this->outcomeid)) {
376 $this->outcomeid = null;
379 if (parent::insert($source)) {
380 // force regrading of items if needed
381 $this->force_regrading();
382 return $this->id;
384 } else {
385 debugging("Could not insert this grade_item in the database!");
386 return false;
391 * Set idnumber of grade item, updates also course_modules table
392 * @param string $idnumber (without magic quotes)
393 * @return boolean success
395 function add_idnumber($idnumber) {
396 if (!empty($this->idnumber)) {
397 return false;
400 if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {
401 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {
402 return false;
404 if (!empty($cm->idnumber)) {
405 return false;
407 if (set_field('course_modules', 'idnumber', addslashes($idnumber), 'id', $cm->id)) {
408 $this->idnumber = $idnumber;
409 return $this->update();
411 return false;
413 } else {
414 $this->idnumber = $idnumber;
415 return $this->update();
420 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
421 * $userid is given) or the locked state of a specific grade within this item if a specific
422 * $userid is given and the grade_item is unlocked.
424 * @param int $userid
425 * @return boolean Locked state
427 function is_locked($userid=NULL) {
428 if (!empty($this->locked)) {
429 return true;
432 if (!empty($userid)) {
433 if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
434 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
435 return $grade->is_locked();
439 return false;
443 * Locks or unlocks this grade_item and (optionally) all its associated final grades.
444 * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
445 * @param boolean $cascade lock/unlock child objects too
446 * @param boolean $refresh refresh grades when unlocking
447 * @return boolean true if grade_item all grades updated, false if at least one update fails
449 function set_locked($lockedstate, $cascade=false, $refresh=true) {
450 if ($lockedstate) {
451 /// setting lock
452 if ($this->needsupdate) {
453 return false; // can not lock grade without first having final grade
456 $this->locked = time();
457 $this->update();
459 if ($cascade) {
460 $grades = $this->get_final();
461 foreach($grades as $g) {
462 $grade = new grade_grade($g, false);
463 $grade->grade_item =& $this;
464 $grade->set_locked(1, null, false);
468 return true;
470 } else {
471 /// removing lock
472 if (!empty($this->locked) and $this->locktime < time()) {
473 //we have to reset locktime or else it would lock up again
474 $this->locktime = 0;
477 $this->locked = 0;
478 $this->update();
480 if ($cascade) {
481 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
482 foreach($grades as $grade) {
483 $grade->grade_item =& $this;
484 $grade->set_locked(0, null, false);
489 if ($refresh) {
490 //refresh when unlocking
491 $this->refresh_grades();
494 return true;
499 * Lock the grade if needed - make sure this is called only when final grades are valid
501 function check_locktime() {
502 if (!empty($this->locked)) {
503 return; // already locked
506 if ($this->locktime and $this->locktime < time()) {
507 $this->locked = time();
508 $this->update('locktime');
513 * Set the locktime for this grade item.
515 * @param int $locktime timestamp for lock to activate
516 * @return void
518 function set_locktime($locktime) {
519 $this->locktime = $locktime;
520 $this->update();
524 * Set the locktime for this grade item.
526 * @return int $locktime timestamp for lock to activate
528 function get_locktime() {
529 return $this->locktime;
533 * Returns the hidden state of this grade_item
534 * @return boolean hidden state
536 function is_hidden() {
537 return ($this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()));
541 * Check grade item hidden status.
542 * @return int 0 means visible, 1 hidden always, timestamp hidden until
544 function get_hidden() {
545 return $this->hidden;
549 * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.
550 * @param int $hidden new hidden status
551 * @param boolean $cascade apply to child objects too
552 * @return void
554 function set_hidden($hidden, $cascade=false) {
555 $this->hidden = $hidden;
556 $this->update();
558 if ($cascade) {
559 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
560 foreach($grades as $grade) {
561 $grade->grade_item =& $this;
562 $grade->set_hidden($hidden, $cascade);
569 * Mark regrading as finished successfully.
571 function regrading_finished() {
572 $this->needsupdate = 0;
573 //do not use $this->update() because we do not want this logged in grade_item_history
574 set_field('grade_items', 'needsupdate', 0, 'id', $this->id);
578 * Performs the necessary calculations on the grades_final referenced by this grade_item.
579 * Also resets the needsupdate flag once successfully performed.
581 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
582 * because the regrading must be done in correct order!!
584 * @return boolean true if ok, error string otherwise
586 function regrade_final_grades($userid=null) {
587 global $CFG;
589 // locked grade items already have correct final grades
590 if ($this->is_locked()) {
591 return true;
594 // calculation produces final value using formula from other final values
595 if ($this->is_calculated()) {
596 if ($this->compute($userid)) {
597 return true;
598 } else {
599 return "Could not calculate grades for grade item"; // TODO: improve and localize
602 // noncalculated outcomes already have final values - raw grades not used
603 } else if ($this->is_outcome_item()) {
604 return true;
606 // aggregate the category grade
607 } else if ($this->is_category_item() or $this->is_course_item()) {
608 // aggregate category grade item
609 $category = $this->get_item_category();
610 $category->grade_item =& $this;
611 if ($category->generate_grades($userid)) {
612 return true;
613 } else {
614 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
617 } else if ($this->is_manual_item()) {
618 // manual items track only final grades, no raw grades
619 return true;
621 } else if (!$this->is_raw_used()) {
622 // hmm - raw grades are not used- nothing to regrade
623 return true;
626 // normal grade item - just new final grades
627 $result = true;
628 $grade_inst = new grade_grade();
629 $fields = implode(',', $grade_inst->required_fields);
630 if ($userid) {
631 $rs = get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid", '', $fields);
632 } else {
633 $rs = get_recordset('grade_grades', 'itemid', $this->id, '', $fields);
635 if ($rs) {
636 if ($rs->RecordCount() > 0) {
637 while ($grade_record = rs_fetch_next_record($rs)) {
638 $grade = new grade_grade($grade_record, false);
640 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
641 // this grade is locked - final grade must be ok
642 continue;
645 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
647 if ($grade_record->finalgrade !== $grade->finalgrade) {
648 if (!$grade->update('system')) {
649 $result = "Internal error updating final grade";
654 rs_close($rs);
657 return $result;
661 * Given a float grade value or integer grade scale, applies a number of adjustment based on
662 * grade_item variables and returns the result.
663 * @param object $rawgrade The raw grade value.
664 * @return mixed
666 function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
667 if (is_null($rawgrade)) {
668 return null;
671 if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
673 if ($this->grademax < $this->grademin) {
674 return null;
677 if ($this->grademax == $this->grademin) {
678 return $this->grademax; // no range
681 // Standardise score to the new grade range
682 // NOTE: this is not compatible with current assignment grading
683 if ($rawmin != $this->grademin or $rawmax != $this->grademax) {
684 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
687 // Apply other grade_item factors
688 $rawgrade *= $this->multfactor;
689 $rawgrade += $this->plusfactor;
691 return bounded_number($this->grademin, $rawgrade, $this->grademax);
693 } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
694 if (empty($this->scale)) {
695 $this->load_scale();
698 if ($this->grademax < 0) {
699 return null; // scale not present - no grade
702 if ($this->grademax == 0) {
703 return $this->grademax; // only one option
706 // Convert scale if needed
707 // NOTE: this is not compatible with current assignment grading
708 if ($rawmin != $this->grademin or $rawmax != $this->grademax) {
709 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
712 return (int)bounded_number(0, round($rawgrade+0.00001), $this->grademax);
715 } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
716 // somebody changed the grading type when grades already existed
717 return null;
719 } else {
720 dubugging("Unkown grade type");
721 return null;;
726 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
727 * @return void
729 function force_regrading() {
730 $this->needsupdate = 1;
731 //mark this item and course item only - categories and calculated items are always regraded
732 $wheresql = "(itemtype='course' OR id={$this->id}) AND courseid={$this->courseid}";
733 set_field_select('grade_items', 'needsupdate', 1, $wheresql);
737 * Instantiates a grade_scale object whose data is retrieved from the DB,
738 * if this item's scaleid variable is set.
739 * @return object grade_scale or null if no scale used
741 function load_scale() {
742 if ($this->gradetype != GRADE_TYPE_SCALE) {
743 $this->scaleid = null;
746 if (!empty($this->scaleid)) {
747 //do not load scale if already present
748 if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
749 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
750 $this->scale->load_items();
753 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
754 // stay with the current min=1 max=count(scaleitems)
755 $this->grademax = count($this->scale->scale_items);
756 $this->grademin = 1;
758 } else {
759 $this->scale = null;
762 return $this->scale;
766 * Instantiates a grade_outcome object whose data is retrieved from the DB,
767 * if this item's outcomeid variable is set.
768 * @return object grade_outcome
770 function load_outcome() {
771 if (!empty($this->outcomeid)) {
772 $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
774 return $this->outcome;
778 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
779 * or category attached to category item.
781 * @return mixed grade_category object if applicable, false if course item
783 function get_parent_category() {
784 if ($this->is_category_item() or $this->is_course_item()) {
785 return $this->get_item_category();
787 } else {
788 return grade_category::fetch(array('id'=>$this->categoryid));
793 * Calls upon the get_parent_category method to retrieve the grade_category object
794 * from the DB and assigns it to $this->parent_category. It also returns the object.
795 * @return object Grade_category
797 function load_parent_category() {
798 if (empty($this->parent_category->id)) {
799 $this->parent_category = $this->get_parent_category();
801 return $this->parent_category;
805 * Returns the grade_category for category item
807 * @return mixed grade_category object if applicable, false otherwise
809 function get_item_category() {
810 if (!$this->is_course_item() and !$this->is_category_item()) {
811 return false;
813 return grade_category::fetch(array('id'=>$this->iteminstance));
817 * Calls upon the get_item_category method to retrieve the grade_category object
818 * from the DB and assigns it to $this->item_category. It also returns the object.
819 * @return object Grade_category
821 function load_item_category() {
822 if (empty($this->category->id)) {
823 $this->item_category = $this->get_item_category();
825 return $this->item_category;
829 * Is the grade item associated with category?
830 * @return boolean
832 function is_category_item() {
833 return ($this->itemtype == 'category');
837 * Is the grade item associated with course?
838 * @return boolean
840 function is_course_item() {
841 return ($this->itemtype == 'course');
845 * Is this a manualy graded item?
846 * @return boolean
848 function is_manual_item() {
849 return ($this->itemtype == 'manual');
853 * Is this an outcome item?
854 * @return boolean
856 function is_outcome_item() {
857 return !empty($this->outcomeid);
861 * Is the grade item normal - associated with module, plugin or something else?
862 * @return boolean
864 function is_normal_item() {
865 return ($this->itemtype != 'course' and $this->itemtype != 'category' and $this->itemtype != 'manual');
869 * Returns true if grade items uses raw grades
870 * @return boolean
872 function is_raw_used() {
873 return ($this->is_normal_item() and !$this->is_calculated() and !$this->is_outcome_item());
877 * Returns grade item associated with the course
878 * @param int $courseid
879 * @return course item object
881 function fetch_course_item($courseid) {
882 if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
883 return $course_item;
886 // first get category - it creates the associated grade item
887 $course_category = grade_category::fetch_course_category($courseid);
889 return grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'));
893 * Is grading object editable?
894 * @return boolean
896 function is_editable() {
897 return true;
901 * Checks if grade calculated. Returns this object's calculation.
902 * @return boolean true if grade item calculated.
904 function is_calculated() {
905 if (empty($this->calculation)) {
906 return false;
910 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
911 * we would have to fetch all course grade items to find out the ids.
912 * Also if user changes the idnumber the formula does not need to be updated.
915 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
916 if (!$this->calculation_normalized and preg_match('/##gi\d+##/', $this->calculation)) {
917 $this->set_calculation($this->calculation);
920 return !empty($this->calculation);
924 * Returns calculation string if grade calculated.
925 * @return mixed string if calculation used, null if not
927 function get_calculation() {
928 if ($this->is_calculated()) {
929 return grade_item::denormalize_formula($this->calculation, $this->courseid);
931 } else {
932 return NULL;
937 * Sets this item's calculation (creates it) if not yet set, or
938 * updates it if already set (in the DB). If no calculation is given,
939 * the calculation is removed.
940 * @param string $formula string representation of formula used for calculation
941 * @return boolean success
943 function set_calculation($formula) {
944 $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
945 $this->calculation_normalized = true;
946 return $this->update();
950 * Denormalizes the calculation formula to [idnumber] form
951 * @static
952 * @param string $formula
953 * @return string denormalized string
955 function denormalize_formula($formula, $courseid) {
956 if (empty($formula)) {
957 return '';
960 // denormalize formula - convert ##giXX## to [[idnumber]]
961 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
962 foreach ($matches[1] as $id) {
963 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
964 if (!empty($grade_item->idnumber)) {
965 $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
971 return $formula;
976 * Normalizes the calculation formula to [#giXX#] form
977 * @static
978 * @param string $formula
979 * @return string normalized string
981 function normalize_formula($formula, $courseid) {
982 $formula = trim($formula);
984 if (empty($formula)) {
985 return NULL;
989 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
990 if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
991 foreach ($grade_items as $grade_item) {
992 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
996 return $formula;
1000 * Returns the final values for this grade item (as imported by module or other source).
1001 * @param int $userid Optional: to retrieve a single final grade
1002 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
1004 function get_final($userid=NULL) {
1005 if ($userid) {
1006 if ($user = get_record('grade_grades', 'itemid', $this->id, 'userid', $userid)) {
1007 return $user;
1010 } else {
1011 if ($grades = get_records('grade_grades', 'itemid', $this->id)) {
1012 //TODO: speed up with better SQL
1013 $result = array();
1014 foreach ($grades as $grade) {
1015 $result[$grade->userid] = $grade;
1017 return $result;
1018 } else {
1019 return array();
1025 * Get (or create if not exist yet) grade for this user
1026 * @param int $userid
1027 * @return object grade_grade object instance
1029 function get_grade($userid, $create=true) {
1030 if (empty($this->id)) {
1031 debugging('Can not use before insert');
1032 return false;
1035 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
1036 if (empty($grade->id) and $create) {
1037 $grade->insert();
1040 return $grade;
1044 * Returns the sortorder of this grade_item. This method is also available in
1045 * grade_category, for cases where the object type is not know.
1046 * @return int Sort order
1048 function get_sortorder() {
1049 return $this->sortorder;
1053 * Returns the idnumber of this grade_item. This method is also available in
1054 * grade_category, for cases where the object type is not know.
1055 * @return string idnumber
1057 function get_idnumber() {
1058 return $this->idnumber;
1062 * Returns this grade_item. This method is also available in
1063 * grade_category, for cases where the object type is not know.
1064 * @return string idnumber
1066 function get_grade_item() {
1067 return $this;
1071 * Sets the sortorder of this grade_item. This method is also available in
1072 * grade_category, for cases where the object type is not know.
1073 * @param int $sortorder
1074 * @return void
1076 function set_sortorder($sortorder) {
1077 $this->sortorder = $sortorder;
1078 $this->update();
1081 function move_after_sortorder($sortorder) {
1082 global $CFG;
1084 //make some room first
1085 $sql = "UPDATE {$CFG->prefix}grade_items
1086 SET sortorder = sortorder + 1
1087 WHERE sortorder > $sortorder AND courseid = {$this->courseid}";
1088 execute_sql($sql, false);
1090 $this->set_sortorder($sortorder + 1);
1094 * Returns the most descriptive field for this object. This is a standard method used
1095 * when we do not know the exact type of an object.
1096 * @return string name
1098 function get_name() {
1099 if (!empty($this->itemname)) {
1100 // MDL-10557
1101 return format_string($this->itemname);
1103 } else if ($this->is_course_item()) {
1104 return get_string('coursetotal', 'grades');
1106 } else if ($this->is_category_item()) {
1107 return get_string('categorytotal', 'grades');
1109 } else {
1110 return get_string('grade');
1115 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1116 * @param int $parentid
1117 * @return boolean success;
1119 function set_parent($parentid) {
1120 if ($this->is_course_item() or $this->is_category_item()) {
1121 error('Can not set parent for category or course item!');
1124 if ($this->categoryid == $parentid) {
1125 return true;
1128 // find parent and check course id
1129 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1130 return false;
1133 $this->force_regrading();
1135 // set new parent
1136 $this->categoryid = $parent_category->id;
1137 $this->parent_category =& $parent_category;
1139 return $this->update();
1143 * Finds out on which other items does this depend directly when doing calculation or category agregation
1144 * @return array of grade_item ids this one depends on
1146 function depends_on() {
1147 global $CFG;
1149 if ($this->is_locked()) {
1150 // locked items do not need to be regraded
1151 return array();
1154 if ($this->is_calculated()) {
1155 if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1156 return array_unique($matches[1]); // remove duplicates
1157 } else {
1158 return array();
1161 } else if ($grade_category = $this->load_item_category()) {
1162 //only items with numeric or scale values can be aggregated
1163 if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {
1164 return array();
1167 // If global aggregateoutcomes is set, override category value
1168 if ($CFG->grade_aggregateoutcomes != -1) {
1169 $grade_category->aggregateoutcomes = $CFG->grade_aggregateoutcomes;
1172 // If global aggregatesubcats is set, override category value
1173 if ($CFG->grade_aggregatesubcats != -1) {
1174 $grade_category->aggregatesubcats = $CFG->grade_aggregatesubcats;
1177 if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {
1178 $outcomes_sql = "";
1179 } else {
1180 $outcomes_sql = "AND gi.outcomeid IS NULL";
1183 if ($grade_category->aggregatesubcats) {
1184 // return all children excluding category items
1185 $sql = "SELECT gi.id
1186 FROM {$CFG->prefix}grade_items gi
1187 WHERE (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")
1188 $outcomes_sql
1189 AND gi.categoryid IN (
1190 SELECT gc.id
1191 FROM {$CFG->prefix}grade_categories gc
1192 WHERE gc.path LIKE '%/{$grade_category->id}/%')";
1194 } else {
1195 $sql = "SELECT gi.id
1196 FROM {$CFG->prefix}grade_items gi
1197 WHERE gi.categoryid = {$grade_category->id}
1198 AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")
1199 $outcomes_sql
1201 UNION
1203 SELECT gi.id
1204 FROM {$CFG->prefix}grade_items gi, {$CFG->prefix}grade_categories gc
1205 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1206 AND gc.parent = {$grade_category->id}
1207 AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")
1208 $outcomes_sql";
1211 if ($children = get_records_sql($sql)) {
1212 return array_keys($children);
1213 } else {
1214 return array();
1217 } else {
1218 return array();
1223 * Refetch grades from moudles, plugins.
1224 * @param int $userid optional, one user only
1226 function refresh_grades($userid=0) {
1227 if ($this->itemtype == 'mod') {
1228 if ($this->is_outcome_item()) {
1229 //nothing to do
1230 return;
1233 if (!$activity = get_record($this->itemmodule, 'id', $this->iteminstance)) {
1234 debugging('Can not find activity');
1235 return;
1238 if (! $cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {
1239 debuggin('Can not find course module');
1240 return;
1243 $activity->modname = $this->itemmodule;
1244 $activity->cmidnumber = $cm->idnumber;
1246 grade_update_mod_grades($activity);
1251 * Updates final grade value for given user, this is a only way to update final
1252 * grades from gradebook and import because it logs the change in history table
1253 * and deals with overridden flag. This flag is set to prevent later overriding
1254 * from raw grades submitted from modules.
1256 * @param int $userid the graded user
1257 * @param mixed $finalgrade float value of final grade - false means do not change
1258 * @param string $howmodified modification source
1259 * @param string $note optional note
1260 * @param mixed $feedback teachers feedback as string - false means do not change
1261 * @param int $feedbackformat
1262 * @return boolean success
1264 function update_final_grade($userid, $finalgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
1265 global $USER, $CFG;
1267 if (empty($usermodified)) {
1268 $usermodified = $USER->id;
1271 $result = true;
1273 // no grading used or locked
1274 if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1275 return false;
1278 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1279 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1281 $grade->usermodified = $usermodified;
1283 if ($grade->is_locked()) {
1284 // do not update locked grades at all
1285 return false;
1288 $locktime = $grade->get_locktime();
1289 if ($locktime and $locktime < time()) {
1290 // do not update grades that should be already locked, force regrade instead
1291 $this->force_regrading();
1292 return false;
1295 $oldgrade = new object();
1296 $oldgrade->finalgrade = $grade->finalgrade;
1297 $oldgrade->overridden = $grade->overridden;
1298 $oldgrade->feedback = $grade->feedback;
1299 $oldgrade->feedbackformat = $grade->feedbackformat;
1301 if ($finalgrade !== false or $feedback !== false) {
1302 if (($this->is_outcome_item() or $this->is_manual_item()) and !$this->is_calculated()) {
1303 // final grades updated only by user - no need for overriding
1304 $grade->overridden = 0;
1306 } else {
1307 $grade->overridden = time();
1311 if ($finalgrade !== false) {
1312 if (!is_null($finalgrade)) {
1313 $finalgrade = bounded_number($this->grademin, $finalgrade, $this->grademax);
1314 } else {
1315 $finalgrade = $finalgrade;
1317 $grade->finalgrade = $finalgrade;
1320 // do we have comment from teacher?
1321 if ($feedback !== false) {
1322 $grade->feedback = $feedback;
1323 $grade->feedbackformat = $feedbackformat;
1326 if (empty($grade->id)) {
1327 $result = (boolean)$grade->insert($source);
1329 } else if ($grade->finalgrade !== $oldgrade->finalgrade
1330 or $grade->feedback !== $oldgrade->feedback
1331 or $grade->feedbackformat !== $oldgrade->feedbackformat) {
1332 $result = $grade->update($source);
1335 if (!$result) {
1336 // something went wrong - better force final grade recalculation
1337 $this->force_regrading();
1339 } else if ($this->is_course_item() and !$this->needsupdate) {
1340 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
1341 $this->force_regrading();
1344 } else if (!$this->needsupdate) {
1345 $course_item = grade_item::fetch_course_item($this->courseid);
1346 if (!$course_item->needsupdate) {
1347 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
1348 $this->force_regrading();
1350 } else {
1351 $this->force_regrading();
1355 return $result;
1360 * Updates raw grade value for given user, this is a only way to update raw
1361 * grades from external source (modules, etc.),
1362 * because it logs the change in history table and deals with final grade recalculation.
1364 * @param int $userid the graded user
1365 * @param mixed $rawgrade float value of raw grade - false means do not change
1366 * @param string $howmodified modification source
1367 * @param string $note optional note
1368 * @param mixed $feedback teachers feedback as string - false means do not change
1369 * @param int $feedbackformat
1370 * @return boolean success
1372 function update_raw_grade($userid, $rawgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
1373 global $USER;
1375 if (empty($usermodified)) {
1376 $usermodified = $USER->id;
1379 $result = true;
1381 // calculated grades can not be updated; course and category can not be updated because they are aggregated
1382 if ($this->is_calculated() or $this->is_outcome_item() or !$this->is_normal_item()
1383 or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1384 return false;
1387 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
1388 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1390 $grade->usermodified = $usermodified;
1392 if ($grade->is_locked()) {
1393 // do not update locked grades at all
1394 return false;
1397 $locktime = $grade->get_locktime();
1398 if ($locktime and $locktime < time()) {
1399 // do not update grades that should be already locked and force regrade
1400 $this->force_regrading();
1401 return false;
1404 $oldgrade = new object();
1405 $oldgrade->finalgrade = $grade->finalgrade;
1406 $oldgrade->rawgrade = $grade->rawgrade;
1407 $oldgrade->rawgrademin = $grade->rawgrademin;
1408 $oldgrade->rawgrademax = $grade->rawgrademax;
1409 $oldgrade->rawscaleid = $grade->rawscaleid;
1410 $oldgrade->feedback = $grade->feedback;
1411 $oldgrade->feedbackformat = $grade->feedbackformat;
1413 // fist copy current grademin/max and scale
1414 $grade->rawgrademin = $this->grademin;
1415 $grade->rawgrademax = $this->grademax;
1416 $grade->rawscaleid = $this->scaleid;
1418 // change raw grade?
1419 if ($rawgrade !== false) {
1420 $grade->rawgrade = $rawgrade;
1423 // do we have comment from teacher?
1424 if ($feedback !== false) {
1425 $grade->feedback = $feedback;
1426 $grade->feedbackformat = $feedbackformat;
1429 if (empty($grade->id)) {
1430 $result = (boolean)$grade->insert($source);
1432 } else if ($grade->finalgrade !== $oldgrade->finalgrade
1433 or $grade->rawgrade !== $oldgrade->rawgrade
1434 or $grade->rawgrademin !== $oldgrade->rawgrademin
1435 or $grade->rawgrademax !== $oldgrade->rawgrademax
1436 or $grade->rawscaleid !== $oldgrade->rawscaleid
1437 or $grade->feedback !== $oldgrade->feedback
1438 or $grade->feedbackformat !== $oldgrade->feedbackformat) {
1440 $result = $grade->update($source);
1443 if (!$result) {
1444 // something went wrong - better force final grade recalculation
1445 $this->force_regrading();
1447 } else if (!$this->needsupdate) {
1448 $course_item = grade_item::fetch_course_item($this->courseid);
1449 if (!$course_item->needsupdate) {
1450 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
1451 $this->force_regrading();
1453 } else {
1454 $this->force_regrading();
1458 return $result;
1462 * Calculates final grade values using the formula in calculation property.
1463 * The parameters are taken from final grades of grade items in current course only.
1464 * @return boolean false if error
1466 function compute($userid=null) {
1467 global $CFG;
1469 if (!$this->is_calculated()) {
1470 return false;
1473 require_once($CFG->libdir.'/mathslib.php');
1475 if ($this->is_locked()) {
1476 return true; // no need to recalculate locked items
1479 // get used items
1480 $useditems = $this->depends_on();
1482 // prepare formula and init maths library
1483 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
1484 $this->formula = new calc_formula($formula);
1486 // where to look for final grades?
1487 // this itemid is added so that we use only one query for source and final grades
1488 $gis = implode(',', array_merge($useditems, array($this->id)));
1490 if ($userid) {
1491 $usersql = "AND g.userid=$userid";
1492 } else {
1493 $usersql = "";
1496 $grade_inst = new grade_grade();
1497 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
1499 $sql = "SELECT $fields
1500 FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
1501 WHERE gi.id = g.itemid AND gi.courseid={$this->courseid} AND gi.id IN ($gis) $usersql
1502 ORDER BY g.userid";
1504 $return = true;
1506 // group the grades by userid and use formula on the group
1507 if ($rs = get_recordset_sql($sql)) {
1508 if ($rs->RecordCount() > 0) {
1509 $prevuser = 0;
1510 $grade_records = array();
1511 $oldgrade = null;
1512 while ($used = rs_fetch_next_record($rs)) {
1513 if ($used->userid != $prevuser) {
1514 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1515 $return = false;
1517 $prevuser = $used->userid;
1518 $grade_records = array();
1519 $oldgrade = null;
1521 if ($used->itemid == $this->id) {
1522 $oldgrade = $used;
1524 $grade_records['gi'.$used->itemid] = $used->finalgrade;
1526 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1527 $return = false;
1530 rs_close($rs);
1533 return $return;
1537 * internal function - does the final grade calculation
1539 function use_formula($userid, $params, $useditems, $oldgrade) {
1540 if (empty($userid)) {
1541 return true;
1544 // add missing final grade values
1545 // not graded (null) is counted as 0 - the spreadsheet way
1546 foreach($useditems as $gi) {
1547 if (!array_key_exists('gi'.$gi, $params)) {
1548 $params['gi'.$gi] = 0;
1549 } else {
1550 $params['gi'.$gi] = (float)$params['gi'.$gi];
1554 // can not use own final grade during calculation
1555 unset($params['gi'.$this->id]);
1557 // insert final grade - will be needed later anyway
1558 if ($oldgrade) {
1559 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
1560 $grade->grade_item =& $this;
1562 } else {
1563 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
1564 $grade->insert('system');
1565 $grade->grade_item =& $this;
1567 $oldgrade = new object();
1568 $oldgrade->finalgrade = $grade->finalgrade;
1569 $oldgrade->rawgrade = $grade->rawgrade;
1572 // no need to recalculate locked or overridden grades
1573 if ($grade->is_locked() or $grade->is_overridden()) {
1574 return true;
1577 // do the calculation
1578 $this->formula->set_params($params);
1579 $result = $this->formula->evaluate();
1581 // no raw grade for calculated grades - only final
1582 $grade->rawgrade = null;
1585 if ($result === false) {
1586 $grade->finalgrade = null;
1588 } else {
1589 // normalize
1590 $result = bounded_number($this->grademin, $result, $this->grademax);
1591 if ($this->gradetype == GRADE_TYPE_SCALE) {
1592 $result = round($result+0.00001); // round scales upwards
1594 $grade->finalgrade = $result;
1597 // update in db if changed
1598 if ( $grade->finalgrade !== $oldgrade->finalgrade
1599 or $grade->rawgrade !== $oldgrade->rawgrade) {
1601 $grade->update('system');
1604 if ($result !== false) {
1605 //lock grade if needed
1608 if ($result === false) {
1609 return false;
1610 } else {
1611 return true;
1617 * Validate the formula.
1618 * @param string $formula
1619 * @return boolean true if calculation possible, false otherwise
1621 function validate_formula($formulastr) {
1622 global $CFG;
1623 require_once($CFG->libdir.'/mathslib.php');
1625 $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);
1627 if (empty($formulastr)) {
1628 return true;
1631 if (strpos($formulastr, '=') !== 0) {
1632 return get_string('errorcalculationnoequal', 'grades');
1635 // get used items
1636 if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
1637 $useditems = array_unique($matches[1]); // remove duplicates
1638 } else {
1639 $useditems = array();
1642 if (!empty($this->id)) {
1643 unset($useditems[$this->id]);
1646 // prepare formula and init maths library
1647 $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
1648 $formula = new calc_formula($formula);
1651 if (empty($useditems)) {
1652 $grade_items = array();
1654 } else {
1655 $gis = implode(',', $useditems);
1657 $sql = "SELECT gi.*
1658 FROM {$CFG->prefix}grade_items gi
1659 WHERE gi.id IN ($gis) and gi.courseid={$this->courseid}"; // from the same course only!
1661 if (!$grade_items = get_records_sql($sql)) {
1662 $grade_items = array();
1666 $params = array();
1667 foreach ($useditems as $itemid) {
1668 // make sure all grade items exist in this course
1669 if (!array_key_exists($itemid, $grade_items)) {
1670 return false;
1672 // use max grade when testing formula, this should be ok in 99.9%
1673 // division by 0 is one of possible problems
1674 $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
1677 // do the calculation
1678 $formula->set_params($params);
1679 $result = $formula->evaluate();
1681 // false as result indicates some problem
1682 if ($result === false) {
1683 // TODO: add more error hints
1684 return get_string('errorcalculationunknown', 'grades');
1685 } else {
1686 return true;
1691 * Returns the value of the display type. It can be set at 3 levels: grade_item, course and site. The lowest level overrides the higher ones.
1692 * @return int Display type
1694 function get_displaytype() {
1695 global $CFG;
1696 static $cache = array();
1698 if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {
1699 if (array_key_exists($this->courseid, $cache)) {
1700 return $cache[$this->courseid];
1701 } else if (count($cache) > 100) {
1702 $cache = array(); // cache size limit
1705 $gradedisplaytype = get_field('grade_items', 'display', 'courseid', $this->courseid, 'itemtype', 'course');
1706 if ($gradedisplaytype == GRADE_DISPLAY_TYPE_DEFAULT) {
1707 $gradedisplaytype = $CFG->grade_report_gradedisplaytype;
1709 $cache[$this->courseid] = $gradedisplaytype;
1710 return $gradedisplaytype;
1712 } else {
1713 return $this->display;
1718 * Returns the value of the decimals field. It can be set at 3 levels: grade_item, course and site. The lowest level overrides the higher ones.
1719 * @return int Decimals (0 - 5)
1721 function get_decimals() {
1722 global $CFG;
1723 static $cache = array();
1725 if (is_null($this->decimals)) {
1726 if (array_key_exists($this->courseid, $cache)) {
1727 return $cache[$this->courseid];
1728 } else if (count($cache) > 100) {
1729 $cache = array(); // cache size limit
1731 $gradedecimals = get_field('grade_items', 'decimals', 'courseid', $this->courseid, 'itemtype', 'course');
1732 if (is_null($gradedecimals)) {
1733 $gradedecimals = $CFG->grade_report_decimalpoints;
1735 $cache[$this->courseid] = $gradedecimals;
1736 return $gradedecimals;
1738 } else {
1739 return $this->decimals;