MDL-20827 workshop: set correct wtype on newly created instances
[moodle.git] / lib / grade / grade_grade.php
blob3b81aa76269ff0d988ffacbef561147fca609733
1 <?php // $Id$
3 ///////////////////////////////////////////////////////////////////////////
4 // //
5 // NOTICE OF COPYRIGHT //
6 // //
7 // Moodle - Modular Object-Oriented Dynamic Learning Environment //
8 // http://moodle.com //
9 // //
10 // Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
11 // //
12 // This program is free software; you can redistribute it and/or modify //
13 // it under the terms of the GNU General Public License as published by //
14 // the Free Software Foundation; either version 2 of the License, or //
15 // (at your option) any later version. //
16 // //
17 // This program is distributed in the hope that it will be useful, //
18 // but WITHOUT ANY WARRANTY; without even the implied warranty of //
19 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
20 // GNU General Public License for more details: //
21 // //
22 // http://www.gnu.org/copyleft/gpl.html //
23 // //
24 ///////////////////////////////////////////////////////////////////////////
26 require_once('grade_object.php');
28 class grade_grade extends grade_object {
30 /**
31 * The DB table.
32 * @var string $table
34 var $table = 'grade_grades';
36 /**
37 * Array of required table fields, must start with 'id'.
38 * @var array $required_fields
40 var $required_fields = array('id', 'itemid', 'userid', 'rawgrade', 'rawgrademax', 'rawgrademin',
41 'rawscaleid', 'usermodified', 'finalgrade', 'hidden', 'locked',
42 'locktime', 'exported', 'overridden', 'excluded', 'timecreated', 'timemodified');
44 /**
45 * Array of optional fields with default values (these should match db defaults)
46 * @var array $optional_fields
48 var $optional_fields = array('feedback'=>null, 'feedbackformat'=>0, 'information'=>null, 'informationformat'=>0);
50 /**
51 * The id of the grade_item this grade belongs to.
52 * @var int $itemid
54 var $itemid;
56 /**
57 * The grade_item object referenced by $this->itemid.
58 * @var object $grade_item
60 var $grade_item;
62 /**
63 * The id of the user this grade belongs to.
64 * @var int $userid
66 var $userid;
68 /**
69 * The grade value of this raw grade, if such was provided by the module.
70 * @var float $rawgrade
72 var $rawgrade;
74 /**
75 * The maximum allowable grade when this grade was created.
76 * @var float $rawgrademax
78 var $rawgrademax = 100;
80 /**
81 * The minimum allowable grade when this grade was created.
82 * @var float $rawgrademin
84 var $rawgrademin = 0;
86 /**
87 * id of the scale, if this grade is based on a scale.
88 * @var int $rawscaleid
90 var $rawscaleid;
92 /**
93 * The userid of the person who last modified this grade.
94 * @var int $usermodified
96 var $usermodified;
98 /**
99 * The final value of this grade.
100 * @var float $finalgrade
102 var $finalgrade;
105 * 0 if visible, 1 always hidden or date not visible until
106 * @var float $hidden
108 var $hidden = 0;
111 * 0 not locked, date when the item was locked
112 * @var float locked
114 var $locked = 0;
117 * 0 no automatic locking, date when to lock the grade automatically
118 * @var float $locktime
120 var $locktime = 0;
123 * Exported flag
124 * @var boolean $exported
126 var $exported = 0;
129 * Overridden flag
130 * @var boolean $overridden
132 var $overridden = 0;
135 * Grade excluded from aggregation functions
136 * @var boolean $excluded
138 var $excluded = 0;
141 * TODO: HACK: create a new field datesubmitted - the date of submission if any
142 * @var boolean $timecreated
144 var $timecreated = null;
147 * TODO: HACK: create a new field dategraded - the date of grading
148 * @var boolean $timemodified
150 var $timemodified = null;
154 * Returns array of grades for given grade_item+users.
155 * @param object $grade_item
156 * @param array $userids
157 * @param bool $include_missing include grades that do not exist yet
158 * @return array userid=>grade_grade array
160 function fetch_users_grades($grade_item, $userids, $include_missing=true) {
162 // hmm, there might be a problem with length of sql query
163 // if there are too many users requested - we might run out of memory anyway
164 $limit = 2000;
165 $count = count($userids);
166 if ($count > $limit) {
167 $half = (int)($count/2);
168 $first = array_slice($userids, 0, $half);
169 $second = array_slice($userids, $half);
170 return grade_grade::fetch_users_grades($grade_item, $first, $include_missing) + grade_grade::fetch_users_grades($grade_item, $second, $include_missing);
173 $user_ids_cvs = implode(',', $userids);
174 $result = array();
175 if ($grade_records = get_records_select('grade_grades', "itemid={$grade_item->id} AND userid IN ($user_ids_cvs)")) {
176 foreach ($grade_records as $record) {
177 $result[$record->userid] = new grade_grade($record, false);
180 if ($include_missing) {
181 foreach ($userids as $userid) {
182 if (!array_key_exists($userid, $result)) {
183 $grade_grade = new grade_grade();
184 $grade_grade->userid = $userid;
185 $grade_grade->itemid = $grade_item->id;
186 $result[$userid] = $grade_grade;
191 return $result;
195 * Loads the grade_item object referenced by $this->itemid and saves it as $this->grade_item for easy access.
196 * @return object grade_item.
198 function load_grade_item() {
199 if (empty($this->itemid)) {
200 debugging('Missing itemid');
201 $this->grade_item = null;
202 return null;
205 if (empty($this->grade_item)) {
206 $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
208 } else if ($this->grade_item->id != $this->itemid) {
209 debugging('Itemid mismatch');
210 $this->grade_item = grade_item::fetch(array('id'=>$this->itemid));
213 return $this->grade_item;
217 * Is grading object editable?
218 * @return boolean
220 function is_editable() {
221 if ($this->is_locked()) {
222 return false;
225 $grade_item = $this->load_grade_item();
227 if ($grade_item->gradetype == GRADE_TYPE_NONE) {
228 return false;
231 return true;
235 * Check grade lock status. Uses both grade item lock and grade lock.
236 * Internally any date in locked field (including future ones) means locked,
237 * the date is stored for logging purposes only.
239 * @return boolean true if locked, false if not
241 function is_locked() {
242 $this->load_grade_item();
243 if (empty($this->grade_item)) {
244 return !empty($this->locked);
245 } else {
246 return !empty($this->locked) or $this->grade_item->is_locked();
251 * Checks if grade overridden
252 * @return boolean
254 function is_overridden() {
255 return !empty($this->overridden);
259 * Returns timestamp of submission related to this grade,
260 * might be null if not submitted.
261 * @return int
263 function get_datesubmitted() {
264 //TODO: HACK - create new fields in 2.0
265 return $this->timecreated;
269 * Returns timestamp when last graded,
270 * might be null if no grade present.
271 * @return int
273 function get_dategraded() {
274 //TODO: HACK - create new fields in 2.0
275 if (is_null($this->finalgrade) and is_null($this->feedback)) {
276 return null; // no grade == no date
277 } else if ($this->overridden) {
278 return $this->overridden;
279 } else {
280 return $this->timemodified;
285 * Set the overridden status of grade
286 * @param boolean $state requested overridden state
287 * @param boolean $refresh refresh grades from external activities if needed
288 * @return boolean true is db state changed
290 function set_overridden($state, $refresh = true) {
291 if (empty($this->overridden) and $state) {
292 $this->overridden = time();
293 $this->update();
294 return true;
296 } else if (!empty($this->overridden) and !$state) {
297 $this->overridden = 0;
298 $this->update();
300 if ($refresh) {
301 //refresh when unlocking
302 $this->grade_item->refresh_grades($this->userid);
305 return true;
307 return false;
311 * Checks if grade excluded from aggregation functions
312 * @return boolean
314 function is_excluded() {
315 return !empty($this->excluded);
319 * Set the excluded status of grade
320 * @param boolean $state requested excluded state
321 * @return boolean true is db state changed
323 function set_excluded($state) {
324 if (empty($this->excluded) and $state) {
325 $this->excluded = time();
326 $this->update();
327 return true;
329 } else if (!empty($this->excluded) and !$state) {
330 $this->excluded = 0;
331 $this->update();
332 return true;
334 return false;
338 * Lock/unlock this grade.
340 * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
341 * @param boolean $cascade ignored param
342 * @param boolean $refresh refresh grades when unlocking
343 * @return boolean true if sucessful, false if can not set new lock state for grade
345 function set_locked($lockedstate, $cascade=false, $refresh=true) {
346 $this->load_grade_item();
348 if ($lockedstate) {
349 if ($this->grade_item->needsupdate) {
350 //can not lock grade if final not calculated!
351 return false;
354 $this->locked = time();
355 $this->update();
357 return true;
359 } else {
360 if (!empty($this->locked) and $this->locktime < time()) {
361 //we have to reset locktime or else it would lock up again
362 $this->locktime = 0;
365 // remove the locked flag
366 $this->locked = 0;
367 $this->update();
369 if ($refresh and !$this->is_overridden()) {
370 //refresh when unlocking and not overridden
371 $this->grade_item->refresh_grades($this->userid);
374 return true;
379 * Lock the grade if needed - make sure this is called only when final grades are valid
380 * @param array $items array of all grade item ids
381 * @return void
383 function check_locktime_all($items) {
384 global $CFG;
386 $items_sql = implode(',', $items);
388 $now = time(); // no rounding needed, this is not supposed to be called every 10 seconds
390 if ($rs = get_recordset_select('grade_grades', "itemid IN ($items_sql) AND locked = 0 AND locktime > 0 AND locktime < $now")) {
391 while ($grade = rs_fetch_next_record($rs)) {
392 $grade_grade = new grade_grade($grade, false);
393 $grade_grade->locked = time();
394 $grade_grade->update('locktime');
396 rs_close($rs);
401 * Set the locktime for this grade.
403 * @param int $locktime timestamp for lock to activate
404 * @return void
406 function set_locktime($locktime) {
407 $this->locktime = $locktime;
408 $this->update();
412 * Set the locktime for this grade.
414 * @return int $locktime timestamp for lock to activate
416 function get_locktime() {
417 $this->load_grade_item();
419 $item_locktime = $this->grade_item->get_locktime();
421 if (empty($this->locktime) or ($item_locktime and $item_locktime < $this->locktime)) {
422 return $item_locktime;
424 } else {
425 return $this->locktime;
430 * Check grade hidden status. Uses data from both grade item and grade.
431 * @return boolean true if hidden, false if not
433 function is_hidden() {
434 $this->load_grade_item();
435 if (empty($this->grade_item)) {
436 return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time());
437 } else {
438 return $this->hidden == 1 or ($this->hidden != 0 and $this->hidden > time()) or $this->grade_item->is_hidden();
443 * Check grade hidden status. Uses data from both grade item and grade.
444 * @return boolean true if hiddenuntil, false if not
446 function is_hiddenuntil() {
447 $this->load_grade_item();
449 if ($this->hidden == 1 or $this->grade_item->hidden == 1) {
450 return false; //always hidden
453 if ($this->hidden > 1 or $this->grade_item->hidden > 1) {
454 return true;
457 return false;
461 * Check grade hidden status. Uses data from both grade item and grade.
462 * @return int 0 means visible, 1 hidden always, timestamp hidden until
464 function get_hidden() {
465 $this->load_grade_item();
467 $item_hidden = $this->grade_item->get_hidden();
469 if ($item_hidden == 1) {
470 return 1;
472 } else if ($item_hidden == 0) {
473 return $this->hidden;
475 } else {
476 if ($this->hidden == 0) {
477 return $item_hidden;
478 } else if ($this->hidden == 1) {
479 return 1;
480 } else if ($this->hidden > $item_hidden) {
481 return $this->hidden;
482 } else {
483 return $item_hidden;
489 * Set the hidden status of grade, 0 mean visible, 1 always hidden, number means date to hide until.
490 * @param boolean $cascade ignored
491 * @param int $hidden new hidden status
493 function set_hidden($hidden, $cascade=false) {
494 $this->hidden = $hidden;
495 $this->update();
499 * Finds and returns a grade_grade instance based on params.
500 * @static
502 * @param array $params associative arrays varname=>value
503 * @return object grade_grade instance or false if none found.
505 function fetch($params) {
506 return grade_object::fetch_helper('grade_grades', 'grade_grade', $params);
510 * Finds and returns all grade_grade instances based on params.
511 * @static
513 * @param array $params associative arrays varname=>value
514 * @return array array of grade_grade insatnces or false if none found.
516 function fetch_all($params) {
517 return grade_object::fetch_all_helper('grade_grades', 'grade_grade', $params);
521 * Given a float value situated between a source minimum and a source maximum, converts it to the
522 * corresponding value situated between a target minimum and a target maximum. Thanks to Darlene
523 * for the formula :-)
525 * @static
526 * @param float $rawgrade
527 * @param float $source_min
528 * @param float $source_max
529 * @param float $target_min
530 * @param float $target_max
531 * @return float Converted value
533 function standardise_score($rawgrade, $source_min, $source_max, $target_min, $target_max) {
534 if (is_null($rawgrade)) {
535 return null;
538 if ($source_max == $source_min or $target_min == $target_max) {
539 // prevent division by 0
540 return $target_max;
543 $factor = ($rawgrade - $source_min) / ($source_max - $source_min);
544 $diff = $target_max - $target_min;
545 $standardised_value = $factor * $diff + $target_min;
546 return $standardised_value;
550 * Return array of grade item ids that are either hidden or indirectly depend
551 * on hidden grades, excluded grades are not returned.
552 * THIS IS A REALLY BIG HACK! to be replaced by conditional aggregation of hidden grades in 2.0
554 * @static
555 * @param array $grades all course grades of one user, & used for better internal caching
556 * @param array $items $grade_items array of grade items, & used for better internal caching
557 * @return array
559 function get_hiding_affected(&$grade_grades, &$grade_items) {
560 global $CFG;
562 if (count($grade_grades) !== count($grade_items)) {
563 error('Incorrect size of arrays in params of grade_grade::get_hiding_affected()!');
566 $dependson = array();
567 $todo = array();
568 $unknown = array(); // can not find altered
569 $altered = array(); // altered grades
571 $hiddenfound = false;
572 foreach($grade_grades as $itemid=>$unused) {
573 $grade_grade =& $grade_grades[$itemid];
574 if ($grade_grade->is_excluded()) {
575 //nothing to do, aggregation is ok
576 } else if ($grade_grade->is_hidden()) {
577 $hiddenfound = true;
578 $altered[$grade_grade->itemid] = null;
579 } else if ($grade_grade->is_locked() or $grade_grade->is_overridden()) {
580 // no need to recalculate locked or overridden grades
581 } else {
582 $dependson[$grade_grade->itemid] = $grade_items[$grade_grade->itemid]->depends_on();
583 if (!empty($dependson[$grade_grade->itemid])) {
584 $todo[] = $grade_grade->itemid;
588 if (!$hiddenfound) {
589 return array('unknown'=>array(), 'altered'=>array());
592 $max = count($todo);
593 for($i=0; $i<$max; $i++) {
594 $found = false;
595 foreach($todo as $key=>$do) {
596 if (array_intersect($dependson[$do], $unknown)) {
597 // this item depends on hidden grade indirectly
598 $unknown[$do] = $do;
599 unset($todo[$key]);
600 $found = true;
601 continue;
603 } else if (!array_intersect($dependson[$do], $todo)) {
604 if (!array_intersect($dependson[$do], array_keys($altered))) {
605 // hiding does not affect this grade
606 unset($todo[$key]);
607 $found = true;
608 continue;
610 } else {
611 // depends on altered grades - we should try to recalculate if possible
612 if ($grade_items[$do]->is_calculated() or (!$grade_items[$do]->is_category_item() and !$grade_items[$do]->is_course_item())) {
613 $unknown[$do] = $do;
614 unset($todo[$key]);
615 $found = true;
616 continue;
618 } else {
619 $grade_category = $grade_items[$do]->load_item_category();
621 $values = array();
622 foreach ($dependson[$do] as $itemid) {
623 if (array_key_exists($itemid, $altered)) {
624 $values[$itemid] = $altered[$itemid];
625 } elseif (!empty($values[$itemid])) {
626 $values[$itemid] = $grade_grades[$itemid]->finalgrade;
630 foreach ($values as $itemid=>$value) {
631 if ($grade_grades[$itemid]->is_excluded()) {
632 unset($values[$itemid]);
633 continue;
635 $values[$itemid] = grade_grade::standardise_score($value, $grade_items[$itemid]->grademin, $grade_items[$itemid]->grademax, 0, 1);
638 if ($grade_category->aggregateonlygraded) {
639 foreach ($values as $itemid=>$value) {
640 if (is_null($value)) {
641 unset($values[$itemid]);
644 } else {
645 foreach ($values as $itemid=>$value) {
646 if (is_null($value)) {
647 $values[$itemid] = 0;
652 // limit and sort
653 $grade_category->apply_limit_rules($values, $grade_items);
654 asort($values, SORT_NUMERIC);
656 // let's see we have still enough grades to do any statistics
657 if (count($values) == 0) {
658 // not enough attempts yet
659 $altered[$do] = null;
660 unset($todo[$key]);
661 $found = true;
662 continue;
665 $agg_grade = $grade_category->aggregate_values($values, $grade_items);
667 // recalculate the rawgrade back to requested range
668 $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $grade_items[$do]->grademin, $grade_items[$do]->grademax);
670 $finalgrade = $grade_items[$do]->bounded_grade($finalgrade);
672 $altered[$do] = $finalgrade;
673 unset($todo[$key]);
674 $found = true;
675 continue;
680 if (!$found) {
681 break;
685 return array('unknown'=>$unknown, 'altered'=>$altered);
689 * Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
690 * @param object $grade_item An optional grade_item of which gradepass value we can use, saves having to load the grade_grade's grade_item
691 * @return boolean
693 function is_passed($grade_item = null) {
694 if (empty($grade_item)) {
695 if (!isset($this->grade_item)) {
696 $this->load_grade_item();
698 } else {
699 $this->grade_item = $grade_item;
700 $this->itemid = $grade_item->id;
703 // Return null if finalgrade is null
704 if (is_null($this->finalgrade)) {
705 return null;
708 // Return null if gradepass == grademin or gradepass is null
709 if (is_null($this->grade_item->gradepass) || $this->grade_item->gradepass == $this->grade_item->grademin) {
710 return null;
713 return $this->finalgrade >= $this->grade_item->gradepass;
716 function insert($source=null) {
717 // TODO: dategraded hack - do not update times, they are used for submission and grading
718 //$this->timecreated = $this->timemodified = time();
719 return parent::insert($source);
723 * In addition to update() as defined in grade_object rounds the float numbers using php function,
724 * the reason is we need to compare the db value with computed number to skip updates if possible.
725 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
726 * @return boolean success
728 function update($source=null) {
729 $this->rawgrade = grade_floatval($this->rawgrade);
730 $this->finalgrade = grade_floatval($this->finalgrade);
731 $this->rawgrademin = grade_floatval($this->rawgrademin);
732 $this->rawgrademax = grade_floatval($this->rawgrademax);
733 return parent::update($source);