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