added/changed custom corners lib and calls to add the necessary divs
[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;
27f95e9b 52
e5c674f1 53 /**
54 * The number of parents this category has.
55 * @var int $depth
56 */
57 var $depth = 0;
58
59 /**
60 * Shows the hierarchical path for this category as /1/2/3 (like course_categories), the last number being
61 * this category's autoincrement ID number.
62 * @var string $path
63 */
64 var $path;
65
8a31e65c 66 /**
67 * The name of this category.
68 * @var string $fullname
69 */
70 var $fullname;
71
72 /**
73 * A constant pointing to one of the predefined aggregation strategies (none, mean, median, sum etc) .
74 * @var int $aggregation
75 */
76 var $aggregation;
77
78 /**
79 * Keep only the X highest items.
80 * @var int $keephigh
81 */
82 var $keephigh;
83
84 /**
85 * Drop the X lowest items.
86 * @var int $droplow
87 */
88 var $droplow;
89
90 /**
91 * Date until which to hide this category. If null, 0 or false, category is not hidden.
92 * @var int $hidden
93 */
94 var $hidden;
95
96 /**
97 * Array of grade_items or grade_categories nested exactly 1 level below this category
98 * @var array $children
99 */
100 var $children;
8a31e65c 101
7c8a963f 102 /**
103 * A hierarchical array of all children below this category. This is stored separately from
104 * $children because it is more memory-intensive and may not be used as often.
105 * @var array $all_children
106 */
107 var $all_children;
108
f151b073 109 /**
110 * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
111 * for this category.
112 * @var object $grade_item
113 */
114 var $grade_item;
115
e5c674f1 116 /**
117 * Constructor. Extends the basic functionality defined in grade_object.
118 * @param array $params Can also be a standard object.
f151b073 119 * @param boolean $fetch Whether or not to fetch the corresponding row from the DB.
120 * @param object $grade_item The associated grade_item object can be passed during construction.
e5c674f1 121 */
f151b073 122 function grade_category($params=NULL, $fetch=true, $grade_item=NULL) {
e5c674f1 123 $this->grade_object($params, $fetch);
f151b073 124 if (!empty($grade_item) && $grade_item->itemtype == 'category') {
125 $this->grade_item = $grade_item;
126 if (empty($this->grade_item->iteminstance)) {
127 $this->grade_item->iteminstance = $this->id;
128 $this->grade_item->update();
129 }
130 }
e5c674f1 131 }
132
133
134 /**
135 * Builds this category's path string based on its parents (if any) and its own id number.
136 * This is typically done just before inserting this object in the DB for the first time,
ce385eb4 137 * or when a new parent is added or changed. It is a recursive function: once the calling
138 * object no longer has a parent, the path is complete.
139 *
140 * @static
141 * @param object $grade_category
142 * @return int The depth of this category (2 means there is one parent)
e5c674f1 143 */
ce385eb4 144 function build_path($grade_category) {
145 if (empty($grade_category->parent)) {
146 return "/$grade_category->id";
147 } else {
148 $parent = get_record('grade_categories', 'id', $grade_category->parent);
149 return grade_category::build_path($parent) . "/$grade_category->id";
150 }
e5c674f1 151 }
152
8a31e65c 153
8a31e65c 154 /**
155 * Finds and returns a grade_category object based on 1-3 field values.
156 *
8a31e65c 157 * @param string $field1
158 * @param string $value1
159 * @param string $field2
160 * @param string $value2
161 * @param string $field3
162 * @param string $value3
163 * @param string $fields
164 * @return object grade_category object or false if none found.
165 */
27f95e9b 166 function fetch($field1, $value1, $field2='', $value2='', $field3='', $value3='', $fields="*") {
8a31e65c 167 if ($grade_category = get_record('grade_categories', $field1, $value1, $field2, $value2, $field3, $value3, $fields)) {
7c8a963f 168 if (isset($this) && get_class($this) == 'grade_category') {
8a31e65c 169 foreach ($grade_category as $param => $value) {
170 $this->$param = $value;
171 }
172 return $this;
7c8a963f 173 } else {
174 $grade_category = new grade_category($grade_category);
175 return $grade_category;
8a31e65c 176 }
177 } else {
178 return false;
179 }
ce385eb4 180 }
181
182 /**
183 * In addition to the normal insert() defined in grade_object, this method sets the depth
184 * and path for this object, and update the record accordingly. The reason why this must
185 * be done here instead of in the constructor, is that they both need to know the record's
27f95e9b 186 * id number, which only gets created at insertion time.
f151b073 187 * This method also creates an associated grade_item if this wasn't done during construction.
ce385eb4 188 */
189 function insert() {
190 $result = parent::insert();
191
192 // Build path and depth variables
193 if (!empty($this->parent)) {
194 $this->path = grade_category::build_path($this);
195 $this->depth = $this->get_depth_from_path();
196 } else {
197 $this->depth = 1;
198 $this->path = "/$this->id";
199 }
200
201 $this->update();
f151b073 202
203 if (empty($this->grade_item)) {
204 $grade_item = new grade_item();
205 $grade_item->iteminstance = $this->id;
206 $grade_item->itemtype = 'category';
6527197b 207
208 if (!$grade_item->insert()) {
209 return false;
210 }
211
f151b073 212 $this->grade_item = $grade_item;
213 }
214
ce385eb4 215 return $result;
216 }
0aa32279 217
218 /**
219 * Generates and saves raw_grades, based on this category's immediate children, then uses the
220 * associated grade_item to generate matching final grades. These immediate children must first have their own
221 * raw and final grades, which means that ultimately we must get grade_items as children. The category's aggregation
222 * method is used to generate these raw grades, which can then be used by the category's associated grade_item
223 * to apply calculations to and generate final grades.
224 */
225 function generate_grades() {
2c72af1f 226 // Check that the children have final grades. If not, call their generate_grades method (recursion)
0aa32279 227 if (empty($this->children)) {
228 $this->children = $this->get_children(1, 'flat');
229 }
230
231 $category_raw_grades = array();
232 $aggregated_grades = array();
233
234 foreach ($this->children as $child) {
235 if (get_class($child) == 'grade_item') {
2c72af1f 236 $category_raw_grades[$child->id] = $child->load_raw();
237 } elseif (get_class($child) == 'grade_category') {
238 $child->load_grade_item();
239 $raw_grades = $child->grade_item->load_raw();
240
241 if (empty($raw_grades)) {
242 $child->generate_grades();
243 $category_raw_grades[$child->id] = $child->grade_item->load_raw();
244 } else {
245 $category_raw_grades[$child->id] = $raw_grades;
0aa32279 246 }
247 }
248 }
2c72af1f 249
0aa32279 250 if (empty($category_raw_grades)) {
251 return null;
252 } else {
253 $aggregated_grades = $this->aggregate_grades($category_raw_grades);
2c72af1f 254
255 if (count($category_raw_grades) == 1) {
256 $aggregated_grades = current($category_raw_grades);
257 }
258
0aa32279 259 foreach ($aggregated_grades as $raw_grade) {
2c72af1f 260 $raw_grade->itemid = $this->grade_item->id;
0aa32279 261 $raw_grade->insert();
262 }
6527197b 263
2c72af1f 264 $this->grade_item->generate_final();
0aa32279 265 }
2c72af1f 266
267 $this->grade_item->load_raw();
268 return $this->grade_item->grade_grades_raw;
0aa32279 269 }
270
271 /**
272 * Given an array of arrays of grade objects (raw or final), uses this category's aggregation method to
2c72af1f 273 * compute and return a single array of grade_raw objects with the aggregated gradevalue. This method
274 * must also standardise all the scores (which have different mins and maxs) so that their values can
275 * be meaningfully aggregated (it would make no sense to perform MEAN(239, 5) on a grade_item with a
6527197b 276 * gradevalue between 20 and 250 and another grade_item with a gradevalue between 0 and 7!). Aggregated
2c72af1f 277 * values will be saved as grade_grades_raw->gradevalue, even when scales are involved.
0aa32279 278 * @param array $raw_grade_sets
279 * @return array Raw grade objects
280 */
281 function aggregate_grades($raw_grade_sets) {
2c72af1f 282 if (empty($raw_grade_sets)) {
283 return null;
284 }
0aa32279 285
2c72af1f 286 $aggregated_grades = array();
287 $pooled_grades = array();
288
289 foreach ($raw_grade_sets as $setkey => $set) {
290 foreach ($set as $gradekey => $raw_grade) {
2c72af1f 291 $this->load_grade_item();
292
6527197b 293 $value = standardise_score($raw_grade->gradevalue, $raw_grade->grademin, $raw_grade->grademax,
2c72af1f 294 $this->grade_item->grademin, $this->grade_item->grademax);
295 $pooled_grades[$raw_grade->userid][] = $value;
296 }
297 }
298
299 foreach ($pooled_grades as $userid => $grades) {
300 $aggregated_value = null;
301
302 switch ($this->aggregation) {
303 case GRADE_AGGREGATE_MEAN : // Arithmetic average
304 $num = count($grades);
305 $sum = array_sum($grades);
306 $aggregated_value = $sum / $num;
307 break;
308 case GRADE_AGGREGATE_MEDIAN : // Middle point value in the set: ignores frequencies
309 sort($grades);
310 $num = count($grades);
311 $halfpoint = intval($num / 2);
312
313 if($num % 2 == 0) {
314 $aggregated_value = ($grades[ceil($halfpoint)] + $grades[floor($halfpoint)]) / 2;
315 } else {
316 $aggregated_value = $grades[$halfpoint];
317 }
318
319 break;
320 case GRADE_AGGREGATE_MODE : // Value that occurs most frequently. Not always useful (all values are likely to be different)
321 // TODO implement or reject
322 break;
323 case GRADE_AGGREGATE_SUM :
324 $aggregated_value = array_sum($grades);
325 break;
326 default:
327 $num = count($grades);
328 $sum = array_sum($grades);
329 $aggregated_value = $sum / $num;
330 break;
331 }
332
333 $grade_raw = new grade_grades_raw();
334 $grade_raw->userid = $userid;
335 $grade_raw->gradevalue = $aggregated_value;
336 $grade_raw->grademin = $this->grade_item->grademin;
337 $grade_raw->grademax = $this->grade_item->grademax;
338 $grade_raw->itemid = $this->grade_item->id;
339 $aggregated_grades[$userid] = $grade_raw;
340 }
341
342 return $aggregated_grades;
0aa32279 343 }
344
ce385eb4 345 /**
346 * Looks at a path string (e.g. /2/45/56) and returns the depth level represented by this path (in this example, 3).
347 * If no string is given, it looks at the obect's path and assigns the resulting depth to its $depth variable.
348 * @param string $path
349 * @return int Depth level
350 */
351 function get_depth_from_path($path=NULL) {
352 if (empty($path)) {
353 $path = $this->path;
354 }
355 preg_match_all('/\/([0-9]+)+?/', $path, $matches);
356 $depth = count($matches[0]);
357
358 return $depth;
359 }
7c8a963f 360
361 /**
362 * Fetches and returns all the children categories and/or grade_items belonging to this category.
363 * By default only returns the immediate children (depth=1), but deeper levels can be requested,
364 * as well as all levels (0).
365 * @param int $depth 1 for immediate children, 0 for all children, and 2+ for specific levels deeper than 1.
366 * @param string $arraytype Either 'nested' or 'flat'. A nested array represents the true hierarchy, but is more difficult to work with.
367 * @return array Array of child objects (grade_category and grade_item).
368 */
369 function get_children($depth=1, $arraytype='nested') {
27f95e9b 370 $children_array = array();
371
372 // Set up $depth for recursion
373 $newdepth = $depth;
374 if ($depth > 1) {
375 $newdepth--;
376 }
377
378 $childrentype = $this->get_childrentype();
f151b073 379
27f95e9b 380 if ($childrentype == 'grade_item') {
f151b073 381 $children = get_records('grade_items', 'categoryid', $this->id);
27f95e9b 382 // No need to proceed with recursion
383 $children_array = $this->children_to_array($children, $arraytype, 'grade_item');
384 $this->children = $this->children_to_array($children, 'flat', 'grade_item');
385 } elseif ($childrentype == 'grade_category') {
386 $children = get_records('grade_categories', 'parent', $this->id, 'id');
f151b073 387
27f95e9b 388 if ($depth == 1) {
389 $children_array = $this->children_to_array($children, $arraytype, 'grade_category');
390 $this->children = $this->children_to_array($children, 'flat', 'grade_category');
7c8a963f 391 } else {
27f95e9b 392 foreach ($children as $id => $child) {
393 $cat = new grade_category($child, false);
394
395 if ($cat->has_children()) {
396 if ($arraytype == 'nested') {
397 $children_array[] = array('object' => $cat, 'children' => $cat->get_children($newdepth, $arraytype));
398 } else {
399 $children_array[] = $cat;
400 $cat_children = $cat->get_children($newdepth, $arraytype);
401 foreach ($cat_children as $id => $cat_child) {
402 $children_array[] = new grade_category($cat_child, false);
403 }
404 }
405 } else {
406 if ($arraytype == 'nested') {
407 $children_array[] = array('object' => $cat);
408 } else {
409 $children_array[] = $cat;
410 }
411 }
7c8a963f 412 }
27f95e9b 413 }
414 } else {
415 return null;
416 }
417
418 return $children_array;
419 }
420
421 /**
422 * Given an array of stdClass children of a certain $object_type, returns a flat or nested
423 * array of these children, ready for appending to a tree built by get_children.
424 * @static
425 * @param array $children
426 * @param string $arraytype
427 * @param string $object_type
428 * @return array
429 */
430 function children_to_array($children, $arraytype='nested', $object_type='grade_item') {
431 $children_array = array();
432
433 foreach ($children as $id => $child) {
434 if ($arraytype == 'nested') {
435 $children_array[] = array('object' => new $object_type($child, false));
436 } else {
437 $children_array[] = new $object_type($child);
438 }
439 }
7c8a963f 440
27f95e9b 441 return $children_array;
442 }
443
444 /**
445 * Returns true if this category has any child grade_category or grade_item.
446 * @return int number of direct children, or false if none found.
447 */
448 function has_children() {
449 return count_records('grade_categories', 'parent', $this->id) + count_records('grade_items', 'categoryid', $this->id);
450 }
451
452 /**
453 * This method checks whether an existing child exists for this
454 * category. If the new child is of a different type, the method will return false (not allowed).
455 * Otherwise it will return true.
456 * @param object $child This must be a complete object, not a stdClass
457 * @return boolean Success or failure
458 */
459 function can_add_child($child) {
460 if ($this->has_children()) {
461 if (get_class($child) != $this->get_childrentype()) {
462 return false;
463 } else {
464 return true;
465 }
466 } else {
467 return true;
468 }
469 }
470
471 /**
472 * Check the type of the first child of this category, to see whether it is a
473 * grade_category or a grade_item, and returns that type as a string (get_class).
474 * @return string
475 */
476 function get_childrentype() {
477 $children = $this->children;
478 if (empty($this->children)) {
479 $count_item_children = count_records('grade_items', 'categoryid', $this->id);
480 $count_cat_children = count_records('grade_categories', 'parent', $this->id);
f151b073 481
27f95e9b 482 if ($count_item_children > 0) {
483 return 'grade_item';
484 } elseif ($count_cat_children > 0) {
485 return 'grade_category';
486 } else {
487 return null;
7c8a963f 488 }
7c8a963f 489 }
27f95e9b 490 return get_class($children[0]);
7c8a963f 491 }
f151b073 492
493 /**
494 * Retrieves from DB, instantiates and saves the associated grade_item object.
495 * @return object Grade_item
496 */
497 function load_grade_item() {
498 $params = get_record('grade_items', 'categoryid', $this->id, 'itemtype', 'category');
499 $this->grade_item = new grade_item($params);
2c72af1f 500
501 // If the associated grade_item isn't yet created, do it now
502 if (empty($this->grade_item->id)) {
503 $this->grade_item->iteminstance = $this->id;
504 $this->grade_item->itemtype = 'category';
505 $this->grade_item->insert();
506 $this->grade_item->update_from_db();
507 }
508
f151b073 509 return $this->grade_item;
510 }
8a31e65c 511}
512
513?>