<?php
require_once $CFG->dirroot.'/lib/grade/grade_item.php';
require_once $CFG->dirroot.'/lib/grade/grade_category.php';
require_once $CFG->dirroot.'/lib/grade/grade_object.php';
require_once $CFG->dirroot.'/grade/edit/tree/lib.php';
require_once $CFG->dirroot.'/grade/lib.php';

/**
 * Prints the page headers, breadcrumb trail, page heading, (optional) dropdown navigation menu and
 * (optional) navigation tabs for any gradebook page. All gradebook pages MUST use these functions
 * in favour of the usual print_header(), print_header_simple(), print_heading() etc.
 * !IMPORTANT! Use of tabs.php file in gradebook pages is forbidden unless tabs are switched off at
 * the site level for the gradebook ($CFG->grade_navmethod = GRADE_NAVMETHOD_DROPDOWN).
 *
 * @param int $courseid
 * @param string $active_type The type of the current page (report, settings, import, export, scales, outcomes, letters)
 * @param string $active_plugin The plugin of the current page (grader, fullview etc...)
 * @param string $heading The heading of the page. Tries to guess if none is given
 * @param boolean $return Whether to return (true) or echo (false) the HTML generated by this function
 * @param string $bodytags Additional attributes that will be added to the <body> tag
 * @param string $buttons Additional buttons to display on the page
 *
 * @return string HTML code or nothing if $return == false
 */
function print_grade_page_head_local($courseid, $active_type, $active_plugin=null, $heading = false, $return=false, $bodytags='', $buttons=false, $extracss=array()) {
    global $CFG, $COURSE;
    $strgrades = get_string('grades');
    $plugin_info = grade_get_plugin_info($courseid, $active_type, $active_plugin);

    // Determine the string of the active plugin
    $stractive_plugin = ($active_plugin) ? $plugin_info['strings']['active_plugin_str'] : $heading;
    $stractive_type = $plugin_info['strings'][$active_type];

    $navlinks = array();
    $first_link = '';

    if ($active_type == 'settings' && $active_plugin != 'coursesettings') {
        $first_link = $plugin_info['report'][$active_plugin]['link'];
    } elseif ($active_type != 'report') {
        $first_link = $CFG->wwwroot.'/grade/index.php?id='.$COURSE->id;
    }

    if ($active_type == 'preferences') {
        $CFG->stylesheets[] = $CFG->wwwroot . '/grade/report/styles.css';
    }

    foreach ($extracss as $css_url) {
        $CFG->stylesheets[] = $css_url;
    }

    $navlinks[] = array('name' => $strgrades,
                        'link' => $first_link,
                        'type' => 'misc');

    $active_type_link = '';

    if (!empty($plugin_info[$active_type]['link']) && $plugin_info[$active_type]['link'] != qualified_me()) {
        $active_type_link = $plugin_info[$active_type]['link'];
    }

    if (!empty($plugin_info[$active_type]['parent']['link'])) {
        $active_type_link = $plugin_info[$active_type]['parent']['link'];
        $navlinks[] = array('name' => $stractive_type, 'link' => $active_type_link, 'type' => 'misc');
    }

    if (empty($plugin_info[$active_type]['id'])) {
        $navlinks[] = array('name' => $stractive_type, 'link' => $active_type_link, 'type' => 'misc');
    }

    $navlinks[] = array('name' => $stractive_plugin, 'link' => null, 'type' => 'misc');

    $navigation = build_navigation($navlinks);

    $title = ': ' . $stractive_plugin;
    if (empty($plugin_info[$active_type]['id']) || !empty($plugin_info[$active_type]['parent'])) {
        $title = ': ' . $stractive_type . ': ' . $stractive_plugin;
    }

    $returnval = print_header_simple($strgrades . ': ' . $stractive_type, $title, $navigation, '',
            $bodytags, true, $buttons, navmenu($COURSE), false, '', $return);

    // Guess heading if not given explicitly
    if (!$heading) {
        $heading = $stractive_plugin;
    }

    if ($CFG->grade_navmethod == GRADE_NAVMETHOD_COMBO || $CFG->grade_navmethod == GRADE_NAVMETHOD_DROPDOWN) {
        $returnval .= print_grade_plugin_selector($plugin_info, $return);
    }
    $returnval .= print_heading($heading);

    if ($CFG->grade_navmethod == GRADE_NAVMETHOD_COMBO || $CFG->grade_navmethod == GRADE_NAVMETHOD_TABS) {
        $returnval .= grade_print_tabs($active_type, $active_plugin, $plugin_info, $return);
    }

    if ($return) {
        return $returnval;
    }
}


/**
 * Returns icon of element
 * @param object $element
 * @param bool $spacerifnone return spacer if no icon found
 * @return string icon or spacer
 */
function get_element_icon_local(&$element, $spacerifnone=false) {
    global $CFG;

    switch ($element->type) {
        case 'item':
        case 'courseitem':
        case 'categoryitem':
            if ($element->is_calculated()) {
                $strcalc = get_string('calculatedgrade', 'grades');
                return '<img src="'.$CFG->pixpath.'/i/calc.gif" class="icon itemicon" title="'.s($strcalc).'" alt="'.s($strcalc).'"/>';

            } else if (($element->is_course_item() or $element->is_category_item())
              and ($element->gradetype == GRADE_TYPE_SCALE or $element->gradetype == GRADE_TYPE_VALUE)) {
                if ($category = $element->get_item_category()) {
                    switch ($category->aggregation) {
                        case GRADE_AGGREGATE_MEDIAN:
                        case GRADE_AGGREGATE_MEDIAN:
                            return '<img src="'.$CFG->pixpath.'/i/agg_sum.gif" class="icon itemicon" alt="'.get_string('aggregation', 'grades').'"/>';
                        case GRADE_AGGREGATE_WEIGHTED_MEAN:
                        case GRADE_AGGREGATE_WEIGHTED_MEAN2:
                        case GRADE_AGGREGATE_EXTRACREDIT_MEAN:
                            return '<img src="'.$CFG->pixpath.'/i/agg_mean.gif" class="icon itemicon" alt="'.get_string('aggregation', 'grades').'"/>';
                        case GRADE_AGGREGATE_SUM:
                            return '<img src="'.$CFG->pixpath.'/i/agg_sum.gif" class="icon itemicon" alt="'.get_string('aggregation', 'grades').'"/>';
                    }
                }

            } else if ($element->itemtype == 'mod') {
                $strmodname = get_string('modulename', $element->itemmodule);
                return '<img src="'.$CFG->modpixpath.'/'.$element->itemmodule.'/icon.gif" class="icon itemicon" title="' .s($strmodname).'" alt="' .s($strmodname).'"/>';

            } else if ($element->itemtype == 'manual') {
/*
                if ($element['object']->is_outcome_item()) {
                    $stroutcome = get_string('outcome', 'grades');
                    return '<img src="'.$CFG->pixpath.'/i/outcomes.gif" class="icon itemicon" title="'.s($stroutcome).'" alt="'.s($stroutcome).'"/>';
                } else {
 *
 */
                    $strmanual = get_string('manualitem', 'grades');
                    return '<img src="'.$CFG->pixpath.'/t/manual_item.gif" class="icon itemicon" title="'.s($strmanual).'" alt="'.s($strmanual).'"/>';
//                }
            }
            break;

        case 'category':
            $strcat = get_string('category', 'grades');
            return '<img src="'.$CFG->pixpath.'/f/folder.gif" class="icon itemicon" title="'.s($strcat).'" alt="'.s($strcat).'" />';
    }

    if ($spacerifnone) {
        return '<img src="'.$CFG->wwwroot.'/pix/spacer.gif" class="icon itemicon" alt=""/>';
    } else {
        return '';
    }
}



/*
 * Handles any droplow or keephigh conditions for the passed category or course
 * all variables dissolve except $item and $items
 *
*/

function limit_item($item, $items, &$grades) {
    if (!empty($items[$item]->item_category->droplow)) {
        $droparray = array();
        foreach($items as $id=>$unused) {
            if ($unused->parent == $item) {
                $droparray[$id] = $grades[$id]->contribution;
            }
        }
        asort($droparray, SORT_NUMERIC);
        $dropped = 0;
        foreach($droparray as $key=>$value) {
            if ($dropped < $items[$item]->item_category->droplow) {
                $grades[$item]->weighted_grade -= $grades[$key]->contribution;
                $grades[$item]->totalcoef -= ($items[$item]->agg_method == GRADE_AGGREGATE_WEIGHTED_MEAN) ? $items[$key]->aggregationcoef : $grades[$key]->rawgrademax;
                $dropped++;
            } else {
                break;
            }
        }
//        $items[$item->parent]->newgrade +=
//            (($item->newgrade / $item->aggtotal * $item->aggregationcoef) - $oldcatgrade);

    } else if (!empty($items[$item]->item_category->keephigh)) {
        $keptarray = array();
        foreach($items as $id=>$unused) {
            if ($unused->parent == $item) {
                $keptarray[$id] = $grades[$id]->contribution;
            }
        }
        arsort($keptarray, SORT_NUMERIC);
        $kept = 0;
        foreach($keptarray as $key=>$value) {
            if ($kept < $items[$item]->item_category->keephigh) {
            } else {
                $grades[$item]->weighted_grade -= $grades[$key]->contribution;
                $greaterthanvalue = ($items[$item]->agg_method == GRADE_AGGREGATE_WEIGHTED_MEAN) ? 1 : 0;
                $grades[$item]->totalcoef -= ($items[$key]->aggregationcoef > $greaterthanvalue ) ? $items[$key]->aggregationcoef : $grades[$key]->rawgrademax;
            }
            $kept++;
        }
    }

}
 /**
 * Updates all final grades in course.
 *
 * @param int $courseid
 * @param int $userid if specified, try to do a quick regrading of grades of this user only
 * @param object $updated_item the item in which
 * @return boolean true if ok, array of errors if problems found (item id is used as key)
 */
function grade_regrade_final_grades_local($courseid, $userid=null, $updated_item=null) {

    // HACK to locallib
//	$course_item = grade_item::fetch_course_item($courseid); // END OF HACK
	$course_item = grade_item_local::fetch_course_item($courseid); // END OF HACK
	
    if ($userid) {
        // one raw grade updated for one user
        if (empty($updated_item)) {
            error("updated_item_id can not be null!");
        }
        if ($course_item->needsupdate) {
            $updated_item->force_regrading();
            return array($course_item->id =>'Can not do fast regrading after updating of raw grades');
        }

    } else {
        if (!$course_item->needsupdate) {
            // nothing to do :-)
            return true;
        }
    }
	
    // HACK instantiate local grade items
//    $grade_items = grade_item::fetch_all(array('courseid'=>$courseid));
    $grade_items = grade_item_local::fetch_all(array('courseid'=>$courseid)); // END OF HACK
    $depends_on = array(); 

    // first mark all category and calculated items as needing regrading
    // this is slower, but 100% accurate
    foreach ($grade_items as $gid=>$gitem) {
        if (!empty($updated_item) and $updated_item->id == $gid) {
            $grade_items[$gid]->needsupdate = 1;

        } else if ($gitem->is_course_item() or $gitem->is_category_item() or $gitem->is_calculated()) {
            $grade_items[$gid]->needsupdate = 1;
        }

        // construct depends_on lookup array
        $depends_on[$gid] = $grade_items[$gid]->depends_on();
    }

    $errors = array();
    $finalids = array();
    $gids     = array_keys($grade_items);
    $failed = 0;

    while (count($finalids) < count($gids)) { // work until all grades are final or error found
        $count = 0;
        foreach ($gids as $gid) {
            if (in_array($gid, $finalids)) {
                continue; // already final
            }

            if (!$grade_items[$gid]->needsupdate) {
                $finalids[] = $gid; // we can make it final - does not need update
                continue;
            }

            $doupdate = true;
            foreach ($depends_on[$gid] as $did) {
                if (!in_array($did, $finalids)) {
                    $doupdate = false;
                    continue; // this item depends on something that is not yet in finals array
                }
            }

            //oki - let's update, calculate or aggregate :-)
            if ($doupdate) {
                $result = $grade_items[$gid]->regrade_final_grades($userid);

                if ($result === true) {
                    $grade_items[$gid]->regrading_finished();
                    $grade_items[$gid]->check_locktime(); // do the locktime item locking
                    $count++;
                    $finalids[] = $gid;

                } else {
                    $grade_items[$gid]->force_regrading();
                    $errors[$gid] = $result;
                }
            }
        }

        if ($count == 0) {
            $failed++;
        } else {
            $failed = 0;
        }

        if ($failed > 1) {
            foreach($gids as $gid) {
                if (in_array($gid, $finalids)) {
                    continue; // this one is ok
                }
                $grade_items[$gid]->force_regrading();
                $errors[$grade_items[$gid]->id] = 'Probably circular reference or broken calculation formula'; // TODO: localize
            }
            break; // oki, found error
        }
    }

    if (count($errors) == 0) {
        if (empty($userid)) {
            // do the locktime locking of grades, but only when doing full regrading
            grade_grade::check_locktime_all($gids);
        }
        return true;
    } else {
        return $errors;
    }
}


class grade_tree_local extends grade_tree {
    /**
     * Constructor, retrieves and stores a hierarchical array of all grade_category and grade_item
     * objects for the given courseid. Full objects are instantiated. Ordering sequence is fixed if needed.
     * @param int $courseid
     * @param boolean $fillers include fillers and colspans, make the levels var "rectangular"
     * @param boolean $category_grade_last category grade item is the last child
     * @param array $collapsed array of collapsed categories
     */
    function grade_tree_local($courseid, $fillers=true, $category_grade_last=false, $collapsed=null, $nooutcomes=false) {
        global $USER, $CFG;

        $this->courseid   = $courseid;
        $this->commonvars = "&amp;sesskey=$USER->sesskey&amp;id=$this->courseid";
        $this->levels     = array();
        $this->context    = get_context_instance(CONTEXT_COURSE, $courseid);
        $this->parents 	  = array();

        // get course grade tree
        $this->top_element = grade_category_local::fetch_course_tree($courseid, true);

        // collapse the categories if requested
        if (!empty($collapsed)) {
            grade_tree_local::category_collapse($this->top_element, $collapsed);
        }

        // no otucomes if requested
        if (!empty($nooutcomes)) {
            grade_tree_local::no_outcomes($this->top_element);
        }

        // move category item to last position in category
        if ($category_grade_last) {
            grade_tree_local::category_grade_last($this->top_element);
        }

        if ($fillers) {
            // inject fake categories == fillers
            grade_tree_local::inject_fillers($this->top_element, 0);
            // add colspans to categories and fillers
            grade_tree_local::inject_colspans($this->top_element);
        }

        grade_tree_local::fill_levels($this->levels, $this->top_element, 0, null, $this->parents);        
    }
    
        /**
     * Static recursive helper - fills the levels array, useful when accessing tree elements of one level
     * @static
     * @param int $levels
     * @param array $element The seed of the recursion
     * @param int $depth
     * @return void
     */
    function fill_levels(&$levels, &$element, $depth, $parent_id=null, &$parents) {
        if (!array_key_exists($depth, $levels)) {
            $levels[$depth] = array();
        }
		// prepare unique identifier
        if ($element['type'] == 'category') {
            $element['eid'] = 'c'.$element['object']->id;
            
            // HACK: Bob Puffer to inform item aggregation of the parent id
            $parents[$element['object']->grade_item->id]->agg_method = $element['object']->aggregation;
            $parents[$element['object']->grade_item->id]->parent = $parent_id; // END OF HACK
       } else if (in_array($element['type'], array('courseitem', 'item', 'categoryitem'))) {
            $element['eid'] = 'i'.$element['object']->id;
            $this->items[$element['object']->id] =& $element['object'];

            // HACK: Bob Puffer to inform item aggregation of the parent id
            // IMPORTANT TO NOTE, parent stored is id for parent, aggregation method stored is agg method for item
            if (isset($parents[$element['object']->id])) {
		$this->items[$element['object']->id]->parent = $parents[$parent_id]->parent;
		$this->items[$element['object']->id]->agg_method = $parents[$parent_id]->agg_method;
            } else {
		$this->items[$element['object']->id]->parent = $parent_id;
                $this->items[$element['object']->id]->parent = $parent_id;
		$this->items[$element['object']->id]->agg_method = $parents[$parent_id]->agg_method;
            }
/*
            if (isset($parents[$element['object']->id])) {
		$this->items[$element['object']->id]->parent = $parents[$element['object']->id]->parent;
		$this->items[$element['object']->id]->agg_method = $parents[$element['object']->id]->agg_method;
            } else {
		$this->items[$element['object']->id]->parent = $parent_id;
            }
 *
 */
            $this->items[$element['object']->id]->newgrade = 0;
            $this->items[$element['object']->id]->aggtotal = 0; // END OF HACK
        }

        $levels[$depth][] =& $element;
        $depth++;
        if (empty($element['children'])) {
            return;
        }
	// HACK: Bob Puffer to get all the parents of the items and subcategories
	if (isset($element['object']->grade_item->id)) {
            $parent_id = $element['object']->grade_item->id; 
	} // END OF HACK
		
        $prev = 0;
        foreach ($element['children'] as $sortorder=>$child) {
            grade_tree_local::fill_levels($levels, $element['children'][$sortorder], $depth, $parent_id, $parents);
            $element['children'][$sortorder]['prev'] = $prev;
            $element['children'][$sortorder]['next'] = 0;
            if ($prev) {
                $element['children'][$prev]['next'] = $sortorder;
            }
            $prev = $sortorder;
        }
    }
    
    /**
     * Returns name of element optionally with icon and link
     * @param object $element
     * @param bool $withlinks
     * @param bool $icons
     * @param bool $spacerifnone return spacer if no icon found
     * @return header string
     */
    function get_element_header(&$element, $withlink=false, $icon=true, $spacerifnone=false) {
        global $CFG;

        $header = '';

        if ($icon) {
            $header .= $this->get_element_icon_local($element, $spacerifnone);
        }

        $header .= $element['object']->get_name();

        if ($element['type'] != 'item' and $element['type'] != 'categoryitem' and $element['type'] != 'courseitem') {
            return $header;
        }

        $itemtype     = $element['object']->itemtype;
        $itemmodule   = $element['object']->itemmodule;
        $iteminstance = $element['object']->iteminstance;

        if ($withlink and $itemtype=='mod' and $iteminstance and $itemmodule) {
            if ($cm = get_coursemodule_from_instance($itemmodule, $iteminstance, $this->courseid)) {

                $a->name = get_string('modulename', $element['object']->itemmodule);
                $title = get_string('linktoactivity', 'grades', $a);
                $dir = $CFG->dirroot.'/mod/'.$itemmodule;

                if (file_exists($dir.'/grade.php')) {
                    $url = $CFG->wwwroot.'/mod/'.$itemmodule.'/grade.php?id='.$cm->id;
                } else {
                    $url = $CFG->wwwroot.'/mod/'.$itemmodule.'/view.php?id='.$cm->id;
                }

                $header = '<a href="'.$url.'" title="'.s($title).'">'.$header.'</a>';
            }
        }

        return $header;
    }


    /**
     * Returns name of element optionally with icon and link
     * @param object $element
     * @param bool $withlinks
     * @param bool $icons
     * @param bool $spacerifnone return spacer if no icon found
     * @return header string
     */
    function get_element_headerLAEgrader(&$element, $withlink=false, $icon=true, $spacerifnone=false) {
        global $CFG;

        $header = '';

        if ($icon) {
//            $header .= get_element_icon_local($element, $spacerifnone) . '<br />';
        }
        if ($element->type == 'categoryitem') {
            $header .= 'C A T E G O R Y';
        } else if ($element->type == 'courseitem') {
            $header .= '* * * * * * * *<br />C O U R S E';
        } else {
            $header .= '- - - - - - - - - - - - - - ';
        }
        $header .= '<br />' . $element->get_name(true);
        $itemtype     = $element->itemtype;
        $itemmodule   = $element->itemmodule;
        $iteminstance = $element->iteminstance;

        if ($withlink and $itemtype=='mod' and $iteminstance and $itemmodule) {
            if ($cm = get_coursemodule_from_instance($itemmodule, $iteminstance, $this->courseid)) {

                $a->name = get_string('modulename', $itemmodule);
                $title = get_string('linktoactivity', 'grades', $a);
                $dir = $CFG->dirroot.'/mod/'.$itemmodule;

                if (file_exists($dir.'/grade.php')) {
                    $url = $CFG->wwwroot.'/mod/'.$itemmodule.'/grade.php?id='.$cm->id;
                } else {
                    $url = $CFG->wwwroot.'/mod/'.$itemmodule.'/view.php?id='.$cm->id;
                }

                $header = '<a href="'.$url.'" title="'.s($title).'">'.$header.'</a>';
            }
        }

        return $header;
    }
    
	
}

class grade_edit_tree_local extends grade_edit_tree {
    var $columns = array();

    /**
     * @var object $gtree          @see grade/lib.php
     */
    var $gtree;

    /**
     * @var grade_plugin_return @see grade/lib.php
     */
    var $gpr;

    /**
     * @var string              $moving The eid of the category or item being moved
     */
    var $moving;

    var $deepest_level;

    var $uses_extra_credit = false;

    var $uses_weight = false;

    /**
     * Constructor
     */
    function grade_edit_tree_local($gtree, $moving=false, $gpr) {
        $this->gtree = $gtree;
        $this->moving = $moving;
        $this->gpr = $gpr;
        $this->deepest_level = $this->get_deepest_level($this->gtree->top_element);

        $this->columns = array(grade_edit_tree_column::factory('name', array('deepest_level' => $this->deepest_level)),
                               grade_edit_tree_column::factory('aggregation', array('flag' => true)));

        if ($this->uses_weight) {
            $this->columns[] = grade_edit_tree_column::factory('weight_local', array('adv' => 'aggregationcoef'));
        }
        if ($this->uses_extra_credit) {
            $this->columns[] = grade_edit_tree_column::factory('extracredit_local', array('adv' => 'aggregationcoef'));
        }

//        $this->columns[] = grade_edit_tree_column::factory('displaytype', array('flag' => true));
        $this->columns[] = grade_edit_tree_column::factory('range'); // This is not a setting... How do we deal with it?
        $this->columns[] = grade_edit_tree_column::factory('aggregateonlygraded', array('flag' => true));
        $this->columns[] = grade_edit_tree_column::factory('aggregatesubcats', array('flag' => true));
        $this->columns[] = grade_edit_tree_column::factory('aggregateoutcomes', array('flag' => true));
        $this->columns[] = grade_edit_tree_column::factory('droplow', array('flag' => true));
        $this->columns[] = grade_edit_tree_column::factory('keephigh', array('flag' => true));
        $this->columns[] = grade_edit_tree_column::factory('multfactor', array('adv' => true));
        $this->columns[] = grade_edit_tree_column::factory('plusfactor', array('adv' => true));
        $this->columns[] = grade_edit_tree_column::factory('actions');
        $this->columns[] = grade_edit_tree_column::factory('select');
    }

    /**
     * Given a grade_item object, returns a labelled input if an aggregation coefficient (weight or extra credit) applies to it.
     * @param grade_item $item
     * @param string type "extra" or "weight": the type of the column hosting the weight input
     * @return string HTML
     */
    function get_weight_input($item, $type) {
        if (!is_object($item) || get_class($item) !== 'grade_item_local') {
            error('grade_edit_tree::get_weight_input($item) was given a variable that is not of the required type (grade_item object)');
        }

        if ($item->is_course_item()) {
            return '';
        }

        $parent_category = $item->get_parent_category();
        $parent_category->apply_forced_settings();
        $aggcoef = $item->get_coefstring();

        if ((($aggcoef == 'aggregationcoefweight' || $aggcoef == 'aggregationcoef') && $type == 'weight') ||
            ($aggcoef == 'aggregationcoefextra' && $type == 'extra')) {
            return '<input type="text" size="6" id="aggregationcoef_'.$item->id.'" name="aggregationcoef_'.$item->id.'"
                value="'.format_float($item->aggregationcoef, 4).'" />';
        } elseif ($aggcoef == 'aggregationcoefextrasum' && $type == 'extra') {
            $checked = ($item->aggregationcoef > 0) ? 'checked="checked"' : '';
            return '<input type="hidden" name="extracredit_'.$item->id.'" value="0" />
                    <input type="checkbox" id="extracredit_'.$item->id.'" name="extracredit_'.$item->id.'" value="1" '."$checked />\n";
        } else {
            return '';
        }
    }

    /**
     * Recursive function for building the table holding the grade categories and items,
     * with CSS indentation and styles.
     *
     * @param array               $element The current tree element being rendered
     * @param boolean             $totals Whether or not to print category grade items (category totals)
     * @param array               $parents An array of parent categories for the current element (used for indentation and row classes)
     *
     * @return string HTML
     */
    function build_html_tree($element, $totals, $parents, &$categories, $level, &$row_count) {
        global $CFG, $COURSE, $USER;

        $object = $element['object'];
        $eid    = $element['eid'];
        $object->name = $this->gtree->get_element_header($element, true, true, false);
        $object->stripped_name = $this->gtree->get_element_header($element, false, false, false);

        $is_category_item = false;
        if ($element['type'] == 'categoryitem' || $element['type'] == 'courseitem') {
            $is_category_item = true;
        }

        $rowclasses = '';
        foreach ($parents as $parent_eid) {
            $rowclasses .= " $parent_eid ";
        }

        $actions = '';

        if (!$is_category_item) {
            $actions .= $this->gtree->get_edit_icon($element, $this->gpr);
        }

        $actions .= $this->gtree->get_calculation_icon($element, $this->gpr);

        if ($element['type'] == 'item' or ($element['type'] == 'category' and $element['depth'] > 1)) {
            if ($this->element_deletable($element)) {
                $actions .= '<a href="index.php?id='.$COURSE->id.'&amp;action=delete&amp;eid='
                         . $eid.'&amp;sesskey='.sesskey().'"><img src="'.$CFG->pixpath.'/t/delete.gif" class="iconsmall" alt="'
                         . get_string('delete').'" title="'.get_string('delete').'"/></a>';
            }
            $actions .= '<a href="index.php?id='.$COURSE->id.'&amp;action=moveselect&amp;eid='
                     . $eid.'&amp;sesskey='.sesskey().'"><img src="'.$CFG->pixpath.'/t/move.gif" class="iconsmall" alt="'
                     . get_string('move').'" title="'.get_string('move').'"/></a>';
        }

        $actions .= $this->gtree->get_hiding_icon($element, $this->gpr);
        $actions .= $this->gtree->get_locking_icon($element, $this->gpr);

        $mode = ($USER->gradeediting[$COURSE->id]) ? 'advanced' : 'simple';

        $html = '';
        $root = false;


        $id = required_param('id', PARAM_INT);

        /// prepare move target if needed
        $last = '';

        /// print the list items now
        if ($this->moving == $eid) {

            // do not diplay children
            return '<tr><td colspan="12" class="'.$element['type'].' moving">'.$object->name.' ('.get_string('move').')</td></tr>';

        }

        if ($element['type'] == 'category') {
            $level++;
            $categories[$object->id] = $object->stripped_name;
            $category = grade_category_local::fetch(array('id' => $object->id));
            $item = $category->get_grade_item();

            // Add aggregation coef input if not a course item and if parent category has correct aggregation type
            $dimmed = ($item->is_hidden()) ? " dimmed " : "";

            // Before we print the category's row, we must find out how many rows will appear below it (for the filler cell's rowspan)
            $aggregation_position = grade_get_setting($COURSE->id, 'aggregationposition', $CFG->grade_aggregationposition);
            $category_total_data = null; // Used if aggregationposition is set to "last", so we can print it last

            $html_children = '';

            $row_count = 0;

            foreach($element['children'] as $child_el) {
                $moveto = '';

                if (empty($child_el['object']->itemtype)) {
                    $child_el['object']->itemtype = false;
                }

                if (($child_el['object']->itemtype == 'course' || $child_el['object']->itemtype == 'category') && !$totals) {
                    continue;
                }

                $child_eid    = $child_el['eid'];
                $first = '';

                if ($child_el['object']->itemtype == 'course' || $child_el['object']->itemtype == 'category') {
                    $first = '&amp;first=1';
                    $child_eid = $eid;
                }

                if ($this->moving && $this->moving != $child_eid) {

                    $strmove     = get_string('move');
                    $strmovehere = get_string('movehere');
                    $actions = ''; // no action icons when moving

                    $moveto = '<tr><td colspan="12"><a href="index.php?id='.$COURSE->id.'&amp;action=move&amp;eid='.$this->moving.'&amp;moveafter='
                            . $child_eid.'&amp;sesskey='.sesskey().$first.'"><img class="movetarget" src="'.$CFG->wwwroot.'/pix/movehere.gif" alt="'
                            . $strmovehere.'" title="'.s($strmovehere).'" /></a></td></tr>';
                }

                $newparents = $parents;
                $newparents[] = $eid;

                $row_count++;
                $child_row_count = 0;

                // If moving, do not print course and category totals, but still print the moveto target box
                if ($this->moving && ($child_el['object']->itemtype == 'course' || $child_el['object']->itemtype == 'category')) {
                    $html_children .= $moveto;
                } elseif ($child_el['object']->itemtype == 'course' || $child_el['object']->itemtype == 'category') {
                    // We don't build the item yet because we first need to know the deepest level of categories (for category/name colspans)
                    $category_total_item = $this->build_html_tree($child_el, $totals, $newparents, $categories, $level, $child_row_count);
                    if (!$aggregation_position) {
                        $html_children .= $category_total_item;
                    }
                } else {
                    $html_children .= $this->build_html_tree($child_el, $totals, $newparents, $categories, $level, $child_row_count) . $moveto;

                    if ($this->moving) {
                        $row_count++;
                    }
                }

                $row_count += $child_row_count;

                // If the child is a category, increment row_count by one more (for the extra coloured row)
                if ($child_el['type'] == 'category') {
                    $row_count++;
                }
            }

            // Print category total at the end if aggregation position is "last" (1)
            if (!empty($category_total_item) && $aggregation_position) {
                $html_children .= $category_total_item;
            }

            // now build the header
            if (isset($element['object']->grade_item) && $element['object']->grade_item->is_course_item()) {
                // Reduce width if advanced elements are not shown
                $width_style = '';

                if ($mode == 'simple') {
                    $width_style = ' style="width:auto;" ';
                }

                $html .= '<table cellpadding="5" class="generaltable" '.$width_style.'>
                            <tr>';

                foreach ($this->columns as $column) {
                    if (!($this->moving && $column->hide_when_moving) && !$column->is_hidden($mode)) {
                        $html .= $column->get_header_cell();
                    }
                }

                $html .= '</tr>';
                $root = true;
            }

            $row_count_offset = 0;

            if (empty($category_total_item) && !$this->moving) {
                $row_count_offset = -1;
            }

            $levelclass = " level$level ";

            $html .= '
                    <tr class="category '.$dimmed.$rowclasses.'">
                      <th scope="row" title="'.s($object->stripped_name).'" class="cell rowspan '.$levelclass.'" rowspan="'.($row_count+1+$row_count_offset).'"></th>';

            foreach ($this->columns as $column) {
                if (!($this->moving && $column->hide_when_moving) && !$column->is_hidden($mode)) {
                    $html .= $column->get_category_cell($category, $levelclass, array('id' => $id, 'name' => $object->name, 'level' => $level, 'actions' => $actions, 'eid' => $eid));
                }
            }

            $html .= '</tr>';

            $html .= $html_children;

            // Print a coloured row to show the end of the category accross the table
            $html .= '<tr><td colspan="'.(19 - $level).'" class="colspan '.$levelclass.'"></td></tr>';

        } else { // Dealing with a grade item

            $item = grade_item_local::fetch(array('id' => $object->id));
            $element['type'] = 'item';
            $element['object'] = $item;

            // Determine aggregation coef element

            $dimmed = ($item->is_hidden()) ? " dimmed_text " : "";
            $html .= '<tr class="item'.$dimmed.$rowclasses.'">';

            foreach ($this->columns as $column) {
                if (!($this->moving && $column->hide_when_moving) && !$column->is_hidden($mode)) {
					// HACK to shorten text of item and category headers
//                	$html .= $column->get_item_cell($item, array('id' => $id, 'name' => $object->name, 'level' => $level, 'actions' => $actions,
//                                                                 'element' => $element, 'eid' => $eid, 'itemtype' => $object->itemtype));
                    $html .= $column->get_item_cell($item, array('id' => $id, 'name' => shorten_text($object->name,60), 'level' => $level, 'actions' => $actions,
                                                                 'element' => $element, 'eid' => $eid, 'itemtype' => $object->itemtype));
                }
            }

            $html .= '</tr>';
        }


        if ($root) {
            $html .= "</table>\n";
        }

        return $html;

    }
}


class grade_edit_tree_column_displaytype extends grade_edit_tree_column {

    function grade_edit_tree_column_displaytype($params) {
        parent::grade_edit_tree_column('displaytype');
    }

    function get_header_cell() {
        return '<th class="header" scope="col">'.get_string('gradedisplaytype', 'grades').helpbutton('gradedisplaytype', 'gradedisplaytype', 'grade', true, false, '', true).'</th>';
    }

    function get_category_cell($category, $levelclass, $params) {
        global $CFG;
        if (empty($params['id'])) {
            error('Array key (id) missing from 3rd param of grade_edit_tree_column_aggregation::get_category_cell($category, $levelclass, $params)');
        }

        $options = array(GRADE_DISPLAY_TYPE_DEFAULT            => get_string('default', 'grades'),
                         GRADE_DISPLAY_TYPE_REAL               => get_string('real', 'grades'),
                         GRADE_DISPLAY_TYPE_PERCENTAGE         => get_string('percentage', 'grades'),
                         GRADE_DISPLAY_TYPE_LETTER             => get_string('letter', 'grades'),
                         GRADE_DISPLAY_TYPE_REAL_PERCENTAGE    => get_string('realpercentage', 'grades'),
                         GRADE_DISPLAY_TYPE_REAL_LETTER        => get_string('realletter', 'grades'),
                         GRADE_DISPLAY_TYPE_LETTER_REAL        => get_string('letterreal', 'grades'),
                         GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE  => get_string('letterpercentage', 'grades'),
                         GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER  => get_string('percentageletter', 'grades'),
                         GRADE_DISPLAY_TYPE_PERCENTAGE_REAL    => get_string('percentagereal', 'grades')
                         );

        $script = "window.location='index.php?id={$params['id']}&amp;category={$category->id}&amp;displaytype='+this.value+'&amp;sesskey=" . sesskey()."';";
        $displaytype = choose_from_menu($options, 'displaytype_'.$category->id, $category->displaytype, null, $script, 0, true);

        if ($this->forced) {
            $displaytype = $options[$category->displaytype];
        }

        return '<td class="cell '.$levelclass.'">' . $displaytype . '</td>';

    }

    function get_item_cell($item, $params) {
        global $CFG;
        if (empty($params['id'])) {
            error('Array key (id) missing from 3rd param of grade_edit_tree_column_aggregation::get_category_cell($category, $levelclass, $params)');
        }

        $options = array(GRADE_DISPLAY_TYPE_DEFAULT            => get_string('default', 'grades'),
                         GRADE_DISPLAY_TYPE_REAL               => get_string('real', 'grades'),
                         GRADE_DISPLAY_TYPE_PERCENTAGE         => get_string('percentage', 'grades'),
                         GRADE_DISPLAY_TYPE_LETTER             => get_string('letter', 'grades'),
                         GRADE_DISPLAY_TYPE_REAL_PERCENTAGE    => get_string('realpercentage', 'grades'),
                         GRADE_DISPLAY_TYPE_REAL_LETTER        => get_string('realletter', 'grades'),
                         GRADE_DISPLAY_TYPE_LETTER_REAL        => get_string('letterreal', 'grades'),
                         GRADE_DISPLAY_TYPE_LETTER_PERCENTAGE  => get_string('letterpercentage', 'grades'),
                         GRADE_DISPLAY_TYPE_PERCENTAGE_LETTER  => get_string('percentageletter', 'grades'),
                         GRADE_DISPLAY_TYPE_PERCENTAGE_REAL    => get_string('percentagereal', 'grades')
                         );

        $script = "window.location='index.php?id={$params['id']}&ampitem={$category->id}&amp;displaytype='+this.value+'&amp;sesskey=" . sesskey()."';";
        if (!in_array($params['element']['object']->itemtype, array('courseitem', 'categoryitem', 'category'))) {
            $displaytype = choose_from_menu($options, 'displaytype_'.$item->id, $item->displaytype, null, $script, 0, true);
        }
        if ($this->forced) {
            $displaytype = $options[$category->displaytype];
        }

        return '<td class="cell">' . $displaytype . '</td>';

    }
}


class grade_edit_tree_column_extracredit_local extends grade_edit_tree_column_extracredit {


    function get_category_cell($category, $levelclass, $params) {

        $item = $category->get_grade_item();
        $aggcoef_input = grade_edit_tree_local::get_weight_input($item, 'extra');
        return '<td class="cell '.$levelclass.'">' . $aggcoef_input . '</td>';
    }

    function get_item_cell($item, $params) {
        if (empty($params['element'])) {
            error('Array key (element) missing from 2nd param of grade_edit_tree_column_weightorextracredit::get_item_cell($item, $params)');
        }

        $html = '<td class="cell">';

        if (!in_array($params['element']['object']->itemtype, array('courseitem', 'categoryitem', 'category'))) {
            $html .= grade_edit_tree_local::get_weight_input($item, 'extra');
        }

        return $html.'</td>';
    }
}

class grade_edit_tree_column_weight_local extends grade_edit_tree_column_weight {

    function get_category_cell($category, $levelclass, $params) {

        $item = $category->get_grade_item();
        $aggcoef_input = grade_edit_tree_local::get_weight_input($item, 'weight');
        return '<td class="cell '.$levelclass.'">' . $aggcoef_input . '</td>';
    }

    function get_item_cell($item, $params) {
        if (empty($params['element'])) {
            error('Array key (element) missing from 2nd param of grade_edit_tree_column_weightorextracredit::get_item_cell($item, $params)');
        }

        $html = '<td class="cell">';

        if (!in_array($params['element']['object']->itemtype, array('courseitem', 'categoryitem', 'category'))) {
            $html .= grade_edit_tree_local::get_weight_input($item, 'weight');
        }

        return $html.'</td>';
    }
}



class grade_object_local extends grade_object {

    /**
     * Factory method - uses the parameters to retrieve matching instance from the DB.
     * @static final protected
     * @return mixed object instance or false if not found
     */
    function fetch_helper($table, $classname, $params) {
        if ($instances = grade_object_local::fetch_all_helper($table, $classname, $params)) {
            if (count($instances) > 1) {
                // we should not tolerate any errors here - problems might appear later
                error('Found more than one record in fetch() !');
            }
            return reset($instances);
        } else {
            return false;
        }
    }

    /**
     * Factory method - uses the parameters to retrieve all matching instances from the DB.
     * @static final protected
     * @return mixed array of object instances or false if not found
     */
    function fetch_all_helper($table, $classname, $params) {
        $instance = new $classname();

        $classvars = (array)$instance;
        $params    = (array)$params;

        $wheresql = array();

        // remove incorrect params
        foreach ($params as $var=>$value) {
            if (!in_array($var, $instance->required_fields) and !array_key_exists($var, $instance->optional_fields)) {
                continue;
            }
            if (is_null($value)) {
                $wheresql[] = " $var IS NULL ";
            } else {
                $value = addslashes($value);
                $wheresql[] = " $var = '$value' ";
            }
        }

        if (empty($wheresql)) {
            $wheresql = '';
        } else {
            $wheresql = implode("AND", $wheresql);
        }

        if ($datas = get_records_select($table, $wheresql, 'id')) {
            $result = array();
            foreach($datas as $data) {
                $instance = new $classname();
                grade_object_local::set_properties($instance, $data);
                $result[$instance->id] = $instance;
            }
            return $result;

        } else {
            return false;
        }
    }


    /**
     * Records this object in the Database, sets its id to the returned value, and returns that value.
     * If successful this function also fetches the new object data from database and stores it
     * in object properties.
     * @param string $source from where was the object inserted (mod/forum, manual, etc.)
     * @return int PK ID if successful, false otherwise
     */
    function insert($source=null) {
        global $USER, $CFG;

        if (!empty($this->id)) {
            debugging("Grade object already exists!");
            return false;
        }

        $data = $this->get_record_data();
        // HACK: Bob Puffer to allow accurate max score to be inserted into the grade_item record
        $data->rawgrademax = $this->grade_item->grademax;  // END OF HACK

        if (!$this->id = insert_record($this->table, addslashes_recursive($data))) {
            debugging("Could not insert object into db");
            return false;
        }

        // set all object properties from real db data
        $this->update_from_db();

        $data = $this->get_record_data();

        if (empty($CFG->disablegradehistory)) {
            unset($data->timecreated);
            $data->action       = GRADE_HISTORY_INSERT;
            $data->oldid        = $this->id;
            $data->source       = $source;
            $data->timemodified = time();
            $data->userlogged   = $USER->id;
            insert_record($this->table.'_history', addslashes_recursive($data));
        }

        return $this->id;
    }
}

class grade_grade_local extends grade_grade {
  	 /**
     * Finds and returns a grade_grade instance based on params.
     * @static
     *
     * @param array $params associative arrays varname=>value
     * @return object grade_grade instance or false if none found.
     */
    function fetch($params) {
        return grade_object_local::fetch_helper('grade_grades', 'grade_grade_local', $params);
    }

    /**
     * Finds and returns all grade_grade instances based on params.
     * @static
     *
     * @param array $params associative arrays varname=>value
     * @return array array of grade_grade insatnces or false if none found.
     */
    function fetch_all($params) {
        return grade_object_local::fetch_all_helper('grade_grades', 'grade_grade_local', $params);
    }

        /**
     * Loads the grade_item object referenced by $this->itemid and saves it as $this->grade_item for easy access.
     * @return object grade_item.
     */
    function load_grade_item() {
        if (empty($this->itemid)) {
            debugging('Missing itemid');
            $this->grade_item = null;
            return null;
        }

        if (empty($this->grade_item)) {
            $this->grade_item = grade_item_local::fetch(array('id'=>$this->itemid));

        } else if ($this->grade_item->id != $this->itemid) {
            debugging('Itemid mismatch');
            $this->grade_item = grade_item_local::fetch(array('id'=>$this->itemid));
        }

        return $this->grade_item;
    }


}

class grade_item_local extends grade_item {

    /**
     * Performs the necessary calculations on the grades_final referenced by this grade_item.
     * Also resets the needsupdate flag once successfully performed.
     *
     * This function must be used ONLY from lib/gradeslib.php/grade_regrade_final_grades(),
     * because the regrading must be done in correct order!!
     *
     * @return boolean true if ok, error string otherwise
     */
    function regrade_final_grades($userid=null) {
        global $CFG;

        // locked grade items already have correct final grades
        if ($this->is_locked()) {
            return true;
        }

        // calculation produces final value using formula from other final values
        if ($this->is_calculated()) {
            if ($this->compute($userid)) {
                return true;
            } else {
                return "Could not calculate grades for grade item"; // TODO: improve and localize
            }

        // noncalculated outcomes already have final values - raw grades not used
        } else if ($this->is_outcome_item()) {
            return true;

        // aggregate the category grade
        } else if ($this->is_category_item() or $this->is_course_item()) {
            // aggregate category grade item
            $category = $this->get_item_category();
            $category->grade_item =& $this;
            if ($category->generate_grades($userid)) {
                return true;
            } else {
                return "Could not aggregate final grades for category:".$this->id; // TODO: improve and localize
            }

        } else if ($this->is_manual_item()) {
            // manual items track only final grades, no raw grades
//            COMMENTED OUT SO MANUAL ITEMS CAN MAKE SURE THEIR MAXGRADE GETS UPDATED CORRECTLY
//            return true;

        } else if (!$this->is_raw_used()) {
            // hmm - raw grades are not used- nothing to regrade
            return true;
        }

        // normal grade item - just new final grades
        $result = true;
        $grade_inst = new grade_grade();
        $fields = implode(',', $grade_inst->required_fields);
        if ($userid) {
            $rs = get_recordset_select('grade_grades', "itemid={$this->id} AND userid=$userid", '', $fields);
        } else {
            $rs = get_recordset('grade_grades', 'itemid', $this->id, '', $fields);
        }
        if ($rs) {
            while ($grade_record = rs_fetch_next_record($rs)) {
                $grade = new grade_grade($grade_record, false);

                if (!empty($grade_record->locked) or !empty($grade_record->overridden)) {
                    // this grade is locked - final grade must be ok
                    continue;
                }

                $grade->rawgrademax = $this->grademax;
                if (! $this->is_manual_item()) {
                    $grade->finalgrade = $this->adjust_raw_grade($grade->rawgrade, $grade->rawgrademin, $grade->rawgrademax);
                }
                if (grade_floats_different($grade_record->finalgrade, $grade->finalgrade) OR grade_floats_different($grade_record->rawgrademax, $grade->rawgrademax)) {
                    if (!$grade->update('system')) {
                        $result = "Internal error updating final grade";
                    }
                }
            }
            rs_close($rs);
        }

        return $result;
    }
	
	/**
     * Returns grade item associated with the course
     * @param int $courseid
     * @return course item object
     */
    function fetch_course_item($courseid) {
        if ($course_item = grade_item_local::fetch(array('courseid'=>$courseid, 'itemtype'=>'course'))) {
            return $course_item;
        }

        // first get category - it creates the associated grade item
        $course_category = grade_category_local::fetch_course_category($courseid);
        return $course_category->get_grade_item();
    }
    /**
     * Finds and returns a grade_item instance based on params.
     * @static
     *
     * @param array $params associative arrays varname=>value
     * @return object grade_item instance or false if none found.
     */
    function fetch($params) {
        return grade_object_local::fetch_helper('grade_items', 'grade_item_local', $params);
    }

    /**
     * Finds and returns all grade_item instances based on params.
     * @static
     *
     * @param array $params associative arrays varname=>value
     * @return array array of grade_item insatnces or false if none found.
     */
    function fetch_all($params) {
        return grade_object_local::fetch_all_helper('grade_items', 'grade_item_local', $params);
    }
    
	
	/**
     * Generates and saves final grades in associated category grade item.
     * These immediate children must already have their own final grades.
     * The category's aggregation method is used to generate final grades.
     *
     * Please note that category grade is either calculated or aggregated - not both at the same time.
     *
     * This method must be used ONLY from grade_item::regrade_final_grades(),
     * because the calculation must be done in correct order!
     *
     * Steps to follow:
     *  1. Get final grades from immediate children
     *  3. Aggregate these grades
     *  4. Save them in final grades of associated category grade item
     */

    /**
     * Returns a string representing the range of grademin - grademax for this grade item.
     * @param int $rangesdisplaytype
     * @param int $rangesdecimalpoints
     * @return string
     */
    function get_formatted_range($rangesdisplaytype=null, $rangesdecimalpoints=null) {

        global $USER;

        // Determine which display type to use for this average
        if (isset($USER->gradeediting) && $USER->gradeediting[$this->courseid]) {
            $displaytype = GRADE_DISPLAY_TYPE_REAL;

        } else if ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // no ==0 here, please resave report and user prefs
            $displaytype = $this->get_displaytype();

        } else {
            $displaytype = $rangesdisplaytype;
        }

        // Override grade_item setting if a display preference (not default) was set for the averages
        if ($rangesdecimalpoints !== null) {
            $decimalpoints = $rangesdecimalpoints;
        } else if ($rangesdecimalpoints == GRADE_REPORT_PREFERENCE_INHERIT) {
            $decimalpoints = $this->get_decimals();
        } else {
            $decimalpoints = $rangesdecimalpoints;
        }

        if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
            $grademin = "0 %";
            $grademax = "100 %";

        } else {
            $grademin = grade_format_gradevalue($this->grademin, $this, true, $displaytype, $decimalpoints);
            $grademax = grade_format_gradevalue($this->grademax, $this, true, $displaytype, $decimalpoints);
        }

        return $grademin.'&ndash;'. $grademax;
    }


    /**
    * Returns the grade_category for category item
    *
    * @return mixed grade_category object if applicable, false otherwise
    */
    function get_item_category() {
        if (!$this->is_course_item() and !$this->is_category_item()) {
            return false;
        }
        return grade_category_local::fetch(array('id'=>$this->iteminstance));
    }
    
        /**
     * Updates final grade value for given user, this is a only way to update final
     * grades from gradebook and import because it logs the change in history table
     * and deals with overridden flag. This flag is set to prevent later overriding
     * from raw grades submitted from modules.
     *
     * @param int $userid the graded user
     * @param mixed $finalgrade float value of final grade - false means do not change
     * @param string $howmodified modification source
     * @param string $note optional note
     * @param mixed $feedback teachers feedback as string - false means do not change
     * @param int $feedbackformat
     * @return boolean success
     */
    function update_final_grade($userid, $finalgrade=false, $source=NULL, $feedback=false, $feedbackformat=FORMAT_MOODLE, $usermodified=null) {
        global $USER, $CFG;

        $result = true;

        // no grading used or locked
        if ($this->gradetype == GRADE_TYPE_NONE or $this->is_locked()) {
            return false;
        }

        $grade = new grade_grade(array('itemid'=>$this->id, 'userid'=>$userid));
        $grade->grade_item =& $this; // prevent db fetching of this grade_item

        if (empty($usermodified)) {
            $grade->usermodified = $USER->id;
        } else {
            $grade->usermodified = $usermodified;
        }

        if ($grade->is_locked()) {
            // do not update locked grades at all
            return false;
        }

        $locktime = $grade->get_locktime();
        if ($locktime and $locktime < time()) {
            // do not update grades that should be already locked, force regrade instead
            $this->force_regrading();
            return false;
        }

        $oldgrade = new object();
        $oldgrade->finalgrade     = $grade->finalgrade;
        $oldgrade->overridden     = $grade->overridden;
        $oldgrade->feedback       = $grade->feedback;
        $oldgrade->feedbackformat = $grade->feedbackformat;

        // changed grade?
        if ($finalgrade !== false) {
            if ($this->is_overridable_item()) {
                $grade->overridden = time();
            } else {
                $grade->overridden = 0;
            }

            $grade->finalgrade = $this->bounded_grade($finalgrade);
        }

        // do we have comment from teacher?
        if ($feedback !== false) {
            if ($this->is_overridable_item_feedback()) {
                // external items (modules, plugins) may have own feedback
                $grade->overridden = time();
            }

            $grade->feedback       = $feedback;
            $grade->feedbackformat = $feedbackformat;
        }

        // HACK: Bob Puffer to allow accurate max score to be inserted into the grade_item record
        $grade->rawgrademax = $this->grademax;  
        // END OF HACK
        
        if (empty($grade->id)) {
            $grade->timecreated  = null;   // hack alert - date submitted - no submission yet
            $grade->timemodified = time(); // hack alert - date graded
            $result = (boolean)$grade->insert($source);

        } else if (grade_floats_different($grade->finalgrade, $oldgrade->finalgrade)
                or $grade->feedback       !== $oldgrade->feedback
                or $grade->feedbackformat != $oldgrade->feedbackformat
                or $grade->overridden     != $oldgrade->overridden) {
            $grade->timemodified = time(); // hack alert - date graded
            $result = $grade->update($source);
        } else {
            // no grade change
            return $result;
        }

        if (!$result) {
            // something went wrong - better force final grade recalculation
            $this->force_regrading();

        } else if ($this->is_course_item() and !$this->needsupdate) {
            if (grade_regrade_final_grades($this->courseid, $userid, $this) !== true) {
                $this->force_regrading();
            }

        } else if (!$this->needsupdate) {
            $course_item = grade_item_local::fetch_course_item($this->courseid);
            if (!$course_item->needsupdate) {
                if (grade_regrade_final_grades_local($this->courseid, $userid, $this) !== true) {
                    $this->force_regrading();
                }
            } else {
                $this->force_regrading();
            }
        }

        return $result;
    }
    /**
     * Set the hidden status of grade_item and all grades, 0 mean visible, 1 always hidden, number means date to hide until.
     * @param int $hidden new hidden status
     * @param boolean $cascade apply to child objects too
     * @return void
     */
    function set_hidden($hidden, $cascade=false) {
        $this->hidden = $hidden;
        $this->update();

        if ($cascade) {
            if ($grades = grade_grade_local::fetch_all(array('itemid'=>$this->id))) {
                foreach($grades as $grade) {
                    $grade->grade_item =& $this;
                    $grade->set_hidden($hidden, $cascade);
                }
            }
        }
    }

        /**
     * Returns the most descriptive field for this object. This is a standard method used
     * when we do not know the exact type of an object.
     * @param boolean $fulltotal: if the item is a category total, returns $categoryname."total" instead of "Category total" or "Course total"
     * @return string name
     */
    function get_name($fulltotal=false) {
        if (!empty($this->itemname)) {
            // MDL-10557
            return format_string(shorten_text($this->itemname));

//        } else if ($this->is_course_item()) {
//            return get_string('coursetotal', 'grades');

        } else if ($this->is_category_item()) {
            if ($fulltotal) {
                $category = $this->load_parent_category();
                $a = new stdClass();
                $a->category = shorten_text($category->get_name());
                return get_string('categorytotalfull', 'grades', $a);
            } else {
                return get_string('categorytotal', 'grades');
            }

        } else {
            return get_string('grade');
        }
    }

    
}

	
class grade_category_local extends grade_category {
    
    /**
     * Finds and returns a grade_category instance based on params.
     * @static
     *
     * @param array $params associative arrays varname=>value
     * @return object grade_category instance or false if none found.
     */
    function fetch($params) {
        return grade_object_local::fetch_helper('grade_categories', 'grade_category_local', $params);
    }

    /**
     * Finds and returns all grade_category instances based on params.
     * @static
     *
     * @param array $params associative arrays varname=>value
     * @return array array of grade_category insatnces or false if none found.
     */
    function fetch_all($params) {
        return grade_object_local::fetch_all_helper('grade_categories', 'grade_category_local', $params);
    }
	
    /**
     * Returns tree with all grade_items and categories as elements
     * @static
     * @param int $courseid
     * @param boolean $include_category_items as category children
     * @return array
     */
    function fetch_course_tree($courseid, $include_category_items=false) {
        $course_category = grade_category_local::fetch_course_category($courseid);
        $category_array = array('object'=>$course_category, 'type'=>'category', 'depth'=>1,
                                'children'=>$course_category->get_children($include_category_items));
        $sortorder = 1;
        $course_category->set_sortorder($sortorder);
        $course_category->sortorder = $sortorder;
        return grade_category_local::_fetch_course_tree_recursion($category_array, $sortorder);
    }
    
    function _fetch_course_tree_recursion($category_array, &$sortorder) {
        // update the sortorder in db if needed
        if ($category_array['object']->sortorder != $sortorder) {
            $category_array['object']->set_sortorder($sortorder);
        }

        // store the grade_item or grade_category instance with extra info
        $result = array('object'=>$category_array['object'], 'type'=>$category_array['type'], 'depth'=>$category_array['depth']);

        // reuse final grades if there
        if (array_key_exists('finalgrades', $category_array)) {
            $result['finalgrades'] = $category_array['finalgrades'];
        }

        // recursively resort children
        if (!empty($category_array['children'])) {
            $result['children'] = array();
            //process the category item first
            $cat_item_id = null;
            foreach($category_array['children'] as $oldorder=>$child_array) {
                if ($child_array['type'] == 'courseitem' or $child_array['type'] == 'categoryitem') {
                    $result['children'][$sortorder] = grade_category_local::_fetch_course_tree_recursion($child_array, $sortorder);
                }
            }
            foreach($category_array['children'] as $oldorder=>$child_array) {
                if ($child_array['type'] != 'courseitem' and $child_array['type'] != 'categoryitem') {
                    $result['children'][++$sortorder] = grade_category_local::_fetch_course_tree_recursion($child_array, $sortorder);
                }
            }
        }

        return $result;
    }


    /**
     * Return the top most course category.
     * @static
     * @return object grade_category instance for course grade
     */
    function fetch_course_category($courseid) {
        if (empty($courseid)) {
            debugging('Missing course id!');
            return false;
        }

        // course category has no parent
        if ($course_category = grade_category_local::fetch(array('courseid'=>$courseid, 'parent'=>null))) {
            return $course_category;
        }

        // create a new one
        $course_category = new grade_category_local();
        $course_category->insert_course_category($courseid);

        return $course_category;
    }
    
    /**
     * Sets the grade_item's hidden variable and updates the grade_item.
     * Method named after grade_item::set_hidden().
     * @param int $hidden 0, 1 or a timestamp int(10) after which date the item will be hidden.
     * @param boolean $cascade apply to child objects too
     * @return void
     */
    function set_hidden($hidden, $cascade=false) {
        $this->load_grade_item();
        $this->grade_item->set_hidden($hidden);
        if ($cascade) {
            if ($children = grade_item_local::fetch_all(array('categoryid'=>$this->id))) {
                foreach($children as $child) {
                    $child->set_hidden($hidden, $cascade);
                }
            }
            if ($children = grade_category_local::fetch_all(array('parent'=>$this->id))) {
                foreach($children as $child) {
                    $child->set_hidden($hidden, $cascade);
                }
            }
        }
    }
    
    /**
     * Retrieves from DB and instantiates the associated grade_item object.
     * If no grade_item exists yet, create one.
     * @return object Grade_item
     */
    function get_grade_item() {
        if (empty($this->id)) {
            debugging("Attempt to obtain a grade_category's associated grade_item without the category's ID being set.");
            return false;
        }

        if (empty($this->parent)) {
            $params = array('courseid'=>$this->courseid, 'itemtype'=>'course', 'iteminstance'=>$this->id);

        } else {
            $params = array('courseid'=>$this->courseid, 'itemtype'=>'category', 'iteminstance'=>$this->id);
        }

        if (!$grade_items = grade_item_local::fetch_all($params)) {
            // create a new one
            $grade_item = new grade_item_local($params, false);
            $grade_item->gradetype = GRADE_TYPE_VALUE;
            $grade_item->insert('system');

        } else if (count($grade_items) == 1){
            // found existing one
            $grade_item = reset($grade_items);

        } else {
            debugging("Found more than one grade_item attached to category id:".$this->id);
            // return first one
            $grade_item = reset($grade_items);
        }

        return $grade_item;
    }

    /**
     * Fetches and returns all the children categories and/or grade_items belonging to this category.
     * By default only returns the immediate children (depth=1), but deeper levels can be requested,
     * as well as all levels (0). The elements are indexed by sort order.
     * @return array Array of child objects (grade_category and grade_item).
     */
    function get_children($include_category_items=false) {

        // This function must be as fast as possible ;-)
        // fetch all course grade items and categories into memory - we do not expect hundreds of these in course
        // we have to limit the number of queries though, because it will be used often in grade reports

        $cats  = get_records('grade_categories', 'courseid', $this->courseid);
        $items = get_records('grade_items', 'courseid', $this->courseid);

        // init children array first
        foreach ($cats as $catid=>$cat) {
            $cats[$catid]->children = array();
        }

        //first attach items to cats and add category sortorder
        foreach ($items as $item) {
            if ($item->itemtype == 'course' or $item->itemtype == 'category') {
                $cats[$item->iteminstance]->sortorder = $item->sortorder;

                if (!$include_category_items) {
                    continue;
                }
                $categoryid = $item->iteminstance;
            } else {
                $categoryid = $item->categoryid;
            }

            // prevent problems with duplicate sortorders in db
            $sortorder = $item->sortorder;
            while(array_key_exists($sortorder, $cats[$categoryid]->children)) {
                //debugging("$sortorder exists in item loop");
                $sortorder++;
            }

            $cats[$categoryid]->children[$sortorder] = $item;

        }

        // now find the requested category and connect categories as children
        $category = false;
        foreach ($cats as $catid=>$cat) {
            if (empty($cat->parent)) {
                if ($cat->path !== '/'.$cat->id.'/') {
                    $grade_category = new grade_category_local($cat, false);
                    $grade_category->path  = '/'.$cat->id.'/';
                    $grade_category->depth = 1;
                    $grade_category->update('system');
                    return $this->get_children($include_category_items);
                }
            } else {
                if (empty($cat->path) or !preg_match('|/'.$cat->parent.'/'.$cat->id.'/$|', $cat->path)) {
                    //fix paths and depts
                    static $recursioncounter = 0; // prevents infinite recursion
                    $recursioncounter++;
                    if ($recursioncounter < 5) {
                        // fix paths and depths!
                        $grade_category = new grade_category_local($cat, false);
                        $grade_category->depth = 0;
                        $grade_category->path  = null;
                        $grade_category->update('system');
                        return $this->get_children($include_category_items);
                    }
                }
                // prevent problems with duplicate sortorders in db
                $sortorder = $cat->sortorder;
                while(array_key_exists($sortorder, $cats[$cat->parent]->children)) {
                    //debugging("$sortorder exists in cat loop");
                    $sortorder++;
                }

                $cats[$cat->parent]->children[$sortorder] = &$cats[$catid];
            }

            if ($catid == $this->id) {
                $category = &$cats[$catid];
            }
        }

        unset($items); // not needed
        unset($cats); // not needed

        $children_array = grade_category_local::_get_children_recursion($category);

        ksort($children_array);

        return $children_array;

    }

    function _get_children_recursion($category) {

        $children_array = array();
        foreach($category->children as $sortorder=>$child) {
            if (array_key_exists('itemtype', $child)) {
                $grade_item = new grade_item_local($child, false);
                if (in_array($grade_item->itemtype, array('course', 'category'))) {
                    $type  = $grade_item->itemtype.'item';
                    $depth = $category->depth;
                } else {
                    $type  = 'item';
                    $depth = $category->depth; // we use this to set the same colour
                }
                $children_array[$sortorder] = array('object'=>$grade_item, 'type'=>$type, 'depth'=>$depth);

            } else {
                $children = grade_category_local::_get_children_recursion($child);
                $grade_category = new grade_category_local($child, false);
                if (empty($children)) {
                    $children = array();
                }
                $children_array[$sortorder] = array('object'=>$grade_category, 'type'=>'category', 'depth'=>$grade_category->depth, 'children'=>$children);
            }
        }

        // sort the array
        ksort($children_array);

        return $children_array;
    }


    function generate_grades($userid=null) {
        global $CFG;

        $this->load_grade_item();

        if ($this->grade_item->is_locked()) {
            return true; // no need to recalculate locked items
        }

        // find grade items of immediate children (category or grade items) and force site settings
        $depends_on = $this->grade_item->depends_on();

        if (empty($depends_on)) {
            $items = false;
        } else {
            $gis = implode(',', $depends_on);
            $sql = "SELECT *
                      FROM {$CFG->prefix}grade_items
                     WHERE id IN ($gis)";
            $items = get_records_sql($sql);
        }

        // needed mostly for SUM agg type
        $this->auto_update_max($items);

        if ($userid) {
            $usersql = "AND g.userid=$userid";
        } else {
            $usersql = "";
        }

        $grade_inst = new grade_grade();
        $fields = 'g.'.implode(',g.', $grade_inst->required_fields);

        // where to look for final grades - include grade of this item too, we will store the results there
        $gis = implode(',', array_merge($depends_on, array($this->grade_item->id)));
        $sql = "SELECT $fields
                  FROM {$CFG->prefix}grade_grades g, {$CFG->prefix}grade_items gi
                 WHERE gi.id = g.itemid AND gi.id IN ($gis) $usersql
              ORDER BY g.userid";

        // group the results by userid and aggregate the grades for this user
        if ($rs = get_recordset_sql($sql)) {
            $prevuser = 0;
            $grade_values = array();
            $excluded     = array();
            $oldgrade     = null;
			// HACK
            $grade_min = array();
            $grade_max = array();  // END OF HACK
            while ($used = rs_fetch_next_record($rs)) {
                if ($used->userid != $prevuser) {
                    $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded,$grade_max, $grade_min);
                    $prevuser = $used->userid;
                    $grade_values = array();
                    // HACK
                    $grade_min = array();
                    $grade_max = array();  // END OF HACK
                    $excluded     = array();
                    $oldgrade     = null;
                }
                $grade_values[$used->itemid] = $used->finalgrade;
                $grade_max[$used->itemid] = $used->rawgrademax;
                $grade_min[$used->itemid] = $used->rawgrademin;
                
                // HACK Bob Puffer 10/28/09 to allow for accurate calculation of total points based on grade_grades.grademax
                // rather than grade_items.maxgrade
//                $items[$used->itemid]->grademax = $used->rawgrademax; // END OF HACK
                
                if ($used->excluded) {
                    $excluded[] = $used->itemid;
                }
                if ($this->grade_item->id == $used->itemid) {
                    $oldgrade = $used;
                }
            }
            $this->aggregate_grades($prevuser, $items, $grade_values, $oldgrade, $excluded,$grade_max, $grade_min);//the last one
            rs_close($rs);
        }

        return true;
    }
    
    /**
     * internal function for category grades aggregation
     *
     * @param int $userid
     * @param array $items
     * @param array $grade_values
     * @param object $oldgrade
     * @param bool $excluded
     * @return boolean (just plain return;)
     */
    function aggregate_grades($userid, $items, $grade_values, $oldgrade, $excluded,$grade_max, $grade_min) {
        global $CFG;
        if (empty($userid)) {
            //ignore first call
            return;
        }

        if ($oldgrade) {
            $oldfinalgrade = $oldgrade->finalgrade;
            $grade = new grade_grade_local($oldgrade, false);
            $grade->grade_item =& $this->grade_item;

        } else {
            // insert final grade - it will be needed later anyway
            $grade = new grade_grade_local(array('itemid'=>$this->grade_item->id, 'userid'=>$userid), false);
            $grade->grade_item =& $this->grade_item;
            $grade->insert('system');
            $oldfinalgrade = null;
        }

        // no need to recalculate locked or overridden grades
        if ($grade->is_locked() or $grade->is_overridden()) {
            return;
        }

        // can not use own final category grade in calculation
        unset($grade_values[$this->grade_item->id]);


    /// sum is a special aggregation types - it adjusts the min max, does not use relative values
		// HACK: Bob Puffer 12/8/09 to allow for correct summing of point totals for all aggregation methods
        if ($this->aggregation == GRADE_AGGREGATE_SUM) {
            $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded);
            return;
        }

        // CONSIDER REINSTATING THIS IF IT DOESN'T WORK OUT TO REMOVE IT
        // sum all aggregation methods
//        $this->sum_grades($grade, $oldfinalgrade, $items, $grade_values, $excluded); // END OF HACK
        
        // if no grades calculation possible or grading not allowed clear final grade
        if (empty($grade_values) or empty($items) or ($this->grade_item->gradetype != GRADE_TYPE_VALUE and $this->grade_item->gradetype != GRADE_TYPE_SCALE)) {
            $grade->finalgrade = null;
            if (!is_null($oldfinalgrade)) {
                $grade->update('aggregation');
            }
            return;
        }

    /// normalize the grades first - all will have value 0...1
        // ungraded items are not used in aggregation
        foreach ($grade_values as $itemid=>$v) {
            if (is_null($v)) {
                // null means no grade
                unset($grade_values[$itemid]);
                continue;
            } else if (in_array($itemid, $excluded)) {
                unset($grade_values[$itemid]);
                continue;
            }
//            $grade_values[$itemid] = grade_grade::standardise_score($v, $items[$itemid]->grademin, $items[$itemid]->grademax, 0, 1);
            $items[$itemid]->grademax = $grade_max[$itemid];
            $grade_values[$itemid] = grade_grade::standardise_score($v, $grade_min[$itemid], $grade_max[$itemid], 0, 1);
        }

        // use min grade if grade missing for these types
        if (!$this->aggregateonlygraded) {
            foreach($items as $itemid=>$value) {
                if (!isset($grade_values[$itemid]) and !in_array($itemid, $excluded)) {
                    $grade_values[$itemid] = 0;
                }
            }
        }

        // limit and sort
        $this->apply_limit_rules($grade_values, $items);
        asort($grade_values, SORT_NUMERIC);

        // HACK 10/29/09 Bob Puffer to allow accurate computation of category maxgrade
        // has to be done after any dropped grades are dropped
        $cat_max = 0; // END OF HACK
        foreach ($grade_values as $itemid=>$v) {
            if ($items[$itemid]->aggregationcoef == 1 AND $this->aggregation <> GRADE_AGGREGATE_WEIGHTED_MEAN) {
            } else if ($items[$itemid]->itemtype == 'category' ){
//                $gradegradesrec = new grade_grade_local(array('itemid'=>$itemid, 'userid'=>$userid), true);
//              if (isset($gradegradesrec->itemid->itemtype) AND $gradegradesrec->itemid->itemtype == 'category') {
//                $cat_max += $gradegradesrec->rawgrademax;
                $cat_max += $grade_max[$itemid];
            } else {
//                $cat_max += $items[$itemid]->grademax;
//                    $cat_max += $gradegradesrec->itemid->grademax;
                    $cat_max += $grade_max[$itemid];
//                }
            }
        }  // END OF HACK
        
        // let's see we have still enough grades to do any statistics
        if (count($grade_values) == 0) {
            // not enough attempts yet
            $grade->finalgrade = null;
            if (!is_null($oldfinalgrade)) {
                $grade->update('aggregation');
            }
            return;
        }

        // do the maths
		// HACK: Bob Puffer 10/29/09 to allow proper totalling of points for the category
//        $agg_grade = $this->aggregate_values($grade_values, $items);
        // recalculate the grade back to requested range
//        $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax);
        $this->grade_item->grademax = $cat_max; 
       	$oldmaxgrade = $grade->rawgrademax; 
       	$grade->rawgrademax = $cat_max; 
       	// HACK we don't want to call the aggregate_values function
       	if ($this->aggregation == GRADE_AGGREGATE_WEIGHTED_MEAN) {
                $weightsum = 0;
                $sum       = null;
                foreach($grade_values as $itemid=>$grade_value) {
                    $weight = $grade_max[$itemid] - $grade_min[$itemid];
//                    $weight = $items[$itemid]->grademax - $items[$itemid]->grademin;
                    if ($weight <= 0) {
                        continue;
                    }
                    $sum += $weight * $grade_value;
                }
                if ($weightsum == 0) {
                    $finalgrade = $sum; // only extra credits
                } else {
                    $finalgrade = $sum / $weightsum;
                }
       	} else {
            $agg_grade = $this->aggregate_values($grade_values, $items);
        	// recalculate the grade back to requested range
            $finalgrade = grade_grade::standardise_score($agg_grade, 0, 1, $this->grade_item->grademin, $this->grade_item->grademax);
        } // END OF HACK
        
        $grade->finalgrade = $this->grade_item->bounded_grade($finalgrade);

        // update in db if changed
        // HACK to update category maxes in the db if they change
//        if (grade_floats_different($grade->finalgrade, $oldfinalgrade)) {
        if (grade_floats_different($grade->finalgrade, $oldfinalgrade) OR grade_floats_different($grade->rawgrademax, $oldmaxgrade)) {
        	$grade->update('aggregation');
        }

        return;
    }
        
        /**
     * Some aggregation tpyes may update max grade
     * @param array $items sub items
     * @return void
     */
    function auto_update_max($items) {
        if ($this->aggregation != GRADE_AGGREGATE_SUM) {
            // not needed at all
//      HACK: Bob Puffer 10/29/09 allowing all category and course grade_item records to maintain accurate
//      maximum awardable points regardless of aggregation method
//            return; // End of hack
        }

        if (!$items) {
            if ($this->grade_item->grademax != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
                $this->grade_item->grademax  = 0;
                $this->grade_item->grademin  = 0;
                $this->grade_item->gradetype = GRADE_TYPE_VALUE;
                $this->grade_item->update('aggregation');
            }
            return;
        }

        //find max grade possible
        $maxes = array();
        foreach ($items as $item) {
            // HACK to allow maxes to be calculated for categories with weighted items
//            if ($item->aggregationcoef > 0) { // remove original line 
//        	if ($item->aggregationcoef > 0) {
        	if ($item->aggregationcoef > 0 AND $this->aggregation <> GRADE_AGGREGATE_WEIGHTED_MEAN) { // END OF HACK
                // extra credit from this activity - does not affect total
                continue;
            }
            if ($item->gradetype == GRADE_TYPE_VALUE) {
                $maxes[$item->id] = $item->grademax;
            } else if ($item->gradetype == GRADE_TYPE_SCALE) {
                $maxes[$item->id] = $item->grademax; // 0 = nograde, 1 = first scale item, 2 = second scale item
            }
        }
        // apply droplow and keephigh
        $this->apply_limit_rules($maxes, $items);
        $max = array_sum($maxes);

        // update db if anything changed
        if ($this->grade_item->grademax != $max or $this->grade_item->grademin != 0 or $this->grade_item->gradetype != GRADE_TYPE_VALUE) {
            $this->grade_item->grademax  = $max;
            $this->grade_item->grademin  = 0;
            $this->grade_item->gradetype = GRADE_TYPE_VALUE;
            $this->grade_item->update('aggregation');
        }
    }
}
?>