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 'aggregationcoef2', 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime',
54 'needsupdate', 'weightoverride', 'timecreated', 'timemodified');
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 or extra credit
203 * @var float $aggregationcoef
205 public $aggregationcoef = 0;
208 * Aggregation coeficient used for weighted averages only
209 * @var float $aggregationcoef2
211 public $aggregationcoef2 = 0;
214 * Sorting order of the columns.
215 * @var int $sortorder
217 public $sortorder = 0;
220 * Display type of the grades (Real, Percentage, Letter, or default).
223 public $display = GRADE_DISPLAY_TYPE_DEFAULT
;
226 * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.
229 public $decimals = null;
232 * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
238 * Date after which the grade will be locked. Empty means no automatic locking.
241 public $locktime = 0;
244 * If set, the whole column will be recalculated, then this flag will be switched off.
245 * @var bool $needsupdate
247 public $needsupdate = 1;
250 * If set, the grade item's weight has been overridden by a user and should not be automatically adjusted.
252 public $weightoverride = 0;
255 * Cached dependson array
256 * @var array An array of cached grade item dependencies.
258 public $dependson_cache = null;
261 * Constructor. Optionally (and by default) attempts to fetch corresponding row from the database
263 * @param array $params An array with required parameters for this grade object.
264 * @param bool $fetch Whether to fetch corresponding row from the database or not,
265 * optional fields might not be defined if false used
267 public function __construct($params = null, $fetch = true) {
269 // Set grademax from $CFG->gradepointdefault .
270 self
::set_properties($this, array('grademax' => $CFG->gradepointdefault
));
271 parent
::__construct($params, $fetch);
275 * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
276 * Force regrading if necessary, rounds the float numbers using php function,
277 * the reason is we need to compare the db value with computed number to skip regrading if possible.
279 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
280 * @return bool success
282 public function update($source=null) {
284 $this->dependson_cache
= null;
286 // Retrieve scale and infer grademax/min from it if needed
289 // make sure there is not 0 in outcomeid
290 if (empty($this->outcomeid
)) {
291 $this->outcomeid
= null;
294 if ($this->qualifies_for_regrading()) {
295 $this->force_regrading();
298 $this->timemodified
= time();
300 $this->grademin
= grade_floatval($this->grademin
);
301 $this->grademax
= grade_floatval($this->grademax
);
302 $this->multfactor
= grade_floatval($this->multfactor
);
303 $this->plusfactor
= grade_floatval($this->plusfactor
);
304 $this->aggregationcoef
= grade_floatval($this->aggregationcoef
);
305 $this->aggregationcoef2
= grade_floatval($this->aggregationcoef2
);
307 return parent
::update($source);
311 * Compares the values held by this object with those of the matching record in DB, and returns
312 * whether or not these differences are sufficient to justify an update of all parent objects.
313 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
317 public function qualifies_for_regrading() {
318 if (empty($this->id
)) {
322 $db_item = new grade_item(array('id' => $this->id
));
324 $calculationdiff = $db_item->calculation
!= $this->calculation
;
325 $categorydiff = $db_item->categoryid
!= $this->categoryid
;
326 $gradetypediff = $db_item->gradetype
!= $this->gradetype
;
327 $scaleiddiff = $db_item->scaleid
!= $this->scaleid
;
328 $outcomeiddiff = $db_item->outcomeid
!= $this->outcomeid
;
329 $locktimediff = $db_item->locktime
!= $this->locktime
;
330 $grademindiff = grade_floats_different($db_item->grademin
, $this->grademin
);
331 $grademaxdiff = grade_floats_different($db_item->grademax
, $this->grademax
);
332 $multfactordiff = grade_floats_different($db_item->multfactor
, $this->multfactor
);
333 $plusfactordiff = grade_floats_different($db_item->plusfactor
, $this->plusfactor
);
334 $acoefdiff = grade_floats_different($db_item->aggregationcoef
, $this->aggregationcoef
);
335 $acoefdiff2 = grade_floats_different($db_item->aggregationcoef2
, $this->aggregationcoef2
);
336 $weightoverride = grade_floats_different($db_item->weightoverride
, $this->weightoverride
);
338 $needsupdatediff = !$db_item->needsupdate
&& $this->needsupdate
; // force regrading only if setting the flag first time
339 $lockeddiff = !empty($db_item->locked
) && empty($this->locked
); // force regrading only when unlocking
341 return ($calculationdiff ||
$categorydiff ||
$gradetypediff ||
$grademaxdiff ||
$grademindiff ||
$scaleiddiff
342 ||
$outcomeiddiff ||
$multfactordiff ||
$plusfactordiff ||
$needsupdatediff
343 ||
$lockeddiff ||
$acoefdiff ||
$acoefdiff2 ||
$weightoverride ||
$locktimediff);
347 * Finds and returns a grade_item instance based on params.
350 * @param array $params associative arrays varname=>value
351 * @return grade_item|bool Returns a grade_item instance or false if none found
353 public static function fetch($params) {
354 return grade_object
::fetch_helper('grade_items', 'grade_item', $params);
358 * Check to see if there are any existing grades for this grade_item.
360 * @return boolean - true if there are valid grades for this grade_item.
362 public function has_grades() {
365 $count = $DB->count_records_select('grade_grades',
366 'itemid = :gradeitemid AND finalgrade IS NOT NULL',
367 array('gradeitemid' => $this->id
));
372 * Check to see if there are existing overridden grades for this grade_item.
374 * @return boolean - true if there are overridden grades for this grade_item.
376 public function has_overridden_grades() {
379 $count = $DB->count_records_select('grade_grades',
380 'itemid = :gradeitemid AND finalgrade IS NOT NULL AND overridden > 0',
381 array('gradeitemid' => $this->id
));
386 * Finds and returns all grade_item instances based on params.
389 * @param array $params associative arrays varname=>value
390 * @return array array of grade_item instances or false if none found.
392 public static function fetch_all($params) {
393 return grade_object
::fetch_all_helper('grade_items', 'grade_item', $params);
397 * Delete all grades and force_regrading of parent category.
399 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
400 * @return bool success
402 public function delete($source=null) {
403 $this->delete_all_grades($source);
404 return parent
::delete($source);
410 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
413 public function delete_all_grades($source=null) {
414 if (!$this->is_course_item()) {
415 $this->force_regrading();
418 if ($grades = grade_grade
::fetch_all(array('itemid'=>$this->id
))) {
419 foreach ($grades as $grade) {
420 $grade->delete($source);
428 * In addition to perform parent::insert(), calls force_regrading() method too.
430 * @param string $source From where was the object inserted (mod/forum, manual, etc.)
431 * @return int PK ID if successful, false otherwise
433 public function insert($source=null) {
436 if (empty($this->courseid
)) {
437 print_error('cannotinsertgrade');
440 // load scale if needed
443 // add parent category if needed
444 if (empty($this->categoryid
) and !$this->is_course_item() and !$this->is_category_item()) {
445 $course_category = grade_category
::fetch_course_category($this->courseid
);
446 $this->categoryid
= $course_category->id
;
450 // always place the new items at the end, move them after insert if needed
451 $last_sortorder = $DB->get_field_select('grade_items', 'MAX(sortorder)', "courseid = ?", array($this->courseid
));
452 if (!empty($last_sortorder)) {
453 $this->sortorder
= $last_sortorder +
1;
455 $this->sortorder
= 1;
458 // add proper item numbers to manual items
459 if ($this->itemtype
== 'manual') {
460 if (empty($this->itemnumber
)) {
461 $this->itemnumber
= 0;
465 // make sure there is not 0 in outcomeid
466 if (empty($this->outcomeid
)) {
467 $this->outcomeid
= null;
470 $this->timecreated
= $this->timemodified
= time();
472 if (parent
::insert($source)) {
473 // force regrading of items if needed
474 $this->force_regrading();
478 debugging("Could not insert this grade_item in the database!");
484 * Set idnumber of grade item, updates also course_modules table
486 * @param string $idnumber (without magic quotes)
487 * @return bool success
489 public function add_idnumber($idnumber) {
491 if (!empty($this->idnumber
)) {
495 if ($this->itemtype
== 'mod' and !$this->is_outcome_item()) {
496 if ($this->itemnumber
== 0) {
497 // for activity modules, itemnumber 0 is synced with the course_modules
498 if (!$cm = get_coursemodule_from_instance($this->itemmodule
, $this->iteminstance
, $this->courseid
)) {
501 if (!empty($cm->idnumber
)) {
504 $DB->set_field('course_modules', 'idnumber', $idnumber, array('id' => $cm->id
));
505 $this->idnumber
= $idnumber;
506 return $this->update();
508 $this->idnumber
= $idnumber;
509 return $this->update();
513 $this->idnumber
= $idnumber;
514 return $this->update();
519 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
520 * $userid is given) or the locked state of a specific grade within this item if a specific
521 * $userid is given and the grade_item is unlocked.
523 * @param int $userid The user's ID
524 * @return bool Locked state
526 public function is_locked($userid=NULL) {
529 // Override for any grade items belonging to activities which are in the process of being deleted.
530 require_once($CFG->dirroot
. '/course/lib.php');
531 if (course_module_instance_pending_deletion($this->courseid
, $this->itemmodule
, $this->iteminstance
)) {
535 if (!empty($this->locked
)) {
539 if (!empty($userid)) {
540 if ($grade = grade_grade
::fetch(array('itemid'=>$this->id
, 'userid'=>$userid))) {
541 $grade->grade_item
=& $this; // prevent db fetching of cached grade_item
542 return $grade->is_locked();
550 * Locks or unlocks this grade_item and (optionally) all its associated final grades.
552 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
553 * @param bool $cascade Lock/unlock child objects too
554 * @param bool $refresh Refresh grades when unlocking
555 * @return bool True if grade_item all grades updated, false if at least one update fails
557 public function set_locked($lockedstate, $cascade=false, $refresh=true) {
560 if ($this->needsupdate
) {
561 return false; // can not lock grade without first having final grade
564 $this->locked
= time();
568 $grades = $this->get_final();
569 foreach($grades as $g) {
570 $grade = new grade_grade($g, false);
571 $grade->grade_item
=& $this;
572 $grade->set_locked(1, null, false);
580 if (!empty($this->locked
) and $this->locktime
< time()) {
581 //we have to reset locktime or else it would lock up again
589 if ($grades = grade_grade
::fetch_all(array('itemid'=>$this->id
))) {
590 foreach($grades as $grade) {
591 $grade->grade_item
=& $this;
592 $grade->set_locked(0, null, false);
598 //refresh when unlocking
599 $this->refresh_grades();
607 * Lock the grade if needed. Make sure this is called only when final grades are valid
609 public function check_locktime() {
610 if (!empty($this->locked
)) {
611 return; // already locked
614 if ($this->locktime
and $this->locktime
< time()) {
615 $this->locked
= time();
616 $this->update('locktime');
621 * Set the locktime for this grade item.
623 * @param int $locktime timestamp for lock to activate
626 public function set_locktime($locktime) {
627 $this->locktime
= $locktime;
632 * Set the locktime for this grade item.
634 * @return int $locktime timestamp for lock to activate
636 public function get_locktime() {
637 return $this->locktime
;
641 * Set the hidden status of grade_item and all grades.
643 * 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
645 * @param int $hidden new hidden status
646 * @param bool $cascade apply to child objects too
648 public function set_hidden($hidden, $cascade=false) {
649 parent
::set_hidden($hidden, $cascade);
652 if ($grades = grade_grade
::fetch_all(array('itemid'=>$this->id
))) {
653 foreach($grades as $grade) {
654 $grade->grade_item
=& $this;
655 $grade->set_hidden($hidden, $cascade);
660 //if marking item visible make sure category is visible MDL-21367
662 $category_array = grade_category
::fetch_all(array('id'=>$this->categoryid
));
663 if ($category_array && array_key_exists($this->categoryid
, $category_array)) {
664 $category = $category_array[$this->categoryid
];
665 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
666 //if($category->is_hidden()) {
667 $category->set_hidden($hidden, false);
674 * Returns the number of grades that are hidden
676 * @param string $groupsql SQL to limit the query by group
677 * @param array $params SQL params for $groupsql
678 * @param string $groupwheresql Where conditions for $groupsql
679 * @return int The number of hidden grades
681 public function has_hidden_grades($groupsql="", array $params=null, $groupwheresql="") {
683 $params = (array)$params;
684 $params['itemid'] = $this->id
;
686 return $DB->get_field_sql("SELECT COUNT(*) FROM {grade_grades} g LEFT JOIN "
687 ."{user} u ON g.userid = u.id $groupsql WHERE itemid = :itemid AND hidden = 1 $groupwheresql", $params);
691 * Mark regrading as finished successfully. This will also be called when subsequent regrading will not change any grades.
692 * Situations such as an error being found will still result in the regrading being finished.
694 public function regrading_finished() {
696 $this->needsupdate
= 0;
697 //do not use $this->update() because we do not want this logged in grade_item_history
698 $DB->set_field('grade_items', 'needsupdate', 0, array('id' => $this->id
));
702 * Performs the necessary calculations on the grades_final referenced by this grade_item.
703 * Also resets the needsupdate flag once successfully performed.
705 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
706 * because the regrading must be done in correct order!!
708 * @param int $userid Supply a user ID to limit the regrading to a single user
709 * @return bool true if ok, error string otherwise
711 public function regrade_final_grades($userid=null) {
714 // locked grade items already have correct final grades
715 if ($this->is_locked()) {
719 // calculation produces final value using formula from other final values
720 if ($this->is_calculated()) {
721 if ($this->compute($userid)) {
724 return "Could not calculate grades for grade item"; // TODO: improve and localize
727 // noncalculated outcomes already have final values - raw grades not used
728 } else if ($this->is_outcome_item()) {
731 // aggregate the category grade
732 } else if ($this->is_category_item() or $this->is_course_item()) {
733 // aggregate category grade item
734 $category = $this->load_item_category();
735 $category->grade_item
=& $this;
736 if ($category->generate_grades($userid)) {
739 return "Could not aggregate final grades for category:".$this->id
; // TODO: improve and localize
742 } else if ($this->is_manual_item()) {
743 // manual items track only final grades, no raw grades
746 } else if (!$this->is_raw_used()) {
747 // hmm - raw grades are not used- nothing to regrade
751 // normal grade item - just new final grades
753 $grade_inst = new grade_grade();
754 $fields = implode(',', $grade_inst->required_fields
);
756 $params = array($this->id
, $userid);
757 $rs = $DB->get_recordset_select('grade_grades', "itemid=? AND userid=?", $params, '', $fields);
759 $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id
), '', $fields);
762 foreach ($rs as $grade_record) {
763 $grade = new grade_grade($grade_record, false);
765 if (!empty($grade_record->locked
) or !empty($grade_record->overridden
)) {
766 // this grade is locked - final grade must be ok
770 $grade->finalgrade
= $this->adjust_raw_grade($grade->rawgrade
, $grade->rawgrademin
, $grade->rawgrademax
);
772 if (grade_floats_different($grade_record->finalgrade
, $grade->finalgrade
)) {
773 $success = $grade->update('system');
775 // If successful trigger a user_graded event.
777 $grade->load_grade_item();
778 \core\event\user_graded
::create_from_grade($grade, \core\event\base
::USER_OTHER
)->trigger();
780 $result = "Internal error updating final grade";
791 * Given a float grade value or integer grade scale, applies a number of adjustment based on
792 * grade_item variables and returns the result.
794 * @param float $rawgrade The raw grade value
795 * @param float $rawmin original rawmin
796 * @param float $rawmax original rawmax
799 public function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {
800 if (is_null($rawgrade)) {
804 if ($this->gradetype
== GRADE_TYPE_VALUE
) { // Dealing with numerical grade
806 if ($this->grademax
< $this->grademin
) {
810 if ($this->grademax
== $this->grademin
) {
811 return $this->grademax
; // no range
814 // Standardise score to the new grade range
815 // NOTE: skip if the activity provides a manual rescaling option.
816 $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule
, 'rescale_activity_grades') !== false);
817 if (!$manuallyrescale && ($rawmin != $this->grademin
or $rawmax != $this->grademax
)) {
818 $rawgrade = grade_grade
::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin
, $this->grademax
);
821 // Apply other grade_item factors
822 $rawgrade *= $this->multfactor
;
823 $rawgrade +
= $this->plusfactor
;
825 return $this->bounded_grade($rawgrade);
827 } else if ($this->gradetype
== GRADE_TYPE_SCALE
) { // Dealing with a scale value
828 if (empty($this->scale
)) {
832 if ($this->grademax
< 0) {
833 return null; // scale not present - no grade
836 if ($this->grademax
== 0) {
837 return $this->grademax
; // only one option
840 // Convert scale if needed
841 // NOTE: skip if the activity provides a manual rescaling option.
842 $manuallyrescale = (component_callback_exists('mod_' . $this->itemmodule
, 'rescale_activity_grades') !== false);
843 if (!$manuallyrescale && ($rawmin != $this->grademin
or $rawmax != $this->grademax
)) {
844 // This should never happen because scales are locked if they are in use.
845 $rawgrade = grade_grade
::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin
, $this->grademax
);
848 return $this->bounded_grade($rawgrade);
851 } else if ($this->gradetype
== GRADE_TYPE_TEXT
or $this->gradetype
== GRADE_TYPE_NONE
) { // no value
852 // somebody changed the grading type when grades already existed
856 debugging("Unknown grade type");
862 * Update the rawgrademax and rawgrademin for all grade_grades records for this item.
863 * Scale every rawgrade to maintain the percentage. This function should be called
864 * after the gradeitem has been updated to the new min and max values.
866 * @param float $oldgrademin The previous grade min value
867 * @param float $oldgrademax The previous grade max value
868 * @param float $newgrademin The new grade min value
869 * @param float $newgrademax The new grade max value
870 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
871 * @return bool True on success
873 public function rescale_grades_keep_percentage($oldgrademin, $oldgrademax, $newgrademin, $newgrademax, $source = null) {
876 if (empty($this->id
)) {
880 if ($oldgrademax <= $oldgrademin) {
881 // Grades cannot be scaled.
884 $scale = ($newgrademax - $newgrademin) / ($oldgrademax - $oldgrademin);
885 if (($newgrademax - $newgrademin) <= 1) {
886 // We would lose too much precision, lets bail.
890 $rs = $DB->get_recordset('grade_grades', array('itemid' => $this->id
));
892 foreach ($rs as $graderecord) {
893 // For each record, create an object to work on.
894 $grade = new grade_grade($graderecord, false);
895 // Set this object in the item so it doesn't re-fetch it.
896 $grade->grade_item
= $this;
898 if (!$this->is_category_item() ||
($this->is_category_item() && $grade->is_overridden())) {
899 // Updating the raw grade automatically updates the min/max.
900 if ($this->is_raw_used()) {
901 $rawgrade = (($grade->rawgrade
- $oldgrademin) * $scale) +
$newgrademin;
902 $this->update_raw_grade(false, $rawgrade, $source, false, FORMAT_MOODLE
, null, null, null, $grade);
904 $finalgrade = (($grade->finalgrade
- $oldgrademin) * $scale) +
$newgrademin;
905 $this->update_final_grade($grade->userid
, $finalgrade, $source);
911 // Mark this item for regrading.
912 $this->force_regrading();
918 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
922 public function force_regrading() {
924 $this->needsupdate
= 1;
925 //mark this item and course item only - categories and calculated items are always regraded
926 $wheresql = "(itemtype='course' OR id=?) AND courseid=?";
927 $params = array($this->id
, $this->courseid
);
928 $DB->set_field_select('grade_items', 'needsupdate', 1, $wheresql, $params);
932 * Instantiates a grade_scale object from the DB if this item's scaleid variable is set
934 * @return grade_scale Returns a grade_scale object or null if no scale used
936 public function load_scale() {
937 if ($this->gradetype
!= GRADE_TYPE_SCALE
) {
938 $this->scaleid
= null;
941 if (!empty($this->scaleid
)) {
942 //do not load scale if already present
943 if (empty($this->scale
->id
) or $this->scale
->id
!= $this->scaleid
) {
944 $this->scale
= grade_scale
::fetch(array('id'=>$this->scaleid
));
946 debugging('Incorrect scale id: '.$this->scaleid
);
950 $this->scale
->load_items();
953 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
954 // stay with the current min=1 max=count(scaleitems)
955 $this->grademax
= count($this->scale
->scale_items
);
966 * Instantiates a grade_outcome object from the DB if this item's outcomeid variable is set
968 * @return grade_outcome This grade item's associated grade_outcome or null
970 public function load_outcome() {
971 if (!empty($this->outcomeid
)) {
972 $this->outcome
= grade_outcome
::fetch(array('id'=>$this->outcomeid
));
974 return $this->outcome
;
978 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
979 * or category attached to category item.
981 * @return grade_category|bool Returns a grade_category object if applicable or false if this is a course item
983 public function get_parent_category() {
984 if ($this->is_category_item() or $this->is_course_item()) {
985 return $this->get_item_category();
988 return grade_category
::fetch(array('id'=>$this->categoryid
));
993 * Calls upon the get_parent_category method to retrieve the grade_category object
994 * from the DB and assigns it to $this->parent_category. It also returns the object.
996 * @return grade_category This grade item's parent grade_category.
998 public function load_parent_category() {
999 if (empty($this->parent_category
->id
)) {
1000 $this->parent_category
= $this->get_parent_category();
1002 return $this->parent_category
;
1006 * Returns the grade_category for a grade category grade item
1008 * @return grade_category|bool Returns a grade_category instance if applicable or false otherwise
1010 public function get_item_category() {
1011 if (!$this->is_course_item() and !$this->is_category_item()) {
1014 return grade_category
::fetch(array('id'=>$this->iteminstance
));
1018 * Calls upon the get_item_category method to retrieve the grade_category object
1019 * from the DB and assigns it to $this->item_category. It also returns the object.
1021 * @return grade_category
1023 public function load_item_category() {
1024 if (empty($this->item_category
->id
)) {
1025 $this->item_category
= $this->get_item_category();
1027 return $this->item_category
;
1031 * Is the grade item associated with category?
1035 public function is_category_item() {
1036 return ($this->itemtype
== 'category');
1040 * Is the grade item associated with course?
1044 public function is_course_item() {
1045 return ($this->itemtype
== 'course');
1049 * Is this a manually graded item?
1053 public function is_manual_item() {
1054 return ($this->itemtype
== 'manual');
1058 * Is this an outcome item?
1062 public function is_outcome_item() {
1063 return !empty($this->outcomeid
);
1067 * Is the grade item external - associated with module, plugin or something else?
1071 public function is_external_item() {
1072 return ($this->itemtype
== 'mod');
1076 * Is the grade item overridable
1080 public function is_overridable_item() {
1081 if ($this->is_course_item() or $this->is_category_item()) {
1082 $overridable = (bool) get_config('moodle', 'grade_overridecat');
1084 $overridable = false;
1087 return !$this->is_outcome_item() and ($this->is_external_item() or $this->is_calculated() or $overridable);
1091 * Is the grade item feedback overridable
1095 public function is_overridable_item_feedback() {
1096 return !$this->is_outcome_item() and $this->is_external_item();
1100 * Returns true if grade items uses raw grades
1104 public function is_raw_used() {
1105 return ($this->is_external_item() and !$this->is_calculated() and !$this->is_outcome_item());
1109 * Returns true if the grade item is an aggreggated type grade.
1111 * @since Moodle 2.8.7, 2.9.1
1114 public function is_aggregate_item() {
1115 return ($this->is_category_item() ||
$this->is_course_item());
1119 * Returns the grade item associated with the course
1121 * @param int $courseid
1122 * @return grade_item Course level grade item object
1124 public static function fetch_course_item($courseid) {
1125 if ($course_item = grade_item
::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
1126 return $course_item;
1129 // first get category - it creates the associated grade item
1130 $course_category = grade_category
::fetch_course_category($courseid);
1131 return $course_category->get_grade_item();
1135 * Is grading object editable?
1139 public function is_editable() {
1144 * Checks if grade calculated. Returns this object's calculation.
1146 * @return bool true if grade item calculated.
1148 public function is_calculated() {
1149 if (empty($this->calculation
)) {
1154 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
1155 * we would have to fetch all course grade items to find out the ids.
1156 * Also if user changes the idnumber the formula does not need to be updated.
1159 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
1160 if (!$this->calculation_normalized
and strpos($this->calculation
, '[[') !== false) {
1161 $this->set_calculation($this->calculation
);
1164 return !empty($this->calculation
);
1168 * Returns calculation string if grade calculated.
1170 * @return string Returns the grade item's calculation if calculation is used, null if not
1172 public function get_calculation() {
1173 if ($this->is_calculated()) {
1174 return grade_item
::denormalize_formula($this->calculation
, $this->courseid
);
1182 * Sets this item's calculation (creates it) if not yet set, or
1183 * updates it if already set (in the DB). If no calculation is given,
1184 * the calculation is removed.
1186 * @param string $formula string representation of formula used for calculation
1187 * @return bool success
1189 public function set_calculation($formula) {
1190 $this->calculation
= grade_item
::normalize_formula($formula, $this->courseid
);
1191 $this->calculation_normalized
= true;
1192 return $this->update();
1196 * Denormalizes the calculation formula to [idnumber] form
1198 * @param string $formula A string representation of the formula
1199 * @param int $courseid The course ID
1200 * @return string The denormalized formula as a string
1202 public static function denormalize_formula($formula, $courseid) {
1203 if (empty($formula)) {
1207 // denormalize formula - convert ##giXX## to [[idnumber]]
1208 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
1209 foreach ($matches[1] as $id) {
1210 if ($grade_item = grade_item
::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
1211 if (!empty($grade_item->idnumber
)) {
1212 $formula = str_replace('##gi'.$grade_item->id
.'##', '[['.$grade_item->idnumber
.']]', $formula);
1223 * Normalizes the calculation formula to [#giXX#] form
1225 * @param string $formula The formula
1226 * @param int $courseid The course ID
1227 * @return string The normalized formula as a string
1229 public static function normalize_formula($formula, $courseid) {
1230 $formula = trim($formula);
1232 if (empty($formula)) {
1237 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
1238 if ($grade_items = grade_item
::fetch_all(array('courseid'=>$courseid))) {
1239 foreach ($grade_items as $grade_item) {
1240 $formula = str_replace('[['.$grade_item->idnumber
.']]', '##gi'.$grade_item->id
.'##', $formula);
1248 * Returns the final values for this grade item (as imported by module or other source).
1250 * @param int $userid Optional: to retrieve a single user's final grade
1251 * @return array|grade_grade An array of all grade_grade instances for this grade_item, or a single grade_grade instance.
1253 public function get_final($userid=NULL) {
1256 if ($user = $DB->get_record('grade_grades', array('itemid' => $this->id
, 'userid' => $userid))) {
1261 if ($grades = $DB->get_records('grade_grades', array('itemid' => $this->id
))) {
1262 //TODO: speed up with better SQL (MDL-31380)
1264 foreach ($grades as $grade) {
1265 $result[$grade->userid
] = $grade;
1275 * Get (or create if not exist yet) grade for this user
1277 * @param int $userid The user ID
1278 * @param bool $create If true and the user has no grade for this grade item a new grade_grade instance will be inserted
1279 * @return grade_grade The grade_grade instance for the user for this grade item
1281 public function get_grade($userid, $create=true) {
1282 if (empty($this->id
)) {
1283 debugging('Can not use before insert');
1287 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id
));
1288 if (empty($grade->id
) and $create) {
1296 * Returns the sortorder of this grade_item. This method is also available in
1297 * grade_category, for cases where the object type is not know.
1299 * @return int Sort order
1301 public function get_sortorder() {
1302 return $this->sortorder
;
1306 * Returns the idnumber of this grade_item. This method is also available in
1307 * grade_category, for cases where the object type is not know.
1309 * @return string The grade item idnumber
1311 public function get_idnumber() {
1312 return $this->idnumber
;
1316 * Returns this grade_item. This method is also available in
1317 * grade_category, for cases where the object type is not know.
1319 * @return grade_item
1321 public function get_grade_item() {
1326 * Sets the sortorder of this grade_item. This method is also available in
1327 * grade_category, for cases where the object type is not know.
1329 * @param int $sortorder
1331 public function set_sortorder($sortorder) {
1332 if ($this->sortorder
== $sortorder) {
1335 $this->sortorder
= $sortorder;
1340 * Update this grade item's sortorder so that it will appear after $sortorder
1342 * @param int $sortorder The sort order to place this grade item after
1344 public function move_after_sortorder($sortorder) {
1347 //make some room first
1348 $params = array($sortorder, $this->courseid
);
1349 $sql = "UPDATE {grade_items}
1350 SET sortorder = sortorder + 1
1351 WHERE sortorder > ? AND courseid = ?";
1352 $DB->execute($sql, $params);
1354 $this->set_sortorder($sortorder +
1);
1358 * Detect duplicate grade item's sortorder and re-sort them.
1359 * Note: Duplicate sortorder will be introduced while duplicating activities or
1360 * merging two courses.
1362 * @param int $courseid id of the course for which grade_items sortorder need to be fixed.
1364 public static function fix_duplicate_sortorder($courseid) {
1367 $transaction = $DB->start_delegated_transaction();
1369 $sql = "SELECT DISTINCT g1.id, g1.courseid, g1.sortorder
1370 FROM {grade_items} g1
1371 JOIN {grade_items} g2 ON g1.courseid = g2.courseid
1372 WHERE g1.sortorder = g2.sortorder AND g1.id != g2.id AND g1.courseid = :courseid
1373 ORDER BY g1.sortorder DESC, g1.id DESC";
1375 // Get all duplicates in course highest sort order, and higest id first so that we can make space at the
1376 // bottom higher end of the sort orders and work down by id.
1377 $rs = $DB->get_recordset_sql($sql, array('courseid' => $courseid));
1379 foreach($rs as $duplicate) {
1380 $DB->execute("UPDATE {grade_items}
1381 SET sortorder = sortorder + 1
1382 WHERE courseid = :courseid AND
1383 (sortorder > :sortorder OR (sortorder = :sortorder2 AND id > :id))",
1384 array('courseid' => $duplicate->courseid
,
1385 'sortorder' => $duplicate->sortorder
,
1386 'sortorder2' => $duplicate->sortorder
,
1387 'id' => $duplicate->id
));
1390 $transaction->allow_commit();
1394 * Returns the most descriptive field for this object.
1396 * Determines what type of grade item it is then returns the appropriate string
1398 * @param bool $fulltotal If the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
1399 * @return string name
1401 public function get_name($fulltotal=false) {
1403 require_once($CFG->dirroot
. '/course/lib.php');
1404 if (strval($this->itemname
) !== '') {
1407 // Make it obvious to users if the course module to which this grade item relates, is currently being removed.
1408 $deletionpending = course_module_instance_pending_deletion($this->courseid
, $this->itemmodule
, $this->iteminstance
);
1409 $deletionnotice = get_string('gradesmoduledeletionprefix', 'grades');
1411 $options = ['context' => context_course
::instance($this->courseid
)];
1412 return $deletionpending ?
1413 format_string($deletionnotice . ' ' . $this->itemname
, true, $options) :
1414 format_string($this->itemname
, true, $options);
1416 } else if ($this->is_course_item()) {
1417 return get_string('coursetotal', 'grades');
1419 } else if ($this->is_category_item()) {
1421 $category = $this->load_parent_category();
1422 $a = new stdClass();
1423 $a->category
= $category->get_name();
1424 return get_string('categorytotalfull', 'grades', $a);
1426 return get_string('categorytotal', 'grades');
1430 return get_string('grade');
1435 * A grade item can return a more detailed description which will be added to the header of the column/row in some reports.
1437 * @return string description
1439 public function get_description() {
1440 if ($this->is_course_item() ||
$this->is_category_item()) {
1441 $categoryitem = $this->load_item_category();
1442 return $categoryitem->get_description();
1448 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1450 * @param int $parentid The ID of the new parent
1451 * @param bool $updateaggregationfields Whether or not to convert the aggregation fields when switching between category.
1452 * Set this to false when the aggregation fields have been updated in prevision of the new
1453 * category, typically when the item is freshly created.
1454 * @return bool True if success
1456 public function set_parent($parentid, $updateaggregationfields = true) {
1457 if ($this->is_course_item() or $this->is_category_item()) {
1458 print_error('cannotsetparentforcatoritem');
1461 if ($this->categoryid
== $parentid) {
1465 // find parent and check course id
1466 if (!$parent_category = grade_category
::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid
))) {
1470 $currentparent = $this->load_parent_category();
1472 if ($updateaggregationfields) {
1473 $this->set_aggregation_fields_for_aggregation($currentparent->aggregation
, $parent_category->aggregation
);
1476 $this->force_regrading();
1479 $this->categoryid
= $parent_category->id
;
1480 $this->parent_category
=& $parent_category;
1482 return $this->update();
1486 * Update the aggregation fields when the aggregation changed.
1488 * This method should always be called when the aggregation has changed, but also when
1489 * the item was moved to another category, even it if uses the same aggregation method.
1491 * Some values such as the weight only make sense within a category, once moved the
1492 * values should be reset to let the user adapt them accordingly.
1494 * Note that this method does not save the grade item.
1495 * {@link grade_item::update()} has to be called manually after using this method.
1497 * @param int $from Aggregation method constant value.
1498 * @param int $to Aggregation method constant value.
1499 * @return boolean True when at least one field was changed, false otherwise
1501 public function set_aggregation_fields_for_aggregation($from, $to) {
1502 $defaults = grade_category
::get_default_aggregation_coefficient_values($to);
1504 $origaggregationcoef = $this->aggregationcoef
;
1505 $origaggregationcoef2 = $this->aggregationcoef2
;
1506 $origweighoverride = $this->weightoverride
;
1508 if ($from == GRADE_AGGREGATE_SUM
&& $to == GRADE_AGGREGATE_SUM
&& $this->weightoverride
) {
1509 // Do nothing. We are switching from SUM to SUM and the weight is overriden,
1510 // a teacher would not expect any change in this situation.
1512 } else if ($from == GRADE_AGGREGATE_WEIGHTED_MEAN
&& $to == GRADE_AGGREGATE_WEIGHTED_MEAN
) {
1513 // Do nothing. The weights can be kept in this case.
1515 } else if (in_array($from, array(GRADE_AGGREGATE_SUM
, GRADE_AGGREGATE_EXTRACREDIT_MEAN
, GRADE_AGGREGATE_WEIGHTED_MEAN2
))
1516 && in_array($to, array(GRADE_AGGREGATE_SUM
, GRADE_AGGREGATE_EXTRACREDIT_MEAN
, GRADE_AGGREGATE_WEIGHTED_MEAN2
))) {
1518 // Reset all but the the extra credit field.
1519 $this->aggregationcoef2
= $defaults['aggregationcoef2'];
1520 $this->weightoverride
= $defaults['weightoverride'];
1522 if ($to != GRADE_AGGREGATE_EXTRACREDIT_MEAN
) {
1523 // Normalise extra credit, except for 'Mean with extra credit' which supports higher values than 1.
1524 $this->aggregationcoef
= min(1, $this->aggregationcoef
);
1528 $this->aggregationcoef
= $defaults['aggregationcoef'];
1529 $this->aggregationcoef2
= $defaults['aggregationcoef2'];
1530 $this->weightoverride
= $defaults['weightoverride'];
1533 $acoefdiff = grade_floats_different($origaggregationcoef, $this->aggregationcoef
);
1534 $acoefdiff2 = grade_floats_different($origaggregationcoef2, $this->aggregationcoef2
);
1535 $weightoverride = grade_floats_different($origweighoverride, $this->weightoverride
);
1537 return $acoefdiff ||
$acoefdiff2 ||
$weightoverride;
1541 * Makes sure value is a valid grade value.
1543 * @param float $gradevalue
1544 * @return mixed float or int fixed grade value
1546 public function bounded_grade($gradevalue) {
1549 if (is_null($gradevalue)) {
1553 if ($this->gradetype
== GRADE_TYPE_SCALE
) {
1554 // no >100% grades hack for scale grades!
1555 // 1.5 is rounded to 2 ;-)
1556 return (int)bounded_number($this->grademin
, round($gradevalue+
0.00001), $this->grademax
);
1559 $grademax = $this->grademax
;
1561 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1562 $maxcoef = isset($CFG->gradeoverhundredprocentmax
) ?
$CFG->gradeoverhundredprocentmax
: 10; // 1000% max by default
1564 if (!empty($CFG->unlimitedgrades
)) {
1565 // NOTE: if you change this value you must manually reset the needsupdate flag in all grade items
1566 $grademax = $grademax * $maxcoef;
1567 } else if ($this->is_category_item() or $this->is_course_item()) {
1568 $category = $this->load_item_category();
1569 if ($category->aggregation
>= 100) {
1571 $grademax = $grademax * $maxcoef;
1575 return (float)bounded_number($this->grademin
, $gradevalue, $grademax);
1579 * Finds out on which other items does this depend directly when doing calculation or category aggregation
1581 * @param bool $reset_cache
1582 * @return array of grade_item IDs this one depends on
1584 public function depends_on($reset_cache=false) {
1588 $this->dependson_cache
= null;
1589 } else if (isset($this->dependson_cache
)) {
1590 return $this->dependson_cache
;
1593 if ($this->is_locked()) {
1594 // locked items do not need to be regraded
1595 $this->dependson_cache
= array();
1596 return $this->dependson_cache
;
1599 if ($this->is_calculated()) {
1600 if (preg_match_all('/##gi(\d+)##/', $this->calculation
, $matches)) {
1601 $this->dependson_cache
= array_unique($matches[1]); // remove duplicates
1602 return $this->dependson_cache
;
1604 $this->dependson_cache
= array();
1605 return $this->dependson_cache
;
1608 } else if ($grade_category = $this->load_item_category()) {
1611 //only items with numeric or scale values can be aggregated
1612 if ($this->gradetype
!= GRADE_TYPE_VALUE
and $this->gradetype
!= GRADE_TYPE_SCALE
) {
1613 $this->dependson_cache
= array();
1614 return $this->dependson_cache
;
1617 $grade_category->apply_forced_settings();
1619 if (empty($CFG->enableoutcomes
) or $grade_category->aggregateoutcomes
) {
1622 $outcomes_sql = "AND gi.outcomeid IS NULL";
1625 if (empty($CFG->grade_includescalesinaggregation
)) {
1626 $gtypes = "gi.gradetype = ?";
1627 $params[] = GRADE_TYPE_VALUE
;
1629 $gtypes = "(gi.gradetype = ? OR gi.gradetype = ?)";
1630 $params[] = GRADE_TYPE_VALUE
;
1631 $params[] = GRADE_TYPE_SCALE
;
1634 $params[] = $grade_category->id
;
1635 $params[] = $this->courseid
;
1636 $params[] = $grade_category->id
;
1637 $params[] = $this->courseid
;
1638 if (empty($CFG->grade_includescalesinaggregation
)) {
1639 $params[] = GRADE_TYPE_VALUE
;
1641 $params[] = GRADE_TYPE_VALUE
;
1642 $params[] = GRADE_TYPE_SCALE
;
1644 $sql = "SELECT gi.id
1645 FROM {grade_items} gi
1647 AND gi.categoryid = ?
1653 FROM {grade_items} gi, {grade_categories} gc
1654 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1660 if ($children = $DB->get_records_sql($sql, $params)) {
1661 $this->dependson_cache
= array_keys($children);
1662 return $this->dependson_cache
;
1664 $this->dependson_cache
= array();
1665 return $this->dependson_cache
;
1669 $this->dependson_cache
= array();
1670 return $this->dependson_cache
;
1675 * Refetch grades from modules, plugins.
1677 * @param int $userid optional, limit the refetch to a single user
1678 * @return bool Returns true on success or if there is nothing to do
1680 public function refresh_grades($userid=0) {
1682 if ($this->itemtype
== 'mod') {
1683 if ($this->is_outcome_item()) {
1688 if (!$activity = $DB->get_record($this->itemmodule
, array('id' => $this->iteminstance
))) {
1689 debugging("Can not find $this->itemmodule activity with id $this->iteminstance");
1693 if (!$cm = get_coursemodule_from_instance($this->itemmodule
, $activity->id
, $this->courseid
)) {
1694 debugging('Can not find course module');
1698 $activity->modname
= $this->itemmodule
;
1699 $activity->cmidnumber
= $cm->idnumber
;
1701 return grade_update_mod_grades($activity, $userid);
1708 * Updates final grade value for given user, this is a only way to update final
1709 * grades from gradebook and import because it logs the change in history table
1710 * and deals with overridden flag. This flag is set to prevent later overriding
1711 * from raw grades submitted from modules.
1713 * @param int $userid The graded user
1714 * @param float|false $finalgrade The float value of final grade, false means do not change
1715 * @param string $source The modification source
1716 * @param string $feedback Optional teacher feedback
1717 * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1718 * @param int $usermodified The ID of the user making the modification
1719 * @return bool success
1721 public function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE
, $usermodified=null) {
1726 // no grading used or locked
1727 if ($this->gradetype
== GRADE_TYPE_NONE
or $this->is_locked()) {
1731 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$userid));
1732 $grade->grade_item
=& $this; // prevent db fetching of this grade_item
1734 if (empty($usermodified)) {
1735 $grade->usermodified
= $USER->id
;
1737 $grade->usermodified
= $usermodified;
1740 if ($grade->is_locked()) {
1741 // do not update locked grades at all
1745 $locktime = $grade->get_locktime();
1746 if ($locktime and $locktime < time()) {
1747 // do not update grades that should be already locked, force regrade instead
1748 $this->force_regrading();
1752 $oldgrade = new stdClass();
1753 $oldgrade->finalgrade
= $grade->finalgrade
;
1754 $oldgrade->overridden
= $grade->overridden
;
1755 $oldgrade->feedback
= $grade->feedback
;
1756 $oldgrade->feedbackformat
= $grade->feedbackformat
;
1757 $oldgrade->rawgrademin
= $grade->rawgrademin
;
1758 $oldgrade->rawgrademax
= $grade->rawgrademax
;
1760 // MDL-31713 rawgramemin and max must be up to date so conditional access %'s works properly.
1761 $grade->rawgrademin
= $this->grademin
;
1762 $grade->rawgrademax
= $this->grademax
;
1763 $grade->rawscaleid
= $this->scaleid
;
1766 if ($finalgrade !== false) {
1767 if ($this->is_overridable_item()) {
1768 $grade->overridden
= time();
1771 $grade->finalgrade
= $this->bounded_grade($finalgrade);
1774 // do we have comment from teacher?
1775 if ($feedback !== false) {
1776 if ($this->is_overridable_item_feedback()) {
1777 // external items (modules, plugins) may have own feedback
1778 $grade->overridden
= time();
1781 $grade->feedback
= $feedback;
1782 $grade->feedbackformat
= $feedbackformat;
1785 $gradechanged = false;
1786 if (empty($grade->id
)) {
1787 $grade->timecreated
= null; // hack alert - date submitted - no submission yet
1788 $grade->timemodified
= time(); // hack alert - date graded
1789 $result = (bool)$grade->insert($source);
1791 // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1792 if ($result && !is_null($grade->finalgrade
)) {
1793 \core\event\user_graded
::create_from_grade($grade)->trigger();
1795 $gradechanged = true;
1797 // Existing grade_grades.
1799 if (grade_floats_different($grade->finalgrade
, $oldgrade->finalgrade
)
1800 or grade_floats_different($grade->rawgrademin
, $oldgrade->rawgrademin
)
1801 or grade_floats_different($grade->rawgrademax
, $oldgrade->rawgrademax
)
1802 or ($oldgrade->overridden
== 0 and $grade->overridden
> 0)) {
1803 $gradechanged = true;
1806 if ($grade->feedback
=== $oldgrade->feedback
and $grade->feedbackformat
== $oldgrade->feedbackformat
and
1807 $gradechanged === false) {
1808 // No grade nor feedback changed.
1812 $grade->timemodified
= time(); // hack alert - date graded
1813 $result = $grade->update($source);
1815 // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1816 if ($result && grade_floats_different($grade->finalgrade
, $oldgrade->finalgrade
)) {
1817 \core\event\user_graded
::create_from_grade($grade)->trigger();
1822 // Something went wrong - better force final grade recalculation.
1823 $this->force_regrading();
1827 // If we are not updating grades we don't need to recalculate the whole course.
1828 if (!$gradechanged) {
1832 if ($this->is_course_item() and !$this->needsupdate
) {
1833 if (grade_regrade_final_grades($this->courseid
, $userid, $this) !== true) {
1834 $this->force_regrading();
1837 } else if (!$this->needsupdate
) {
1839 $course_item = grade_item
::fetch_course_item($this->courseid
);
1840 if (!$course_item->needsupdate
) {
1841 if (grade_regrade_final_grades($this->courseid
, $userid, $this) !== true) {
1842 $this->force_regrading();
1845 $this->force_regrading();
1854 * Updates raw grade value for given user, this is a only way to update raw
1855 * grades from external source (modules, etc.),
1856 * because it logs the change in history table and deals with final grade recalculation.
1858 * @param int $userid the graded user
1859 * @param mixed $rawgrade float value of raw grade - false means do not change
1860 * @param string $source modification source
1861 * @param string $feedback optional teacher feedback
1862 * @param int $feedbackformat A format like FORMAT_PLAIN or FORMAT_HTML
1863 * @param int $usermodified the ID of the user who did the grading
1864 * @param int $dategraded A timestamp of when the student's work was graded
1865 * @param int $datesubmitted A timestamp of when the student's work was submitted
1866 * @param grade_grade $grade A grade object, useful for bulk upgrades
1867 * @return bool success
1869 public function update_raw_grade($userid, $rawgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE
, $usermodified=null, $dategraded=null, $datesubmitted=null, $grade=null) {
1874 // calculated grades can not be updated; course and category can not be updated because they are aggregated
1875 if (!$this->is_raw_used() or $this->gradetype
== GRADE_TYPE_NONE
or $this->is_locked()) {
1879 if (is_null($grade)) {
1881 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$userid));
1883 $grade->grade_item
=& $this; // prevent db fetching of this grade_item
1885 if (empty($usermodified)) {
1886 $grade->usermodified
= $USER->id
;
1888 $grade->usermodified
= $usermodified;
1891 if ($grade->is_locked()) {
1892 // do not update locked grades at all
1896 $locktime = $grade->get_locktime();
1897 if ($locktime and $locktime < time()) {
1898 // do not update grades that should be already locked and force regrade
1899 $this->force_regrading();
1903 $oldgrade = new stdClass();
1904 $oldgrade->finalgrade
= $grade->finalgrade
;
1905 $oldgrade->rawgrade
= $grade->rawgrade
;
1906 $oldgrade->rawgrademin
= $grade->rawgrademin
;
1907 $oldgrade->rawgrademax
= $grade->rawgrademax
;
1908 $oldgrade->rawscaleid
= $grade->rawscaleid
;
1909 $oldgrade->feedback
= $grade->feedback
;
1910 $oldgrade->feedbackformat
= $grade->feedbackformat
;
1912 // use new min and max
1913 $grade->rawgrade
= $grade->rawgrade
;
1914 $grade->rawgrademin
= $this->grademin
;
1915 $grade->rawgrademax
= $this->grademax
;
1916 $grade->rawscaleid
= $this->scaleid
;
1918 // change raw grade?
1919 if ($rawgrade !== false) {
1920 $grade->rawgrade
= $rawgrade;
1923 // empty feedback means no feedback at all
1924 if ($feedback === '') {
1928 // do we have comment from teacher?
1929 if ($feedback !== false and !$grade->is_overridden()) {
1930 $grade->feedback
= $feedback;
1931 $grade->feedbackformat
= $feedbackformat;
1934 // update final grade if possible
1935 if (!$grade->is_locked() and !$grade->is_overridden()) {
1936 $grade->finalgrade
= $this->adjust_raw_grade($grade->rawgrade
, $grade->rawgrademin
, $grade->rawgrademax
);
1939 // TODO: hack alert - create new fields for these in 2.0
1940 $oldgrade->timecreated
= $grade->timecreated
;
1941 $oldgrade->timemodified
= $grade->timemodified
;
1943 $grade->timecreated
= $datesubmitted;
1945 if ($grade->is_overridden()) {
1946 // keep original graded date - update_final_grade() sets this for overridden grades
1948 } else if (is_null($grade->rawgrade
) and is_null($grade->feedback
)) {
1949 // no grade and feedback means no grading yet
1950 $grade->timemodified
= null;
1952 } else if (!empty($dategraded)) {
1953 // fine - module sends info when graded (yay!)
1954 $grade->timemodified
= $dategraded;
1956 } else if (grade_floats_different($grade->finalgrade
, $oldgrade->finalgrade
)
1957 or $grade->feedback
!== $oldgrade->feedback
) {
1958 // guess - if either grade or feedback changed set new graded date
1959 $grade->timemodified
= time();
1962 //keep original graded date
1964 // end of hack alert
1966 $gradechanged = false;
1967 if (empty($grade->id
)) {
1968 $result = (bool)$grade->insert($source);
1970 // If the grade insert was successful and the final grade was not null then trigger a user_graded event.
1971 if ($result && !is_null($grade->finalgrade
)) {
1972 \core\event\user_graded
::create_from_grade($grade)->trigger();
1974 $gradechanged = true;
1976 // Existing grade_grades.
1978 if (grade_floats_different($grade->finalgrade
, $oldgrade->finalgrade
)
1979 or grade_floats_different($grade->rawgrade
, $oldgrade->rawgrade
)
1980 or grade_floats_different($grade->rawgrademin
, $oldgrade->rawgrademin
)
1981 or grade_floats_different($grade->rawgrademax
, $oldgrade->rawgrademax
)
1982 or $grade->rawscaleid
!= $oldgrade->rawscaleid
) {
1983 $gradechanged = true;
1986 // The timecreated and timemodified checking is part of the hack above.
1987 if ($gradechanged === false and
1988 $grade->feedback
=== $oldgrade->feedback
and
1989 $grade->feedbackformat
== $oldgrade->feedbackformat
and
1990 $grade->timecreated
== $oldgrade->timecreated
and
1991 $grade->timemodified
== $oldgrade->timemodified
) {
1995 $result = $grade->update($source);
1997 // If the grade update was successful and the actual grade has changed then trigger a user_graded event.
1998 if ($result && grade_floats_different($grade->finalgrade
, $oldgrade->finalgrade
)) {
1999 \core\event\user_graded
::create_from_grade($grade)->trigger();
2004 // Something went wrong - better force final grade recalculation.
2005 $this->force_regrading();
2009 // If we are not updating grades we don't need to recalculate the whole course.
2010 if (!$gradechanged) {
2014 if (!$this->needsupdate
) {
2015 $course_item = grade_item
::fetch_course_item($this->courseid
);
2016 if (!$course_item->needsupdate
) {
2017 if (grade_regrade_final_grades($this->courseid
, $userid, $this) !== true) {
2018 $this->force_regrading();
2027 * Calculates final grade values using the formula in the calculation property.
2028 * The parameters are taken from final grades of grade items in current course only.
2030 * @param int $userid Supply a user ID to limit the calculations to the grades of a single user
2031 * @return bool false if error
2033 public function compute($userid=null) {
2036 if (!$this->is_calculated()) {
2040 require_once($CFG->libdir
.'/mathslib.php');
2042 if ($this->is_locked()) {
2043 return true; // no need to recalculate locked items
2046 // Precreate grades - we need them to exist
2049 if (!$DB->record_exists('grade_grades', array('itemid'=>$this->id
, 'userid'=>$userid))) {
2050 $m = new stdClass();
2051 $m->userid
= $userid;
2055 // Find any users who have grades for some but not all grade items in this course
2056 $params = array('gicourseid' => $this->courseid
, 'ggitemid' => $this->id
);
2057 $sql = "SELECT gg.userid
2058 FROM {grade_grades} gg
2059 JOIN {grade_items} gi
2060 ON (gi.id = gg.itemid AND gi.courseid = :gicourseid)
2062 HAVING SUM(CASE WHEN gg.itemid = :ggitemid THEN 1 ELSE 0 END) = 0";
2063 $missing = $DB->get_records_sql($sql, $params);
2067 foreach ($missing as $m) {
2068 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$m->userid
), false);
2069 $grade->grade_item
=& $this;
2070 $grade->insert('system');
2075 $useditems = $this->depends_on();
2077 // prepare formula and init maths library
2078 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation
);
2079 if (strpos($formula, '[[') !== false) {
2083 $this->formula
= new calc_formula($formula);
2085 // where to look for final grades?
2086 // this itemid is added so that we use only one query for source and final grades
2087 $gis = array_merge($useditems, array($this->id
));
2088 list($usql, $params) = $DB->get_in_or_equal($gis);
2091 $usersql = "AND g.userid=?";
2092 $params[] = $userid;
2097 $grade_inst = new grade_grade();
2098 $fields = 'g.'.implode(',g.', $grade_inst->required_fields
);
2100 $params[] = $this->courseid
;
2101 $sql = "SELECT $fields
2102 FROM {grade_grades} g, {grade_items} gi
2103 WHERE gi.id = g.itemid AND gi.id $usql $usersql AND gi.courseid=?
2108 // group the grades by userid and use formula on the group
2109 $rs = $DB->get_recordset_sql($sql, $params);
2112 $grade_records = array();
2114 foreach ($rs as $used) {
2115 if ($used->userid
!= $prevuser) {
2116 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2119 $prevuser = $used->userid
;
2120 $grade_records = array();
2123 if ($used->itemid
== $this->id
) {
2126 $grade_records['gi'.$used->itemid
] = $used->finalgrade
;
2128 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
2138 * Internal function that does the final grade calculation
2140 * @param int $userid The user ID
2141 * @param array $params An array of grade items of the form {'gi'.$itemid]} => $finalgrade
2142 * @param array $useditems An array of grade item IDs that this grade item depends on plus its own ID
2143 * @param grade_grade $oldgrade A grade_grade instance containing the old values from the database
2144 * @return bool False if an error occurred
2146 public function use_formula($userid, $params, $useditems, $oldgrade) {
2147 if (empty($userid)) {
2151 // add missing final grade values
2152 // not graded (null) is counted as 0 - the spreadsheet way
2153 $allinputsnull = true;
2154 foreach($useditems as $gi) {
2155 if (!array_key_exists('gi'.$gi, $params) ||
is_null($params['gi'.$gi])) {
2156 $params['gi'.$gi] = 0;
2158 $params['gi'.$gi] = (float)$params['gi'.$gi];
2159 if ($gi != $this->id
) {
2160 $allinputsnull = false;
2165 // can not use own final grade during calculation
2166 unset($params['gi'.$this->id
]);
2168 // Check to see if the gradebook is frozen. This allows grades to not be altered at all until a user verifies that they
2169 // wish to update the grades.
2170 $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->courseid
);
2172 $rawminandmaxchanged = false;
2173 // insert final grade - will be needed later anyway
2175 // Only run through this code if the gradebook isn't frozen.
2176 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2179 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2180 // grade_item grade maximum and minimum respectively.
2181 if ($oldgrade->rawgrademax
!= $this->grademax ||
$oldgrade->rawgrademin
!= $this->grademin
) {
2182 $rawminandmaxchanged = true;
2183 $oldgrade->rawgrademax
= $this->grademax
;
2184 $oldgrade->rawgrademin
= $this->grademin
;
2187 $oldfinalgrade = $oldgrade->finalgrade
;
2188 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
2189 $grade->grade_item
=& $this;
2192 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$userid), false);
2193 $grade->grade_item
=& $this;
2194 $rawminandmaxchanged = false;
2195 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2198 // The grade_grade for a calculated item should have the raw grade maximum and minimum set to the
2199 // grade_item grade maximum and minimum respectively.
2200 $rawminandmaxchanged = true;
2201 $grade->rawgrademax
= $this->grademax
;
2202 $grade->rawgrademin
= $this->grademin
;
2204 $grade->insert('system');
2205 $oldfinalgrade = null;
2208 // no need to recalculate locked or overridden grades
2209 if ($grade->is_locked() or $grade->is_overridden()) {
2213 if ($allinputsnull) {
2214 $grade->finalgrade
= null;
2219 // do the calculation
2220 $this->formula
->set_params($params);
2221 $result = $this->formula
->evaluate();
2223 if ($result === false) {
2224 $grade->finalgrade
= null;
2228 $grade->finalgrade
= $this->bounded_grade($result);
2232 // Only run through this code if the gradebook isn't frozen.
2233 if ($gradebookcalculationsfreeze && (int)$gradebookcalculationsfreeze <= 20150627) {
2234 // Update in db if changed.
2235 if (grade_floats_different($grade->finalgrade
, $oldfinalgrade)) {
2236 $grade->timemodified
= time();
2237 $success = $grade->update('compute');
2239 // If successful trigger a user_graded event.
2241 \core\event\user_graded
::create_from_grade($grade)->trigger();
2245 // Update in db if changed.
2246 if (grade_floats_different($grade->finalgrade
, $oldfinalgrade) ||
$rawminandmaxchanged) {
2247 $grade->timemodified
= time();
2248 $success = $grade->update('compute');
2250 // If successful trigger a user_graded event.
2252 \core\event\user_graded
::create_from_grade($grade)->trigger();
2257 if ($result !== false) {
2258 //lock grade if needed
2261 if ($result === false) {
2270 * Validate the formula.
2272 * @param string $formulastr
2273 * @return bool true if calculation possible, false otherwise
2275 public function validate_formula($formulastr) {
2277 require_once($CFG->libdir
.'/mathslib.php');
2279 $formulastr = grade_item
::normalize_formula($formulastr, $this->courseid
);
2281 if (empty($formulastr)) {
2285 if (strpos($formulastr, '=') !== 0) {
2286 return get_string('errorcalculationnoequal', 'grades');
2290 if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {
2291 $useditems = array_unique($matches[1]); // remove duplicates
2293 $useditems = array();
2297 // unset the value if formula is trying to reference to itself
2298 // but array keys does not match itemid
2299 if (!empty($this->id
)) {
2300 $useditems = array_diff($useditems, array($this->id
));
2301 //unset($useditems[$this->id]);
2304 // prepare formula and init maths library
2305 $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);
2306 $formula = new calc_formula($formula);
2309 if (empty($useditems)) {
2310 $grade_items = array();
2313 list($usql, $params) = $DB->get_in_or_equal($useditems);
2314 $params[] = $this->courseid
;
2316 FROM {grade_items} gi
2317 WHERE gi.id $usql and gi.courseid=?"; // from the same course only!
2319 if (!$grade_items = $DB->get_records_sql($sql, $params)) {
2320 $grade_items = array();
2325 foreach ($useditems as $itemid) {
2326 // make sure all grade items exist in this course
2327 if (!array_key_exists($itemid, $grade_items)) {
2330 // use max grade when testing formula, this should be ok in 99.9%
2331 // division by 0 is one of possible problems
2332 $params['gi'.$grade_items[$itemid]->id
] = $grade_items[$itemid]->grademax
;
2335 // do the calculation
2336 $formula->set_params($params);
2337 $result = $formula->evaluate();
2339 // false as result indicates some problem
2340 if ($result === false) {
2341 // TODO: add more error hints
2342 return get_string('errorcalculationunknown', 'grades');
2349 * Returns the value of the display type
2351 * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2353 * @return int Display type
2355 public function get_displaytype() {
2358 if ($this->display
== GRADE_DISPLAY_TYPE_DEFAULT
) {
2359 return grade_get_setting($this->courseid
, 'displaytype', $CFG->grade_displaytype
);
2362 return $this->display
;
2367 * Returns the value of the decimals field
2369 * It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.
2371 * @return int Decimals (0 - 5)
2373 public function get_decimals() {
2376 if (is_null($this->decimals
)) {
2377 return grade_get_setting($this->courseid
, 'decimalpoints', $CFG->grade_decimalpoints
);
2380 return $this->decimals
;
2385 * Returns a string representing the range of grademin - grademax for this grade item.
2387 * @param int $rangesdisplaytype
2388 * @param int $rangesdecimalpoints
2391 function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {
2395 // Determine which display type to use for this average
2396 if (isset($USER->gradeediting
) && array_key_exists($this->courseid
, $USER->gradeediting
) && $USER->gradeediting
[$this->courseid
]) {
2397 $displaytype = GRADE_DISPLAY_TYPE_REAL
;
2399 } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT
) { // no ==0 here, please resave report and user prefs
2400 $displaytype = $this->get_displaytype();
2403 $displaytype = $rangesdisplaytype;
2406 // Override grade_item setting if a display preference (not default) was set for the averages
2407 if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT
) {
2408 $decimalpoints = $this->get_decimals();
2411 $decimalpoints = $rangesdecimalpoints;
2414 if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE
) {
2416 $grademax = "100 %";
2419 $grademin = grade_format_gradevalue($this->grademin
, $this, true, $displaytype, $decimalpoints);
2420 $grademax = grade_format_gradevalue($this->grademax
, $this, true, $displaytype, $decimalpoints);
2423 return $grademin.'–'. $grademax;
2427 * Queries parent categories recursively to find the aggregationcoef type that applies to this grade item.
2429 * @return string|false Returns the coefficient string of false is no coefficient is being used
2431 public function get_coefstring() {
2432 $parent_category = $this->load_parent_category();
2433 if ($this->is_category_item()) {
2434 $parent_category = $parent_category->load_parent_category();
2437 if ($parent_category->is_aggregationcoef_used()) {
2438 return $parent_category->get_coefstring();
2445 * Returns whether the grade item can control the visibility of the grades
2449 public function can_control_visibility() {
2450 if (core_component
::get_plugin_directory($this->itemtype
, $this->itemmodule
)) {
2451 return !plugin_supports($this->itemtype
, $this->itemmodule
, FEATURE_CONTROLS_GRADE_VISIBILITY
, false);
2453 return parent
::can_control_visibility();
2457 * Used to notify the completion system (if necessary) that a user's grade
2458 * has changed, and clear up a possible score cache.
2460 * @param bool $deleted True if grade was actually deleted
2462 protected function notify_changed($deleted) {
2465 // Condition code may cache the grades for conditional availability of
2466 // modules or sections. (This code should use a hook for communication
2467 // with plugin, but hooks are not implemented at time of writing.)
2468 if (!empty($CFG->enableavailability
) && class_exists('\availability_grade\callbacks')) {
2469 \availability_grade\callbacks
::grade_item_changed($this->courseid
);