DIsabling Chameleon for the general case
[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 }
e5c674f1 137 }
138
139
140 /**
141 * Builds this category's path string based on its parents (if any) and its own id number.
142 * This is typically done just before inserting this object in the DB for the first time,
ce385eb4 143 * or when a new parent is added or changed. It is a recursive function: once the calling
144 * object no longer has a parent, the path is complete.
145 *
146 * @static
147 * @param object $grade_category
148 * @return int The depth of this category (2 means there is one parent)
e5c674f1 149 */
ce385eb4 150 function build_path($grade_category) {
151 if (empty($grade_category->parent)) {
152 return "/$grade_category->id";
153 } else {
154 $parent = get_record('grade_categories', 'id', $grade_category->parent);
155 return grade_category::build_path($parent) . "/$grade_category->id";
156 }
e5c674f1 157 }
158
8a31e65c 159
8a31e65c 160 /**
161 * Finds and returns a grade_category object based on 1-3 field values.
162 *
8a31e65c 163 * @param string $field1
164 * @param string $value1
165 * @param string $field2
166 * @param string $value2
167 * @param string $field3
168 * @param string $value3
169 * @param string $fields
170 * @return object grade_category object or false if none found.
171 */
27f95e9b 172 function fetch($field1, $value1, $field2='', $value2='', $field3='', $value3='', $fields="*") {
8a31e65c 173 if ($grade_category = get_record('grade_categories', $field1, $value1, $field2, $value2, $field3, $value3, $fields)) {
7c8a963f 174 if (isset($this) && get_class($this) == 'grade_category') {
8a31e65c 175 foreach ($grade_category as $param => $value) {
176 $this->$param = $value;
177 }
178 return $this;
7c8a963f 179 } else {
180 $grade_category = new grade_category($grade_category);
181 return $grade_category;
8a31e65c 182 }
183 } else {
184 return false;
185 }
ce385eb4 186 }
187
188 /**
189 * In addition to the normal insert() defined in grade_object, this method sets the depth
190 * and path for this object, and update the record accordingly. The reason why this must
191 * be done here instead of in the constructor, is that they both need to know the record's
27f95e9b 192 * id number, which only gets created at insertion time.
f151b073 193 * This method also creates an associated grade_item if this wasn't done during construction.
ce385eb4 194 */
195 function insert() {
196 $result = parent::insert();
197
198 // Build path and depth variables
199 if (!empty($this->parent)) {
200 $this->path = grade_category::build_path($this);
201 $this->depth = $this->get_depth_from_path();
202 } else {
203 $this->depth = 1;
204 $this->path = "/$this->id";
205 }
206
207 $this->update();
f151b073 208
209 if (empty($this->grade_item)) {
210 $grade_item = new grade_item();
211 $grade_item->iteminstance = $this->id;
212 $grade_item->itemtype = 'category';
6527197b 213
214 if (!$grade_item->insert()) {
215 return false;
216 }
217
f151b073 218 $this->grade_item = $grade_item;
219 }
220
ce385eb4 221 return $result;
222 }
8c846243 223
224 /**
225 * Sets this category's and its parent's grade_item.needsupdate to true.
226 * This is triggered whenever any change in any lower level may cause grade_finals
227 * for this category to require an update. The flag needs to be propagated up all
228 * levels until it reaches the top category. This is then used to determine whether or not
229 * to regenerate the raw and final grades for each category grade_item.
230 * @return boolean Success or failure
231 */
232 function flag_for_update() {
233 $result = true;
234
235 $this->load_grade_item();
236 $this->grade_item->needsupdate = true;
237 $this->load_parent_category();
238 if (!empty($this->parent_category)) {
239 $result = $result && $this->parent_category->flag_for_update();
240 }
241
242 return $result;
243 }
244
0aa32279 245 /**
246 * Generates and saves raw_grades, based on this category's immediate children, then uses the
247 * associated grade_item to generate matching final grades. These immediate children must first have their own
248 * raw and final grades, which means that ultimately we must get grade_items as children. The category's aggregation
249 * method is used to generate these raw grades, which can then be used by the category's associated grade_item
250 * to apply calculations to and generate final grades.
251 */
252 function generate_grades() {
2c72af1f 253 // Check that the children have final grades. If not, call their generate_grades method (recursion)
0aa32279 254 if (empty($this->children)) {
255 $this->children = $this->get_children(1, 'flat');
256 }
257
258 $category_raw_grades = array();
259 $aggregated_grades = array();
260
261 foreach ($this->children as $child) {
262 if (get_class($child) == 'grade_item') {
2c72af1f 263 $category_raw_grades[$child->id] = $child->load_raw();
264 } elseif (get_class($child) == 'grade_category') {
265 $child->load_grade_item();
266 $raw_grades = $child->grade_item->load_raw();
267
268 if (empty($raw_grades)) {
269 $child->generate_grades();
270 $category_raw_grades[$child->id] = $child->grade_item->load_raw();
271 } else {
272 $category_raw_grades[$child->id] = $raw_grades;
0aa32279 273 }
274 }
275 }
2c72af1f 276
0aa32279 277 if (empty($category_raw_grades)) {
278 return null;
279 } else {
280 $aggregated_grades = $this->aggregate_grades($category_raw_grades);
2c72af1f 281
282 if (count($category_raw_grades) == 1) {
283 $aggregated_grades = current($category_raw_grades);
284 }
285
0aa32279 286 foreach ($aggregated_grades as $raw_grade) {
2c72af1f 287 $raw_grade->itemid = $this->grade_item->id;
0aa32279 288 $raw_grade->insert();
289 }
6527197b 290
2c72af1f 291 $this->grade_item->generate_final();
0aa32279 292 }
2c72af1f 293
294 $this->grade_item->load_raw();
295 return $this->grade_item->grade_grades_raw;
0aa32279 296 }
297
298 /**
299 * Given an array of arrays of grade objects (raw or final), uses this category's aggregation method to
2c72af1f 300 * compute and return a single array of grade_raw objects with the aggregated gradevalue. This method
301 * must also standardise all the scores (which have different mins and maxs) so that their values can
302 * be meaningfully aggregated (it would make no sense to perform MEAN(239, 5) on a grade_item with a
6527197b 303 * gradevalue between 20 and 250 and another grade_item with a gradevalue between 0 and 7!). Aggregated
2c72af1f 304 * values will be saved as grade_grades_raw->gradevalue, even when scales are involved.
0aa32279 305 * @param array $raw_grade_sets
306 * @return array Raw grade objects
307 */
308 function aggregate_grades($raw_grade_sets) {
2c72af1f 309 if (empty($raw_grade_sets)) {
310 return null;
311 }
0aa32279 312
2c72af1f 313 $aggregated_grades = array();
314 $pooled_grades = array();
315
316 foreach ($raw_grade_sets as $setkey => $set) {
317 foreach ($set as $gradekey => $raw_grade) {
2c72af1f 318 $this->load_grade_item();
319
6527197b 320 $value = standardise_score($raw_grade->gradevalue, $raw_grade->grademin, $raw_grade->grademax,
2c72af1f 321 $this->grade_item->grademin, $this->grade_item->grademax);
322 $pooled_grades[$raw_grade->userid][] = $value;
323 }
324 }
325
326 foreach ($pooled_grades as $userid => $grades) {
327 $aggregated_value = null;
328
329 switch ($this->aggregation) {
330 case GRADE_AGGREGATE_MEAN : // Arithmetic average
331 $num = count($grades);
332 $sum = array_sum($grades);
333 $aggregated_value = $sum / $num;
334 break;
335 case GRADE_AGGREGATE_MEDIAN : // Middle point value in the set: ignores frequencies
336 sort($grades);
337 $num = count($grades);
338 $halfpoint = intval($num / 2);
339
340 if($num % 2 == 0) {
341 $aggregated_value = ($grades[ceil($halfpoint)] + $grades[floor($halfpoint)]) / 2;
342 } else {
343 $aggregated_value = $grades[$halfpoint];
344 }
345
346 break;
347 case GRADE_AGGREGATE_MODE : // Value that occurs most frequently. Not always useful (all values are likely to be different)
348 // TODO implement or reject
349 break;
350 case GRADE_AGGREGATE_SUM :
351 $aggregated_value = array_sum($grades);
352 break;
353 default:
354 $num = count($grades);
355 $sum = array_sum($grades);
356 $aggregated_value = $sum / $num;
357 break;
358 }
359
360 $grade_raw = new grade_grades_raw();
361 $grade_raw->userid = $userid;
362 $grade_raw->gradevalue = $aggregated_value;
363 $grade_raw->grademin = $this->grade_item->grademin;
364 $grade_raw->grademax = $this->grade_item->grademax;
365 $grade_raw->itemid = $this->grade_item->id;
366 $aggregated_grades[$userid] = $grade_raw;
367 }
368
369 return $aggregated_grades;
0aa32279 370 }
371
ce385eb4 372 /**
373 * Looks at a path string (e.g. /2/45/56) and returns the depth level represented by this path (in this example, 3).
374 * If no string is given, it looks at the obect's path and assigns the resulting depth to its $depth variable.
375 * @param string $path
376 * @return int Depth level
377 */
378 function get_depth_from_path($path=NULL) {
379 if (empty($path)) {
380 $path = $this->path;
381 }
382 preg_match_all('/\/([0-9]+)+?/', $path, $matches);
383 $depth = count($matches[0]);
384
385 return $depth;
386 }
7c8a963f 387
388 /**
389 * Fetches and returns all the children categories and/or grade_items belonging to this category.
390 * By default only returns the immediate children (depth=1), but deeper levels can be requested,
391 * as well as all levels (0).
392 * @param int $depth 1 for immediate children, 0 for all children, and 2+ for specific levels deeper than 1.
393 * @param string $arraytype Either 'nested' or 'flat'. A nested array represents the true hierarchy, but is more difficult to work with.
394 * @return array Array of child objects (grade_category and grade_item).
395 */
396 function get_children($depth=1, $arraytype='nested') {
27f95e9b 397 $children_array = array();
398
399 // Set up $depth for recursion
400 $newdepth = $depth;
401 if ($depth > 1) {
402 $newdepth--;
403 }
404
405 $childrentype = $this->get_childrentype();
f151b073 406
27f95e9b 407 if ($childrentype == 'grade_item') {
f151b073 408 $children = get_records('grade_items', 'categoryid', $this->id);
27f95e9b 409 // No need to proceed with recursion
410 $children_array = $this->children_to_array($children, $arraytype, 'grade_item');
411 $this->children = $this->children_to_array($children, 'flat', 'grade_item');
412 } elseif ($childrentype == 'grade_category') {
413 $children = get_records('grade_categories', 'parent', $this->id, 'id');
f151b073 414
27f95e9b 415 if ($depth == 1) {
416 $children_array = $this->children_to_array($children, $arraytype, 'grade_category');
417 $this->children = $this->children_to_array($children, 'flat', 'grade_category');
7c8a963f 418 } else {
27f95e9b 419 foreach ($children as $id => $child) {
420 $cat = new grade_category($child, false);
421
422 if ($cat->has_children()) {
423 if ($arraytype == 'nested') {
424 $children_array[] = array('object' => $cat, 'children' => $cat->get_children($newdepth, $arraytype));
425 } else {
426 $children_array[] = $cat;
427 $cat_children = $cat->get_children($newdepth, $arraytype);
428 foreach ($cat_children as $id => $cat_child) {
429 $children_array[] = new grade_category($cat_child, false);
430 }
431 }
432 } else {
433 if ($arraytype == 'nested') {
434 $children_array[] = array('object' => $cat);
435 } else {
436 $children_array[] = $cat;
437 }
438 }
7c8a963f 439 }
27f95e9b 440 }
441 } else {
442 return null;
443 }
444
445 return $children_array;
446 }
447
448 /**
449 * Given an array of stdClass children of a certain $object_type, returns a flat or nested
450 * array of these children, ready for appending to a tree built by get_children.
451 * @static
452 * @param array $children
453 * @param string $arraytype
454 * @param string $object_type
455 * @return array
456 */
457 function children_to_array($children, $arraytype='nested', $object_type='grade_item') {
458 $children_array = array();
459
460 foreach ($children as $id => $child) {
461 if ($arraytype == 'nested') {
462 $children_array[] = array('object' => new $object_type($child, false));
463 } else {
464 $children_array[] = new $object_type($child);
465 }
466 }
7c8a963f 467
27f95e9b 468 return $children_array;
469 }
470
471 /**
472 * Returns true if this category has any child grade_category or grade_item.
473 * @return int number of direct children, or false if none found.
474 */
475 function has_children() {
476 return count_records('grade_categories', 'parent', $this->id) + count_records('grade_items', 'categoryid', $this->id);
477 }
478
479 /**
480 * This method checks whether an existing child exists for this
481 * category. If the new child is of a different type, the method will return false (not allowed).
482 * Otherwise it will return true.
483 * @param object $child This must be a complete object, not a stdClass
484 * @return boolean Success or failure
485 */
486 function can_add_child($child) {
487 if ($this->has_children()) {
488 if (get_class($child) != $this->get_childrentype()) {
489 return false;
490 } else {
491 return true;
492 }
493 } else {
494 return true;
495 }
496 }
497
498 /**
499 * Check the type of the first child of this category, to see whether it is a
500 * grade_category or a grade_item, and returns that type as a string (get_class).
501 * @return string
502 */
503 function get_childrentype() {
504 $children = $this->children;
505 if (empty($this->children)) {
506 $count_item_children = count_records('grade_items', 'categoryid', $this->id);
507 $count_cat_children = count_records('grade_categories', 'parent', $this->id);
f151b073 508
27f95e9b 509 if ($count_item_children > 0) {
510 return 'grade_item';
511 } elseif ($count_cat_children > 0) {
512 return 'grade_category';
513 } else {
514 return null;
7c8a963f 515 }
7c8a963f 516 }
27f95e9b 517 return get_class($children[0]);
7c8a963f 518 }
f151b073 519
520 /**
521 * Retrieves from DB, instantiates and saves the associated grade_item object.
522 * @return object Grade_item
523 */
524 function load_grade_item() {
525 $params = get_record('grade_items', 'categoryid', $this->id, 'itemtype', 'category');
526 $this->grade_item = new grade_item($params);
2c72af1f 527
528 // If the associated grade_item isn't yet created, do it now
529 if (empty($this->grade_item->id)) {
530 $this->grade_item->iteminstance = $this->id;
531 $this->grade_item->itemtype = 'category';
532 $this->grade_item->insert();
533 $this->grade_item->update_from_db();
534 }
535
f151b073 536 return $this->grade_item;
537 }
8c846243 538
539 /**
540 * Uses $this->parent to instantiate $this->parent_category based on the
541 * referenced record in the DB.
542 * @return object Parent_category
543 */
544 function load_parent_category() {
545 if (empty($this->parent_category) && !empty($this->parent)) {
546 $this->parent_category = grade_category::fetch('id', $this->parent);
547 }
548 return $this->parent_category;
549 }
8a31e65c 550}
551
552?>