fixing previous commit
[moodle.git] / lib / grade / grade_item.php
CommitLineData
e6acec95 1<?php // $Id$\r
2\r
3///////////////////////////////////////////////////////////////////////////\r
4// //\r
5// NOTICE OF COPYRIGHT //\r
6// //\r
7// Moodle - Modular Object-Oriented Dynamic Learning Environment //\r
8// http://moodle.com //\r
9// //\r
10// Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //\r
11// //\r
12// This program is free software; you can redistribute it and/or modify //\r
13// it under the terms of the GNU General Public License as published by //\r
14// the Free Software Foundation; either version 2 of the License, or //\r
15// (at your option) any later version. //\r
16// //\r
17// This program is distributed in the hope that it will be useful, //\r
18// but WITHOUT ANY WARRANTY; without even the implied warranty of //\r
19// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //\r
20// GNU General Public License for more details: //\r
21// //\r
22// http://www.gnu.org/copyleft/gpl.html //\r
23// //\r
24///////////////////////////////////////////////////////////////////////////\r
25\r
26require_once('grade_object.php');\r
27\r
28/**\r
29 * Class representing a grade item. It is responsible for handling its DB representation,\r
30 * modifying and returning its metadata.\r
31 */\r
32class grade_item extends grade_object {\r
33 /**\r
34 * DB Table (used by grade_object).\r
35 * @var string $table\r
36 */\r
37 var $table = 'grade_items';\r
38\r
39 /**\r
40 * Array of required table fields, must start with 'id'.\r
41 * @var array $required_fields\r
42 */\r
43 var $required_fields = array('id', 'courseid', 'categoryid', 'itemname', 'itemtype', 'itemmodule', 'iteminstance',\r
44 'itemnumber', 'iteminfo', 'idnumber', 'calculation', 'gradetype', 'grademax', 'grademin',\r
45 'scaleid', 'outcomeid', 'gradepass', 'multfactor', 'plusfactor', 'aggregationcoef',\r
46 'sortorder', 'display', 'decimals', 'hidden', 'locked', 'locktime', 'needsupdate', 'timecreated',\r
47 'timemodified');\r
48\r
49 /**\r
50 * The course this grade_item belongs to.\r
51 * @var int $courseid\r
52 */\r
53 var $courseid;\r
54\r
55 /**\r
56 * The category this grade_item belongs to (optional).\r
57 * @var int $categoryid\r
58 */\r
59 var $categoryid;\r
60\r
61 /**\r
62 * The grade_category object referenced $this->iteminstance (itemtype must be == 'category' or == 'course' in that case).\r
63 * @var object $item_category\r
64 */\r
65 var $item_category;\r
66\r
67 /**\r
68 * The grade_category object referenced by $this->categoryid.\r
69 * @var object $parent_category\r
70 */\r
71 var $parent_category;\r
72\r
73\r
74 /**\r
75 * The name of this grade_item (pushed by the module).\r
76 * @var string $itemname\r
77 */\r
78 var $itemname;\r
79\r
80 /**\r
81 * e.g. 'category', 'course' and 'mod', 'blocks', 'import', etc...\r
82 * @var string $itemtype\r
83 */\r
84 var $itemtype;\r
85\r
86 /**\r
87 * The module pushing this grade (e.g. 'forum', 'quiz', 'assignment' etc).\r
88 * @var string $itemmodule\r
89 */\r
90 var $itemmodule;\r
91\r
92 /**\r
93 * ID of the item module\r
94 * @var int $iteminstance\r
95 */\r
96 var $iteminstance;\r
97\r
98 /**\r
99 * Number of the item in a series of multiple grades pushed by an activity.\r
100 * @var int $itemnumber\r
101 */\r
102 var $itemnumber;\r
103\r
104 /**\r
105 * Info and notes about this item.\r
106 * @var string $iteminfo\r
107 */\r
108 var $iteminfo;\r
109\r
110 /**\r
111 * Arbitrary idnumber provided by the module responsible.\r
112 * @var string $idnumber\r
113 */\r
114 var $idnumber;\r
115\r
116 /**\r
117 * Calculation string used for this item.\r
118 * @var string $calculation\r
119 */\r
120 var $calculation;\r
121\r
122 /**\r
123 * Indicates if we already tried to normalize the grade calculation formula.\r
124 * This flag helps to minimize db access when broken formulas used in calculation.\r
125 * @var boolean\r
126 */\r
127 var $calculation_normalized;\r
128 /**\r
129 * Math evaluation object\r
130 */\r
131 var $formula;\r
132\r
133 /**\r
134 * The type of grade (0 = none, 1 = value, 2 = scale, 3 = text)\r
135 * @var int $gradetype\r
136 */\r
137 var $gradetype = GRADE_TYPE_VALUE;\r
138\r
139 /**\r
140 * Maximum allowable grade.\r
141 * @var float $grademax\r
142 */\r
143 var $grademax = 100;\r
144\r
145 /**\r
146 * Minimum allowable grade.\r
147 * @var float $grademin\r
148 */\r
149 var $grademin = 0;\r
150\r
151 /**\r
152 * id of the scale, if this grade is based on a scale.\r
153 * @var int $scaleid\r
154 */\r
155 var $scaleid;\r
156\r
157 /**\r
158 * A grade_scale object (referenced by $this->scaleid).\r
159 * @var object $scale\r
160 */\r
161 var $scale;\r
162\r
163 /**\r
164 * The id of the optional grade_outcome associated with this grade_item.\r
165 * @var int $outcomeid\r
166 */\r
167 var $outcomeid;\r
168\r
169 /**\r
170 * The grade_outcome this grade is associated with, if applicable.\r
171 * @var object $outcome\r
172 */\r
173 var $outcome;\r
174\r
175 /**\r
176 * grade required to pass. (grademin <= gradepass <= grademax)\r
177 * @var float $gradepass\r
178 */\r
179 var $gradepass = 0;\r
180\r
181 /**\r
182 * Multiply all grades by this number.\r
183 * @var float $multfactor\r
184 */\r
185 var $multfactor = 1.0;\r
186\r
187 /**\r
188 * Add this to all grades.\r
189 * @var float $plusfactor\r
190 */\r
191 var $plusfactor = 0;\r
192\r
193 /**\r
194 * Aggregation coeficient used for weighted averages\r
195 * @var float $aggregationcoef\r
196 */\r
197 var $aggregationcoef = 0;\r
198\r
199 /**\r
200 * Sorting order of the columns.\r
201 * @var int $sortorder\r
202 */\r
203 var $sortorder = 0;\r
204\r
205 /**\r
206 * Display type of the grades (Real, Percentage, Letter, or default).\r
207 * @var int $display\r
208 */\r
209 var $display = GRADE_DISPLAY_TYPE_DEFAULT;\r
210\r
211 /**\r
212 * The number of digits after the decimal point symbol. Applies only to REAL and PERCENTAGE grade display types.\r
213 * @var int $decimals\r
214 */\r
215 var $decimals = null;\r
216\r
217 /**\r
218 * 0 if visible, 1 always hidden or date not visible until\r
219 * @var int $hidden\r
220 */\r
221 var $hidden = 0;\r
222\r
223 /**\r
224 * Grade item lock flag. Empty if not locked, locked if any value present, usually date when item was locked. Locking prevents updating.\r
225 * @var int $locked\r
226 */\r
227 var $locked = 0;\r
228\r
229 /**\r
230 * Date after which the grade will be locked. Empty means no automatic locking.\r
231 * @var int $locktime\r
232 */\r
233 var $locktime = 0;\r
234\r
235 /**\r
236 * If set, the whole column will be recalculated, then this flag will be switched off.\r
237 * @var boolean $needsupdate\r
238 */\r
239 var $needsupdate = 1;\r
240\r
241 /**\r
242 * Cached dependson array\r
243 */\r
244 var $dependson_cache = null;\r
245\r
246 /**\r
247 * In addition to update() as defined in grade_object, handle the grade_outcome and grade_scale objects.\r
248 * Force regrading if necessary\r
249 * @param string $source from where was the object inserted (mod/forum, manual, etc.)\r
250 * @return boolean success\r
251 */\r
252 function update($source=null) {\r
253 // reset caches\r
254 $this->dependson_cache = null;\r
255\r
256 // Retrieve scale and infer grademax/min from it if needed\r
257 $this->load_scale();\r
258\r
259 // make sure there is not 0 in outcomeid\r
260 if (empty($this->outcomeid)) {\r
261 $this->outcomeid = null;\r
262 }\r
263\r
264 if ($this->qualifies_for_regrading()) {\r
265 $this->force_regrading();\r
266 }\r
267\r
268 return parent::update($source);\r
269 }\r
270\r
271 /**\r
272 * Compares the values held by this object with those of the matching record in DB, and returns\r
273 * whether or not these differences are sufficient to justify an update of all parent objects.\r
274 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.\r
275 * @return boolean\r
276 */\r
277 function qualifies_for_regrading() {\r
278 if (empty($this->id)) {\r
279 return false;\r
280 }\r
281\r
282 $db_item = new grade_item(array('id' => $this->id));\r
283\r
284 $calculationdiff = $db_item->calculation != $this->calculation;\r
285 $categorydiff = $db_item->categoryid != $this->categoryid;\r
286 $gradetypediff = $db_item->gradetype != $this->gradetype;\r
287 $grademaxdiff = $db_item->grademax != $this->grademax;\r
288 $grademindiff = $db_item->grademin != $this->grademin;\r
289 $scaleiddiff = $db_item->scaleid != $this->scaleid;\r
290 $outcomeiddiff = $db_item->outcomeid != $this->outcomeid;\r
291 $multfactordiff = $db_item->multfactor != $this->multfactor;\r
292 $plusfactordiff = $db_item->plusfactor != $this->plusfactor;\r
293 $locktimediff = $db_item->locktime != $this->locktime;\r
294 $acoefdiff = $db_item->aggregationcoef != $this->aggregationcoef;\r
295\r
296 $needsupdatediff = !$db_item->needsupdate && $this->needsupdate; // force regrading only if setting the flag first time\r
297 $lockeddiff = !empty($db_item->locked) && empty($this->locked); // force regrading only when unlocking\r
298\r
299 return ($calculationdiff || $categorydiff || $gradetypediff || $grademaxdiff || $grademindiff || $scaleiddiff\r
300 || $outcomeiddiff || $multfactordiff || $plusfactordiff || $needsupdatediff\r
301 || $lockeddiff || $acoefdiff || $locktimediff);\r
302 }\r
303\r
304 /**\r
305 * Finds and returns a grade_item instance based on params.\r
306 * @static\r
307 *\r
308 * @param array $params associative arrays varname=>value\r
309 * @return object grade_item instance or false if none found.\r
310 */\r
311 function fetch($params) {\r
312 return grade_object::fetch_helper('grade_items', 'grade_item', $params);\r
313 }\r
314\r
315 /**\r
316 * Finds and returns all grade_item instances based on params.\r
317 * @static\r
318 *\r
319 * @param array $params associative arrays varname=>value\r
320 * @return array array of grade_item insatnces or false if none found.\r
321 */\r
322 function fetch_all($params) {\r
323 return grade_object::fetch_all_helper('grade_items', 'grade_item', $params);\r
324 }\r
325\r
326 /**\r
327 * Delete all grades and force_regrading of parent category.\r
328 * @param string $source from where was the object deleted (mod/forum, manual, etc.)\r
329 * @return boolean success\r
330 */\r
331 function delete($source=null) {\r
332 if (!$this->is_course_item()) {\r
333 $this->force_regrading();\r
334 }\r
335\r
336 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {\r
337 foreach ($grades as $grade) {\r
338 $grade->delete($source);\r
339 }\r
340 }\r
341\r
342 return parent::delete($source);\r
343 }\r
344\r
345 /**\r
346 * In addition to perform parent::insert(), calls force_regrading() method too.\r
347 * @param string $source from where was the object inserted (mod/forum, manual, etc.)\r
348 * @return int PK ID if successful, false otherwise\r
349 */\r
350 function insert($source=null) {\r
351 global $CFG;\r
352\r
353 if (empty($this->courseid)) {\r
354 error('Can not insert grade item without course id!');\r
355 }\r
356\r
357 // load scale if needed\r
358 $this->load_scale();\r
359\r
360 // add parent category if needed\r
361 if (empty($this->categoryid) and !$this->is_course_item() and !$this->is_category_item()) {\r
362 $course_category = grade_category::fetch_course_category($this->courseid);\r
363 $this->categoryid = $course_category->id;\r
364\r
365 }\r
366\r
367 // always place the new items at the end, move them after insert if needed\r
368 $last_sortorder = get_field_select('grade_items', 'MAX(sortorder)', "courseid = {$this->courseid}");\r
369 if (!empty($last_sortorder)) {\r
370 $this->sortorder = $last_sortorder + 1;\r
371 } else {\r
372 $this->sortorder = 1;\r
373 }\r
374\r
375 // add proper item numbers to manual items\r
376 if ($this->itemtype == 'manual') {\r
377 if (empty($this->itemnumber)) {\r
378 $this->itemnumber = 0;\r
379 }\r
380 }\r
381\r
382 // make sure there is not 0 in outcomeid\r
383 if (empty($this->outcomeid)) {\r
384 $this->outcomeid = null;\r
385 }\r
386\r
387 if (parent::insert($source)) {\r
388 // force regrading of items if needed\r
389 $this->force_regrading();\r
390 return $this->id;\r
391\r
392 } else {\r
393 debugging("Could not insert this grade_item in the database!");\r
394 return false;\r
395 }\r
396 }\r
397\r
398 /**\r
399 * Set idnumber of grade item, updates also course_modules table\r
400 * @param string $idnumber (without magic quotes)\r
401 * @return boolean success\r
402 */\r
403 function add_idnumber($idnumber) {\r
404 if (!empty($this->idnumber)) {\r
405 return false;\r
406 }\r
407\r
408 if ($this->itemtype == 'mod' and !$this->is_outcome_item()) {\r
409 if (!$cm = get_coursemodule_from_instance($this->itemmodule, $this->iteminstance, $this->courseid)) {\r
410 return false;\r
411 }\r
412 if (!empty($cm->idnumber)) {\r
413 return false;\r
414 }\r
415 if (set_field('course_modules', 'idnumber', addslashes($idnumber), 'id', $cm->id)) {\r
416 $this->idnumber = $idnumber;\r
417 return $this->update();\r
418 }\r
419 return false;\r
420\r
421 } else {\r
422 $this->idnumber = $idnumber;\r
423 return $this->update();\r
424 }\r
425 }\r
426\r
427 /**\r
428 * Returns the locked state of this grade_item (if the grade_item is locked OR no specific\r
429 * $userid is given) or the locked state of a specific grade within this item if a specific\r
430 * $userid is given and the grade_item is unlocked.\r
431 *\r
432 * @param int $userid\r
433 * @return boolean Locked state\r
434 */\r
435 function is_locked($userid=NULL) {\r
436 if (!empty($this->locked)) {\r
437 return true;\r
438 }\r
439\r
440 if (!empty($userid)) {\r
441 if ($grade = grade_grade::fetch(array('itemid'=>$this->id, 'userid'=>$userid))) {\r
442 $grade->grade_item =& $this; // prevent db fetching of cached grade_item\r
443 return $grade->is_locked();\r
444 }\r
445 }\r
446\r
447 return false;\r
448 }\r
449\r
450 /**\r
451 * Locks or unlocks this grade_item and (optionally) all its associated final grades.\r
452 * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.\r
453 * @param boolean $cascade lock/unlock child objects too\r
454 * @param boolean $refresh refresh grades when unlocking\r
455 * @return boolean true if grade_item all grades updated, false if at least one update fails\r
456 */\r
457 function set_locked($lockedstate, $cascade=false, $refresh=true) {\r
458 if ($lockedstate) {\r
459 /// setting lock\r
460 if ($this->needsupdate) {\r
461 return false; // can not lock grade without first having final grade\r
462 }\r
463\r
464 $this->locked = time();\r
465 $this->update();\r
466\r
467 if ($cascade) {\r
468 $grades = $this->get_final();\r
469 foreach($grades as $g) {\r
470 $grade = new grade_grade($g, false);\r
471 $grade->grade_item =& $this;\r
472 $grade->set_locked(1, null, false);\r
473 }\r
474 }\r
475\r
476 return true;\r
477\r
478 } else {\r
479 /// removing lock\r
480 if (!empty($this->locked) and $this->locktime < time()) {\r
481 //we have to reset locktime or else it would lock up again\r
482 $this->locktime = 0;\r
483 }\r
484\r
485 $this->locked = 0;\r
486 $this->update();\r
487\r
488 if ($cascade) {\r
489 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {\r
490 foreach($grades as $grade) {\r
491 $grade->grade_item =& $this;\r
492 $grade->set_locked(0, null, false);\r
493 }\r
494 }\r
495 }\r
496\r
497 if ($refresh) {\r
498 //refresh when unlocking\r
499 $this->refresh_grades();\r
500 }\r
501\r
502 return true;\r
503 }\r
504 }\r
505\r
506 /**\r
507 * Lock the grade if needed - make sure this is called only when final grades are valid\r
508 */\r
509 function check_locktime() {\r
510 if (!empty($this->locked)) {\r
511 return; // already locked\r
512 }\r
513\r
514 if ($this->locktime and $this->locktime < time()) {\r
515 $this->locked = time();\r
516 $this->update('locktime');\r
517 }\r
518 }\r
519\r
520 /**\r
521 * Set the locktime for this grade item.\r
522 *\r
523 * @param int $locktime timestamp for lock to activate\r
524 * @return void\r
525 */\r
526 function set_locktime($locktime) {\r
527 $this->locktime = $locktime;\r
528 $this->update();\r
529 }\r
530\r
531 /**\r
532 * Set the locktime for this grade item.\r
533 *\r
534 * @return int $locktime timestamp for lock to activate\r
535 */\r
536 function get_locktime() {\r
537 return $this->locktime;\r
538 }\r
539\r
540 /**\r
541 * Returns the hidden state of this grade_item\r
542 * @return boolean hidden state\r
543 */\r
544 function is_hidden() {\r
545 return ($this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()));\r
546 }\r
547\r
548 /**\r
549 * Check grade item hidden status.\r
550 * @return int 0 means visible, 1 hidden always, timestamp hidden until\r
551 */\r
552 function get_hidden() {\r
553 return $this->hidden;\r
554 }\r
555\r
556 /**\r
557 * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.\r
558 * @param int $hidden new hidden status\r
559 * @param boolean $cascade apply to child objects too\r
560 * @return void\r
561 */\r
562 function set_hidden($hidden, $cascade=false) {\r
563 $this->hidden = $hidden;\r
564 $this->update();\r
565\r
566 if ($cascade) {\r
567 if ($grades = grade_grade::fetch_all(array('itemid'=>$this->id))) {\r
568 foreach($grades as $grade) {\r
569 $grade->grade_item =& $this;\r
570 $grade->set_hidden($hidden, $cascade);\r
571 }\r
572 }\r
573 }\r
574 }\r
575\r
576 /**\r
577 * Returns the number of grades that are hidden.\r
578 * @param return int Number of hidden grades\r
579 */\r
580 function has_hidden_grades($groupsql="", $groupwheresql="") {\r
581 global $CFG;\r
582 return get_field_sql("SELECT COUNT(*) FROM {$CFG->prefix}grade_grades g LEFT JOIN "\r
583 ."{$CFG->prefix}user u ON g.userid = u.id $groupsql WHERE itemid = $this->id AND hidden = 1 $groupwheresql");\r
584 }\r
585\r
586 /**\r
587 * Mark regrading as finished successfully.\r
588 */\r
589 function regrading_finished() {\r
590 $this->needsupdate = 0;\r
591 //do not use $this->update() because we do not want this logged in grade_item_history\r
592 set_field('grade_items', 'needsupdate', 0, 'id', $this->id);\r
593 }\r
594\r
595 /**\r
596 * Performs the necessary calculations on the grades_final referenced by this grade_item.\r
597 * Also resets the needsupdate flag once successfully performed.\r
598 *\r
599 * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),\r
600 * because the regrading must be done in correct order!!\r
601 *\r
602 * @return boolean true if ok, error string otherwise\r
603 */\r
604 function regrade_final_grades($userid=null) {\r
605 global $CFG;\r
606\r
607 // locked grade items already have correct final grades\r
608 if ($this->is_locked()) {\r
609 return true;\r
610 }\r
611\r
612 // calculation produces final value using formula from other final values\r
613 if ($this->is_calculated()) {\r
614 if ($this->compute($userid)) {\r
615 return true;\r
616 } else {\r
617 return "Could not calculate grades for grade item"; // TODO: improve and localize\r
618 }\r
619\r
620 // noncalculated outcomes already have final values - raw grades not used\r
621 } else if ($this->is_outcome_item()) {\r
622 return true;\r
623\r
624 // aggregate the category grade\r
625 } else if ($this->is_category_item() or $this->is_course_item()) {\r
626 // aggregate category grade item\r
627 $category = $this->get_item_category();\r
628 $category->grade_item =& $this;\r
629 if ($category->generate_grades($userid)) {\r
630 return true;\r
631 } else {\r
632 return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize\r
633 }\r
634\r
635 } else if ($this->is_manual_item()) {\r
636 // manual items track only final grades, no raw grades\r
637 return true;\r
638\r
639 } else if (!$this->is_raw_used()) {\r
640 // hmm - raw grades are not used- nothing to regrade\r
641 return true;\r
642 }\r
643\r
644 // normal grade item - just new final grades\r
645 $result = true;\r
646 $grade_inst = new grade_grade();\r
647 $fields = implode(',', $grade_inst->required_fields);\r
648 if ($userid) {\r
649 $rs = get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid", '', $fields);\r
650 } else {\r
651 $rs = get_recordset('grade_grades', 'itemid', $this->id, '', $fields);\r
652 }\r
653 if ($rs) {\r
654 while ($grade_record = rs_fetch_next_record($rs)) {\r
655 $grade = new grade_grade($grade_record, false);\r
656\r
657 if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {\r
658 // this grade is locked - final grade must be ok\r
659 continue;\r
660 }\r
661\r
662 $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);\r
663\r
664 if ($grade_record->finalgrade !== $grade->finalgrade) {\r
665 if (!$grade->update('system')) {\r
666 $result = "Internal error updating final grade";\r
667 }\r
668 }\r
669 }\r
670 rs_close($rs);\r
671 }\r
672\r
673 return $result;\r
674 }\r
675\r
676 /**\r
677 * Given a float grade value or integer grade scale, applies a number of adjustment based on\r
678 * grade_item variables and returns the result.\r
679 * @param object $rawgrade The raw grade value.\r
680 * @return mixed\r
681 */\r
682 function adjust_raw_grade($rawgrade, $rawmin, $rawmax) {\r
683 if (is_null($rawgrade)) {\r
684 return null;\r
685 }\r
686\r
687 if ($this->gradetype == GRADE_TYPE_VALUE) { // Dealing with numerical grade\r
688\r
689 if ($this->grademax < $this->grademin) {\r
690 return null;\r
691 }\r
692\r
693 if ($this->grademax == $this->grademin) {\r
694 return $this->grademax; // no range\r
695 }\r
696\r
697 // Standardise score to the new grade range\r
698 // NOTE: this is not compatible with current assignment grading\r
699 if ($rawmin != $this->grademin or $rawmax != $this->grademax) {\r
700 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);\r
701 }\r
702\r
703 // Apply other grade_item factors\r
704 $rawgrade *= $this->multfactor;\r
705 $rawgrade += $this->plusfactor;\r
706\r
707 return bounded_number($this->grademin, $rawgrade, $this->grademax);\r
708\r
709 } else if ($this->gradetype == GRADE_TYPE_SCALE) { // Dealing with a scale value\r
710 if (empty($this->scale)) {\r
711 $this->load_scale();\r
712 }\r
713\r
714 if ($this->grademax < 0) {\r
715 return null; // scale not present - no grade\r
716 }\r
717\r
718 if ($this->grademax == 0) {\r
719 return $this->grademax; // only one option\r
720 }\r
721\r
722 // Convert scale if needed\r
723 // NOTE: this is not compatible with current assignment grading\r
724 if ($rawmin != $this->grademin or $rawmax != $this->grademax) {\r
725 $rawgrade = grade_grade::standardise_score($rawgrade, $rawmin, $rawmax, $this->grademin, $this->grademax);\r
726 }\r
727\r
728 return (int)bounded_number(0, round($rawgrade+0.00001), $this->grademax);\r
729\r
730\r
731 } else if ($this->gradetype == GRADE_TYPE_TEXT or $this->gradetype == GRADE_TYPE_NONE) { // no value\r
732 // somebody changed the grading type when grades already existed\r
733 return null;\r
734\r
735 } else {\r
736 dubugging("Unkown grade type");\r
737 return null;;\r
738 }\r
739 }\r
740\r
741 /**\r
742 * Sets this grade_item's needsupdate to true. Also marks the course item as needing update.\r
743 * @return void\r
744 */\r
745 function force_regrading() {\r
746 $this->needsupdate = 1;\r
747 //mark this item and course item only - categories and calculated items are always regraded\r
748 $wheresql = "(itemtype='course' OR id={$this->id}) AND courseid={$this->courseid}";\r
749 set_field_select('grade_items', 'needsupdate', 1, $wheresql);\r
750 }\r
751\r
752 /**\r
753 * Instantiates a grade_scale object whose data is retrieved from the DB,\r
754 * if this item's scaleid variable is set.\r
755 * @return object grade_scale or null if no scale used\r
756 */\r
757 function load_scale() {\r
758 if ($this->gradetype != GRADE_TYPE_SCALE) {\r
759 $this->scaleid = null;\r
760 }\r
761\r
762 if (!empty($this->scaleid)) {\r
763 //do not load scale if already present\r
764 if (empty($this->scale->id) or $this->scale->id != $this->scaleid) {\r
765 $this->scale = grade_scale::fetch(array('id'=>$this->scaleid));\r
766 $this->scale->load_items();\r
767 }\r
768\r
769 // Until scales are uniformly set to min=0 max=count(scaleitems)-1 throughout Moodle, we\r
770 // stay with the current min=1 max=count(scaleitems)\r
771 $this->grademax = count($this->scale->scale_items);\r
772 $this->grademin = 1;\r
773\r
774 } else {\r
775 $this->scale = null;\r
776 }\r
777\r
778 return $this->scale;\r
779 }\r
780\r
781 /**\r
782 * Instantiates a grade_outcome object whose data is retrieved from the DB,\r
783 * if this item's outcomeid variable is set.\r
784 * @return object grade_outcome\r
785 */\r
786 function load_outcome() {\r
787 if (!empty($this->outcomeid)) {\r
788 $this->outcome = grade_outcome::fetch(array('id'=>$this->outcomeid));\r
789 }\r
790 return $this->outcome;\r
791 }\r
792\r
793 /**\r
794 * Returns the grade_category object this grade_item belongs to (referenced by categoryid)\r
795 * or category attached to category item.\r
796 *\r
797 * @return mixed grade_category object if applicable, false if course item\r
798 */\r
799 function get_parent_category() {\r
800 if ($this->is_category_item() or $this->is_course_item()) {\r
801 return $this->get_item_category();\r
802\r
803 } else {\r
804 return grade_category::fetch(array('id'=>$this->categoryid));\r
805 }\r
806 }\r
807\r
808 /**\r
809 * Calls upon the get_parent_category method to retrieve the grade_category object\r
810 * from the DB and assigns it to $this->parent_category. It also returns the object.\r
811 * @return object Grade_category\r
812 */\r
813 function load_parent_category() {\r
814 if (empty($this->parent_category->id)) {\r
815 $this->parent_category = $this->get_parent_category();\r
816 }\r
817 return $this->parent_category;\r
818 }\r
819\r
820 /**\r
821 * Returns the grade_category for category item\r
822 *\r
823 * @return mixed grade_category object if applicable, false otherwise\r
824 */\r
825 function get_item_category() {\r
826 if (!$this->is_course_item() and !$this->is_category_item()) {\r
827 return false;\r
828 }\r
829 return grade_category::fetch(array('id'=>$this->iteminstance));\r
830 }\r
831\r
832 /**\r
833 * Calls upon the get_item_category method to retrieve the grade_category object\r
834 * from the DB and assigns it to $this->item_category. It also returns the object.\r
835 * @return object Grade_category\r
836 */\r
837 function load_item_category() {\r
838 if (empty($this->category->id)) {\r
839 $this->item_category = $this->get_item_category();\r
840 }\r
841 return $this->item_category;\r
842 }\r
843\r
844 /**\r
845 * Is the grade item associated with category?\r
846 * @return boolean\r
847 */\r
848 function is_category_item() {\r
849 return ($this->itemtype == 'category');\r
850 }\r
851\r
852 /**\r
853 * Is the grade item associated with course?\r
854 * @return boolean\r
855 */\r
856 function is_course_item() {\r
857 return ($this->itemtype == 'course');\r
858 }\r
859\r
860 /**\r
861 * Is this a manualy graded item?\r
862 * @return boolean\r
863 */\r
864 function is_manual_item() {\r
865 return ($this->itemtype == 'manual');\r
866 }\r
867\r
868 /**\r
869 * Is this an outcome item?\r
870 * @return boolean\r
871 */\r
872 function is_outcome_item() {\r
873 return !empty($this->outcomeid);\r
874 }\r
875\r
876 /**\r
877 * Is the grade item normal - associated with module, plugin or something else?\r
878 * @return boolean\r
879 */\r
880 function is_normal_item() {\r
881 return ($this->itemtype != 'course' and $this->itemtype != 'category' and $this->itemtype != 'manual');\r
882 }\r
883\r
884 /**\r
885 * Returns true if grade items uses raw grades\r
886 * @return boolean\r
887 */\r
888 function is_raw_used() {\r
889 return ($this->is_normal_item() and !$this->is_calculated() and !$this->is_outcome_item());\r
890 }\r
891\r
892 /**\r
893 * Returns grade item associated with the course\r
894 * @param int $courseid\r
895 * @return course item object\r
896 */\r
897 function fetch_course_item($courseid) {\r
898 if ($course_item = grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {\r
899 return $course_item;\r
900 }\r
901\r
902 // first get category - it creates the associated grade item\r
903 $course_category = grade_category::fetch_course_category($courseid);\r
904\r
905 return grade_item::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'));\r
906 }\r
907\r
908 /**\r
909 * Is grading object editable?\r
910 * @return boolean\r
911 */\r
912 function is_editable() {\r
913 return true;\r
914 }\r
915\r
916 /**\r
917 * Checks if grade calculated. Returns this object's calculation.\r
918 * @return boolean true if grade item calculated.\r
919 */\r
920 function is_calculated() {\r
921 if (empty($this->calculation)) {\r
922 return false;\r
923 }\r
924\r
925 /*\r
926 * The main reason why we use the ##gixxx## instead of [[idnumber]] is speed of depends_on(),\r
927 * we would have to fetch all course grade items to find out the ids.\r
928 * Also if user changes the idnumber the formula does not need to be updated.\r
929 */\r
930\r
931 // first detect if we need to change calculation formula from [[idnumber]] to ##giXXX## (after backup, etc.)\r
932 if (!$this->calculation_normalized and preg_match('/##gi\d+##/', $this->calculation)) {\r
933 $this->set_calculation($this->calculation);\r
934 }\r
935\r
936 return !empty($this->calculation);\r
937 }\r
938\r
939 /**\r
940 * Returns calculation string if grade calculated.\r
941 * @return mixed string if calculation used, null if not\r
942 */\r
943 function get_calculation() {\r
944 if ($this->is_calculated()) {\r
945 return grade_item::denormalize_formula($this->calculation, $this->courseid);\r
946\r
947 } else {\r
948 return NULL;\r
949 }\r
950 }\r
951\r
952 /**\r
953 * Sets this item's calculation (creates it) if not yet set, or\r
954 * updates it if already set (in the DB). If no calculation is given,\r
955 * the calculation is removed.\r
956 * @param string $formula string representation of formula used for calculation\r
957 * @return boolean success\r
958 */\r
959 function set_calculation($formula) {\r
960 $this->calculation = grade_item::normalize_formula($formula, $this->courseid);\r
961 $this->calculation_normalized = true;\r
962 return $this->update();\r
963 }\r
964\r
965 /**\r
966 * Denormalizes the calculation formula to [idnumber] form\r
967 * @static\r
968 * @param string $formula\r
969 * @return string denormalized string\r
970 */\r
971 function denormalize_formula($formula, $courseid) {\r
972 if (empty($formula)) {\r
973 return '';\r
974 }\r
975\r
976 // denormalize formula - convert ##giXX## to [[idnumber]]\r
977 if (preg_match_all('/##gi(\d+)##/', $formula, $matches)) {\r
978 foreach ($matches[1] as $id) {\r
979 if ($grade_item = grade_item::fetch(array('id'=>$id, 'courseid'=>$courseid))) {\r
980 if (!empty($grade_item->idnumber)) {\r
981 $formula = str_replace('##gi'.$grade_item->id.'##', '[['.$grade_item->idnumber.']]', $formula);\r
982 }\r
983 }\r
984 }\r
985 }\r
986\r
987 return $formula;\r
988\r
989 }\r
990\r
991 /**\r
992 * Normalizes the calculation formula to [#giXX#] form\r
993 * @static\r
994 * @param string $formula\r
995 * @return string normalized string\r
996 */\r
997 function normalize_formula($formula, $courseid) {\r
998 $formula = trim($formula);\r
999\r
1000 if (empty($formula)) {\r
1001 return NULL;\r
1002\r
1003 }\r
1004\r
1005 // normalize formula - we want grade item ids ##giXXX## instead of [[idnumber]]\r
1006 if ($grade_items = grade_item::fetch_all(array('courseid'=>$courseid))) {\r
1007 foreach ($grade_items as $grade_item) {\r
1008 $formula = str_replace('[['.$grade_item->idnumber.']]', '##gi'.$grade_item->id.'##', $formula);\r
1009 }\r
1010 }\r
1011\r
1012 return $formula;\r
1013 }\r
1014\r
1015 /**\r
1016 * Returns the final values for this grade item (as imported by module or other source).\r
1017 * @param int $userid Optional: to retrieve a single final grade\r
1018 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.\r
1019 */\r
1020 function get_final($userid=NULL) {\r
1021 if ($userid) {\r
1022 if ($user = get_record('grade_grades', 'itemid', $this->id, 'userid', $userid)) {\r
1023 return $user;\r
1024 }\r
1025\r
1026 } else {\r
1027 if ($grades = get_records('grade_grades', 'itemid', $this->id)) {\r
1028 //TODO: speed up with better SQL\r
1029 $result = array();\r
1030 foreach ($grades as $grade) {\r
1031 $result[$grade->userid] = $grade;\r
1032 }\r
1033 return $result;\r
1034 } else {\r
1035 return array();\r
1036 }\r
1037 }\r
1038 }\r
1039\r
1040 /**\r
1041 * Get (or create if not exist yet) grade for this user\r
1042 * @param int $userid\r
1043 * @return object grade_grade object instance\r
1044 */\r
1045 function get_grade($userid, $create=true) {\r
1046 if (empty($this->id)) {\r
1047 debugging('Can not use before insert');\r
1048 return false;\r
1049 }\r
1050\r
1051 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$this->id));\r
1052 if (empty($grade->id) and $create) {\r
1053 $grade->insert();\r
1054 }\r
1055\r
1056 return $grade;\r
1057 }\r
1058\r
1059 /**\r
1060 * Returns the sortorder of this grade_item. This method is also available in\r
1061 * grade_category, for cases where the object type is not know.\r
1062 * @return int Sort order\r
1063 */\r
1064 function get_sortorder() {\r
1065 return $this->sortorder;\r
1066 }\r
1067\r
1068 /**\r
1069 * Returns the idnumber of this grade_item. This method is also available in\r
1070 * grade_category, for cases where the object type is not know.\r
1071 * @return string idnumber\r
1072 */\r
1073 function get_idnumber() {\r
1074 return $this->idnumber;\r
1075 }\r
1076\r
1077 /**\r
1078 * Returns this grade_item. This method is also available in\r
1079 * grade_category, for cases where the object type is not know.\r
1080 * @return string idnumber\r
1081 */\r
1082 function get_grade_item() {\r
1083 return $this;\r
1084 }\r
1085\r
1086 /**\r
1087 * Sets the sortorder of this grade_item. This method is also available in\r
1088 * grade_category, for cases where the object type is not know.\r
1089 * @param int $sortorder\r
1090 * @return void\r
1091 */\r
1092 function set_sortorder($sortorder) {\r
1093 $this->sortorder = $sortorder;\r
1094 $this->update();\r
1095 }\r
1096\r
1097 function move_after_sortorder($sortorder) {\r
1098 global $CFG;\r
1099\r
1100 //make some room first\r
1101 $sql = "UPDATE {$CFG->prefix}grade_items\r
1102 SET sortorder = sortorder + 1\r
1103 WHERE sortorder > $sortorder AND courseid = {$this->courseid}";\r
1104 execute_sql($sql, false);\r
1105\r
1106 $this->set_sortorder($sortorder + 1);\r
1107 }\r
1108\r
1109 /**\r
1110 * Returns the most descriptive field for this object. This is a standard method used\r
1111 * when we do not know the exact type of an object.\r
1112 * @return string name\r
1113 */\r
1114 function get_name() {\r
1115 if (!empty($this->itemname)) {\r
1116 // MDL-10557\r
1117 return format_string($this->itemname);\r
1118\r
1119 } else if ($this->is_course_item()) {\r
1120 return get_string('coursetotal', 'grades');\r
1121\r
1122 } else if ($this->is_category_item()) {\r
1123 return get_string('categorytotal', 'grades');\r
1124\r
1125 } else {\r
1126 return get_string('grade');\r
1127 }\r
1128 }\r
1129\r
1130 /**\r
1131 * Sets this item's categoryid. A generic method shared by objects that have a parent id of some kind.\r
1132 * @param int $parentid\r
1133 * @return boolean success;\r
1134 */\r
1135 function set_parent($parentid) {\r
1136 if ($this->is_course_item() or $this->is_category_item()) {\r
1137 error('Can not set parent for category or course item!');\r
1138 }\r
1139\r
1140 if ($this->categoryid == $parentid) {\r
1141 return true;\r
1142 }\r
1143\r
1144 // find parent and check course id\r
1145 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {\r
1146 return false;\r
1147 }\r
1148\r
1149 $this->force_regrading();\r
1150\r
1151 // set new parent\r
1152 $this->categoryid = $parent_category->id;\r
1153 $this->parent_category =& $parent_category;\r
1154\r
1155 return $this->update();\r
1156 }\r
1157\r
1158 /**\r
1159 * Finds out on which other items does this depend directly when doing calculation or category agregation\r
1160 * @param bool $reset_cache\r
1161 * @return array of grade_item ids this one depends on\r
1162 */\r
1163 function depends_on($reset_cache=false) {\r
1164 global $CFG;\r
1165\r
1166 if ($reset_cache) {\r
1167 $this->dependson_cache = null;\r
1168 } else if (isset($this->dependson_cache)) {\r
1169 return $this->dependson_cache;\r
1170 }\r
1171\r
1172 if ($this->is_locked()) {\r
1173 // locked items do not need to be regraded\r
1174 $this->dependson_cache = array();\r
1175 return $this->dependson_cache;\r
1176 }\r
1177\r
1178 if ($this->is_calculated()) {\r
1179 if (preg_match_all('/##gi(\d+)##/', $this->calculation, $matches)) {\r
1180 $this->dependson_cache = array_unique($matches[1]); // remove duplicates\r
1181 return $this->dependson_cache;\r
1182 } else {\r
1183 $this->dependson_cache = array();\r
1184 return $this->dependson_cache;\r
1185 }\r
1186\r
1187 } else if ($grade_category = $this->load_item_category()) {\r
1188 //only items with numeric or scale values can be aggregated\r
1189 if ($this->gradetype != GRADE_TYPE_VALUE and $this->gradetype != GRADE_TYPE_SCALE) {\r
1190 $this->dependson_cache = array();\r
1191 return $this->dependson_cache;\r
1192 }\r
1193\r
1194 $grade_category->apply_forced_settings();\r
1195\r
1196 if (empty($CFG->enableoutcomes) or $grade_category->aggregateoutcomes) {\r
1197 $outcomes_sql = "";\r
1198 } else {\r
1199 $outcomes_sql = "AND gi.outcomeid IS NULL";\r
1200 }\r
1201\r
1202 if ($grade_category->aggregatesubcats) {\r
1203 // return all children excluding category items\r
1204 $sql = "SELECT gi.id\r
1205 FROM {$CFG->prefix}grade_items gi\r
1206 WHERE (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")\r
1207 $outcomes_sql\r
1208 AND gi.categoryid IN (\r
1209 SELECT gc.id\r
1210 FROM {$CFG->prefix}grade_categories gc\r
1211 WHERE gc.path LIKE '%/{$grade_category->id}/%')";\r
1212\r
1213 } else {\r
1214 $sql = "SELECT gi.id\r
1215 FROM {$CFG->prefix}grade_items gi\r
1216 WHERE gi.categoryid = {$grade_category->id}\r
1217 AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")\r
1218 $outcomes_sql\r
1219\r
1220 UNION\r
1221\r
1222 SELECT gi.id\r
1223 FROM {$CFG->prefix}grade_items gi, {$CFG->prefix}grade_categories gc\r
1224 WHERE (gi.itemtype = 'category' OR gi.itemtype = 'course') AND gi.iteminstance=gc.id\r
1225 AND gc.parent = {$grade_category->id}\r
1226 AND (gi.gradetype = ".GRADE_TYPE_VALUE." OR gi.gradetype = ".GRADE_TYPE_SCALE.")\r
1227 $outcomes_sql";\r
1228 }\r
1229\r
1230 if ($children = get_records_sql($sql)) {\r
1231 $this->dependson_cache = array_keys($children);\r
1232 return $this->dependson_cache;\r
1233 } else {\r
1234 $this->dependson_cache = array();\r
1235 return $this->dependson_cache;\r
1236 }\r
1237\r
1238 } else {\r
1239 $this->dependson_cache = array();\r
1240 return $this->dependson_cache;\r
1241 }\r
1242 }\r
1243\r
1244 /**\r
1245 * Refetch grades from moudles, plugins.\r
1246 * @param int $userid optional, one user only\r
1247 */\r
1248 function refresh_grades($userid=0) {\r
1249 if ($this->itemtype == 'mod') {\r
1250 if ($this->is_outcome_item()) {\r
1251 //nothing to do\r
1252 return;\r
1253 }\r
1254\r
1255 if (!$activity = get_record($this->itemmodule, 'id', $this->iteminstance)) {\r
1256 debugging('Can not find activity');\r
1257 return;\r
1258 }\r
1259\r
1260 if (! $cm = get_coursemodule_from_instance($this->itemmodule, $activity->id, $this->courseid)) {\r
1261 debuggin('Can not find course module');\r
1262 return;\r
1263 }\r
1264\r
1265 $activity->modname = $this->itemmodule;\r
1266 $activity->cmidnumber = $cm->idnumber;\r
1267\r
1268 grade_update_mod_grades($activity);\r
1269 }\r
1270 }\r
1271\r
1272 /**\r
1273 * Updates final grade value for given user, this is a only way to update final\r
1274 * grades from gradebook and import because it logs the change in history table\r
1275 * and deals with overridden flag. This flag is set to prevent later overriding\r
1276 * from raw grades submitted from modules.\r
1277 *\r
1278 * @param int $userid the graded user\r
1279 * @param mixed $finalgrade float value of final grade - false means do not change\r
1280 * @param string $howmodified modification source\r
1281 * @param string $note optional note\r
1282 * @param mixed $feedback teachers feedback as string - false means do not change\r
1283 * @param int $feedbackformat\r
1284 * @return boolean success\r
1285 */\r
1286 function update_final_grade($userid, $finalgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {\r
1287 global $USER, $CFG;\r
1288\r
1289 if (empty($usermodified)) {\r
1290 $usermodified = $USER->id;\r
1291 }\r
1292\r
1293 $result = true;\r
1294\r
1295 // no grading used or locked\r
1296 if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {\r
1297 return false;\r
1298 }\r
1299\r
1300 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));\r
1301 $grade->grade_item =& $this; // prevent db fetching of this grade_item\r
1302\r
1303 $grade->usermodified = $usermodified;\r
1304\r
1305 if ($grade->is_locked()) {\r
1306 // do not update locked grades at all\r
1307 return false;\r
1308 }\r
1309\r
1310 $locktime = $grade->get_locktime();\r
1311 if ($locktime and $locktime < time()) {\r
1312 // do not update grades that should be already locked, force regrade instead\r
1313 $this->force_regrading();\r
1314 return false;\r
1315 }\r
1316\r
1317 $oldgrade = new object();\r
1318 $oldgrade->finalgrade = $grade->finalgrade;\r
1319 $oldgrade->overridden = $grade->overridden;\r
1320 $oldgrade->feedback = $grade->feedback;\r
1321 $oldgrade->feedbackformat = $grade->feedbackformat;\r
1322\r
1323 if ($finalgrade !== false or $feedback !== false) {\r
1324 if (($this->is_outcome_item() or $this->is_manual_item()) and !$this->is_calculated()) {\r
1325 // final grades updated only by user - no need for overriding\r
1326 $grade->overridden = 0;\r
1327\r
1328 } else {\r
1329 $grade->overridden = time();\r
1330 }\r
1331 }\r
1332\r
1333 if ($finalgrade !== false) {\r
1334 if (!is_null($finalgrade)) {\r
1335 $finalgrade = bounded_number($this->grademin, $finalgrade, $this->grademax);\r
1336 } else {\r
1337 $finalgrade = $finalgrade;\r
1338 }\r
1339 $grade->finalgrade = $finalgrade;\r
1340 }\r
1341\r
1342 // do we have comment from teacher?\r
1343 if ($feedback !== false) {\r
1344 $grade->feedback = $feedback;\r
1345 $grade->feedbackformat = $feedbackformat;\r
1346 }\r
1347\r
1348 if (empty($grade->id)) {\r
1349 $result = (boolean)$grade->insert($source);\r
1350\r
1351 } else if ($grade->finalgrade !== $oldgrade->finalgrade\r
1352 or $grade->feedback !== $oldgrade->feedback\r
1353 or $grade->feedbackformat !== $oldgrade->feedbackformat) {\r
1354 $result = $grade->update($source);\r
1355 }\r
1356\r
1357 if (!$result) {\r
1358 // something went wrong - better force final grade recalculation\r
1359 $this->force_regrading();\r
1360\r
1361 } else if ($this->is_course_item() and !$this->needsupdate) {\r
1362 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {\r
1363 $this->force_regrading();\r
1364 }\r
1365\r
1366 } else if (!$this->needsupdate) {\r
1367 $course_item = grade_item::fetch_course_item($this->courseid);\r
1368 if (!$course_item->needsupdate) {\r
1369 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {\r
1370 $this->force_regrading();\r
1371 }\r
1372 } else {\r
1373 $this->force_regrading();\r
1374 }\r
1375 }\r
1376\r
1377 return $result;\r
1378 }\r
1379\r
1380\r
1381 /**\r
1382 * Updates raw grade value for given user, this is a only way to update raw\r
1383 * grades from external source (modules, etc.),\r
1384 * because it logs the change in history table and deals with final grade recalculation.\r
1385 *\r
1386 * @param int $userid the graded user\r
1387 * @param mixed $rawgrade float value of raw grade - false means do not change\r
1388 * @param string $howmodified modification source\r
1389 * @param string $note optional note\r
1390 * @param mixed $feedback teachers feedback as string - false means do not change\r
1391 * @param int $feedbackformat\r
1392 * @return boolean success\r
1393 */\r
1394 function update_raw_grade($userid, $rawgrade=false, $source=NULL, $note=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {\r
1395 global $USER;\r
1396\r
1397 if (empty($usermodified)) {\r
1398 $usermodified = $USER->id;\r
1399 }\r
1400\r
1401 $result = true;\r
1402\r
1403 // calculated grades can not be updated; course and category can not be updated because they are aggregated\r
1404 if ($this->is_calculated() or $this->is_outcome_item() or !$this->is_normal_item()\r
1405 or $this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {\r
1406 return false;\r
1407 }\r
1408\r
1409 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));\r
1410 $grade->grade_item =& $this; // prevent db fetching of this grade_item\r
1411\r
1412 $grade->usermodified = $usermodified;\r
1413\r
1414 if ($grade->is_locked()) {\r
1415 // do not update locked grades at all\r
1416 return false;\r
1417 }\r
1418\r
1419 $locktime = $grade->get_locktime();\r
1420 if ($locktime and $locktime < time()) {\r
1421 // do not update grades that should be already locked and force regrade\r
1422 $this->force_regrading();\r
1423 return false;\r
1424 }\r
1425\r
1426 $oldgrade = new object();\r
1427 $oldgrade->finalgrade = $grade->finalgrade;\r
1428 $oldgrade->rawgrade = $grade->rawgrade;\r
1429 $oldgrade->rawgrademin = $grade->rawgrademin;\r
1430 $oldgrade->rawgrademax = $grade->rawgrademax;\r
1431 $oldgrade->rawscaleid = $grade->rawscaleid;\r
1432 $oldgrade->feedback = $grade->feedback;\r
1433 $oldgrade->feedbackformat = $grade->feedbackformat;\r
1434\r
1435 // fist copy current grademin/max and scale\r
1436 $grade->rawgrademin = $this->grademin;\r
1437 $grade->rawgrademax = $this->grademax;\r
1438 $grade->rawscaleid = $this->scaleid;\r
1439\r
1440 // change raw grade?\r
1441 if ($rawgrade !== false) {\r
1442 $grade->rawgrade = $rawgrade;\r
1443 }\r
1444\r
1445 // do we have comment from teacher?\r
1446 if ($feedback !== false) {\r
1447 $grade->feedback = $feedback;\r
1448 $grade->feedbackformat = $feedbackformat;\r
1449 }\r
1450\r
1451 if (empty($grade->id)) {\r
1452 $result = (boolean)$grade->insert($source);\r
1453\r
1454 } else if ($grade->finalgrade !== $oldgrade->finalgrade\r
1455 or $grade->rawgrade !== $oldgrade->rawgrade\r
1456 or $grade->rawgrademin !== $oldgrade->rawgrademin\r
1457 or $grade->rawgrademax !== $oldgrade->rawgrademax\r
1458 or $grade->rawscaleid !== $oldgrade->rawscaleid\r
1459 or $grade->feedback !== $oldgrade->feedback\r
1460 or $grade->feedbackformat !== $oldgrade->feedbackformat) {\r
1461\r
1462 $result = $grade->update($source);\r
1463 }\r
1464\r
1465 if (!$result) {\r
1466 // something went wrong - better force final grade recalculation\r
1467 $this->force_regrading();\r
1468\r
1469 } else if (!$this->needsupdate) {\r
1470 $course_item = grade_item::fetch_course_item($this->courseid);\r
1471 if (!$course_item->needsupdate) {\r
1472 if (!grade_regrade_final_grades($this->courseid, $userid, $this)) {\r
1473 $this->force_regrading();\r
1474 }\r
1475 } else {\r
1476 $this->force_regrading();\r
1477 }\r
1478 }\r
1479\r
1480 return $result;\r
1481 }\r
1482\r
1483 /**\r
1484 * Calculates final grade values using the formula in calculation property.\r
1485 * The parameters are taken from final grades of grade items in current course only.\r
1486 * @return boolean false if error\r
1487 */\r
1488 function compute($userid=null) {\r
1489 global $CFG;\r
1490\r
1491 if (!$this->is_calculated()) {\r
1492 return false;\r
1493 }\r
1494\r
1495 require_once($CFG->libdir.'/mathslib.php');\r
1496\r
1497 if ($this->is_locked()) {\r
1498 return true; // no need to recalculate locked items\r
1499 }\r
1500\r
1501 // get used items\r
1502 $useditems = $this->depends_on();\r
1503\r
1504 // prepare formula and init maths library\r
1505 $formula = preg_replace('/##(gi\d+)##/', '\1', $this->calculation);\r
1506 $this->formula = new calc_formula($formula);\r
1507\r
1508 // where to look for final grades?\r
1509 // this itemid is added so that we use only one query for source and final grades\r
1510 $gis = implode(',', array_merge($useditems, array($this->id)));\r
1511\r
1512 if ($userid) {\r
1513 $usersql = "AND g.userid=$userid";\r
1514 } else {\r
1515 $usersql = "";\r
1516 }\r
1517\r
1518 $grade_inst = new grade_grade();\r
1519 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);\r
1520\r
1521 $sql = "SELECT $fields\r
1522 FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi\r
1523 WHERE gi.id = g.itemid AND gi.courseid={$this->courseid} AND gi.id IN ($gis) $usersql\r
1524 ORDER BY g.userid";\r
1525\r
1526 $return = true;\r
1527\r
1528 // group the grades by userid and use formula on the group\r
1529 if ($rs = get_recordset_sql($sql)) {\r
1530 $prevuser = 0;\r
1531 $grade_records = array();\r
1532 $oldgrade = null;\r
1533 while ($used = rs_fetch_next_record($rs)) {\r
1534 if ($used->userid != $prevuser) {\r
1535 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {\r
1536 $return = false;\r
1537 }\r
1538 $prevuser = $used->userid;\r
1539 $grade_records = array();\r
1540 $oldgrade = null;\r
1541 }\r
1542 if ($used->itemid == $this->id) {\r
1543 $oldgrade = $used;\r
1544 }\r
1545 $grade_records['gi'.$used->itemid] = $used->finalgrade;\r
1546 }\r
1547 if (!$this->use_formula($prevuser, $grade_records, $useditems, $oldgrade)) {\r
1548 $return = false;\r
1549 }\r
1550 }\r
1551 rs_close($rs);\r
1552\r
1553 return $return;\r
1554 }\r
1555\r
1556 /**\r
1557 * internal function - does the final grade calculation\r
1558 */\r
1559 function use_formula($userid, $params, $useditems, $oldgrade) {\r
1560 if (empty($userid)) {\r
1561 return true;\r
1562 }\r
1563\r
1564 // add missing final grade values\r
1565 // not graded (null) is counted as 0 - the spreadsheet way\r
1566 foreach($useditems as $gi) {\r
1567 if (!array_key_exists('gi'.$gi, $params)) {\r
1568 $params['gi'.$gi] = 0;\r
1569 } else {\r
1570 $params['gi'.$gi] = (float)$params['gi'.$gi];\r
1571 }\r
1572 }\r
1573\r
1574 // can not use own final grade during calculation\r
1575 unset($params['gi'.$this->id]);\r
1576\r
1577 // insert final grade - will be needed later anyway\r
1578 if ($oldgrade) {\r
1579 $grade = new grade_grade($oldgrade, false); // fetching from db is not needed\r
1580 $grade->grade_item =& $this;\r
1581\r
1582 } else {\r
1583 $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid), false);\r
1584 $grade->insert('system');\r
1585 $grade->grade_item =& $this;\r
1586\r
1587 $oldgrade = new object();\r
1588 $oldgrade->finalgrade = $grade->finalgrade;\r
1589 $oldgrade->rawgrade = $grade->rawgrade;\r
1590 }\r
1591\r
1592 // no need to recalculate locked or overridden grades\r
1593 if ($grade->is_locked() or $grade->is_overridden()) {\r
1594 return true;\r
1595 }\r
1596\r
1597 // do the calculation\r
1598 $this->formula->set_params($params);\r
1599 $result = $this->formula->evaluate();\r
1600\r
1601 // no raw grade for calculated grades - only final\r
1602 $grade->rawgrade = null;\r
1603\r
1604\r
1605 if ($result === false) {\r
1606 $grade->finalgrade = null;\r
1607\r
1608 } else {\r
1609 // normalize\r
1610 $result = bounded_number($this->grademin, $result, $this->grademax);\r
1611 if ($this->gradetype == GRADE_TYPE_SCALE) {\r
1612 $result = round($result+0.00001); // round scales upwards\r
1613 }\r
1614 $grade->finalgrade = $result;\r
1615 }\r
1616\r
1617 // update in db if changed\r
1618 if ( $grade->finalgrade !== $oldgrade->finalgrade\r
1619 or $grade->rawgrade !== $oldgrade->rawgrade) {\r
1620\r
1621 $grade->update('system');\r
1622 }\r
1623\r
1624 if ($result !== false) {\r
1625 //lock grade if needed\r
1626 }\r
1627\r
1628 if ($result === false) {\r
1629 return false;\r
1630 } else {\r
1631 return true;\r
1632 }\r
1633\r
1634 }\r
1635\r
1636 /**\r
1637 * Validate the formula.\r
1638 * @param string $formula\r
1639 * @return boolean true if calculation possible, false otherwise\r
1640 */\r
1641 function validate_formula($formulastr) {\r
1642 global $CFG;\r
1643 require_once($CFG->libdir.'/mathslib.php');\r
1644\r
1645 $formulastr = grade_item::normalize_formula($formulastr, $this->courseid);\r
1646\r
1647 if (empty($formulastr)) {\r
1648 return true;\r
1649 }\r
1650\r
1651 if (strpos($formulastr, '=') !== 0) {\r
1652 return get_string('errorcalculationnoequal', 'grades');\r
1653 }\r
1654\r
1655 // get used items\r
1656 if (preg_match_all('/##gi(\d+)##/', $formulastr, $matches)) {\r
1657 $useditems = array_unique($matches[1]); // remove duplicates\r
1658 } else {\r
1659 $useditems = array();\r
1660 }\r
1661 \r
1662 // MDL-11902\r
1663 // unset the value if formula is trying to reference to itself\r
1664 // but array keys does not match itemid\r
1665 if (!empty($this->id)) {\r
1666 $useditems = array_diff($useditems, array($this->id));\r
1667 //unset($useditems[$this->id]);\r
1668 }\r
1669\r
1670 // prepare formula and init maths library\r
1671 $formula = preg_replace('/##(gi\d+)##/', '\1', $formulastr);\r
1672 $formula = new calc_formula($formula);\r
1673\r
1674\r
1675 if (empty($useditems)) {\r
1676 $grade_items = array();\r
1677\r
1678 } else {\r
1679 $gis = implode(',', $useditems);\r
1680\r
1681 $sql = "SELECT gi.*\r
1682 FROM {$CFG->prefix}grade_items gi\r
1683 WHERE gi.id IN ($gis) and gi.courseid={$this->courseid}"; // from the same course only!\r
1684\r
1685 if (!$grade_items = get_records_sql($sql)) {\r
1686 $grade_items = array();\r
1687 }\r
1688 }\r
1689\r
1690 $params = array();\r
1691 foreach ($useditems as $itemid) {\r
1692 // make sure all grade items exist in this course\r
1693 if (!array_key_exists($itemid, $grade_items)) {\r
1694 return false;\r
1695 }\r
1696 // use max grade when testing formula, this should be ok in 99.9%\r
1697 // division by 0 is one of possible problems\r
1698 $params['gi'.$grade_items[$itemid]->id] = $grade_items[$itemid]->grademax;\r
1699 }\r
1700\r
1701 // do the calculation\r
1702 $formula->set_params($params);\r
1703 $result = $formula->evaluate();\r
1704\r
1705 // false as result indicates some problem\r
1706 if ($result === false) {\r
1707 // TODO: add more error hints\r
1708 return get_string('errorcalculationunknown', 'grades');\r
1709 } else {\r
1710 return true;\r
1711 }\r
1712 }\r
1713\r
1714 /**\r
1715 * Returns the value of the display type. It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.\r
1716 * @return int Display type\r
1717 */\r
1718 function get_displaytype() {\r
1719 global $CFG;\r
1720\r
1721 if ($this->display == GRADE_DISPLAY_TYPE_DEFAULT) {\r
1722 return grade_get_setting($this->courseid, 'displaytype', $CFG->grade_displaytype);\r
1723\r
1724 } else {\r
1725 return $this->display;\r
1726 }\r
1727 }\r
1728\r
1729 /**\r
1730 * Returns the value of the decimals field. It can be set at 3 levels: grade_item, course setting and site. The lowest level overrides the higher ones.\r
1731 * @return int Decimals (0 - 5)\r
1732 */\r
1733 function get_decimals() {\r
1734 global $CFG;\r
1735\r
1736 if (is_null($this->decimals)) {\r
1737 return grade_get_setting($this->courseid, 'decimalpoints', $CFG->grade_decimalpoints);\r
1738\r
1739 } else {\r
1740 return $this->decimals;\r
1741 }\r
1742 }\r
1743}\r
1744?>\r