3 ///////////////////////////////////////////////////////////////////////////
5 // NOTICE OF COPYRIGHT //
7 // Moodle - Modular Object-Oriented Dynamic Learning Environment //
8 // http://moodle.com //
10 // Copyright (C) 1999 onwards Martin Dougiamas http://dougiamas.com //
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. //
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: //
22 // http://www.gnu.org/copyleft/gpl.html //
24 ///////////////////////////////////////////////////////////////////////////
26 require_once('grade_object.php');
28 class grade_grade
extends grade_object
{
34 var $table = 'grade_grades';
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');
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);
51 * The id of the grade_item this grade belongs to.
57 * The grade_item object referenced by $this->itemid.
58 * @var object $grade_item
63 * The id of the user this grade belongs to.
69 * The grade value of this raw grade, if such was provided by the module.
70 * @var float $rawgrade
75 * The maximum allowable grade when this grade was created.
76 * @var float $rawgrademax
78 var $rawgrademax = 100;
81 * The minimum allowable grade when this grade was created.
82 * @var float $rawgrademin
87 * id of the scale, if this grade is based on a scale.
88 * @var int $rawscaleid
93 * The userid of the person who last modified this grade.
94 * @var int $usermodified
99 * The final value of this grade.
100 * @var float $finalgrade
105 * 0 if visible, 1 always hidden or date not visible until
111 * 0 not locked, date when the item was locked
117 * 0 no automatic locking, date when to lock the grade automatically
118 * @var float $locktime
124 * @var boolean $exported
130 * @var boolean $overridden
135 * Grade excluded from aggregation functions
136 * @var boolean $excluded
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
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);
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;
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;
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?
220 function is_editable() {
221 if ($this->is_locked()) {
225 $grade_item = $this->load_grade_item();
227 if ($grade_item->gradetype
== GRADE_TYPE_NONE
) {
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
);
246 return !empty($this->locked
) or $this->grade_item
->is_locked();
251 * Checks if grade overridden
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.
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.
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
;
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();
296 } else if (!empty($this->overridden
) and !$state) {
297 $this->overridden
= 0;
301 //refresh when unlocking
302 $this->grade_item
->refresh_grades($this->userid
);
311 * Checks if grade excluded from aggregation functions
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();
329 } else if (!empty($this->excluded
) and !$state) {
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();
349 if ($this->grade_item
->needsupdate
) {
350 //can not lock grade if final not calculated!
354 $this->locked
= time();
360 if (!empty($this->locked
) and $this->locktime
< time()) {
361 //we have to reset locktime or else it would lock up again
365 // remove the locked flag
369 if ($refresh and !$this->is_overridden()) {
370 //refresh when unlocking and not overridden
371 $this->grade_item
->refresh_grades($this->userid
);
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
383 function check_locktime_all($items) {
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');
401 * Set the locktime for this grade.
403 * @param int $locktime timestamp for lock to activate
406 function set_locktime($locktime) {
407 $this->locktime
= $locktime;
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;
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());
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) {
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) {
472 } else if ($item_hidden == 0) {
473 return $this->hidden
;
476 if ($this->hidden
== 0) {
478 } else if ($this->hidden
== 1) {
480 } else if ($this->hidden
> $item_hidden) {
481 return $this->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;
499 * Finds and returns a grade_grade instance based on params.
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.
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 :-)
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)) {
538 if ($source_max == $source_min or $target_min == $target_max) {
539 // prevent division by 0
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
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
559 function get_hiding_affected(&$grade_grades, &$grade_items) {
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();
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()) {
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
582 $dependson[$grade_grade->itemid
] = $grade_items[$grade_grade->itemid
]->depends_on();
583 if (!empty($dependson[$grade_grade->itemid
])) {
584 $todo[] = $grade_grade->itemid
;
589 return array('unknown'=>array(), 'altered'=>array());
593 for($i=0; $i<$max; $i++
) {
595 foreach($todo as $key=>$do) {
596 if (array_intersect($dependson[$do], $unknown)) {
597 // this item depends on hidden grade indirectly
603 } else if (!array_intersect($dependson[$do], $todo)) {
604 if (!array_intersect($dependson[$do], array_keys($altered))) {
605 // hiding does not affect this grade
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())) {
619 $grade_category = $grade_items[$do]->load_item_category();
622 foreach ($dependson[$do] as $itemid) {
623 if (array_key_exists($itemid, $altered)) {
624 $values[$itemid] = $altered[$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]);
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]);
645 foreach ($values as $itemid=>$value) {
646 if (is_null($value)) {
647 $values[$itemid] = 0;
653 $grade_category->apply_limit_rules($values);
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;
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 if (!is_null($finalgrade)) {
671 $finalgrade = bounded_number($grade_items[$do]->grademin
, $finalgrade, $grade_items[$do]->grademax
);
674 $altered[$do] = $finalgrade;
687 return array('unknown'=>$unknown, 'altered'=>$altered);
691 * Returns true if the grade's value is superior or equal to the grade item's gradepass value, false otherwise.
692 * @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
695 function is_passed($grade_item = null) {
696 if (empty($grade_item)) {
697 if (!isset($this->grade_item
)) {
698 $this->load_grade_item();
701 $this->grade_item
= $grade_item;
702 $this->itemid
= $grade_item->id
;
705 // Return null if finalgrade is null
706 if (is_null($this->finalgrade
)) {
710 // Return null if gradepass == grademin or gradepass is null
711 if (is_null($this->grade_item
->gradepass
) ||
$this->grade_item
->gradepass
== $this->grade_item
->grademin
) {
715 return $this->finalgrade
>= $this->grade_item
->gradepass
;
718 function insert($source=null) {
719 // TODO: dategraded hack - do not update times, they are used for submission and grading
720 //$this->timecreated = $this->timemodified = time();
721 return parent
::insert($source);
725 * In addition to update() as defined in grade_object rounds the float numbers using php function,
726 * the reason is we need to compare the db value with computed number to skip updates if possible.
727 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
728 * @return boolean success
730 function update($source=null) {
731 $this->rawgrade
= grade_floatval($this->rawgrade
);
732 $this->finalgrade
= grade_floatval($this->finalgrade
);
733 $this->rawgrademin
= grade_floatval($this->rawgrademin
);
734 $this->rawgrademax
= grade_floatval($this->rawgrademax
);
735 return parent
::update($source);