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