3 ///////////////////////////////////////////////////////////////////////////
5 // NOTICE OF COPYRIGHT //
7 // Moodle - Modular Object-Oriented Dynamic Learning Environment //
8 // http://moodle.com //
10 // Copyright (C) 2001-2003 Martin Dougiamas http://dougiamas.com //
12 // This program is free software; you can redistribute it and/or modify //
13 // it under the terms of the GNU General Public License as published by //
14 // the Free Software Foundation; either version 2 of the License, or //
15 // (at your option) any later version. //
17 // This program is distributed in the hope that it will be useful, //
18 // but WITHOUT ANY WARRANTY; without even the implied warranty of //
19 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
20 // GNU General Public License for more details: //
22 // http://www.gnu.org/copyleft/gpl.html //
24 ///////////////////////////////////////////////////////////////////////////
26 require_once('grade_object.php');
29 * Class representing a grade item. It is responsible for handling its DB representation,
30 * modifying and returning its metadata.
32 class grade_item
extends grade_object
{
34 * DB Table (used by grade_object).
37 var $table = 'grade_items';
40 * Array of class variables that are not part of the DB table fields
41 * @var array $nonfields
43 var $nonfields = array('table', 'nonfields', 'required_fields', 'formula', 'calculation_normalized', 'scale', 'item_category', 'parent_category', 'outcome');
46 * The course this grade_item belongs to.
52 * The category this grade_item belongs to (optional).
53 * @var int $categoryid
58 * The grade_category object referenced $this->iteminstance (itemtype must be == 'category' or == 'course' in that case).
59 * @var object $item_category
64 * The grade_category object referenced by $this->categoryid.
65 * @var object $parent_category
71 * The name of this grade_item (pushed by the module).
72 * @var string $itemname
77 * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
78 * @var string $itemtype
83 * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
84 * @var string $itemmodule
89 * ID of the item module
90 * @var int $iteminstance
95 * Number of the item in a series of multiple grades pushed by an activity.
96 * @var int $itemnumber
101 * Info and notes about this item.
102 * @var string $iteminfo
107 * Arbitrary idnumber provided by the module responsible.
108 * @var string $idnumber
113 * Calculation string used for this item.
114 * @var string $calculation
119 * Indicates if we already tried to normalize the grade calculation formula.
120 * This flag helps to minimize db access when broken formulas used in calculation.
123 var $calculation_normalized;
125 * Math evaluation object
130 * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)
131 * @var int $gradetype
133 var $gradetype = GRADE_TYPE_VALUE
;
136 * Maximum allowable grade.
137 * @var float $grademax
142 * Minimum allowable grade.
143 * @var float $grademin
148 * id of the scale, if this grade is based on a scale.
154 * A grade_scale object (referenced by $this->scaleid).
160 * The id of the optional grade_outcome associated with this grade_item.
161 * @var int $outcomeid
166 * The grade_outcome this grade is associated with, if applicable.
167 * @var object $outcome
172 * grade required to pass. (grademin <= gradepass <= grademax)
173 * @var float $gradepass
178 * Multiply all grades by this number.
179 * @var float $multfactor
181 var $multfactor = 1.0;
184 * Add this to all grades.
185 * @var float $plusfactor
190 * Aggregation coeficient used for weighted averages
191 * @var float $aggregationcoef
193 var $aggregationcoef = 0;
196 * Sorting order of the columns.
197 * @var int $sortorder
202 * 0 if visible, 1 always hidden or date not visible until
208 * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
214 * Date after which the grade will be locked. Empty means no automatic locking.
220 * If set, the whole column will be recalculated, then this flag will be switched off.
221 * @var boolean $needsupdate
223 var $needsupdate = 1;
226 * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.
227 * Force regrading if necessary
228 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
229 * @return boolean success
231 function update($source=null) {
232 // Retrieve scale and infer grademax/min from it if needed
235 // make sure there is not 0 in outcomeid
236 if (empty($this->outcomeid
)) {
237 $this->outcomeid
= null;
240 if ($this->qualifies_for_regrading()) {
241 $this->force_regrading();
244 return parent
::update($source);
248 * Compares the values held by this object with those of the matching record in DB, and returns
249 * whether or not these differences are sufficient to justify an update of all parent objects.
250 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
253 function qualifies_for_regrading() {
254 if (empty($this->id
)) {
258 $db_item = new grade_item(array('id' => $this->id
));
260 $calculationdiff = $db_item->calculation
!= $this->calculation
;
261 $categorydiff = $db_item->categoryid
!= $this->categoryid
;
262 $gradetypediff = $db_item->gradetype
!= $this->gradetype
;
263 $grademaxdiff = $db_item->grademax
!= $this->grademax
;
264 $grademindiff = $db_item->grademin
!= $this->grademin
;
265 $scaleiddiff = $db_item->scaleid
!= $this->scaleid
;
266 $outcomeiddiff = $db_item->outcomeid
!= $this->outcomeid
;
267 $multfactordiff = $db_item->multfactor
!= $this->multfactor
;
268 $plusfactordiff = $db_item->plusfactor
!= $this->plusfactor
;
269 $locktimediff = $db_item->locktime
!= $this->locktime
;
270 $acoefdiff = $db_item->aggregationcoef
!= $this->aggregationcoef
;
272 $needsupdatediff = !$db_item->needsupdate
&& $this->needsupdate
; // force regrading only if setting the flag first time
273 $lockeddiff = !empty($db_item->locked
) && empty($this->locked
); // force regrading only when unlocking
275 return ($calculationdiff ||
$categorydiff ||
$gradetypediff ||
$grademaxdiff ||
$grademindiff ||
$scaleiddiff
276 ||
$outcomeiddiff ||
$multfactordiff ||
$plusfactordiff ||
$needsupdatediff
277 ||
$lockeddiff ||
$acoefdiff ||
$locktimediff);
281 * Finds and returns a grade_item instance based on params.
284 * @param array $params associative arrays varname=>value
285 * @return object grade_item instance or false if none found.
287 function fetch($params) {
288 return grade_object
::fetch_helper('grade_items', 'grade_item', $params);
292 * Finds and returns all grade_item instances based on params.
295 * @param array $params associative arrays varname=>value
296 * @return array array of grade_item insatnces or false if none found.
298 function fetch_all($params) {
299 return grade_object
::fetch_all_helper('grade_items', 'grade_item', $params);
303 * Delete all grades and force_regrading of parent category.
304 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
305 * @return boolean success
307 function delete($source=null) {
308 if (!$this->is_course_item()) {
309 $this->force_regrading();
312 if ($grades = grade_grade
::fetch_all(array('itemid'=>$this->id
))) {
313 foreach ($grades as $grade) {
314 $grade->delete($source);
318 return parent
::delete($source);
322 * In addition to perform parent::insert(), calls force_regrading() method too.
323 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
324 * @return int PK ID if successful, false otherwise
326 function insert($source=null) {
329 if (empty($this->courseid
)) {
330 error('Can not insert grade item without course id!');
333 // load scale if needed
336 // add parent category if needed
337 if (empty($this->categoryid
) and !$this->is_course_item() and !$this->is_category_item()) {
338 $course_category = grade_category
::fetch_course_category($this->courseid
);
339 $this->categoryid
= $course_category->id
;
343 // always place the new items at the end, move them after insert if needed
344 $last_sortorder = get_field_select('grade_items', 'MAX(sortorder)', "courseid = {$this->courseid}");
345 if (!empty($last_sortorder)) {
346 $this->sortorder
= $last_sortorder +
1;
348 $this->sortorder
= 1;
351 // add proper item numbers to manual items
352 if ($this->itemtype
== 'manual') {
353 if (empty($this->itemnumber
)) {
354 $this->itemnumber
= 0;
358 // make sure there is not 0 in outcomeid
359 if (empty($this->outcomeid
)) {
360 $this->outcomeid
= null;
363 if (parent
::insert($source)) {
364 // force regrading of items if needed
365 $this->force_regrading();
369 debugging("Could not insert this grade_item in the database!");
375 * Set idnumber of grade item, updates also course_modules table
376 * @param string $idnumber (without magic quotes)
377 * @return boolean success
379 function add_idnumber($idnumber) {
380 if (!empty($this->idnumber
)) {
384 if ($this->itemtype
== 'mod' and !$this->is_outcome_item()) {
385 if (!$cm = get_coursemodule_from_instance($this->itemmodule
, $this->iteminstance
, $this->courseid
)) {
388 if (!empty($cm->idnumber
)) {
391 if (set_field('course_modules', 'idnumber', addslashes($idnumber), 'id', $cm->id
)) {
392 $this->idnumber
= $idnumber;
393 return $this->update();
398 $this->idnumber
= $idnumber;
399 return $this->update();
404 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
405 * $userid is given) or the locked state of a specific grade within this item if a specific
406 * $userid is given and the grade_item is unlocked.
409 * @return boolean Locked state
411 function is_locked($userid=NULL) {
412 if (!empty($this->locked
)) {
416 if (!empty($userid)) {
417 if ($grade = grade_grade
::fetch(array('itemid'=>$this->id
, 'userid'=>$userid))) {
418 $grade->grade_item
=& $this; // prevent db fetching of cached grade_item
419 return $grade->is_locked();
427 * Locks or unlocks this grade_item and (optionally) all its associated final grades.
428 * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
429 * @param boolean $cascade lock/unlock child objects too
430 * @param boolean $refresh refresh grades when unlocking
431 * @return boolean true if grade_item all grades updated, false if at least one update fails
433 function set_locked($lockedstate, $cascade=false, $refresh=true) {
436 if ($this->needsupdate
) {
437 return false; // can not lock grade without first having final grade
440 $this->locked
= time();
444 $grades = $this->get_final();
445 foreach($grades as $g) {
446 $grade = new grade_grade($g, false);
447 $grade->grade_item
=& $this;
448 $grade->set_locked(1, null, false);
456 if (!empty($this->locked
) and $this->locktime
< time()) {
457 //we have to reset locktime or else it would lock up again
465 if ($grades = grade_grade
::fetch_all(array('itemid'=>$this->id
))) {
466 foreach($grades as $grade) {
467 $grade->grade_item
=& $this;
468 $grade->set_locked(0, null, false);
474 //refresh when unlocking
475 $this->refresh_grades();
483 * Lock the grade if needed - make sure this is called only when final grades are valid
485 function check_locktime() {
486 if (!empty($this->locked
)) {
487 return; // already locked
490 if ($this->locktime
and $this->locktime
< time()) {
491 $this->locked
= time();
492 $this->update('locktime');
497 * Set the locktime for this grade item.
499 * @param int $locktime timestamp for lock to activate
502 function set_locktime($locktime) {
503 $this->locktime
= $locktime;
508 * Set the locktime for this grade item.
510 * @return int $locktime timestamp for lock to activate
512 function get_locktime() {
513 return $this->locktime
;
517 * Returns the hidden state of this grade_item
518 * @return boolean hidden state
520 function is_hidden($userid=NULL) {
521 return ($this->hidden
== 1 or ($this->hidden
!= 0 and $this->hidden
> time()));
525 * Check grade item hidden status.
526 * @return int 0 means visible, 1 hidden always, timestamp hidden until
528 function get_hidden() {
529 return $this->hidden
;
533 * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.
534 * @param int $hidden new hidden status
535 * @param boolean $cascade apply to child objects too
538 function set_hidden($hidden, $cascade=false) {
539 $this->hidden
= $hidden;
543 if ($grades = grade_grade
::fetch_all(array('itemid'=>$this->id
))) {
544 foreach($grades as $grade) {
545 $grade->grade_item
=& $this;
546 $grade->set_hidden($hidden, $cascade);
553 * Mark regrading as finished successfully.
555 function regrading_finished() {
556 $this->needsupdate
= 0;
557 //do not use $this->update() because we do not want this logged in grade_item_history
558 set_field('grade_items', 'needsupdate', 0, 'id', $this->id
);
562 * Performs the necessary calculations on the grades_final referenced by this grade_item.
563 * Also resets the needsupdate flag once successfully performed.
565 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
566 * because the regrading must be done in correct order!!
568 * @return boolean true if ok, error string otherwise
570 function regrade_final_grades($userid=null) {
573 // locked grade items already have correct final grades
574 if ($this->is_locked()) {
578 // calculation produces final value using formula from other final values
579 if ($this->is_calculated()) {
580 if ($this->compute($userid)) {
583 return "Could not calculate grades for grade item"; // TODO: improve and localize
586 // noncalculated outcomes already have final values - raw grades not used
587 } else if ($this->is_outcome_item()) {
590 // aggregate the category grade
591 } else if ($this->is_category_item() or $this->is_course_item()) {
592 // aggregate category grade item
593 $category = $this->get_item_category();
594 $category->grade_item
=& $this;
595 if ($category->generate_grades($userid)) {
598 return "Could not aggregate final grades for category:".$this->id
; // TODO: improve and localize
600 } else if ($this->is_manual_item()) {
601 // manual items track only final grades, no raw grades
605 // normal grade item - just new final grades
608 $rs = get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid");
610 $rs = get_recordset('grade_grades', 'itemid', $this->id
);
613 if ($rs->RecordCount() > 0) {
614 while ($grade_record = rs_fetch_next_record($rs)) {
615 $grade = new grade_grade($grade_record, false);
617 if (!empty($grade_record->locked
) or !empty($grade_record->overridden
)) {
618 // this grade is locked - final grade must be ok
622 $grade->finalgrade
= $this->adjust_grade($grade->rawgrade
, $grade->rawgrademin
, $grade->rawgrademax
);
624 if ($grade_record->finalgrade
!== $grade->finalgrade
) {
625 if (!$grade->update('system')) {
626 $result = "Internal error updating final grade";
638 * Given a float grade value or integer grade scale, applies a number of adjustment based on
639 * grade_item variables and returns the result.
640 * @param object $rawgrade The raw grade value.
643 function adjust_grade($rawgrade, $rawmin, $rawmax) {
644 if (is_null($rawgrade)) {
648 if ($this->gradetype
== GRADE_TYPE_VALUE
) { // Dealing with numerical grade
650 if ($this->grademax
< $this->grademin
) {
654 if ($this->grademax
== $this->grademin
) {
655 return $this->grademax
; // no range
658 // Standardise score to the new grade range
659 // NOTE: this is not compatible with current assignment grading
660 if ($rawmin != $this->grademin
or $rawmax != $this->grademax
) {
661 $rawgrade = grade_grade
::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin
, $this->grademax
);
664 // Apply other grade_item factors
665 $rawgrade *= $this->multfactor
;
666 $rawgrade +
= $this->plusfactor
;
668 return bounded_number($this->grademin
, $rawgrade, $this->grademax
);
670 } else if($this->gradetype
== GRADE_TYPE_SCALE
) { // Dealing with a scale value
671 if (empty($this->scale
)) {
675 if ($this->grademax
< 0) {
676 return null; // scale not present - no grade
679 if ($this->grademax
== 0) {
680 return $this->grademax
; // only one option
683 // Convert scale if needed
684 // NOTE: this is not compatible with current assignment grading
685 if ($rawmin != $this->grademin
or $rawmax != $this->grademax
) {
686 $rawgrade = grade_grade
::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin
, $this->grademax
);
689 return (int)bounded_number(0, round($rawgrade+
0.00001), $this->grademax
);
692 } else if ($this->gradetype
== GRADE_TYPE_TEXT
or $this->gradetype
== GRADE_TYPE_NONE
) { // no value
693 // somebody changed the grading type when grades already existed
697 dubugging("Unkown grade type");
703 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
706 function force_regrading() {
707 $this->needsupdate
= 1;
708 //mark this item and course item only - categories and calculated items are always regraded
709 $wheresql = "(itemtype='course' OR id={$this->id}) AND courseid={$this->courseid}";
710 set_field_select('grade_items', 'needsupdate', 1, $wheresql);
714 * Instantiates a grade_scale object whose data is retrieved from the DB,
715 * if this item's scaleid variable is set.
716 * @return object grade_scale or null if no scale used
718 function load_scale() {
719 if ($this->gradetype
!= GRADE_TYPE_SCALE
) {
720 $this->scaleid
= null;
723 if (!empty($this->scaleid
)) {
724 //do not load scale if already present
725 if (empty($this->scale
->id
) or $this->scale
->id
!= $this->scaleid
) {
726 $this->scale
= grade_scale
::fetch(array('id'=>$this->scaleid
));
727 $this->scale
->load_items();
730 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
731 // stay with the current min=1 max=count(scaleitems)
732 $this->grademax
= count($this->scale
->scale_items
);
743 * Instantiates a grade_outcome object whose data is retrieved from the DB,
744 * if this item's outcomeid variable is set.
745 * @return object grade_outcome
747 function load_outcome() {
748 if (!empty($this->outcomeid
)) {
749 $this->outcome
= grade_outcome
::fetch(array('id'=>$this->outcomeid
));
751 return $this->outcome
;
755 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
756 * or category attached to category item.
758 * @return mixed grade_category object if applicable, false if course item
760 function get_parent_category() {
761 if ($this->is_category_item() or $this->is_course_item()) {
762 return $this->get_item_category();
765 return grade_category
::fetch(array('id'=>$this->categoryid
));
770 * Calls upon the get_parent_category method to retrieve the grade_category object
771 * from the DB and assigns it to $this->parent_category. It also returns the object.
772 * @return object Grade_category
774 function load_parent_category() {
775 if (empty($this->parent_category
->id
)) {
776 $this->parent_category
= $this->get_parent_category();
778 return $this->parent_category
;
782 * Returns the grade_category for category item
784 * @return mixed grade_category object if applicable, false otherwise
786 function get_item_category() {
787 if (!$this->is_course_item() and !$this->is_category_item()) {
790 return grade_category
::fetch(array('id'=>$this->iteminstance
));
794 * Calls upon the get_item_category method to retrieve the grade_category object
795 * from the DB and assigns it to $this->item_category. It also returns the object.
796 * @return object Grade_category
798 function load_item_category() {
799 if (empty($this->category
->id
)) {
800 $this->item_category
= $this->get_item_category();
802 return $this->item_category
;
806 * Is the grade item associated with category?
809 function is_category_item() {
810 return ($this->itemtype
== 'category');
814 * Is the grade item associated with course?
817 function is_course_item() {
818 return ($this->itemtype
== 'course');
822 * Is this a manualy graded item?
825 function is_manual_item() {
826 return ($this->itemtype
== 'manual');
830 * Is this an outcome item?
833 function is_outcome_item() {
834 return !empty($this->outcomeid
);
838 * Is the grade item normal - associated with module, plugin or something else?
841 function is_normal_item() {
842 return ($this->itemtype
!= 'course' and $this->itemtype
!= 'category' and $this->itemtype
!= 'manual');
846 * Returns grade item associated with the course
847 * @param int $courseid
848 * @return course item object
850 function fetch_course_item($courseid) {
851 if ($course_item = grade_item
::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
855 // first get category - it creates the associated grade item
856 $course_category = grade_category
::fetch_course_category($courseid);
858 return grade_item
::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'));
862 * Is grading object editable?
865 function is_editable() {
870 * Checks if grade calculated. Returns this object's calculation.
871 * @return boolean true if grade item calculated.
873 function is_calculated() {
874 if (empty($this->calculation
)) {
879 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
880 * we would have to fetch all course grade items to find out the ids.
881 * Also if user changes the idnumber the formula does not need to be updated.
884 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
885 if (!$this->calculation_normalized
and preg_match('/##gi\d+##/', $this->calculation
)) {
886 $this->set_calculation($this->calculation
);
889 return !empty($this->calculation
);
893 * Returns calculation string if grade calculated.
894 * @return mixed string if calculation used, null if not
896 function get_calculation() {
897 if ($this->is_calculated()) {
898 return grade_item
::denormalize_formula($this->calculation
, $this->courseid
);
906 * Sets this item's calculation (creates it) if not yet set, or
907 * updates it if already set (in the DB). If no calculation is given,
908 * the calculation is removed.
909 * @param string $formula string representation of formula used for calculation
910 * @return boolean success
912 function set_calculation($formula) {
913 $this->calculation
= grade_item
::normalize_formula($formula, $this->courseid
);
914 $this->calculation_normalized
= true;
915 return $this->update();
919 * Denormalizes the calculation formula to [idnumber] form
921 * @param string $formula
922 * @return string denormalized string
924 function denormalize_formula($formula, $courseid) {
925 if (empty($formula)) {
929 // denormalize formula - convert ##giXX## to [[idnumber]]
930 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
931 foreach ($matches[1] as $id) {
932 if ($grade_item = grade_item
::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
933 if (!empty($grade_item->idnumber
)) {
934 $formula = str_replace('##gi'.$grade_item->id
.'##', '[['.$grade_item->idnumber
.']]', $formula);
945 * Normalizes the calculation formula to [#giXX#] form
947 * @param string $formula
948 * @return string normalized string
950 function normalize_formula($formula, $courseid) {
951 $formula = trim($formula);
953 if (empty($formula)) {
958 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
959 if ($grade_items = grade_item
::fetch_all(array('courseid'=>$courseid))) {
960 foreach ($grade_items as $grade_item) {
961 $formula = str_replace('[['.$grade_item->idnumber
.']]', '##gi'.$grade_item->id
.'##', $formula);
969 * Returns the final values for this grade item (as imported by module or other source).
970 * @param int $userid Optional: to retrieve a single final grade
971 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
973 function get_final($userid=NULL) {
975 if ($user = get_record('grade_grades', 'itemid', $this->id
, 'userid', $userid)) {
980 if ($grades = get_records('grade_grades', 'itemid', $this->id
)) {
981 //TODO: speed up with better SQL
983 foreach ($grades as $grade) {
984 $result[$grade->userid
] = $grade;
994 * Get (or create if not exist yet) grade for this user
996 * @return object grade_grade object instance
998 function get_grade($userid, $create=true) {
999 if (empty($this->id
)) {
1000 debugging('Can not use before insert');
1004 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id
));
1005 if (empty($grade->id
) and $create) {
1013 * Returns the sortorder of this grade_item. This method is also available in
1014 * grade_category, for cases where the object type is not know.
1015 * @return int Sort order
1017 function get_sortorder() {
1018 return $this->sortorder
;
1022 * Returns the idnumber of this grade_item. This method is also available in
1023 * grade_category, for cases where the object type is not know.
1024 * @return string idnumber
1026 function get_idnumber() {
1027 return $this->idnumber
;
1031 * Returns this grade_item. This method is also available in
1032 * grade_category, for cases where the object type is not know.
1033 * @return string idnumber
1035 function get_grade_item() {
1040 * Sets the sortorder of this grade_item. This method is also available in
1041 * grade_category, for cases where the object type is not know.
1042 * @param int $sortorder
1045 function set_sortorder($sortorder) {
1046 $this->sortorder
= $sortorder;
1050 function move_after_sortorder($sortorder) {
1053 //make some room first
1054 $sql = "UPDATE {$CFG->prefix}grade_items
1055 SET sortorder = sortorder + 1
1056 WHERE sortorder > $sortorder AND courseid = {$this->courseid}";
1057 execute_sql($sql, false);
1059 $this->set_sortorder($sortorder +
1);
1063 * Returns the most descriptive field for this object. This is a standard method used
1064 * when we do not know the exact type of an object.
1065 * @return string name
1067 function get_name() {
1068 if (!empty($this->itemname
)) {
1070 return format_string($this->itemname
);
1072 } else if ($this->is_course_item()) {
1073 return get_string('coursetotal', 'grades');
1075 } else if ($this->is_category_item()) {
1076 return get_string('categorytotal', 'grades');
1079 return get_string('grade');
1084 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1085 * @param int $parentid
1086 * @return boolean success;
1088 function set_parent($parentid) {
1089 if ($this->is_course_item() or $this->is_category_item()) {
1090 error('Can not set parent for category or course item!');
1093 if ($this->categoryid
== $parentid) {
1097 // find parent and check course id
1098 if (!$parent_category = grade_category
::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid
))) {
1102 $this->force_regrading();
1105 $this->categoryid
= $parent_category->id
;
1106 $this->parent_category
=& $parent_category;
1108 return $this->update();
1112 * Finds out on which other items does this depend directly when doing calculation or category agregation
1113 * @return array of grade_item ids this one depends on
1115 function depends_on() {
1118 if ($this->is_locked()) {
1119 // locked items do not need to be regraded
1123 if ($this->is_calculated()) {
1124 if (preg_match_all('/##gi(\d+)##/', $this->calculation
, $matches)) {
1125 return array_unique($matches[1]); // remove duplicates
1130 } else if ($grade_category = $this->load_item_category()) {
1131 //only items with numeric or scale values can be aggregated
1132 if ($this->gradetype
!= GRADE_TYPE_VALUE
and $this->gradetype
!= GRADE_TYPE_SCALE
) {
1136 if (empty($CFG->enableoutcomes
) or $grade_category->aggregateoutcomes
) {
1139 $outcomes_sql = "AND gi.outcomeid IS NULL";
1142 $sql = "SELECT gi.id
1143 FROM {$CFG->prefix}grade_items gi
1144 WHERE gi.categoryid = {$grade_category->id}
1145 AND (gi.gradetype = ".GRADE_TYPE_VALUE
." OR gi.gradetype = ".GRADE_TYPE_SCALE
.")
1151 FROM {$CFG->prefix}grade_items gi, {$CFG->prefix}grade_categories gc
1152 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1153 AND gc.parent = {$grade_category->id}
1154 AND (gi.gradetype = ".GRADE_TYPE_VALUE
." OR gi.gradetype = ".GRADE_TYPE_SCALE
.")
1157 if ($children = get_records_sql($sql)) {
1158 return array_keys($children);
1169 * Refetch grades from moudles, plugins.
1170 * @param int $userid optional, one user only
1172 function refresh_grades($userid=0) {
1173 if ($this->itemtype
== 'mod') {
1174 if ($this->is_outcome_item()) {
1179 if (!$activity = get_record($this->itemmodule
, 'id', $this->iteminstance
)) {
1180 debugging('Can not find activity');
1184 if (! $cm = get_coursemodule_from_instance($this->itemmodule
, $activity->id
, $this->courseid
)) {
1185 debuggin('Can not find course module');
1189 $activity->modname
= $this->itemmodule
;
1190 $activity->cmidnumber
= $cm->idnumber
;
1192 grade_update_mod_grades($activity);
1197 * Updates final grade value for given user, this is a only way to update final
1198 * grades from gradebook and import because it logs the change in history table
1199 * and deals with overridden flag. This flag is set to prevent later overriding
1200 * from raw grades submitted from modules.
1202 * @param int $userid the graded user
1203 * @param mixed $finalgrade float value of final grade - false means do not change
1204 * @param string $howmodified modification source
1205 * @param string $note optional note
1206 * @param mixed $feedback teachers feedback as string - false means do not change
1207 * @param int $feedbackformat
1208 * @return boolean success
1209 * TODO Allow for a change of feedback without a change of finalgrade. Currently I get notice about uninitialised $result
1211 function update_final_grade($userid, $finalgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE
, $usermodified=null) {
1213 if (empty($usermodified)) {
1214 $usermodified = $USER->id
;
1217 // no grading used or locked
1218 if ($this->gradetype
== GRADE_TYPE_NONE
or $this->is_locked()) {
1222 if (!$grade = grade_grade
::fetch(array('itemid'=>$this->id
, 'userid'=>$userid))) {
1223 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$userid), false);
1225 $grade->grade_item
=& $this; // prevent db fetching of this grade_item
1227 if ($grade->is_locked()) {
1228 // do not update locked grades at all
1232 $locktime = $grade->get_locktime();
1233 if ($locktime and $locktime < time()) {
1234 // do not update grades that should be already locked and force regrade
1235 $this->force_regrading();
1239 $oldgrade = new object();
1240 $oldgrade->finalgrade
= $grade->finalgrade
;
1241 $oldgrade->rawgrade
= $grade->rawgrade
;
1242 $oldgrade->rawgrademin
= $grade->rawgrademin
;
1243 $oldgrade->rawgrademax
= $grade->rawgrademax
;
1244 $oldgrade->rawscaleid
= $grade->rawscaleid
;
1245 $oldgrade->overridden
= $grade->overridden
;
1247 if ($finalgrade !== false) {
1248 if (!is_null($finalgrade)) {
1249 $grade->finalgrade
= bounded_number($this->grademin
, $finalgrade, $this->grademax
);
1251 $grade->finalgrade
= $finalgrade;
1254 if ($this->is_manual_item() and !$this->is_calculated()) {
1255 // no overriding on manual grades - raw not used
1257 } else if ($this->is_outcome_item() and !$this->is_calculated()) {
1258 // no updates of raw grades for outcomes - raw grades not used
1260 } else if (!$this->is_normal_item() or $this->plusfactor
!= 0 or $this->multfactor
!= 1
1261 or !events_is_registered('grade_updated', $this->itemtype
.'/'.$this->itemmodule
)) {
1262 // we can not update the raw grade - flag it as overridden
1263 if (!$grade->overridden
) {
1264 $grade->overridden
= time();
1268 $grade->rawgrade
= $finalgrade;
1269 // copy current grademin/max and scale
1270 $grade->rawgrademin
= $this->grademin
;
1271 $grade->rawgrademax
= $this->grademax
;
1272 $grade->rawscaleid
= $this->scaleid
;
1276 if (empty($grade->id
)) {
1277 $result = (boolean
)$grade->insert($source);
1279 } else if ($grade->finalgrade
!== $oldgrade->finalgrade
1280 or $grade->rawgrade
!== $oldgrade->rawgrade
1281 or $grade->rawgrademin
!== $oldgrade->rawgrademin
1282 or $grade->rawgrademax
!== $oldgrade->rawgrademax
1283 or $grade->rawscaleid
!== $oldgrade->rawscaleid
1284 or $grade->overridden
!== $oldgrade->overridden
) {
1286 $result = $grade->update($source);
1292 // do we have comment from teacher?
1293 if ($result and $feedback !== false) {
1294 $result = $grade->update_feedback($feedback, $feedbackformat, $usermodified);
1297 if ($this->is_course_item() and !$this->needsupdate
) {
1298 if (!grade_regrade_final_grades($this->courseid
, $userid, $this)) {
1299 $this->force_regrading();
1302 } else if (!$this->needsupdate
) {
1303 $course_item = grade_item
::fetch_course_item($this->courseid
);
1304 if (!$course_item->needsupdate
) {
1305 if (!grade_regrade_final_grades($this->courseid
, $userid, $this)) {
1306 $this->force_regrading();
1309 $this->force_regrading();
1313 // no events for overridden items and outcomes
1314 if ($result and !$grade->overridden
and $this->itemnumber
< 1000) {
1315 $this->trigger_raw_updated($grade, $source);
1323 * Updates raw grade value for given user, this is a only way to update raw
1324 * grades from external source (modules, etc.),
1325 * because it logs the change in history table and deals with final grade recalculation.
1327 * @param int $userid the graded user
1328 * @param mixed $rawgrade float value of raw grade - false means do not change
1329 * @param string $howmodified modification source
1330 * @param string $note optional note
1331 * @param mixed $feedback teachers feedback as string - false means do not change
1332 * @param int $feedbackformat
1333 * @return boolean success
1335 function update_raw_grade($userid, $rawgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE
, $usermodified=null) {
1338 if (empty($usermodified)) {
1339 $usermodified = $USER->id
;
1342 // calculated grades can not be updated; course and category can not be updated because they are aggregated
1343 if ($this->is_calculated() or $this->is_outcome_item() or !$this->is_normal_item()
1344 or $this->gradetype
== GRADE_TYPE_NONE
or $this->is_locked()) {
1348 if (!$grade = grade_grade
::fetch(array('itemid'=>$this->id
, 'userid'=>$userid))) {
1349 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$userid), false);
1351 $grade->grade_item
=& $this; // prevent db fetching of this grade_item
1353 if ($grade->is_locked()) {
1354 // do not update locked grades at all
1358 $locktime = $grade->get_locktime();
1359 if ($locktime and $locktime < time()) {
1360 // do not update grades that should be already locked and force regrade
1361 $this->force_regrading();
1365 $oldgrade = new object();
1366 $oldgrade->finalgrade
= $grade->finalgrade
;
1367 $oldgrade->rawgrade
= $grade->rawgrade
;
1368 $oldgrade->rawgrademin
= $grade->rawgrademin
;
1369 $oldgrade->rawgrademax
= $grade->rawgrademax
;
1370 $oldgrade->rawscaleid
= $grade->rawscaleid
;
1372 // fist copy current grademin/max and scale
1373 $grade->rawgrademin
= $this->grademin
;
1374 $grade->rawgrademax
= $this->grademax
;
1375 $grade->rawscaleid
= $this->scaleid
;
1377 if ($rawgrade !== false) {
1378 $grade->rawgrade
= $rawgrade;
1381 if (empty($grade->id
)) {
1382 $result = (boolean
)$grade->insert($source);
1384 } else if ($grade->finalgrade
!== $oldgrade->finalgrade
1385 or $grade->rawgrade
!== $oldgrade->rawgrade
1386 or $grade->rawgrademin
!== $oldgrade->rawgrademin
1387 or $grade->rawgrademax
!== $oldgrade->rawgrademax
1388 or $grade->rawscaleid
!== $oldgrade->rawscaleid
) {
1390 $result = $grade->update($source);
1396 // do we have comment from teacher?
1397 if ($result and $feedback !== false) {
1398 $result = $grade->update_feedback($feedback, $feedbackformat, $usermodified);
1401 if (!$this->needsupdate
) {
1402 $course_item = grade_item
::fetch_course_item($this->courseid
);
1403 if (!$course_item->needsupdate
) {
1404 if (!grade_regrade_final_grades($this->courseid
, $userid, $this)) {
1405 $this->force_regrading();
1408 $this->force_regrading();
1412 // no events for outcomes
1413 if ($result and $this->itemnumber
< 1000) {
1414 $this->trigger_raw_updated($grade, $source);
1421 * Internal function used by update_final/raw_grade() only.
1423 function trigger_raw_updated($grade, $source) {
1425 require_once($CFG->libdir
.'/eventslib.php');
1427 // trigger grade_updated event notification
1428 $eventdata = new object();
1430 $eventdata->source
= $source;
1431 $eventdata->itemid
= $this->id
;
1432 $eventdata->courseid
= $this->courseid
;
1433 $eventdata->itemtype
= $this->itemtype
;
1434 $eventdata->itemmodule
= $this->itemmodule
;
1435 $eventdata->iteminstance
= $this->iteminstance
;
1436 $eventdata->itemnumber
= $this->itemnumber
;
1437 $eventdata->idnumber
= $this->idnumber
;
1438 $eventdata->userid
= $grade->userid
;
1439 $eventdata->rawgrade
= $grade->rawgrade
;
1441 // load existing text annotation
1442 if ($grade_text = $grade->load_text()) {
1443 $eventdata->feedback
= $grade_text->feedback
;
1444 $eventdata->feedbackformat
= $grade_text->feedbackformat
;
1445 $eventdata->information
= $grade_text->information
;
1446 $eventdata->informationformat
= $grade_text->informationformat
;
1449 events_trigger('grade_updated', $eventdata);
1453 * Calculates final grade values using the formula in calculation property.
1454 * The parameters are taken from final grades of grade items in current course only.
1455 * @return boolean false if error
1457 function compute($userid=null) {
1460 if (!$this->is_calculated()) {
1464 require_once($CFG->libdir
.'/mathslib.php');
1466 if ($this->is_locked()) {
1467 return true; // no need to recalculate locked items
1471 $useditems = $this->depends_on();
1473 // prepare formula and init maths library
1474 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation
);
1475 $this->formula
= new calc_formula($formula);
1477 // where to look for final grades?
1478 // this itemid is added so that we use only one query for source and final grades
1479 $gis = implode(',', array_merge($useditems, array($this->id
)));
1482 $usersql = "AND g.userid=$userid";
1488 FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
1489 WHERE gi.id = g.itemid AND gi.courseid={$this->courseid} AND gi.id IN ($gis) $usersql
1494 // group the grades by userid and use formula on the group
1495 if ($rs = get_recordset_sql($sql)) {
1496 if ($rs->RecordCount() > 0) {
1498 $grade_records = array();
1500 while ($used = rs_fetch_next_record($rs)) {
1501 if ($used->userid
!= $prevuser) {
1502 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1505 $prevuser = $used->userid
;
1506 $grade_records = array();
1509 if ($used->itemid
== $this->id
) {
1512 $grade_records['gi'.$used->itemid
] = $used->finalgrade
;
1514 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1525 * internal function - does the final grade calculation
1527 function use_formula($userid, $params, $useditems, $oldgrade) {
1528 if (empty($userid)) {
1532 // add missing final grade values
1533 // not graded (null) is counted as 0 - the spreadsheet way
1534 foreach($useditems as $gi) {
1535 if (!array_key_exists('gi'.$gi, $params)) {
1536 $params['gi'.$gi] = 0;
1538 $params['gi'.$gi] = (float)$params['gi'.$gi];
1542 // can not use own final grade during calculation
1543 unset($params['gi'.$this->id
]);
1545 // insert final grade - will be needed later anyway
1547 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
1548 $grade->grade_item
=& $this;
1551 $grade = new grade_grade(array('itemid'=>$this->id
, 'userid'=>$userid), false);
1552 $grade->insert('system');
1553 $grade->grade_item
=& $this;
1555 $oldgrade = new object();
1556 $oldgrade->finalgrade
= $grade->finalgrade
;
1557 $oldgrade->rawgrade
= $grade->rawgrade
;
1560 // no need to recalculate locked or overridden grades
1561 if ($grade->is_locked() or $grade->is_overridden()) {
1565 // do the calculation
1566 $this->formula
->set_params($params);
1567 $result = $this->formula
->evaluate();
1569 // no raw grade for calculated grades - only final
1570 $grade->rawgrade
= null;
1573 if ($result === false) {
1574 $grade->finalgrade
= null;
1578 $result = bounded_number($this->grademin
, $result, $this->grademax
);
1579 if ($this->gradetype
== GRADE_TYPE_SCALE
) {
1580 $result = round($result+
0.00001); // round scales upwards
1582 $grade->finalgrade
= $result;
1585 // update in db if changed
1586 if ( $grade->finalgrade
!== $oldgrade->finalgrade
1587 or $grade->rawgrade
!== $oldgrade->rawgrade
) {
1589 $grade->update('system');
1592 if ($result !== false) {
1593 //lock grade if needed
1596 if ($result === false) {
1605 * Validate the formula.
1606 * @param string $formula
1607 * @return boolean true if calculation possible, false otherwise
1609 function validate_formula($formula) {
1611 require_once($CFG->libdir
.'/mathslib.php');
1613 $formula = grade_item
::normalize_formula($formula, $this->courseid
);
1615 if (empty($formula)) {
1619 if (strpos($formula, '=') !== 0) {
1620 return get_string('errorcalculationnoequal', 'grades');
1623 // prepare formula and init maths library
1624 $formula = preg_replace('/##(gi\d+)##/', '\1', $formula);
1625 $formula = new calc_formula($formula);
1628 $useditems = $this->depends_on();
1630 if (empty($useditems)) {
1631 $grade_items = array();
1634 $gis = implode(',', $useditems);
1637 FROM {$CFG->prefix}grade_items gi
1638 WHERE gi.id IN ($gis) and gi.courseid={$this->courseid}"; // from the same course only!
1640 if (!$grade_items = get_records_sql($sql)) {
1641 $grade_items = array();
1646 foreach ($useditems as $itemid) {
1647 // make sure all grade items exist in this course
1648 if (!array_key_exists($itemid, $grade_items)) {
1651 // use max grade when testing formula, this should be ok in 99.9%
1652 // division by 0 is one of possible problems
1653 $params['gi'.$grade_items[$itemid]->id
] = $grade_items[$itemid]->grademax
;
1656 // do the calculation
1657 $formula->set_params($params);
1658 $result = $formula->evaluate();
1660 // false as result indicates some problem
1661 if ($result === false) {
1662 // TODO: add more error hints
1663 return get_string('errorcalculationunknown', 'grades');