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