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