3 ///////////////////////////////////////////////////////////////////////////
5 // NOTICE OF COPYRIGHT //
7 // Moodle - Modular Object-Oriented Dynamic Learning Environment //
8 // http://moodle.com //
10 // Copyright (C) 2001-2003 Martin Dougiamas http://dougiamas.com //
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. //
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: //
22 // http://www.gnu.org/copyleft/gpl.html //
24 ///////////////////////////////////////////////////////////////////////////
27 * This class represents a complete tree of categories, grade_items and final grades,
28 * organises as an array primarily, but which can also be converted to other formats.
29 * It has simple method calls with complex implementations, allowing for easy insertion,
30 * deletion and moving of items and categories within the tree.
34 * The first sortorder for this tree, before any changes were made.
35 * @var int $first_sortorder
40 * The basic representation of the tree as a hierarchical, 3-tiered array.
41 * @var array $tree_array
43 var $tree_array = array();
46 * Another array with fillers for categories and items that do not have a parent, but have
47 * are not at level 2. This is used by the display_grades method.
48 * @var array $tree_filled
50 var $tree_filled = array();
53 * An array of objects that need updating (normally just grade_item.sortorder).
54 * @var array $need_update
56 var $need_update = array();
59 * An array of objects that need inserting in the DB.
60 * @var array $need_insert
62 var $need_insert = array();
65 * An array of objects that need deleting from the DB.
66 * @var array $need_delete
68 var $need_delete = array();
72 * Constructor, retrieves and stores a hierarchical array of all grade_category and grade_item
73 * objects for the given courseid or the entire site if no courseid given. Full objects are instantiated
74 * by default, but this can be switched off. The tree is indexed by sortorder, to facilitate CRUD operations
76 * @param int $courseid
77 * @param boolean $fullobjects
80 function grade_tree($courseid=NULL, $fullobjects=true, $tree=NULL) {
81 $this->courseid
= $courseid;
83 $this->tree_array
= $tree;
85 $this->tree_array
= $this->get_tree($fullobjects);
88 $this->first_sortorder
= key($this->tree_array
);
92 * Parses the array in search of a given sort order (the elements are indexed by
93 * sortorder), and returns a stdClass object with vital information about the
94 * element it has found.
95 * @param int $sortorder
96 * @return object element
98 function locate_element($sortorder) {
102 if (empty($this->tree_array
)) {
103 debugging("grade_tree->tree_array was empty, I could not locate the element at sortorder $sortorder");
109 foreach ($this->tree_array
as $levelkey1 => $level1) {
112 $retval = new stdClass();
113 $retval->index
= $levelkey1;
115 if ($levelkey1 == $sortorder) {
116 $retval->element
= $level1;
117 $retval->position
= $level1count;
121 if (!empty($level1['children'])) {
122 foreach ($level1['children'] as $level2key => $level2) {
126 $retval->index
= "$levelkey1/$level2key";
127 if ($level2key == $sortorder) {
128 $retval->element
= $level2;
129 $retval->position
= $level2count;
133 if (!empty($level2['children'])) {
134 foreach ($level2['children'] as $level3key => $level3) {
136 $retval->index
= "$levelkey1/$level2key/$level3key";
138 if ($level3key == $sortorder) {
139 $retval->element
= $level3;
140 $retval->position
= $level3count;
152 * Given an element object, returns its type (topcat, subcat or item).
153 * The $element can be a straight object (fully instantiated), an array of 'object' and 'children'/'final_grades', or a stdClass element
154 * as produced by grade_tree::locate_element(). This method supports all three types of inputs.
155 * @param object $element
156 * @return string Type
158 function get_element_type($element) {
159 if (!empty($element->element
['object'])) {
160 $object = $element->element
['object'];
161 } elseif (!empty($element['object'])) {
162 $object = $element['object'];
163 } elseif (is_object($element)) {
166 debugging("Invalid element given to grade_tree::get_element_type.");
170 if (get_class($object) == 'grade_item') {
172 } elseif (get_class($object) == 'grade_category') {
173 $object->get_children();
174 if (!empty($object->children
)) {
175 $first_child = current($object->children
);
176 if (get_class($first_child) == 'grade_item') {
178 } elseif (get_class($first_child) == 'grade_category') {
181 debugging("The category's first child was neither a category nor an item.");
185 debugging("The category did not have any children.");
189 debugging("Invalid element given to grade_tree::get_element_type.");
193 debugging("Could not determine the type of the given element.");
198 * Removes the given element (a stdClass object or a sortorder), remove_elements
199 * it from the tree. This does not renumber the tree. If a sortorder (int) is given, this
200 * method will first retrieve the referenced element from the tree, then re-run the method with that object.
201 * @var object $element An stdClass object typically returned by $this->locate(), or a sortorder (int)
204 function remove_element($element) {
205 if (empty($this->first_sortorder
)) {
206 $this->reset_first_sortorder();
209 if (isset($element->index
)) {
210 // Decompose the element's index and build string for eval(unset) statement to follow
211 $indices = explode('/', $element->index
);
212 $element_to_unset = '$this->tree_array[' . $indices[0] . ']';
214 if (isset($indices[1])) {
215 $element_to_unset .= "['children'][" . $indices[1] . ']';
218 if (isset($indices[2])) {
219 $element_to_unset .= "['children'][" . $indices[2] . ']';
222 eval("unset($element_to_unset);");
224 if (empty($element->element
['object'])) {
225 debugging("Could not delete this element from the DB due to missing information.");
229 $this->need_delete
[$element->element
['object']->id
] = $element->element
['object'];
233 $element = $this->locate_element($element);
234 if (!empty($element)) {
235 return $this->remove_element($element);
237 debugging("The element you provided grade_tree::remove_element() is not valid.");
242 debugging("Unable to remove an element from the grade_tree.");
247 * Inserts an element in the tree. This can be either an array as returned by the grade_category methods, or
248 * an element object returned by grade_tree.
249 * @param mixed $element array or object. If object, the sub-tree is contained in $object->element
250 * @param int $destination_sortorder Where to insert the element
251 * @param string $position Either 'before' the destination_sortorder or 'after'
254 function insert_element($element, $destination_sortorder, $position='before') {
255 if (empty($this->first_sortorder
)) {
256 $this->reset_first_sortorder();
259 if ($position == 'before') {
261 } elseif ($position == 'after') {
264 debugging('move_element(..... $position) can only be "before" or "after", you gave ' . $position);
268 if (is_array($element)) {
269 $new_element = new stdClass();
270 $new_element->element
= $element;
271 } elseif (is_object($element)) {
272 $new_element = $element;
275 // If the object is a grade_item, but the final_grades index isn't yet loaded, make the switch now. Same for grade_category and children
276 if (get_class($new_element->element
['object']) == 'grade_item' && empty($new_element->element
['final_grades'])) {
277 $new_element->element
['final_grades'] = $new_element->element
['object']->load_final();
278 unset($new_element->element
['object']->grade_grades_final
);
279 } elseif (get_class($new_element->element
['object']) == 'grade_category' &&
280 empty($new_element->element
['children']) &&
281 $new_element->element
['object']->has_children()) {
282 $new_element->element
['children'] = $new_element->element
['object']->get_children(1);
283 unset($new_element->element
['object']->children
);
287 // TODO Problem when moving topcategories: sortorder gets reindexed when splicing the array
288 $destination_array = array($destination_sortorder => $new_element->element
);
290 // Get the position of the destination element
291 $destination_element = $this->locate_element($destination_sortorder);
292 $position = $destination_element->position
;
294 // Decompose the element's index and build string for eval(array_splice) statement to follow
295 $indices = explode('/', $destination_element->index
);
297 if (empty($indices)) {
298 debugging("The destination element did not have a valid index (as assigned by grade_tree::locate_element).");
302 $element_to_splice = '$this->tree_array';
304 if (isset($indices[1])) {
305 $element_to_splice .= '[' . $indices[0] . "]['children']";
308 if (isset($indices[2])) {
309 $element_to_splice .= '[' . $indices[1] . "]['children']";
312 eval("array_splice($element_to_splice, \$position + \$offset, 0, \$destination_array);");
314 if (!is_object($new_element)) {
315 debugging("Could not insert this element into the DB due to missing information.");
319 $this->need_insert
[$new_element->element
['object']->id
] = $new_element->element
['object'];
325 * Moves an existing element in the tree to another position OF EQUAL LEVEL. This
326 * constraint is essential and very important.
327 * @param int $source_sortorder The sortorder of the element to move
328 * @param int $destination_sortorder The sortorder where the element will go
329 * @param string $position Either 'before' the destination_sortorder or 'after' it
332 function move_element($source_sortorder, $destination_sortorder, $position='before') {
333 if (empty($this->first_sortorder
)) {
334 $this->reset_first_sortorder();
337 // Locate the position of the source element in the tree
338 $source = $this->locate_element($source_sortorder);
340 // Remove this element from the tree
341 $this->remove_element($source);
343 $destination = $this->locate_element($destination_sortorder);
345 // Insert the element before the destination sortorder
346 $this->insert_element($source, $destination_sortorder, $position);
352 * Uses the key of the first entry in this->tree_array to reset the first_sortorder of this tree. Essential
353 * after each renumbering.
355 function reset_first_sortorder() {
356 if (count($this->tree_array
) < 1) {
357 debugging("Cannot reset the grade_tree's first_sortorder because the tree_array hasn't been loaded or is empty.");
360 reset($this->tree_array
);
361 $this->first_sortorder
= key($this->tree_array
);
362 return $this->first_sortorder
;
366 * One at a time, re-assigns new sort orders for every element in the tree, starting
367 * with a base number.
368 * @return array A debugging array which shows the progression of variables throughout this method. This is very useful
369 * to identify problems and implement new functionality.
371 function renumber($starting_sortorder=NULL) {
372 $sortorder = $starting_sortorder;
374 if (empty($starting_sortorder)) {
375 if (empty($this->first_sortorder
)) {
376 debugging("The tree's first_order variable isn't set, you must provide a starting_sortorder to the renumber method.");
379 $sortorder = $this->first_sortorder
- 1;
383 $topcatsortorder = 0;
386 foreach ($this->tree_array
as $topcat) {
388 $subcatsortorder = 0;
390 $debug[] = array('sortorder' => $sortorder,
391 'need_update' => $this->need_update
,
394 if (!empty($topcat['children'])) {
395 $topcatsortorder = $sortorder;
396 $debug[] = array('sortorder' => $sortorder,
397 'topcatsortorder' => $topcatsortorder,
398 'need_update' => $this->need_update
,
401 foreach ($topcat['children'] as $subcat) {
403 $debug[] = array('sortorder' => $sortorder,
404 'topcatsortorder' => $topcatsortorder,
405 'need_update' => $this->need_update
,
408 if (!empty($subcat['children'])) {
409 $subcatsortorder = $sortorder;
411 $debug[] = array('sortorder' => $sortorder,
412 'topcatsortorder' => $topcatsortorder,
413 'subcatsortorder' => $subcatsortorder,
414 'need_update' => $this->need_update
,
417 foreach ($subcat['children'] as $item) {
420 $debug[] = array('sortorder' => $sortorder,
421 'topcatsortorder' => $topcatsortorder,
422 'subcatsortorder' => $subcatsortorder,
423 'need_update' => $this->need_update
,
426 $newtree[$topcatsortorder]['children'][$subcatsortorder]['children'][$sortorder] = $item;
428 if ($sortorder != $item['object']->sortorder
) {
429 $this->need_update
[$item['object']->id
] = array('old_sortorder' => $item['object']->sortorder
, 'new_sortorder' => $sortorder);
430 $debug[] = array('sortorder' => $sortorder,
431 'topcatsortorder' => $topcatsortorder,
432 'subcatsortorder' => $subcatsortorder,
433 'need_update' => $this->need_update
,
438 $newtree[$topcatsortorder]['children'][$subcatsortorder]['object'] = $subcat['object'];
439 $newsortorder = $subcatsortorder;
441 $newtree[$topcatsortorder]['children'][$sortorder] = $subcat;
442 $newsortorder = $sortorder;
445 if ($newsortorder != $subcat['object']->sortorder
) {
446 $this->need_update
[$subcat['object']->id
] = array('old_sortorder' => $subcat['object']->sortorder
, 'new_sortorder' => $newsortorder);
447 $debug[] = array('sortorder' => $sortorder,
448 'topcatsortorder' => $topcatsortorder,
449 'subcatsortorder' => $subcatsortorder,
450 'need_update' => $this->need_update
,
455 $newtree[$topcatsortorder]['object'] = $topcat['object'];
456 $newsortorder = $topcatsortorder;
458 $newsortorder = $sortorder;
459 $newtree[$sortorder] = $topcat;
462 if ($newsortorder != $topcat['object']->sortorder
) {
463 $this->need_update
[$topcat['object']->id
] = array('old_sortorder' => $topcat['object']->sortorder
, 'new_sortorder' => $newsortorder);
464 $debug[] = array('sortorder' => $sortorder,
465 'topcatsortorder' => $topcatsortorder,
466 'subcatsortorder' => $subcatsortorder,
467 'need_update' => $this->need_update
,
473 $this->tree_array
= $newtree;
474 unset($this->first_sortorder
);
475 $this->build_tree_filled();
480 * Static method that returns a sorted, nested array of all grade_categories and grade_items for
481 * a given course, or for the entire site if no courseid is given.
482 * @param boolean $fullobjects Whether to instantiate full objects based on the data or not
485 function get_tree($fullobjects=true) {
492 $category_table = $CFG->prefix
. 'grade_categories';
493 $items_table = $CFG->prefix
. 'grade_items';
496 $itemconstraint = '';
498 if (!empty($this->courseid
)) {
499 $catconstraint = " AND $category_table.courseid = $this->courseid ";
500 $itemconstraint = " AND $items_table.courseid = $this->courseid ";
503 // Get ordered list of grade_items (not category type)
504 $query = "SELECT * FROM $items_table WHERE itemtype <> 'category' $itemconstraint ORDER BY sortorder";
505 $grade_items = get_records_sql($query);
507 // For every grade_item that doesn't have a parent category, create category fillers
508 foreach ($grade_items as $itemid => $item) {
509 if (empty($item->categoryid
)) {
511 $item = new grade_item($item);
513 $fillers[$item->sortorder
] = $item;
517 // Get all top categories
518 $query = "SELECT $category_table.*, sortorder FROM $category_table, $items_table
519 WHERE iteminstance = $category_table.id $catconstraint ORDER BY sortorder";
521 $topcats = get_records_sql($query);
523 if (empty($topcats)) {
527 // If any of these categories has grade_items as children, create a topcategory filler with colspan=count(children)
528 foreach ($topcats as $topcatid => $topcat) {
529 $topcatobject = new grade_category($topcat, false);
530 if ($topcatobject->get_childrentype() == 'grade_item' && empty($topcatobject->parent
)) {
531 $topcatobject->childrencount
= $topcatobject->has_children();
532 $fillers[$topcat->sortorder
] = $topcatobject;
533 unset($topcats[$topcatid]);
537 foreach ($topcats as $topcatid => $topcat) {
538 // Check the fillers array, see if one must be inserted before this topcat
539 if (key($fillers) < $topcat->sortorder
) {
540 $sortorder = key($fillers);
541 $object = current($fillers);
542 unset($fillers[$sortorder]);
544 $this->tree_filled
[$sortorder] = $this->get_filler($object, $fullobjects);
547 if (get_class($object) == 'grade_category') {
548 $children = $object->get_children(1);
549 unset($object->children
);
550 $element['children'] = $children;
551 } elseif (get_class($object) == 'grade_item') {
552 $final_grades = $object->get_final();
553 unset($object->grade_grades_final
);
554 $element['final_grades'] = $final_grades;
557 $object->sortorder
= $sortorder;
558 $element['object'] = $object;
559 $tree[$sortorder] = $element;
562 $query = "SELECT $category_table.*, sortorder FROM $category_table, $items_table
563 WHERE iteminstance = $category_table.id AND parent = $topcatid ORDER BY sortorder";
564 $subcats = get_records_sql($query);
565 $subcattree = array();
567 if (empty($subcats)) {
571 foreach ($subcats as $subcatid => $subcat) {
573 $items = get_records('grade_items', 'categoryid', $subcatid, 'sortorder');
579 foreach ($items as $itemid => $item) {
580 $finaltree = array();
583 $final = new grade_grades_final();
584 $final->itemid
= $itemid;
585 $finals = $final->fetch_all_using_this();
587 $finals = get_records('grade_grades_final', 'itemid', $itemid);
591 $sortorder = $item->sortorder
;
592 $item = new grade_item($item);
593 $item->sortorder
= $sortorder;
596 $itemtree[$item->sortorder
] = array('object' => $item, 'finalgrades' => $finals);
600 $sortorder = $subcat->sortorder
;
601 $subcat = new grade_category($subcat, false);
602 $subcat->sortorder
= $sortorder;
604 $subcattree[$subcat->sortorder
] = array('object' => $subcat, 'children' => $itemtree);
608 $sortorder = $topcat->sortorder
;
609 $topcat = new grade_category($topcat, false);
610 $topcat->sortorder
= $sortorder;
613 $tree[$topcat->sortorder
] = array('object' => $topcat, 'children' => $subcattree);
614 $this->tree_filled
[$topcat->sortorder
] = array('object' => $topcat, 'children' => $subcattree);
617 // If there are still grade_items or grade_categories without a top category, add another filler
618 if (!empty($fillers)) {
619 foreach ($fillers as $sortorder => $object) {
620 $this->tree_filled
[$sortorder] = $this->get_filler($object, $fullobjects);
622 if (get_class($object) == 'grade_category') {
623 $children = $object->get_children(1);
624 unset($object->children
);
625 $element['children'] = $children;
626 } elseif (get_class($object) == 'grade_item') {
627 $final_grades = $object->get_final();
628 unset($object->grade_grades_final
);
629 $element['final_grades'] = $final_grades;
632 $object->sortorder
= $sortorder;
633 $element['object'] = $object;
634 $tree[$sortorder] = $element;
643 * Returns a hierarchical array, prefilled with the values needed to populate
644 * the tree of grade_items in the cases where a grade_item or grade_category doesn't have a
645 * 2nd level topcategory.
646 * @param object $object A grade_item or a grade_category object
647 * @param boolean $fullobjects Whether to instantiate full objects or just return stdClass objects
650 function get_filler($object, $fullobjects=true) {
651 $filler_array = array();
653 // Depending on whether the filler is for a grade_item or a category...
654 if (isset($object->itemname
)) {
655 if (get_class($object) == 'grade_item') {
656 $finals = $object->load_final();
658 $item_object = new grade_item($object, false);
659 $finals = $object->load_final();
662 $filler_array = array('object' => 'filler', 'children' =>
663 array(0 => array('object' => 'filler', 'children' =>
664 array(0 => array('object' => $object, 'finalgrades' => $finals)))));
665 } elseif (method_exists($object, 'get_children')) {
667 $subcat_children = $object->get_children(0, 'flat');
668 $children_for_tree = array();
669 foreach ($subcat_children as $itemid => $item) {
672 if (get_class($item) == 'grade_item') {
673 $finals = $item->load_final();
675 $item_object = new grade_item($item, false);
676 if (method_exists($item, 'load_final')) {
677 $finals = $item->load_final();
681 $children_for_tree[$itemid] = array('object' => $item, 'finalgrades' => $finals);
684 $filler_array = array('object' => 'filler', 'colspan' => $object->childrencount
, 'children' =>
685 array(0 => array('object' => $object, 'children' => $children_for_tree)));
688 return $filler_array;
692 * Returns a HTML table with all the grades in the course requested, or all the grades in the site.
693 * IMPORTANT: This method (and its associated methods) assumes that we are using only 2 levels of categories (topcat and subcat)
694 * @todo Return extra column for students
695 * @todo Return a row of final grades for each student
697 * @todo Return totals
698 * @todo Return row below headers for grading range
699 * @return string HTML table
701 function display_grades() {
702 // 1. Fetch all top-level categories for this course, with all children preloaded, sorted by sortorder
703 $tree = $this->tree_filled
;
705 if (empty($this->tree_filled
)) {
706 debugging("The tree_filled array wasn't initialised, grade_tree could not display the grades correctly.");
710 $topcathtml = '<tr>';
714 foreach ($tree as $topcat) {
717 foreach ($topcat['children'] as $catkey => $cat) {
720 foreach ($cat['children'] as $item) {
723 $itemhtml .= '<td>' . $item['object']->itemname
. '</td>';
726 if ($cat['object'] == 'filler') {
727 $cathtml .= '<td class="subfiller"> </td>';
729 $cat['object']->load_grade_item();
730 $cathtml .= '<td colspan="' . $catitemcount . '">' . $cat['object']->fullname
. '</td>';
734 if ($topcat['object'] == 'filler') {
736 if (!empty($topcat['colspan'])) {
737 $colspan = 'colspan="' . $topcat['colspan'] . '" ';
739 $topcathtml .= '<td ' . $colspan . 'class="topfiller"> </td>';
741 $topcathtml .= '<th colspan="' . $itemcount . '">' . $topcat['object']->fullname
. '</th>';
746 $itemhtml .= '</tr>';
748 $topcathtml .= '</tr>';
750 return "<table style=\"text-align: center\" border=\"1\">$topcathtml$cathtml$itemhtml</table>";
755 * Using $this->tree_array, builds $this->tree_filled, which is the same array but with fake categories as
756 * fillers. These are used by display_grades, to print out empty cells over orphan grade_items and grade_categories.
757 * @return boolean Success or Failure.
759 function build_tree_filled() {
760 if (empty($this->tree_array
)) {
761 debugging("You cannot build the tree_filled array until the tree_array is filled.");
765 $this->tree_filled
= array();
767 foreach ($this->tree_array
as $level1order => $level1) {
768 if ($this->get_element_type($level1) == 'item' ||
$this->get_element_type($level1) == 'subcat') {
769 $this->tree_filled
[$level1order] = $this->get_filler($level1['object']);
771 $this->tree_filled
[$level1order] = $level1;
775 reset($this->tree_array
);
781 * Performs any delete, insert or update queries required, depending on the objects
782 * stored in $this->need_update, need_insert and need_delete.
783 * @return boolean Success or Failure
785 function update_db() {
786 // Perform deletions first
787 foreach ($this->need_delete
as $id => $object) {
788 // If an item is both in the delete AND insert arrays, it must be an existing object that only needs updating, so ignore it.
789 if (empty($this->need_insert
[$id])) {
790 if (!$object->delete()) {
791 debugging("Could not delete object from DB.");
796 foreach ($this->need_insert
as $id => $object) {
797 if (empty($this->need_delete
[$id])) {
798 if (!$object->insert()) {
799 debugging("Could not insert object into DB.");
804 $this->need_delete
= array();
805 $this->need_insert
= array();
807 foreach ($this->need_update
as $id => $sortorders) {
808 if (!set_field('grade_items', 'sortorder', $sortorders['new_sortorder'], 'id', $id)) {
809 debugging("Could not update the grade_item's sortorder in DB.");
813 $this->need_update
= array();