Automatic installer.php lang files by installer_builder (20070726)
[moodle-linuxchix.git] / lib / grade / grade_item.php
blob0dd3a3586ed339635dcf61408dd19e46f5b7427d
1 <?php // $Id$
3 ///////////////////////////////////////////////////////////////////////////
4 // //
5 // NOTICE OF COPYRIGHT //
6 // //
7 // Moodle - Modular Object-Oriented Dynamic Learning Environment //
8 // http://moodle.com //
9 // //
10 // Copyright (C) 2001-2003 Martin Dougiamas http://dougiamas.com //
11 // //
12 // This program is free software; you can redistribute it and/or modify //
13 // it under the terms of the GNU General Public License as published by //
14 // the Free Software Foundation; either version 2 of the License, or //
15 // (at your option) any later version. //
16 // //
17 // This program is distributed in the hope that it will be useful, //
18 // but WITHOUT ANY WARRANTY; without even the implied warranty of //
19 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
20 // GNU General Public License for more details: //
21 // //
22 // http://www.gnu.org/copyleft/gpl.html //
23 // //
24 ///////////////////////////////////////////////////////////////////////////
26 require_once('grade_object.php');
28 /**
29 * Class representing a grade item. It is responsible for handling its DB representation,
30 * modifying and returning its metadata.
32 class grade_item extends grade_object {
33 /**
34 * DB Table (used by grade_object).
35 * @var string $table
37 var $table = 'grade_items';
39 /**
40 * Array of 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');
45 /**
46 * The course this grade_item belongs to.
47 * @var int $courseid
49 var $courseid;
51 /**
52 * The category this grade_item belongs to (optional).
53 * @var int $categoryid
55 var $categoryid;
57 /**
58 * The grade_category object referenced $this->iteminstance (itemtype must be == 'category' or == 'course' in that case).
59 * @var object $item_category
61 var $item_category;
63 /**
64 * The grade_category object referenced by $this->categoryid.
65 * @var object $parent_category
67 var $parent_category;
70 /**
71 * The name of this grade_item (pushed by the module).
72 * @var string $itemname
74 var $itemname;
76 /**
77 * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...
78 * @var string $itemtype
80 var $itemtype;
82 /**
83 * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).
84 * @var string $itemmodule
86 var $itemmodule;
88 /**
89 * ID of the item module
90 * @var int $iteminstance
92 var $iteminstance;
94 /**
95 * Number of the item in a series of multiple grades pushed by an activity.
96 * @var int $itemnumber
98 var $itemnumber;
101 * Info and notes about this item.
102 * @var string $iteminfo
104 var $iteminfo;
107 * Arbitrary idnumber provided by the module responsible.
108 * @var string $idnumber
110 var $idnumber;
113 * Calculation string used for this item.
114 * @var string $calculation
116 var $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.
121 * @var boolean
123 var $calculation_normalized;
125 * Math evaluation object
127 var $formula;
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
139 var $grademax = 100;
142 * Minimum allowable grade.
143 * @var float $grademin
145 var $grademin = 0;
148 * id of the scale, if this grade is based on a scale.
149 * @var int $scaleid
151 var $scaleid;
154 * A grade_scale object (referenced by $this->scaleid).
155 * @var object $scale
157 var $scale;
160 * The id of the optional grade_outcome associated with this grade_item.
161 * @var int $outcomeid
163 var $outcomeid;
166 * The grade_outcome this grade is associated with, if applicable.
167 * @var object $outcome
169 var $outcome;
172 * grade required to pass. (grademin <= gradepass <= grademax)
173 * @var float $gradepass
175 var $gradepass = 0;
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
187 var $plusfactor = 0;
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
199 var $sortorder = 0;
202 * 0 if visible, 1 always hidden or date not visible until
203 * @var int $hidden
205 var $hidden = 0;
208 * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.
209 * @var int $locked
211 var $locked = 0;
214 * Date after which the grade will be locked. Empty means no automatic locking.
215 * @var int $locktime
217 var $locktime = 0;
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
233 $this->load_scale();
235 if ($this->qualifies_for_regrading()) {
236 $this->force_regrading();
239 return parent::update($source);
243 * Compares the values held by this object with those of the matching record in DB, and returns
244 * whether or not these differences are sufficient to justify an update of all parent objects.
245 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
246 * @return boolean
248 function qualifies_for_regrading() {
249 if (empty($this->id)) {
250 return false;
253 $db_item = new grade_item(array('id' => $this->id));
255 $calculationdiff = $db_item->calculation != $this->calculation;
256 $categorydiff = $db_item->categoryid != $this->categoryid;
257 $gradetypediff = $db_item->gradetype != $this->gradetype;
258 $grademaxdiff = $db_item->grademax != $this->grademax;
259 $grademindiff = $db_item->grademin != $this->grademin;
260 $scaleiddiff = $db_item->scaleid != $this->scaleid;
261 $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;
262 $multfactordiff = $db_item->multfactor != $this->multfactor;
263 $plusfactordiff = $db_item->plusfactor != $this->plusfactor;
264 $acoefdiff = $db_item->aggregationcoef != $this->aggregationcoef;
266 $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time
267 $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking
269 return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff
270 || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff
271 || $lockeddiff || $acoefdiff);
275 * Finds and returns a grade_item instance based on params.
276 * @static
278 * @param array $params associative arrays varname=>value
279 * @return object grade_item instance or false if none found.
281 function fetch($params) {
282 return grade_object::fetch_helper('grade_items', 'grade_item', $params);
286 * Finds and returns all grade_item instances based on params.
287 * @static
289 * @param array $params associative arrays varname=>value
290 * @return array array of grade_item insatnces or false if none found.
292 function fetch_all($params) {
293 return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);
297 * Delete all grades and force_regrading of parent category.
298 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
299 * @return boolean success
301 function delete($source=null) {
302 if ($this->is_course_item()) {
303 debuggin('Can not delete course or category item!');
304 return false;
307 $this->force_regrading();
309 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
310 foreach ($grades as $grade) {
311 $grade->delete($source);
315 return parent::delete($source);
319 * In addition to perform parent::insert(), calls force_regrading() method too.
320 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
321 * @return int PK ID if successful, false otherwise
323 function insert($source=null) {
324 global $CFG;
326 if (empty($this->courseid)) {
327 error('Can not insert grade item without course id!');
330 // load scale if needed
331 $this->load_scale();
333 // add parent category if needed
334 if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {
335 $course_category = grade_category::fetch_course_category($this->courseid);
336 $this->categoryid = $course_category->id;
340 // always place the new items at the end, move them after insert if needed
341 $last_sortorder = get_field_select('grade_items', 'MAX(sortorder)', "courseid = {$this->courseid}");
342 if (!empty($last_sortorder)) {
343 $this->sortorder = $last_sortorder + 1;
344 } else {
345 $this->sortorder = 1;
348 // If not set, generate an idnumber from itemmodule and iteminstance
349 if (empty($this->idnumber)) {
350 if (!empty($this->itemmodule) && !empty($this->iteminstance)) {
351 $this->idnumber = "$this->itemmodule.$this->iteminstance";
352 } else { // No itemmodule or iteminstance, generate a random idnumber
353 $this->idnumber = rand(0,9999999999); // TODO replace rand() with proper random generator
357 // add proper item numbers to manual items
358 if ($this->itemtype == 'manual') {
359 if (empty($this->itemnumber)) {
360 $this->itemnumber = 0;
362 while (grade_item::fetch(array('courseid'=>$this->courseid, 'itemtype'=>'manual', 'itemnumber'=>$this->itemnumber))) {
363 $this->itemnumber++;
367 if (parent::insert($source)) {
368 // force regrading of items if needed
369 $this->force_regrading();
370 return $this->id;
372 } else {
373 debugging("Could not insert this grade_item in the database!");
374 return false;
379 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific
380 * $userid is given) or the locked state of a specific grade within this item if a specific
381 * $userid is given and the grade_item is unlocked.
383 * @param int $userid
384 * @return boolean Locked state
386 function is_locked($userid=NULL) {
387 if (!empty($this->locked)) {
388 return true;
391 if (!empty($userid)) {
392 if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
393 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
394 return $grade->is_locked();
398 return false;
402 * Locks or unlocks this grade_item and (optionally) all its associated final grades.
403 * @param boolean $update_final Whether to update final grades too
404 * @param boolean $new_state Optional new state. Will use inverse of current state otherwise.
405 * @return boolean true if grade_item all grades updated, false if at least one update fails
407 function set_locked($lockedstate) {
408 if ($lockedstate) {
409 /// setting lock
410 if (!empty($this->locked)) {
411 return true; // already locked
414 if ($this->needsupdate) {
415 return false; // can not lock grade without first calculating final grade
418 $this->locked = time();
419 $this->update();
421 // this could be improved with direct SQL update
422 $result = true;
423 $grades = $this->get_final();
424 foreach($grades as $g) {
425 $grade = new grade_grade($g, false);
426 $grade->grade_item =& $this;
427 if (!$grade->set_locked(true)) {
428 $result = false;
432 return $result;
434 } else {
435 /// removing lock
436 if (empty($this->locked)) {
437 return true; // not locked
440 if (!empty($this->locktime) and $this->locktime < time()) {
441 return false; // can not unlock grade item that should be already locked
444 $this->locked = 0;
445 $this->update();
447 // this could be improved with direct SQL update
448 $result = true;
449 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
450 foreach($grades as $grade) {
451 $grade->grade_item =& $this;
453 if (!empty($grade->locktime) and $grade->locktime < time()) {
454 $result = false; // can not unlock grade that should be already locked
457 if (!$grade->set_locked(false)) {
458 $result = false;
463 return $result;
469 * Set the locktime for this grade.
471 * @param int $locktime timestamp for lock to activate
472 * @return boolean true if sucessful, false if can not set new lock state for grade
474 function set_locktime($locktime) {
476 if ($locktime) {
477 // if current locktime is before, no need to reset
479 if ($this->locktime && $this->locktime <= $locktime) {
480 return true;
484 if ($this->grade_item->needsupdate) {
485 //can not lock grade if final not calculated!
486 return false;
490 $this->locktime = $locktime;
491 $this->update();
493 return true;
495 } else {
497 // remove the locktime timestamp
498 $this->locktime = 0;
500 $this->update();
502 return true;
507 * Returns the hidden state of this grade_item (if the grade_item is hidden OR no specific
508 * $userid is given) or the hidden state of a specific grade within this item if a specific
509 * $userid is given and the grade_item is unhidden.
511 * @param int $userid
512 * @return boolean hidden state
514 function is_hidden($userid=NULL) {
515 if ($this->hidden == 1 or $this->hidden > time()) {
516 return true;
519 if (!empty($userid)) {
520 if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
521 $grade->grade_item =& $this; // prevent db fetching of cached grade_item
522 return $grade->is_hidden();
526 return false;
530 * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.
531 * @param int $hidden new hidden status
532 * @return void
534 function set_hidden($hidden) {
535 $this->hidden = $hidden;
536 $this->update();
538 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {
539 foreach($grades as $grade) {
540 $grade->grade_item =& $this;
541 $grade->set_hidden($hidden);
547 * Mark regrading as finished successfully.
549 function regrading_finished() {
550 $this->needsupdate = 0;
551 //do not use $this->update() because we do not want this logged in grade_item_history
552 set_field('grade_items', 'needsupdate', 0, 'id', $this->id);
554 if (!empty($this->locktime) and empty($this->locked) and $this->locktime < time()) {
555 // time to lock this grade_item
556 $this->set_locked(true);
561 * Performs the necessary calculations on the grades_final referenced by this grade_item.
562 * Also resets the needsupdate flag once successfully performed.
564 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
565 * because the regrading must be done in correct order!!
567 * @return boolean true if ok, error string otherwise
569 function regrade_final_grades($userid=null) {
570 global $CFG;
572 // locked grade items already have correct final grades
573 if ($this->is_locked()) {
574 return true;
577 // calculation produces final value using formula from other final values
578 if ($this->is_calculated()) {
579 if ($this->compute($userid)) {
580 return true;
581 } else {
582 return "Could not calculate grades for grade item"; // TODO: improve and localize
585 // aggregate the category grade
586 } else if ($this->is_category_item() or $this->is_course_item()) {
587 // aggregate category grade item
588 $category = $this->get_item_category();
589 $category->grade_item =& $this;
590 if ($category->generate_grades($userid)) {
591 return true;
592 } else {
593 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
595 } else if ($this->is_manual_item()) {
596 // manual items track only final grades, no raw grades
597 return true;
600 // normal grade item - just new final grades
601 $result = true;
602 if ($userid) {
603 $rs = get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid");
604 } else {
605 $rs = get_recordset('grade_grades', 'itemid', $this->id);
607 if ($rs) {
608 if ($rs->RecordCount() > 0) {
609 while ($grade_record = rs_fetch_next_record($rs)) {
610 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
611 // this grade is locked - final grade must be ok
612 continue;
615 $grade = new grade_grade($grade_record, false);
616 $grade->finalgrade = $this->adjust_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
618 if ($grade_record->finalgrade !== $grade->finalgrade) {
619 if (!$grade->update('system')) {
620 $result = "Internal error updating final grade";
624 // time to lock this grade?
625 if (!empty($grade->locktime) and empty($grade->locked) and $grade->locktime < time()) {
626 $grade->locked = time();
627 $grade->grade_item =& $this;
628 $grade->set_locked(true);
632 rs_close($rs);
635 return $result;
639 * Given a float grade value or integer grade scale, applies a number of adjustment based on
640 * grade_item variables and returns the result.
641 * @param object $rawgrade The raw grade value.
642 * @return mixed
644 function adjust_grade($rawgrade, $rawmin, $rawmax) {
645 if (is_null($rawgrade)) {
646 return null;
649 if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade
651 if ($this->grademax < $this->grademin) {
652 return null;
655 if ($this->grademax == $this->grademin) {
656 return $this->grademax; // no range
659 // Standardise score to the new grade range
660 // NOTE: this is not compatible with current assignment grading
661 if ($rawmin != $this->grademin or $rawmax != $this->grademax) {
662 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
665 // Apply other grade_item factors
666 $rawgrade *= $this->multfactor;
667 $rawgrade += $this->plusfactor;
669 return bounded_number($this->grademin, $rawgrade, $this->grademax);
671 } else if($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value
672 if (empty($this->scale)) {
673 $this->load_scale();
676 if ($this->grademax < 0) {
677 return null; // scale not present - no grade
680 if ($this->grademax == 0) {
681 return $this->grademax; // only one option
684 // Convert scale if needed
685 // NOTE: this is not compatible with current assignment grading
686 if ($rawmin != $this->grademin or $rawmax != $this->grademax) {
687 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);
690 return (int)bounded_number(0, round($rawgrade+0.00001), $this->grademax);
693 } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value
694 // somebody changed the grading type when grades already existed
695 return null;
697 } else {
698 dubugging("Unkown grade type");
699 return null;;
704 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.
705 * @return void
707 function force_regrading() {
708 $this->needsupdate = 1;
709 //mark this item and course item only - categories and calculated items are always regraded
710 $wheresql = "(itemtype='course' OR id={$this->id}) AND courseid={$this->courseid}";
711 set_field_select('grade_items', 'needsupdate', 1, $wheresql);
715 * Instantiates a grade_scale object whose data is retrieved from the DB,
716 * if this item's scaleid variable is set.
717 * @return object grade_scale or null if no scale used
719 function load_scale() {
720 if ($this->gradetype != GRADE_TYPE_SCALE) {
721 $this->scaleid = null;
724 if (!empty($this->scaleid)) {
725 //do not load scale if already present
726 if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {
727 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));
728 $this->scale->load_items();
731 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we
732 // stay with the current min=1 max=count(scaleitems)
733 $this->grademax = count($this->scale->scale_items);
734 $this->grademin = 1;
736 } else {
737 $this->scale = null;
740 return $this->scale;
744 * Instantiates a grade_outcome object whose data is retrieved from the DB,
745 * if this item's outcomeid variable is set.
746 * @return object grade_outcome
748 function load_outcome() {
749 if (!empty($this->outcomeid)) {
750 $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));
752 return $this->outcome;
756 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)
757 * or category attached to category item.
759 * @return mixed grade_category object if applicable, false if course item
761 function get_parent_category() {
762 if ($this->is_category_item() or $this->is_course_item()) {
763 return $this->get_item_category();
765 } else {
766 return grade_category::fetch(array('id'=>$this->categoryid));
771 * Calls upon the get_parent_category method to retrieve the grade_category object
772 * from the DB and assigns it to $this->parent_category. It also returns the object.
773 * @return object Grade_category
775 function load_parent_category() {
776 if (empty($this->parent_category->id)) {
777 $this->parent_category = $this->get_parent_category();
779 return $this->parent_category;
783 * Returns the grade_category for category item
785 * @return mixed grade_category object if applicable, false otherwise
787 function get_item_category() {
788 if (!$this->is_course_item() and !$this->is_category_item()) {
789 return false;
791 return grade_category::fetch(array('id'=>$this->iteminstance));
795 * Calls upon the get_item_category method to retrieve the grade_category object
796 * from the DB and assigns it to $this->item_category. It also returns the object.
797 * @return object Grade_category
799 function load_item_category() {
800 if (empty($this->category->id)) {
801 $this->item_category = $this->get_item_category();
803 return $this->item_category;
807 * Is the grade item associated with category?
808 * @return boolean
810 function is_category_item() {
811 return ($this->itemtype == 'category');
815 * Is the grade item associated with course?
816 * @return boolean
818 function is_course_item() {
819 return ($this->itemtype == 'course');
823 * Is this a manualy graded item?
824 * @return boolean
826 function is_manual_item() {
827 return ($this->itemtype == 'manual');
831 * Is the grade item normal - associated with module, plugin or something else?
832 * @return boolean
834 function is_normal_item() {
835 return ($this->itemtype != 'course' and $this->itemtype != 'category' and $this->itemtype != 'manual');
839 * Returns grade item associated with the course
840 * @param int $courseid
841 * @return course item object
843 function fetch_course_item($courseid) {
844 if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
845 return $course_item;
848 // first get category - it creates the associated grade item
849 $course_category = grade_category::fetch_course_category($courseid);
851 return grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'));
855 * Is grading object editable?
856 * @return boolean
858 function is_editable() {
859 return true;
863 * Checks if grade calculated. Returns this object's calculation.
864 * @return boolean true if grade item calculated.
866 function is_calculated() {
867 if (empty($this->calculation)) {
868 return false;
872 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),
873 * we would have to fetch all course grade items to find out the ids.
874 * Also if user changes the idnumber the formula does not need to be updated.
877 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)
878 if (!$this->calculation_normalized and preg_match('/##gi\d+##/', $this->calculation)) {
879 $this->set_calculation($this->calculation);
882 return !empty($this->calculation);
886 * Returns calculation string if grade calculated.
887 * @return mixed string if calculation used, null if not
889 function get_calculation() {
890 if ($this->is_calculated()) {
891 return grade_item::denormalize_formula($this->calculation, $this->courseid);
893 } else {
894 return NULL;
899 * Sets this item's calculation (creates it) if not yet set, or
900 * updates it if already set (in the DB). If no calculation is given,
901 * the calculation is removed.
902 * @param string $formula string representation of formula used for calculation
903 * @return boolean success
905 function set_calculation($formula) {
906 $this->calculation = grade_item::normalize_formula($formula, $this->courseid);
907 $this->calculation_normalized = true;
908 return $this->update();
912 * Denormalizes the calculation formula to [idnumber] form
913 * @static
914 * @param string $formula
915 * @return string denormalized string
917 function denormalize_formula($formula, $courseid) {
918 if (empty($formula)) {
919 return '';
922 // denormalize formula - convert ##giXX## to [[idnumber]]
923 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {
924 foreach ($matches[1] as $id) {
925 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {
926 if (!empty($grade_item->idnumber)) {
927 $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);
933 return $formula;
938 * Normalizes the calculation formula to [#giXX#] form
939 * @static
940 * @param string $formula
941 * @return string normalized string
943 function normalize_formula($formula, $courseid) {
944 $formula = trim($formula);
946 if (empty($formula)) {
947 return NULL;
951 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]
952 if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {
953 foreach ($grade_items as $grade_item) {
954 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);
958 return $formula;
962 * Returns the final values for this grade item (as imported by module or other source).
963 * @param int $userid Optional: to retrieve a single final grade
964 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
966 function get_final($userid=NULL) {
967 if ($userid) {
968 if ($user = get_record('grade_grades', 'itemid', $this->id, 'userid', $userid)) {
969 return $user;
972 } else {
973 if ($grades = get_records('grade_grades', 'itemid', $this->id)) {
974 //TODO: speed up with better SQL
975 $result = array();
976 foreach ($grades as $grade) {
977 $result[$grade->userid] = $grade;
979 return $result;
980 } else {
981 return array();
987 * Get (or create if not exist yet) grade for this user
988 * @param int $userid
989 * @return object grade_grade object instance
991 function get_grade($userid) {
992 if (empty($this->id)) {
993 debugging('Can not use before insert');
994 return false;
997 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));
998 if (empty($grade->id)) {
999 $grade->insert();
1002 return $grade;
1006 * Returns the sortorder of this grade_item. This method is also available in
1007 * grade_category, for cases where the object type is not know.
1008 * @return int Sort order
1010 function get_sortorder() {
1011 return $this->sortorder;
1015 * Sets the sortorder of this grade_item. This method is also available in
1016 * grade_category, for cases where the object type is not know.
1017 * @param int $sortorder
1018 * @return void
1020 function set_sortorder($sortorder) {
1021 $this->sortorder = $sortorder;
1022 $this->update();
1025 function move_after_sortorder($sortorder) {
1026 global $CFG;
1028 //make some room first
1029 $sql = "UPDATE {$CFG->prefix}grade_items
1030 SET sortorder = sortorder + 1
1031 WHERE sortorder > $sortorder AND courseid = {$this->courseid}";
1032 execute_sql($sql, false);
1034 $this->set_sortorder($sortorder + 1);
1038 * Returns the most descriptive field for this object. This is a standard method used
1039 * when we do not know the exact type of an object.
1040 * @return string name
1042 function get_name() {
1043 if (!empty($this->itemname)) {
1044 return $this->itemname;
1046 } else if ($this->is_course_item()) {
1047 return get_string('total');
1049 } else {
1050 return get_string('grade');
1055 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.
1056 * @param int $parentid
1057 * @return boolean success;
1059 function set_parent($parentid) {
1060 if ($this->is_course_item() or $this->is_category_item()) {
1061 error('Can not set parent for category or course item!');
1064 if ($this->categoryid == $parentid) {
1065 return true;
1068 // find parent and check course id
1069 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1070 return false;
1073 $this->force_regrading();
1075 // set new parent
1076 $this->categoryid = $parent_category->id;
1077 $this->parent_category =& $parent_category;
1079 return $this->update();
1083 * Finds out on which other items does this depend directly when doing calculation or category agregation
1084 * @return array of grade_item ids this one depends on
1086 function depends_on() {
1087 global $CFG;
1089 if ($this->is_locked()) {
1090 // locked items do not need to be regraded
1091 return array();
1094 if ($this->is_calculated()) {
1095 if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {
1096 return array_unique($matches[1]); // remove duplicates
1097 } else {
1098 return array();
1101 } else if ($grade_category = $this->load_item_category()) {
1102 $sql = "SELECT gi.id
1103 FROM {$CFG->prefix}grade_items gi
1104 WHERE gi.categoryid ={$grade_category->id}
1106 UNION
1108 SELECT gi.id
1109 FROM {$CFG->prefix}grade_items gi, {$CFG->prefix}grade_categories gc
1110 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id
1111 AND gc.parent = {$grade_category->id}";
1113 if ($children = get_records_sql($sql)) {
1114 return array_keys($children);
1115 } else {
1116 return array();
1119 } else {
1120 return array();
1125 * Updates final grade value for given user, this is a only way to update final
1126 * grades from gradebook and import because it logs the change in history table
1127 * and deals with overridden flag. This flag is set to prevent later overriding
1128 * from raw grades submitted from modules.
1130 * @param int $userid the graded user
1131 * @param mixed $finalgrade float value of final grade - false means do not change
1132 * @param string $howmodified modification source
1133 * @param string $note optional note
1134 * @param mixed $feedback teachers feedback as string - false means do not change
1135 * @param int $feedbackformat
1136 * @return boolean success
1137 * TODO Allow for a change of feedback without a change of finalgrade. Currently I get notice about uninitialised $result
1139 function update_final_grade($userid, $finalgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
1140 global $USER;
1141 if (empty($usermodified)) {
1142 $usermodified = $USER->id;
1145 // no grading used or locked
1146 if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1147 return false;
1150 if (!$grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
1151 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
1154 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1155 $oldgrade = new object();
1156 $oldgrade->finalgrade = $grade->finalgrade;
1157 $oldgrade->rawgrade = $grade->rawgrade;
1158 $oldgrade->rawgrademin = $grade->rawgrademin;
1159 $oldgrade->rawgrademax = $grade->rawgrademax;
1160 $oldgrade->rawscaleid = $grade->rawscaleid;
1161 $oldgrade->overridden = $grade->overridden;
1163 if ($grade->is_locked()) {
1164 // do not update locked grades at all
1165 return false;
1168 if (!empty($grade->locktime) and $grade->locktime < time()) {
1169 // do not update grades that should be already locked
1170 // this does not solve all problems, cron is still needed to recalculate the final grades periodically
1171 return false;
1174 if ($finalgrade !== false) {
1175 if (!is_null($finalgrade)) {
1176 $grade->finalgrade = bounded_number($this->grademin, $finalgrade, $this->grademax);
1177 } else {
1178 $grade->finalgrade = $finalgrade;
1181 // if we can update the raw grade, do update it
1182 if (!$this->is_normal_item() or $this->plusfactor != 0 or $this->multfactor != 1
1183 or !events_is_registered('grade_updated', $this->itemtype.'/'.$this->itemmodule)) {
1184 if (!$grade->overridden) {
1185 $grade->overridden = time();
1187 } else {
1188 $grade->rawgrade = $finalgrade;
1189 // copy current grademin/max and scale
1190 $grade->rawgrademin = $this->grademin;
1191 $grade->rawgrademax = $this->grademax;
1192 $grade->rawscaleid = $this->scaleid;
1196 if (empty($grade->id)) {
1197 $result = (boolean)$grade->insert($source);
1199 } else if ($grade->finalgrade !== $oldgrade->finalgrade
1200 or $grade->rawgrade !== $oldgrade->rawgrade
1201 or $grade->rawgrademin !== $oldgrade->rawgrademin
1202 or $grade->rawgrademax !== $oldgrade->rawgrademax
1203 or $grade->rawscaleid !== $oldgrade->rawscaleid
1204 or $grade->overridden !== $oldgrade->overridden) {
1206 $result = $grade->update($source);
1208 } else {
1209 $result = true;
1212 // do we have comment from teacher?
1213 if ($result and $feedback !== false) {
1214 $result = $grade->update_feedback($feedback, $feedbackformat, $usermodified);
1217 if ($this->is_course_item() and !$this->needsupdate) {
1218 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
1219 $this->force_regrading();
1222 } else if (!$this->needsupdate) {
1223 $course_item = grade_item::fetch_course_item($this->courseid);
1224 if (!$course_item->needsupdate) {
1225 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
1226 $this->force_regrading();
1228 } else {
1229 $this->force_regrading();
1233 if ($result and !$grade->overridden) {
1234 $this->trigger_raw_updated($grade, $source);
1237 return $result;
1242 * Updates raw grade value for given user, this is a only way to update raw
1243 * grades from external source (modules, etc.),
1244 * because it logs the change in history table and deals with final grade recalculation.
1246 * @param int $userid the graded user
1247 * @param mixed $rawgrade float value of raw grade - false means do not change
1248 * @param string $howmodified modification source
1249 * @param string $note optional note
1250 * @param mixed $feedback teachers feedback as string - false means do not change
1251 * @param int $feedbackformat
1252 * @return boolean success
1254 function update_raw_grade($userid, $rawgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
1255 global $USER;
1257 if (empty($usermodified)) {
1258 $usermodified = $USER->id;
1261 // calculated grades can not be updated; course and category can not be updated because they are aggregated
1262 if ($this->is_calculated() or !$this->is_normal_item() or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
1263 return false;
1266 if (!$grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {
1267 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
1270 $grade->grade_item =& $this; // prevent db fetching of this grade_item
1271 $oldgrade = new object();
1272 $oldgrade->finalgrade = $grade->finalgrade;
1273 $oldgrade->rawgrade = $grade->rawgrade;
1274 $oldgrade->rawgrademin = $grade->rawgrademin;
1275 $oldgrade->rawgrademax = $grade->rawgrademax;
1276 $oldgrade->rawscaleid = $grade->rawscaleid;
1278 if ($grade->is_locked()) {
1279 // do not update locked grades at all
1280 return false;
1283 if (!empty($grade->locktime) and $grade->locktime < time()) {
1284 // do not update grades that should be already locked
1285 // this does not solve all problems, cron is still needed to recalculate the final grades periodically
1286 return false;
1289 // fist copy current grademin/max and scale
1290 $grade->rawgrademin = $this->grademin;
1291 $grade->rawgrademax = $this->grademax;
1292 $grade->rawscaleid = $this->scaleid;
1294 if ($rawgrade !== false) {
1295 $grade->rawgrade = $rawgrade;
1298 if (empty($grade->id)) {
1299 $result = (boolean)$grade->insert($source);
1301 } else if ($grade->finalgrade !== $oldgrade->finalgrade
1302 or $grade->rawgrade !== $oldgrade->rawgrade
1303 or $grade->rawgrademin !== $oldgrade->rawgrademin
1304 or $grade->rawgrademax !== $oldgrade->rawgrademax
1305 or $grade->rawscaleid !== $oldgrade->rawscaleid) {
1307 $result = $grade->update($source);
1309 } else {
1310 $result = true;
1313 // do we have comment from teacher?
1314 if ($result and $feedback !== false) {
1315 $result = $grade->update_feedback($feedback, $feedbackformat, $usermodified);
1318 if (!$this->needsupdate) {
1319 $course_item = grade_item::fetch_course_item($this->courseid);
1320 if (!$course_item->needsupdate) {
1321 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {
1322 $this->force_regrading();
1324 } else {
1325 $this->force_regrading();
1329 if ($result) {
1330 $this->trigger_raw_updated($grade, $source);
1333 return $result;
1337 * Internal function used by update_final/raw_grade() only.
1339 function trigger_raw_updated($grade, $source) {
1340 global $CFG;
1341 require_once($CFG->libdir.'/eventslib.php');
1343 // trigger grade_updated event notification
1344 $eventdata = new object();
1346 $eventdata->source = $source;
1347 $eventdata->itemid = $this->id;
1348 $eventdata->courseid = $this->courseid;
1349 $eventdata->itemtype = $this->itemtype;
1350 $eventdata->itemmodule = $this->itemmodule;
1351 $eventdata->iteminstance = $this->iteminstance;
1352 $eventdata->itemnumber = $this->itemnumber;
1353 $eventdata->idnumber = $this->idnumber;
1354 $eventdata->userid = $grade->userid;
1355 $eventdata->rawgrade = $grade->rawgrade;
1357 // load existing text annotation
1358 if ($grade_text = $grade->load_text()) {
1359 $eventdata->feedback = $grade_text->feedback;
1360 $eventdata->feedbackformat = $grade_text->feedbackformat;
1361 $eventdata->information = $grade_text->information;
1362 $eventdata->informationformat = $grade_text->informationformat;
1365 events_trigger('grade_updated', $eventdata);
1369 * Calculates final grade values using the formula in calculation property.
1370 * The parameters are taken from final grades of grade items in current course only.
1371 * @return boolean false if error
1373 function compute($userid=null) {
1374 global $CFG;
1376 if (!$this->is_calculated()) {
1377 return false;
1380 require_once($CFG->libdir.'/mathslib.php');
1382 if ($this->is_locked()) {
1383 return true; // no need to recalculate locked items
1386 // get used items
1387 $useditems = $this->depends_on();
1389 // prepare formula and init maths library
1390 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);
1391 $this->formula = new calc_formula($formula);
1393 // where to look for final grades?
1394 // this itemid is added so that we use only one query for source and final grades
1395 $gis = implode(',', array_merge($useditems, array($this->id)));
1397 if ($userid) {
1398 $usersql = "AND g.userid=$userid";
1399 } else {
1400 $usersql = "";
1403 $sql = "SELECT g.*
1404 FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
1405 WHERE gi.id = g.itemid AND gi.courseid={$this->courseid} AND gi.id IN ($gis) $usersql
1406 ORDER BY g.userid";
1408 $return = true;
1410 // group the grades by userid and use formula on the group
1411 if ($rs = get_recordset_sql($sql)) {
1412 if ($rs->RecordCount() > 0) {
1413 $prevuser = 0;
1414 $grade_records = array();
1415 $oldgrade = null;
1416 while ($used = rs_fetch_next_record($rs)) {
1417 if ($used->userid != $prevuser) {
1418 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1419 $return = false;
1421 $prevuser = $used->userid;
1422 $grade_records = array();
1423 $oldgrade = null;
1425 if ($used->itemid == $this->id) {
1426 $oldgrade = $used;
1428 $grade_records['gi'.$used->itemid] = $used->finalgrade;
1430 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {
1431 $return = false;
1434 rs_close($rs);
1437 return $return;
1441 * internal function - does the final grade calculation
1443 function use_formula($userid, $params, $useditems, $oldgrade) {
1444 if (empty($userid)) {
1445 return true;
1448 // add missing final grade values
1449 // not graded (null) is counted as 0 - the spreadsheet way
1450 foreach($useditems as $gi) {
1451 if (!array_key_exists('gi'.$gi, $params)) {
1452 $params['gi'.$gi] = 0;
1453 } else {
1454 $params['gi'.$gi] = (float)$params['gi'.$gi];
1458 // can not use own final grade during calculation
1459 unset($params['gi'.$this->id]);
1461 // insert final grade - will be needed later anyway
1462 if ($oldgrade) {
1463 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed
1464 $grade->grade_item =& $this;
1466 } else {
1467 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);
1468 $grade->insert('system');
1469 $grade->grade_item =& $this;
1471 $oldgrade = new object();
1472 $oldgrade->finalgrade = $grade->finalgrade;
1473 $oldgrade->rawgrade = $grade->rawgrade;
1476 // no need to recalculate locked or overridden grades
1477 if ($grade->is_locked() or $grade->is_overridden()) {
1478 return true;
1481 // do the calculation
1482 $this->formula->set_params($params);
1483 $result = $this->formula->evaluate();
1485 // no raw grade for calculated grades - only final
1486 $grade->rawgrade = null;
1489 if ($result === false) {
1490 $grade->finalgrade = null;
1492 } else {
1493 // normalize
1494 $result = bounded_number($this->grademin, $result, $this->grademax);
1495 if ($this->gradetype == GRADE_TYPE_SCALE) {
1496 $result = round($result+0.00001); // round scales upwards
1498 $grade->finalgrade = $result;
1501 // update in db if changed
1502 if ( $grade->finalgrade !== $oldgrade->finalgrade
1503 or $grade->rawgrade !== $oldgrade->rawgrade) {
1505 $grade->update('system');
1508 if ($result === false) {
1509 return false;
1510 } else {
1511 return true;
1517 * Validate the formula.
1518 * @param string $formula
1519 * @return boolean true if calculation possible, false otherwise
1521 function validate_formula($formula) {
1522 global $CFG;
1523 require_once($CFG->libdir.'/mathslib.php');
1525 $formula = grade_item::normalize_formula($formula, $this->courseid);
1527 if (empty($formula)) {
1528 return true;
1531 if (strpos($formula, '=') !== 0) {
1532 return get_string('errorcalculationnoequal', 'grades');
1535 // prepare formula and init maths library
1536 $formula = preg_replace('/##(gi\d+)##/', '\1', $formula);
1537 $formula = new calc_formula($formula);
1539 // get used items
1540 $useditems = $this->depends_on();
1542 if (empty($useditems)) {
1543 $grade_items = array();
1545 } else {
1546 $gis = implode(',', $useditems);
1548 $sql = "SELECT gi.*
1549 FROM {$CFG->prefix}grade_items gi
1550 WHERE gi.id IN ($gis) and gi.courseid={$this->courseid}"; // from the same course only!
1552 if (!$grade_items = get_records_sql($sql)) {
1553 $grade_items = array();
1557 $params = array();
1558 foreach ($useditems as $itemid) {
1559 // make sure all grade items exist in this course
1560 if (!array_key_exists($itemid, $grade_items)) {
1561 return false;
1563 // use max grade when testing formula, this should be ok in 99.9%
1564 // division by 0 is one of possible problems
1565 $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;
1568 // do the calculation
1569 $formula->set_params($params);
1570 $result = $formula->evaluate();
1572 // false as result indicates some problem
1573 if ($result === false) {
1574 // TODO: add more error hints
1575 return get_string('errorcalculationunknown', 'grades');
1576 } else {
1577 return true;