[ Index ]

PHP Cross Reference of Moodle 1.9.3 [Build 15-Oct-2008]

title

Body

[close]

/lib/grade/ -> grade_category.php (source)

   1  <?php // $Id: grade_category.php,v 1.96.2.21 2008/03/12 19:44:39 skodak Exp $
   2  
   3  ///////////////////////////////////////////////////////////////////////////
   4  //                                                                       //
   5  // NOTICE OF COPYRIGHT                                                   //
   6  //                                                                       //
   7  // Moodle - Modular Object-Oriented Dynamic Learning Environment         //
   8  //          http://moodle.com                                            //
   9  //                                                                       //
  10  // Copyright (C) 1999 onwards Martin Dougiamas  http://dougiamas.com     //
  11  //                                                                       //
  12  // This program is free software; you can redistribute it and/or modify  //
  13  // it under the terms of the GNU General Public License as published by  //
  14  // the Free Software Foundation; either version 2 of the License, or     //
  15  // (at your option) any later version.                                   //
  16  //                                                                       //
  17  // This program is distributed in the hope that it will be useful,       //
  18  // but WITHOUT ANY WARRANTY; without even the implied warranty of        //
  19  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         //
  20  // GNU General Public License for more details:                          //
  21  //                                                                       //
  22  //          http://www.gnu.org/copyleft/gpl.html                         //
  23  //                                                                       //
  24  ///////////////////////////////////////////////////////////////////////////
  25  
  26  require_once ('grade_object.php');
  27  
  28  class grade_category extends grade_object {
  29      /**
  30       * The DB table.
  31       * @var string $table
  32       */
  33      var $table = 'grade_categories';
  34  
  35      /**
  36       * Array of required table fields, must start with 'id'.
  37       * @var array $required_fields
  38       */
  39      var $required_fields = array('id', 'courseid', 'parent', 'depth', 'path', 'fullname', 'aggregation',
  40                                   'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes',
  41                                   'aggregatesubcats', 'timecreated', 'timemodified');
  42  
  43      /**
  44       * The course this category belongs to.
  45       * @var int $courseid
  46       */
  47      var $courseid;
  48  
  49      /**
  50       * The category this category belongs to (optional).
  51       * @var int $parent
  52       */
  53      var $parent;
  54  
  55      /**
  56       * The grade_category object referenced by $this->parent (PK).
  57       * @var object $parent_category
  58       */
  59      var $parent_category;
  60  
  61      /**
  62       * The number of parents this category has.
  63       * @var int $depth
  64       */
  65      var $depth = 0;
  66  
  67      /**
  68       * Shows the hierarchical path for this category as /1/2/3/ (like course_categories), the last number being
  69       * this category's autoincrement ID number.
  70       * @var string $path
  71       */
  72      var $path;
  73  
  74      /**
  75       * The name of this category.
  76       * @var string $fullname
  77       */
  78      var $fullname;
  79  
  80      /**
  81       * A constant pointing to one of the predefined aggregation strategies (none, mean, median, sum etc) .
  82       * @var int $aggregation
  83       */
  84      var $aggregation = GRADE_AGGREGATE_MEAN;
  85  
  86      /**
  87       * Keep only the X highest items.
  88       * @var int $keephigh
  89       */
  90      var $keephigh = 0;
  91  
  92      /**
  93       * Drop the X lowest items.
  94       * @var int $droplow
  95       */
  96      var $droplow = 0;
  97  
  98      /**
  99       * Aggregate only graded items
 100       * @var int $aggregateonlygraded
 101       */
 102      var $aggregateonlygraded = 0;
 103  
 104      /**
 105       * Aggregate outcomes together with normal items
 106       * @var int $aggregateoutcomes
 107       */
 108      var $aggregateoutcomes = 0;
 109  
 110      /**
 111       * Ignore subcategories when aggregating
 112       * @var int $aggregatesubcats
 113       */
 114      var $aggregatesubcats = 0;
 115  
 116      /**
 117       * Array of grade_items or grade_categories nested exactly 1 level below this category
 118       * @var array $children
 119       */
 120      var $children;
 121  
 122      /**
 123       * A hierarchical array of all children below this category. This is stored separately from
 124       * $children because it is more memory-intensive and may not be used as often.
 125       * @var array $all_children
 126       */
 127      var $all_children;
 128  
 129      /**
 130       * An associated grade_item object, with itemtype=category, used to calculate and cache a set of grade values
 131       * for this category.
 132       * @var object $grade_item
 133       */
 134      var $grade_item;
 135  
 136      /**
 137       * Temporary sortorder for speedup of children resorting
 138       */
 139      var $sortorder;
 140  
 141      /**
 142       * List of options which can be "forced" from site settings.
 143       */
 144      var $forceable = array('aggregation', 'keephigh', 'droplow', 'aggregateonlygraded', 'aggregateoutcomes', 'aggregatesubcats');
 145  
 146      /**
 147       * Builds this category's path string based on its parents (if any) and its own id number.
 148       * This is typically done just before inserting this object in the DB for the first time,
 149       * or when a new parent is added or changed. It is a recursive function: once the calling
 150       * object no longer has a parent, the path is complete.
 151       *
 152       * @static
 153       * @param object $grade_category
 154       * @return int The depth of this category (2 means there is one parent)
 155       */
 156      function build_path($grade_category) {
 157          if (empty($grade_category->parent)) {
 158              return '/'.$grade_category->id.'/';
 159          } else {
 160              $parent = get_record('grade_categories', 'id', $grade_category->parent);
 161              return grade_category::build_path($parent).$grade_category->id.'/';
 162          }
 163      }
 164  
 165      /**
 166       * Finds and returns a grade_category instance based on params.
 167       * @static
 168       *
 169       * @param array $params associative arrays varname=>value
 170       * @return object grade_category instance or false if none found.
 171       */
 172      function fetch($params) {
 173          return grade_object::fetch_helper('grade_categories', 'grade_category', $params);
 174      }
 175  
 176      /**
 177       * Finds and returns all grade_category instances based on params.
 178       * @static
 179       *
 180       * @param array $params associative arrays varname=>value
 181       * @return array array of grade_category insatnces or false if none found.
 182       */
 183      function fetch_all($params) {
 184          return grade_object::fetch_all_helper('grade_categories', 'grade_category', $params);
 185      }
 186  
 187      /**
 188       * In addition to update() as defined in grade_object, call force_regrading of parent categories, if applicable.
 189       * @param string $source from where was the object updated (mod/forum, manual, etc.)
 190       * @return boolean success
 191       */
 192      function update($source=null) {
 193          // load the grade item or create a new one
 194          $this->load_grade_item();
 195  
 196          // force recalculation of path;
 197          if (empty($this->path)) {
 198              $this->path  = grade_category::build_path($this);
 199              $this->depth = substr_count($this->path, '/') - 1;
 200              $updatechildren = true;
 201          } else {
 202              $updatechildren = false;
 203          }
 204  
 205          $this->apply_forced_settings();
 206  
 207          // these are exclusive
 208          if ($this->droplow > 0) {
 209              $this->keephigh = 0;
 210          } else if ($this->keephigh > 0) {
 211              $this->droplow = 0;
 212          }
 213  
 214          // Recalculate grades if needed
 215          if ($this->qualifies_for_regrading()) {
 216              $this->force_regrading();
 217          }
 218  
 219          $this->timemodified = time();
 220  
 221          $result = parent::update($source);
 222  
 223          // now update paths in all child categories
 224          if ($result and $updatechildren) {
 225              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
 226                  foreach ($children as $child) {
 227                      $child->path  = null;
 228                      $child->depth = 0;
 229                      $child->update($source);
 230                  } 
 231              }
 232          }
 233  
 234          return $result;
 235      }
 236  
 237      /**
 238       * If parent::delete() is successful, send force_regrading message to parent category.
 239       * @param string $source from where was the object deleted (mod/forum, manual, etc.)
 240       * @return boolean success
 241       */
 242      function delete($source=null) {
 243          $grade_item = $this->load_grade_item();
 244  
 245          if ($this->is_course_category()) {
 246              if ($categories = grade_category::fetch_all(array('courseid'=>$this->courseid))) {
 247                  foreach ($categories as $category) {
 248                      if ($category->id == $this->id) {
 249                          continue; // do not delete course category yet
 250                      }
 251                      $category->delete($source);
 252                  }
 253              }
 254  
 255              if ($items = grade_item::fetch_all(array('courseid'=>$this->courseid))) {
 256                  foreach ($items as $item) {
 257                      if ($item->id == $grade_item->id) {
 258                          continue; // do not delete course item yet
 259                      }
 260                      $item->delete($source);
 261                  }
 262              }
 263  
 264          } else {
 265              $this->force_regrading();
 266  
 267              $parent = $this->load_parent_category();
 268  
 269              // Update children's categoryid/parent field first
 270              if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
 271                  foreach ($children as $child) {
 272                      $child->set_parent($parent->id);
 273                  }
 274              }
 275              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
 276                  foreach ($children as $child) {
 277                      $child->set_parent($parent->id);
 278                  }
 279              }
 280          }
 281  
 282          // first delete the attached grade item and grades
 283          $grade_item->delete($source);
 284  
 285          // delete category itself
 286          return parent::delete($source);
 287      }
 288  
 289      /**
 290       * In addition to the normal insert() defined in grade_object, this method sets the depth
 291       * and path for this object, and update the record accordingly. The reason why this must
 292       * be done here instead of in the constructor, is that they both need to know the record's
 293       * id number, which only gets created at insertion time.
 294       * This method also creates an associated grade_item if this wasn't done during construction.
 295       * @param string $source from where was the object inserted (mod/forum, manual, etc.)
 296       * @return int PK ID if successful, false otherwise
 297       */
 298      function insert($source=null) {
 299  
 300          if (empty($this->courseid)) {
 301              error('Can not insert grade category without course id!');
 302          }
 303  
 304          if (empty($this->parent)) {
 305              $course_category = grade_category::fetch_course_category($this->courseid);
 306              $this->parent = $course_category->id;
 307          }
 308  
 309          $this->path = null;
 310  
 311          $this->timecreated = $this->timemodified = time();
 312  
 313          if (!parent::insert($source)) {
 314              debugging("Could not insert this category: " . print_r($this, true));
 315              return false;
 316          }
 317  
 318          $this->force_regrading();
 319  
 320          // build path and depth
 321          $this->update($source);
 322  
 323          return $this->id;
 324      }
 325  
 326      /**
 327       * Internal function - used only from fetch_course_category()
 328       * Normal insert() can not be used for course category
 329       * @param int $courseid
 330       * @return bool success
 331       */
 332      function insert_course_category($courseid) {
 333          $this->courseid    = $courseid;
 334          $this->fullname    = '?';
 335          $this->path        = null;
 336          $this->parent      = null;
 337          $this->aggregation = GRADE_AGGREGATE_WEIGHTED_MEAN2;
 338  
 339          $this->apply_default_settings();
 340          $this->apply_forced_settings();
 341  
 342          $this->timecreated = $this->timemodified = time();
 343  
 344          if (!parent::insert('system')) {
 345              debugging("Could not insert this category: " . print_r($this, true));
 346              return false;
 347          }
 348  
 349          // build path and depth
 350          $this->update('system');
 351  
 352          return $this->id;
 353      }
 354  
 355      /**
 356       * Compares the values held by this object with those of the matching record in DB, and returns
 357       * whether or not these differences are sufficient to justify an update of all parent objects.
 358       * This assumes that this object has an id number and a matching record in DB. If not, it will return false.
 359       * @return boolean
 360       */
 361      function qualifies_for_regrading() {
 362          if (empty($this->id)) {
 363              debugging("Can not regrade non existing category");
 364              return false;
 365          }
 366  
 367          $db_item = grade_category::fetch(array('id'=>$this->id));
 368  
 369          $aggregationdiff = $db_item->aggregation         != $this->aggregation;
 370          $keephighdiff    = $db_item->keephigh            != $this->keephigh;
 371          $droplowdiff     = $db_item->droplow             != $this->droplow;
 372          $aggonlygrddiff  = $db_item->aggregateonlygraded != $this->aggregateonlygraded;
 373          $aggoutcomesdiff = $db_item->aggregateoutcomes   != $this->aggregateoutcomes;
 374          $aggsubcatsdiff  = $db_item->aggregatesubcats    != $this->aggregatesubcats;
 375  
 376          return ($aggregationdiff || $keephighdiff || $droplowdiff || $aggonlygrddiff || $aggoutcomesdiff || $aggsubcatsdiff);
 377      }
 378  
 379      /**
 380       * Marks the category and course item as needing update - categories are always regraded.
 381       * @return void
 382       */
 383      function force_regrading() {
 384          $grade_item = $this->load_grade_item();
 385          $grade_item->force_regrading();
 386      }
 387  
 388      /**
 389       * Generates and saves final grades in associated category grade item.
 390       * These immediate children must already have their own final grades.
 391       * The category's aggregation method is used to generate final grades.
 392       *
 393       * Please note that category grade is either calculated or aggregated - not both at the same time.
 394       *
 395       * This method must be used ONLY from grade_item::regrade_final_grades(),
 396       * because the calculation must be done in correct order!
 397       *
 398       * Steps to follow:
 399       *  1. Get final grades from immediate children
 400       *  3. Aggregate these grades
 401       *  4. Save them in final grades of associated category grade item
 402       */
 403      function generate_grades($userid=null) {
 404          global $CFG;
 405  
 406          $this->load_grade_item();
 407  
 408          if ($this->grade_item->is_locked()) {
 409              return true; // no need to recalculate locked items
 410          }
 411  
 412          // find grade items of immediate children (category or grade items) and force site settings
 413          $depends_on = $this->grade_item->depends_on();
 414  
 415          if (empty($depends_on)) {
 416              $items = false;
 417          } else {
 418              $gis = implode(',', $depends_on);
 419              $sql = "SELECT *
 420                        FROM {$CFG->prefix}grade_items
 421                       WHERE id IN ($gis)";
 422              $items = get_records_sql($sql);
 423          }
 424  
 425          if ($userid) {
 426              $usersql = "AND g.userid=$userid";
 427          } else {
 428              $usersql = "";
 429          }
 430  
 431          $grade_inst = new grade_grade();
 432          $fields = 'g.'.implode(',g.', $grade_inst->required_fields);
 433  
 434          // where to look for final grades - include grade of this item too, we will store the results there
 435          $gis = implode(',', array_merge($depends_on, array($this->grade_item->id)));
 436          $sql = "SELECT $fields
 437                    FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
 438                   WHERE gi.id = g.itemid AND gi.id IN ($gis) $usersql
 439                ORDER BY g.userid";
 440  
 441          // group the results by userid and aggregate the grades for this user
 442          if ($rs = get_recordset_sql($sql)) {
 443              $prevuser = 0;
 444              $grade_values = array();
 445              $excluded     = array();
 446              $oldgrade     = null;
 447              while ($used = rs_fetch_next_record($rs)) {
 448                  if ($used->userid != $prevuser) {
 449                      $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);
 450                      $prevuser = $used->userid;
 451                      $grade_values = array();
 452                      $excluded     = array();
 453                      $oldgrade     = null;
 454                  }
 455                  $grade_values[$used->itemid] = $used->finalgrade;
 456                  if ($used->excluded) {
 457                      $excluded[] = $used->itemid;
 458                  }
 459                  if ($this->grade_item->id == $used->itemid) {
 460                      $oldgrade = $used;
 461                  }
 462              }
 463              $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded);//the last one
 464              rs_close($rs);
 465          }
 466  
 467          return true;
 468      }
 469  
 470      /**
 471       * internal function for category grades aggregation
 472       *
 473       * @param int $userid
 474       * @param array $items
 475       * @param array $grade_values
 476       * @param object $oldgrade
 477       * @param bool $excluded
 478       * @return boolean (just plain return;)
 479       */
 480      function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded) {
 481          global $CFG;
 482          if (empty($userid)) {
 483              //ignore first call
 484              return;
 485          }
 486  
 487          if ($oldgrade) {
 488              $oldfinalgrade = $oldgrade->finalgrade;
 489              $grade = new grade_grade($oldgrade, false);
 490              $grade->grade_item =& $this->grade_item;
 491  
 492          } else {
 493              // insert final grade - it will be needed later anyway
 494              $grade = new grade_grade(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
 495              $grade->grade_item =& $this->grade_item;
 496              $grade->insert('system');
 497              $oldfinalgrade = null;
 498          }
 499  
 500          // no need to recalculate locked or overridden grades
 501          if ($grade->is_locked() or $grade->is_overridden()) {
 502              return;
 503          }
 504  
 505          // can not use own final category grade in calculation
 506          unset($grade_values[$this->grade_item->id]);
 507  
 508  
 509      /// sum is a special aggregation types - it adjusts the min max, does not use relative values
 510          if ($this->aggregation == GRADE_AGGREGATE_SUM) {
 511              $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded);
 512              return;
 513          }
 514  
 515          // if no grades calculation possible or grading not allowed clear final grade
 516          if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
 517              $grade->finalgrade = null;
 518              if (!is_null($oldfinalgrade)) {
 519                  $grade->update('aggregation');
 520              }
 521              return;
 522          }
 523  
 524      /// normalize the grades first - all will have value 0...1
 525          // ungraded items are not used in aggregation
 526          foreach ($grade_values as $itemid=>$v) {
 527              if (is_null($v)) {
 528                  // null means no grade
 529                  unset($grade_values[$itemid]);
 530                  continue;
 531              } else if (in_array($itemid, $excluded)) {
 532                  unset($grade_values[$itemid]);
 533                  continue;
 534              }
 535              $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1);
 536          }
 537  
 538          // use min grade if grade missing for these types
 539          if (!$this->aggregateonlygraded) {
 540              foreach($items as $itemid=>$value) {
 541                  if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
 542                      $grade_values[$itemid] = 0;
 543                  }
 544              }
 545          }
 546  
 547          // limit and sort
 548          $this->apply_limit_rules($grade_values);
 549          asort($grade_values, SORT_NUMERIC);
 550  
 551          // let's see we have still enough grades to do any statistics
 552          if (count($grade_values) == 0) {
 553              // not enough attempts yet
 554              $grade->finalgrade = null;
 555              if (!is_null($oldfinalgrade)) {
 556                  $grade->update('aggregation');
 557              }
 558              return;
 559          }
 560  
 561          // do the maths
 562          $agg_grade = $this->aggregate_values($grade_values, $items);
 563  
 564          // recalculate the grade back to requested range
 565          $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax);
 566  
 567          if (is_null($finalgrade)) {
 568              $grade->finalgrade = null;
 569          } else {
 570              $grade->finalgrade = (float)bounded_number($this->grade_item->grademin, $finalgrade, $this->grade_item->grademax);
 571          }
 572  
 573          // update in db if changed
 574          if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
 575              $grade->update('aggregation');
 576          }
 577  
 578          return;
 579      }
 580  
 581      /**
 582       * Internal function - aggregation maths.
 583       */
 584      function aggregate_values($grade_values, $items) {
 585          switch ($this->aggregation) {
 586              case GRADE_AGGREGATE_MEDIAN: // Middle point value in the set: ignores frequencies
 587                  $num = count($grade_values);
 588                  $grades = array_values($grade_values);
 589                  if ($num % 2 == 0) {
 590                      $agg_grade = ($grades[intval($num/2)-1] + $grades[intval($num/2)]) / 2;
 591                  } else {
 592                      $agg_grade = $grades[intval(($num/2)-0.5)];
 593                  }
 594                  break;
 595  
 596              case GRADE_AGGREGATE_MIN:
 597                  $agg_grade = reset($grade_values);
 598                  break;
 599  
 600              case GRADE_AGGREGATE_MAX:
 601                  $agg_grade = array_pop($grade_values);
 602                  break;
 603  
 604              case GRADE_AGGREGATE_MODE:       // the most common value, average used if multimode
 605                  $freq = array_count_values($grade_values);
 606                  arsort($freq);                      // sort by frequency keeping keys
 607                  $top = reset($freq);               // highest frequency count
 608                  $modes = array_keys($freq, $top);  // search for all modes (have the same highest count)
 609                  rsort($modes, SORT_NUMERIC);       // get highes mode
 610                  $agg_grade = reset($modes);
 611                  break;
 612  
 613              case GRADE_AGGREGATE_WEIGHTED_MEAN: // Weighted average of all existing final grades, weight specified in coef
 614                  $weightsum = 0;
 615                  $sum       = 0;
 616                  foreach($grade_values as $itemid=>$grade_value) {
 617                      if ($items[$itemid]->aggregationcoef <= 0) {
 618                          continue;
 619                      }
 620                      $weightsum += $items[$itemid]->aggregationcoef;
 621                      $sum       += $items[$itemid]->aggregationcoef * $grade_value;
 622                  }
 623                  if ($weightsum == 0) {
 624                      $agg_grade = null;
 625                  } else {
 626                      $agg_grade = $sum / $weightsum;
 627                  }
 628                  break;
 629  
 630              case GRADE_AGGREGATE_WEIGHTED_MEAN2: // Weighted average of all existing final grades, weight is the range of grade (ususally grademax)
 631                  $weightsum = 0;
 632                  $sum       = 0;
 633                  foreach($grade_values as $itemid=>$grade_value) {
 634                      $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
 635                      if ($weight <= 0) {
 636                          continue;
 637                      }
 638                      $weightsum += $weight;
 639                      $sum       += $weight * $grade_value;
 640                  }
 641                  if ($weightsum == 0) {
 642                      $agg_grade = null;
 643                  } else {
 644                      $agg_grade = $sum / $weightsum;
 645                  }
 646                  break;
 647  
 648              case GRADE_AGGREGATE_EXTRACREDIT_MEAN: // special average
 649                  $num = 0;
 650                  $sum = 0;
 651                  foreach($grade_values as $itemid=>$grade_value) {
 652                      if ($items[$itemid]->aggregationcoef == 0) {
 653                          $num += 1;
 654                          $sum += $grade_value;
 655                      } else if ($items[$itemid]->aggregationcoef > 0) {
 656                          $sum += $items[$itemid]->aggregationcoef * $grade_value;
 657                      }
 658                  }
 659                  if ($num == 0) {
 660                      $agg_grade = $sum; // only extra credits or wrong coefs
 661                  } else {
 662                      $agg_grade = $sum / $num;
 663                  }
 664                  break;
 665  
 666              case GRADE_AGGREGATE_MEAN:    // Arithmetic average of all grade items (if ungraded aggregated, NULL counted as minimum)
 667              default:
 668                  $num = count($grade_values);
 669                  $sum = array_sum($grade_values);
 670                  $agg_grade = $sum / $num;
 671                  break;
 672          }
 673  
 674          return $agg_grade;
 675      }
 676  
 677      /**
 678       * internal function for category grades summing
 679       *
 680       * @param object $grade
 681       * @param int $userid
 682       * @param float $oldfinalgrade
 683       * @param array $items
 684       * @param array $grade_values
 685       * @param bool $excluded
 686       * @return boolean (just plain return;)
 687       */
 688      function sum_grades(&$grade, $oldfinalgrade, $items, $grade_values, $excluded) {
 689          // ungraded and exluded items are not used in aggregation
 690          foreach ($grade_values as $itemid=>$v) {
 691              if (is_null($v)) {
 692                  unset($grade_values[$itemid]);
 693              } else if (in_array($itemid, $excluded)) {
 694                  unset($grade_values[$itemid]);
 695              }
 696          }
 697  
 698          // use 0 if grade missing, droplow used and aggregating all items
 699          if (!$this->aggregateonlygraded and !empty($this->droplow)) {
 700              foreach($items as $itemid=>$value) {
 701                  if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
 702                      $grade_values[$itemid] = 0;
 703                  }
 704              }
 705          }
 706  
 707          $max = 0;
 708  
 709          //find max grade
 710          foreach ($items as $item) {
 711              if ($item->aggregationcoef > 0) {
 712                  // extra credit from this activity - does not affect total
 713                  continue;
 714              }
 715              if ($item->gradetype == GRADE_TYPE_VALUE) {
 716                  $max += $item->grademax;
 717              } else if ($item->gradetype == GRADE_TYPE_SCALE) {
 718                  $max += $item->grademax - 1; // scales min is 1
 719              }
 720          }
 721  
 722          if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE){
 723              $this->grade_item->grademax = $max;
 724              $this->grade_item->grademin = 0;
 725              $this->grade_item->gradetype = GRADE_TYPE_VALUE;
 726              $this->grade_item->update('aggregation');
 727          }
 728  
 729          $this->apply_limit_rules($grade_values);
 730  
 731          $sum = array_sum($grade_values);
 732          $grade->finalgrade = bounded_number($this->grade_item->grademin, $sum, $this->grade_item->grademax);
 733  
 734          // update in db if changed
 735          if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
 736              $grade->update('aggregation');
 737          }
 738  
 739          return;
 740      }
 741  
 742      /**
 743       * Given an array of grade values (numerical indices), applies droplow or keephigh
 744       * rules to limit the final array.
 745       * @param array $grade_values
 746       * @return array Limited grades.
 747       */
 748      function apply_limit_rules(&$grade_values) {
 749          arsort($grade_values, SORT_NUMERIC);
 750          if (!empty($this->droplow)) {
 751              for ($i = 0; $i < $this->droplow; $i++) {
 752                  array_pop($grade_values);
 753              }
 754          } elseif (!empty($this->keephigh)) {
 755              while (count($grade_values) > $this->keephigh) {
 756                  array_pop($grade_values);
 757              }
 758          }
 759      }
 760  
 761  
 762      /**
 763       * Returns true if category uses special aggregation coeficient
 764       * @return boolean true if coeficient used
 765       */
 766      function is_aggregationcoef_used() {
 767          return ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN
 768               or $this->aggregation == GRADE_AGGREGATE_EXTRACREDIT_MEAN
 769               or $this->aggregation == GRADE_AGGREGATE_SUM);
 770  
 771      }
 772  
 773      /**
 774       * Returns tree with all grade_items and categories as elements
 775       * @static
 776       * @param int $courseid
 777       * @param boolean $include_category_items as category children
 778       * @return array
 779       */
 780      function fetch_course_tree($courseid, $include_category_items=false) {
 781          $course_category = grade_category::fetch_course_category($courseid);
 782          $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
 783                                  'children'=>$course_category->get_children($include_category_items));
 784          $sortorder = 1;
 785          $course_category->set_sortorder($sortorder);
 786          $course_category->sortorder = $sortorder;
 787          return grade_category::_fetch_course_tree_recursion($category_array, $sortorder);
 788      }
 789  
 790      function _fetch_course_tree_recursion($category_array, &$sortorder) {
 791          // update the sortorder in db if needed
 792          if ($category_array['object']->sortorder != $sortorder) {
 793              $category_array['object']->set_sortorder($sortorder);
 794          }
 795  
 796          // store the grade_item or grade_category instance with extra info
 797          $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);
 798  
 799          // reuse final grades if there
 800          if (array_key_exists('finalgrades', $category_array)) {
 801              $result['finalgrades'] = $category_array['finalgrades'];
 802          }
 803  
 804          // recursively resort children
 805          if (!empty($category_array['children'])) {
 806              $result['children'] = array();
 807              //process the category item first
 808              $cat_item_id = null;
 809              foreach($category_array['children'] as $oldorder=>$child_array) {
 810                  if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
 811                      $result['children'][$sortorder] = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
 812                  }
 813              }
 814              foreach($category_array['children'] as $oldorder=>$child_array) {
 815                  if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
 816                      $result['children'][++$sortorder] = grade_category::_fetch_course_tree_recursion($child_array, $sortorder);
 817                  }
 818              }
 819          }
 820  
 821          return $result;
 822      }
 823  
 824      /**
 825       * Fetches and returns all the children categories and/or grade_items belonging to this category.
 826       * By default only returns the immediate children (depth=1), but deeper levels can be requested,
 827       * as well as all levels (0). The elements are indexed by sort order.
 828       * @return array Array of child objects (grade_category and grade_item).
 829       */
 830      function get_children($include_category_items=false) {
 831  
 832          // This function must be as fast as possible ;-)
 833          // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
 834          // we have to limit the number of queries though, because it will be used often in grade reports
 835  
 836          $cats  = get_records('grade_categories', 'courseid', $this->courseid);
 837          $items = get_records('grade_items', 'courseid', $this->courseid);
 838  
 839          // init children array first
 840          foreach ($cats as $catid=>$cat) {
 841              $cats[$catid]->children = array();
 842          }
 843  
 844          //first attach items to cats and add category sortorder
 845          foreach ($items as $item) {
 846              if ($item->itemtype == 'course' or $item->itemtype == 'category') {
 847                  $cats[$item->iteminstance]->sortorder = $item->sortorder;
 848  
 849                  if (!$include_category_items) {
 850                      continue;
 851                  }
 852                  $categoryid = $item->iteminstance;
 853              } else {
 854                  $categoryid = $item->categoryid;
 855              }
 856  
 857              // prevent problems with duplicate sortorders in db
 858              $sortorder = $item->sortorder;
 859              while(array_key_exists($sortorder, $cats[$categoryid]->children)) {
 860                  //debugging("$sortorder exists in item loop");
 861                  $sortorder++;
 862              }
 863  
 864              $cats[$categoryid]->children[$sortorder] = $item;
 865  
 866          }
 867  
 868          // now find the requested category and connect categories as children
 869          $category = false;
 870          foreach ($cats as $catid=>$cat) {
 871              if (empty($cat->parent)) {
 872                  if ($cat->path !== '/'.$cat->id.'/') {
 873                      $grade_category = new grade_category($cat, false);
 874                      $grade_category->path  = '/'.$cat->id.'/';
 875                      $grade_category->depth = 1;
 876                      $grade_category->update('system');
 877                      return $this->get_children($include_category_items);
 878                  }
 879              } else {
 880                  if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) {
 881                      //fix paths and depts
 882                      static $recursioncounter = 0; // prevents infinite recursion
 883                      $recursioncounter++;
 884                      if ($recursioncounter < 5) { 
 885                          // fix paths and depths!
 886                          $grade_category = new grade_category($cat, false);
 887                          $grade_category->depth = 0;
 888                          $grade_category->path  = null;
 889                          $grade_category->update('system');
 890                          return $this->get_children($include_category_items);
 891                      }
 892                  } 
 893                  // prevent problems with duplicate sortorders in db
 894                  $sortorder = $cat->sortorder;
 895                  while(array_key_exists($sortorder, $cats[$cat->parent]->children)) {
 896                      //debugging("$sortorder exists in cat loop");
 897                      $sortorder++;
 898                  }
 899  
 900                  $cats[$cat->parent]->children[$sortorder] = &$cats[$catid];
 901              }
 902  
 903              if ($catid == $this->id) {
 904                  $category = &$cats[$catid];
 905              }
 906          }
 907  
 908          unset($items); // not needed
 909          unset($cats); // not needed
 910  
 911          $children_array = grade_category::_get_children_recursion($category);
 912  
 913          ksort($children_array);
 914  
 915          return $children_array;
 916  
 917      }
 918  
 919      function _get_children_recursion($category) {
 920  
 921          $children_array = array();
 922          foreach($category->children as $sortorder=>$child) {
 923              if (array_key_exists('itemtype', $child)) {
 924                  $grade_item = new grade_item($child, false);
 925                  if (in_array($grade_item->itemtype, array('course', 'category'))) {
 926                      $type  = $grade_item->itemtype.'item';
 927                      $depth = $category->depth;
 928                  } else {
 929                      $type  = 'item';
 930                      $depth = $category->depth; // we use this to set the same colour
 931                  }
 932                  $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);
 933  
 934              } else {
 935                  $children = grade_category::_get_children_recursion($child);
 936                  $grade_category = new grade_category($child, false);
 937                  if (empty($children)) {
 938                      $children = array();
 939                  }
 940                  $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
 941              }
 942          }
 943  
 944          // sort the array
 945          ksort($children_array);
 946  
 947          return $children_array;
 948      }
 949  
 950      /**
 951       * Uses get_grade_item to load or create a grade_item, then saves it as $this->grade_item.
 952       * @return object Grade_item
 953       */
 954      function load_grade_item() {
 955          if (empty($this->grade_item)) {
 956              $this->grade_item = $this->get_grade_item();
 957          }
 958          return $this->grade_item;
 959      }
 960  
 961      /**
 962       * Retrieves from DB and instantiates the associated grade_item object.
 963       * If no grade_item exists yet, create one.
 964       * @return object Grade_item
 965       */
 966      function get_grade_item() {
 967          if (empty($this->id)) {
 968              debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
 969              return false;
 970          }
 971  
 972          if (empty($this->parent)) {
 973              $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);
 974  
 975          } else {
 976              $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
 977          }
 978  
 979          if (!$grade_items = grade_item::fetch_all($params)) {
 980              // create a new one
 981              $grade_item = new grade_item($params, false);
 982              $grade_item->gradetype = GRADE_TYPE_VALUE;
 983              $grade_item->insert('system');
 984  
 985          } else if (count($grade_items) == 1){
 986              // found existing one
 987              $grade_item = reset($grade_items);
 988  
 989          } else {
 990              debugging("Found more than one grade_item attached to category id:".$this->id);
 991              // return first one
 992              $grade_item = reset($grade_items);
 993          }
 994  
 995          return $grade_item;
 996      }
 997  
 998      /**
 999       * Uses $this->parent to instantiate $this->parent_category based on the
1000       * referenced record in the DB.
1001       * @return object Parent_category
1002       */
1003      function load_parent_category() {
1004          if (empty($this->parent_category) && !empty($this->parent)) {
1005              $this->parent_category = $this->get_parent_category();
1006          }
1007          return $this->parent_category;
1008      }
1009  
1010      /**
1011       * Uses $this->parent to instantiate and return a grade_category object.
1012       * @return object Parent_category
1013       */
1014      function get_parent_category() {
1015          if (!empty($this->parent)) {
1016              $parent_category = new grade_category(array('id' => $this->parent));
1017              return $parent_category;
1018          } else {
1019              return null;
1020          }
1021      }
1022  
1023      /**
1024       * Returns the most descriptive field for this object. This is a standard method used
1025       * when we do not know the exact type of an object.
1026       * @return string name
1027       */
1028      function get_name() {
1029          // For a course category, we return the course name if the fullname is set to '?' in the DB (empty in the category edit form)
1030          if (empty($this->parent) && $this->fullname == '?') {
1031              $course = get_record('course', 'id', $this->courseid);
1032              return format_string($course->fullname);
1033          } else {
1034              return $this->fullname;
1035          }
1036      }
1037  
1038      /**
1039       * Sets this category's parent id. A generic method shared by objects that have a parent id of some kind.
1040       * @param int parentid
1041       * @return boolean success
1042       */
1043      function set_parent($parentid, $source=null) {
1044          if ($this->parent == $parentid) {
1045              return true;
1046          }
1047  
1048          if ($parentid == $this->id) {
1049              error('Can not assign self as parent!');
1050          }
1051  
1052          if (empty($this->parent) and $this->is_course_category()) {
1053              error('Course category can not have parent!');
1054          }
1055  
1056          // find parent and check course id
1057          if (!$parent_category = grade_category::fetch(array('id'=>$parentid, 'courseid'=>$this->courseid))) {
1058              return false;
1059          }
1060  
1061          $this->force_regrading();
1062  
1063          // set new parent category
1064          $this->parent          = $parent_category->id;
1065          $this->parent_category =& $parent_category;
1066          $this->path            = null;       // remove old path and depth - will be recalculated in update()
1067          $this->depth           = 0;          // remove old path and depth - will be recalculated in update()
1068          $this->update($source);
1069  
1070          return $this->update($source);
1071      }
1072  
1073      /**
1074       * Returns the final values for this grade category.
1075       * @param int $userid Optional: to retrieve a single final grade
1076       * @return mixed An array of all final_grades (stdClass objects) for this grade_item, or a single final_grade.
1077       */
1078      function get_final($userid=NULL) {
1079          $this->load_grade_item();
1080          return $this->grade_item->get_final($userid);
1081      }
1082  
1083      /**
1084       * Returns the sortorder of the associated grade_item. This method is also available in
1085       * grade_item, for cases where the object type is not known.
1086       * @return int Sort order
1087       */
1088      function get_sortorder() {
1089          $this->load_grade_item();
1090          return $this->grade_item->get_sortorder();
1091      }
1092  
1093      /**
1094       * Returns the idnumber of the associated grade_item. This method is also available in
1095       * grade_item, for cases where the object type is not known.
1096       * @return string idnumber
1097       */
1098      function get_idnumber() {
1099          $this->load_grade_item();
1100          return $this->grade_item->get_idnumber();
1101      }
1102  
1103      /**
1104       * Sets sortorder variable for this category.
1105       * This method is also available in grade_item, for cases where the object type is not know.
1106       * @param int $sortorder
1107       * @return void
1108       */
1109      function set_sortorder($sortorder) {
1110          $this->load_grade_item();
1111          $this->grade_item->set_sortorder($sortorder);
1112      }
1113  
1114      /**
1115       * Move this category after the given sortorder - does not change the parent
1116       * @param int $sortorder to place after
1117       */
1118      function move_after_sortorder($sortorder) {
1119          $this->load_grade_item();
1120          $this->grade_item->move_after_sortorder($sortorder);
1121      }
1122  
1123      /**
1124       * Return true if this is the top most category that represents the total course grade.
1125       * @return boolean
1126       */
1127      function is_course_category() {
1128          $this->load_grade_item();
1129          return $this->grade_item->is_course_item();
1130      }
1131  
1132      /**
1133       * Return the top most course category.
1134       * @static
1135       * @return object grade_category instance for course grade
1136       */
1137      function fetch_course_category($courseid) {
1138          if (empty($courseid)) {
1139              debugging('Missing course id!');
1140              return false;
1141          }
1142  
1143          // course category has no parent
1144          if ($course_category = grade_category::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
1145              return $course_category;
1146          }
1147  
1148          // create a new one
1149          $course_category = new grade_category();
1150          $course_category->insert_course_category($courseid);
1151  
1152          return $course_category;
1153      }
1154  
1155      /**
1156       * Is grading object editable?
1157       * @return boolean
1158       */
1159      function is_editable() {
1160          return true;
1161      }
1162  
1163      /**
1164       * Returns the locked state/date of the associated grade_item. This method is also available in
1165       * grade_item, for cases where the object type is not known.
1166       * @return boolean
1167       */
1168      function is_locked() {
1169          $this->load_grade_item();
1170          return $this->grade_item->is_locked();
1171      }
1172  
1173      /**
1174       * Sets the grade_item's locked variable and updates the grade_item.
1175       * Method named after grade_item::set_locked().
1176       * @param int $locked 0, 1 or a timestamp int(10) after which date the item will be locked.
1177       * @param boolean $cascade lock/unlock child objects too
1178       * @param boolean $refresh refresh grades when unlocking
1179       * @return boolean success if category locked (not all children mayb be locked though)
1180       */
1181      function set_locked($lockedstate, $cascade=false, $refresh=true) {
1182          $this->load_grade_item();
1183  
1184          $result = $this->grade_item->set_locked($lockedstate, $cascade, true);
1185  
1186          if ($cascade) {
1187              //process all children - items and categories
1188              if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
1189                  foreach($children as $child) {
1190                      $child->set_locked($lockedstate, true, false);
1191                      if (empty($lockedstate) and $refresh) {
1192                          //refresh when unlocking
1193                          $child->refresh_grades();
1194                      }
1195                  }
1196              }
1197              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
1198                  foreach($children as $child) {
1199                      $child->set_locked($lockedstate, true, true);
1200                  }
1201              }
1202          }
1203  
1204          return $result;
1205      }
1206  
1207      /**
1208       * Returns the hidden state/date of the associated grade_item. This method is also available in
1209       * grade_item.
1210       * @return boolean
1211       */
1212      function is_hidden() {
1213          $this->load_grade_item();
1214          return $this->grade_item->is_hidden();
1215      }
1216  
1217      /**
1218       * Check grade hidden status. Uses data from both grade item and grade.
1219       * @return boolean true if hiddenuntil, false if not
1220       */
1221      function is_hiddenuntil() {
1222          $this->load_grade_item();
1223          return $this->grade_item->is_hiddenuntil();
1224      }
1225  
1226      /**
1227       * Sets the grade_item's hidden variable and updates the grade_item.
1228       * Method named after grade_item::set_hidden().
1229       * @param int $hidden 0, 1 or a timestamp int(10) after which date the item will be hidden.
1230       * @param boolean $cascade apply to child objects too
1231       * @return void
1232       */
1233      function set_hidden($hidden, $cascade=false) {
1234          $this->load_grade_item();
1235          $this->grade_item->set_hidden($hidden);
1236          if ($cascade) {
1237              if ($children = grade_item::fetch_all(array('categoryid'=>$this->id))) {
1238                  foreach($children as $child) {
1239                      $child->set_hidden($hidden, $cascade);
1240                  }
1241              }
1242              if ($children = grade_category::fetch_all(array('parent'=>$this->id))) {
1243                  foreach($children as $child) {
1244                      $child->set_hidden($hidden, $cascade);
1245                  }
1246              }
1247          }
1248      }
1249  
1250      /**
1251       * Applies default settings on this category
1252       * @return bool true if anything changed
1253       */
1254      function apply_default_settings() {
1255          global $CFG;
1256  
1257          foreach ($this->forceable as $property) {
1258              if (isset($CFG->{"grade_$property"})) {
1259                  if ($CFG->{"grade_$property"} == -1) {
1260                      continue; //temporary bc before version bump
1261                  }
1262                  $this->$property = $CFG->{"grade_$property"};
1263              }
1264          }
1265      }
1266  
1267      /**
1268       * Applies forced settings on this category
1269       * @return bool true if anything changed
1270       */
1271      function apply_forced_settings() {
1272          global $CFG;
1273  
1274          $updated = false;
1275          foreach ($this->forceable as $property) {
1276              if (isset($CFG->{"grade_$property"}) and isset($CFG->{"grade_{$property}_flag"}) and ((int)$CFG->{"grade_{$property}_flag"} & 1)) {
1277                  if ($CFG->{"grade_$property"} == -1) {
1278                      continue; //temporary bc before version bump
1279                  }
1280                  $this->$property = $CFG->{"grade_$property"};
1281                  $updated = true;
1282              }
1283          }
1284  
1285          return $updated;
1286      }
1287  
1288      /**
1289       * Notification of change in forced category settings.
1290       * @static
1291       */
1292      function updated_forced_settings() {
1293          global $CFG;
1294          $sql = "UPDATE {$CFG->prefix}grade_items SET needsupdate=1 WHERE itemtype='course' or itemtype='category'";
1295          execute_sql($sql, false);
1296      }
1297  }
1298  ?>


Generated: Wed Jan 14 11:33:29 2009 Cross-referenced by PHPXref 0.7