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