MDL-47377 core_grades: Behat test to cover 0 weights
[moodle.git] / lib / grade / grade_category.php
CommitLineData
b79fe189 1<?php
b79fe189 2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
a153c9f2 16
b79fe189 17/**
a153c9f2 18 * Definition of a class to represent a grade category
b79fe189 19 *
a153c9f2
AD
20 * @package core_grades
21 * @copyright 2006 Nicolas Connault
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
b79fe189 23 */
7ad5a627
PS
24
25defined('MOODLE_INTERNAL') || die();
26
8a31e65c 27require_once('grade_object.php');
28
b79fe189 29/**
a153c9f2 30 * grade_category is an object mapped to DB table {prefix}grade_categories
b79fe189 31 *
a153c9f2
AD
32 * @package core_grades
33 * @category grade
34 * @copyright 2007 Nicolas Connault
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
b79fe189 36 */
3058964f 37class grade_category extends grade_object {
8a31e65c 38 /**
7c8a963f 39 * The DB table.
8a31e65c 40 * @var string $table
41 */
da3801e8 42 public $table = 'grade_categories';
4a490db0 43
8a31e65c 44 /**
3f2b0c8a 45 * Array of required table fields, must start with 'id'.
46 * @var array $required_fields
8a31e65c 47 */
da3801e8 48 public $required_fields = array('id', 'courseid', 'parent', 'depth', 'path', 'fullname', 'aggregation',
3f2b0c8a 49 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes',
a25bb902 50 'aggregatesubcats', 'timecreated', 'timemodified', 'hidden');
4a490db0 51
8a31e65c 52 /**
53 * The course this category belongs to.
54 * @var int $courseid
55 */
da3801e8 56 public $courseid;
4a490db0 57
8a31e65c 58 /**
59 * The category this category belongs to (optional).
4a490db0 60 * @var int $parent
8a31e65c 61 */
da3801e8 62 public $parent;
4a490db0 63
8c846243 64 /**
65 * The grade_category object referenced by $this->parent (PK).
a153c9f2 66 * @var grade_category $parent_category
8c846243 67 */
da3801e8 68 public $parent_category;
27f95e9b 69
e5c674f1 70 /**
71 * The number of parents this category has.
72 * @var int $depth
73 */
da3801e8 74 public $depth = 0;
e5c674f1 75
76 /**
c2efb501 77 * Shows the hierarchical path for this category as /1/2/3/ (like course_categories), the last number being
e5c674f1 78 * this category's autoincrement ID number.
79 * @var string $path
80 */
da3801e8 81 public $path;
e5c674f1 82
8a31e65c 83 /**
84 * The name of this category.
85 * @var string $fullname
86 */
da3801e8 87 public $fullname;
4a490db0 88
8a31e65c 89 /**
90 * A constant pointing to one of the predefined aggregation strategies (none, mean, median, sum etc) .
4a490db0 91 * @var int $aggregation
8a31e65c 92 */
da3801e8 93 public $aggregation = GRADE_AGGREGATE_MEAN;
4a490db0 94
8a31e65c 95 /**
96 * Keep only the X highest items.
97 * @var int $keephigh
98 */
da3801e8 99 public $keephigh = 0;
4a490db0 100
8a31e65c 101 /**
102 * Drop the X lowest items.
103 * @var int $droplow
104 */
da3801e8 105 public $droplow = 0;
4a490db0 106
c2efb501 107 /**
108 * Aggregate only graded items
109 * @var int $aggregateonlygraded
110 */
da3801e8 111 public $aggregateonlygraded = 0;
c2efb501 112
29d509f5 113 /**
114 * Aggregate outcomes together with normal items
c2efb501 115 * @var int $aggregateoutcomes
29d509f5 116 */
da3801e8 117 public $aggregateoutcomes = 0;
29d509f5 118
c2efb501 119 /**
120 * Ignore subcategories when aggregating
121 * @var int $aggregatesubcats
122 */
da3801e8 123 public $aggregatesubcats = 0;
c2efb501 124
8a31e65c 125 /**
126 * Array of grade_items or grade_categories nested exactly 1 level below this category
127 * @var array $children
128 */
da3801e8 129 public $children;
8a31e65c 130
7c8a963f 131 /**
4a490db0 132 * A hierarchical array of all children below this category. This is stored separately from
7c8a963f 133 * $children because it is more memory-intensive and may not be used as often.
134 * @var array $all_children
135 */
da3801e8 136 public $all_children;
7c8a963f 137
f151b073 138 /**
139 * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
140 * for this category.
a153c9f2 141 * @var grade_item $grade_item
f151b073 142 */
da3801e8 143 public $grade_item;
f151b073 144
b3ac6c3e 145 /**
146 * Temporary sortorder for speedup of children resorting
a153c9f2 147 * @var int $sortorder
b3ac6c3e 148 */
da3801e8 149 public $sortorder;
b3ac6c3e 150
89a5f827 151 /**
152 * List of options which can be "forced" from site settings.
a153c9f2 153 * @var array $forceable
89a5f827 154 */
da3801e8 155 public $forceable = array('aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 'aggregatesubcats');
89a5f827 156
653a8648 157 /**
158 * String representing the aggregation coefficient. Variable is used as cache.
a153c9f2 159 * @var string $coefstring
653a8648 160 */
b79fe189 161 public $coefstring = null;
653a8648 162
4272386a
FM
163 /**
164 * Static variable storing the result from {@link self::can_apply_limit_rules}.
165 * @var bool
166 */
167 protected $canapplylimitrules;
168
e5c674f1 169 /**
170 * Builds this category's path string based on its parents (if any) and its own id number.
171 * This is typically done just before inserting this object in the DB for the first time,
ce385eb4 172 * or when a new parent is added or changed. It is a recursive function: once the calling
173 * object no longer has a parent, the path is complete.
174 *
a153c9f2
AD
175 * @param grade_category $grade_category A Grade_Category object
176 * @return string The category's path string
e5c674f1 177 */
22a9b6d8 178 public static function build_path($grade_category) {
da3801e8 179 global $DB;
180
ce385eb4 181 if (empty($grade_category->parent)) {
c2efb501 182 return '/'.$grade_category->id.'/';
b79fe189 183
ce385eb4 184 } else {
da3801e8 185 $parent = $DB->get_record('grade_categories', array('id' => $grade_category->parent));
9a68cffc 186 return grade_category::build_path($parent).$grade_category->id.'/';
ce385eb4 187 }
e5c674f1 188 }
189
8a31e65c 190 /**
f92dcad8 191 * Finds and returns a grade_category instance based on params.
8a31e65c 192 *
f92dcad8 193 * @param array $params associative arrays varname=>value
a153c9f2 194 * @return grade_category The retrieved grade_category instance or false if none found.
f92dcad8 195 */
da3801e8 196 public static function fetch($params) {
f3ac8eb4 197 return grade_object::fetch_helper('grade_categories', 'grade_category', $params);
f92dcad8 198 }
199
200 /**
201 * Finds and returns all grade_category instances based on params.
f92dcad8 202 *
203 * @param array $params associative arrays varname=>value
204 * @return array array of grade_category insatnces or false if none found.
205 */
da3801e8 206 public static function fetch_all($params) {
f3ac8eb4 207 return grade_object::fetch_all_helper('grade_categories', 'grade_category', $params);
ce385eb4 208 }
209
8f4a626d 210 /**
2cc4b0f9 211 * In addition to update() as defined in grade_object, call force_regrading of parent categories, if applicable.
a153c9f2 212 *
aaff71da 213 * @param string $source from where was the object updated (mod/forum, manual, etc.)
a153c9f2 214 * @return bool success
8f4a626d 215 */
da3801e8 216 public function update($source=null) {
b3ac6c3e 217 // load the grade item or create a new one
218 $this->load_grade_item();
219
220 // force recalculation of path;
221 if (empty($this->path)) {
9a68cffc 222 $this->path = grade_category::build_path($this);
c2efb501 223 $this->depth = substr_count($this->path, '/') - 1;
1909a127 224 $updatechildren = true;
b79fe189 225
1909a127 226 } else {
227 $updatechildren = false;
4a490db0 228 }
0fc7f624 229
89a5f827 230 $this->apply_forced_settings();
231
b79fe189 232 // these are exclusive
89a5f827 233 if ($this->droplow > 0) {
234 $this->keephigh = 0;
b79fe189 235
89a5f827 236 } else if ($this->keephigh > 0) {
237 $this->droplow = 0;
238 }
4a490db0 239
b3ac6c3e 240 // Recalculate grades if needed
241 if ($this->qualifies_for_regrading()) {
f8e6e4db 242 $this->force_regrading();
4a490db0 243 }
f8e6e4db 244
ced5ee59 245 $this->timemodified = time();
246
1909a127 247 $result = parent::update($source);
248
249 // now update paths in all child categories
250 if ($result and $updatechildren) {
b79fe189 251
1909a127 252 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
b79fe189 253
1909a127 254 foreach ($children as $child) {
1909a127 255 $child->path = null;
256 $child->depth = 0;
257 $child->update($source);
da3801e8 258 }
1909a127 259 }
260 }
261
262 return $result;
8f4a626d 263 }
4a490db0 264
8f4a626d 265 /**
2cc4b0f9 266 * If parent::delete() is successful, send force_regrading message to parent category.
a153c9f2 267 *
aaff71da 268 * @param string $source from where was the object deleted (mod/forum, manual, etc.)
a153c9f2 269 * @return bool success
8f4a626d 270 */
da3801e8 271 public function delete($source=null) {
f13002d5 272 $grade_item = $this->load_grade_item();
4a490db0 273
f615fbab 274 if ($this->is_course_category()) {
b79fe189 275
f3ac8eb4 276 if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) {
b79fe189 277
f615fbab 278 foreach ($categories as $category) {
b79fe189 279
f615fbab 280 if ($category->id == $this->id) {
281 continue; // do not delete course category yet
282 }
283 $category->delete($source);
284 }
aaff71da 285 }
2b0f65e2 286
f3ac8eb4 287 if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) {
b79fe189 288
f615fbab 289 foreach ($items as $item) {
b79fe189 290
f615fbab 291 if ($item->id == $grade_item->id) {
292 continue; // do not delete course item yet
293 }
294 $item->delete($source);
295 }
296 }
297
298 } else {
299 $this->force_regrading();
2b0f65e2 300
f615fbab 301 $parent = $this->load_parent_category();
2b0f65e2 302
f615fbab 303 // Update children's categoryid/parent field first
f3ac8eb4 304 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
f615fbab 305 foreach ($children as $child) {
306 $child->set_parent($parent->id);
307 }
308 }
b79fe189 309
f3ac8eb4 310 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
f615fbab 311 foreach ($children as $child) {
312 $child->set_parent($parent->id);
313 }
aaff71da 314 }
315 }
f13002d5 316
aaff71da 317 // first delete the attached grade item and grades
318 $grade_item->delete($source);
f13002d5 319
320 // delete category itself
aaff71da 321 return parent::delete($source);
8f4a626d 322 }
4a490db0 323
ce385eb4 324 /**
325 * In addition to the normal insert() defined in grade_object, this method sets the depth
a153c9f2
AD
326 * and path for this object, and update the record accordingly.
327 *
328 * We do this here instead of in the constructor as they both need to know the record's
329 * ID number, which only gets created at insertion time.
f151b073 330 * This method also creates an associated grade_item if this wasn't done during construction.
a153c9f2 331 *
aaff71da 332 * @param string $source from where was the object inserted (mod/forum, manual, etc.)
333 * @return int PK ID if successful, false otherwise
ce385eb4 334 */
da3801e8 335 public function insert($source=null) {
b3ac6c3e 336
337 if (empty($this->courseid)) {
2f137aa1 338 print_error('cannotinsertgrade');
b8ff92b6 339 }
4a490db0 340
b3ac6c3e 341 if (empty($this->parent)) {
f3ac8eb4 342 $course_category = grade_category::fetch_course_category($this->courseid);
b3ac6c3e 343 $this->parent = $course_category->id;
ce385eb4 344 }
4a490db0 345
b3ac6c3e 346 $this->path = null;
347
ced5ee59 348 $this->timecreated = $this->timemodified = time();
349
aaff71da 350 if (!parent::insert($source)) {
b3ac6c3e 351 debugging("Could not insert this category: " . print_r($this, true));
352 return false;
353 }
354
f8e6e4db 355 $this->force_regrading();
356
b3ac6c3e 357 // build path and depth
aaff71da 358 $this->update($source);
4a490db0 359
aaff71da 360 return $this->id;
b3ac6c3e 361 }
362
f2c88356 363 /**
f615fbab 364 * Internal function - used only from fetch_course_category()
365 * Normal insert() can not be used for course category
b79fe189 366 *
367 * @param int $courseid The course ID
a153c9f2 368 * @return int The ID of the new course category
f2c88356 369 */
da3801e8 370 public function insert_course_category($courseid) {
1f0e4921 371 $this->courseid = $courseid;
8f6fdf43 372 $this->fullname = '?';
1f0e4921 373 $this->path = null;
374 $this->parent = null;
0c87b5aa 375 $this->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
4a490db0 376
190af29f 377 $this->apply_default_settings();
89a5f827 378 $this->apply_forced_settings();
379
ced5ee59 380 $this->timecreated = $this->timemodified = time();
381
aaff71da 382 if (!parent::insert('system')) {
b3ac6c3e 383 debugging("Could not insert this category: " . print_r($this, true));
384 return false;
f151b073 385 }
4a490db0 386
b3ac6c3e 387 // build path and depth
aaff71da 388 $this->update('system');
b3ac6c3e 389
aaff71da 390 return $this->id;
ce385eb4 391 }
4a490db0 392
8f4a626d 393 /**
394 * Compares the values held by this object with those of the matching record in DB, and returns
395 * whether or not these differences are sufficient to justify an update of all parent objects.
a153c9f2
AD
396 * This assumes that this object has an ID number and a matching record in DB. If not, it will return false.
397 *
398 * @return bool
8f4a626d 399 */
da3801e8 400 public function qualifies_for_regrading() {
8f4a626d 401 if (empty($this->id)) {
6639ead3 402 debugging("Can not regrade non existing category");
8f4a626d 403 return false;
404 }
f3ac8eb4 405
406 $db_item = grade_category::fetch(array('id'=>$this->id));
4a490db0 407
c2efb501 408 $aggregationdiff = $db_item->aggregation != $this->aggregation;
409 $keephighdiff = $db_item->keephigh != $this->keephigh;
410 $droplowdiff = $db_item->droplow != $this->droplow;
411 $aggonlygrddiff = $db_item->aggregateonlygraded != $this->aggregateonlygraded;
412 $aggoutcomesdiff = $db_item->aggregateoutcomes != $this->aggregateoutcomes;
413 $aggsubcatsdiff = $db_item->aggregatesubcats != $this->aggregatesubcats;
8f4a626d 414
c2efb501 415 return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff || $aggsubcatsdiff);
8f4a626d 416 }
8c846243 417
418 /**
a153c9f2 419 * Marks this grade categories' associated grade item as needing regrading
8c846243 420 */
da3801e8 421 public function force_regrading() {
f8e6e4db 422 $grade_item = $this->load_grade_item();
423 $grade_item->force_regrading();
8c846243 424 }
425
fcf6e015
FM
426 /**
427 * Something that should be called before we start regrading the whole course.
428 *
429 * @return void
430 */
431 public function pre_regrade_final_grades() {
fcf6e015 432 $this->auto_update_weights();
64055d50 433 $this->auto_update_max();
fcf6e015
FM
434 }
435
0aa32279 436 /**
0758a08e 437 * Generates and saves final grades in associated category grade item.
1994d890 438 * These immediate children must already have their own final grades.
0758a08e 439 * The category's aggregation method is used to generate final grades.
ac9b0805 440 *
a153c9f2 441 * Please note that category grade is either calculated or aggregated, not both at the same time.
ac9b0805 442 *
c86caae7 443 * This method must be used ONLY from grade_item::regrade_final_grades(),
ac9b0805 444 * because the calculation must be done in correct order!
b8ff92b6 445 *
4a490db0 446 * Steps to follow:
ac9b0805 447 * 1. Get final grades from immediate children
2df71235 448 * 3. Aggregate these grades
0758a08e 449 * 4. Save them in final grades of associated category grade item
b79fe189 450 *
a153c9f2 451 * @param int $userid The user ID if final grade generation should be limited to a single user
b79fe189 452 * @return bool
0aa32279 453 */
da3801e8 454 public function generate_grades($userid=null) {
455 global $CFG, $DB;
4a490db0 456
ac9b0805 457 $this->load_grade_item();
2cc4b0f9 458
459 if ($this->grade_item->is_locked()) {
460 return true; // no need to recalculate locked items
461 }
462
89a5f827 463 // find grade items of immediate children (category or grade items) and force site settings
61c33818 464 $depends_on = $this->grade_item->depends_on();
b3ac6c3e 465
f8e6e4db 466 if (empty($depends_on)) {
467 $items = false;
b79fe189 468
f8e6e4db 469 } else {
5b0af8c5 470 list($usql, $params) = $DB->get_in_or_equal($depends_on);
f8e6e4db 471 $sql = "SELECT *
5b0af8c5 472 FROM {grade_items}
473 WHERE id $usql";
474 $items = $DB->get_records_sql($sql, $params);
f8e6e4db 475 }
4a490db0 476
5b0af8c5 477 $grade_inst = new grade_grade();
478 $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
479
480 // where to look for final grades - include grade of this item too, we will store the results there
481 $gis = array_merge($depends_on, array($this->grade_item->id));
482 list($usql, $params) = $DB->get_in_or_equal($gis);
483
f8e6e4db 484 if ($userid) {
5b0af8c5 485 $usersql = "AND g.userid=?";
486 $params[] = $userid;
b79fe189 487
f8e6e4db 488 } else {
489 $usersql = "";
b8ff92b6 490 }
4a490db0 491
3f2b0c8a 492 $sql = "SELECT $fields
5b0af8c5 493 FROM {grade_grades} g, {grade_items} gi
494 WHERE gi.id = g.itemid AND gi.id $usql $usersql
ac9b0805 495 ORDER BY g.userid";
b8ff92b6 496
9580a21f 497 // group the results by userid and aggregate the grades for this user
1b42e677
EL
498 $rs = $DB->get_recordset_sql($sql, $params);
499 if ($rs->valid()) {
03cedd62 500 $prevuser = 0;
501 $grade_values = array();
502 $excluded = array();
503 $oldgrade = null;
5a59aeb1
DW
504 $grademaxoverrides = array();
505 $grademinoverrides = array();
b79fe189 506
da3801e8 507 foreach ($rs as $used) {
b79fe189 508
03cedd62 509 if ($used->userid != $prevuser) {
5a59aeb1
DW
510 $this->aggregate_grades($prevuser,
511 $items,
512 $grade_values,
513 $oldgrade,
514 $excluded,
515 $grademinoverrides,
516 $grademaxoverrides);
03cedd62 517 $prevuser = $used->userid;
518 $grade_values = array();
519 $excluded = array();
520 $oldgrade = null;
5a59aeb1
DW
521 $grademaxoverrides = array();
522 $grademinoverrides = array();
03cedd62 523 }
524 $grade_values[$used->itemid] = $used->finalgrade;
5a59aeb1
DW
525 $grademaxoverrides[$used->itemid] = $used->rawgrademax;
526 $grademinoverrides[$used->itemid] = $used->rawgrademin;
b79fe189 527
03cedd62 528 if ($used->excluded) {
529 $excluded[] = $used->itemid;
530 }
b79fe189 531
03cedd62 532 if ($this->grade_item->id == $used->itemid) {
533 $oldgrade = $used;
2df71235 534 }
b8ff92b6 535 }
5a59aeb1
DW
536 $this->aggregate_grades($prevuser,
537 $items,
538 $grade_values,
539 $oldgrade,
540 $excluded,
541 $grademinoverrides,
542 $grademaxoverrides);//the last one
b8ff92b6 543 }
1b42e677 544 $rs->close();
b8ff92b6 545
b8ff92b6 546 return true;
547 }
548
549 /**
a153c9f2 550 * Internal function for grade category grade aggregation
ced5ee59 551 *
b79fe189 552 * @param int $userid The User ID
553 * @param array $items Grade items
554 * @param array $grade_values Array of grade values
555 * @param object $oldgrade Old grade
22a9b6d8 556 * @param array $excluded Excluded
5a59aeb1
DW
557 * @param array $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid)
558 * @param array $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid)
b8ff92b6 559 */
5a59aeb1
DW
560 private function aggregate_grades($userid,
561 $items,
562 $grade_values,
563 $oldgrade,
564 $excluded,
565 $grademinoverrides,
566 $grademaxoverrides) {
ee07a54b 567 global $CFG, $DB;
bfe969e8
DW
568
569 // Remember these so we can set flags on them to describe how they were used in the aggregation.
570 $novalue = array();
571 $dropped = array();
53771c40 572 $extracredit = array();
bfe969e8
DW
573 $usedweights = array();
574
b8ff92b6 575 if (empty($userid)) {
f8e6e4db 576 //ignore first call
b8ff92b6 577 return;
578 }
4a490db0 579
f8e6e4db 580 if ($oldgrade) {
66690b69 581 $oldfinalgrade = $oldgrade->finalgrade;
f3ac8eb4 582 $grade = new grade_grade($oldgrade, false);
f8e6e4db 583 $grade->grade_item =& $this->grade_item;
b8ff92b6 584
f8e6e4db 585 } else {
586 // insert final grade - it will be needed later anyway
f3ac8eb4 587 $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
f8e6e4db 588 $grade->grade_item =& $this->grade_item;
f1ad9e04 589 $grade->insert('system');
590 $oldfinalgrade = null;
f8e6e4db 591 }
f1ad9e04 592
c86caae7 593 // no need to recalculate locked or overridden grades
594 if ($grade->is_locked() or $grade->is_overridden()) {
2cc4b0f9 595 return;
ac9b0805 596 }
597
f8e6e4db 598 // can not use own final category grade in calculation
9580a21f 599 unset($grade_values[$this->grade_item->id]);
f8e6e4db 600
53771c40 601 // Make sure a grade_grade exists for every grade_item.
6070e533
DW
602 // We need to do this so we can set the aggregationstatus
603 // with a set_field call instead of checking if each one exists and creating/updating.
c529f4b3 604 if (!empty($items)) {
ee07a54b
DW
605 list($ggsql, $params) = $DB->get_in_or_equal(array_keys($items), SQL_PARAMS_NAMED, 'g');
606
607
608 $params['userid'] = $userid;
609 $sql = "SELECT itemid
610 FROM {grade_grades}
611 WHERE itemid $ggsql AND userid = :userid";
612 $existingitems = $DB->get_records_sql($sql, $params);
613
614 $notexisting = array_diff(array_keys($items), array_keys($existingitems));
615 foreach ($notexisting as $itemid) {
6077a4d4 616 $gradeitem = $items[$itemid];
ee07a54b
DW
617 $gradegrade = new grade_grade(array('itemid' => $itemid,
618 'userid' => $userid,
619 'rawgrademin' => $gradeitem->grademin,
620 'rawgrademax' => $gradeitem->grademax), false);
621 $gradegrade->grade_item = $gradeitem;
53771c40
DW
622 $gradegrade->insert('system');
623 }
53771c40
DW
624 }
625
0758a08e 626 // if no grades calculation possible or grading not allowed clear final grade
9580a21f 627 if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
f8e6e4db 628 $grade->finalgrade = null;
b79fe189 629
66690b69 630 if (!is_null($oldfinalgrade)) {
0758a08e 631 $grade->update('aggregation');
f8e6e4db 632 }
bfe969e8 633 $dropped = $grade_values;
53771c40 634 $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
b8ff92b6 635 return;
636 }
0758a08e 637
53771c40
DW
638 // Normalize the grades first - all will have value 0...1
639 // ungraded items are not used in aggregation.
825fba1c 640 foreach ($grade_values as $itemid=>$v) {
53771c40
DW
641 if (is_null($v)) {
642 // If null, it means no grade.
643 if ($this->aggregateonlygraded) {
aa705529 644 unset($grade_values[$itemid]);
ee07a54b 645 // Mark this item as "excluded empty" because it has no grade.
aa705529
JO
646 $novalue[$itemid] = 0;
647 continue;
aa705529 648 }
0e999796 649 }
53771c40
DW
650 if (in_array($itemid, $excluded)) {
651 unset($grade_values[$itemid]);
652 $dropped[$itemid] = 0;
653 continue;
654 }
5a59aeb1
DW
655 // Check for user specific grade min/max overrides.
656 $usergrademin = $items[$itemid]->grademin;
657 $usergrademax = $items[$itemid]->grademax;
658 if (isset($grademinoverrides[$itemid])) {
659 $usergrademin = $grademinoverrides[$itemid];
660 }
661 if (isset($grademaxoverrides[$itemid])) {
662 $usergrademax = $grademaxoverrides[$itemid];
663 }
664 $grade_values[$itemid] = grade_grade::standardise_score($v, $usergrademin, $usergrademax, 0, 1);
0aa32279 665 }
b79fe189 666
ee07a54b 667 // For items with no value, and not excluded - either set their grade to 0 or exclude them.
53771c40
DW
668 foreach ($items as $itemid=>$value) {
669 if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
670 if (!$this->aggregateonlygraded) {
c2efb501 671 $grade_values[$itemid] = 0;
53771c40 672 } else {
ee07a54b 673 // We are specifically marking these items as "excluded empty".
53771c40 674 $novalue[$itemid] = 0;
eacd3700 675 }
c2efb501 676 }
eacd3700 677 }
678
679 // limit and sort
bfe969e8 680 $allvalues = $grade_values;
4272386a
FM
681 if ($this->can_apply_limit_rules()) {
682 $this->apply_limit_rules($grade_values, $items);
683 }
bfe969e8
DW
684
685 $moredropped = array_diff($allvalues, $grade_values);
686 foreach ($moredropped as $drop => $unused) {
687 $dropped[$drop] = 0;
688 }
53771c40
DW
689
690 foreach ($grade_values as $itemid => $val) {
691 if (self::is_extracredit_used() && ($items[$itemid]->aggregationcoef > 0)) {
692 $extracredit[$itemid] = 0;
693 }
694 }
695
9580a21f 696 asort($grade_values, SORT_NUMERIC);
4a490db0 697
d5f0aa01 698 // let's see we have still enough grades to do any statistics
9580a21f 699 if (count($grade_values) == 0) {
ac9b0805 700 // not enough attempts yet
f8e6e4db 701 $grade->finalgrade = null;
b79fe189 702
66690b69 703 if (!is_null($oldfinalgrade)) {
0758a08e 704 $grade->update('aggregation');
b8ff92b6 705 }
53771c40 706 $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
b8ff92b6 707 return;
708 }
2df71235 709
d297269d 710 // do the maths
5a59aeb1
DW
711 $result = $this->aggregate_values_and_adjust_bounds($grade_values,
712 $items,
713 $usedweights,
714 $grademinoverrides,
715 $grademaxoverrides);
5232d3f2 716 $agg_grade = $result['grade'];
d297269d 717
825fba1c
FM
718 // Recalculate the grade back to requested range.
719 $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $result['grademin'], $result['grademax']);
d297269d 720
653a8648 721 $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade);
90cc8ce7 722 $oldrawgrademax = $grade->rawgrademax;
5a59aeb1 723 $grade->rawgrademax = $result['grademax'];
d297269d 724
725 // update in db if changed
90cc8ce7
DW
726 if (grade_floats_different($grade->finalgrade, $oldfinalgrade) ||
727 grade_floats_different($grade->rawgrademax, $oldrawgrademax)) {
0758a08e 728 $grade->update('aggregation');
d297269d 729 }
730
53771c40 731 $this->set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit);
bfe969e8 732
d297269d 733 return;
734 }
735
bfe969e8
DW
736 /**
737 * Set the flags on the grade_grade items to indicate how individual grades are used
738 * in the aggregation.
739 *
740 * @param int $userid The user we have aggregated the grades for.
741 * @param array $usedweights An array with keys for each of the grade_item columns included in the aggregation. The value are the relative weight.
742 * @param array $novalue An array with keys for each of the grade_item columns skipped because
6070e533 743 * they had no value in the aggregation.
bfe969e8 744 * @param array $dropped An array with keys for each of the grade_item columns dropped
6070e533
DW
745 * because of any drop lowest/highest settings in the aggregation.
746 * @param array $extracredit An array with keys for each of the grade_item columns
747 * considered extra credit by the aggregation.
bfe969e8 748 */
53771c40 749 private function set_usedinaggregation($userid, $usedweights, $novalue, $dropped, $extracredit) {
bfe969e8
DW
750 global $DB;
751
752 // Included.
753 if (!empty($usedweights)) {
754 // The usedweights items are updated individually to record the weights.
755 foreach ($usedweights as $gradeitemid => $contribution) {
bfe969e8 756 $DB->set_field_select('grade_grades',
a1740d7b 757 'aggregationweight',
bfe969e8
DW
758 $contribution,
759 "itemid = :itemid AND userid = :userid",
760 array('itemid'=>$gradeitemid, 'userid'=>$userid));
761 }
a1740d7b
DW
762
763 // Now set the status flag for all these weights.
764 list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($usedweights), SQL_PARAMS_NAMED, 'g');
765 $itemlist['userid'] = $userid;
766
767 $DB->set_field_select('grade_grades',
768 'aggregationstatus',
769 'used',
770 "itemid $itemsql AND userid = :userid",
771 $itemlist);
bfe969e8
DW
772 }
773
774 // No value.
775 if (!empty($novalue)) {
776 list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($novalue), SQL_PARAMS_NAMED, 'g');
777
778 $itemlist['userid'] = $userid;
779
780 $DB->set_field_select('grade_grades',
a1740d7b 781 'aggregationstatus',
bfe969e8
DW
782 'novalue',
783 "itemid $itemsql AND userid = :userid",
784 $itemlist);
785 }
786
787 // Dropped.
788 if (!empty($dropped)) {
789 list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($dropped), SQL_PARAMS_NAMED, 'g');
790
791 $itemlist['userid'] = $userid;
792
793 $DB->set_field_select('grade_grades',
a1740d7b 794 'aggregationstatus',
bfe969e8
DW
795 'dropped',
796 "itemid $itemsql AND userid = :userid",
797 $itemlist);
798 }
53771c40
DW
799 // Extra credit.
800 if (!empty($extracredit)) {
801 list($itemsql, $itemlist) = $DB->get_in_or_equal(array_keys($extracredit), SQL_PARAMS_NAMED, 'g');
802
803 $itemlist['userid'] = $userid;
804
805 $DB->set_field_select('grade_grades',
806 'aggregationstatus',
807 'extra',
808 "itemid $itemsql AND userid = :userid",
809 $itemlist);
810 }
bfe969e8
DW
811 }
812
89a5f827 813 /**
5232d3f2 814 * Internal function that calculates the aggregated grade and new min/max for this grade category
b79fe189 815 *
a153c9f2 816 * Must be public as it is used by grade_grade::get_hiding_affected()
b79fe189 817 *
a153c9f2
AD
818 * @param array $grade_values An array of values to be aggregated
819 * @param array $items The array of grade_items
d6caf637 820 * @since Moodle 2.6.5, 2.7.2
bfe969e8
DW
821 * @param array & $weights If provided, will be filled with the normalized weights
822 * for each grade_item as used in the aggregation.
ee07a54b
DW
823 * Some rules for the weights are:
824 * 1. The weights must add up to 1 (unless there are extra credit)
825 * 2. The contributed points column must add up to the course
826 * final grade and this column is calculated from these weights.
5a59aeb1
DW
827 * @param array $grademinoverrides User specific grademin values if different to the grade_item grademin (key is itemid)
828 * @param array $grademaxoverrides User specific grademax values if different to the grade_item grademax (key is itemid)
5232d3f2
DW
829 * @return array containing values for:
830 * 'grade' => the new calculated grade
831 * 'grademin' => the new calculated min grade for the category
832 * 'grademax' => the new calculated max grade for the category
89a5f827 833 */
5a59aeb1
DW
834 public function aggregate_values_and_adjust_bounds($grade_values,
835 $items,
836 & $weights = null,
837 $grademinoverrides = array(),
838 $grademaxoverrides = array()) {
5232d3f2
DW
839 $category_item = $this->get_grade_item();
840 $grademin = $category_item->grademin;
841 $grademax = $category_item->grademax;
842
b8ff92b6 843 switch ($this->aggregation) {
b79fe189 844
c2efb501 845 case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
9580a21f 846 $num = count($grade_values);
c186c7b2 847 $grades = array_values($grade_values);
b79fe189 848
852fae0b
DW
849 // The median gets 100% - others get 0.
850 if ($weights !== null && $num > 0) {
851 $count = 0;
852 foreach ($grade_values as $itemid=>$grade_value) {
853 if (($num % 2 == 0) && ($count == intval($num/2)-1 || $count == intval($num/2))) {
854 $weights[$itemid] = 0.5;
855 } else if (($num % 2 != 0) && ($count == intval(($num/2)-0.5))) {
856 $weights[$itemid] = 1.0;
857 } else {
858 $weights[$itemid] = 0;
859 }
860 $count++;
861 }
862 }
c186c7b2 863 if ($num % 2 == 0) {
9c8d38fa 864 $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2;
b8ff92b6 865 } else {
9c8d38fa 866 $agg_grade = $grades[intval(($num/2)-0.5)];
b8ff92b6 867 }
bfe969e8 868
b8ff92b6 869 break;
ac9b0805 870
c2efb501 871 case GRADE_AGGREGATE_MIN:
9c8d38fa 872 $agg_grade = reset($grade_values);
bfe969e8
DW
873 // Record the weights as used.
874 if ($weights !== null) {
875 foreach ($grade_values as $itemid=>$grade_value) {
876 $weights[$itemid] = 0;
877 }
878 }
879 // Set the first item to 1.
880 $itemids = array_keys($grade_values);
881 $weights[reset($itemids)] = 1;
b8ff92b6 882 break;
883
c2efb501 884 case GRADE_AGGREGATE_MAX:
bfe969e8
DW
885 // Record the weights as used.
886 if ($weights !== null) {
887 foreach ($grade_values as $itemid=>$grade_value) {
888 $weights[$itemid] = 0;
889 }
890 }
891 // Set the last item to 1.
892 $itemids = array_keys($grade_values);
893 $weights[end($itemids)] = 1;
894 $agg_grade = end($grade_values);
b8ff92b6 895 break;
896
bfe969e8 897 case GRADE_AGGREGATE_MODE: // the most common value
0198929c 898 // array_count_values only counts INT and STRING, so if grades are floats we must convert them to string
899 $converted_grade_values = array();
900
901 foreach ($grade_values as $k => $gv) {
b79fe189 902
0198929c 903 if (!is_int($gv) && !is_string($gv)) {
904 $converted_grade_values[$k] = (string) $gv;
b79fe189 905
0198929c 906 } else {
907 $converted_grade_values[$k] = $gv;
908 }
bfe969e8
DW
909 if ($weights !== null) {
910 $weights[$k] = 0;
911 }
0198929c 912 }
913
914 $freq = array_count_values($converted_grade_values);
95affb8a 915 arsort($freq); // sort by frequency keeping keys
916 $top = reset($freq); // highest frequency count
917 $modes = array_keys($freq, $top); // search for all modes (have the same highest count)
f7d515b6 918 rsort($modes, SORT_NUMERIC); // get highest mode
9c8d38fa 919 $agg_grade = reset($modes);
bfe969e8
DW
920 // Record the weights as used.
921 if ($weights !== null && $top > 0) {
922 foreach ($grade_values as $k => $gv) {
923 if ($gv == $agg_grade) {
924 $weights[$k] = 1.0 / $top;
925 }
926 }
927 }
d5fab31f 928 break;
95affb8a 929
1426edac 930 case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef
9580a21f 931 $weightsum = 0;
932 $sum = 0;
b79fe189 933
934 foreach ($grade_values as $itemid=>$grade_value) {
935
eacd3700 936 if ($items[$itemid]->aggregationcoef <= 0) {
9580a21f 937 continue;
938 }
eacd3700 939 $weightsum += $items[$itemid]->aggregationcoef;
940 $sum += $items[$itemid]->aggregationcoef * $grade_value;
bfe969e8
DW
941 if ($weights !== null) {
942 $weights[$itemid] = $items[$itemid]->aggregationcoef;
943 }
9580a21f 944 }
945 if ($weightsum == 0) {
9c8d38fa 946 $agg_grade = null;
b79fe189 947
9580a21f 948 } else {
9c8d38fa 949 $agg_grade = $sum / $weightsum;
bfe969e8
DW
950 if ($weights !== null) {
951 // Normalise the weights.
952 foreach ($weights as $itemid => $weight) {
953 $weights[$itemid] = $weight / $weightsum;
954 }
955 }
956
9580a21f 957 }
958 break;
959
d9ae2ab5 960 case GRADE_AGGREGATE_WEIGHTED_MEAN2:
961 // Weighted average of all existing final grades with optional extra credit flag,
f7d515b6 962 // weight is the range of grade (usually grademax)
1426edac 963 $weightsum = 0;
d9ae2ab5 964 $sum = null;
b79fe189 965
966 foreach ($grade_values as $itemid=>$grade_value) {
1426edac 967 $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
b79fe189 968
1426edac 969 if ($weight <= 0) {
970 continue;
971 }
b79fe189 972
d9ae2ab5 973 if ($items[$itemid]->aggregationcoef == 0) {
974 $weightsum += $weight;
975 }
976 $sum += $weight * $grade_value;
1426edac 977 }
978 if ($weightsum == 0) {
d9ae2ab5 979 $agg_grade = $sum; // only extra credits
b79fe189 980
1426edac 981 } else {
982 $agg_grade = $sum / $weightsum;
983 }
bfe969e8
DW
984 // Record the weights as used.
985 if ($weights !== null) {
986 foreach ($grade_values as $itemid=>$grade_value) {
852fae0b 987 if ($weightsum > 0) {
bfe969e8 988 $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
852fae0b 989 $weights[$itemid] = $weight / $weightsum;
bfe969e8
DW
990 } else {
991 $weights[$itemid] = 0;
992 }
993 }
994 }
1426edac 995 break;
996
c2efb501 997 case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
9580a21f 998 $num = 0;
d9ae2ab5 999 $sum = null;
b79fe189 1000
1001 foreach ($grade_values as $itemid=>$grade_value) {
1002
eacd3700 1003 if ($items[$itemid]->aggregationcoef == 0) {
9580a21f 1004 $num += 1;
1005 $sum += $grade_value;
bfe969e8
DW
1006 if ($weights !== null) {
1007 $weights[$itemid] = 1;
1008 }
b79fe189 1009
eacd3700 1010 } else if ($items[$itemid]->aggregationcoef > 0) {
1011 $sum += $items[$itemid]->aggregationcoef * $grade_value;
bfe969e8 1012 if ($weights !== null) {
852fae0b 1013 $weights[$itemid] = 1;
bfe969e8
DW
1014 }
1015 }
1016 }
1017 if ($weights !== null && $num > 0) {
1018 foreach ($grade_values as $itemid=>$grade_value) {
1019 if ($weights[$itemid]) {
1020 $weights[$itemid] = 1.0 / $num;
1021 }
9580a21f 1022 }
1023 }
b79fe189 1024
9580a21f 1025 if ($num == 0) {
9c8d38fa 1026 $agg_grade = $sum; // only extra credits or wrong coefs
b79fe189 1027
9580a21f 1028 } else {
9c8d38fa 1029 $agg_grade = $sum / $num;
9580a21f 1030 }
1031 break;
1032
5232d3f2
DW
1033 case GRADE_AGGREGATE_SUM: // Add up all the items.
1034 $num = count($grade_values);
d57ea599 1035 $sum = 0;
a2c13f68 1036 $sumweights = 0;
a2c13f68
DW
1037 $grademin = 0;
1038 $grademax = 0;
1039 foreach ($grade_values as $itemid => $gradevalue) {
5a59aeb1 1040 // We need to check if the grademax/min was adjusted per user because of excluded items.
057063b1 1041 $usergrademin = $items[$itemid]->grademin;
5a59aeb1
DW
1042 $usergrademax = $items[$itemid]->grademax;
1043 if (isset($grademinoverrides[$itemid])) {
1044 $usergrademin = $grademinoverrides[$itemid];
1045 }
1046 if (isset($grademaxoverrides[$itemid])) {
1047 $usergrademax = $grademaxoverrides[$itemid];
1048 }
057063b1 1049 $gradeitemrange = $usergrademax - $usergrademin;
a2c13f68 1050
64055d50
FM
1051 // Ignore extra credit and items with a weight of 0.
1052 if ($items[$itemid]->aggregationcoef <= 0 && $items[$itemid]->aggregationcoef2 > 0) {
a2c13f68
DW
1053 $grademax += $gradeitemrange;
1054 $sumweights += $items[$itemid]->aggregationcoef2;
aa705529
JO
1055 }
1056 }
057063b1
DW
1057 $userweights = array();
1058 $totaloverriddenweight = 0;
1059 $totaloverriddengrademax = 0;
1060 // We first need to rescale all manually assigned weights down by the
1061 // percentage of weights missing from the category.
1062 foreach ($grade_values as $itemid => $gradevalue) {
1063 if ($items[$itemid]->weightoverride) {
64055d50
FM
1064 if ($items[$itemid]->aggregationcoef2 <= 0) {
1065 // Records the weight of 0 and continue.
1066 $userweights[$itemid] = 0;
1067 continue;
1068 }
057063b1
DW
1069 $userweights[$itemid] = $items[$itemid]->aggregationcoef2 / $sumweights;
1070 $totaloverriddenweight += $userweights[$itemid];
1071 $usergrademax = $items[$itemid]->grademax;
1072 if (isset($grademaxoverrides[$itemid])) {
1073 $usergrademax = $grademaxoverrides[$itemid];
1074 }
1075 $totaloverriddengrademax += $usergrademax;
1076 }
1077 }
1078 $nonoverriddenpoints = $grademax - $totaloverriddengrademax;
1079
1080 // Then we need to recalculate the automatic weights.
1081 foreach ($grade_values as $itemid => $gradevalue) {
1082 if (!$items[$itemid]->weightoverride) {
64055d50
FM
1083 $usergrademax = $items[$itemid]->grademax;
1084 if (isset($grademaxoverrides[$itemid])) {
1085 $usergrademax = $grademaxoverrides[$itemid];
1086 }
057063b1 1087 if ($nonoverriddenpoints > 0) {
057063b1
DW
1088 $userweights[$itemid] = ($usergrademax/$nonoverriddenpoints) * (1 - $totaloverriddenweight);
1089 } else {
1090 $userweights[$itemid] = 0;
64055d50
FM
1091 if ($items[$itemid]->aggregationcoef2 > 0) {
1092 // Items with a weight of 0 should not count for the grade max,
1093 // though this only applies if the weight was changed to 0.
1094 $grademax -= $usergrademax;
1095 }
057063b1
DW
1096 }
1097 }
a2c13f68 1098 }
057063b1
DW
1099
1100 // We can use our freshly corrected weights below.
a2c13f68 1101 foreach ($grade_values as $itemid => $gradevalue) {
057063b1 1102 $sum += $gradevalue * $userweights[$itemid] * $grademax;
a2c13f68 1103 if ($weights !== null) {
057063b1 1104 $weights[$itemid] = $userweights[$itemid];
a2c13f68
DW
1105 }
1106 }
1107 if ($grademax > 0) {
1108 $agg_grade = $sum / $grademax; // Re-normalize score.
1109 } else {
1110 // Every item in the category is extra credit.
1111 $agg_grade = $sum;
1112 $grademax = $sum;
1113 }
b50b12b3 1114
5232d3f2
DW
1115 break;
1116
c2efb501 1117 case GRADE_AGGREGATE_MEAN: // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
ac9b0805 1118 default:
9580a21f 1119 $num = count($grade_values);
1120 $sum = array_sum($grade_values);
9c8d38fa 1121 $agg_grade = $sum / $num;
bfe969e8
DW
1122 // Record the weights evenly.
1123 if ($weights !== null && $num > 0) {
1124 foreach ($grade_values as $itemid=>$grade_value) {
1125 $weights[$itemid] = 1.0 / $num;
1126 }
1127 }
b8ff92b6 1128 break;
1129 }
1130
5232d3f2
DW
1131 return array('grade' => $agg_grade, 'grademin' => $grademin, 'grademax' => $grademax);
1132 }
1133
1134 /**
1135 * Internal function that calculates the aggregated grade for this grade category
1136 *
1137 * Must be public as it is used by grade_grade::get_hiding_affected()
1138 *
1139 * @deprecated since Moodle 2.8
1140 * @param array $grade_values An array of values to be aggregated
1141 * @param array $items The array of grade_items
1142 * @return float The aggregate grade for this grade category
1143 */
1144 public function aggregate_values($grade_values, $items) {
1145 debugging('grade_category::aggregate_values() is deprecated.
1146 Call grade_category::aggregate_values_and_adjust_bounds() instead.', DEBUG_DEVELOPER);
1147 $result = $this->aggregate_values_and_adjust_bounds($grade_values, $items);
1148 return $result['grade'];
0aa32279 1149 }
0758a08e 1150
3a03653e 1151 /**
fcf6e015 1152 * Some aggregation types may need to update their max grade.
a153c9f2 1153 *
64055d50
FM
1154 * This must be executed after updating the weights as it relies on them.
1155 *
fcf6e015 1156 * @return void
3a03653e 1157 */
fcf6e015
FM
1158 private function auto_update_max() {
1159 global $DB;
3a03653e 1160 if ($this->aggregation != GRADE_AGGREGATE_SUM) {
1161 // not needed at all
1162 return;
1163 }
1164
fcf6e015
FM
1165 // Find grade items of immediate children (category or grade items) and force site settings.
1166 $this->load_grade_item();
1167 $depends_on = $this->grade_item->depends_on();
1168
1169 $items = false;
1170 if (!empty($depends_on)) {
1171 list($usql, $params) = $DB->get_in_or_equal($depends_on);
1172 $sql = "SELECT *
1173 FROM {grade_items}
1174 WHERE id $usql";
1175 $items = $DB->get_records_sql($sql, $params);
1176 }
1177
3a03653e 1178 if (!$items) {
b79fe189 1179
3a03653e 1180 if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
1181 $this->grade_item->grademax = 0;
1182 $this->grade_item->grademin = 0;
1183 $this->grade_item->gradetype = GRADE_TYPE_VALUE;
1184 $this->grade_item->update('aggregation');
1185 }
1186 return;
1187 }
1188
d57ea599 1189 //find max grade possible
59080eee 1190 $maxes = array();
b79fe189 1191
3a03653e 1192 foreach ($items as $item) {
b79fe189 1193
d57ea599
JO
1194 if ($item->aggregationcoef > 0) {
1195 // extra credit from this activity - does not affect total
1196 continue;
64055d50
FM
1197 } else if ($item->aggregationcoef2 <= 0) {
1198 // Items with a weight of 0 do not affect the total.
1199 continue;
3a03653e 1200 }
11eca362 1201
d57ea599
JO
1202 if ($item->gradetype == GRADE_TYPE_VALUE) {
1203 $maxes[$item->id] = $item->grademax;
1204
1205 } else if ($item->gradetype == GRADE_TYPE_SCALE) {
1206 $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item
1207 }
1208 }
4272386a
FM
1209
1210 if ($this->can_apply_limit_rules()) {
1211 // Apply droplow and keephigh.
1212 $this->apply_limit_rules($maxes, $items);
1213 }
59080eee 1214 $max = array_sum($maxes);
3a03653e 1215
59080eee 1216 // update db if anything changed
1217 if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
3a03653e 1218 $this->grade_item->grademax = $max;
1219 $this->grade_item->grademin = 0;
1220 $this->grade_item->gradetype = GRADE_TYPE_VALUE;
1221 $this->grade_item->update('aggregation');
1222 }
1223 }
1224
a2f1f65d
AD
1225 /**
1226 * Recalculate the weights of the grade items in this category.
fcf6e015 1227 *
64055d50
FM
1228 * The category total is not updated here, a further call to
1229 * {@link self::auto_update_max()} is required.
1230 *
fcf6e015 1231 * @return void
a2f1f65d
AD
1232 */
1233 private function auto_update_weights() {
1234 if ($this->aggregation != GRADE_AGGREGATE_SUM) {
1235 // This is only required if we are using natural weights.
1236 return;
1237 }
1238 $children = $this->get_children();
1239
1240 $grade_item = null;
1241
1242 // Calculate the sum of the grademax's of all the items within this category.
1243 $totalgrademax = 0;
45da5361
AD
1244
1245 // Out of 1, how much weight has been manually overriden by a user?
1246 $totaloverriddenweight = 0;
1247 $totaloverriddengrademax = 0;
a2f1f65d
AD
1248 foreach ($children as $sortorder => $child) {
1249 $grade_item = null;
1250
1251 if ($child['type'] == 'item') {
1252 $grade_item = $child['object'];
1253 } else if ($child['type'] == 'category') {
1254 $grade_item = $child['object']->load_grade_item();
1255 }
45da5361 1256
b14c5870 1257 if ($grade_item->aggregationcoef > 0) {
64055d50
FM
1258 // An extra credit grade item doesn't contribute to $totaloverriddengrademax.
1259 continue;
1260 } else if ($grade_item->weightoverride && $grade_item->aggregationcoef2 <= 0) {
1261 // An overriden item that defines a weight of 0 does not contribute to $totaloverriddengrademax.
b14c5870
AD
1262 continue;
1263 }
1264
a2f1f65d 1265 $totalgrademax += $grade_item->grademax;
45da5361
AD
1266 if ($grade_item->weightoverride) {
1267 $totaloverriddenweight += $grade_item->aggregationcoef2;
1268 $totaloverriddengrademax += $grade_item->grademax;
1269 }
a2f1f65d
AD
1270 }
1271
45da5361
AD
1272 $totalgrademax -= $totaloverriddengrademax;
1273
a2f1f65d
AD
1274 reset($children);
1275 foreach ($children as $sortorder => $child) {
1276 $grade_item = null;
1277
1278 if ($child['type'] == 'item') {
1279 $grade_item = $child['object'];
1280 } else if ($child['type'] == 'category') {
1281 $grade_item = $child['object']->load_grade_item();
1282 }
45da5361 1283 if (!$grade_item->weightoverride) {
64055d50
FM
1284 if ($totaloverriddenweight >= 1) {
1285 // There is no more weight to distribute.
1286 $grade_item->aggregationcoef2 = 0;
1287 } else {
1288 // Calculate this item's weight as a percentage of the non-overridden total grade maxes
1289 // then convert it to a proportion of the available non-overriden weight.
1290 $grade_item->aggregationcoef2 = ($grade_item->grademax/$totalgrademax) * (1 - $totaloverriddenweight);
1291 }
45da5361
AD
1292 $grade_item->update();
1293 }
a2f1f65d
AD
1294 }
1295 }
1296
adc2f286 1297 /**
a153c9f2 1298 * Given an array of grade values (numerical indices) applies droplow or keephigh rules to limit the final array.
b79fe189 1299 *
a153c9f2 1300 * @param array $grade_values itemid=>$grade_value float
b79fe189 1301 * @param array $items grade item objects
adc2f286 1302 * @return array Limited grades.
1303 */
a9e38ac8 1304 public function apply_limit_rules(&$grade_values, $items) {
1305 $extraused = $this->is_extracredit_used();
1306
adc2f286 1307 if (!empty($this->droplow)) {
a9e38ac8 1308 asort($grade_values, SORT_NUMERIC);
1309 $dropped = 0;
b79fe189 1310
fb80cb2a
AD
1311 // If we have fewer grade items available to drop than $this->droplow, use this flag to escape the loop
1312 // May occur because of "extra credit" or if droplow is higher than the number of grade items
1313 $droppedsomething = true;
b79fe189 1314
fb80cb2a
AD
1315 while ($dropped < $this->droplow && $droppedsomething) {
1316 $droppedsomething = false;
b79fe189 1317
fb80cb2a 1318 $grade_keys = array_keys($grade_values);
aee968da
AD
1319 $gradekeycount = count($grade_keys);
1320
1321 if ($gradekeycount === 0) {
fb80cb2a
AD
1322 //We've dropped all grade items
1323 break;
1324 }
b79fe189 1325
fb80cb2a
AD
1326 $originalindex = $founditemid = $foundmax = null;
1327
1328 // Find the first remaining grade item that is available to be dropped
1329 foreach ($grade_keys as $gradekeyindex=>$gradekey) {
1330 if (!$extraused || $items[$gradekey]->aggregationcoef <= 0) {
1331 // Found a non-extra credit grade item that is eligible to be dropped
1332 $originalindex = $gradekeyindex;
1333 $founditemid = $grade_keys[$originalindex];
1334 $foundmax = $items[$founditemid]->grademax;
1335 break;
a9e38ac8 1336 }
fb80cb2a 1337 }
b79fe189 1338
fb80cb2a
AD
1339 if (empty($founditemid)) {
1340 // No grade items available to drop
a9e38ac8 1341 break;
59080eee 1342 }
fb80cb2a
AD
1343
1344 // Now iterate over the remaining grade items
1345 // We're looking for other grade items with the same grade value but a higher grademax
1346 $i = 1;
aee968da 1347 while ($originalindex + $i < $gradekeycount) {
52929486 1348
fb80cb2a 1349 $possibleitemid = $grade_keys[$originalindex+$i];
52929486
AD
1350 $i++;
1351
fb80cb2a
AD
1352 if ($grade_values[$founditemid] != $grade_values[$possibleitemid]) {
1353 // The next grade item has a different grade value. Stop looking.
1354 break;
1355 }
1356
1357 if ($extraused && $items[$possibleitemid]->aggregationcoef > 0) {
1358 // Don't drop extra credit grade items. Continue the search.
1359 continue;
1360 }
1361
1362 if ($foundmax < $items[$possibleitemid]->grademax) {
1363 // Found a grade item with the same grade value and a higher grademax
1364 $foundmax = $items[$possibleitemid]->grademax;
1365 $founditemid = $possibleitemid;
1366 // Continue searching to see if there is an even higher grademax
1367 }
fb80cb2a
AD
1368 }
1369
1370 // Now drop whatever grade item we have found
1371 unset($grade_values[$founditemid]);
1372 $dropped++;
1373 $droppedsomething = true;
adc2f286 1374 }
a9e38ac8 1375
1376 } else if (!empty($this->keephigh)) {
1377 arsort($grade_values, SORT_NUMERIC);
1378 $kept = 0;
b79fe189 1379
a9e38ac8 1380 foreach ($grade_values as $itemid=>$value) {
b79fe189 1381
a9e38ac8 1382 if ($extraused and $items[$itemid]->aggregationcoef > 0) {
1383 // we keep all extra credits
b79fe189 1384
a9e38ac8 1385 } else if ($kept < $this->keephigh) {
1386 $kept++;
b79fe189 1387
a9e38ac8 1388 } else {
1389 unset($grade_values[$itemid]);
1390 }
adc2f286 1391 }
1392 }
0aa32279 1393 }
1394
4272386a
FM
1395 /**
1396 * Returns whether or not we can apply the limit rules.
1397 *
1398 * There are cases where drop lowest or keep highest should not be used
1399 * at all. This method will determine whether or not this logic can be
1400 * applied considering the current setup of the category.
1401 *
1402 * @return bool
1403 */
1404 public function can_apply_limit_rules() {
1405 if ($this->canapplylimitrules !== null) {
1406 return $this->canapplylimitrules;
1407 }
1408
1409 // Set it to be supported by default.
1410 $this->canapplylimitrules = true;
1411
1412 // Natural aggregation.
1413 if ($this->aggregation == GRADE_AGGREGATE_SUM) {
1414 $canapply = true;
1415
1416 // Check until one child breaks the rules.
1417 $gradeitems = $this->get_children();
1418 $validitems = 0;
1419 $lastweight = null;
1420 $lastmaxgrade = null;
1421 foreach ($gradeitems as $gradeitem) {
1422 $gi = $gradeitem['object'];
1423
1424 if ($gradeitem['type'] == 'category') {
1425 // Sub categories are not allowed because they can have dynamic weights/maxgrades.
1426 $canapply = false;
1427 break;
1428 }
1429
1430 if ($gi->aggregationcoef > 0) {
1431 // Extra credit items are not allowed.
1432 $canapply = false;
1433 break;
1434 }
1435
1436 if ($lastweight !== null && $lastweight != $gi->aggregationcoef2) {
1437 // One of the weight differs from another item.
1438 $canapply = false;
1439 break;
1440 }
1441
1442 if ($lastmaxgrade !== null && $lastmaxgrade != $gi->grademax) {
1443 // One of the max grade differ from another item. This is not allowed for now
1444 // because we could be end up with different max grade between users for this category.
1445 $canapply = false;
1446 break;
1447 }
1448
1449 $lastweight = $gi->aggregationcoef2;
1450 $lastmaxgrade = $gi->grademax;
1451 }
1452
1453 $this->canapplylimitrules = $canapply;
1454 }
1455
1456 return $this->canapplylimitrules;
1457 }
1458
793253ae 1459 /**
1460 * Returns true if category uses extra credit of any kind
b79fe189 1461 *
a153c9f2 1462 * @return bool True if extra credit used
793253ae 1463 */
134c514b
RT
1464 public function is_extracredit_used() {
1465 return self::aggregation_uses_extracredit($this->aggregation);
1466 }
1467
1468 /**
1469 * Returns true if aggregation passed is using extracredit.
1470 *
1471 * @param int $aggregation Aggregation const.
1472 * @return bool True if extra credit used
1473 */
1474 public static function aggregation_uses_extracredit($aggregation) {
1475 return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
1476 or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
1477 or $aggregation == GRADE_AGGREGATE_SUM);
793253ae 1478 }
1479
9580a21f 1480 /**
f7d515b6 1481 * Returns true if category uses special aggregation coefficient
b79fe189 1482 *
a153c9f2 1483 * @return bool True if an aggregation coefficient is being used
9580a21f 1484 */
4e9ca991 1485 public function is_aggregationcoef_used() {
134c514b
RT
1486 return self::aggregation_uses_aggregationcoef($this->aggregation);
1487
1488 }
1489
1490 /**
1491 * Returns true if aggregation uses aggregationcoef
1492 *
1493 * @param int $aggregation Aggregation const.
1494 * @return bool True if an aggregation coefficient is being used
1495 */
1496 public static function aggregation_uses_aggregationcoef($aggregation) {
1497 return ($aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN
1498 or $aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2
1499 or $aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
1500 or $aggregation == GRADE_AGGREGATE_SUM);
ba74762b 1501
9580a21f 1502 }
1503
653a8648 1504 /**
a153c9f2 1505 * Recursive function to find which weight/extra credit field to use in the grade item form.
b79fe189 1506 *
a153c9f2 1507 * Inherits from a parent category if that category has aggregatesubcats set to true.
b79fe189 1508 *
a153c9f2 1509 * @param string $first Whether or not this is the first item in the recursion
b79fe189 1510 * @return string
653a8648 1511 */
1512 public function get_coefstring($first=true) {
1513 if (!is_null($this->coefstring)) {
1514 return $this->coefstring;
1515 }
1516
1517 $overriding_coefstring = null;
1518
1519 // Stop recursing upwards if this category aggregates subcats or has no parent
1520 if (!$first && !$this->aggregatesubcats) {
b79fe189 1521
121d8006 1522 if ($parent_category = $this->load_parent_category()) {
653a8648 1523 return $parent_category->get_coefstring(false);
b79fe189 1524
653a8648 1525 } else {
1526 return null;
1527 }
b79fe189 1528
1529 } else if ($first) {
1530
653a8648 1531 if (!$this->aggregatesubcats) {
b79fe189 1532
121d8006 1533 if ($parent_category = $this->load_parent_category()) {
653a8648 1534 $overriding_coefstring = $parent_category->get_coefstring(false);
1535 }
1536 }
1537 }
1538
1539 // If an overriding coefstring has trickled down from one of the parent categories, return it. Otherwise, return self.
1540 if (!is_null($overriding_coefstring)) {
1541 return $overriding_coefstring;
1542 }
1543
1544 // No parent category is overriding this category's aggregation, return its string
1545 if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
1546 $this->coefstring = 'aggregationcoefweight';
b79fe189 1547
d9ae2ab5 1548 } else if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN2) {
1549 $this->coefstring = 'aggregationcoefextrasum';
b79fe189 1550
653a8648 1551 } else if ($this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN) {
3869ab1a 1552 $this->coefstring = 'aggregationcoefextraweight';
b79fe189 1553
653a8648 1554 } else if ($this->aggregation == GRADE_AGGREGATE_SUM) {
b6a1e366 1555 $this->coefstring = 'aggregationcoefextraweightsum';
b79fe189 1556
653a8648 1557 } else {
1558 $this->coefstring = 'aggregationcoef';
1559 }
1560 return $this->coefstring;
1561 }
1562
1c307f21 1563 /**
b3ac6c3e 1564 * Returns tree with all grade_items and categories as elements
b79fe189 1565 *
1566 * @param int $courseid The course ID
a153c9f2 1567 * @param bool $include_category_items as category children
b3ac6c3e 1568 * @return array
1c307f21 1569 */
da3801e8 1570 public static function fetch_course_tree($courseid, $include_category_items=false) {
f3ac8eb4 1571 $course_category = grade_category::fetch_course_category($courseid);
514a3467 1572 $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
1573 'children'=>$course_category->get_children($include_category_items));
b146d984
AD
1574
1575 $course_category->sortorder = $course_category->get_sortorder();
e2bb3c92
PS
1576 $sortorder = $course_category->get_sortorder();
1577 return grade_category::_fetch_course_tree_recursion($category_array, $sortorder);
1c307f21 1578 }
1579
b79fe189 1580 /**
a153c9f2 1581 * An internal function that recursively sorts grade categories within a course
b79fe189 1582 *
1583 * @param array $category_array The seed of the recursion
a153c9f2
AD
1584 * @param int $sortorder The current sortorder
1585 * @return array An array containing 'object', 'type', 'depth' and optionally 'children'
b79fe189 1586 */
1587 static private function _fetch_course_tree_recursion($category_array, &$sortorder) {
b3ac6c3e 1588 // update the sortorder in db if needed
b146d984
AD
1589 //NOTE: This leads to us resetting sort orders every time the categories and items page is viewed :(
1590 //if ($category_array['object']->sortorder != $sortorder) {
1591 //$category_array['object']->set_sortorder($sortorder);
1592 //}
ce385eb4 1593
7bac3777
AD
1594 if (isset($category_array['object']->gradetype) && $category_array['object']->gradetype==GRADE_TYPE_NONE) {
1595 return null;
1596 }
1597
314c4336 1598 // store the grade_item or grade_category instance with extra info
1599 $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);
b3ac6c3e 1600
1601 // reuse final grades if there
1602 if (array_key_exists('finalgrades', $category_array)) {
1603 $result['finalgrades'] = $category_array['finalgrades'];
1604 }
1605
1606 // recursively resort children
1607 if (!empty($category_array['children'])) {
1608 $result['children'] = array();
29d509f5 1609 //process the category item first
7bac3777 1610 $child = null;
b79fe189 1611
1612 foreach ($category_array['children'] as $oldorder=>$child_array) {
1613
314c4336 1614 if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
7bac3777
AD
1615 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1616 if (!empty($child)) {
1617 $result['children'][$sortorder] = $child;
1618 }
29d509f5 1619 }
2b0f65e2 1620 }
b79fe189 1621
1622 foreach ($category_array['children'] as $oldorder=>$child_array) {
1623
29d509f5 1624 if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
7bac3777
AD
1625 $child = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
1626 if (!empty($child)) {
1627 $result['children'][++$sortorder] = $child;
1628 }
b3ac6c3e 1629 }
1630 }
1631 }
1632
1633 return $result;
ce385eb4 1634 }
7c8a963f 1635
1636 /**
4a490db0 1637 * Fetches and returns all the children categories and/or grade_items belonging to this category.
1638 * By default only returns the immediate children (depth=1), but deeper levels can be requested,
a39cac25 1639 * as well as all levels (0). The elements are indexed by sort order.
b79fe189 1640 *
1641 * @param bool $include_category_items Whether or not to include category grade_items in the children array
7c8a963f 1642 * @return array Array of child objects (grade_category and grade_item).
1643 */
da3801e8 1644 public function get_children($include_category_items=false) {
1645 global $DB;
b3ac6c3e 1646
1647 // This function must be as fast as possible ;-)
1648 // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
1649 // we have to limit the number of queries though, because it will be used often in grade reports
1650
da3801e8 1651 $cats = $DB->get_records('grade_categories', array('courseid' => $this->courseid));
1652 $items = $DB->get_records('grade_items', array('courseid' => $this->courseid));
4a490db0 1653
b3ac6c3e 1654 // init children array first
1655 foreach ($cats as $catid=>$cat) {
1656 $cats[$catid]->children = array();
27f95e9b 1657 }
4a490db0 1658
b3ac6c3e 1659 //first attach items to cats and add category sortorder
1660 foreach ($items as $item) {
b79fe189 1661
b3ac6c3e 1662 if ($item->itemtype == 'course' or $item->itemtype == 'category') {
1663 $cats[$item->iteminstance]->sortorder = $item->sortorder;
4a490db0 1664
b3ac6c3e 1665 if (!$include_category_items) {
1666 continue;
1667 }
1668 $categoryid = $item->iteminstance;
b79fe189 1669
b3ac6c3e 1670 } else {
1671 $categoryid = $item->categoryid;
63354ab5
AD
1672 if (empty($categoryid)) {
1673 debugging('Found a grade item that isnt in a category');
1674 }
b3ac6c3e 1675 }
1676
1677 // prevent problems with duplicate sortorders in db
1678 $sortorder = $item->sortorder;
b79fe189 1679
63354ab5
AD
1680 while (array_key_exists($categoryid, $cats)
1681 && array_key_exists($sortorder, $cats[$categoryid]->children)) {
1682
b3ac6c3e 1683 $sortorder++;
1684 }
1685
1686 $cats[$categoryid]->children[$sortorder] = $item;
1687
1688 }
1689
1690 // now find the requested category and connect categories as children
1691 $category = false;
b79fe189 1692
b3ac6c3e 1693 foreach ($cats as $catid=>$cat) {
b79fe189 1694
ec3717e1 1695 if (empty($cat->parent)) {
b79fe189 1696
ec3717e1 1697 if ($cat->path !== '/'.$cat->id.'/') {
1698 $grade_category = new grade_category($cat, false);
1699 $grade_category->path = '/'.$cat->id.'/';
1700 $grade_category->depth = 1;
1701 $grade_category->update('system');
1702 return $this->get_children($include_category_items);
1703 }
b79fe189 1704
ec3717e1 1705 } else {
b79fe189 1706
ec3717e1 1707 if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) {
1708 //fix paths and depts
1709 static $recursioncounter = 0; // prevents infinite recursion
1710 $recursioncounter++;
b79fe189 1711
da3801e8 1712 if ($recursioncounter < 5) {
ec3717e1 1713 // fix paths and depths!
1714 $grade_category = new grade_category($cat, false);
1715 $grade_category->depth = 0;
1716 $grade_category->path = null;
1717 $grade_category->update('system');
1718 return $this->get_children($include_category_items);
1719 }
da3801e8 1720 }
b3ac6c3e 1721 // prevent problems with duplicate sortorders in db
1722 $sortorder = $cat->sortorder;
b79fe189 1723
1724 while (array_key_exists($sortorder, $cats[$cat->parent]->children)) {
f13002d5 1725 //debugging("$sortorder exists in cat loop");
b3ac6c3e 1726 $sortorder++;
1727 }
1728
65370356 1729 $cats[$cat->parent]->children[$sortorder] = &$cats[$catid];
b3ac6c3e 1730 }
f3ac8eb4 1731
b3ac6c3e 1732 if ($catid == $this->id) {
1733 $category = &$cats[$catid];
1734 }
1735 }
1736
1737 unset($items); // not needed
1738 unset($cats); // not needed
1739
f3ac8eb4 1740 $children_array = grade_category::_get_children_recursion($category);
b3ac6c3e 1741
1742 ksort($children_array);
1743
1744 return $children_array;
1745
1746 }
1747
b79fe189 1748 /**
1749 * Private method used to retrieve all children of this category recursively
1750 *
1751 * @param grade_category $category Source of current recursion
a153c9f2 1752 * @return array An array of child grade categories
b79fe189 1753 */
22a9b6d8 1754 private static function _get_children_recursion($category) {
b3ac6c3e 1755
1756 $children_array = array();
b79fe189 1757 foreach ($category->children as $sortorder=>$child) {
1758
b3ac6c3e 1759 if (array_key_exists('itemtype', $child)) {
f3ac8eb4 1760 $grade_item = new grade_item($child, false);
b79fe189 1761
4faf5f99 1762 if (in_array($grade_item->itemtype, array('course', 'category'))) {
1763 $type = $grade_item->itemtype.'item';
1764 $depth = $category->depth;
b79fe189 1765
314c4336 1766 } else {
1767 $type = 'item';
1768 $depth = $category->depth; // we use this to set the same colour
b3ac6c3e 1769 }
4faf5f99 1770 $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);
4a490db0 1771
7c8a963f 1772 } else {
f3ac8eb4 1773 $children = grade_category::_get_children_recursion($child);
1774 $grade_category = new grade_category($child, false);
b79fe189 1775
b3ac6c3e 1776 if (empty($children)) {
314c4336 1777 $children = array();
7c8a963f 1778 }
4faf5f99 1779 $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
314c4336 1780 }
27f95e9b 1781 }
1782
b3ac6c3e 1783 // sort the array
1784 ksort($children_array);
1785
27f95e9b 1786 return $children_array;
1787 }
4a490db0 1788
f151b073 1789 /**
a4d76049 1790 * Uses {@link get_grade_item()} to load or create a grade_item, then saves it as $this->grade_item.
a153c9f2
AD
1791 *
1792 * @return grade_item
f151b073 1793 */
da3801e8 1794 public function load_grade_item() {
ac9b0805 1795 if (empty($this->grade_item)) {
1796 $this->grade_item = $this->get_grade_item();
1797 }
ab53054f 1798 return $this->grade_item;
1799 }
4a490db0 1800
ab53054f 1801 /**
a153c9f2
AD
1802 * Retrieves this grade categories' associated grade_item from the database
1803 *
1804 * If no grade_item exists yet, creates one.
1805 *
1806 * @return grade_item
ab53054f 1807 */
da3801e8 1808 public function get_grade_item() {
c91ed4be 1809 if (empty($this->id)) {
1810 debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
1811 return false;
1812 }
1813
b3ac6c3e 1814 if (empty($this->parent)) {
1815 $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);
1816
1817 } else {
1818 $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
1819 }
4ac209d5 1820
f3ac8eb4 1821 if (!$grade_items = grade_item::fetch_all($params)) {
b8ff92b6 1822 // create a new one
f3ac8eb4 1823 $grade_item = new grade_item($params, false);
b8ff92b6 1824 $grade_item->gradetype = GRADE_TYPE_VALUE;
f8e6e4db 1825 $grade_item->insert('system');
4a490db0 1826
b79fe189 1827 } else if (count($grade_items) == 1) {
b8ff92b6 1828 // found existing one
1829 $grade_item = reset($grade_items);
4a490db0 1830
b8ff92b6 1831 } else {
1832 debugging("Found more than one grade_item attached to category id:".$this->id);
ac9b0805 1833 // return first one
1834 $grade_item = reset($grade_items);
2c72af1f 1835 }
1836
ab53054f 1837 return $grade_item;
f151b073 1838 }
8c846243 1839
1840 /**
a153c9f2
AD
1841 * Uses $this->parent to instantiate $this->parent_category based on the referenced record in the DB
1842 *
1843 * @return grade_category The parent category
8c846243 1844 */
da3801e8 1845 public function load_parent_category() {
8c846243 1846 if (empty($this->parent_category) && !empty($this->parent)) {
ab53054f 1847 $this->parent_category = $this->get_parent_category();
8c846243 1848 }
1849 return $this->parent_category;
4a490db0 1850 }
1851
ab53054f 1852 /**
a153c9f2
AD
1853 * Uses $this->parent to instantiate and return a grade_category object
1854 *
1855 * @return grade_category Returns the parent category or null if this category has no parent
ab53054f 1856 */
da3801e8 1857 public function get_parent_category() {
ab53054f 1858 if (!empty($this->parent)) {
f3ac8eb4 1859 $parent_category = new grade_category(array('id' => $this->parent));
4a490db0 1860 return $parent_category;
ab53054f 1861 } else {
1862 return null;
1863 }
1864 }
1865
2186f72c 1866 /**
a153c9f2 1867 * Returns the most descriptive field for this grade category
b79fe189 1868 *
2186f72c 1869 * @return string name
1870 */
da3801e8 1871 public function get_name() {
1872 global $DB;
8f6fdf43 1873 // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form)
1874 if (empty($this->parent) && $this->fullname == '?') {
da3801e8 1875 $course = $DB->get_record('course', array('id'=> $this->courseid));
410753fb 1876 return format_string($course->fullname);
b79fe189 1877
314c4336 1878 } else {
1879 return $this->fullname;
1880 }
2186f72c 1881 }
c91ed4be 1882
65c2ac93
DW
1883 /**
1884 * Describe the aggregation settings for this category so the reports make more sense.
1885 *
1886 * @return string description
1887 */
1888 public function get_description() {
1889 $allhelp = array();
1890 $aggrstrings = grade_helper::get_aggregation_strings();
1891 $allhelp[] = $aggrstrings[$this->aggregation];
1892
ded8ea8f 1893 if ($this->droplow && $this->can_apply_limit_rules()) {
65c2ac93
DW
1894 $allhelp[] = get_string('droplowestvalues', 'grades', $this->droplow);
1895 }
ded8ea8f 1896 if ($this->keephigh && $this->can_apply_limit_rules()) {
65c2ac93
DW
1897 $allhelp[] = get_string('keephighestvalues', 'grades', $this->keephigh);
1898 }
1899 if (!$this->aggregateonlygraded) {
1900 $allhelp[] = get_string('aggregatenotonlygraded', 'grades');
1901 }
1902 if ($this->aggregatesubcats) {
1903 $allhelp[] = get_string('aggregatesubcatsshort', 'grades');
1904 }
1905 return implode('. ', $allhelp) . '.';
1906 }
1907
0fc7f624 1908 /**
a153c9f2 1909 * Sets this category's parent id
b79fe189 1910 *
a153c9f2
AD
1911 * @param int $parentid The ID of the category that is the new parent to $this
1912 * @param string $source From where was the object updated (mod/forum, manual, etc.)
1913 * @return bool success
0fc7f624 1914 */
da3801e8 1915 public function set_parent($parentid, $source=null) {
f13002d5 1916 if ($this->parent == $parentid) {
1917 return true;
1918 }
1919
1920 if ($parentid == $this->id) {
2f137aa1 1921 print_error('cannotassignselfasparent');
f13002d5 1922 }
1923
1924 if (empty($this->parent) and $this->is_course_category()) {
2f137aa1 1925 print_error('cannothaveparentcate');
b3ac6c3e 1926 }
f13002d5 1927
1928 // find parent and check course id
f3ac8eb4 1929 if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
b3ac6c3e 1930 return false;
1931 }
1932
f8e6e4db 1933 $this->force_regrading();
b3ac6c3e 1934
1935 // set new parent category
f8e6e4db 1936 $this->parent = $parent_category->id;
1937 $this->parent_category =& $parent_category;
b3ac6c3e 1938 $this->path = null; // remove old path and depth - will be recalculated in update()
ec3717e1 1939 $this->depth = 0; // remove old path and depth - will be recalculated in update()
f8e6e4db 1940 $this->update($source);
b3ac6c3e 1941
15b462da 1942 return $this->update($source);
b3ac6c3e 1943 }
1944
1945 /**
a153c9f2 1946 * Returns the final grade values for this grade category.
b79fe189 1947 *
a153c9f2 1948 * @param int $userid Optional user ID to retrieve a single user's final grade
b3ac6c3e 1949 * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
1950 */
b79fe189 1951 public function get_final($userid=null) {
b3ac6c3e 1952 $this->load_grade_item();
1953 return $this->grade_item->get_final($userid);
0fc7f624 1954 }
4a490db0 1955
0fc7f624 1956 /**
a153c9f2
AD
1957 * Returns the sortorder of the grade categories' associated grade_item
1958 *
1959 * This method is also available in grade_item for cases where the object type is not known.
b79fe189 1960 *
0fc7f624 1961 * @return int Sort order
1962 */
da3801e8 1963 public function get_sortorder() {
b3ac6c3e 1964 $this->load_grade_item();
1965 return $this->grade_item->get_sortorder();
0fc7f624 1966 }
1967
be7c0693 1968 /**
a153c9f2
AD
1969 * Returns the idnumber of the grade categories' associated grade_item.
1970 *
1971 * This method is also available in grade_item for cases where the object type is not known.
b79fe189 1972 *
be7c0693 1973 * @return string idnumber
1974 */
da3801e8 1975 public function get_idnumber() {
be7c0693 1976 $this->load_grade_item();
1977 return $this->grade_item->get_idnumber();
1978 }
1979
0fc7f624 1980 /**
a153c9f2
AD
1981 * Sets the sortorder variable for this category.
1982 *
4a490db0 1983 * This method is also available in grade_item, for cases where the object type is not know.
b79fe189 1984 *
1985 * @param int $sortorder The sortorder to assign to this category
0fc7f624 1986 */
da3801e8 1987 public function set_sortorder($sortorder) {
b3ac6c3e 1988 $this->load_grade_item();
1989 $this->grade_item->set_sortorder($sortorder);
1990 }
1991
6639ead3 1992 /**
a153c9f2 1993 * Move this category after the given sortorder
b79fe189 1994 *
a153c9f2 1995 * Does not change the parent
b79fe189 1996 *
a153c9f2 1997 * @param int $sortorder to place after.
b79fe189 1998 * @return void
6639ead3 1999 */
da3801e8 2000 public function move_after_sortorder($sortorder) {
f13002d5 2001 $this->load_grade_item();
2002 $this->grade_item->move_after_sortorder($sortorder);
2003 }
2004
b3ac6c3e 2005 /**
f13002d5 2006 * Return true if this is the top most category that represents the total course grade.
b79fe189 2007 *
a153c9f2 2008 * @return bool
b3ac6c3e 2009 */
da3801e8 2010 public function is_course_category() {
b3ac6c3e 2011 $this->load_grade_item();
2012 return $this->grade_item->is_course_item();
2013 }
2014
2015 /**
a153c9f2 2016 * Return the course level grade_category object
b79fe189 2017 *
2018 * @param int $courseid The Course ID
a153c9f2 2019 * @return grade_category Returns the course level grade_category instance
b3ac6c3e 2020 */
22a9b6d8 2021 public static function fetch_course_category($courseid) {
a4503119 2022 if (empty($courseid)) {
2023 debugging('Missing course id!');
2024 return false;
2025 }
b3ac6c3e 2026
2027 // course category has no parent
f3ac8eb4 2028 if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
b3ac6c3e 2029 return $course_category;
2030 }
2031
2032 // create a new one
f3ac8eb4 2033 $course_category = new grade_category();
b3ac6c3e 2034 $course_category->insert_course_category($courseid);
2035
2036 return $course_category;
0fc7f624 2037 }
4ac209d5 2038
79eabc2a 2039 /**
2040 * Is grading object editable?
b79fe189 2041 *
a153c9f2 2042 * @return bool
79eabc2a 2043 */
da3801e8 2044 public function is_editable() {
79eabc2a 2045 return true;
2046 }
2047
5fad5061 2048 /**
a153c9f2
AD
2049 * Returns the locked state/date of the grade categories' associated grade_item.
2050 *
2051 * This method is also available in grade_item, for cases where the object type is not known.
2052 *
2053 * @return bool
5fad5061 2054 */
da3801e8 2055 public function is_locked() {
5fad5061 2056 $this->load_grade_item();
22e23c78 2057 return $this->grade_item->is_locked();
5fad5061 2058 }
2059
2060 /**
2061 * Sets the grade_item's locked variable and updates the grade_item.
a153c9f2
AD
2062 *
2063 * Calls set_locked() on the categories' grade_item
b79fe189 2064 *
2065 * @param int $lockedstate 0, 1 or a timestamp int(10) after which date the item will be locked.
2066 * @param bool $cascade lock/unlock child objects too
2067 * @param bool $refresh refresh grades when unlocking
a153c9f2 2068 * @return bool success if category locked (not all children mayb be locked though)
5fad5061 2069 */
da3801e8 2070 public function set_locked($lockedstate, $cascade=false, $refresh=true) {
5fad5061 2071 $this->load_grade_item();
2b0f65e2 2072
fb0e3570 2073 $result = $this->grade_item->set_locked($lockedstate, $cascade, true);
2074
2075 if ($cascade) {
2076 //process all children - items and categories
f3ac8eb4 2077 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
b79fe189 2078
2079 foreach ($children as $child) {
fb0e3570 2080 $child->set_locked($lockedstate, true, false);
b79fe189 2081
fb0e3570 2082 if (empty($lockedstate) and $refresh) {
2083 //refresh when unlocking
2084 $child->refresh_grades();
2085 }
2b0f65e2 2086 }
7a7a53d3 2087 }
b79fe189 2088
f3ac8eb4 2089 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
b79fe189 2090
2091 foreach ($children as $child) {
fb0e3570 2092 $child->set_locked($lockedstate, true, true);
2093 }
7a7a53d3 2094 }
2095 }
2b0f65e2 2096
b121b544 2097 return $result;
5fad5061 2098 }
4a490db0 2099
a153c9f2
AD
2100 /**
2101 * Overrides grade_object::set_properties() to add special handling for changes to category aggregation types
2102 *
2103 * @param stdClass $instance the object to set the properties on
2104 * @param array|stdClass $params Either an associative array or an object containing property name, property value pairs
2105 */
79b260cc
AD
2106 public static function set_properties(&$instance, $params) {
2107 global $DB;
2108
2109 parent::set_properties($instance, $params);
2110
c1024411 2111 //if they've changed aggregation type we made need to do some fiddling to provide appropriate defaults
79b260cc
AD
2112 if (!empty($params->aggregation)) {
2113
2114 //weight and extra credit share a column :( Would like a default of 1 for weight and 0 for extra credit
2115 //Flip from the default of 0 to 1 (or vice versa) if ALL items in the category are still set to the old default.
134c514b 2116 if (self::aggregation_uses_aggregationcoef($params->aggregation)) {
79b260cc 2117 $sql = $defaultaggregationcoef = null;
7ad5a627 2118
134c514b 2119 if (!self::aggregation_uses_extracredit($params->aggregation)) {
79b260cc
AD
2120 //if all items in this category have aggregation coefficient of 0 we can change it to 1 ie evenly weighted
2121 $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=0";
2122 $defaultaggregationcoef = 1;
134c514b 2123 } else {
79b260cc
AD
2124 //if all items in this category have aggregation coefficient of 1 we can change it to 0 ie no extra credit
2125 $sql = "select count(id) from {grade_items} where categoryid=:categoryid and aggregationcoef!=1";
2126 $defaultaggregationcoef = 0;
2127 }
2128
2129 $params = array('categoryid'=>$instance->id);
2130 $count = $DB->count_records_sql($sql, $params);
2131 if ($count===0) { //category is either empty or all items are set to a default value so we can switch defaults
2132 $params['aggregationcoef'] = $defaultaggregationcoef;
2133 $DB->execute("update {grade_items} set aggregationcoef=:aggregationcoef where categoryid=:categoryid",$params);
2134 }
2135 }
2136 }
2137 }
2138
5fad5061 2139 /**
4a490db0 2140 * Sets the grade_item's hidden variable and updates the grade_item.
a153c9f2
AD
2141 *
2142 * Overrides grade_item::set_hidden() to add cascading of the hidden value to grade items in this grade category
2143 *
2144 * @param int $hidden 0 mean always visible, 1 means always hidden and a number > 1 is a timestamp to hide until
2145 * @param bool $cascade apply to child objects too
5fad5061 2146 */
da3801e8 2147 public function set_hidden($hidden, $cascade=false) {
5fad5061 2148 $this->load_grade_item();
a25bb902 2149 //this hides the associated grade item (the course total)
1762a264 2150 $this->grade_item->set_hidden($hidden, $cascade);
a25bb902
AD
2151 //this hides the category itself and everything it contains
2152 parent::set_hidden($hidden, $cascade);
b79fe189 2153
f60c61b1 2154 if ($cascade) {
b79fe189 2155
f3ac8eb4 2156 if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
b79fe189 2157
2158 foreach ($children as $child) {
39873128
TH
2159 if ($child->can_control_visibility()) {
2160 $child->set_hidden($hidden, $cascade);
2161 }
f60c61b1 2162 }
f13002d5 2163 }
b79fe189 2164
f3ac8eb4 2165 if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
b79fe189 2166
2167 foreach ($children as $child) {
f60c61b1 2168 $child->set_hidden($hidden, $cascade);
2169 }
f13002d5 2170 }
2171 }
d90aa634
AD
2172
2173 //if marking category visible make sure parent category is visible MDL-21367
2174 if( !$hidden ) {
2175 $category_array = grade_category::fetch_all(array('id'=>$this->parent));
2176 if ($category_array && array_key_exists($this->parent, $category_array)) {
2177 $category = $category_array[$this->parent];
2178 //call set_hidden on the category regardless of whether it is hidden as its parent might be hidden
2179 //if($category->is_hidden()) {
2180 $category->set_hidden($hidden, false);
2181 //}
2182 }
2183 }
5fad5061 2184 }
89a5f827 2185
190af29f 2186 /**
2187 * Applies default settings on this category
a153c9f2
AD
2188 *
2189 * @return bool True if anything changed
190af29f 2190 */
da3801e8 2191 public function apply_default_settings() {
190af29f 2192 global $CFG;
2193
2194 foreach ($this->forceable as $property) {
b79fe189 2195
190af29f 2196 if (isset($CFG->{"grade_$property"})) {
b79fe189 2197
190af29f 2198 if ($CFG->{"grade_$property"} == -1) {
2199 continue; //temporary bc before version bump
2200 }
2201 $this->$property = $CFG->{"grade_$property"};
2202 }
2203 }
2204 }
2205
89a5f827 2206 /**
2207 * Applies forced settings on this category
a153c9f2
AD
2208 *
2209 * @return bool True if anything changed
89a5f827 2210 */
da3801e8 2211 public function apply_forced_settings() {
89a5f827 2212 global $CFG;
2213
2214 $updated = false;
b79fe189 2215
89a5f827 2216 foreach ($this->forceable as $property) {
b79fe189 2217
2218 if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and
2219 ((int) $CFG->{"grade_{$property}_flag"} & 1)) {
2220
190af29f 2221 if ($CFG->{"grade_$property"} == -1) {
2222 continue; //temporary bc before version bump
2223 }
89a5f827 2224 $this->$property = $CFG->{"grade_$property"};
2225 $updated = true;
2226 }
2227 }
2228
2229 return $updated;
2230 }
2231
2232 /**
2233 * Notification of change in forced category settings.
b79fe189 2234 *
a153c9f2 2235 * Causes all course and category grade items to be marked as needing to be updated
89a5f827 2236 */
da3801e8 2237 public static function updated_forced_settings() {
5b0af8c5 2238 global $CFG, $DB;
2239 $params = array(1, 'course', 'category');
2240 $sql = "UPDATE {grade_items} SET needsupdate=? WHERE itemtype=? or itemtype=?";
2241 $DB->execute($sql, $params);
89a5f827 2242 }
4a490db0 2243}