Added CSS class for current language to every page MDL-9750
[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';
207 $result = $result & $grade_item->insert();
208 $this->grade_item = $grade_item;
209 }
210
ce385eb4 211 return $result;
212 }
0aa32279 213
214 /**
215 * Generates and saves raw_grades, based on this category's immediate children, then uses the
216 * associated grade_item to generate matching final grades. These immediate children must first have their own
217 * raw and final grades, which means that ultimately we must get grade_items as children. The category's aggregation
218 * method is used to generate these raw grades, which can then be used by the category's associated grade_item
219 * to apply calculations to and generate final grades.
220 */
221 function generate_grades() {
2c72af1f 222 // Check that the children have final grades. If not, call their generate_grades method (recursion)
0aa32279 223 if (empty($this->children)) {
224 $this->children = $this->get_children(1, 'flat');
225 }
226
227 $category_raw_grades = array();
228 $aggregated_grades = array();
229
230 foreach ($this->children as $child) {
231 if (get_class($child) == 'grade_item') {
2c72af1f 232 $category_raw_grades[$child->id] = $child->load_raw();
233 } elseif (get_class($child) == 'grade_category') {
234 $child->load_grade_item();
235 $raw_grades = $child->grade_item->load_raw();
236
237 if (empty($raw_grades)) {
238 $child->generate_grades();
239 $category_raw_grades[$child->id] = $child->grade_item->load_raw();
240 } else {
241 $category_raw_grades[$child->id] = $raw_grades;
0aa32279 242 }
243 }
244 }
2c72af1f 245
0aa32279 246 if (empty($category_raw_grades)) {
247 return null;
248 } else {
249 $aggregated_grades = $this->aggregate_grades($category_raw_grades);
2c72af1f 250
251 if (count($category_raw_grades) == 1) {
252 $aggregated_grades = current($category_raw_grades);
253 }
254
0aa32279 255 foreach ($aggregated_grades as $raw_grade) {
2c72af1f 256 $raw_grade->itemid = $this->grade_item->id;
0aa32279 257 $raw_grade->insert();
258 }
2c72af1f 259 $this->load_grade_item();
260 $this->grade_item->generate_final();
0aa32279 261 }
2c72af1f 262
263 $this->grade_item->load_raw();
264 return $this->grade_item->grade_grades_raw;
0aa32279 265 }
266
267 /**
268 * Given an array of arrays of grade objects (raw or final), uses this category's aggregation method to
2c72af1f 269 * compute and return a single array of grade_raw objects with the aggregated gradevalue. This method
270 * must also standardise all the scores (which have different mins and maxs) so that their values can
271 * be meaningfully aggregated (it would make no sense to perform MEAN(239, 5) on a grade_item with a
272 * gradevalue between 20 and 250 and another grade_item with a gradescale between 0 and 7!). Aggregated
273 * values will be saved as grade_grades_raw->gradevalue, even when scales are involved.
0aa32279 274 * @param array $raw_grade_sets
275 * @return array Raw grade objects
276 */
277 function aggregate_grades($raw_grade_sets) {
2c72af1f 278 if (empty($raw_grade_sets)) {
279 return null;
280 }
0aa32279 281
2c72af1f 282 $aggregated_grades = array();
283 $pooled_grades = array();
284
285 foreach ($raw_grade_sets as $setkey => $set) {
286 foreach ($set as $gradekey => $raw_grade) {
287 $valuetype = 'gradevalue';
288
289 if (!empty($raw_grade->gradescale)) {
290 $valuetype = 'gradescale';
291 }
292 $this->load_grade_item();
293
294 $value = standardise_score($raw_grade->$valuetype, $raw_grade->grademin, $raw_grade->grademax,
295 $this->grade_item->grademin, $this->grade_item->grademax);
296 $pooled_grades[$raw_grade->userid][] = $value;
297 }
298 }
299
300 foreach ($pooled_grades as $userid => $grades) {
301 $aggregated_value = null;
302
303 switch ($this->aggregation) {
304 case GRADE_AGGREGATE_MEAN : // Arithmetic average
305 $num = count($grades);
306 $sum = array_sum($grades);
307 $aggregated_value = $sum / $num;
308 break;
309 case GRADE_AGGREGATE_MEDIAN : // Middle point value in the set: ignores frequencies
310 sort($grades);
311 $num = count($grades);
312 $halfpoint = intval($num / 2);
313
314 if($num % 2 == 0) {
315 $aggregated_value = ($grades[ceil($halfpoint)] + $grades[floor($halfpoint)]) / 2;
316 } else {
317 $aggregated_value = $grades[$halfpoint];
318 }
319
320 break;
321 case GRADE_AGGREGATE_MODE : // Value that occurs most frequently. Not always useful (all values are likely to be different)
322 // TODO implement or reject
323 break;
324 case GRADE_AGGREGATE_SUM :
325 $aggregated_value = array_sum($grades);
326 break;
327 default:
328 $num = count($grades);
329 $sum = array_sum($grades);
330 $aggregated_value = $sum / $num;
331 break;
332 }
333
334 $grade_raw = new grade_grades_raw();
335 $grade_raw->userid = $userid;
336 $grade_raw->gradevalue = $aggregated_value;
337 $grade_raw->grademin = $this->grade_item->grademin;
338 $grade_raw->grademax = $this->grade_item->grademax;
339 $grade_raw->itemid = $this->grade_item->id;
340 $aggregated_grades[$userid] = $grade_raw;
341 }
342
343 return $aggregated_grades;
0aa32279 344 }
345
ce385eb4 346 /**
347 * Looks at a path string (e.g. /2/45/56) and returns the depth level represented by this path (in this example, 3).
348 * If no string is given, it looks at the obect's path and assigns the resulting depth to its $depth variable.
349 * @param string $path
350 * @return int Depth level
351 */
352 function get_depth_from_path($path=NULL) {
353 if (empty($path)) {
354 $path = $this->path;
355 }
356 preg_match_all('/\/([0-9]+)+?/', $path, $matches);
357 $depth = count($matches[0]);
358
359 return $depth;
360 }
7c8a963f 361
362 /**
363 * Fetches and returns all the children categories and/or grade_items belonging to this category.
364 * By default only returns the immediate children (depth=1), but deeper levels can be requested,
365 * as well as all levels (0).
366 * @param int $depth 1 for immediate children, 0 for all children, and 2+ for specific levels deeper than 1.
367 * @param string $arraytype Either 'nested' or 'flat'. A nested array represents the true hierarchy, but is more difficult to work with.
368 * @return array Array of child objects (grade_category and grade_item).
369 */
370 function get_children($depth=1, $arraytype='nested') {
27f95e9b 371 $children_array = array();
372
373 // Set up $depth for recursion
374 $newdepth = $depth;
375 if ($depth > 1) {
376 $newdepth--;
377 }
378
379 $childrentype = $this->get_childrentype();
f151b073 380
27f95e9b 381 if ($childrentype == 'grade_item') {
f151b073 382 $children = get_records('grade_items', 'categoryid', $this->id);
27f95e9b 383 // No need to proceed with recursion
384 $children_array = $this->children_to_array($children, $arraytype, 'grade_item');
385 $this->children = $this->children_to_array($children, 'flat', 'grade_item');
386 } elseif ($childrentype == 'grade_category') {
387 $children = get_records('grade_categories', 'parent', $this->id, 'id');
f151b073 388
27f95e9b 389 if ($depth == 1) {
390 $children_array = $this->children_to_array($children, $arraytype, 'grade_category');
391 $this->children = $this->children_to_array($children, 'flat', 'grade_category');
7c8a963f 392 } else {
27f95e9b 393 foreach ($children as $id => $child) {
394 $cat = new grade_category($child, false);
395
396 if ($cat->has_children()) {
397 if ($arraytype == 'nested') {
398 $children_array[] = array('object' => $cat, 'children' => $cat->get_children($newdepth, $arraytype));
399 } else {
400 $children_array[] = $cat;
401 $cat_children = $cat->get_children($newdepth, $arraytype);
402 foreach ($cat_children as $id => $cat_child) {
403 $children_array[] = new grade_category($cat_child, false);
404 }
405 }
406 } else {
407 if ($arraytype == 'nested') {
408 $children_array[] = array('object' => $cat);
409 } else {
410 $children_array[] = $cat;
411 }
412 }
7c8a963f 413 }
27f95e9b 414 }
415 } else {
416 return null;
417 }
418
419 return $children_array;
420 }
421
422 /**
423 * Given an array of stdClass children of a certain $object_type, returns a flat or nested
424 * array of these children, ready for appending to a tree built by get_children.
425 * @static
426 * @param array $children
427 * @param string $arraytype
428 * @param string $object_type
429 * @return array
430 */
431 function children_to_array($children, $arraytype='nested', $object_type='grade_item') {
432 $children_array = array();
433
434 foreach ($children as $id => $child) {
435 if ($arraytype == 'nested') {
436 $children_array[] = array('object' => new $object_type($child, false));
437 } else {
438 $children_array[] = new $object_type($child);
439 }
440 }
7c8a963f 441
27f95e9b 442 return $children_array;
443 }
444
445 /**
446 * Returns true if this category has any child grade_category or grade_item.
447 * @return int number of direct children, or false if none found.
448 */
449 function has_children() {
450 return count_records('grade_categories', 'parent', $this->id) + count_records('grade_items', 'categoryid', $this->id);
451 }
452
453 /**
454 * This method checks whether an existing child exists for this
455 * category. If the new child is of a different type, the method will return false (not allowed).
456 * Otherwise it will return true.
457 * @param object $child This must be a complete object, not a stdClass
458 * @return boolean Success or failure
459 */
460 function can_add_child($child) {
461 if ($this->has_children()) {
462 if (get_class($child) != $this->get_childrentype()) {
463 return false;
464 } else {
465 return true;
466 }
467 } else {
468 return true;
469 }
470 }
471
472 /**
473 * Check the type of the first child of this category, to see whether it is a
474 * grade_category or a grade_item, and returns that type as a string (get_class).
475 * @return string
476 */
477 function get_childrentype() {
478 $children = $this->children;
479 if (empty($this->children)) {
480 $count_item_children = count_records('grade_items', 'categoryid', $this->id);
481 $count_cat_children = count_records('grade_categories', 'parent', $this->id);
f151b073 482
27f95e9b 483 if ($count_item_children > 0) {
484 return 'grade_item';
485 } elseif ($count_cat_children > 0) {
486 return 'grade_category';
487 } else {
488 return null;
7c8a963f 489 }
7c8a963f 490 }
27f95e9b 491 return get_class($children[0]);
7c8a963f 492 }
f151b073 493
494 /**
495 * Retrieves from DB, instantiates and saves the associated grade_item object.
496 * @return object Grade_item
497 */
498 function load_grade_item() {
499 $params = get_record('grade_items', 'categoryid', $this->id, 'itemtype', 'category');
500 $this->grade_item = new grade_item($params);
2c72af1f 501
502 // If the associated grade_item isn't yet created, do it now
503 if (empty($this->grade_item->id)) {
504 $this->grade_item->iteminstance = $this->id;
505 $this->grade_item->itemtype = 'category';
506 $this->grade_item->insert();
507 $this->grade_item->update_from_db();
508 }
509
f151b073 510 return $this->grade_item;
511 }
8a31e65c 512}
513
514?>