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