weekly release 2.3dev
[moodle.git] / lib / grade / grade_category.php
CommitLineData
b79fe189 1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17/**
18 * Definitions of constants for gradebook
19 *
7ad5a627 20 * @package core
b79fe189 21 * @subpackage grade
22 * @copyright 2006 Nicolas Connault
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
7ad5a627
PS
25
26defined('MOODLE_INTERNAL') || die();
27
8a31e65c 28require_once('grade_object.php');
29
b79fe189 30/**
31 * Grade_Category is an object mapped to DB table {prefix}grade_categories
32 *
33 * @package moodlecore
34 * @subpackage grade
35 * @copyright 2007 Nicolas Connault
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
3058964f 38class grade_category extends grade_object {
8a31e65c 39 /**
7c8a963f 40 * The DB table.
8a31e65c 41 * @var string $table
42 */
da3801e8 43 public $table = 'grade_categories';
4a490db0 44
8a31e65c 45 /**
3f2b0c8a 46 * Array of required table fields, must start with 'id'.
47 * @var array $required_fields
8a31e65c 48 */
da3801e8 49 public $required_fields = array('id', 'courseid', 'parent', 'depth', 'path', 'fullname', 'aggregation',
3f2b0c8a 50 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes',
a25bb902 51 'aggregatesubcats', 'timecreated', 'timemodified', 'hidden');
4a490db0 52
8a31e65c 53 /**
54 * The course this category belongs to.
55 * @var int $courseid
56 */
da3801e8 57 public $courseid;
4a490db0 58
8a31e65c 59 /**
60 * The category this category belongs to (optional).
4a490db0 61 * @var int $parent
8a31e65c 62 */
da3801e8 63 public $parent;
4a490db0 64
8c846243 65 /**
66 * The grade_category object referenced by $this->parent (PK).
67 * @var object $parent_category
68 */
da3801e8 69 public $parent_category;
27f95e9b 70
e5c674f1 71 /**
72 * The number of parents this category has.
73 * @var int $depth
74 */
da3801e8 75 public $depth = 0;
e5c674f1 76
77 /**
c2efb501 78 * Shows the hierarchical path for this category as /1/2/3/ (like course_categories), the last number being
e5c674f1 79 * this category's autoincrement ID number.
80 * @var string $path
81 */
da3801e8 82 public $path;
e5c674f1 83
8a31e65c 84 /**
85 * The name of this category.
86 * @var string $fullname
87 */
da3801e8 88 public $fullname;
4a490db0 89
8a31e65c 90 /**
91 * A constant pointing to one of the predefined aggregation strategies (none, mean, median, sum etc) .
4a490db0 92 * @var int $aggregation
8a31e65c 93 */
da3801e8 94 public $aggregation = GRADE_AGGREGATE_MEAN;
4a490db0 95
8a31e65c 96 /**
97 * Keep only the X highest items.
98 * @var int $keephigh
99 */
da3801e8 100 public $keephigh = 0;
4a490db0 101
8a31e65c 102 /**
103 * Drop the X lowest items.
104 * @var int $droplow
105 */
da3801e8 106 public $droplow = 0;
4a490db0 107
c2efb501 108 /**
109 * Aggregate only graded items
110 * @var int $aggregateonlygraded
111 */
da3801e8 112 public $aggregateonlygraded = 0;
c2efb501 113
29d509f5 114 /**
115 * Aggregate outcomes together with normal items
c2efb501 116 * @var int $aggregateoutcomes
29d509f5 117 */
da3801e8 118 public $aggregateoutcomes = 0;
29d509f5 119
c2efb501 120 /**
121 * Ignore subcategories when aggregating
122 * @var int $aggregatesubcats
123 */
da3801e8 124 public $aggregatesubcats = 0;
c2efb501 125
8a31e65c 126 /**
127 * Array of grade_items or grade_categories nested exactly 1 level below this category
128 * @var array $children
129 */
da3801e8 130 public $children;
8a31e65c 131
7c8a963f 132 /**
4a490db0 133 * A hierarchical array of all children below this category. This is stored separately from
7c8a963f 134 * $children because it is more memory-intensive and may not be used as often.
135 * @var array $all_children
136 */
da3801e8 137 public $all_children;
7c8a963f 138
f151b073 139 /**
140 * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
141 * for this category.
142 * @var object $grade_item
143 */
da3801e8 144 public $grade_item;
f151b073 145
b3ac6c3e 146 /**
147 * Temporary sortorder for speedup of children resorting
148 */
da3801e8 149 public $sortorder;
b3ac6c3e 150
89a5f827 151 /**
152 * List of options which can be "forced" from site settings.
153 */
da3801e8 154 public $forceable = array('aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 'aggregatesubcats');
89a5f827 155
653a8648 156 /**
157 * String representing the aggregation coefficient. Variable is used as cache.
158 */
b79fe189 159 public $coefstring = null;
653a8648 160
e5c674f1 161 /**
162 * Builds this category's path string based on its parents (if any) and its own id number.
163 * This is typically done just before inserting this object in the DB for the first time,
ce385eb4 164 * or when a new parent is added or changed. It is a recursive function: once the calling
165 * object no longer has a parent, the path is complete.
166 *
b79fe189 167 * @param object $grade_category A Grade_Category object
ce385eb4 168 * @return int The depth of this category (2 means there is one parent)
b79fe189 169 * @static
e5c674f1 170 */
22a9b6d8 171 public static function build_path($grade_category) {
da3801e8 172 global $DB;
173
ce385eb4 174 if (empty($grade_category->parent)) {
c2efb501 175 return '/'.$grade_category->id.'/';
b79fe189 176
ce385eb4 177 } else {
da3801e8 178 $parent = $DB->get_record('grade_categories', array('id' => $grade_category->parent));
9a68cffc 179 return grade_category::build_path($parent).$grade_category->id.'/';
ce385eb4 180 }
e5c674f1 181 }
182
8a31e65c 183 /**
f92dcad8 184 * Finds and returns a grade_category instance based on params.
61c33818 185 * @static
8a31e65c 186 *
f92dcad8 187 * @param array $params associative arrays varname=>value
188 * @return object grade_category instance or false if none found.
189 */
da3801e8 190 public static function fetch($params) {
f3ac8eb4 191 return grade_object::fetch_helper('grade_categories', 'grade_category', $params);
f92dcad8 192 }
193
194 /**
195 * Finds and returns all grade_category instances based on params.
196 * @static
197 *
198 * @param array $params associative arrays varname=>value
199 * @return array array of grade_category insatnces or false if none found.
200 */
da3801e8 201 public static function fetch_all($params) {
f3ac8eb4 202 return grade_object::fetch_all_helper('grade_categories', 'grade_category', $params);
ce385eb4 203 }
204
8f4a626d 205 /**
2cc4b0f9 206 * In addition to update() as defined in grade_object, call force_regrading of parent categories, if applicable.
aaff71da 207 * @param string $source from where was the object updated (mod/forum, manual, etc.)
208 * @return boolean success
8f4a626d 209 */
da3801e8 210 public function update($source=null) {
b3ac6c3e 211 // load the grade item or create a new one
212 $this->load_grade_item();
213
214 // force recalculation of path;
215 if (empty($this->path)) {
9a68cffc 216 $this->path = grade_category::build_path($this);
c2efb501 217 $this->depth = substr_count($this->path, '/') - 1;
1909a127 218 $updatechildren = true;
b79fe189 219
1909a127 220 } else {
221 $updatechildren = false;
4a490db0 222 }
0fc7f624 223
89a5f827 224 $this->apply_forced_settings();
225
b79fe189 226 // these are exclusive
89a5f827 227 if ($this->droplow > 0) {
228 $this->keephigh = 0;
b79fe189 229
89a5f827 230 } else if ($this->keephigh > 0) {
231 $this->droplow = 0;
232 }
4a490db0 233
b3ac6c3e 234 // Recalculate grades if needed
235 if ($this->qualifies_for_regrading()) {
f8e6e4db 236 $this->force_regrading();
4a490db0 237 }
f8e6e4db 238
ced5ee59 239 $this->timemodified = time();
240
1909a127 241 $result = parent::update($source);
242
243 // now update paths in all child categories
244 if ($result and $updatechildren) {
b79fe189 245
1909a127 246 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
b79fe189 247
1909a127 248 foreach ($children as $child) {
1909a127 249 $child->path = null;
250 $child->depth = 0;
251 $child->update($source);
da3801e8 252 }
1909a127 253 }
254 }
255
256 return $result;
8f4a626d 257 }
4a490db0 258
8f4a626d 259 /**
2cc4b0f9 260 * If parent::delete() is successful, send force_regrading message to parent category.
aaff71da 261 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
262 * @return boolean success
8f4a626d 263 */
da3801e8 264 public function delete($source=null) {
f13002d5 265 $grade_item = $this->load_grade_item();
4a490db0 266
f615fbab 267 if ($this->is_course_category()) {
b79fe189 268
f3ac8eb4 269 if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) {
b79fe189 270
f615fbab 271 foreach ($categories as $category) {
b79fe189 272
f615fbab 273 if ($category->id == $this->id) {
274 continue; // do not delete course category yet
275 }
276 $category->delete($source);
277 }
aaff71da 278 }
2b0f65e2 279
f3ac8eb4 280 if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) {
b79fe189 281
f615fbab 282 foreach ($items as $item) {
b79fe189 283
f615fbab 284 if ($item->id == $grade_item->id) {
285 continue; // do not delete course item yet
286 }
287 $item->delete($source);
288 }
289 }
290
291 } else {
292 $this->force_regrading();
2b0f65e2 293
f615fbab 294 $parent = $this->load_parent_category();
2b0f65e2 295
f615fbab 296 // Update children's categoryid/parent field first
f3ac8eb4 297 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
f615fbab 298 foreach ($children as $child) {
299 $child->set_parent($parent->id);
300 }
301 }
b79fe189 302
f3ac8eb4 303 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
f615fbab 304 foreach ($children as $child) {
305 $child->set_parent($parent->id);
306 }
aaff71da 307 }
308 }
f13002d5 309
aaff71da 310 // first delete the attached grade item and grades
311 $grade_item->delete($source);
f13002d5 312
313 // delete category itself
aaff71da 314 return parent::delete($source);
8f4a626d 315 }
4a490db0 316
ce385eb4 317 /**
318 * In addition to the normal insert() defined in grade_object, this method sets the depth
319 * and path for this object, and update the record accordingly. The reason why this must
320 * be done here instead of in the constructor, is that they both need to know the record's
4a490db0 321 * id number, which only gets created at insertion time.
f151b073 322 * This method also creates an associated grade_item if this wasn't done during construction.
aaff71da 323 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
324 * @return int PK ID if successful, false otherwise
ce385eb4 325 */
da3801e8 326 public function insert($source=null) {
b3ac6c3e 327
328 if (empty($this->courseid)) {
2f137aa1 329 print_error('cannotinsertgrade');
b8ff92b6 330 }
4a490db0 331
b3ac6c3e 332 if (empty($this->parent)) {
f3ac8eb4 333 $course_category = grade_category::fetch_course_category($this->courseid);
b3ac6c3e 334 $this->parent = $course_category->id;
ce385eb4 335 }
4a490db0 336
b3ac6c3e 337 $this->path = null;
338
ced5ee59 339 $this->timecreated = $this->timemodified = time();
340
aaff71da 341 if (!parent::insert($source)) {
b3ac6c3e 342 debugging("Could not insert this category: " . print_r($this, true));
343 return false;
344 }
345
f8e6e4db 346 $this->force_regrading();
347
b3ac6c3e 348 // build path and depth
aaff71da 349 $this->update($source);
4a490db0 350
aaff71da 351 return $this->id;
b3ac6c3e 352 }
353
f2c88356 354 /**
f615fbab 355 * Internal function - used only from fetch_course_category()
356 * Normal insert() can not be used for course category
b79fe189 357 *
358 * @param int $courseid The course ID
359 *
f615fbab 360 * @return bool success
f2c88356 361 */
da3801e8 362 public function insert_course_category($courseid) {
1f0e4921 363 $this->courseid = $courseid;
8f6fdf43 364 $this->fullname = '?';
1f0e4921 365 $this->path = null;
366 $this->parent = null;
0c87b5aa 367 $this->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
4a490db0 368
190af29f 369 $this->apply_default_settings();
89a5f827 370 $this->apply_forced_settings();
371
ced5ee59 372 $this->timecreated = $this->timemodified = time();
373
aaff71da 374 if (!parent::insert('system')) {
b3ac6c3e 375 debugging("Could not insert this category: " . print_r($this, true));
376 return false;
f151b073 377 }
4a490db0 378
b3ac6c3e 379 // build path and depth
aaff71da 380 $this->update('system');
b3ac6c3e 381
aaff71da 382 return $this->id;
ce385eb4 383 }
4a490db0 384
8f4a626d 385 /**
386 * Compares the values held by this object with those of the matching record in DB, and returns
387 * whether or not these differences are sufficient to justify an update of all parent objects.
388 * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
389 * @return boolean
390 */
da3801e8 391 public function qualifies_for_regrading() {
8f4a626d 392 if (empty($this->id)) {
6639ead3 393 debugging("Can not regrade non existing category");
8f4a626d 394 return false;
395 }
f3ac8eb4 396
397 $db_item = grade_category::fetch(array('id'=>$this->id));
4a490db0 398
c2efb501 399 $aggregationdiff = $db_item->aggregation != $this->aggregation;
400 $keephighdiff = $db_item->keephigh != $this->keephigh;
401 $droplowdiff = $db_item->droplow != $this->droplow;
402 $aggonlygrddiff = $db_item->aggregateonlygraded != $this->aggregateonlygraded;
403 $aggoutcomesdiff = $db_item->aggregateoutcomes != $this->aggregateoutcomes;
404 $aggsubcatsdiff = $db_item->aggregatesubcats != $this->aggregatesubcats;
8f4a626d 405
c2efb501 406 return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff || $aggsubcatsdiff);
8f4a626d 407 }
8c846243 408
409 /**
f8e6e4db 410 * Marks the category and course item as needing update - categories are always regraded.
411 * @return void
8c846243 412 */
da3801e8 413 public function force_regrading() {
f8e6e4db 414 $grade_item = $this->load_grade_item();
415 $grade_item->force_regrading();
8c846243 416 }
417
0aa32279 418 /**
0758a08e 419 * Generates and saves final grades in associated category grade item.
1994d890 420 * These immediate children must already have their own final grades.
0758a08e 421 * The category's aggregation method is used to generate final grades.
ac9b0805 422 *
423 * Please note that category grade is either calculated or aggregated - not both at the same time.
424 *
c86caae7 425 * This method must be used ONLY from grade_item::regrade_final_grades(),
ac9b0805 426 * because the calculation must be done in correct order!
b8ff92b6 427 *
4a490db0 428 * Steps to follow:
ac9b0805 429 * 1. Get final grades from immediate children
2df71235 430 * 3. Aggregate these grades
0758a08e 431 * 4. Save them in final grades of associated category grade item
b79fe189 432 *
433 * @param int $userid The user ID
434 *
435 * @return bool
0aa32279 436 */
da3801e8 437 public function generate_grades($userid=null) {
438 global $CFG, $DB;
4a490db0 439
ac9b0805 440 $this->load_grade_item();
2cc4b0f9 441
442 if ($this->grade_item->is_locked()) {
443 return true; // no need to recalculate locked items
444 }
445
89a5f827 446 // find grade items of immediate children (category or grade items) and force site settings
61c33818 447 $depends_on = $this->grade_item->depends_on();
b3ac6c3e 448
f8e6e4db 449 if (empty($depends_on)) {
450 $items = false;
b79fe189 451
f8e6e4db 452 } else {
5b0af8c5 453 list($usql, $params) = $DB->get_in_or_equal($depends_on);
f8e6e4db 454 $sql = "SELECT *
5b0af8c5 455 FROM {grade_items}
456 WHERE id $usql";
457 $items = $DB->get_records_sql($sql, $params);
f8e6e4db 458 }
4a490db0 459
3a03653e 460 // needed mostly for SUM agg type
461 $this->auto_update_max($items);
462
5b0af8c5 463 $grade_inst = new grade_grade();
464 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
465
466 // where to look for final grades - include grade of this item too, we will store the results there
467 $gis = array_merge($depends_on, array($this->grade_item->id));
468 list($usql, $params) = $DB->get_in_or_equal($gis);
469
f8e6e4db 470 if ($userid) {
5b0af8c5 471 $usersql = "AND g.userid=?";
472 $params[] = $userid;
b79fe189 473
f8e6e4db 474 } else {
475 $usersql = "";
b8ff92b6 476 }
4a490db0 477
3f2b0c8a 478 $sql = "SELECT $fields
5b0af8c5 479 FROM {grade_grades} g, {grade_items} gi
480 WHERE gi.id = g.itemid AND gi.id $usql $usersql
ac9b0805 481 ORDER BY g.userid";
b8ff92b6 482
9580a21f 483 // group the results by userid and aggregate the grades for this user
1b42e677
EL
484 $rs = $DB->get_recordset_sql($sql, $params);
485 if ($rs->valid()) {
03cedd62 486 $prevuser = 0;
487 $grade_values = array();
488 $excluded = array();
489 $oldgrade = null;
b79fe189 490
da3801e8 491 foreach ($rs as $used) {
b79fe189 492
03cedd62 493 if ($used->userid != $prevuser) {
494 $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);
495 $prevuser = $used->userid;
496 $grade_values = array();
497 $excluded = array();
498 $oldgrade = null;
499 }
500 $grade_values[$used->itemid] = $used->finalgrade;
b79fe189 501
03cedd62 502 if ($used->excluded) {
503 $excluded[] = $used->itemid;
504 }
b79fe189 505
03cedd62 506 if ($this->grade_item->id == $used->itemid) {
507 $oldgrade = $used;
2df71235 508 }
b8ff92b6 509 }
03cedd62 510 $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);//the last one
b8ff92b6 511 }
1b42e677 512 $rs->close();
b8ff92b6 513
b8ff92b6 514 return true;
515 }
516
517 /**
ac9b0805 518 * internal function for category grades aggregation
ced5ee59 519 *
b79fe189 520 * @param int $userid The User ID
521 * @param array $items Grade items
522 * @param array $grade_values Array of grade values
523 * @param object $oldgrade Old grade
22a9b6d8 524 * @param array $excluded Excluded
b79fe189 525 *
1994d890 526 * @return boolean (just plain return;)
b79fe189 527 * @todo Document correctly
b8ff92b6 528 */
da3801e8 529 private function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) {
e171963b 530 global $CFG;
b8ff92b6 531 if (empty($userid)) {
f8e6e4db 532 //ignore first call
b8ff92b6 533 return;
534 }
4a490db0 535
f8e6e4db 536 if ($oldgrade) {
66690b69 537 $oldfinalgrade = $oldgrade->finalgrade;
f3ac8eb4 538 $grade = new grade_grade($oldgrade, false);
f8e6e4db 539 $grade->grade_item =& $this->grade_item;
b8ff92b6 540
f8e6e4db 541 } else {
542 // insert final grade - it will be needed later anyway
f3ac8eb4 543 $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
f8e6e4db 544 $grade->grade_item =& $this->grade_item;
f1ad9e04 545 $grade->insert('system');
546 $oldfinalgrade = null;
f8e6e4db 547 }
f1ad9e04 548
c86caae7 549 // no need to recalculate locked or overridden grades
550 if ($grade->is_locked() or $grade->is_overridden()) {
2cc4b0f9 551 return;
ac9b0805 552 }
553
f8e6e4db 554 // can not use own final category grade in calculation
9580a21f 555 unset($grade_values[$this->grade_item->id]);
f8e6e4db 556
f1ad9e04 557
b79fe189 558 // sum is a special aggregation types - it adjusts the min max, does not use relative values
f1ad9e04 559 if ($this->aggregation == GRADE_AGGREGATE_SUM) {
560 $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded);
561 return;
562 }
563
0758a08e 564 // if no grades calculation possible or grading not allowed clear final grade
9580a21f 565 if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
f8e6e4db 566 $grade->finalgrade = null;
b79fe189 567
66690b69 568 if (!is_null($oldfinalgrade)) {
0758a08e 569 $grade->update('aggregation');
f8e6e4db 570 }
b8ff92b6 571 return;
572 }
0758a08e 573
b79fe189 574 // normalize the grades first - all will have value 0...1
d5f0aa01 575 // ungraded items are not used in aggregation
23207a1a 576 foreach ($grade_values as $itemid=>$v) {
b79fe189 577
b8ff92b6 578 if (is_null($v)) {
579 // null means no grade
23207a1a 580 unset($grade_values[$itemid]);
581 continue;
b79fe189 582
23207a1a 583 } else if (in_array($itemid, $excluded)) {
584 unset($grade_values[$itemid]);
b8ff92b6 585 continue;
0aa32279 586 }
9a68cffc 587 $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1);
0aa32279 588 }
dda0c7e6 589
eacd3700 590 // use min grade if grade missing for these types
c2efb501 591 if (!$this->aggregateonlygraded) {
b79fe189 592
593 foreach ($items as $itemid=>$value) {
594
c2efb501 595 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
596 $grade_values[$itemid] = 0;
eacd3700 597 }
c2efb501 598 }
eacd3700 599 }
600
601 // limit and sort
a9e38ac8 602 $this->apply_limit_rules($grade_values, $items);
9580a21f 603 asort($grade_values, SORT_NUMERIC);
4a490db0 604
d5f0aa01 605 // let's see we have still enough grades to do any statistics
9580a21f 606 if (count($grade_values) == 0) {
ac9b0805 607 // not enough attempts yet
f8e6e4db 608 $grade->finalgrade = null;
b79fe189 609
66690b69 610 if (!is_null($oldfinalgrade)) {
0758a08e 611 $grade->update('aggregation');
b8ff92b6 612 }
613 return;
614 }
2df71235 615
d297269d 616 // do the maths
617 $agg_grade = $this->aggregate_values($grade_values, $items);
618
0758a08e 619 // recalculate the grade back to requested range
9a68cffc 620 $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax);
d297269d 621
653a8648 622 $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade);
d297269d 623
624 // update in db if changed
25bcd908 625 if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
0758a08e 626 $grade->update('aggregation');
d297269d 627 }
628
629 return;
630 }
631
89a5f827 632 /**
633 * Internal function - aggregation maths.
54c4a2cb 634 * Must be public: used by grade_grade::get_hiding_affected()
b79fe189 635 *
636 * @param array $grade_values The values being aggregated
637 * @param array $items The array of grade_items
638 *
639 * @return float
89a5f827 640 */
54c4a2cb 641 public function aggregate_values($grade_values, $items) {
b8ff92b6 642 switch ($this->aggregation) {
b79fe189 643
c2efb501 644 case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
9580a21f 645 $num = count($grade_values);
c186c7b2 646 $grades = array_values($grade_values);
b79fe189 647
c186c7b2 648 if ($num % 2 == 0) {
9c8d38fa 649 $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2;
b79fe189 650
b8ff92b6 651 } else {
9c8d38fa 652 $agg_grade = $grades[intval(($num/2)-0.5)];
b8ff92b6 653 }
654 break;
ac9b0805 655
c2efb501 656 case GRADE_AGGREGATE_MIN:
9c8d38fa 657 $agg_grade = reset($grade_values);
b8ff92b6 658 break;
659
c2efb501 660 case GRADE_AGGREGATE_MAX:
9c8d38fa 661 $agg_grade = array_pop($grade_values);
b8ff92b6 662 break;
663
c2efb501 664 case GRADE_AGGREGATE_MODE: // the most common value, average used if multimode
0198929c 665 // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string
666 $converted_grade_values = array();
667
668 foreach ($grade_values as $k => $gv) {
b79fe189 669
0198929c 670 if (!is_int($gv) && !is_string($gv)) {
671 $converted_grade_values[$k] = (string) $gv;
b79fe189 672
0198929c 673 } else {
674 $converted_grade_values[$k] = $gv;
675 }
676 }
677
678 $freq = array_count_values($converted_grade_values);
95affb8a 679 arsort($freq); // sort by frequency keeping keys
680 $top = reset($freq); // highest frequency count
681 $modes = array_keys($freq, $top); // search for all modes (have the same highest count)
f7d515b6 682 rsort($modes, SORT_NUMERIC); // get highest mode
9c8d38fa 683 $agg_grade = reset($modes);
d5fab31f 684 break;
95affb8a 685
1426edac 686 case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef
9580a21f 687 $weightsum = 0;
688 $sum = 0;
b79fe189 689
690 foreach ($grade_values as $itemid=>$grade_value) {
691
eacd3700 692 if ($items[$itemid]->aggregationcoef <= 0) {
9580a21f 693 continue;
694 }
eacd3700 695 $weightsum += $items[$itemid]->aggregationcoef;
696 $sum += $items[$itemid]->aggregationcoef * $grade_value;
9580a21f 697 }
b79fe189 698
9580a21f 699 if ($weightsum == 0) {
9c8d38fa 700 $agg_grade = null;
b79fe189 701
9580a21f 702 } else {
9c8d38fa 703 $agg_grade = $sum / $weightsum;
9580a21f 704 }
705 break;
706
d9ae2ab5 707 case GRADE_AGGREGATE_WEIGHTED_MEAN2:
708 // Weighted average of all existing final grades with optional extra credit flag,
f7d515b6 709 // weight is the range of grade (usually grademax)
1426edac 710 $weightsum = 0;
d9ae2ab5 711 $sum = null;
b79fe189 712
713 foreach ($grade_values as $itemid=>$grade_value) {
1426edac 714 $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
b79fe189 715
1426edac 716 if ($weight <= 0) {
717 continue;
718 }
b79fe189 719
d9ae2ab5 720 if ($items[$itemid]->aggregationcoef == 0) {
721 $weightsum += $weight;
722 }
723 $sum += $weight * $grade_value;
1426edac 724 }
b79fe189 725
1426edac 726 if ($weightsum == 0) {
d9ae2ab5 727 $agg_grade = $sum; // only extra credits
b79fe189 728
1426edac 729 } else {
730 $agg_grade = $sum / $weightsum;
731 }
732 break;
733
c2efb501 734 case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
9580a21f 735 $num = 0;
d9ae2ab5 736 $sum = null;
b79fe189 737
738 foreach ($grade_values as $itemid=>$grade_value) {
739
eacd3700 740 if ($items[$itemid]->aggregationcoef == 0) {
9580a21f 741 $num += 1;
742 $sum += $grade_value;
b79fe189 743
eacd3700 744 } else if ($items[$itemid]->aggregationcoef > 0) {
745 $sum += $items[$itemid]->aggregationcoef * $grade_value;
9580a21f 746 }
747 }
b79fe189 748
9580a21f 749 if ($num == 0) {
9c8d38fa 750 $agg_grade = $sum; // only extra credits or wrong coefs
b79fe189 751
9580a21f 752 } else {
9c8d38fa 753 $agg_grade = $sum / $num;
9580a21f 754 }
755 break;
756
c2efb501 757 case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
ac9b0805 758 default:
9580a21f 759 $num = count($grade_values);
760 $sum = array_sum($grade_values);
9c8d38fa 761 $agg_grade = $sum / $num;
b8ff92b6 762 break;
763 }
764
d297269d 765 return $agg_grade;
0aa32279 766 }
0758a08e 767
3a03653e 768 /**
c1024411 769 * Some aggregation types may update max grade
3a03653e 770 * @param array $items sub items
771 * @return void
772 */
773 private function auto_update_max($items) {
774 if ($this->aggregation != GRADE_AGGREGATE_SUM) {
775 // not needed at all
776 return;
777 }
778
779 if (!$items) {
b79fe189 780
3a03653e 781 if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
782 $this->grade_item->grademax = 0;
783 $this->grade_item->grademin = 0;
784 $this->grade_item->gradetype = GRADE_TYPE_VALUE;
785 $this->grade_item->update('aggregation');
786 }
787 return;
788 }
789
59080eee 790 //find max grade possible
791 $maxes = array();
b79fe189 792
3a03653e 793 foreach ($items as $item) {
b79fe189 794
3a03653e 795 if ($item->aggregationcoef > 0) {
796 // extra credit from this activity - does not affect total
797 continue;
798 }
b79fe189 799
3a03653e 800 if ($item->gradetype == GRADE_TYPE_VALUE) {
b9b199be 801 $maxes[$item->id] = $item->grademax;
b79fe189 802
3a03653e 803 } else if ($item->gradetype == GRADE_TYPE_SCALE) {
b9b199be 804 $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item
3a03653e 805 }
806 }
59080eee 807 // apply droplow and keephigh
a9e38ac8 808 $this->apply_limit_rules($maxes, $items);
59080eee 809 $max = array_sum($maxes);
3a03653e 810
59080eee 811 // update db if anything changed
812 if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
3a03653e 813 $this->grade_item->grademax = $max;
814 $this->grade_item->grademin = 0;
815 $this->grade_item->gradetype = GRADE_TYPE_VALUE;
816 $this->grade_item->update('aggregation');
817 }
818 }
819
0758a08e 820 /**
821 * internal function for category grades summing
822 *
22a9b6d8 823 * @param grade_grade &$grade The grade item
b79fe189 824 * @param float $oldfinalgrade Old Final grade?
825 * @param array $items Grade items
826 * @param array $grade_values Grade values
22a9b6d8 827 * @param array $excluded Excluded
b79fe189 828 *
0758a08e 829 * @return boolean (just plain return;)
830 */
da3801e8 831 private function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded) {
653a8648 832 if (empty($items)) {
833 return null;
834 }
835
c1024411 836 // ungraded and excluded items are not used in aggregation
0758a08e 837 foreach ($grade_values as $itemid=>$v) {
b79fe189 838
0758a08e 839 if (is_null($v)) {
840 unset($grade_values[$itemid]);
b79fe189 841
0758a08e 842 } else if (in_array($itemid, $excluded)) {
843 unset($grade_values[$itemid]);
844 }
845 }
846
d28f25a4 847 // use 0 if grade missing, droplow used and aggregating all items
848 if (!$this->aggregateonlygraded and !empty($this->droplow)) {
b79fe189 849
850 foreach ($items as $itemid=>$value) {
851
d28f25a4 852 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
853 $grade_values[$itemid] = 0;
854 }
855 }
856 }
857
a9e38ac8 858 $this->apply_limit_rules($grade_values, $items);
0758a08e 859
860 $sum = array_sum($grade_values);
653a8648 861 $grade->finalgrade = $this->grade_item->bounded_grade($sum);
0758a08e 862
863 // update in db if changed
25bcd908 864 if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
0758a08e 865 $grade->update('aggregation');
866 }
867
868 return;
869 }
870
adc2f286 871 /**
872 * Given an array of grade values (numerical indices), applies droplow or keephigh
873 * rules to limit the final array.
b79fe189 874 *
875 * @param array &$grade_values itemid=>$grade_value float
876 * @param array $items grade item objects
877 *
adc2f286 878 * @return array Limited grades.
879 */
a9e38ac8 880 public function apply_limit_rules(&$grade_values, $items) {
881 $extraused = $this->is_extracredit_used();
882
adc2f286 883 if (!empty($this->droplow)) {
a9e38ac8 884 asort($grade_values, SORT_NUMERIC);
885 $dropped = 0;
b79fe189 886
a9e38ac8 887 foreach ($grade_values as $itemid=>$value) {
b79fe189 888
a9e38ac8 889 if ($dropped < $this->droplow) {
b79fe189 890
a9e38ac8 891 if ($extraused and $items[$itemid]->aggregationcoef > 0) {
892 // no drop low for extra credits
b79fe189 893
a9e38ac8 894 } else {
895 unset($grade_values[$itemid]);
896 $dropped++;
897 }
b79fe189 898
a9e38ac8 899 } else {
900 // we have dropped enough
901 break;
59080eee 902 }
adc2f286 903 }
a9e38ac8 904
905 } else if (!empty($this->keephigh)) {
906 arsort($grade_values, SORT_NUMERIC);
907 $kept = 0;
b79fe189 908
a9e38ac8 909 foreach ($grade_values as $itemid=>$value) {
b79fe189 910
a9e38ac8 911 if ($extraused and $items[$itemid]->aggregationcoef > 0) {
912 // we keep all extra credits
b79fe189 913
a9e38ac8 914 } else if ($kept < $this->keephigh) {
915 $kept++;
b79fe189 916
a9e38ac8 917 } else {
918 unset($grade_values[$itemid]);
919 }
adc2f286 920 }
921 }
0aa32279 922 }
923
793253ae 924 /**
925 * Returns true if category uses extra credit of any kind
b79fe189 926 *
793253ae 927 * @return boolean true if extra credit used
928 */
929 function is_extracredit_used() {
930 return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
931 or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
932 or $this->aggregation == GRADE_AGGREGATE_SUM);
933 }
934
9580a21f 935 /**
f7d515b6 936 * Returns true if category uses special aggregation coefficient
b79fe189 937 *
f7d515b6 938 * @return boolean true if coefficient used
9580a21f 939 */
4e9ca991 940 public function is_aggregationcoef_used() {
c2efb501 941 return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN
d9ae2ab5 942 or $this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
2e0d37fe 943 or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
944 or $this->aggregation == GRADE_AGGREGATE_SUM);
ba74762b 945
9580a21f 946 }
947
653a8648 948 /**
949 * Recursive function to find which weight/extra credit field to use in the grade item form. Inherits from a parent category
950 * if that category has aggregatesubcats set to true.
b79fe189 951 *
952 * @param string $first Whether or not this is the first item in the recursion
953 *
954 * @return string
653a8648 955 */
956 public function get_coefstring($first=true) {
957 if (!is_null($this->coefstring)) {
958 return $this->coefstring;
959 }
960
961 $overriding_coefstring = null;
962
963 // Stop recursing upwards if this category aggregates subcats or has no parent
964 if (!$first && !$this->aggregatesubcats) {
b79fe189 965
121d8006 966 if ($parent_category = $this->load_parent_category()) {
653a8648 967 return $parent_category->get_coefstring(false);
b79fe189 968
653a8648 969 } else {
970 return null;
971 }
b79fe189 972
973 } else if ($first) {
974
653a8648 975 if (!$this->aggregatesubcats) {
b79fe189 976
121d8006 977 if ($parent_category = $this->load_parent_category()) {
653a8648 978 $overriding_coefstring = $parent_category->get_coefstring(false);
979 }
980 }
981 }
982
983 // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self.
984 if (!is_null($overriding_coefstring)) {
985 return $overriding_coefstring;
986 }
987
988 // No parent category is overriding this category's aggregation, return its string
989 if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
990 $this->coefstring = 'aggregationcoefweight';
b79fe189 991
d9ae2ab5 992 } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
993 $this->coefstring = 'aggregationcoefextrasum';
b79fe189 994
653a8648 995 } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
3869ab1a 996 $this->coefstring = 'aggregationcoefextraweight';
b79fe189 997
653a8648 998 } else if ($this->aggregation == GRADE_AGGREGATE_SUM) {
999 $this->coefstring = 'aggregationcoefextrasum';
b79fe189 1000
653a8648 1001 } else {
1002 $this->coefstring = 'aggregationcoef';
1003 }
1004 return $this->coefstring;
1005 }
1006
1c307f21 1007 /**
b3ac6c3e 1008 * Returns tree with all grade_items and categories as elements
b79fe189 1009 *
1010 * @param int $courseid The course ID
b3ac6c3e 1011 * @param boolean $include_category_items as category children
b79fe189 1012 *
b3ac6c3e 1013 * @return array
b79fe189 1014 * @static
1c307f21 1015 */
da3801e8 1016 public static function fetch_course_tree($courseid, $include_category_items=false) {
f3ac8eb4 1017 $course_category = grade_category::fetch_course_category($courseid);
514a3467 1018 $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
1019 'children'=>$course_category->get_children($include_category_items));
b146d984
AD
1020
1021 $course_category->sortorder = $course_category->get_sortorder();
1022 return grade_category::_fetch_course_tree_recursion($category_array, $course_category->get_sortorder());
1c307f21 1023 }
1024
b79fe189 1025 /**
1026 * Needs documenting
1027 *
1028 * @param array $category_array The seed of the recursion
1029 * @param int &$sortorder The current sortorder
1030 *
1031 * @return array
1032 * @static
1033 * @todo Document
1034 */
1035 static private function _fetch_course_tree_recursion($category_array, &$sortorder) {
b3ac6c3e 1036 // update the sortorder in db if needed
b146d984
AD
1037 //NOTE: This leads to us resetting sort orders every time the categories and items page is viewed :(
1038 //if ($category_array['object']->sortorder != $sortorder) {
1039 //$category_array['object']->set_sortorder($sortorder);
1040 //}
ce385eb4 1041
7bac3777
AD
1042 if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) {
1043 return null;
1044 }
1045
314c4336 1046 // store the grade_item or grade_category instance with extra info
1047 $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);
b3ac6c3e 1048
1049 // reuse final grades if there
1050 if (array_key_exists('finalgrades', $category_array)) {
1051 $result['finalgrades'] = $category_array['finalgrades'];
1052 }
1053
1054 // recursively resort children
1055 if (!empty($category_array['children'])) {
1056 $result['children'] = array();
29d509f5 1057 //process the category item first
7bac3777 1058 $child = null;
b79fe189 1059
1060 foreach ($category_array['children'] as $oldorder=>$child_array) {
1061
314c4336 1062 if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
7bac3777
AD
1063 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1064 if (!empty($child)) {
1065 $result['children'][$sortorder] = $child;
1066 }
29d509f5 1067 }
2b0f65e2 1068 }
b79fe189 1069
1070 foreach ($category_array['children'] as $oldorder=>$child_array) {
1071
29d509f5 1072 if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
7bac3777
AD
1073 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1074 if (!empty($child)) {
1075 $result['children'][++$sortorder] = $child;
1076 }
b3ac6c3e 1077 }
1078 }
1079 }
1080
1081 return $result;
ce385eb4 1082 }
7c8a963f 1083
1084 /**
4a490db0 1085 * Fetches and returns all the children categories and/or grade_items belonging to this category.
1086 * By default only returns the immediate children (depth=1), but deeper levels can be requested,
a39cac25 1087 * as well as all levels (0). The elements are indexed by sort order.
b79fe189 1088 *
1089 * @param bool $include_category_items Whether or not to include category grade_items in the children array
1090 *
7c8a963f 1091 * @return array Array of child objects (grade_category and grade_item).
1092 */
da3801e8 1093 public function get_children($include_category_items=false) {
1094 global $DB;
b3ac6c3e 1095
1096 // This function must be as fast as possible ;-)
1097 // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
1098 // we have to limit the number of queries though, because it will be used often in grade reports
1099
da3801e8 1100 $cats = $DB->get_records('grade_categories', array('courseid' => $this->courseid));
1101 $items = $DB->get_records('grade_items', array('courseid' => $this->courseid));
4a490db0 1102
b3ac6c3e 1103 // init children array first
1104 foreach ($cats as $catid=>$cat) {
1105 $cats[$catid]->children = array();
27f95e9b 1106 }
4a490db0 1107
b3ac6c3e 1108 //first attach items to cats and add category sortorder
1109 foreach ($items as $item) {
b79fe189 1110
b3ac6c3e 1111 if ($item->itemtype == 'course' or $item->itemtype == 'category') {
1112 $cats[$item->iteminstance]->sortorder = $item->sortorder;
4a490db0 1113
b3ac6c3e 1114 if (!$include_category_items) {
1115 continue;
1116 }
1117 $categoryid = $item->iteminstance;
b79fe189 1118
b3ac6c3e 1119 } else {
1120 $categoryid = $item->categoryid;
1121 }
1122
1123 // prevent problems with duplicate sortorders in db
1124 $sortorder = $item->sortorder;
b79fe189 1125
1126 while (array_key_exists($sortorder, $cats[$categoryid]->children)) {
f13002d5 1127 //debugging("$sortorder exists in item loop");
b3ac6c3e 1128 $sortorder++;
1129 }
1130
1131 $cats[$categoryid]->children[$sortorder] = $item;
1132
1133 }
1134
1135 // now find the requested category and connect categories as children
1136 $category = false;
b79fe189 1137
b3ac6c3e 1138 foreach ($cats as $catid=>$cat) {
b79fe189 1139
ec3717e1 1140 if (empty($cat->parent)) {
b79fe189 1141
ec3717e1 1142 if ($cat->path !== '/'.$cat->id.'/') {
1143 $grade_category = new grade_category($cat, false);
1144 $grade_category->path = '/'.$cat->id.'/';
1145 $grade_category->depth = 1;
1146 $grade_category->update('system');
1147 return $this->get_children($include_category_items);
1148 }
b79fe189 1149
ec3717e1 1150 } else {
b79fe189 1151
ec3717e1 1152 if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) {
1153 //fix paths and depts
1154 static $recursioncounter = 0; // prevents infinite recursion
1155 $recursioncounter++;
b79fe189 1156
da3801e8 1157 if ($recursioncounter < 5) {
ec3717e1 1158 // fix paths and depths!
1159 $grade_category = new grade_category($cat, false);
1160 $grade_category->depth = 0;
1161 $grade_category->path = null;
1162 $grade_category->update('system');
1163 return $this->get_children($include_category_items);
1164 }
da3801e8 1165 }
b3ac6c3e 1166 // prevent problems with duplicate sortorders in db
1167 $sortorder = $cat->sortorder;
b79fe189 1168
1169 while (array_key_exists($sortorder, $cats[$cat->parent]->children)) {
f13002d5 1170 //debugging("$sortorder exists in cat loop");
b3ac6c3e 1171 $sortorder++;
1172 }
1173
65370356 1174 $cats[$cat->parent]->children[$sortorder] = &$cats[$catid];
b3ac6c3e 1175 }
f3ac8eb4 1176
b3ac6c3e 1177 if ($catid == $this->id) {
1178 $category = &$cats[$catid];
1179 }
1180 }
1181
1182 unset($items); // not needed
1183 unset($cats); // not needed
1184
f3ac8eb4 1185 $children_array = grade_category::_get_children_recursion($category);
b3ac6c3e 1186
1187 ksort($children_array);
1188
1189 return $children_array;
1190
1191 }
1192
b79fe189 1193 /**
1194 * Private method used to retrieve all children of this category recursively
1195 *
1196 * @param grade_category $category Source of current recursion
1197 *
1198 * @return array
1199 */
22a9b6d8 1200 private static function _get_children_recursion($category) {
b3ac6c3e 1201
1202 $children_array = array();
b79fe189 1203 foreach ($category->children as $sortorder=>$child) {
1204
b3ac6c3e 1205 if (array_key_exists('itemtype', $child)) {
f3ac8eb4 1206 $grade_item = new grade_item($child, false);
b79fe189 1207
4faf5f99 1208 if (in_array($grade_item->itemtype, array('course', 'category'))) {
1209 $type = $grade_item->itemtype.'item';
1210 $depth = $category->depth;
b79fe189 1211
314c4336 1212 } else {
1213 $type = 'item';
1214 $depth = $category->depth; // we use this to set the same colour
b3ac6c3e 1215 }
4faf5f99 1216 $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);
4a490db0 1217
7c8a963f 1218 } else {
f3ac8eb4 1219 $children = grade_category::_get_children_recursion($child);
1220 $grade_category = new grade_category($child, false);
b79fe189 1221
b3ac6c3e 1222 if (empty($children)) {
314c4336 1223 $children = array();
7c8a963f 1224 }
4faf5f99 1225 $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
314c4336 1226 }
27f95e9b 1227 }
1228
b3ac6c3e 1229 // sort the array
1230 ksort($children_array);
1231
27f95e9b 1232 return $children_array;
1233 }
4a490db0 1234
f151b073 1235 /**
ab53054f 1236 * Uses get_grade_item to load or create a grade_item, then saves it as $this->grade_item.
f151b073 1237 * @return object Grade_item
1238 */
da3801e8 1239 public function load_grade_item() {
ac9b0805 1240 if (empty($this->grade_item)) {
1241 $this->grade_item = $this->get_grade_item();
1242 }
ab53054f 1243 return $this->grade_item;
1244 }
4a490db0 1245
ab53054f 1246 /**
1247 * Retrieves from DB and instantiates the associated grade_item object.
1248 * If no grade_item exists yet, create one.
1249 * @return object Grade_item
1250 */
da3801e8 1251 public function get_grade_item() {
c91ed4be 1252 if (empty($this->id)) {
1253 debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
1254 return false;
1255 }
1256
b3ac6c3e 1257 if (empty($this->parent)) {
1258 $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);
1259
1260 } else {
1261 $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
1262 }
4ac209d5 1263
f3ac8eb4 1264 if (!$grade_items = grade_item::fetch_all($params)) {
b8ff92b6 1265 // create a new one
f3ac8eb4 1266 $grade_item = new grade_item($params, false);
b8ff92b6 1267 $grade_item->gradetype = GRADE_TYPE_VALUE;
f8e6e4db 1268 $grade_item->insert('system');
4a490db0 1269
b79fe189 1270 } else if (count($grade_items) == 1) {
b8ff92b6 1271 // found existing one
1272 $grade_item = reset($grade_items);
4a490db0 1273
b8ff92b6 1274 } else {
1275 debugging("Found more than one grade_item attached to category id:".$this->id);
ac9b0805 1276 // return first one
1277 $grade_item = reset($grade_items);
2c72af1f 1278 }
1279
ab53054f 1280 return $grade_item;
f151b073 1281 }
8c846243 1282
1283 /**
1284 * Uses $this->parent to instantiate $this->parent_category based on the
1285 * referenced record in the DB.
1286 * @return object Parent_category
1287 */
da3801e8 1288 public function load_parent_category() {
8c846243 1289 if (empty($this->parent_category) && !empty($this->parent)) {
ab53054f 1290 $this->parent_category = $this->get_parent_category();
8c846243 1291 }
1292 return $this->parent_category;
4a490db0 1293 }
1294
ab53054f 1295 /**
1296 * Uses $this->parent to instantiate and return a grade_category object.
1297 * @return object Parent_category
1298 */
da3801e8 1299 public function get_parent_category() {
ab53054f 1300 if (!empty($this->parent)) {
f3ac8eb4 1301 $parent_category = new grade_category(array('id' => $this->parent));
4a490db0 1302 return $parent_category;
ab53054f 1303 } else {
1304 return null;
1305 }
1306 }
1307
2186f72c 1308 /**
4a490db0 1309 * Returns the most descriptive field for this object. This is a standard method used
2186f72c 1310 * when we do not know the exact type of an object.
b79fe189 1311 *
2186f72c 1312 * @return string name
1313 */
da3801e8 1314 public function get_name() {
1315 global $DB;
8f6fdf43 1316 // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form)
1317 if (empty($this->parent) && $this->fullname == '?') {
da3801e8 1318 $course = $DB->get_record('course', array('id'=> $this->courseid));
410753fb 1319 return format_string($course->fullname);
b79fe189 1320
314c4336 1321 } else {
1322 return $this->fullname;
1323 }
2186f72c 1324 }
c91ed4be 1325
0fc7f624 1326 /**
1327 * Sets this category's parent id. A generic method shared by objects that have a parent id of some kind.
b79fe189 1328 *
1329 * @param int $parentid The ID of the category parent to $this
1330 * @param grade_category $source An optional grade_category to use as the source for the parent
1331 *
f13002d5 1332 * @return boolean success
0fc7f624 1333 */
da3801e8 1334 public function set_parent($parentid, $source=null) {
f13002d5 1335 if ($this->parent == $parentid) {
1336 return true;
1337 }
1338
1339 if ($parentid == $this->id) {
2f137aa1 1340 print_error('cannotassignselfasparent');
f13002d5 1341 }
1342
1343 if (empty($this->parent) and $this->is_course_category()) {
2f137aa1 1344 print_error('cannothaveparentcate');
b3ac6c3e 1345 }
f13002d5 1346
1347 // find parent and check course id
f3ac8eb4 1348 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
b3ac6c3e 1349 return false;
1350 }
1351
f8e6e4db 1352 $this->force_regrading();
b3ac6c3e 1353
1354 // set new parent category
f8e6e4db 1355 $this->parent = $parent_category->id;
1356 $this->parent_category =& $parent_category;
b3ac6c3e 1357 $this->path = null; // remove old path and depth - will be recalculated in update()
ec3717e1 1358 $this->depth = 0; // remove old path and depth - will be recalculated in update()
f8e6e4db 1359 $this->update($source);
b3ac6c3e 1360
15b462da 1361 return $this->update($source);
b3ac6c3e 1362 }
1363
1364 /**
1365 * Returns the final values for this grade category.
b79fe189 1366 *
b3ac6c3e 1367 * @param int $userid Optional: to retrieve a single final grade
b79fe189 1368 *
b3ac6c3e 1369 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
1370 */
b79fe189 1371 public function get_final($userid=null) {
b3ac6c3e 1372 $this->load_grade_item();
1373 return $this->grade_item->get_final($userid);
0fc7f624 1374 }
4a490db0 1375
0fc7f624 1376 /**
4a490db0 1377 * Returns the sortorder of the associated grade_item. This method is also available in
5fad5061 1378 * grade_item, for cases where the object type is not known.
b79fe189 1379 *
0fc7f624 1380 * @return int Sort order
1381 */
da3801e8 1382 public function get_sortorder() {
b3ac6c3e 1383 $this->load_grade_item();
1384 return $this->grade_item->get_sortorder();
0fc7f624 1385 }
1386
be7c0693 1387 /**
1388 * Returns the idnumber of the associated grade_item. This method is also available in
1389 * grade_item, for cases where the object type is not known.
b79fe189 1390 *
be7c0693 1391 * @return string idnumber
1392 */
da3801e8 1393 public function get_idnumber() {
be7c0693 1394 $this->load_grade_item();
1395 return $this->grade_item->get_idnumber();
1396 }
1397
0fc7f624 1398 /**
b3ac6c3e 1399 * Sets sortorder variable for this category.
4a490db0 1400 * This method is also available in grade_item, for cases where the object type is not know.
b79fe189 1401 *
1402 * @param int $sortorder The sortorder to assign to this category
1403 *
0fc7f624 1404 * @return void
1405 */
da3801e8 1406 public function set_sortorder($sortorder) {
b3ac6c3e 1407 $this->load_grade_item();
1408 $this->grade_item->set_sortorder($sortorder);
1409 }
1410
6639ead3 1411 /**
1412 * Move this category after the given sortorder - does not change the parent
b79fe189 1413 *
1414 * @param int $sortorder to place after.
1415 *
1416 * @return void
6639ead3 1417 */
da3801e8 1418 public function move_after_sortorder($sortorder) {
f13002d5 1419 $this->load_grade_item();
1420 $this->grade_item->move_after_sortorder($sortorder);
1421 }
1422
b3ac6c3e 1423 /**
f13002d5 1424 * Return true if this is the top most category that represents the total course grade.
b79fe189 1425 *
b3ac6c3e 1426 * @return boolean
1427 */
da3801e8 1428 public function is_course_category() {
b3ac6c3e 1429 $this->load_grade_item();
1430 return $this->grade_item->is_course_item();
1431 }
1432
1433 /**
1434 * Return the top most course category.
b79fe189 1435 *
1436 * @param int $courseid The Course ID
1437 *
b3ac6c3e 1438 * @return object grade_category instance for course grade
b79fe189 1439 * @static
b3ac6c3e 1440 */
22a9b6d8 1441 public static function fetch_course_category($courseid) {
a4503119 1442 if (empty($courseid)) {
1443 debugging('Missing course id!');
1444 return false;
1445 }
b3ac6c3e 1446
1447 // course category has no parent
f3ac8eb4 1448 if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
b3ac6c3e 1449 return $course_category;
1450 }
1451
1452 // create a new one
f3ac8eb4 1453 $course_category = new grade_category();
b3ac6c3e 1454 $course_category->insert_course_category($courseid);
1455
1456 return $course_category;
0fc7f624 1457 }
4ac209d5 1458
79eabc2a 1459 /**
1460 * Is grading object editable?
b79fe189 1461 *
79eabc2a 1462 * @return boolean
1463 */
da3801e8 1464 public function is_editable() {
79eabc2a 1465 return true;
1466 }
1467
5fad5061 1468 /**
4a490db0 1469 * Returns the locked state/date of the associated grade_item. This method is also available in
1470 * grade_item, for cases where the object type is not known.
22e23c78 1471 * @return boolean
5fad5061 1472 */
da3801e8 1473 public function is_locked() {
5fad5061 1474 $this->load_grade_item();
22e23c78 1475 return $this->grade_item->is_locked();
5fad5061 1476 }
1477
1478 /**
1479 * Sets the grade_item's locked variable and updates the grade_item.
1480 * Method named after grade_item::set_locked().
b79fe189 1481 *
1482 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
1483 * @param bool $cascade lock/unlock child objects too
1484 * @param bool $refresh refresh grades when unlocking
1485 *
9580a21f 1486 * @return boolean success if category locked (not all children mayb be locked though)
5fad5061 1487 */
da3801e8 1488 public function set_locked($lockedstate, $cascade=false, $refresh=true) {
5fad5061 1489 $this->load_grade_item();
2b0f65e2 1490
fb0e3570 1491 $result = $this->grade_item->set_locked($lockedstate, $cascade, true);
1492
1493 if ($cascade) {
1494 //process all children - items and categories
f3ac8eb4 1495 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
b79fe189 1496
1497 foreach ($children as $child) {
fb0e3570 1498 $child->set_locked($lockedstate, true, false);
b79fe189 1499
fb0e3570 1500 if (empty($lockedstate) and $refresh) {
1501 //refresh when unlocking
1502 $child->refresh_grades();
1503 }
2b0f65e2 1504 }
7a7a53d3 1505 }
b79fe189 1506
f3ac8eb4 1507 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
b79fe189 1508
1509 foreach ($children as $child) {
fb0e3570 1510 $child->set_locked($lockedstate, true, true);
1511 }
7a7a53d3 1512 }
1513 }
2b0f65e2 1514
b121b544 1515 return $result;
5fad5061 1516 }
4a490db0 1517
79b260cc
AD
1518 public static function set_properties(&$instance, $params) {
1519 global $DB;
1520
1521 parent::set_properties($instance, $params);
1522
c1024411 1523 //if they've changed aggregation type we made need to do some fiddling to provide appropriate defaults
79b260cc
AD
1524 if (!empty($params->aggregation)) {
1525
1526 //weight and extra credit share a column :( Would like a default of 1 for weight and 0 for extra credit
1527 //Flip from the default of 0 to 1 (or vice versa) if ALL items in the category are still set to the old default.
1528 if ($params->aggregation==GRADE_AGGREGATE_WEIGHTED_MEAN || $params->aggregation==GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1529 $sql = $defaultaggregationcoef = null;
7ad5a627 1530
79b260cc
AD
1531 if ($params->aggregation==GRADE_AGGREGATE_WEIGHTED_MEAN) {
1532 //if all items in this category have aggregation coefficient of 0 we can change it to 1 ie evenly weighted
1533 $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=0";
1534 $defaultaggregationcoef = 1;
1535 } else if ($params->aggregation==GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
1536 //if all items in this category have aggregation coefficient of 1 we can change it to 0 ie no extra credit
1537 $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=1";
1538 $defaultaggregationcoef = 0;
1539 }
1540
1541 $params = array('categoryid'=>$instance->id);
1542 $count = $DB->count_records_sql($sql, $params);
1543 if ($count===0) { //category is either empty or all items are set to a default value so we can switch defaults
1544 $params['aggregationcoef'] = $defaultaggregationcoef;
1545 $DB->execute("update {grade_items} set aggregationcoef=:aggregationcoef where categoryid=:categoryid",$params);
1546 }
1547 }
1548 }
1549 }
1550
5fad5061 1551 /**
4a490db0 1552 * Sets the grade_item's hidden variable and updates the grade_item.
5fad5061 1553 * Method named after grade_item::set_hidden().
1554 * @param int $hidden 0, 1 or a timestamp int(10) after which date the item will be hidden.
f60c61b1 1555 * @param boolean $cascade apply to child objects too
5fad5061 1556 * @return void
1557 */
da3801e8 1558 public function set_hidden($hidden, $cascade=false) {
5fad5061 1559 $this->load_grade_item();
a25bb902 1560 //this hides the associated grade item (the course total)
1762a264 1561 $this->grade_item->set_hidden($hidden, $cascade);
a25bb902
AD
1562 //this hides the category itself and everything it contains
1563 parent::set_hidden($hidden, $cascade);
b79fe189 1564
f60c61b1 1565 if ($cascade) {
b79fe189 1566
f3ac8eb4 1567 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
b79fe189 1568
1569 foreach ($children as $child) {
f60c61b1 1570 $child->set_hidden($hidden, $cascade);
1571 }
f13002d5 1572 }
b79fe189 1573
f3ac8eb4 1574 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
b79fe189 1575
1576 foreach ($children as $child) {
f60c61b1 1577 $child->set_hidden($hidden, $cascade);
1578 }
f13002d5 1579 }
1580 }
d90aa634
AD
1581
1582 //if marking category visible make sure parent category is visible MDL-21367
1583 if( !$hidden ) {
1584 $category_array = grade_category::fetch_all(array('id'=>$this->parent));
1585 if ($category_array && array_key_exists($this->parent, $category_array)) {
1586 $category = $category_array[$this->parent];
1587 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
1588 //if($category->is_hidden()) {
1589 $category->set_hidden($hidden, false);
1590 //}
1591 }
1592 }
5fad5061 1593 }
89a5f827 1594
190af29f 1595 /**
1596 * Applies default settings on this category
1597 * @return bool true if anything changed
1598 */
da3801e8 1599 public function apply_default_settings() {
190af29f 1600 global $CFG;
1601
1602 foreach ($this->forceable as $property) {
b79fe189 1603
190af29f 1604 if (isset($CFG->{"grade_$property"})) {
b79fe189 1605
190af29f 1606 if ($CFG->{"grade_$property"} == -1) {
1607 continue; //temporary bc before version bump
1608 }
1609 $this->$property = $CFG->{"grade_$property"};
1610 }
1611 }
1612 }
1613
89a5f827 1614 /**
1615 * Applies forced settings on this category
1616 * @return bool true if anything changed
1617 */
da3801e8 1618 public function apply_forced_settings() {
89a5f827 1619 global $CFG;
1620
1621 $updated = false;
b79fe189 1622
89a5f827 1623 foreach ($this->forceable as $property) {
b79fe189 1624
1625 if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and
1626 ((int) $CFG->{"grade_{$property}_flag"} & 1)) {
1627
190af29f 1628 if ($CFG->{"grade_$property"} == -1) {
1629 continue; //temporary bc before version bump
1630 }
89a5f827 1631 $this->$property = $CFG->{"grade_$property"};
1632 $updated = true;
1633 }
1634 }
1635
1636 return $updated;
1637 }
1638
1639 /**
1640 * Notification of change in forced category settings.
b79fe189 1641 *
1642 * @return void
89a5f827 1643 * @static
1644 */
da3801e8 1645 public static function updated_forced_settings() {
5b0af8c5 1646 global $CFG, $DB;
1647 $params = array(1, 'course', 'category');
1648 $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?";
1649 $DB->execute($sql, $params);
89a5f827 1650 }
4a490db0 1651}