MDL-11517 reserved word MOD used in table alias in questions backup code
[moodle-pu.git] / lib / grade / grade_category.php
blob4b3159d4b9430268f76f0af6698ea3f5cb476919
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) 2001-2003 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_category extends grade_object {
29 /**
30 * The DB table.
31 * @var string $table
33 var $table = 'grade_categories';
35 /**
36 * Array of required table fields, must start with 'id'.
37 * @var array $required_fields
39 var $required_fields = array('id', 'courseid', 'parent', 'depth', 'path', 'fullname', 'aggregation',
40 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes',
41 'aggregatesubcats', 'timecreated', 'timemodified');
43 /**
44 * The course this category belongs to.
45 * @var int $courseid
47 var $courseid;
49 /**
50 * The category this category belongs to (optional).
51 * @var int $parent
53 var $parent;
55 /**
56 * The grade_category object referenced by $this->parent (PK).
57 * @var object $parent_category
59 var $parent_category;
61 /**
62 * The number of parents this category has.
63 * @var int $depth
65 var $depth = 0;
67 /**
68 * Shows the hierarchical path for this category as /1/2/3/ (like course_categories), the last number being
69 * this category's autoincrement ID number.
70 * @var string $path
72 var $path;
74 /**
75 * The name of this category.
76 * @var string $fullname
78 var $fullname;
80 /**
81 * A constant pointing to one of the predefined aggregation strategies (none, mean, median, sum etc) .
82 * @var int $aggregation
84 var $aggregation = GRADE_AGGREGATE_MEAN;
86 /**
87 * Keep only the X highest items.
88 * @var int $keephigh
90 var $keephigh = 0;
92 /**
93 * Drop the X lowest items.
94 * @var int $droplow
96 var $droplow = 0;
98 /**
99 * Aggregate only graded items
100 * @var int $aggregateonlygraded
102 var $aggregateonlygraded = 0;
105 * Aggregate outcomes together with normal items
106 * @var int $aggregateoutcomes
108 var $aggregateoutcomes = 0;
111 * Ignore subcategories when aggregating
112 * @var int $aggregatesubcats
114 var $aggregatesubcats = 0;
117 * Array of grade_items or grade_categories nested exactly 1 level below this category
118 * @var array $children
120 var $children;
123 * A hierarchical array of all children below this category. This is stored separately from
124 * $children because it is more memory-intensive and may not be used as often.
125 * @var array $all_children
127 var $all_children;
130 * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
131 * for this category.
132 * @var object $grade_item
134 var $grade_item;
137 * Temporary sortorder for speedup of children resorting
139 var $sortorder;
142 * Builds this category's path string based on its parents (if any) and its own id number.
143 * This is typically done just before inserting this object in the DB for the first time,
144 * or when a new parent is added or changed. It is a recursive function: once the calling
145 * object no longer has a parent, the path is complete.
147 * @static
148 * @param object $grade_category
149 * @return int The depth of this category (2 means there is one parent)
151 function build_path($grade_category) {
152 if (empty($grade_category->parent)) {
153 return '/'.$grade_category->id.'/';
154 } else {
155 $parent = get_record('grade_categories', 'id', $grade_category->parent);
156 return grade_category::build_path($parent).$grade_category->id.'/';
161 * Finds and returns a grade_category instance based on params.
162 * @static
164 * @param array $params associative arrays varname=>value
165 * @return object grade_category instance or false if none found.
167 function fetch($params) {
168 return grade_object::fetch_helper('grade_categories', 'grade_category', $params);
172 * Finds and returns all grade_category instances based on params.
173 * @static
175 * @param array $params associative arrays varname=>value
176 * @return array array of grade_category insatnces or false if none found.
178 function fetch_all($params) {
179 return grade_object::fetch_all_helper('grade_categories', 'grade_category', $params);
183 * In addition to update() as defined in grade_object, call force_regrading of parent categories, if applicable.
184 * @param string $source from where was the object updated (mod/forum, manual, etc.)
185 * @return boolean success
187 function update($source=null) {
188 // load the grade item or create a new one
189 $this->load_grade_item();
191 // force recalculation of path;
192 if (empty($this->path)) {
193 $this->path = grade_category::build_path($this);
194 $this->depth = substr_count($this->path, '/') - 1;
198 // Recalculate grades if needed
199 if ($this->qualifies_for_regrading()) {
200 $this->force_regrading();
203 return parent::update($source);
207 * If parent::delete() is successful, send force_regrading message to parent category.
208 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
209 * @return boolean success
211 function delete($source=null) {
212 $grade_item = $this->load_grade_item();
214 if ($this->is_course_category()) {
215 if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) {
216 foreach ($categories as $category) {
217 if ($category->id == $this->id) {
218 continue; // do not delete course category yet
220 $category->delete($source);
224 if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) {
225 foreach ($items as $item) {
226 if ($item->id == $grade_item->id) {
227 continue; // do not delete course item yet
229 $item->delete($source);
233 } else {
234 $this->force_regrading();
236 $parent = $this->load_parent_category();
238 // Update children's categoryid/parent field first
239 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
240 foreach ($children as $child) {
241 $child->set_parent($parent->id);
244 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
245 foreach ($children as $child) {
246 $child->set_parent($parent->id);
251 // first delete the attached grade item and grades
252 $grade_item->delete($source);
254 // delete category itself
255 return parent::delete($source);
259 * In addition to the normal insert() defined in grade_object, this method sets the depth
260 * and path for this object, and update the record accordingly. The reason why this must
261 * be done here instead of in the constructor, is that they both need to know the record's
262 * id number, which only gets created at insertion time.
263 * This method also creates an associated grade_item if this wasn't done during construction.
264 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
265 * @return int PK ID if successful, false otherwise
267 function insert($source=null) {
269 if (empty($this->courseid)) {
270 error('Can not insert grade category without course id!');
273 if (empty($this->parent)) {
274 $course_category = grade_category::fetch_course_category($this->courseid);
275 $this->parent = $course_category->id;
278 $this->path = null;
280 if (!parent::insert($source)) {
281 debugging("Could not insert this category: " . print_r($this, true));
282 return false;
285 $this->force_regrading();
287 // build path and depth
288 $this->update($source);
290 return $this->id;
294 * Internal function - used only from fetch_course_category()
295 * Normal insert() can not be used for course category
296 * @param int $courseid
297 * @return bool success
299 function insert_course_category($courseid) {
300 $this->courseid = $courseid;
301 $this->fullname = get_string('coursegradecategory', 'grades');
302 $this->path = null;
303 $this->parent = null;
304 $this->aggregate = GRADE_AGGREGATE_MEAN;
306 if (!parent::insert('system')) {
307 debugging("Could not insert this category: " . print_r($this, true));
308 return false;
311 // build path and depth
312 $this->update('system');
314 return $this->id;
318 * Compares the values held by this object with those of the matching record in DB, and returns
319 * whether or not these differences are sufficient to justify an update of all parent objects.
320 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
321 * @return boolean
323 function qualifies_for_regrading() {
324 if (empty($this->id)) {
325 debugging("Can not regrade non existing category");
326 return false;
329 $db_item = grade_category::fetch(array('id'=>$this->id));
331 $aggregationdiff = $db_item->aggregation != $this->aggregation;
332 $keephighdiff = $db_item->keephigh != $this->keephigh;
333 $droplowdiff = $db_item->droplow != $this->droplow;
334 $aggonlygrddiff = $db_item->aggregateonlygraded != $this->aggregateonlygraded;
335 $aggoutcomesdiff = $db_item->aggregateoutcomes != $this->aggregateoutcomes;
336 $aggsubcatsdiff = $db_item->aggregatesubcats != $this->aggregatesubcats;
338 return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff || $aggsubcatsdiff);
342 * Marks the category and course item as needing update - categories are always regraded.
343 * @return void
345 function force_regrading() {
346 $grade_item = $this->load_grade_item();
347 $grade_item->force_regrading();
351 * Generates and saves raw_grades in associated category grade item.
352 * These immediate children must alrady have their own final grades.
353 * The category's aggregation method is used to generate raw grades.
355 * Please note that category grade is either calculated or aggregated - not both at the same time.
357 * This method must be used ONLY from grade_item::regrade_final_grades(),
358 * because the calculation must be done in correct order!
360 * Steps to follow:
361 * 1. Get final grades from immediate children
362 * 3. Aggregate these grades
363 * 4. Save them in raw grades of associated category grade item
365 function generate_grades($userid=null) {
366 global $CFG;
368 $this->load_grade_item();
370 if ($this->grade_item->is_locked()) {
371 return true; // no need to recalculate locked items
374 $this->grade_item->load_scale();
376 // find grade items of immediate children (category or grade items)
377 $depends_on = $this->grade_item->depends_on();
379 if (empty($depends_on)) {
380 $items = false;
381 } else {
382 $gis = implode(',', $depends_on);
383 $sql = "SELECT *
384 FROM {$CFG->prefix}grade_items
385 WHERE id IN ($gis)";
386 $items = get_records_sql($sql);
389 if ($userid) {
390 $usersql = "AND g.userid=$userid";
391 } else {
392 $usersql = "";
395 $grade_inst = new grade_grade();
396 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
398 // where to look for final grades - include grade of this item too, we will store the results there
399 $gis = implode(',', array_merge($depends_on, array($this->grade_item->id)));
400 $sql = "SELECT $fields
401 FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
402 WHERE gi.id = g.itemid AND gi.id IN ($gis) $usersql
403 ORDER BY g.userid";
405 // group the results by userid and aggregate the grades for this user
406 if ($rs = get_recordset_sql($sql)) {
407 if ($rs->RecordCount() > 0) {
408 $prevuser = 0;
409 $grade_values = array();
410 $excluded = array();
411 $oldgrade = null;
412 while ($used = rs_fetch_next_record($rs)) {
413 if ($used->userid != $prevuser) {
414 $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);
415 $prevuser = $used->userid;
416 $grade_values = array();
417 $excluded = array();
418 $oldgrade = null;
420 $grade_values[$used->itemid] = $used->finalgrade;
421 if ($used->excluded) {
422 $excluded[] = $used->itemid;
424 if ($this->grade_item->id == $used->itemid) {
425 $oldgrade = $used;
428 $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);//the last one
430 rs_close($rs);
433 return true;
437 * internal function for category grades aggregation
439 function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) {
440 global $CFG;
441 if (empty($userid)) {
442 //ignore first call
443 return;
446 if ($oldgrade) {
447 $grade = new grade_grade($oldgrade, false);
448 $grade->grade_item =& $this->grade_item;
450 } else {
451 // insert final grade - it will be needed later anyway
452 $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
453 $grade->insert('system');
454 $grade->grade_item =& $this->grade_item;
456 $oldgrade = new object();
457 $oldgrade->finalgrade = $grade->finalgrade;
458 $oldgrade->rawgrade = $grade->rawgrade;
459 $oldgrade->rawgrademin = $grade->rawgrademin;
460 $oldgrade->rawgrademax = $grade->rawgrademax;
461 $oldgrade->rawscaleid = $grade->rawscaleid;
464 // no need to recalculate locked or overridden grades
465 if ($grade->is_locked() or $grade->is_overridden()) {
466 return;
469 // can not use own final category grade in calculation
470 unset($grade_values[$this->grade_item->id]);
472 // if no grades calculation possible or grading not allowed clear both final and raw
473 if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
474 $grade->finalgrade = null;
475 $grade->rawgrade = null;
476 if ($grade->finalgrade !== $oldgrade->finalgrade or $grade->rawgrade !== $oldgrade->rawgrade) {
477 $grade->update('system');
479 return;
482 /// normalize the grades first - all will have value 0...1
483 // ungraded items are not used in aggregation
484 foreach ($grade_values as $itemid=>$v) {
485 if (is_null($v)) {
486 // null means no grade
487 unset($grade_values[$itemid]);
488 continue;
489 } else if (in_array($itemid, $excluded)) {
490 unset($grade_values[$itemid]);
491 continue;
494 $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1);
497 // If global aggregateonlygraded is set, override category value
498 if ($CFG->grade_aggregateonlygraded != -1) {
499 $this->aggregateonlygraded = $CFG->grade_aggregateonlygraded;
502 // use min grade if grade missing for these types
503 if (!$this->aggregateonlygraded) {
504 foreach($items as $itemid=>$value) {
505 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
506 $grade_values[$itemid] = 0;
511 // limit and sort
512 $this->apply_limit_rules($grade_values);
513 asort($grade_values, SORT_NUMERIC);
515 // let's see we have still enough grades to do any statistics
516 if (count($grade_values) == 0) {
517 // not enough attempts yet
518 $grade->finalgrade = null;
519 $grade->rawgrade = null;
520 if ($grade->finalgrade !== $oldgrade->finalgrade or $grade->rawgrade !== $oldgrade->rawgrade) {
521 $grade->update('system');
523 return;
526 /// start the aggregation
527 switch ($this->aggregation) {
528 case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
529 $num = count($grade_values);
530 $grades = array_values($grade_values);
531 if ($num % 2 == 0) {
532 $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2;
533 } else {
534 $agg_grade = $grades[intval(($num/2)-0.5)];
536 break;
538 case GRADE_AGGREGATE_MIN:
539 $agg_grade = reset($grade_values);
540 break;
542 case GRADE_AGGREGATE_MAX:
543 $agg_grade = array_pop($grade_values);
544 break;
546 case GRADE_AGGREGATE_MODE: // the most common value, average used if multimode
547 $freq = array_count_values($grade_values);
548 arsort($freq); // sort by frequency keeping keys
549 $top = reset($freq); // highest frequency count
550 $modes = array_keys($freq, $top); // search for all modes (have the same highest count)
551 rsort($modes, SORT_NUMERIC); // get highes mode
552 $agg_grade = reset($modes);
553 break;
555 case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades
556 $weightsum = 0;
557 $sum = 0;
558 foreach($grade_values as $itemid=>$grade_value) {
559 if ($items[$itemid]->aggregationcoef <= 0) {
560 continue;
562 $weightsum += $items[$itemid]->aggregationcoef;
563 $sum += $items[$itemid]->aggregationcoef * $grade_value;
565 if ($weightsum == 0) {
566 $agg_grade = null;
567 } else {
568 $agg_grade = $sum / $weightsum;
570 break;
572 case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
573 $num = 0;
574 $sum = 0;
575 foreach($grade_values as $itemid=>$grade_value) {
576 if ($items[$itemid]->aggregationcoef == 0) {
577 $num += 1;
578 $sum += $grade_value;
579 } else if ($items[$itemid]->aggregationcoef > 0) {
580 $sum += $items[$itemid]->aggregationcoef * $grade_value;
583 if ($num == 0) {
584 $agg_grade = $sum; // only extra credits or wrong coefs
585 } else {
586 $agg_grade = $sum / $num;
588 break;
590 case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
591 default:
592 $num = count($grade_values);
593 $sum = array_sum($grade_values);
594 $agg_grade = $sum / $num;
595 break;
598 /// prepare update of new raw grade
599 $grade->rawgrademin = $this->grade_item->grademin;
600 $grade->rawgrademax = $this->grade_item->grademax;
601 $grade->rawscaleid = $this->grade_item->scaleid;
602 $grade->rawgrade = null; // categories do not use raw grades
604 // recalculate the rawgrade back to requested range
605 $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax);
607 if (!is_null($finalgrade)) {
608 $grade->finalgrade = bounded_number($this->grade_item->grademin, $finalgrade, $this->grade_item->grademax);
609 } else {
610 $grade->finalgrade = $finalgrade;
613 // update in db if changed
614 if ( $grade->finalgrade !== $oldgrade->finalgrade
615 or $grade->rawgrade !== $oldgrade->rawgrade
616 or $grade->rawgrademin !== $oldgrade->rawgrademin
617 or $grade->rawgrademax !== $oldgrade->rawgrademax
618 or $grade->rawscaleid !== $oldgrade->rawscaleid) {
620 $grade->update('system');
623 return;
627 * Given an array of grade values (numerical indices), applies droplow or keephigh
628 * rules to limit the final array.
629 * @param array $grade_values
630 * @return array Limited grades.
632 function apply_limit_rules(&$grade_values) {
633 global $CFG;
635 // If global keephigh and/or droplow are set, override category variable
636 if ($CFG->grade_keephigh != -1) {
637 $this->keephigh = $CFG->grade_keephigh;
640 if ($CFG->grade_droplow != -1) {
641 $this->droplow = $CFG->grade_droplow;
644 arsort($grade_values, SORT_NUMERIC);
645 if (!empty($this->droplow)) {
646 for ($i = 0; $i < $this->droplow; $i++) {
647 array_pop($grade_values);
649 } elseif (!empty($this->keephigh)) {
650 while (count($grade_values) > $this->keephigh) {
651 array_pop($grade_values);
658 * Returns true if category uses special aggregation coeficient
659 * @return boolean true if coeficient used
661 function is_aggregationcoef_used() {
662 return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN
663 or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN);
668 * Returns tree with all grade_items and categories as elements
669 * @static
670 * @param int $courseid
671 * @param boolean $include_category_items as category children
672 * @return array
674 function fetch_course_tree($courseid, $include_category_items=false) {
675 $course_category = grade_category::fetch_course_category($courseid);
676 $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
677 'children'=>$course_category->get_children($include_category_items));
678 $sortorder = 1;
679 $course_category->set_sortorder($sortorder);
680 $course_category->sortorder = $sortorder;
681 return grade_category::_fetch_course_tree_recursion($category_array, $sortorder);
684 function _fetch_course_tree_recursion($category_array, &$sortorder) {
685 // update the sortorder in db if needed
686 if ($category_array['object']->sortorder != $sortorder) {
687 $category_array['object']->set_sortorder($sortorder);
690 // store the grade_item or grade_category instance with extra info
691 $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);
693 // reuse final grades if there
694 if (array_key_exists('finalgrades', $category_array)) {
695 $result['finalgrades'] = $category_array['finalgrades'];
698 // recursively resort children
699 if (!empty($category_array['children'])) {
700 $result['children'] = array();
701 //process the category item first
702 $cat_item_id = null;
703 foreach($category_array['children'] as $oldorder=>$child_array) {
704 if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
705 $result['children'][$sortorder] = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
708 foreach($category_array['children'] as $oldorder=>$child_array) {
709 if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
710 $result['children'][++$sortorder] = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
715 return $result;
719 * Fetches and returns all the children categories and/or grade_items belonging to this category.
720 * By default only returns the immediate children (depth=1), but deeper levels can be requested,
721 * as well as all levels (0). The elements are indexed by sort order.
722 * @return array Array of child objects (grade_category and grade_item).
724 function get_children($include_category_items=false) {
726 // This function must be as fast as possible ;-)
727 // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
728 // we have to limit the number of queries though, because it will be used often in grade reports
730 $cats = get_records('grade_categories', 'courseid', $this->courseid);
731 $items = get_records('grade_items', 'courseid', $this->courseid);
733 // init children array first
734 foreach ($cats as $catid=>$cat) {
735 $cats[$catid]->children = array();
738 //first attach items to cats and add category sortorder
739 foreach ($items as $item) {
740 if ($item->itemtype == 'course' or $item->itemtype == 'category') {
741 $cats[$item->iteminstance]->sortorder = $item->sortorder;
743 if (!$include_category_items) {
744 continue;
746 $categoryid = $item->iteminstance;
747 } else {
748 $categoryid = $item->categoryid;
751 // prevent problems with duplicate sortorders in db
752 $sortorder = $item->sortorder;
753 while(array_key_exists($sortorder, $cats[$categoryid]->children)) {
754 //debugging("$sortorder exists in item loop");
755 $sortorder++;
758 $cats[$categoryid]->children[$sortorder] = $item;
762 // now find the requested category and connect categories as children
763 $category = false;
764 foreach ($cats as $catid=>$cat) {
765 if (!empty($cat->parent)) {
766 // prevent problems with duplicate sortorders in db
767 $sortorder = $cat->sortorder;
768 while(array_key_exists($sortorder, $cats[$cat->parent]->children)) {
769 //debugging("$sortorder exists in cat loop");
770 $sortorder++;
773 $cats[$cat->parent]->children[$sortorder] = $cat;
776 if ($catid == $this->id) {
777 $category = &$cats[$catid];
781 unset($items); // not needed
782 unset($cats); // not needed
784 $children_array = grade_category::_get_children_recursion($category);
786 ksort($children_array);
788 return $children_array;
792 function _get_children_recursion($category) {
794 $children_array = array();
795 foreach($category->children as $sortorder=>$child) {
796 if (array_key_exists('itemtype', $child)) {
797 $grade_item = new grade_item($child, false);
798 if (in_array($grade_item->itemtype, array('course', 'category'))) {
799 $type = $grade_item->itemtype.'item';
800 $depth = $category->depth;
801 } else {
802 $type = 'item';
803 $depth = $category->depth; // we use this to set the same colour
805 $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);
807 } else {
808 $children = grade_category::_get_children_recursion($child);
809 $grade_category = new grade_category($child, false);
810 if (empty($children)) {
811 $children = array();
813 $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
817 // sort the array
818 ksort($children_array);
820 return $children_array;
824 * Uses get_grade_item to load or create a grade_item, then saves it as $this->grade_item.
825 * @return object Grade_item
827 function load_grade_item() {
828 if (empty($this->grade_item)) {
829 $this->grade_item = $this->get_grade_item();
831 return $this->grade_item;
835 * Retrieves from DB and instantiates the associated grade_item object.
836 * If no grade_item exists yet, create one.
837 * @return object Grade_item
839 function get_grade_item() {
840 if (empty($this->id)) {
841 debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
842 return false;
845 if (empty($this->parent)) {
846 $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);
848 } else {
849 $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
852 if (!$grade_items = grade_item::fetch_all($params)) {
853 // create a new one
854 $grade_item = new grade_item($params, false);
855 $grade_item->gradetype = GRADE_TYPE_VALUE;
856 $grade_item->insert('system');
858 } else if (count($grade_items) == 1){
859 // found existing one
860 $grade_item = reset($grade_items);
862 } else {
863 debugging("Found more than one grade_item attached to category id:".$this->id);
864 // return first one
865 $grade_item = reset($grade_items);
868 return $grade_item;
872 * Uses $this->parent to instantiate $this->parent_category based on the
873 * referenced record in the DB.
874 * @return object Parent_category
876 function load_parent_category() {
877 if (empty($this->parent_category) && !empty($this->parent)) {
878 $this->parent_category = $this->get_parent_category();
880 return $this->parent_category;
884 * Uses $this->parent to instantiate and return a grade_category object.
885 * @return object Parent_category
887 function get_parent_category() {
888 if (!empty($this->parent)) {
889 $parent_category = new grade_category(array('id' => $this->parent));
890 return $parent_category;
891 } else {
892 return null;
897 * Returns the most descriptive field for this object. This is a standard method used
898 * when we do not know the exact type of an object.
899 * @return string name
901 function get_name() {
902 if (empty($this->parent)) {
903 $course = get_record('course', 'id', $this->courseid);
904 return format_string($course->fullname);
905 } else {
906 return $this->fullname;
911 * Sets this category's parent id. A generic method shared by objects that have a parent id of some kind.
912 * @param int parentid
913 * @return boolean success
915 function set_parent($parentid, $source=null) {
916 if ($this->parent == $parentid) {
917 return true;
920 if ($parentid == $this->id) {
921 error('Can not assign self as parent!');
924 if (empty($this->parent) and $this->is_course_category()) {
925 error('Course category can not have parent!');
928 // find parent and check course id
929 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
930 return false;
933 $this->force_regrading();
935 // set new parent category
936 $this->parent = $parent_category->id;
937 $this->parent_category =& $parent_category;
938 $this->path = null; // remove old path and depth - will be recalculated in update()
939 $this->depth = null; // remove old path and depth - will be recalculated in update()
940 $this->update($source);
942 return $this->update($source);
946 * Returns the final values for this grade category.
947 * @param int $userid Optional: to retrieve a single final grade
948 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
950 function get_final($userid=NULL) {
951 $this->load_grade_item();
952 return $this->grade_item->get_final($userid);
956 * Returns the sortorder of the associated grade_item. This method is also available in
957 * grade_item, for cases where the object type is not known.
958 * @return int Sort order
960 function get_sortorder() {
961 $this->load_grade_item();
962 return $this->grade_item->get_sortorder();
966 * Returns the idnumber of the associated grade_item. This method is also available in
967 * grade_item, for cases where the object type is not known.
968 * @return string idnumber
970 function get_idnumber() {
971 $this->load_grade_item();
972 return $this->grade_item->get_idnumber();
976 * Sets sortorder variable for this category.
977 * This method is also available in grade_item, for cases where the object type is not know.
978 * @param int $sortorder
979 * @return void
981 function set_sortorder($sortorder) {
982 $this->load_grade_item();
983 $this->grade_item->set_sortorder($sortorder);
987 * Move this category after the given sortorder - does not change the parent
988 * @param int $sortorder to place after
990 function move_after_sortorder($sortorder) {
991 $this->load_grade_item();
992 $this->grade_item->move_after_sortorder($sortorder);
996 * Return true if this is the top most category that represents the total course grade.
997 * @return boolean
999 function is_course_category() {
1000 $this->load_grade_item();
1001 return $this->grade_item->is_course_item();
1005 * Return the top most course category.
1006 * @static
1007 * @return object grade_category instance for course grade
1009 function fetch_course_category($courseid) {
1011 // course category has no parent
1012 if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
1013 return $course_category;
1016 // create a new one
1017 $course_category = new grade_category();
1018 $course_category->insert_course_category($courseid);
1020 return $course_category;
1024 * Is grading object editable?
1025 * @return boolean
1027 function is_editable() {
1028 return true;
1032 * Returns the locked state/date of the associated grade_item. This method is also available in
1033 * grade_item, for cases where the object type is not known.
1034 * @return boolean
1036 function is_locked() {
1037 $this->load_grade_item();
1038 return $this->grade_item->is_locked();
1042 * Sets the grade_item's locked variable and updates the grade_item.
1043 * Method named after grade_item::set_locked().
1044 * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
1045 * @param boolean $cascade lock/unlock child objects too
1046 * @param boolean $refresh refresh grades when unlocking
1047 * @return boolean success if category locked (not all children mayb be locked though)
1049 function set_locked($lockedstate, $cascade=false, $refresh=true) {
1050 $this->load_grade_item();
1052 $result = $this->grade_item->set_locked($lockedstate, $cascade, true);
1054 if ($cascade) {
1055 //process all children - items and categories
1056 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
1057 foreach($children as $child) {
1058 $child->set_locked($lockedstate, true, false);
1059 if (empty($lockedstate) and $refresh) {
1060 //refresh when unlocking
1061 $child->refresh_grades();
1065 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
1066 foreach($children as $child) {
1067 $child->set_locked($lockedstate, true, true);
1072 return $result;
1076 * Returns the hidden state/date of the associated grade_item. This method is also available in
1077 * grade_item.
1078 * @return boolean
1080 function is_hidden() {
1081 $this->load_grade_item();
1082 return $this->grade_item->is_hidden();
1086 * Sets the grade_item's hidden variable and updates the grade_item.
1087 * Method named after grade_item::set_hidden().
1088 * @param int $hidden 0, 1 or a timestamp int(10) after which date the item will be hidden.
1089 * @param boolean $cascade apply to child objects too
1090 * @return void
1092 function set_hidden($hidden, $cascade=false) {
1093 $this->load_grade_item();
1094 $this->grade_item->set_hidden($hidden);
1095 if ($cascade) {
1096 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
1097 foreach($children as $child) {
1098 $child->set_hidden($hidden, $cascade);
1101 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
1102 foreach($children as $child) {
1103 $child->set_hidden($hidden, $cascade);