Removed set_timecreated and just tidied up some time-related things
[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
8f4a626d 188 /**
189 * In addition to update() as defined in grade_object, call flag_for_update of parent categories, if applicable.
190 */
191 function update() {
192 $qualifies = $this->qualifies_for_update();
193
194 $result = parent::update();
195
cb64c6b2 196 // Use $this->path to update all parent categories
8f4a626d 197 if ($result && $qualifies) {
cb64c6b2 198 $this->flag_for_update();
8f4a626d 199 }
8f4a626d 200 return $result;
201 }
202
203 /**
204 * If parent::delete() is successful, send flag_for_update message to parent category.
205 * @return boolean Success or failure.
206 */
207 function delete() {
208 $result = parent::delete();
209
210 if ($result) {
211 $this->load_parent_category();
212 if (!empty($this->parent_category)) {
213 $result = $result && $this->parent_category->flag_for_update();
214 }
215 }
216
217 return $result;
218 }
219
ce385eb4 220 /**
221 * In addition to the normal insert() defined in grade_object, this method sets the depth
222 * and path for this object, and update the record accordingly. The reason why this must
223 * be done here instead of in the constructor, is that they both need to know the record's
27f95e9b 224 * id number, which only gets created at insertion time.
f151b073 225 * This method also creates an associated grade_item if this wasn't done during construction.
ce385eb4 226 */
227 function insert() {
228 $result = parent::insert();
229
230 // Build path and depth variables
231 if (!empty($this->parent)) {
232 $this->path = grade_category::build_path($this);
233 $this->depth = $this->get_depth_from_path();
234 } else {
235 $this->depth = 1;
236 $this->path = "/$this->id";
237 }
238
239 $this->update();
f151b073 240
241 if (empty($this->grade_item)) {
242 $grade_item = new grade_item();
243 $grade_item->iteminstance = $this->id;
244 $grade_item->itemtype = 'category';
6527197b 245
246 if (!$grade_item->insert()) {
247 return false;
248 }
249
f151b073 250 $this->grade_item = $grade_item;
251 }
cb64c6b2 252
8f4a626d 253 // Notify parent category of need to update.
254 if ($result) {
255 $this->load_parent_category();
256 if (!empty($this->parent_category)) {
257 if (!$this->parent_category->flag_for_update()) {
258 return false;
259 }
260 }
261 }
ce385eb4 262 return $result;
263 }
8f4a626d 264
265 /**
266 * Compares the values held by this object with those of the matching record in DB, and returns
267 * whether or not these differences are sufficient to justify an update of all parent objects.
268 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
269 * @return boolean
270 */
271 function qualifies_for_update() {
272 if (empty($this->id)) {
273 return false;
274 }
275
276 $db_item = new grade_category(array('id' => $this->id));
277
278 $aggregationdiff = $db_item->aggregation != $this->aggregation;
279 $keephighdiff = $db_item->keephigh != $this->keephigh;
280 $droplowdiff = $db_item->droplow != $this->droplow;
281
282 if ($aggregationdiff || $keephighdiff || $droplowdiff) {
283 return true;
284 } else {
285 return false;
286 }
287 }
8c846243 288
289 /**
290 * Sets this category's and its parent's grade_item.needsupdate to true.
291 * This is triggered whenever any change in any lower level may cause grade_finals
292 * for this category to require an update. The flag needs to be propagated up all
293 * levels until it reaches the top category. This is then used to determine whether or not
cb64c6b2 294 * to regenerate the raw and final grades for each category grade_item. This is accomplished
295 * thanks to the path variable, so we don't need to use recursion.
8c846243 296 * @return boolean Success or failure
297 */
298 function flag_for_update() {
299 $result = true;
8f4a626d 300
8c846243 301 $this->load_grade_item();
8f4a626d 302
303 if (empty($this->grade_item)) {
304 die("Associated grade_item object does not exist for this grade_category!" . print_object($this));
305 // TODO Send error message, this is a critical error: each category MUST have a matching grade_item object
306 }
307
8c846243 308 $this->grade_item->needsupdate = true;
8f4a626d 309
310 $result = $result && $this->grade_item->update();
311
cb64c6b2 312 $paths = explode('/', $this->path);
313
314 $wheresql = '';
315
316 foreach ($paths as $categoryid) {
317 $wheresql .= "iteminstance = $categoryid OR";
8c846243 318 }
319
cb64c6b2 320 $wheresql = substr($wheresql, 0, strrpos($wheresql, 'OR'));
321
322 // TODO use this sql fragment to set needsupdate to true for all grade_items whose iteminstance matches the categoryids
8c846243 323 return $result;
324 }
325
0aa32279 326 /**
327 * Generates and saves raw_grades, based on this category's immediate children, then uses the
328 * associated grade_item to generate matching final grades. These immediate children must first have their own
329 * raw and final grades, which means that ultimately we must get grade_items as children. The category's aggregation
330 * method is used to generate these raw grades, which can then be used by the category's associated grade_item
331 * to apply calculations to and generate final grades.
332 */
333 function generate_grades() {
2c72af1f 334 // Check that the children have final grades. If not, call their generate_grades method (recursion)
0aa32279 335 if (empty($this->children)) {
336 $this->children = $this->get_children(1, 'flat');
337 }
338
339 $category_raw_grades = array();
340 $aggregated_grades = array();
341
342 foreach ($this->children as $child) {
343 if (get_class($child) == 'grade_item') {
2c72af1f 344 $category_raw_grades[$child->id] = $child->load_raw();
345 } elseif (get_class($child) == 'grade_category') {
346 $child->load_grade_item();
347 $raw_grades = $child->grade_item->load_raw();
348
349 if (empty($raw_grades)) {
350 $child->generate_grades();
351 $category_raw_grades[$child->id] = $child->grade_item->load_raw();
352 } else {
353 $category_raw_grades[$child->id] = $raw_grades;
0aa32279 354 }
355 }
356 }
2c72af1f 357
0aa32279 358 if (empty($category_raw_grades)) {
359 return null;
360 } else {
361 $aggregated_grades = $this->aggregate_grades($category_raw_grades);
2c72af1f 362
363 if (count($category_raw_grades) == 1) {
364 $aggregated_grades = current($category_raw_grades);
365 }
366
0aa32279 367 foreach ($aggregated_grades as $raw_grade) {
2c72af1f 368 $raw_grade->itemid = $this->grade_item->id;
0aa32279 369 $raw_grade->insert();
370 }
6527197b 371
2c72af1f 372 $this->grade_item->generate_final();
0aa32279 373 }
2c72af1f 374
375 $this->grade_item->load_raw();
376 return $this->grade_item->grade_grades_raw;
0aa32279 377 }
378
379 /**
380 * Given an array of arrays of grade objects (raw or final), uses this category's aggregation method to
2c72af1f 381 * compute and return a single array of grade_raw objects with the aggregated gradevalue. This method
382 * must also standardise all the scores (which have different mins and maxs) so that their values can
383 * be meaningfully aggregated (it would make no sense to perform MEAN(239, 5) on a grade_item with a
6527197b 384 * gradevalue between 20 and 250 and another grade_item with a gradevalue between 0 and 7!). Aggregated
2c72af1f 385 * values will be saved as grade_grades_raw->gradevalue, even when scales are involved.
0aa32279 386 * @param array $raw_grade_sets
387 * @return array Raw grade objects
388 */
389 function aggregate_grades($raw_grade_sets) {
2c72af1f 390 if (empty($raw_grade_sets)) {
391 return null;
392 }
0aa32279 393
2c72af1f 394 $aggregated_grades = array();
395 $pooled_grades = array();
396
397 foreach ($raw_grade_sets as $setkey => $set) {
398 foreach ($set as $gradekey => $raw_grade) {
2c72af1f 399 $this->load_grade_item();
400
6527197b 401 $value = standardise_score($raw_grade->gradevalue, $raw_grade->grademin, $raw_grade->grademax,
2c72af1f 402 $this->grade_item->grademin, $this->grade_item->grademax);
403 $pooled_grades[$raw_grade->userid][] = $value;
404 }
405 }
406
407 foreach ($pooled_grades as $userid => $grades) {
408 $aggregated_value = null;
409
410 switch ($this->aggregation) {
411 case GRADE_AGGREGATE_MEAN : // Arithmetic average
412 $num = count($grades);
413 $sum = array_sum($grades);
414 $aggregated_value = $sum / $num;
415 break;
416 case GRADE_AGGREGATE_MEDIAN : // Middle point value in the set: ignores frequencies
417 sort($grades);
418 $num = count($grades);
419 $halfpoint = intval($num / 2);
420
421 if($num % 2 == 0) {
422 $aggregated_value = ($grades[ceil($halfpoint)] + $grades[floor($halfpoint)]) / 2;
423 } else {
424 $aggregated_value = $grades[$halfpoint];
425 }
426
427 break;
428 case GRADE_AGGREGATE_MODE : // Value that occurs most frequently. Not always useful (all values are likely to be different)
429 // TODO implement or reject
430 break;
431 case GRADE_AGGREGATE_SUM :
432 $aggregated_value = array_sum($grades);
433 break;
434 default:
435 $num = count($grades);
436 $sum = array_sum($grades);
437 $aggregated_value = $sum / $num;
438 break;
439 }
440
441 $grade_raw = new grade_grades_raw();
442 $grade_raw->userid = $userid;
443 $grade_raw->gradevalue = $aggregated_value;
444 $grade_raw->grademin = $this->grade_item->grademin;
445 $grade_raw->grademax = $this->grade_item->grademax;
446 $grade_raw->itemid = $this->grade_item->id;
447 $aggregated_grades[$userid] = $grade_raw;
448 }
449
450 return $aggregated_grades;
0aa32279 451 }
452
ce385eb4 453 /**
454 * Looks at a path string (e.g. /2/45/56) and returns the depth level represented by this path (in this example, 3).
455 * If no string is given, it looks at the obect's path and assigns the resulting depth to its $depth variable.
456 * @param string $path
457 * @return int Depth level
458 */
459 function get_depth_from_path($path=NULL) {
460 if (empty($path)) {
461 $path = $this->path;
462 }
463 preg_match_all('/\/([0-9]+)+?/', $path, $matches);
464 $depth = count($matches[0]);
465
466 return $depth;
467 }
7c8a963f 468
469 /**
470 * Fetches and returns all the children categories and/or grade_items belonging to this category.
471 * By default only returns the immediate children (depth=1), but deeper levels can be requested,
472 * as well as all levels (0).
473 * @param int $depth 1 for immediate children, 0 for all children, and 2+ for specific levels deeper than 1.
474 * @param string $arraytype Either 'nested' or 'flat'. A nested array represents the true hierarchy, but is more difficult to work with.
475 * @return array Array of child objects (grade_category and grade_item).
476 */
477 function get_children($depth=1, $arraytype='nested') {
27f95e9b 478 $children_array = array();
479
480 // Set up $depth for recursion
481 $newdepth = $depth;
482 if ($depth > 1) {
483 $newdepth--;
484 }
485
486 $childrentype = $this->get_childrentype();
f151b073 487
27f95e9b 488 if ($childrentype == 'grade_item') {
f151b073 489 $children = get_records('grade_items', 'categoryid', $this->id);
27f95e9b 490 // No need to proceed with recursion
491 $children_array = $this->children_to_array($children, $arraytype, 'grade_item');
492 $this->children = $this->children_to_array($children, 'flat', 'grade_item');
493 } elseif ($childrentype == 'grade_category') {
494 $children = get_records('grade_categories', 'parent', $this->id, 'id');
f151b073 495
27f95e9b 496 if ($depth == 1) {
497 $children_array = $this->children_to_array($children, $arraytype, 'grade_category');
498 $this->children = $this->children_to_array($children, 'flat', 'grade_category');
7c8a963f 499 } else {
27f95e9b 500 foreach ($children as $id => $child) {
501 $cat = new grade_category($child, false);
502
503 if ($cat->has_children()) {
504 if ($arraytype == 'nested') {
505 $children_array[] = array('object' => $cat, 'children' => $cat->get_children($newdepth, $arraytype));
506 } else {
507 $children_array[] = $cat;
508 $cat_children = $cat->get_children($newdepth, $arraytype);
509 foreach ($cat_children as $id => $cat_child) {
510 $children_array[] = new grade_category($cat_child, false);
511 }
512 }
513 } else {
514 if ($arraytype == 'nested') {
515 $children_array[] = array('object' => $cat);
516 } else {
517 $children_array[] = $cat;
518 }
519 }
7c8a963f 520 }
27f95e9b 521 }
522 } else {
523 return null;
524 }
525
526 return $children_array;
527 }
528
529 /**
530 * Given an array of stdClass children of a certain $object_type, returns a flat or nested
531 * array of these children, ready for appending to a tree built by get_children.
532 * @static
533 * @param array $children
534 * @param string $arraytype
535 * @param string $object_type
536 * @return array
537 */
538 function children_to_array($children, $arraytype='nested', $object_type='grade_item') {
539 $children_array = array();
540
541 foreach ($children as $id => $child) {
542 if ($arraytype == 'nested') {
543 $children_array[] = array('object' => new $object_type($child, false));
544 } else {
545 $children_array[] = new $object_type($child);
546 }
547 }
7c8a963f 548
27f95e9b 549 return $children_array;
550 }
551
552 /**
553 * Returns true if this category has any child grade_category or grade_item.
554 * @return int number of direct children, or false if none found.
555 */
556 function has_children() {
557 return count_records('grade_categories', 'parent', $this->id) + count_records('grade_items', 'categoryid', $this->id);
558 }
559
560 /**
561 * This method checks whether an existing child exists for this
562 * category. If the new child is of a different type, the method will return false (not allowed).
563 * Otherwise it will return true.
564 * @param object $child This must be a complete object, not a stdClass
565 * @return boolean Success or failure
566 */
567 function can_add_child($child) {
568 if ($this->has_children()) {
569 if (get_class($child) != $this->get_childrentype()) {
570 return false;
571 } else {
572 return true;
573 }
574 } else {
575 return true;
576 }
577 }
578
579 /**
580 * Check the type of the first child of this category, to see whether it is a
581 * grade_category or a grade_item, and returns that type as a string (get_class).
582 * @return string
583 */
584 function get_childrentype() {
585 $children = $this->children;
586 if (empty($this->children)) {
587 $count_item_children = count_records('grade_items', 'categoryid', $this->id);
588 $count_cat_children = count_records('grade_categories', 'parent', $this->id);
f151b073 589
27f95e9b 590 if ($count_item_children > 0) {
591 return 'grade_item';
592 } elseif ($count_cat_children > 0) {
593 return 'grade_category';
594 } else {
595 return null;
7c8a963f 596 }
7c8a963f 597 }
27f95e9b 598 return get_class($children[0]);
7c8a963f 599 }
f151b073 600
601 /**
602 * Retrieves from DB, instantiates and saves the associated grade_item object.
603 * @return object Grade_item
604 */
605 function load_grade_item() {
8f4a626d 606 $grade_items = get_records_select('grade_items', "iteminstance = $this->id AND itemtype = 'category'", null, '*', 0, 1);
607
608 $params = current($grade_items);
f151b073 609 $this->grade_item = new grade_item($params);
2c72af1f 610
8f4a626d 611 // If the associated grade_item isn't yet created, do it now. But first try loading it, in case it exists in DB.
2c72af1f 612 if (empty($this->grade_item->id)) {
613 $this->grade_item->iteminstance = $this->id;
614 $this->grade_item->itemtype = 'category';
615 $this->grade_item->insert();
616 $this->grade_item->update_from_db();
617 }
618
f151b073 619 return $this->grade_item;
620 }
8c846243 621
622 /**
623 * Uses $this->parent to instantiate $this->parent_category based on the
624 * referenced record in the DB.
625 * @return object Parent_category
626 */
627 function load_parent_category() {
628 if (empty($this->parent_category) && !empty($this->parent)) {
629 $this->parent_category = grade_category::fetch('id', $this->parent);
630 }
631 return $this->parent_category;
632 }
8a31e65c 633}
634
635?>