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 |
26 | require_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 |
32 | class 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 |