2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Definition of a class to represent a grade item
20 * @package core_grades
22 * @copyright 2006 Nicolas Connault
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 defined('MOODLE_INTERNAL') ||
die();
27 require_once('grade_object.php');
30 * Class representing a grade item.
32 * It is responsible for handling its DB representation, modifying and returning its metadata.
34 * @package core_grades
36 * @copyright 2006 Nicolas Connault
37 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39 class grade_item
extends grade_object
{
41 * DB Table (used by grade_object).
44 public $table = 'grade_items';
47 * Array of required table fields, must start with 'id'.
48 * @var array $required_fields
50 public $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',
51 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',
52 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',
53 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated',
57 * The course this grade_item belongs to.
63 * The category this grade_item belongs to (optional).
64 * @var int $categoryid
69 * The grade_category object referenced $this->iteminstance if itemtype == 'category' or == 'course'.
70 * @var grade_category $item_category
72 public $item_category;
75 * The grade_category object referenced by $this->categoryid.
76 * @var grade_category $parent_category
78 public $parent_category;
82 * The name of this grade_item (pushed by the module).
83 * @var string $itemname
88 * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
89 * @var string $itemtype
94 * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
95 * @var string $itemmodule
100 * ID of the item module
101 * @var int $iteminstance
103 public $iteminstance;
106 * Number of the item in a series of multiple grades pushed by an activity.
107 * @var int $itemnumber
112 * Info and notes about this item.
113 * @var string $iteminfo
118 * Arbitrary idnumber provided by the module responsible.
119 * @var string $idnumber
124 * Calculation string used for this item.
125 * @var string $calculation
130 * Indicates if we already tried to normalize the grade calculation formula.
131 * This flag helps to minimize db access when broken formulas used in calculation.
134 public $calculation_normalized;
136 * Math evaluation object
137 * @var calc_formula A formula object
142 * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
143 * @var int $gradetype
145 public $gradetype = GRADE_TYPE_VALUE
;
148 * Maximum allowable grade.
149 * @var float $grademax
151 public $grademax = 100;
154 * Minimum allowable grade.
155 * @var float $grademin
157 public $grademin = 0;
160 * id of the scale, if this grade is based on a scale.
166 * The grade_scale object referenced by $this->scaleid.
167 * @var grade_scale $scale
172 * The id of the optional grade_outcome associated with this grade_item.
173 * @var int $outcomeid
178 * The grade_outcome this grade is associated with, if applicable.
179 * @var grade_outcome $outcome
184 * grade required to pass. (grademin <= gradepass <= grademax)
185 * @var float $gradepass
187 public $gradepass = 0;
190 * Multiply all grades by this number.
191 * @var float $multfactor
193 public $multfactor = 1.0;
196 * Add this to all grades.
197 * @var float $plusfactor
199 public $plusfactor = 0;
202 * Aggregation coeficient used for weighted averages
203 * @var float $aggregationcoef
205 public $aggregationcoef = 0;
208 * Sorting order of the columns.
209 * @var int $sortorder
211 public $sortorder = 0;
214 * Display type of the grades (Real, Percentage, Letter, or default).
217 public $display = GRADE_DISPLAY_TYPE_DEFAULT
;
220 * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
223 public $decimals = null;
226 * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
232 * Date after which the grade will be locked. Empty means no automatic locking.
235 public $locktime = 0;
238 * If set, the whole column will be recalculated, then this flag will be switched off.
239 * @var bool $needsupdate
241 public $needsupdate = 1;
244 * Cached dependson array
245 * @var array An array of cached grade item dependencies.
247 public $dependson_cache = null;
250 * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
251 * Force regrading if necessary, rounds the float numbers using php function,
252 * the reason is we need to compare the db value with computed number to skip regrading if possible.
254 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
255 * @return bool success
257 public function update($source=null) {
259 $this->dependson_cache
= null;
261 // Retrieve scale and infer grademax/min from it if needed
264 // make sure there is not 0 in outcomeid
265 if (empty($this->outcomeid
)) {
266 $this->outcomeid
= null;
269 if ($this->qualifies_for_regrading()) {
270 $this->force_regrading();
273 $this->timemodified
= time();
275 $this->grademin
= grade_floatval($this->grademin
);
276 $this->grademax
= grade_floatval($this->grademax
);
277 $this->multfactor
= grade_floatval($this->multfactor
);
278 $this->plusfactor
= grade_floatval($this->plusfactor
);
279 $this->aggregationcoef
= grade_floatval($this->aggregationcoef
);
281 return parent
::update($source);
285 * Compares the values held by this object with those of the matching record in DB, and returns
286 * whether or not these differences are sufficient to justify an update of all parent objects.
287 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
291 public function qualifies_for_regrading() {
292 if (empty($this->id
)) {
296 $db_item = new grade_item(array('id' => $this->id
));
298 $calculationdiff = $db_item->calculation
!= $this->calculation
;
299 $categorydiff = $db_item->categoryid
!= $this->categoryid
;
300 $gradetypediff = $db_item->gradetype
!= $this->gradetype
;
301 $scaleiddiff = $db_item->scaleid
!= $this->scaleid
;
302 $outcomeiddiff = $db_item->outcomeid
!= $this->outcomeid
;
303 $locktimediff = $db_item->locktime
!= $this->locktime
;
304 $grademindiff = grade_floats_different($db_item->grademin
, $this->grademin
);
305 $grademaxdiff = grade_floats_different($db_item->grademax
, $this->grademax
);
306 $multfactordiff = grade_floats_different($db_item->multfactor
, $this->multfactor
);
307 $plusfactordiff = grade_floats_different($db_item->plusfactor
, $this->plusfactor
);
308 $acoefdiff = grade_floats_different($db_item->aggregationcoef
, $this->aggregationcoef
);
310 $needsupdatediff = !$db_item->needsupdate
&& $this->needsupdate
; // force regrading only if setting the flag first time
311 $lockeddiff = !empty($db_item->locked
) && empty($this->locked
); // force regrading only when unlocking
313 return ($calculationdiff ||
$categorydiff ||
$gradetypediff ||
$grademaxdiff ||
$grademindiff ||
$scaleiddiff
314 ||
$outcomeiddiff ||
$multfactordiff ||
$plusfactordiff ||
$needsupdatediff
315 ||
$lockeddiff ||
$acoefdiff ||
$locktimediff);
319 * Finds and returns a grade_item instance based on params.
322 * @param array $params associative arrays varname=>value
323 * @return grade_item|bool Returns a grade_item instance or false if none found
325 public static function fetch($params) {
326 return grade_object
::fetch_helper('grade_items', 'grade_item', $params);
330 * Finds and returns all grade_item instances based on params.
333 * @param array $params associative arrays varname=>value
334 * @return array array of grade_item instances or false if none found.
336 public static function fetch_all($params) {
337 return grade_object
::fetch_all_helper('grade_items', 'grade_item', $params);
341 * Delete all grades and force_regrading of parent category.
343 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
344 * @return bool success
346 public function delete($source=null) {
347 $this->delete_all_grades($source);
348 return parent
::delete($source);
354 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
357 public function delete_all_grades($source=null) {
358 if (!$this->is_course_item()) {
359 $this->force_regrading();
362 if ($grades = grade_grade
::fetch_all(array('itemid'=>$this->id
))) {
363 foreach ($grades as $grade) {
364 $grade->delete($source);
372 * In addition to perform parent::insert(), calls force_regrading() method too.
374 * @param string $source From where was the object inserted (mod/forum, manual, etc.)
375 * @return int PK ID if successful, false otherwise
377 public function insert($source=null) {
380 if (empty($this->courseid
)) {
381 print_error('cannotinsertgrade');
384 // load scale if needed
387 // add parent category if needed
388 if (empty($this->categoryid
) and !$this->is_course_item() and !$this->is_category_item()) {
389 $course_category = grade_category
::fetch_course_category($this->courseid
);
390 $this->categoryid
= $course_category->id
;
394 // always place the new items at the end, move them after insert if needed
395 $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid
));
396 if (!empty($last_sortorder)) {
397 $this->sortorder
= $last_sortorder +
1;
399 $this->sortorder
= 1;
402 // add proper item numbers to manual items
403 if ($this->itemtype
== 'manual') {
404 if (empty($this->itemnumber
)) {
405 $this->itemnumber
= 0;
409 // make sure there is not 0 in outcomeid
410 if (empty($this->outcomeid
)) {
411 $this->outcomeid
= null;
414 $this->timecreated
= $this->timemodified
= time();
416 if (parent
::insert($source)) {
417 // force regrading of items if needed
418 $this->force_regrading();
422 debugging("Could not insert this grade_item in the database!");
428 * Set idnumber of grade item, updates also course_modules table
430 * @param string $idnumber (without magic quotes)
431 * @return bool success
433 public function add_idnumber($idnumber) {
435 if (!empty($this->idnumber
)) {
439 if ($this->itemtype
== 'mod' and !$this->is_outcome_item()) {
440 if ($this->itemnumber
=== 0) {
441 // for activity modules, itemnumber 0 is synced with the course_modules
442 if (!$cm = get_coursemodule_from_instance($this->itemmodule
, $this->iteminstance
, $this->courseid
)) {
445 if (!empty($cm->idnumber
)) {
448 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id
));
449 $this->idnumber
= $idnumber;
450 return $this->update();
452 $this->idnumber
= $idnumber;
453 return $this->update();
457 $this->idnumber
= $idnumber;
458 return $this->update();
463 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
464 * $userid is given) or the locked state of a specific grade within this item if a specific
465 * $userid is given and the grade_item is unlocked.
467 * @param int $userid The user's ID
468 * @return bool Locked state
470 public function is_locked($userid=NULL) {
471 if (!empty($this->locked
)) {
475 if (!empty($userid)) {
476 if ($grade = grade_grade
::fetch(array('itemid'=>$this->id
, 'userid'=>$userid))) {
477 $grade->grade_item
=& $this; // prevent db fetching of cached grade_item
478 return $grade->is_locked();
486 * Locks or unlocks this grade_item and (optionally) all its associated final grades.
488 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
489 * @param bool $cascade Lock/unlock child objects too
490 * @param bool $refresh Refresh grades when unlocking
491 * @return bool True if grade_item all grades updated, false if at least one update fails
493 public function set_locked($lockedstate, $cascade=false, $refresh=true) {
496 if ($this->needsupdate
) {
497 return false; // can not lock grade without first having final grade
500 $this->locked
= time();
504 $grades = $this->get_final();
505 foreach($grades as $g) {
506 $grade = new grade_grade($g, false);
507 $grade->grade_item
=& $this;
508 $grade->set_locked(1, null, false);
516 if (!empty($this->locked
) and $this->locktime
< time()) {
517 //we have to reset locktime or else it would lock up again
525 if ($grades = grade_grade
::fetch_all(array('itemid'=>$this->id
))) {
526 foreach($grades as $grade) {
527 $grade->grade_item
=& $this;
528 $grade->set_locked(0, null, false);
534 //refresh when unlocking
535 $this->refresh_grades();
543 * Lock the grade if needed. Make sure this is called only when final grades are valid
545 public function check_locktime() {
546 if (!empty($this->locked
)) {
547 return; // already locked
550 if ($this->locktime
and $this->locktime
< time()) {
551 $this->locked
= time();
552 $this->update('locktime');
557 * Set the locktime for this grade item.
559 * @param int $locktime timestamp for lock to activate
562 public function set_locktime($locktime) {
563 $this->locktime
= $locktime;
568 * Set the locktime for this grade item.
570 * @return int $locktime timestamp for lock to activate
572 public function get_locktime() {
573 return $this->locktime
;
577 * Set the hidden status of grade_item and all grades.
579 * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
581 * @param int $hidden new hidden status
582 * @param bool $cascade apply to child objects too
584 public function set_hidden($hidden, $cascade=false) {
585 parent
::set_hidden($hidden, $cascade);
588 if ($grades = grade_grade
::fetch_all(array('itemid'=>$this->id
))) {
589 foreach($grades as $grade) {
590 $grade->grade_item
=& $this;
591 $grade->set_hidden($hidden, $cascade);
596 //if marking item visible make sure category is visible MDL-21367
598 $category_array = grade_category
::fetch_all(array('id'=>$this->categoryid
));
599 if ($category_array && array_key_exists($this->categoryid
, $category_array)) {
600 $category = $category_array[$this->categoryid
];
601 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
602 //if($category->is_hidden()) {
603 $category->set_hidden($hidden, false);
610 * Returns the number of grades that are hidden
612 * @param string $groupsql SQL to limit the query by group
613 * @param array $params SQL params for $groupsql
614 * @param string $groupwheresql Where conditions for $groupsql
615 * @return int The number of hidden grades
617 public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
619 $params = (array)$params;
620 $params['itemid'] = $this->id
;
622 return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
623 ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
627 * Mark regrading as finished successfully.
629 public function regrading_finished() {
631 $this->needsupdate
= 0;
632 //do not use $this->update() because we do not want this logged in grade_item_history
633 $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id
));
637 * Performs the necessary calculations on the grades_final referenced by this grade_item.
638 * Also resets the needsupdate flag once successfully performed.
640 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
641 * because the regrading must be done in correct order!!
643 * @param int $userid Supply a user ID to limit the regrading to a single user
644 * @return bool true if ok, error string otherwise
646 public function regrade_final_grades($userid=null) {
649 // locked grade items already have correct final grades
650 if ($this->is_locked()) {
654 // calculation produces final value using formula from other final values
655 if ($this->is_calculated()) {
656 if ($this->compute($userid)) {
659 return "Could not calculate grades for grade item"; // TODO: improve and localize
662 // noncalculated outcomes already have final values - raw grades not used
663 } else if ($this->is_outcome_item()) {
666 // aggregate the category grade
667 } else if ($this->is_category_item() or $this->is_course_item()) {
668 // aggregate category grade item
669 $category = $this->get_item_category();
670 $category->grade_item
=& $this;
671 if ($category->generate_grades($userid)) {
674 return "Could not aggregate final grades for category:".$this->id
; // TODO: improve and localize
677 } else if ($this->is_manual_item()) {
678 // manual items track only final grades, no raw grades
681 } else if (!$this->is_raw_used()) {
682 // hmm - raw grades are not used- nothing to regrade
686 // normal grade item - just new final grades
688 $grade_inst = new grade_grade();
689 $fields = implode(',', $grade_inst->required_fields
);
691 $params = array($this->id
, $userid);
692 $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
694 $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id
), '', $fields);
697 foreach ($rs as $grade_record) {
698 $grade = new grade_grade($grade_record, false);
700 if (!empty($grade_record->locked
) or !empty($grade_record->overridden
)) {
701 // this grade is locked - final grade must be ok
705 $grade->finalgrade
= $this->adjust_raw_grade($grade->rawgrade
, $grade->rawgrademin
, $grade->rawgrademax
);
707 if (grade_floats_different($grade_record->finalgrade
, $grade->finalgrade
)) {
708 if (!$grade->update('system')) {
709 $result = "Internal error updating final grade";
720 * Given a float grade value or integer grade scale, applies a number of adjustment based on
721 * grade_item variables and returns the result.
723 * @param float $rawgrade The raw grade value
724 * @param float $rawmin original rawmin
725 * @param float $rawmax original rawmax
728 public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
729 if (is_null($rawgrade)) {
733 if ($this->gradetype
== GRADE_TYPE_VALUE
) { // Dealing with numerical grade
735 if ($this->grademax
< $this->grademin
) {
739 if ($this->grademax
== $this->grademin
) {
740 return $this->grademax
; // no range
743 // Standardise score to the new grade range
744 // NOTE: this is not compatible with current assignment grading
745 $isassignmentmodule = ($this->itemmodule
== 'assignment') ||
($this->itemmodule
== 'assign');
746 if (!$isassignmentmodule && ($rawmin != $this->grademin
or $rawmax != $this->grademax
)) {
747 $rawgrade = grade_grade
::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin
, $this->grademax
);
750 // Apply other grade_item factors
751 $rawgrade *= $this->multfactor
;
752 $rawgrade +
= $this->plusfactor
;
754 return $this->bounded_grade($rawgrade);
756 } else if ($this->gradetype
== GRADE_TYPE_SCALE
) { // Dealing with a scale value
757 if (empty($this->scale
)) {
761 if ($this->grademax
< 0) {
762 return null; // scale not present - no grade
765 if ($this->grademax
== 0) {
766 return $this->grademax
; // only one option
769 // Convert scale if needed
770 // NOTE: this is not compatible with current assignment grading
771 if ($this->itemmodule
!= 'assignment' and ($rawmin != $this->grademin
or $rawmax != $this->grademax
)) {
772 $rawgrade = grade_grade
::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin
, $this->grademax
);
775 return $this->bounded_grade($rawgrade);
778 } else if ($this->gradetype
== GRADE_TYPE_TEXT
or $this->gradetype
== GRADE_TYPE_NONE
) { // no value
779 // somebody changed the grading type when grades already existed
783 debugging("Unknown grade type");
789 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
793 public function force_regrading() {
795 $this->needsupdate
= 1;
796 //mark this item and course item only - categories and calculated items are always regraded
797 $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
798 $params = array($this->id
, $this->courseid
);
799 $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
803 * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
805 * @return grade_scale Returns a grade_scale object or null if no scale used
807 public function load_scale() {
808 if ($this->gradetype
!= GRADE_TYPE_SCALE
) {
809 $this->scaleid
= null;
812 if (!empty($this->scaleid
)) {
813 //do not load scale if already present
814 if (empty($this->scale
->id
) or $this->scale
->id
!= $this->scaleid
) {
815 $this->scale
= grade_scale
::fetch(array('id'=>$this->scaleid
));
817 debugging('Incorrect scale id: '.$this->scaleid
);
821 $this->scale
->load_items();
824 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
825 // stay with the current min=1 max=count(scaleitems)
826 $this->grademax
= count($this->scale
->scale_items
);
837 * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
839 * @return grade_outcome This grade item's associated grade_outcome or null
841 public function load_outcome() {
842 if (!empty($this->outcomeid
)) {
843 $this->outcome
= grade_outcome
::fetch(array('id'=>$this->outcomeid
));
845 return $this->outcome
;
849 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
850 * or category attached to category item.
852 * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
854 public function get_parent_category() {
855 if ($this->is_category_item() or $this->is_course_item()) {
856 return $this->get_item_category();
859 return grade_category
::fetch(array('id'=>$this->categoryid
));
864 * Calls upon the get_parent_category method to retrieve the grade_category object
865 * from the DB and assigns it to $this->parent_category. It also returns the object.
867 * @return grade_category This grade item's parent grade_category.
869 public function load_parent_category() {
870 if (empty($this->parent_category
->id
)) {
871 $this->parent_category
= $this->get_parent_category();
873 return $this->parent_category
;
877 * Returns the grade_category for a grade category grade item
879 * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
881 public function get_item_category() {
882 if (!$this->is_course_item() and !$this->is_category_item()) {
885 return grade_category
::fetch(array('id'=>$this->iteminstance
));
889 * Calls upon the get_item_category method to retrieve the grade_category object
890 * from the DB and assigns it to $this->item_category. It also returns the object.
892 * @return grade_category
894 public function load_item_category() {
895 if (empty($this->item_category
->id
)) {
896 $this->item_category
= $this->get_item_category();
898 return $this->item_category
;
902 * Is the grade item associated with category?
906 public function is_category_item() {
907 return ($this->itemtype
== 'category');
911 * Is the grade item associated with course?
915 public function is_course_item() {
916 return ($this->itemtype
== 'course');
920 * Is this a manually graded item?
924 public function is_manual_item() {
925 return ($this->itemtype
== 'manual');
929 * Is this an outcome item?
933 public function is_outcome_item() {
934 return !empty($this->outcomeid
);
938 * Is the grade item external - associated with module, plugin or something else?
942 public function is_external_item() {
943 return ($this->itemtype
== 'mod');
947 * Is the grade item overridable
951 public function is_overridable_item() {
952 return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $this->is_course_item() or $this->is_category_item());
956 * Is the grade item feedback overridable
960 public function is_overridable_item_feedback() {
961 return !$this->is_outcome_item() and $this->is_external_item();
965 * Returns true if grade items uses raw grades
969 public function is_raw_used() {
970 return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
974 * Returns the grade item associated with the course
976 * @param int $courseid
977 * @return grade_item Course level grade item object
979 public static function fetch_course_item($courseid) {
980 if ($course_item = grade_item
::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
984 // first get category - it creates the associated grade item
985 $course_category = grade_category
::fetch_course_category($courseid);
986 return $course_category->get_grade_item();
990 * Is grading object editable?
994 public function is_editable() {
999 * Checks if grade calculated. Returns this object's calculation.
1001 * @return bool true if grade item calculated.
1003 public function is_calculated() {
1004 if (empty($this->calculation
)) {
1009 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1010 * we would have to fetch all course grade items to find out the ids.
1011 * Also if user changes the idnumber the formula does not need to be updated.
1014 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1015 if (!$this->calculation_normalized
and strpos($this->calculation
, '[[') !== false) {
1016 $this->set_calculation($this->calculation
);
1019 return !empty($this->calculation
);
1023 * Returns calculation string if grade calculated.
1025 * @return string Returns the grade item's calculation if calculation is used, null if not
1027 public function get_calculation() {
1028 if ($this->is_calculated()) {
1029 return grade_item
::denormalize_formula($this->calculation
, $this->courseid
);
1037 * Sets this item's calculation (creates it) if not yet set, or
1038 * updates it if already set (in the DB). If no calculation is given,
1039 * the calculation is removed.
1041 * @param string $formula string representation of formula used for calculation
1042 * @return bool success
1044 public function set_calculation($formula) {
1045 $this->calculation
= grade_item
::normalize_formula($formula, $this->courseid
);
1046 $this->calculation_normalized
= true;
1047 return $this->update();
1051 * Denormalizes the calculation formula to [idnumber] form
1053 * @param string $formula A string representation of the formula
1054 * @param int $courseid The course ID
1055 * @return string The denormalized formula as a string
1057 public static function denormalize_formula($formula, $courseid) {
1058 if (empty($formula)) {
1062 // denormalize formula - convert ##giXX## to [[idnumber]]
1063 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1064 foreach ($matches[1] as $id) {
1065 if ($grade_item = grade_item
::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1066 if (!empty($grade_item->idnumber
)) {
1067 $formula = str_replace('##gi'.$grade_item->id
.'##', '[['.$grade_item->idnumber
.']]', $formula);
1078 * Normalizes the calculation formula to [#giXX#] form
1080 * @param string $formula The formula
1081 * @param int $courseid The course ID
1082 * @return string The normalized formula as a string
1084 public static function normalize_formula($formula, $courseid) {
1085 $formula = trim($formula);
1087 if (empty($formula)) {
1092 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1093 if ($grade_items = grade_item
::fetch_all(array('courseid'=>$courseid))) {
1094 foreach ($grade_items as $grade_item) {
1095 $formula = str_replace('[['.$grade_item->idnumber
.']]', '##gi'.$grade_item->id
.'##', $formula);
1103 * Returns the final values for this grade item (as imported by module or other source).
1105 * @param int $userid Optional: to retrieve a single user's final grade
1106 * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1108 public function get_final($userid=NULL) {
1111 if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id
, 'userid' => $userid))) {
1116 if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id
))) {
1117 //TODO: speed up with better SQL (MDL-31380)
1119 foreach ($grades as $grade) {
1120 $result[$grade->userid
] = $grade;
1130 * Get (or create if not exist yet) grade for this user
1132 * @param int $userid The user ID
1133 * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1134 * @return grade_grade The grade_grade instance for the user for this grade item
1136 public function get_grade($userid, $create=true) {
1137 if (empty($this->id
)) {
1138 debugging('Can not use before insert');
1142 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id
));
1143 if (empty($grade->id
) and $create) {
1151 * Returns the sortorder of this grade_item. This method is also available in
1152 * grade_category, for cases where the object type is not know.
1154 * @return int Sort order
1156 public function get_sortorder() {
1157 return $this->sortorder
;
1161 * Returns the idnumber of this grade_item. This method is also available in
1162 * grade_category, for cases where the object type is not know.
1164 * @return string The grade item idnumber
1166 public function get_idnumber() {
1167 return $this->idnumber
;
1171 * Returns this grade_item. This method is also available in
1172 * grade_category, for cases where the object type is not know.
1174 * @return grade_item
1176 public function get_grade_item() {
1181 * Sets the sortorder of this grade_item. This method is also available in
1182 * grade_category, for cases where the object type is not know.
1184 * @param int $sortorder
1186 public function set_sortorder($sortorder) {
1187 if ($this->sortorder
== $sortorder) {
1190 $this->sortorder
= $sortorder;
1195 * Update this grade item's sortorder so that it will appear after $sortorder
1197 * @param int $sortorder The sort order to place this grade item after
1199 public function move_after_sortorder($sortorder) {
1202 //make some room first
1203 $params = array($sortorder, $this->courseid
);
1204 $sql = "UPDATE {grade_items}
1205 SET sortorder = sortorder + 1
1206 WHERE sortorder > ? AND courseid = ?";
1207 $DB->execute($sql, $params);
1209 $this->set_sortorder($sortorder +
1);
1213 * Returns the most descriptive field for this object.
1215 * Determines what type of grade item it is then returns the appropriate string
1217 * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1218 * @return string name
1220 public function get_name($fulltotal=false) {
1221 if (!empty($this->itemname
)) {
1223 return format_string($this->itemname
);
1225 } else if ($this->is_course_item()) {
1226 return get_string('coursetotal', 'grades');
1228 } else if ($this->is_category_item()) {
1230 $category = $this->load_parent_category();
1231 $a = new stdClass();
1232 $a->category
= $category->get_name();
1233 return get_string('categorytotalfull', 'grades', $a);
1235 return get_string('categorytotal', 'grades');
1239 return get_string('grade');
1244 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1246 * @param int $parentid The ID of the new parent
1247 * @return bool True if success
1249 public function set_parent($parentid) {
1250 if ($this->is_course_item() or $this->is_category_item()) {
1251 print_error('cannotsetparentforcatoritem');
1254 if ($this->categoryid
== $parentid) {
1258 // find parent and check course id
1259 if (!$parent_category = grade_category
::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid
))) {
1263 // MDL-19407 If moving from a non-SWM category to a SWM category, convert aggregationcoef to 0
1264 $currentparent = $this->load_parent_category();
1266 if ($currentparent->aggregation
!= GRADE_AGGREGATE_WEIGHTED_MEAN2
&& $parent_category->aggregation
== GRADE_AGGREGATE_WEIGHTED_MEAN2
) {
1267 $this->aggregationcoef
= 0;
1270 $this->force_regrading();
1273 $this->categoryid
= $parent_category->id
;
1274 $this->parent_category
=& $parent_category;
1276 return $this->update();
1280 * Makes sure value is a valid grade value.
1282 * @param float $gradevalue
1283 * @return mixed float or int fixed grade value
1285 public function bounded_grade($gradevalue) {
1288 if (is_null($gradevalue)) {
1292 if ($this->gradetype
== GRADE_TYPE_SCALE
) {
1293 // no >100% grades hack for scale grades!
1294 // 1.5 is rounded to 2 ;-)
1295 return (int)bounded_number($this->grademin
, round($gradevalue+
0.00001), $this->grademax
);
1298 $grademax = $this->grademax
;
1300 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1301 $maxcoef = isset($CFG->gradeoverhundredprocentmax
) ?
$CFG->gradeoverhundredprocentmax
: 10; // 1000% max by default
1303 if (!empty($CFG->unlimitedgrades
)) {
1304 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1305 $grademax = $grademax * $maxcoef;
1306 } else if ($this->is_category_item() or $this->is_course_item()) {
1307 $category = $this->load_item_category();
1308 if ($category->aggregation
>= 100) {
1310 $grademax = $grademax * $maxcoef;
1314 return (float)bounded_number($this->grademin
, $gradevalue, $grademax);
1318 * Finds out on which other items does this depend directly when doing calculation or category aggregation
1320 * @param bool $reset_cache
1321 * @return array of grade_item IDs this one depends on
1323 public function depends_on($reset_cache=false) {
1327 $this->dependson_cache
= null;
1328 } else if (isset($this->dependson_cache
)) {
1329 return $this->dependson_cache
;
1332 if ($this->is_locked()) {
1333 // locked items do not need to be regraded
1334 $this->dependson_cache
= array();
1335 return $this->dependson_cache
;
1338 if ($this->is_calculated()) {
1339 if (preg_match_all('/##gi(\d+)##/', $this->calculation
, $matches)) {
1340 $this->dependson_cache
= array_unique($matches[1]); // remove duplicates
1341 return $this->dependson_cache
;
1343 $this->dependson_cache
= array();
1344 return $this->dependson_cache
;
1347 } else if ($grade_category = $this->load_item_category()) {
1350 //only items with numeric or scale values can be aggregated
1351 if ($this->gradetype
!= GRADE_TYPE_VALUE
and $this->gradetype
!= GRADE_TYPE_SCALE
) {
1352 $this->dependson_cache
= array();
1353 return $this->dependson_cache
;
1356 $grade_category->apply_forced_settings();
1358 if (empty($CFG->enableoutcomes
) or $grade_category->aggregateoutcomes
) {
1361 $outcomes_sql = "AND gi.outcomeid IS NULL";
1364 if (empty($CFG->grade_includescalesinaggregation
)) {
1365 $gtypes = "gi.gradetype = ?";
1366 $params[] = GRADE_TYPE_VALUE
;
1368 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1369 $params[] = GRADE_TYPE_VALUE
;
1370 $params[] = GRADE_TYPE_SCALE
;
1373 if ($grade_category->aggregatesubcats
) {
1374 // return all children excluding category items
1375 $params[] = '%/' . $grade_category->id
. '/%';
1376 $sql = "SELECT gi.id
1377 FROM {grade_items} gi
1380 AND gi.categoryid IN (
1382 FROM {grade_categories} gc
1383 WHERE gc.path LIKE ?)";
1385 $params[] = $grade_category->id
;
1386 $params[] = $grade_category->id
;
1387 if (empty($CFG->grade_includescalesinaggregation
)) {
1388 $params[] = GRADE_TYPE_VALUE
;
1390 $params[] = GRADE_TYPE_VALUE
;
1391 $params[] = GRADE_TYPE_SCALE
;
1393 $sql = "SELECT gi.id
1394 FROM {grade_items} gi
1396 AND gi.categoryid = ?
1401 FROM {grade_items} gi, {grade_categories} gc
1402 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1408 if ($children = $DB->get_records_sql($sql, $params)) {
1409 $this->dependson_cache
= array_keys($children);
1410 return $this->dependson_cache
;
1412 $this->dependson_cache
= array();
1413 return $this->dependson_cache
;
1417 $this->dependson_cache
= array();
1418 return $this->dependson_cache
;
1423 * Refetch grades from modules, plugins.
1425 * @param int $userid optional, limit the refetch to a single user
1426 * @return bool Returns true on success or if there is nothing to do
1428 public function refresh_grades($userid=0) {
1430 if ($this->itemtype
== 'mod') {
1431 if ($this->is_outcome_item()) {
1436 if (!$activity = $DB->get_record($this->itemmodule
, array('id' => $this->iteminstance
))) {
1437 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1441 if (!$cm = get_coursemodule_from_instance($this->itemmodule
, $activity->id
, $this->courseid
)) {
1442 debugging('Can not find course module');
1446 $activity->modname
= $this->itemmodule
;
1447 $activity->cmidnumber
= $cm->idnumber
;
1449 return grade_update_mod_grades($activity, $userid);
1456 * Updates final grade value for given user, this is a only way to update final
1457 * grades from gradebook and import because it logs the change in history table
1458 * and deals with overridden flag. This flag is set to prevent later overriding
1459 * from raw grades submitted from modules.
1461 * @param int $userid The graded user
1462 * @param float|false $finalgrade The float value of final grade, false means do not change
1463 * @param string $source The modification source
1464 * @param string $feedback Optional teacher feedback
1465 * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1466 * @param int $usermodified The ID of the user making the modification
1467 * @return bool success
1469 public function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE
, $usermodified=null) {
1474 // no grading used or locked
1475 if ($this->gradetype
== GRADE_TYPE_NONE
or $this->is_locked()) {
1479 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$userid));
1480 $grade->grade_item
=& $this; // prevent db fetching of this grade_item
1482 if (empty($usermodified)) {
1483 $grade->usermodified
= $USER->id
;
1485 $grade->usermodified
= $usermodified;
1488 if ($grade->is_locked()) {
1489 // do not update locked grades at all
1493 $locktime = $grade->get_locktime();
1494 if ($locktime and $locktime < time()) {
1495 // do not update grades that should be already locked, force regrade instead
1496 $this->force_regrading();
1500 $oldgrade = new stdClass();
1501 $oldgrade->finalgrade
= $grade->finalgrade
;
1502 $oldgrade->overridden
= $grade->overridden
;
1503 $oldgrade->feedback
= $grade->feedback
;
1504 $oldgrade->feedbackformat
= $grade->feedbackformat
;
1506 // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1507 $grade->rawgrademin
= $this->grademin
;
1508 $grade->rawgrademax
= $this->grademax
;
1509 $grade->rawscaleid
= $this->scaleid
;
1512 if ($finalgrade !== false) {
1513 if ($this->is_overridable_item()) {
1514 $grade->overridden
= time();
1517 $grade->finalgrade
= $this->bounded_grade($finalgrade);
1520 // do we have comment from teacher?
1521 if ($feedback !== false) {
1522 if ($this->is_overridable_item_feedback()) {
1523 // external items (modules, plugins) may have own feedback
1524 $grade->overridden
= time();
1527 $grade->feedback
= $feedback;
1528 $grade->feedbackformat
= $feedbackformat;
1531 if (empty($grade->id
)) {
1532 $grade->timecreated
= null; // hack alert - date submitted - no submission yet
1533 $grade->timemodified
= time(); // hack alert - date graded
1534 $result = (bool)$grade->insert($source);
1536 } else if (grade_floats_different($grade->finalgrade
, $oldgrade->finalgrade
)
1537 or $grade->feedback
!== $oldgrade->feedback
1538 or $grade->feedbackformat
!= $oldgrade->feedbackformat
1539 or ($oldgrade->overridden
== 0 and $grade->overridden
> 0)) {
1540 $grade->timemodified
= time(); // hack alert - date graded
1541 $result = $grade->update($source);
1548 // something went wrong - better force final grade recalculation
1549 $this->force_regrading();
1551 } else if ($this->is_course_item() and !$this->needsupdate
) {
1552 if (grade_regrade_final_grades($this->courseid
, $userid, $this) !== true) {
1553 $this->force_regrading();
1556 } else if (!$this->needsupdate
) {
1557 $course_item = grade_item
::fetch_course_item($this->courseid
);
1558 if (!$course_item->needsupdate
) {
1559 if (grade_regrade_final_grades($this->courseid
, $userid, $this) !== true) {
1560 $this->force_regrading();
1563 $this->force_regrading();
1572 * Updates raw grade value for given user, this is a only way to update raw
1573 * grades from external source (modules, etc.),
1574 * because it logs the change in history table and deals with final grade recalculation.
1576 * @param int $userid the graded user
1577 * @param mixed $rawgrade float value of raw grade - false means do not change
1578 * @param string $source modification source
1579 * @param string $feedback optional teacher feedback
1580 * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1581 * @param int $usermodified the ID of the user who did the grading
1582 * @param int $dategraded A timestamp of when the student's work was graded
1583 * @param int $datesubmitted A timestamp of when the student's work was submitted
1584 * @param grade_grade $grade A grade object, useful for bulk upgrades
1585 * @return bool success
1587 public function update_raw_grade($userid, $rawgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE
, $usermodified=null, $dategraded=null, $datesubmitted=null, $grade=null) {
1592 // calculated grades can not be updated; course and category can not be updated because they are aggregated
1593 if (!$this->is_raw_used() or $this->gradetype
== GRADE_TYPE_NONE
or $this->is_locked()) {
1597 if (is_null($grade)) {
1599 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$userid));
1601 $grade->grade_item
=& $this; // prevent db fetching of this grade_item
1603 if (empty($usermodified)) {
1604 $grade->usermodified
= $USER->id
;
1606 $grade->usermodified
= $usermodified;
1609 if ($grade->is_locked()) {
1610 // do not update locked grades at all
1614 $locktime = $grade->get_locktime();
1615 if ($locktime and $locktime < time()) {
1616 // do not update grades that should be already locked and force regrade
1617 $this->force_regrading();
1621 $oldgrade = new stdClass();
1622 $oldgrade->finalgrade
= $grade->finalgrade
;
1623 $oldgrade->rawgrade
= $grade->rawgrade
;
1624 $oldgrade->rawgrademin
= $grade->rawgrademin
;
1625 $oldgrade->rawgrademax
= $grade->rawgrademax
;
1626 $oldgrade->rawscaleid
= $grade->rawscaleid
;
1627 $oldgrade->feedback
= $grade->feedback
;
1628 $oldgrade->feedbackformat
= $grade->feedbackformat
;
1630 // use new min and max
1631 $grade->rawgrade
= $grade->rawgrade
;
1632 $grade->rawgrademin
= $this->grademin
;
1633 $grade->rawgrademax
= $this->grademax
;
1634 $grade->rawscaleid
= $this->scaleid
;
1636 // change raw grade?
1637 if ($rawgrade !== false) {
1638 $grade->rawgrade
= $rawgrade;
1641 // empty feedback means no feedback at all
1642 if ($feedback === '') {
1646 // do we have comment from teacher?
1647 if ($feedback !== false and !$grade->is_overridden()) {
1648 $grade->feedback
= $feedback;
1649 $grade->feedbackformat
= $feedbackformat;
1652 // update final grade if possible
1653 if (!$grade->is_locked() and !$grade->is_overridden()) {
1654 $grade->finalgrade
= $this->adjust_raw_grade($grade->rawgrade
, $grade->rawgrademin
, $grade->rawgrademax
);
1657 // TODO: hack alert - create new fields for these in 2.0
1658 $oldgrade->timecreated
= $grade->timecreated
;
1659 $oldgrade->timemodified
= $grade->timemodified
;
1661 $grade->timecreated
= $datesubmitted;
1663 if ($grade->is_overridden()) {
1664 // keep original graded date - update_final_grade() sets this for overridden grades
1666 } else if (is_null($grade->rawgrade
) and is_null($grade->feedback
)) {
1667 // no grade and feedback means no grading yet
1668 $grade->timemodified
= null;
1670 } else if (!empty($dategraded)) {
1671 // fine - module sends info when graded (yay!)
1672 $grade->timemodified
= $dategraded;
1674 } else if (grade_floats_different($grade->finalgrade
, $oldgrade->finalgrade
)
1675 or $grade->feedback
!== $oldgrade->feedback
) {
1676 // guess - if either grade or feedback changed set new graded date
1677 $grade->timemodified
= time();
1680 //keep original graded date
1682 // end of hack alert
1684 if (empty($grade->id
)) {
1685 $result = (bool)$grade->insert($source);
1687 } else if (grade_floats_different($grade->finalgrade
, $oldgrade->finalgrade
)
1688 or grade_floats_different($grade->rawgrade
, $oldgrade->rawgrade
)
1689 or grade_floats_different($grade->rawgrademin
, $oldgrade->rawgrademin
)
1690 or grade_floats_different($grade->rawgrademax
, $oldgrade->rawgrademax
)
1691 or $grade->rawscaleid
!= $oldgrade->rawscaleid
1692 or $grade->feedback
!== $oldgrade->feedback
1693 or $grade->feedbackformat
!= $oldgrade->feedbackformat
1694 or $grade->timecreated
!= $oldgrade->timecreated
// part of hack above
1695 or $grade->timemodified
!= $oldgrade->timemodified
// part of hack above
1697 $result = $grade->update($source);
1703 // something went wrong - better force final grade recalculation
1704 $this->force_regrading();
1706 } else if (!$this->needsupdate
) {
1707 $course_item = grade_item
::fetch_course_item($this->courseid
);
1708 if (!$course_item->needsupdate
) {
1709 if (grade_regrade_final_grades($this->courseid
, $userid, $this) !== true) {
1710 $this->force_regrading();
1719 * Calculates final grade values using the formula in the calculation property.
1720 * The parameters are taken from final grades of grade items in current course only.
1722 * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
1723 * @return bool false if error
1725 public function compute($userid=null) {
1728 if (!$this->is_calculated()) {
1732 require_once($CFG->libdir
.'/mathslib.php');
1734 if ($this->is_locked()) {
1735 return true; // no need to recalculate locked items
1738 // Precreate grades - we need them to exist
1741 if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id
, 'userid'=>$userid))) {
1742 $m = new stdClass();
1743 $m->userid
= $userid;
1747 // Find any users who have grades for some but not all grade items in this course
1748 $params = array('gicourseid' => $this->courseid
, 'ggitemid' => $this->id
);
1749 $sql = "SELECT gg.userid
1750 FROM {grade_grades} gg
1751 JOIN {grade_items} gi
1752 ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
1754 HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
1755 $missing = $DB->get_records_sql($sql, $params);
1759 foreach ($missing as $m) {
1760 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$m->userid
), false);
1761 $grade->grade_item
=& $this;
1762 $grade->insert('system');
1767 $useditems = $this->depends_on();
1769 // prepare formula and init maths library
1770 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation
);
1771 if (strpos($formula, '[[') !== false) {
1775 $this->formula
= new calc_formula($formula);
1777 // where to look for final grades?
1778 // this itemid is added so that we use only one query for source and final grades
1779 $gis = array_merge($useditems, array($this->id
));
1780 list($usql, $params) = $DB->get_in_or_equal($gis);
1783 $usersql = "AND g.userid=?";
1784 $params[] = $userid;
1789 $grade_inst = new grade_grade();
1790 $fields = 'g.'.implode(',g.', $grade_inst->required_fields
);
1792 $params[] = $this->courseid
;
1793 $sql = "SELECT $fields
1794 FROM {grade_grades} g, {grade_items} gi
1795 WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
1800 // group the grades by userid and use formula on the group
1801 $rs = $DB->get_recordset_sql($sql, $params);
1804 $grade_records = array();
1806 foreach ($rs as $used) {
1807 if ($used->userid
!= $prevuser) {
1808 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1811 $prevuser = $used->userid
;
1812 $grade_records = array();
1815 if ($used->itemid
== $this->id
) {
1818 $grade_records['gi'.$used->itemid
] = $used->finalgrade
;
1820 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1830 * Internal function that does the final grade calculation
1832 * @param int $userid The user ID
1833 * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
1834 * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
1835 * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
1836 * @return bool False if an error occurred
1838 public function use_formula($userid, $params, $useditems, $oldgrade) {
1839 if (empty($userid)) {
1843 // add missing final grade values
1844 // not graded (null) is counted as 0 - the spreadsheet way
1845 $allinputsnull = true;
1846 foreach($useditems as $gi) {
1847 if (!array_key_exists('gi'.$gi, $params) ||
is_null($params['gi'.$gi])) {
1848 $params['gi'.$gi] = 0;
1850 $params['gi'.$gi] = (float)$params['gi'.$gi];
1851 if ($gi != $this->id
) {
1852 $allinputsnull = false;
1857 // can not use own final grade during calculation
1858 unset($params['gi'.$this->id
]);
1860 // insert final grade - will be needed later anyway
1862 $oldfinalgrade = $oldgrade->finalgrade
;
1863 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
1864 $grade->grade_item
=& $this;
1867 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$userid), false);
1868 $grade->grade_item
=& $this;
1869 $grade->insert('system');
1870 $oldfinalgrade = null;
1873 // no need to recalculate locked or overridden grades
1874 if ($grade->is_locked() or $grade->is_overridden()) {
1878 if ($allinputsnull) {
1879 $grade->finalgrade
= null;
1884 // do the calculation
1885 $this->formula
->set_params($params);
1886 $result = $this->formula
->evaluate();
1888 if ($result === false) {
1889 $grade->finalgrade
= null;
1893 $grade->finalgrade
= $this->bounded_grade($result);
1898 // update in db if changed
1899 if (grade_floats_different($grade->finalgrade
, $oldfinalgrade)) {
1900 $grade->timemodified
= time();
1901 $grade->update('compute');
1904 if ($result !== false) {
1905 //lock grade if needed
1908 if ($result === false) {
1917 * Validate the formula.
1919 * @param string $formulastr
1920 * @return bool true if calculation possible, false otherwise
1922 public function validate_formula($formulastr) {
1924 require_once($CFG->libdir
.'/mathslib.php');
1926 $formulastr = grade_item
::normalize_formula($formulastr, $this->courseid
);
1928 if (empty($formulastr)) {
1932 if (strpos($formulastr, '=') !== 0) {
1933 return get_string('errorcalculationnoequal', 'grades');
1937 if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
1938 $useditems = array_unique($matches[1]); // remove duplicates
1940 $useditems = array();
1944 // unset the value if formula is trying to reference to itself
1945 // but array keys does not match itemid
1946 if (!empty($this->id
)) {
1947 $useditems = array_diff($useditems, array($this->id
));
1948 //unset($useditems[$this->id]);
1951 // prepare formula and init maths library
1952 $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
1953 $formula = new calc_formula($formula);
1956 if (empty($useditems)) {
1957 $grade_items = array();
1960 list($usql, $params) = $DB->get_in_or_equal($useditems);
1961 $params[] = $this->courseid
;
1963 FROM {grade_items} gi
1964 WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
1966 if (!$grade_items = $DB->get_records_sql($sql, $params)) {
1967 $grade_items = array();
1972 foreach ($useditems as $itemid) {
1973 // make sure all grade items exist in this course
1974 if (!array_key_exists($itemid, $grade_items)) {
1977 // use max grade when testing formula, this should be ok in 99.9%
1978 // division by 0 is one of possible problems
1979 $params['gi'.$grade_items[$itemid]->id
] = $grade_items[$itemid]->grademax
;
1982 // do the calculation
1983 $formula->set_params($params);
1984 $result = $formula->evaluate();
1986 // false as result indicates some problem
1987 if ($result === false) {
1988 // TODO: add more error hints
1989 return get_string('errorcalculationunknown', 'grades');
1996 * Returns the value of the display type
1998 * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2000 * @return int Display type
2002 public function get_displaytype() {
2005 if ($this->display
== GRADE_DISPLAY_TYPE_DEFAULT
) {
2006 return grade_get_setting($this->courseid
, 'displaytype', $CFG->grade_displaytype
);
2009 return $this->display
;
2014 * Returns the value of the decimals field
2016 * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2018 * @return int Decimals (0 - 5)
2020 public function get_decimals() {
2023 if (is_null($this->decimals
)) {
2024 return grade_get_setting($this->courseid
, 'decimalpoints', $CFG->grade_decimalpoints
);
2027 return $this->decimals
;
2032 * Returns a string representing the range of grademin - grademax for this grade item.
2034 * @param int $rangesdisplaytype
2035 * @param int $rangesdecimalpoints
2038 function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2042 // Determine which display type to use for this average
2043 if (isset($USER->gradeediting
) && array_key_exists($this->courseid
, $USER->gradeediting
) && $USER->gradeediting
[$this->courseid
]) {
2044 $displaytype = GRADE_DISPLAY_TYPE_REAL
;
2046 } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT
) { // no ==0 here, please resave report and user prefs
2047 $displaytype = $this->get_displaytype();
2050 $displaytype = $rangesdisplaytype;
2053 // Override grade_item setting if a display preference (not default) was set for the averages
2054 if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT
) {
2055 $decimalpoints = $this->get_decimals();
2058 $decimalpoints = $rangesdecimalpoints;
2061 if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE
) {
2063 $grademax = "100 %";
2066 $grademin = grade_format_gradevalue($this->grademin
, $this, true, $displaytype, $decimalpoints);
2067 $grademax = grade_format_gradevalue($this->grademax
, $this, true, $displaytype, $decimalpoints);
2070 return $grademin.'–'. $grademax;
2074 * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2076 * @return string|false Returns the coefficient string of false is no coefficient is being used
2078 public function get_coefstring() {
2079 $parent_category = $this->load_parent_category();
2080 if ($this->is_category_item()) {
2081 $parent_category = $parent_category->load_parent_category();
2084 if ($parent_category->is_aggregationcoef_used()) {
2085 return $parent_category->get_coefstring();
2092 * Returns whether the grade item can control the visibility of the grades
2096 public function can_control_visibility() {
2097 if (get_plugin_directory($this->itemtype
, $this->itemmodule
)) {
2098 return !plugin_supports($this->itemtype
, $this->itemmodule
, FEATURE_CONTROLS_GRADE_VISIBILITY
, false);