MDL-9629 Added student names at left of grades table.
[moodle.git] / lib / grade / grade_category.php
CommitLineData
8a31e65c 1<?php // $Id$
2
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///////////////////////////////////////////////////////////////////////////
25
26require_once('grade_object.php');
27
3058964f 28class grade_category extends grade_object {
8a31e65c 29 /**
7c8a963f 30 * The DB table.
8a31e65c 31 * @var string $table
32 */
33 var $table = 'grade_categories';
4a490db0 34
8a31e65c 35 /**
36 * Array of class variables that are not part of the DB table fields
37 * @var array $nonfields
38 */
7c8a963f 39 var $nonfields = array('table', 'nonfields', 'children', 'all_children');
4a490db0 40
8a31e65c 41 /**
42 * The course this category belongs to.
43 * @var int $courseid
44 */
45 var $courseid;
4a490db0 46
8a31e65c 47 /**
48 * The category this category belongs to (optional).
4a490db0 49 * @var int $parent
8a31e65c 50 */
e5c674f1 51 var $parent;
4a490db0 52
8c846243 53 /**
54 * The grade_category object referenced by $this->parent (PK).
55 * @var object $parent_category
56 */
57 var $parent_category;
27f95e9b 58
88e794d6 59 /**
60 * A grade_category object this category used to belong to before getting updated. Will be deleted shortly.
61 * @var object $old_parent
62 */
63 var $old_parent;
64
e5c674f1 65 /**
66 * The number of parents this category has.
67 * @var int $depth
68 */
69 var $depth = 0;
70
71 /**
72 * Shows the hierarchical path for this category as /1/2/3 (like course_categories), the last number being
73 * this category's autoincrement ID number.
74 * @var string $path
75 */
76 var $path;
77
8a31e65c 78 /**
79 * The name of this category.
80 * @var string $fullname
81 */
82 var $fullname;
4a490db0 83
8a31e65c 84 /**
85 * A constant pointing to one of the predefined aggregation strategies (none, mean, median, sum etc) .
4a490db0 86 * @var int $aggregation
8a31e65c 87 */
88 var $aggregation;
4a490db0 89
8a31e65c 90 /**
91 * Keep only the X highest items.
92 * @var int $keephigh
93 */
94 var $keephigh;
4a490db0 95
8a31e65c 96 /**
97 * Drop the X lowest items.
98 * @var int $droplow
99 */
100 var $droplow;
4a490db0 101
8a31e65c 102 /**
103 * Array of grade_items or grade_categories nested exactly 1 level below this category
104 * @var array $children
105 */
106 var $children;
8a31e65c 107
7c8a963f 108 /**
4a490db0 109 * A hierarchical array of all children below this category. This is stored separately from
7c8a963f 110 * $children because it is more memory-intensive and may not be used as often.
111 * @var array $all_children
112 */
113 var $all_children;
114
f151b073 115 /**
116 * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
117 * for this category.
118 * @var object $grade_item
119 */
120 var $grade_item;
121
e5c674f1 122 /**
123 * Constructor. Extends the basic functionality defined in grade_object.
124 * @param array $params Can also be a standard object.
f151b073 125 * @param boolean $fetch Whether or not to fetch the corresponding row from the DB.
126 * @param object $grade_item The associated grade_item object can be passed during construction.
e5c674f1 127 */
b8ff92b6 128 function grade_category($params=NULL, $fetch=true) {
e5c674f1 129 $this->grade_object($params, $fetch);
2df71235 130 $this->path = grade_category::build_path($this);
e5c674f1 131 }
132
4a490db0 133
e5c674f1 134 /**
135 * Builds this category's path string based on its parents (if any) and its own id number.
136 * This is typically done just before inserting this object in the DB for the first time,
ce385eb4 137 * or when a new parent is added or changed. It is a recursive function: once the calling
138 * object no longer has a parent, the path is complete.
139 *
140 * @static
141 * @param object $grade_category
142 * @return int The depth of this category (2 means there is one parent)
e5c674f1 143 */
ce385eb4 144 function build_path($grade_category) {
145 if (empty($grade_category->parent)) {
146 return "/$grade_category->id";
147 } else {
148 $parent = get_record('grade_categories', 'id', $grade_category->parent);
149 return grade_category::build_path($parent) . "/$grade_category->id";
150 }
e5c674f1 151 }
152
8a31e65c 153
8a31e65c 154 /**
155 * Finds and returns a grade_category object based on 1-3 field values.
156 *
8a31e65c 157 * @param string $field1
158 * @param string $value1
159 * @param string $field2
160 * @param string $value2
161 * @param string $field3
162 * @param string $value3
163 * @param string $fields
164 * @return object grade_category object or false if none found.
165 */
4a490db0 166 function fetch($field1, $value1, $field2='', $value2='', $field3='', $value3='', $fields="*") {
8a31e65c 167 if ($grade_category = get_record('grade_categories', $field1, $value1, $field2, $value2, $field3, $value3, $fields)) {
7c8a963f 168 if (isset($this) && get_class($this) == 'grade_category') {
8a31e65c 169 foreach ($grade_category as $param => $value) {
170 $this->$param = $value;
171 }
172 return $this;
7c8a963f 173 } else {
174 $grade_category = new grade_category($grade_category);
175 return $grade_category;
8a31e65c 176 }
177 } else {
178 return false;
179 }
ce385eb4 180 }
181
8f4a626d 182 /**
183 * In addition to update() as defined in grade_object, call flag_for_update of parent categories, if applicable.
184 */
4a490db0 185 function update() {
8f4a626d 186 $qualifies = $this->qualifies_for_update();
187
0fc7f624 188 // Update the grade_item's sortorder if needed
189 if (!empty($this->sortorder)) {
190 $this->load_grade_item();
191 if (!empty($this->grade_item)) {
192 $this->grade_item->sortorder = $this->sortorder;
193 $this->grade_item->update();
194 }
195 unset($this->sortorder);
4a490db0 196 }
0fc7f624 197
8f4a626d 198 $result = parent::update();
4a490db0 199
cb64c6b2 200 // Use $this->path to update all parent categories
8f4a626d 201 if ($result && $qualifies) {
cb64c6b2 202 $this->flag_for_update();
4a490db0 203 }
8f4a626d 204 return $result;
205 }
4a490db0 206
8f4a626d 207 /**
208 * If parent::delete() is successful, send flag_for_update message to parent category.
209 * @return boolean Success or failure.
210 */
211 function delete() {
212 $result = parent::delete();
4a490db0 213
8f4a626d 214 if ($result) {
215 $this->load_parent_category();
216 if (!empty($this->parent_category)) {
217 $result = $result && $this->parent_category->flag_for_update();
218 }
4a490db0 219
220 // Update children's categoryid/parent field
221 global $db;
222 $set_field_result = set_field('grade_items', 'categoryid', null, 'categoryid', $this->id);
223 $set_field_result = set_field('grade_categories', 'parent', null, 'parent', $this->id);
8f4a626d 224 }
225
226 return $result;
227 }
4a490db0 228
ce385eb4 229 /**
230 * In addition to the normal insert() defined in grade_object, this method sets the depth
231 * and path for this object, and update the record accordingly. The reason why this must
232 * be done here instead of in the constructor, is that they both need to know the record's
4a490db0 233 * id number, which only gets created at insertion time.
f151b073 234 * This method also creates an associated grade_item if this wasn't done during construction.
ce385eb4 235 */
236 function insert() {
b8ff92b6 237 if (!parent::insert()) {
238 debugging("Could not insert this category: " . print_r($this, true));
239 return false;
240 }
4a490db0 241
77d2540e 242 $this->path = grade_category::build_path($this);
ce385eb4 243
244 // Build path and depth variables
245 if (!empty($this->parent)) {
ce385eb4 246 $this->depth = $this->get_depth_from_path();
247 } else {
248 $this->depth = 1;
ce385eb4 249 }
4a490db0 250
ce385eb4 251 $this->update();
4a490db0 252
b8ff92b6 253 // initialize grade_item for this category
254 $this->grade_item = $this->get_grade_item();
4a490db0 255
b8ff92b6 256 // Notify parent category of need to update.
257 $this->load_parent_category();
258 if (!empty($this->parent_category)) {
259 if (!$this->parent_category->flag_for_update()) {
260 debugging("Could not notify parent category of the need to update its final grades.");
6527197b 261 return false;
262 }
f151b073 263 }
4a490db0 264
b8ff92b6 265 return true;
ce385eb4 266 }
4a490db0 267
8f4a626d 268 /**
269 * Compares the values held by this object with those of the matching record in DB, and returns
270 * whether or not these differences are sufficient to justify an update of all parent objects.
271 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
272 * @return boolean
273 */
274 function qualifies_for_update() {
275 if (empty($this->id)) {
276 return false;
277 }
278
279 $db_item = new grade_category(array('id' => $this->id));
4a490db0 280
8f4a626d 281 $aggregationdiff = $db_item->aggregation != $this->aggregation;
282 $keephighdiff = $db_item->keephigh != $this->keephigh;
283 $droplowdiff = $db_item->droplow != $this->droplow;
284
285 if ($aggregationdiff || $keephighdiff || $droplowdiff) {
286 return true;
287 } else {
288 return false;
289 }
290 }
8c846243 291
292 /**
293 * Sets this category's and its parent's grade_item.needsupdate to true.
294 * This is triggered whenever any change in any lower level may cause grade_finals
295 * for this category to require an update. The flag needs to be propagated up all
296 * levels until it reaches the top category. This is then used to determine whether or not
cb64c6b2 297 * to regenerate the raw and final grades for each category grade_item. This is accomplished
298 * thanks to the path variable, so we don't need to use recursion.
8c846243 299 * @return boolean Success or failure
300 */
301 function flag_for_update() {
b8ff92b6 302 if (empty($this->id)) {
303 debugging("Needsupdate requested before insering grade category.");
304 return true;
305 }
306
8c846243 307 $result = true;
4a490db0 308
8c846243 309 $this->load_grade_item();
8f4a626d 310
cb64c6b2 311 $paths = explode('/', $this->path);
4a490db0 312
77d2540e 313 // Remove the first index, which is always empty
314 unset($paths[0]);
4a490db0 315
77d2540e 316 if (!empty($paths)) {
317 $wheresql = '';
4a490db0 318
77d2540e 319 foreach ($paths as $categoryid) {
320 $wheresql .= "iteminstance = $categoryid OR ";
321 }
322 $wheresql = substr($wheresql, 0, strrpos($wheresql, 'OR'));
a3d55942 323 $grade_items = set_field_select('grade_items', 'needsupdate', '1', $wheresql . ' AND courseid = ' . $this->courseid);
77d2540e 324 $this->grade_item->update_from_db();
8c846243 325 }
8c846243 326 return $result;
327 }
328
0aa32279 329 /**
4a490db0 330 * Generates and saves raw_grades, based on this category's immediate children, then uses the
0aa32279 331 * associated grade_item to generate matching final grades. These immediate children must first have their own
332 * raw and final grades, which means that ultimately we must get grade_items as children. The category's aggregation
333 * method is used to generate these raw grades, which can then be used by the category's associated grade_item
334 * to apply calculations to and generate final grades.
b8ff92b6 335 *
336 * This function must be use ONLY from grade_item::update_final_grade(),
337 * because the calculation must be done in correct order!!
338
4a490db0 339 * Steps to follow:
b8ff92b6 340 * 1. Get final grades from immediate children (if the children are categories, get the final grades from their grade_item)
2df71235 341 * 3. Aggregate these grades
342 * 4. Save them under $this->grade_item->grade_grades_raw
0aa32279 343 */
344 function generate_grades() {
b8ff92b6 345 global $CFG;
4a490db0 346
b8ff92b6 347 $grade_item = $this->get_grade_item();
348 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
349 $grade_item->load_scale();
0aa32279 350 }
2df71235 351
b8ff92b6 352 $dependson = $grade_item->dependson();
353 $items = array();
4a490db0 354
b8ff92b6 355 foreach($dependson as $dep) {
356 $items[$dep] = grade_item::fetch('id', $dep);
357 }
4a490db0 358
b8ff92b6 359 // where to look for final grades
360 $gis = implode(',', array_merge($dependson, array($grade_item->id)));
361
362 $sql = "SELECT f.*
363 FROM {$CFG->prefix}grade_grades_final f, {$CFG->prefix}grade_items gi
364 WHERE gi.id = f.itemid AND gi.courseid={$grade_item->courseid} AND gi.id IN ($gis)
365 ORDER BY f.userid";
366
367 if ($rs = get_recordset_sql($sql)) {
368 if ($rs->RecordCount() > 0) {
369 $prevuser = 0;
370 $grades = array();
371 while ($subfinal = rs_fetch_next_record($rs)) {
372 if ($subfinal->userid != $prevuser) {
373 $this->aggregate_grades($prevuser, $items, $grades, $grade_item, $dependson);
374 $prevuser = $subfinal->userid;
375 $grades = array();
376 }
377 $grades[$subfinal->itemid] = $subfinal->gradevalue;
2df71235 378 }
b8ff92b6 379 $this->aggregate_grades($prevuser, $items, $grades, $grade_item, $dependson);
380 }
381 }
382
383 //TODO: set grade to null for raw grades that do not have corresponding final grade
384 // using left join
385
386 return true;
387 }
388
389 /**
390 * internal function for vategory grades aggregation
391 */
392 function aggregate_grades($userid, $items, $grades, &$grade_item, $dependson) {
393 if (empty($userid)) {
394 return;
395 }
4a490db0 396
b8ff92b6 397 // remove the final item we used to get all existing final grades of this category
398 unset($grades[$grade_item->id]);
399
400 if (empty($grades) or empty($items) or ($grade_item->gradetype != GRADE_TYPE_VALUE and $grade_item->gradetype != GRADE_TYPE_SCALE)) {
401 // no grading
402 if ($raw = grade_grades_raw::fetch('itemid', $grade_item->id, 'userid', $userid)) {
403 $raw->gradevalue = null;
404 $raw->update();
2c72af1f 405 }
b8ff92b6 406 return;
407 }
4a490db0 408
b8ff92b6 409 // normalize the grades first - all will have value 0...1
410 foreach ($grades as $k=>$v) {
411 if (is_null($v)) {
412 // null means no grade
413 unset($grades[$k]);
414 continue;
0aa32279 415 }
b8ff92b6 416 $grades[$k] = standardise_score($v, $items[$k]->grademin, $items[$k]->grademax, 0, 1);
0aa32279 417 }
dda0c7e6 418
b8ff92b6 419 //sort and limit
420 $this->apply_limit_rules($grades);
421 sort($grades, SORT_NUMERIC);
4a490db0 422
b8ff92b6 423 if (count($grades) == 0) {
424 // no grading yet
425 if ($raw = grade_grades_raw::fetch('itemid', $grade_item->id, 'userid', $userid)) {
426 $raw->gradevalue = null;
427 $raw->update();
428 }
429 return;
430 }
2df71235 431
b8ff92b6 432 switch ($this->aggregation) {
433 case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
434 $num = count($grades);
435 $halfpoint = intval($num / 2);
436
437 if($num % 2 == 0) {
438 $gradevalue = ($grades[ceil($halfpoint)] + $grades[floor($halfpoint)]) / 2;
439 } else {
440 $gradevalue = $grades[$halfpoint];
441 }
442 break;
443 case GRADE_AGGREGATE_MIN:
444 $gradevalue = reset($grades);
445 break;
446
447 case GRADE_AGGREGATE_MAX:
448 $gradevalue = array_pop($grades);
449 break;
450
95affb8a 451 case GRADE_AGGREGATE_MEAN_ALL: // Arithmetic average of all grade items including even NULLs; NULL grade caunted as minimum
452 $num = count($dependson); // you can calculate sum from this one if you multiply it with count($this->dependson() ;-)
b8ff92b6 453 $sum = array_sum($grades);
454 $gradevalue = $sum / $num;
455 break;
456
95affb8a 457 case GRADE_AGGREGATE_MODE: // the most common value, the highest one if multimode
458 $freq = array_count_values($grades);
459 arsort($freq); // sort by frequency keeping keys
460 $top = reset($freq); // highest frequency count
461 $modes = array_keys($freq, $top); // search for all modes (have the same highest count)
462 rsort($modes, SORT_NUMERIC); // get highes mode
463 $gradevalue = reset($modes);
464
465 case GRADE_AGGREGATE_MEAN_GRADED: // Arithmetic average of all final grades, unfinished are not calculated
b8ff92b6 466 $num = count($grades);
467 $sum = array_sum($grades);
468 $gradevalue = $sum / $num;
469 default:
470 break;
471 }
472
473 $raw = new grade_grades_raw(array('itemid'=>$grade_item->id, 'userid'=>$userid));
474 $raw->gradevalue = $gradevalue;
475 $raw->gradetype = $grade_item->gradetype;
476 $raw->scaleid = $grade_item->scaleid;
477 $raw->grademin = 0;
478 $raw->grademax = 1;
479
480 // recalculate the gradevalue bvack to requested range
481 $raw->gradevalue = $grade_item->adjust_grade($raw);
482
483 $raw->grademin = $grade_item->grademin;
484 $raw->grademax = $grade_item->grademax;
485
486 if ($raw->id) {
487 $raw->update();
488 } else {
489 $raw->insert();
490 }
2df71235 491
0aa32279 492 }
493
adc2f286 494 /**
495 * Given an array of grade values (numerical indices), applies droplow or keephigh
496 * rules to limit the final array.
497 * @param array $grades
498 * @return array Limited grades.
499 */
b8ff92b6 500 function apply_limit_rules(&$grades) {
adc2f286 501 rsort($grades, SORT_NUMERIC);
502 if (!empty($this->droplow)) {
503 for ($i = 0; $i < $this->droplow; $i++) {
504 array_pop($grades);
505 }
4a490db0 506 } elseif (!empty($this->keephigh)) {
adc2f286 507 while (count($grades) > $this->keephigh) {
4a490db0 508 array_pop($grades);
adc2f286 509 }
510 }
0aa32279 511 }
512
0fc7f624 513 /**
514 * Given an array of stdClass children of a certain $object_type, returns a flat or nested
515 * array of these children, ready for appending to a tree built by get_children.
516 * @static
517 * @param array $children
518 * @param string $arraytype
519 * @param string $object_type
520 * @return array
521 */
522 function children_to_array($children, $arraytype='nested', $object_type='grade_item') {
523 $children_array = array();
524
525 foreach ($children as $id => $child) {
526 $child = new $object_type($child, false);
527 if ($arraytype == 'nested') {
528 $children_array[$child->get_sortorder()] = array('object' => $child);
529 } else {
530 $children_array[$child->get_sortorder()] = $child;
531 }
4a490db0 532 }
0fc7f624 533
534 return $children_array;
535 }
536
537 /**
538 * Returns true if this category has any child grade_category or grade_item.
539 * @return int number of direct children, or false if none found.
540 */
541 function has_children() {
542 return count_records('grade_categories', 'parent', $this->id) + count_records('grade_items', 'categoryid', $this->id);
543 }
544
545 /**
4a490db0 546 * Checks whether an existing child exists for this category. If the new child is of a
210611f6 547 * different type, the method will return false (not allowed). Otherwise it will return true.
0fc7f624 548 * @param object $child This must be a complete object, not a stdClass
549 * @return boolean Success or failure
550 */
551 function can_add_child($child) {
552 if ($this->has_children()) {
553 if (get_class($child) != $this->get_childrentype()) {
554 return false;
555 } else {
556 return true;
557 }
558 } else {
559 return true;
560 }
561 }
562
1c307f21 563 /**
564 * Disassociates this category from its category parent(s). The object is then updated in DB.
565 * @return boolean Success or Failure
566 */
567 function divorce_parent() {
4a490db0 568 $this->old_parent = $this->get_parent_category();
1c307f21 569 $this->parent = null;
570 $this->parent_category = null;
571 $this->depth = 1;
572 $this->path = '/' . $this->id;
4a490db0 573 return $this->update();
1c307f21 574 }
575
ce385eb4 576 /**
577 * Looks at a path string (e.g. /2/45/56) and returns the depth level represented by this path (in this example, 3).
578 * If no string is given, it looks at the obect's path and assigns the resulting depth to its $depth variable.
579 * @param string $path
580 * @return int Depth level
581 */
582 function get_depth_from_path($path=NULL) {
583 if (empty($path)) {
584 $path = $this->path;
585 }
586 preg_match_all('/\/([0-9]+)+?/', $path, $matches);
587 $depth = count($matches[0]);
588
589 return $depth;
590 }
7c8a963f 591
592 /**
4a490db0 593 * Fetches and returns all the children categories and/or grade_items belonging to this category.
594 * By default only returns the immediate children (depth=1), but deeper levels can be requested,
a39cac25 595 * as well as all levels (0). The elements are indexed by sort order.
7c8a963f 596 * @param int $depth 1 for immediate children, 0 for all children, and 2+ for specific levels deeper than 1.
597 * @param string $arraytype Either 'nested' or 'flat'. A nested array represents the true hierarchy, but is more difficult to work with.
598 * @return array Array of child objects (grade_category and grade_item).
599 */
600 function get_children($depth=1, $arraytype='nested') {
27f95e9b 601 $children_array = array();
4a490db0 602
27f95e9b 603 // Set up $depth for recursion
604 $newdepth = $depth;
605 if ($depth > 1) {
606 $newdepth--;
607 }
4a490db0 608
27f95e9b 609 $childrentype = $this->get_childrentype();
4a490db0 610
27f95e9b 611 if ($childrentype == 'grade_item') {
f151b073 612 $children = get_records('grade_items', 'categoryid', $this->id);
27f95e9b 613 // No need to proceed with recursion
614 $children_array = $this->children_to_array($children, $arraytype, 'grade_item');
615 $this->children = $this->children_to_array($children, 'flat', 'grade_item');
616 } elseif ($childrentype == 'grade_category') {
617 $children = get_records('grade_categories', 'parent', $this->id, 'id');
4a490db0 618
27f95e9b 619 if ($depth == 1) {
620 $children_array = $this->children_to_array($children, $arraytype, 'grade_category');
621 $this->children = $this->children_to_array($children, 'flat', 'grade_category');
7c8a963f 622 } else {
27f95e9b 623 foreach ($children as $id => $child) {
624 $cat = new grade_category($child, false);
625
626 if ($cat->has_children()) {
627 if ($arraytype == 'nested') {
a39cac25 628 $children_array[$cat->get_sortorder()] = array('object' => $cat, 'children' => $cat->get_children($newdepth, $arraytype));
27f95e9b 629 } else {
a39cac25 630 $children_array[$cat->get_sortorder()] = $cat;
27f95e9b 631 $cat_children = $cat->get_children($newdepth, $arraytype);
632 foreach ($cat_children as $id => $cat_child) {
a39cac25 633 $children_array[$cat_child->get_sortorder()] = new grade_category($cat_child, false);
27f95e9b 634 }
635 }
636 } else {
637 if ($arraytype == 'nested') {
a39cac25 638 $children_array[$cat->get_sortorder()] = array('object' => $cat);
27f95e9b 639 } else {
a39cac25 640 $children_array[$cat->get_sortorder()] = $cat;
27f95e9b 641 }
642 }
7c8a963f 643 }
27f95e9b 644 }
645 } else {
646 return null;
647 }
648
649 return $children_array;
650 }
4a490db0 651
27f95e9b 652 /**
4a490db0 653 * Check the type of the first child of this category, to see whether it is a
27f95e9b 654 * grade_category or a grade_item, and returns that type as a string (get_class).
655 * @return string
656 */
657 function get_childrentype() {
27f95e9b 658 if (empty($this->children)) {
659 $count_item_children = count_records('grade_items', 'categoryid', $this->id);
660 $count_cat_children = count_records('grade_categories', 'parent', $this->id);
4a490db0 661
27f95e9b 662 if ($count_item_children > 0) {
663 return 'grade_item';
664 } elseif ($count_cat_children > 0) {
665 return 'grade_category';
666 } else {
667 return null;
7c8a963f 668 }
7c8a963f 669 }
a15428a2 670 reset($this->children);
671 return get_class(current($this->children));
7c8a963f 672 }
f151b073 673
674 /**
ab53054f 675 * Uses get_grade_item to load or create a grade_item, then saves it as $this->grade_item.
f151b073 676 * @return object Grade_item
677 */
678 function load_grade_item() {
ab53054f 679 $this->grade_item = $this->get_grade_item();
680 return $this->grade_item;
681 }
4a490db0 682
ab53054f 683 /**
684 * Retrieves from DB and instantiates the associated grade_item object.
685 * If no grade_item exists yet, create one.
686 * @return object Grade_item
687 */
688 function get_grade_item() {
c91ed4be 689 if (empty($this->id)) {
690 debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
691 return false;
692 }
693
b8ff92b6 694 $grade_item = new grade_item(array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id), false);
695 if (!$grade_items = $grade_item->fetch_all_using_this()) {
696 // create a new one
697 $grade_item->gradetype = GRADE_TYPE_VALUE;
698 $grade_item->insert();
4a490db0 699
b8ff92b6 700 } else if (count($grade_items) == 1){
701 // found existing one
702 $grade_item = reset($grade_items);
4a490db0 703
b8ff92b6 704 } else {
705 debugging("Found more than one grade_item attached to category id:".$this->id);
706 return false;
2c72af1f 707 }
708
ab53054f 709 return $grade_item;
f151b073 710 }
8c846243 711
712 /**
713 * Uses $this->parent to instantiate $this->parent_category based on the
714 * referenced record in the DB.
715 * @return object Parent_category
716 */
717 function load_parent_category() {
718 if (empty($this->parent_category) && !empty($this->parent)) {
ab53054f 719 $this->parent_category = $this->get_parent_category();
8c846243 720 }
721 return $this->parent_category;
4a490db0 722 }
723
ab53054f 724 /**
725 * Uses $this->parent to instantiate and return a grade_category object.
726 * @return object Parent_category
727 */
728 function get_parent_category() {
729 if (!empty($this->parent)) {
730 $parent_category = new grade_category(array('id' => $this->parent));
4a490db0 731 return $parent_category;
ab53054f 732 } else {
733 return null;
734 }
735 }
736
03f01edd 737 /**
c91ed4be 738 * Sets this category as the parent for the given children. If the category's courseid isn't set, it uses that of the children items.
03f01edd 739 * A number of constraints are necessary:
740 * - The children must all be of the same type and at the same level
03f01edd 741 * - The children cannot already be top categories
c91ed4be 742 * - The children all belong to the same course
03f01edd 743 * @param array $children An array of fully instantiated grade_category OR grade_item objects
ffa6e8d3 744 *
03f01edd 745 * @return boolean Success or Failure
ffa6e8d3 746 * @TODO big problem of performance
03f01edd 747 */
748 function set_as_parent($children) {
749 global $CFG;
750
210611f6 751 if (empty($children) || !is_array($children)) {
752 debugging("Passed an empty or non-array variable to grade_category::set_as_parent()");
753 return false;
754 }
755
03f01edd 756 // Check type and sortorder of first child
757 $first_child = current($children);
758 $first_child_type = get_class($first_child);
9f9afbdb 759
760 // If this->courseid is not set, set it to the first child's courseid
761 if (empty($this->courseid)) {
762 $this->courseid = $first_child->courseid;
763 }
764
765 $grade_tree = new grade_tree();
03f01edd 766
767 foreach ($children as $child) {
768 if (get_class($child) != $first_child_type) {
769 debugging("Violated constraint: Attempted to set a category as a parent over children of 2 different types.");
770 return false;
771 }
4a490db0 772
526e1a8a 773 if ($grade_tree->get_element_type($child) == 'topcat') {
03f01edd 774 debugging("Violated constraint: Attempted to set a category over children which are already top categories.");
775 return false;
776 }
4a490db0 777
9f9afbdb 778 if ($first_child_type == 'grade_category' or $first_child_type == 'grade_item') {
03f01edd 779 if (!empty($child->parent)) {
780 debugging("Violated constraint: Attempted to set a category over children that already have a top category.");
4a490db0 781 return false;
03f01edd 782 }
783 } else {
784 debugging("Attempted to set a category over children that are neither grade_items nor grade_categories.");
785 return false;
4a490db0 786 }
c91ed4be 787
9f9afbdb 788 if ($child->courseid != $this->courseid) {
c91ed4be 789 debugging("Attempted to set a category over children which do not belong to the same course.");
790 return false;
791 }
4a490db0 792 }
03f01edd 793
794 // We passed all the checks, time to set the category as a parent.
795 foreach ($children as $child) {
1c307f21 796 $child->divorce_parent();
797 $child->set_parent_id($this->id);
798 if (!$child->update()) {
799 debugging("Could not set this category as a parent for one of its children, DB operation failed.");
800 return false;
4a490db0 801 }
03f01edd 802 }
803
804 // TODO Assign correct sortorders to the newly assigned children and parent. Simply add 1 to all of them!
805 $this->load_grade_item();
806 $this->grade_item->sortorder = $first_child->get_sortorder();
4a490db0 807
03f01edd 808 if (!$this->update()) {
809 debugging("Could not update this category's sortorder in DB.");
810 return false;
811 }
4a490db0 812
750b0550 813 $query = "UPDATE {$CFG->prefix}grade_items SET sortorder = sortorder + 1 WHERE sortorder >= {$this->grade_item->sortorder}";
ffa6e8d3 814 $query .= " AND courseid = $this->courseid";
815
03f01edd 816 if (!execute_sql($query)) {
817 debugging("Could not update the sortorder of grade_items listed after this category.");
750b0550 818 return false;
03f01edd 819 } else {
820 return true;
821 }
822 }
2186f72c 823
824 /**
4a490db0 825 * Returns the most descriptive field for this object. This is a standard method used
2186f72c 826 * when we do not know the exact type of an object.
827 * @return string name
828 */
829 function get_name() {
830 return $this->fullname;
831 }
c91ed4be 832
833 /**
834 * Returns this category's grade_item's id. This is specified for cases where we do not
835 * know an object's type, and want to get either an item's id or a category's item's id.
836 *
837 * @return int
838 */
839 function get_item_id() {
840 $this->load_grade_item();
841 return $this->grade_item->id;
842 }
0fc7f624 843
844 /**
845 * Returns this category's parent id. A generic method shared by objects that have a parent id of some kind.
846 * @return id $parentid
847 */
848 function get_parent_id() {
849 return $this->parent;
850 }
851
852 /**
853 * Sets this category's parent id. A generic method shared by objects that have a parent id of some kind.
854 * @param id $parentid
855 */
856 function set_parent_id($parentid) {
857 $this->parent = $parentid;
88e794d6 858 $this->path = grade_category::build_path($this);
859 $this->depth = $this->get_depth_from_path();
0fc7f624 860 }
4a490db0 861
0fc7f624 862 /**
4a490db0 863 * Returns the sortorder of the associated grade_item. This method is also available in
5fad5061 864 * grade_item, for cases where the object type is not known.
0fc7f624 865 * @return int Sort order
866 */
867 function get_sortorder() {
868 if (empty($this->sortorder)) {
869 $this->load_grade_item();
870 if (!empty($this->grade_item)) {
871 return $this->grade_item->sortorder;
872 }
873 } else {
874 return $this->sortorder;
875 }
876 }
877
878 /**
4a490db0 879 * Sets a temporary sortorder variable for this category. It is used in the update() method to update the grade_item.
880 * This method is also available in grade_item, for cases where the object type is not know.
0fc7f624 881 * @param int $sortorder
882 * @return void
883 */
884 function set_sortorder($sortorder) {
885 $this->sortorder = $sortorder;
886 }
4a490db0 887
5fad5061 888 /**
4a490db0 889 * Returns the locked state/date of the associated grade_item. This method is also available in
890 * grade_item, for cases where the object type is not known.
5fad5061 891 * @return int 0, 1 or timestamp int(10)
892 */
893 function get_locked() {
894 $this->load_grade_item();
895 if (!empty($this->grade_item)) {
896 return $this->grade_item->locked;
897 } else {
898 return false;
899 }
900 }
901
902 /**
903 * Sets the grade_item's locked variable and updates the grade_item.
904 * Method named after grade_item::set_locked().
905 * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
906 * @return void
907 */
908 function set_locked($locked) {
909 $this->load_grade_item();
910 if (!empty($this->grade_item)) {
911 $this->grade_item->locked = $locked;
912 return $this->grade_item->update();
913 } else {
914 return false;
915 }
916 }
4a490db0 917
5fad5061 918 /**
4a490db0 919 * Returns the hidden state/date of the associated grade_item. This method is also available in
920 * grade_item, for cases where the object type is not known.
5fad5061 921 * @return int 0, 1 or timestamp int(10)
922 */
923 function get_hidden() {
924 $this->load_grade_item();
925 if (!empty($this->grade_item)) {
926 return $this->grade_item->hidden;
927 } else {
928 return false;
929 }
930 }
931
932 /**
4a490db0 933 * Sets the grade_item's hidden variable and updates the grade_item.
5fad5061 934 * Method named after grade_item::set_hidden().
935 * @param int $hidden 0, 1 or a timestamp int(10) after which date the item will be hidden.
936 * @return void
937 */
938 function set_hidden($hidden) {
939 $this->load_grade_item();
940 if (!empty($this->grade_item)) {
941 $this->grade_item->hidden = $hidden;
942 return $this->grade_item->update();
943 } else {
944 return false;
945 }
946 }
947
4a490db0 948 /**
88e794d6 949 * If the old parent is set (after an update), this checks and returns whether it has any children. Important for
950 * deleting childless categories.
951 * @return boolean
952 */
953 function is_old_parent_childless() {
954 if (!empty($this->old_parent)) {
955 return !$this->old_parent->has_children();
956 } else {
957 return false;
958 }
4a490db0 959 }
960}
8a31e65c 961?>