Issue:
[moodle-pu.git] / grade / report / grader / lib.php
bloba3028083e03eab89b78c15bf03b1c8c643d3a58f
1 <?php // $Id$
2 /**
3 * File in which the grader_report class is defined.
4 * @package gradebook
5 */
7 require_once($CFG->dirroot . '/grade/report/lib.php');
8 require_once($CFG->libdir.'/tablelib.php');
10 /**
11 * Class providing an API for the grader report building and displaying.
12 * @uses grade_report
13 * @package gradebook
15 class grade_report_grader extends grade_report {
16 /**
17 * The final grades.
18 * @var array $finalgrades
20 var $finalgrades;
22 /**
23 * The grade items.
24 * @var array $items
26 var $items;
28 /**
29 * Array of errors for bulk grades updating.
30 * @var array $gradeserror
32 var $gradeserror = array();
34 //// SQL-RELATED
36 /**
37 * The id of the grade_item by which this report will be sorted.
38 * @var int $sortitemid
40 var $sortitemid;
42 /**
43 * Sortorder used in the SQL selections.
44 * @var int $sortorder
46 var $sortorder;
48 /**
49 * An SQL fragment affecting the search for users.
50 * @var string $userselect
52 var $userselect;
54 /**
55 * List of collapsed categories from user preference
56 * @var array $collapsed
58 var $collapsed;
60 /**
61 * A count of the rows, used for css classes.
62 * @var int $rowcount
64 var $rowcount = 0;
66 /**
67 * Constructor. Sets local copies of user preferences and initialises grade_tree.
68 * @param int $courseid
69 * @param object $gpr grade plugin return tracking object
70 * @param string $context
71 * @param int $page The current page being viewed (when report is paged)
72 * @param int $sortitemid The id of the grade_item by which to sort the table
74 function grade_report_grader($courseid, $gpr, $context, $page=null, $sortitemid=null) {
75 global $CFG;
76 parent::grade_report($courseid, $gpr, $context, $page);
78 // load collapsed settings for this report
79 if ($collapsed = get_user_preferences('grade_report_grader_collapsed_categories')) {
80 $this->collapsed = unserialize($collapsed);
81 } else {
82 $this->collapsed = array('aggregatesonly' => array(), 'gradesonly' => array());
85 if (empty($CFG->enableoutcomes)) {
86 $nooutcomes = false;
87 } else {
88 $nooutcomes = get_user_preferences('grade_report_shownooutcomes');
91 // Grab the grade_tree for this course
92 $this->gtree = new grade_tree($this->courseid, true, $this->get_pref('aggregationposition'), $this->collapsed, $nooutcomes);
94 $this->sortitemid = $sortitemid;
96 // base url for sorting by first/last name
97 $studentsperpage = $this->get_pref('studentsperpage');
98 $perpage = '';
99 $curpage = '';
101 if (!empty($studentsperpage)) {
102 $perpage = '&amp;perpage='.$studentsperpage;
103 $curpage = '&amp;page='.$this->page;
105 $this->baseurl = 'index.php?id='.$this->courseid. $perpage.$curpage.'&amp;';
107 $this->pbarurl = 'index.php?id='.$this->courseid.$perpage.'&amp;';
109 // Setup groups if requested
110 if ($this->get_pref('showgroups')) {
111 $this->setup_groups();
114 $this->setup_sortitemid();
118 * Processes the data sent by the form (grades and feedbacks).
119 * @var array $data
120 * @return bool Success or Failure (array of errors).
122 function process_data($data) {
124 if (!has_capability('moodle/grade:override', $this->context)) {
125 return false;
128 // always initialize all arrays
129 $queue = array();
130 foreach ($data as $varname => $postedvalue) {
132 $needsupdate = false;
133 $note = false; // TODO implement note??
135 // skip, not a grade nor feedback
136 if (strpos($varname, 'grade') === 0) {
137 $data_type = 'grade';
138 } else if (strpos($varname, 'feedback') === 0) {
139 $data_type = 'feedback';
140 } else {
141 continue;
144 $gradeinfo = explode("_", $varname);
145 $userid = clean_param($gradeinfo[1], PARAM_INT);
146 $itemid = clean_param($gradeinfo[2], PARAM_INT);
148 $oldvalue = $data->{'old'.$varname};
150 // was change requested?
151 if ($oldvalue == $postedvalue) {
152 continue;
155 if (!$grade_item = grade_item::fetch(array('id'=>$itemid, 'courseid'=>$this->courseid))) { // we must verify course id here!
156 error('Incorrect grade item id');
159 // Pre-process grade
160 if ($data_type == 'grade') {
161 $feedback = false;
162 $feedbackformat = false;
163 if ($grade_item->gradetype == GRADE_TYPE_SCALE) {
164 if ($postedvalue == -1) { // -1 means no grade
165 $finalgrade = null;
166 } else {
167 $finalgrade = $postedvalue;
169 } else {
170 $finalgrade = unformat_float($postedvalue);
173 } else if ($data_type == 'feedback') {
174 $finalgrade = false;
175 $trimmed = trim($postedvalue);
176 if (empty($trimmed)) {
177 $feedback = NULL;
178 } else {
179 $feedback = stripslashes($postedvalue);
183 $grade_item->update_final_grade($userid, $finalgrade, 'gradebook', $note, $feedback);
186 return true;
191 * Setting the sort order, this depends on last state
192 * all this should be in the new table class that we might need to use
193 * for displaying grades.
195 function setup_sortitemid() {
197 global $SESSION;
199 if ($this->sortitemid) {
200 if (!isset($SESSION->gradeuserreport->sort)) {
201 $this->sortorder = $SESSION->gradeuserreport->sort = 'DESC';
202 } else {
203 // this is the first sort, i.e. by last name
204 if (!isset($SESSION->gradeuserreport->sortitemid)) {
205 $this->sortorder = $SESSION->gradeuserreport->sort = 'DESC';
206 } else if ($SESSION->gradeuserreport->sortitemid == $this->sortitemid) {
207 // same as last sort
208 if ($SESSION->gradeuserreport->sort == 'ASC') {
209 $this->sortorder = $SESSION->gradeuserreport->sort = 'DESC';
210 } else {
211 $this->sortorder = $SESSION->gradeuserreport->sort = 'ASC';
213 } else {
214 $this->sortorder = $SESSION->gradeuserreport->sort = 'DESC';
217 $SESSION->gradeuserreport->sortitemid = $this->sortitemid;
218 } else {
219 // not requesting sort, use last setting (for paging)
221 if (isset($SESSION->gradeuserreport->sortitemid)) {
222 $this->sortitemid = $SESSION->gradeuserreport->sortitemid;
224 if (isset($SESSION->gradeuserreport->sort)) {
225 $this->sortorder = $SESSION->gradeuserreport->sort;
226 } else {
227 $this->sortorder = 'ASC';
233 * pulls out the userids of the users to be display, and sort them
234 * the right outer join is needed because potentially, it is possible not
235 * to have the corresponding entry in grade_grades table for some users
236 * this is check for user roles because there could be some users with grades
237 * but not supposed to be displayed
239 function load_users() {
240 global $CFG;
242 if (is_numeric($this->sortitemid)) {
243 $sql = "SELECT u.id, u.firstname, u.lastname
244 FROM {$CFG->prefix}grade_grades g RIGHT OUTER JOIN
245 {$CFG->prefix}user u ON (u.id = g.userid AND g.itemid = $this->sortitemid)
246 LEFT JOIN {$CFG->prefix}role_assignments ra ON u.id = ra.userid
247 $this->groupsql
248 WHERE ra.roleid in ($this->gradebookroles)
249 $this->groupwheresql
250 AND ra.contextid ".get_related_contexts_string($this->context)."
251 ORDER BY g.finalgrade $this->sortorder";
252 $this->users = get_records_sql($sql, $this->get_pref('studentsperpage') * $this->page,
253 $this->get_pref('studentsperpage'));
254 } else {
255 // default sort
256 // get users sorted by lastname
258 // If lastname or firstname is given as sortitemid, add the other name (firstname or lastname respectively) as second sort param
259 $sort2 = '';
260 if ($this->sortitemid == 'lastname') {
261 $sort2 = ', u.firstname ' . $this->sortorder;
262 } elseif ($this->sortitemid == 'firstname') {
263 $sort2 = ', u.lastname ' . $this->sortorder;
266 $this->users = get_role_users($this->gradebookroles, $this->context, false,
267 'u.id, u.firstname, u.lastname', 'u.'.$this->sortitemid .' '. $this->sortorder . $sort2,
268 false, $this->page * $this->get_pref('studentsperpage'), $this->get_pref('studentsperpage'),
269 $this->currentgroup);
270 // need to cut users down by groups
274 if (empty($this->users)) {
275 $this->userselect = '';
276 $this->users = array();
277 } else {
278 $this->userselect = 'AND g.userid in ('.implode(',', array_keys($this->users)).')';
281 return $this->users;
285 * Fetches and returns a count of all the users that will be shown on this page.
286 * @param bool $groups Whether to apply groupsql
287 * @return int Count of users
289 function get_numusers($groups=true) {
290 global $CFG;
292 $countsql = "SELECT COUNT(DISTINCT u.id)
293 FROM {$CFG->prefix}grade_grades g RIGHT OUTER JOIN
294 {$CFG->prefix}user u ON (u.id = g.userid AND g.itemid = $this->sortitemid)
295 LEFT JOIN {$CFG->prefix}role_assignments ra ON u.id = ra.userid ";
296 if ($groups) {
297 $countsql .= $this->groupsql;
299 $countsql .= " WHERE ra.roleid in ($this->gradebookroles) ";
300 if ($groups) {
301 $countsql .= $this->groupwheresql;
303 $countsql .= " AND ra.contextid ".get_related_contexts_string($this->context);
304 return count_records_sql($countsql);
308 * we supply the userids in this query, and get all the grades
309 * pulls out all the grades, this does not need to worry about paging
311 function load_final_grades() {
312 global $CFG;
314 // please note that we must fetch all grade_grades fields if we want to contruct grade_grade object from it!
315 $sql = "SELECT g.*, gt.feedback, gt.feedbackformat, gi.grademin, gi.grademax
316 FROM {$CFG->prefix}grade_items gi,
317 {$CFG->prefix}grade_grades g
318 LEFT JOIN {$CFG->prefix}grade_grades_text gt ON g.id = gt.gradeid
319 WHERE g.itemid = gi.id AND gi.courseid = $this->courseid $this->userselect";
321 if ($grades = get_records_sql($sql)) {
322 foreach ($grades as $grade) {
323 $this->finalgrades[$grade->userid][$grade->itemid] = $grade;
329 * Builds and returns a div with on/off toggles.
330 * @return string HTML code
332 function get_toggles_html() {
333 global $CFG, $USER;
335 $html = '<div id="grade-report-toggles">';
336 if ($USER->gradeediting[$this->courseid]) {
337 if (has_capability('moodle/grade:manage', $this->context) or has_capability('moodle/grade:hide', $this->context)) {
338 $html .= $this->print_toggle('eyecons', true);
340 if (has_capability('moodle/grade:manage', $this->context)
341 or has_capability('moodle/grade:lock', $this->context)
342 or has_capability('moodle/grade:unlock', $this->context)) {
343 $html .= $this->print_toggle('locks', true);
345 if (has_capability('moodle/grade:manage', $this->context)) {
346 $html .= $this->print_toggle('calculations', true);
350 $html .= $this->print_toggle('averages', true);
352 if (has_capability('moodle/grade:viewall', $this->context)
353 and has_capability('moodle/site:accessallgroups', $this->context)
354 and $course_has_groups = true) { // TODO replace that last condition with proper check
355 $html .= $this->print_toggle('groups', true);
358 $html .= $this->print_toggle('ranges', true);
359 if (!empty($CFG->enableoutcomes)) {
360 $html .= $this->print_toggle('nooutcomes', true);
362 $html .= '</div>';
363 return $html;
367 * Shortcut function for printing the grader report toggles.
368 * @param string $type The type of toggle
369 * @param bool $return Whether to return the HTML string rather than printing it
370 * @return void
372 function print_toggle($type, $return=false) {
373 global $CFG;
375 $icons = array('eyecons' => 't/hide.gif',
376 'calculations' => 't/calc.gif',
377 'locks' => 't/lock.gif',
378 'averages' => 't/sigma.gif',
379 'nooutcomes' => 't/outcomes.gif');
381 $pref_name = 'grade_report_show' . $type;
383 if (array_key_exists($pref_name, $CFG)) {
384 $show_pref = get_user_preferences($pref_name, $CFG->$pref_name);
385 } else {
386 $show_pref = get_user_preferences($pref_name);
389 $strshow = $this->get_lang_string('show' . $type, 'grades');
390 $strhide = $this->get_lang_string('hide' . $type, 'grades');
392 $show_hide = 'show';
393 $toggle_action = 1;
395 if ($show_pref) {
396 $show_hide = 'hide';
397 $toggle_action = 0;
400 if (array_key_exists($type, $icons)) {
401 $image_name = $icons[$type];
402 } else {
403 $image_name = "t/$type.gif";
406 $string = ${'str' . $show_hide};
408 $img = '<img src="'.$CFG->pixpath.'/'.$image_name.'" class="iconsmall" alt="'
409 .$string.'" title="'.$string.'" />'. "\n";
411 $retval = '<div class="gradertoggle">' . $img . '<a href="' . $this->baseurl . "&amp;toggle=$toggle_action&amp;toggle_type=$type\">"
412 . $string . '</a></div>';
414 if ($return) {
415 return $retval;
416 } else {
417 echo $retval;
422 * Builds and returns the HTML code for the headers.
423 * @return string $headerhtml
425 function get_headerhtml() {
426 global $CFG, $USER;
428 $strsortasc = $this->get_lang_string('sortasc', 'grades');
429 $strsortdesc = $this->get_lang_string('sortdesc', 'grades');
430 $strfirstname = $this->get_lang_string('firstname');
431 $strlastname = $this->get_lang_string('lastname');
433 if ($this->sortitemid === 'lastname') {
434 if ($this->sortorder == 'ASC') {
435 $lastarrow = print_arrow('up', $strsortasc, true);
436 } else {
437 $lastarrow = print_arrow('down', $strsortdesc, true);
439 } else {
440 $lastarrow = '';
443 if ($this->sortitemid === 'firstname') {
444 if ($this->sortorder == 'ASC') {
445 $firstarrow = print_arrow('up', $strsortasc, true);
446 } else {
447 $firstarrow = print_arrow('down', $strsortdesc, true);
449 } else {
450 $firstarrow = '';
452 // Prepare Table Headers
453 $headerhtml = '';
455 $numrows = count($this->gtree->levels);
457 $columns_to_unset = array();
460 foreach ($this->gtree->levels as $key=>$row) {
461 $columncount = 0;
462 if ($key == 0) {
463 // do not display course grade category
464 // continue;
467 $headerhtml .= '<tr class="heading r'.$this->rowcount++.'">';
469 if ($key == $numrows - 1) {
470 $headerhtml .= '<th class="header c'.$columncount++.' user" scope="col"><a href="'.$this->baseurl.'&amp;sortitemid=firstname">'
471 . $strfirstname . '</a> ' //TODO: localize
472 . $firstarrow. '/ <a href="'.$this->baseurl.'&amp;sortitemid=lastname">' . $strlastname . '</a>'. $lastarrow .'</th>';
473 } else {
474 $headerhtml .= '<td class="cell c'.$columncount++.' topleft">&nbsp;</td>';
477 foreach ($row as $columnkey => $element) {
478 $sort_link = '';
479 if (isset($element['object']->id)) {
480 $sort_link = $this->baseurl.'&amp;sortitemid=' . $element['object']->id;
483 $eid = $element['eid'];
484 $object = $element['object'];
485 $type = $element['type'];
486 $categorystate = @$element['categorystate'];
487 $itemmodule = null;
488 $iteminstance = null;
490 $columnclass = 'c' . $columncount++;
491 if (!empty($element['colspan'])) {
492 $colspan = 'colspan="'.$element['colspan'].'"';
493 $columnclass = '';
494 } else {
495 $colspan = '';
498 if (!empty($element['depth'])) {
499 $catlevel = ' catlevel'.$element['depth'];
500 } else {
501 $catlevel = '';
504 // Element is a filler
505 if ($type == 'filler' or $type == 'fillerfirst' or $type == 'fillerlast') {
506 $headerhtml .= '<th class="'.$columnclass.' '.$type.$catlevel.'" '.$colspan.' scope="col">&nbsp;</th>';
508 // Element is a category
509 else if ($type == 'category') {
510 $headerhtml .= '<th class="header '. $columnclass.' category'.$catlevel.'" '.$colspan.' scope="col">'
511 . $element['object']->get_name();
512 $headerhtml .= $this->get_collapsing_icon($element);
514 // Print icons
515 if ($USER->gradeediting[$this->courseid]) {
516 $headerhtml .= $this->get_icons($element);
519 $headerhtml .= '</th>';
521 // Element is a grade_item
522 else {
523 $itemmodule = $element['object']->itemmodule;
524 $iteminstance = $element['object']->iteminstance;
526 if ($element['object']->id == $this->sortitemid) {
527 if ($this->sortorder == 'ASC') {
528 $arrow = $this->get_sort_arrow('up', $sort_link);
529 } else {
530 $arrow = $this->get_sort_arrow('down', $sort_link);
532 } else {
533 $arrow = $this->get_sort_arrow('move', $sort_link);
536 $dimmed = '';
537 if ($element['object']->is_hidden()) {
538 $dimmed = ' dimmed_text ';
541 if ($object->itemtype == 'mod') {
542 $icon = '<img src="'.$CFG->modpixpath.'/'.$object->itemmodule.'/icon.gif" class="icon" alt="'
543 .$this->get_lang_string('modulename', $object->itemmodule).'"/>';
544 } else if ($object->itemtype == 'manual') {
545 //TODO: add manual grading icon
546 $icon = '<img src="'.$CFG->pixpath.'/t/edit.gif" class="icon" alt="'
547 .$this->get_lang_string('manualgrade', 'grades') .'"/>';
550 $headerlink = $this->get_module_link($element['object']->get_name(), $itemmodule, $iteminstance);
551 $headerhtml .= '<th class="header '.$columnclass.' '.$type.$catlevel.$dimmed.'" scope="col">'. $headerlink . $arrow;
552 $headerhtml .= $this->get_icons($element) . '</th>';
554 $this->items[$element['object']->sortorder] =& $element['object'];
559 $headerhtml .= '</tr>';
561 return $headerhtml;
565 * Builds and return the HTML rows of the table (grades headed by student).
566 * @return string HTML
568 function get_studentshtml() {
569 global $CFG, $USER;
570 $studentshtml = '';
571 $strfeedback = $this->get_lang_string("feedback");
572 $strgrade = $this->get_lang_string('grade');
573 $gradetabindex = 1;
574 $showuserimage = $this->get_pref('showuserimage');
575 $numusers = count($this->users);
577 // Preload scale objects for items with a scaleid
578 $scales_list = '';
579 $tabindices = array();
580 foreach ($this->items as $item) {
581 if (!empty($item->scaleid)) {
582 $scales_list .= "$item->scaleid,";
584 $tabindices[$item->id]['grade'] = $gradetabindex;
585 $tabindices[$item->id]['feedback'] = $gradetabindex + $numusers;
586 $gradetabindex += $numusers * 2;
588 $scales_array = array();
590 if (!empty($scales_list)) {
591 $scales_list = substr($scales_list, 0, -1);
592 $scales_array = get_records_list('scale', 'id', $scales_list);
595 foreach ($this->users as $userid => $user) {
596 $columncount = 0;
597 // Student name and link
598 $user_pic = null;
599 if ($showuserimage) {
600 $user_pic = '<div class="userpic">' . print_user_picture($user->id, $this->courseid, true, 0, true) . '</div>';
603 $studentshtml .= '<tr class="r'.$this->rowcount++.'"><th class="header c'.$columncount++.' user" scope="row">' . $user_pic
604 . '<a href="' . $CFG->wwwroot . '/user/view.php?id='
605 . $user->id . '">' . fullname($user) . '</a></th>';
607 foreach ($this->items as $itemid=>$item) {
608 // Get the decimal points preference for this item
609 $decimalpoints = $this->get_pref('decimalpoints', $item->id);
611 if (isset($this->finalgrades[$userid][$item->id])) {
612 $gradeval = $this->finalgrades[$userid][$item->id]->finalgrade;
613 $grade = new grade_grade($this->finalgrades[$userid][$item->id], false);
614 $grade->feedback = stripslashes_safe($this->finalgrades[$userid][$item->id]->feedback);
615 $grade->feedbackformat = $this->finalgrades[$userid][$item->id]->feedbackformat;
617 } else {
618 $gradeval = null;
619 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$item->id), false);
620 $grade->feedback = '';
623 $grade->courseid = $this->courseid;
624 $grade->grade_item =& $this->items[$itemid]; // this speedsup is_hidden() and other grade_grade methods
626 // emulate grade element
627 $eid = $this->gtree->get_grade_eid($grade);
628 $element = array('eid'=>$eid, 'object'=>$grade, 'type'=>'grade');
630 if ($grade->is_overridden()) {
631 $studentshtml .= '<td class="overridden cell c'.$columncount++.'">';
632 } else {
633 $studentshtml .= '<td class="cell c'.$columncount++.'">';
636 if ($grade->is_excluded()) {
637 $studentshtml .= get_string('excluded', 'grades'); // TODO: improve visual representation of excluded grades
640 // Do not show any icons if no grade (no record in DB to match)
641 if (!$item->needsupdate and $USER->gradeediting[$this->courseid]) {
642 $studentshtml .= $this->get_icons($element);
645 // if in editting mode, we need to print either a text box
646 // or a drop down (for scales)
647 // grades in item of type grade category or course are not directly editable
648 if ($item->needsupdate) {
649 $studentshtml .= '<span class="gradingerror">'.get_string('error').'</span>';
651 } else if ($USER->gradeediting[$this->courseid]) {
652 // We need to retrieve each grade_grade object from DB in order to
653 // know if they are hidden/locked
655 if ($item->scaleid && !empty($scales_array[$item->scaleid])) {
656 $scale = $scales_array[$item->scaleid];
658 $scales = explode(",", $scale->scale);
659 // reindex because scale is off 1
660 $i = 0;
661 foreach ($scales as $scaleoption) {
662 $i++;
663 $scaleopt[$i] = $scaleoption;
666 if ($this->get_pref('quickgrading') and $grade->is_editable()) {
667 $oldval = empty($gradeval) ? -1 : $gradeval;
668 if (empty($item->outcomeid)) {
669 $nogradestr = $this->get_lang_string('nograde');
670 } else {
671 $nogradestr = $this->get_lang_string('nooutcome', 'grades');
673 $studentshtml .= '<input type="hidden" name="oldgrade_'.$userid.'_'
674 .$item->id.'" value="'.$oldval.'"/>';
675 $studentshtml .= choose_from_menu($scaleopt, 'grade_'.$userid.'_'.$item->id,
676 $gradeval, $nogradestr, '', '-1',
677 true, false, $tabindices[$item->id]['grade']);
678 } elseif(!empty($scale)) {
679 $scales = explode(",", $scale->scale);
681 // invalid grade if gradeval < 1
682 if ((int) $gradeval < 1) {
683 $studentshtml .= '-';
684 } else {
685 $studentshtml .= $scales[$gradeval-1];
687 } else {
688 // no such scale, throw error?
691 } else if ($item->gradetype != GRADE_TYPE_TEXT) { // Value type
692 if ($this->get_pref('quickgrading') and $grade->is_editable()) {
693 $value = format_float($gradeval, $decimalpoints);
694 $studentshtml .= '<input type="hidden" name="oldgrade_'.$userid.'_'.$item->id.'" value="'.$value.'" />';
695 $studentshtml .= '<input size="6" tabindex="' . $tabindices[$item->id]['grade']
696 . '" type="text" title="'. $strgrade .'" name="grade_'
697 .$userid.'_' .$item->id.'" value="'.$value.'" />';
698 } else {
699 $studentshtml .= format_float($gradeval, $decimalpoints);
704 // If quickfeedback is on, print an input element
705 if ($this->get_pref('quickfeedback') and $grade->is_editable()) {
706 if ($this->get_pref('quickgrading')) {
707 $studentshtml .= '<br />';
709 $studentshtml .= '<input type="hidden" name="oldfeedback_'
710 .$userid.'_'.$item->id.'" value="' . s($grade->feedback) . '" />';
711 $studentshtml .= '<input class="quickfeedback" tabindex="' . $tabindices[$item->id]['feedback']
712 . '" size="6" title="' . $strfeedback . '" type="text" name="feedback_'
713 .$userid.'_'.$item->id.'" value="' . s($grade->feedback) . '" />';
716 } else {
717 // Percentage format if specified by user (check each item for a set preference)
718 $gradedisplaytype = $this->get_pref('gradedisplaytype', $item->id);
720 $percentsign = '';
721 $grademin = $item->grademin;
722 $grademax = $item->grademax;
724 if ($gradedisplaytype == GRADE_REPORT_GRADE_DISPLAY_TYPE_PERCENTAGE) {
725 if (!is_null($gradeval)) {
726 $gradeval = grade_to_percentage($gradeval, $grademin, $grademax);
728 $percentsign = '%';
731 // If feedback present, surround grade with feedback tooltip
732 if (!empty($grade->feedback)) {
733 if ($grade->feedbackformat == 1) {
734 $overlib = "return overlib('" . s(ltrim($grade->feedback)) . "', FULLHTML);";
735 } else {
736 $overlib = "return overlib('" . ($grade->feedback) . "', CAPTION, '$strfeedback');";
739 $studentshtml .= '<span onmouseover="' . $overlib . '" onmouseout="return nd();">';
742 if ($item->needsupdate) {
743 $studentshtml .= '<span class="gradingerror">'.get_string('error').'</span>';
745 } else if ($gradedisplaytype == GRADE_REPORT_GRADE_DISPLAY_TYPE_LETTER) {
746 $letters = grade_report::get_grade_letters();
747 if (!is_null($gradeval)) {
748 $studentshtml .= grade_grade::get_letter($letters, $gradeval, $grademin, $grademax);
750 } else if ($item->scaleid && !empty($scales_array[$item->scaleid])
751 && $gradedisplaytype == GRADE_REPORT_GRADE_DISPLAY_TYPE_REAL) {
752 $scale = $scales_array[$item->scaleid];
753 $scales = explode(",", $scale->scale);
755 // invalid grade if gradeval < 1
756 if ((int) $gradeval < 1) {
757 $studentshtml .= '-';
758 } else {
759 $studentshtml .= $scales[$gradeval-1];
761 } else {
762 if (is_null($gradeval)) {
763 $studentshtml .= '-';
764 } else {
765 $studentshtml .= format_float($gradeval, $decimalpoints). $percentsign;
768 if (!empty($grade->feedback)) {
769 $studentshtml .= '</span>';
773 if (!empty($this->gradeserror[$item->id][$userid])) {
774 $studentshtml .= $this->gradeserror[$item->id][$userid];
777 $studentshtml .= '</td>' . "\n";
779 $studentshtml .= '</tr>';
781 return $studentshtml;
785 * Builds and return the HTML row of column totals.
786 * @param bool $grouponly Whether to return only group averages or all averages.
787 * @return string HTML
789 function get_avghtml($grouponly=false) {
790 global $CFG, $USER;
792 $averagesdisplaytype = $this->get_pref('averagesdisplaytype');
793 $averagesdecimalpoints = $this->get_pref('averagesdecimalpoints');
794 $meanselection = $this->get_pref('meanselection');
795 $shownumberofgrades = $this->get_pref('shownumberofgrades');
797 $avghtml = '';
798 $avgcssclass = 'avg';
800 if ($grouponly) {
801 $straverage = get_string('groupavg', 'grades');
802 $showaverages = $this->currentgroup && $this->get_pref('showgroups');
803 $groupsql = $this->groupsql;
804 $groupwheresql = $this->groupwheresql;
805 $avgcssclass = 'groupavg';
806 } else {
807 $straverage = get_string('overallaverage', 'grades');
808 $showaverages = $this->get_pref('showaverages');
809 $groupsql = null;
810 $groupwheresql = null;
813 $totalcount = $this->get_numusers($grouponly);
815 if ($showaverages) {
817 // the first join on user is needed for groupsql
818 $SQL = "SELECT g.itemid, SUM(g.finalgrade) as sum
819 FROM {$CFG->prefix}grade_items gi LEFT JOIN
820 {$CFG->prefix}grade_grades g ON gi.id = g.itemid LEFT JOIN
821 {$CFG->prefix}user u ON g.userid = u.id
822 $groupsql
823 WHERE gi.courseid = $this->courseid
824 $groupwheresql
825 AND g.userid IN (
826 SELECT DISTINCT(u.id)
827 FROM {$CFG->prefix}user u LEFT JOIN
828 {$CFG->prefix}role_assignments ra ON u.id = ra.userid
829 WHERE ra.roleid in ($this->gradebookroles)
830 AND ra.contextid ".get_related_contexts_string($this->context)."
832 GROUP BY g.itemid";
833 $sum_array = array();
834 if ($sums = get_records_sql($SQL)) {
835 foreach ($sums as $itemid => $csum) {
836 $sum_array[$itemid] = $csum->sum;
840 $avghtml = '<tr class="' . $avgcssclass . ' r'.$this->rowcount++.'"><th class="header c0" scope="row">'.$straverage.'</th>';
842 $columncount=1;
843 foreach ($this->items as $item) {
844 if (empty($sum_array[$item->id])) {
845 $sum_array[$item->id] = 0;
847 if ($grouponly) {
848 $groupsql = $this->groupsql;
849 $groupwheresql = $this->groupwheresql;
850 } else {
851 $groupsql = '';
852 $groupwheresql = '';
854 // MDL-10875 Empty grades must be evaluated as grademin, NOT always 0
855 // This query returns a count of ungraded grades (NULL finalgrade OR no matching record in grade_grades table)
856 $SQL = "SELECT COUNT(*) AS count FROM {$CFG->prefix}user u
857 WHERE u.id NOT IN
858 (SELECT userid FROM {$CFG->prefix}grade_grades
859 WHERE itemid = $item->id
860 AND finalgrade IS NOT NULL
862 AND u.id IN (
863 SELECT DISTINCT(u.id)
864 FROM {$CFG->prefix}user u LEFT JOIN
865 {$CFG->prefix}role_assignments ra ON u.id = ra.userid
866 $groupsql
867 WHERE ra.roleid in ($this->gradebookroles)
868 AND ra.contextid ".get_related_contexts_string($this->context)."
869 $groupwheresql
872 $ungraded_count = get_field_sql($SQL);
874 if ($meanselection == GRADE_REPORT_MEAN_GRADED) {
875 $mean_count = $totalcount - $ungraded_count;
876 } else { // Bump up the sum by the number of ungraded items * grademin
877 if (isset($sum_array[$item->id])) {
878 $sum_array[$item->id] += $ungraded_count * $item->grademin;
880 $mean_count = $totalcount;
883 $decimalpoints = $this->get_pref('decimalpoints', $item->id);
884 // Determine which display type to use for this average
885 $gradedisplaytype = $this->get_pref('gradedisplaytype', $item->id);
886 if ($USER->gradeediting[$this->courseid]) {
887 $displaytype = GRADE_REPORT_GRADE_DISPLAY_TYPE_REAL;
888 } elseif ($averagesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // Inherit specific column or general preference
889 $displaytype = $gradedisplaytype;
890 } else { // General preference overrides specific column display type
891 $displaytype = $averagesdisplaytype;
894 if ($averagesdecimalpoints != GRADE_REPORT_PREFERENCE_INHERIT) {
895 $decimalpoints = $averagesdecimalpoints;
898 if (!isset($sum_array[$item->id]) || $mean_count == 0) {
899 $avghtml .= '<td class="cell c' . $columncount++.'">-</td>';
900 } else {
901 $sum = $sum_array[$item->id];
903 if ($item->scaleid) {
904 if ($grouponly) {
905 $finalsum = $sum_array[$item->id];
906 $finalavg = $finalsum/$mean_count;
907 } else {
908 $finalavg = $sum/$mean_count;
910 $scaleval = round($finalavg);
911 $scale_object = new grade_scale(array('id' => $item->scaleid), false);
912 $gradehtml = $scale_object->get_nearest_item($scaleval);
913 $rawvalue = $scaleval;
914 } else {
915 $gradeval = format_float($sum/$mean_count, $decimalpoints);
916 $gradehtml = $gradeval;
917 $rawvalue = $gradeval;
920 if ($displaytype == GRADE_REPORT_GRADE_DISPLAY_TYPE_PERCENTAGE) {
921 $gradeval = grade_to_percentage($rawvalue, $item->grademin, $item->grademax);
922 $gradehtml = number_format(format_float($gradeval, $decimalpoints), $decimalpoints) . '%';
923 } elseif ($displaytype == GRADE_REPORT_GRADE_DISPLAY_TYPE_LETTER) {
924 $letters = grade_report::get_grade_letters();
925 $gradehtml = grade_grade::get_letter($letters, $gradeval, $item->grademin, $item->grademax);
928 $numberofgrades = '';
930 if ($shownumberofgrades) {
931 $numberofgrades = " ($mean_count)";
934 $avghtml .= '<td class="cell c' . $columncount++.'">'.$gradehtml.$numberofgrades.'</td>';
937 $avghtml .= '</tr>';
939 return $avghtml;
943 * Builds and return the HTML row of ranges for each column (i.e. range).
944 * @return string HTML
946 function get_rangehtml() {
947 global $USER;
949 $scalehtml = '';
950 if ($this->get_pref('showranges')) {
951 $rangesdisplaytype = $this->get_pref('rangesdisplaytype');
952 $rangesdecimalpoints = $this->get_pref('rangesdecimalpoints');
953 $scalehtml = '<tr class="r'.$this->rowcount++.'">'
954 . '<th class="header c0 range" scope="row">'.$this->get_lang_string('range','grades').'</th>';
956 $columncount = 1;
957 foreach ($this->items as $item) {
959 $decimalpoints = $this->get_pref('decimalpoints', $item->id);
960 // Determine which display type to use for this range
961 $gradedisplaytype = $this->get_pref('gradedisplaytype', $item->id);
963 if ($USER->gradeediting[$this->courseid]) {
964 $displaytype = GRADE_REPORT_GRADE_DISPLAY_TYPE_REAL;
965 } elseif ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // Inherit specific column or general preference
966 $displaytype = $gradedisplaytype;
967 } else { // General preference overrides specific column display type
968 $displaytype = $rangesdisplaytype;
971 if ($rangesdecimalpoints != GRADE_REPORT_PREFERENCE_INHERIT) {
972 $decimalpoints = $rangesdecimalpoints;
975 if ($displaytype == GRADE_REPORT_GRADE_DISPLAY_TYPE_REAL) {
976 $grademin = format_float($item->grademin, $decimalpoints);
977 $grademax = format_float($item->grademax, $decimalpoints);
978 } elseif ($displaytype == GRADE_REPORT_GRADE_DISPLAY_TYPE_PERCENTAGE) {
979 $grademin = 0;
980 $grademax = 100;
981 } elseif ($displaytype == GRADE_REPORT_GRADE_DISPLAY_TYPE_LETTER) {
982 $letters = grade_report::get_grade_letters();
983 $grademin = end($letters);
984 $grademax = reset($letters);
987 $scalehtml .= '<th class="header c'.$columncount++.' range">'. $grademin.'-'. $grademax.'</th>';
989 $scalehtml .= '</tr>';
991 return $scalehtml;
995 * Given a grade_category, grade_item or grade_grade, this function
996 * figures out the state of the object and builds then returns a div
997 * with the icons needed for the grader report.
999 * @param object $object
1000 * @return string HTML
1002 function get_icons($element) {
1003 global $CFG, $USER;
1005 if (!$USER->gradeediting[$this->courseid]) {
1006 return '<div class="grade_icons" />';
1009 // Init all icons
1010 $edit_icon = $this->gtree->get_edit_icon($element, $this->gpr);
1011 $edit_calculation_icon = '';
1012 $show_hide_icon = '';
1013 $lock_unlock_icon = '';
1015 if ($this->get_pref('showcalculations')) {
1016 $edit_calculation_icon = $this->gtree->get_calculation_icon($element, $this->gpr);
1019 if ($this->get_pref('showeyecons')) {
1020 $show_hide_icon = $this->gtree->get_hiding_icon($element, $this->gpr);
1023 if ($this->get_pref('showlocks')) {
1024 $lock_unlock_icon = $this->gtree->get_locking_icon($element, $this->gpr);
1027 return '<div class="grade_icons">'.$edit_icon.$edit_calculation_icon.$show_hide_icon.$lock_unlock_icon.'</div>';
1031 * Given a category element returns collapsing +/- icon if available
1032 * @param object $object
1033 * @return string HTML
1035 function get_collapsing_icon($element) {
1036 global $CFG;
1038 $contract_expand_icon = '';
1039 // If object is a category, display expand/contract icon
1040 if ($element['type'] == 'category') {
1041 // Load language strings
1042 $strswitch_minus = $this->get_lang_string('aggregatesonly', 'grades');
1043 $strswitch_plus = $this->get_lang_string('gradesonly', 'grades');
1044 $strswitch_whole = $this->get_lang_string('fullmode', 'grades');
1046 $expand_contract = 'switch_minus'; // Default: expanded
1047 // $this->get_pref('aggregationview', $element['object']->id) == GRADE_REPORT_AGGREGATION_VIEW_COMPACT
1049 if (in_array($element['object']->id, $this->collapsed['aggregatesonly'])) {
1050 $expand_contract = 'switch_plus';
1051 } elseif (in_array($element['object']->id, $this->collapsed['gradesonly'])) {
1052 $expand_contract = 'switch_whole';
1054 $url = $this->gpr->get_return_url(null, array('target'=>$element['eid'], 'action'=>$expand_contract, 'sesskey'=>sesskey()));
1055 $contract_expand_icon = '<a href="'.$url.'"><img src="'.$CFG->pixpath.'/t/'.$expand_contract.'.gif" class="iconsmall" alt="'
1056 .${'str'.$expand_contract}.'" title="'.${'str'.$expand_contract}.'" /></a>';
1058 return $contract_expand_icon;
1062 * Processes a single action against a category, grade_item or grade.
1063 * @param string $target eid ({type}{id}, e.g. c4 for category4)
1064 * @param string $action Which action to take (edit, delete etc...)
1065 * @return
1067 function process_action($target, $action) {
1068 // TODO: this code should be in some grade_tree static method
1069 $targettype = substr($target, 0, 1);
1070 $targetid = substr($target, 1);
1071 // TODO: end
1073 if ($collapsed = get_user_preferences('grade_report_grader_collapsed_categories')) {
1074 $collapsed = unserialize($collapsed);
1075 } else {
1076 $collapsed = array('aggregatesonly' => array(), 'gradesonly' => array());
1079 switch ($action) {
1080 case 'switch_minus': // Add category to array of aggregatesonly
1081 if (!in_array($targetid, $collapsed['aggregatesonly'])) {
1082 $collapsed['aggregatesonly'][] = $targetid;
1083 set_user_preference('grade_report_grader_collapsed_categories', serialize($collapsed));
1085 break;
1087 case 'switch_plus': // Remove category from array of aggregatesonly, and add it to array of gradesonly
1088 $key = array_search($targetid, $collapsed['aggregatesonly']);
1089 if ($key !== false) {
1090 unset($collapsed['aggregatesonly'][$key]);
1092 if (!in_array($targetid, $collapsed['gradesonly'])) {
1093 $collapsed['gradesonly'][] = $targetid;
1095 set_user_preference('grade_report_grader_collapsed_categories', serialize($collapsed));
1096 break;
1097 case 'switch_whole': // Remove the category from the array of collapsed cats
1098 $key = array_search($targetid, $collapsed['gradesonly']);
1099 if ($key !== false) {
1100 unset($collapsed['gradesonly'][$key]);
1101 set_user_preference('grade_report_grader_collapsed_categories', serialize($collapsed));
1104 break;
1105 default:
1106 break;
1109 return true;