MDL-11456 the .grade-report-grader table {} style was cascading down to the popups...
[moodle-pu.git] / grade / report / grader / lib.php
blob078b2116085a826269b4035c71119879dbbe0831
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:edit', $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.*, gi.grademin, gi.grademax
316 FROM {$CFG->prefix}grade_items gi,
317 {$CFG->prefix}grade_grades g
318 WHERE g.itemid = gi.id AND gi.courseid = $this->courseid $this->userselect";
320 if ($grades = get_records_sql($sql)) {
321 foreach ($grades as $grade) {
322 $this->finalgrades[$grade->userid][$grade->itemid] = $grade;
328 * Builds and returns a div with on/off toggles.
329 * @return string HTML code
331 function get_toggles_html() {
332 global $CFG, $USER;
334 $html = '<div id="grade-report-toggles">';
335 if ($USER->gradeediting[$this->courseid]) {
336 if (has_capability('moodle/grade:manage', $this->context) or has_capability('moodle/grade:hide', $this->context)) {
337 $html .= $this->print_toggle('eyecons', true);
339 if (has_capability('moodle/grade:manage', $this->context)
340 or has_capability('moodle/grade:lock', $this->context)
341 or has_capability('moodle/grade:unlock', $this->context)) {
342 $html .= $this->print_toggle('locks', true);
344 if (has_capability('moodle/grade:manage', $this->context)) {
345 $html .= $this->print_toggle('calculations', true);
349 $html .= $this->print_toggle('averages', true);
351 if (has_capability('moodle/grade:viewall', $this->context)
352 and has_capability('moodle/site:accessallgroups', $this->context)
353 and $course_has_groups = true) { // TODO replace that last condition with proper check
354 $html .= $this->print_toggle('groups', true);
357 $html .= $this->print_toggle('ranges', true);
358 if (!empty($CFG->enableoutcomes)) {
359 $html .= $this->print_toggle('nooutcomes', true);
361 $html .= '</div>';
362 return $html;
366 * Shortcut function for printing the grader report toggles.
367 * @param string $type The type of toggle
368 * @param bool $return Whether to return the HTML string rather than printing it
369 * @return void
371 function print_toggle($type, $return=false) {
372 global $CFG;
374 $icons = array('eyecons' => 't/hide.gif',
375 'calculations' => 't/calc.gif',
376 'locks' => 't/lock.gif',
377 'averages' => 't/sigma.gif',
378 'nooutcomes' => 't/outcomes.gif');
380 $pref_name = 'grade_report_show' . $type;
382 if (array_key_exists($pref_name, $CFG)) {
383 $show_pref = get_user_preferences($pref_name, $CFG->$pref_name);
384 } else {
385 $show_pref = get_user_preferences($pref_name);
388 $strshow = $this->get_lang_string('show' . $type, 'grades');
389 $strhide = $this->get_lang_string('hide' . $type, 'grades');
391 $show_hide = 'show';
392 $toggle_action = 1;
394 if ($show_pref) {
395 $show_hide = 'hide';
396 $toggle_action = 0;
399 if (array_key_exists($type, $icons)) {
400 $image_name = $icons[$type];
401 } else {
402 $image_name = "t/$type.gif";
405 $string = ${'str' . $show_hide};
407 $img = '<img src="'.$CFG->pixpath.'/'.$image_name.'" class="iconsmall" alt="'
408 .$string.'" title="'.$string.'" />'. "\n";
410 $retval = '<div class="gradertoggle">' . $img . '<a href="' . $this->baseurl . "&amp;toggle=$toggle_action&amp;toggle_type=$type\">"
411 . $string . '</a></div>';
413 if ($return) {
414 return $retval;
415 } else {
416 echo $retval;
421 * Builds and returns the HTML code for the headers.
422 * @return string $headerhtml
424 function get_headerhtml() {
425 global $CFG, $USER;
427 $strsortasc = $this->get_lang_string('sortasc', 'grades');
428 $strsortdesc = $this->get_lang_string('sortdesc', 'grades');
429 $strfirstname = $this->get_lang_string('firstname');
430 $strlastname = $this->get_lang_string('lastname');
432 if ($this->sortitemid === 'lastname') {
433 if ($this->sortorder == 'ASC') {
434 $lastarrow = print_arrow('up', $strsortasc, true);
435 } else {
436 $lastarrow = print_arrow('down', $strsortdesc, true);
438 } else {
439 $lastarrow = '';
442 if ($this->sortitemid === 'firstname') {
443 if ($this->sortorder == 'ASC') {
444 $firstarrow = print_arrow('up', $strsortasc, true);
445 } else {
446 $firstarrow = print_arrow('down', $strsortdesc, true);
448 } else {
449 $firstarrow = '';
451 // Prepare Table Headers
452 $headerhtml = '';
454 $numrows = count($this->gtree->levels);
456 $columns_to_unset = array();
459 foreach ($this->gtree->levels as $key=>$row) {
460 $columncount = 0;
461 if ($key == 0) {
462 // do not display course grade category
463 // continue;
466 $headerhtml .= '<tr class="heading r'.$this->rowcount++.'">';
468 if ($key == $numrows - 1) {
469 $headerhtml .= '<th class="header c'.$columncount++.' user" scope="col"><a href="'.$this->baseurl.'&amp;sortitemid=firstname">'
470 . $strfirstname . '</a> ' //TODO: localize
471 . $firstarrow. '/ <a href="'.$this->baseurl.'&amp;sortitemid=lastname">' . $strlastname . '</a>'. $lastarrow .'</th>';
472 } else {
473 $headerhtml .= '<td class="cell c'.$columncount++.' topleft">&nbsp;</td>';
476 foreach ($row as $columnkey => $element) {
477 $sort_link = '';
478 if (isset($element['object']->id)) {
479 $sort_link = $this->baseurl.'&amp;sortitemid=' . $element['object']->id;
482 $eid = $element['eid'];
483 $object = $element['object'];
484 $type = $element['type'];
485 $categorystate = @$element['categorystate'];
486 $itemmodule = null;
487 $iteminstance = null;
489 $columnclass = 'c' . $columncount++;
490 if (!empty($element['colspan'])) {
491 $colspan = 'colspan="'.$element['colspan'].'"';
492 $columnclass = '';
493 } else {
494 $colspan = '';
497 if (!empty($element['depth'])) {
498 $catlevel = ' catlevel'.$element['depth'];
499 } else {
500 $catlevel = '';
503 // Element is a filler
504 if ($type == 'filler' or $type == 'fillerfirst' or $type == 'fillerlast') {
505 $headerhtml .= '<th class="'.$columnclass.' '.$type.$catlevel.'" '.$colspan.' scope="col">&nbsp;</th>';
507 // Element is a category
508 else if ($type == 'category') {
509 $headerhtml .= '<th class="header '. $columnclass.' category'.$catlevel.'" '.$colspan.' scope="col">'
510 . $element['object']->get_name();
511 $headerhtml .= $this->get_collapsing_icon($element);
513 // Print icons
514 if ($USER->gradeediting[$this->courseid]) {
515 $headerhtml .= $this->get_icons($element);
518 $headerhtml .= '</th>';
520 // Element is a grade_item
521 else {
522 $itemmodule = $element['object']->itemmodule;
523 $iteminstance = $element['object']->iteminstance;
525 if ($element['object']->id == $this->sortitemid) {
526 if ($this->sortorder == 'ASC') {
527 $arrow = $this->get_sort_arrow('up', $sort_link);
528 } else {
529 $arrow = $this->get_sort_arrow('down', $sort_link);
531 } else {
532 $arrow = $this->get_sort_arrow('move', $sort_link);
535 $dimmed = '';
536 if ($element['object']->is_hidden()) {
537 $dimmed = ' dimmed_text ';
540 if ($object->itemtype == 'mod') {
541 $icon = '<img src="'.$CFG->modpixpath.'/'.$object->itemmodule.'/icon.gif" class="icon" alt="'
542 .$this->get_lang_string('modulename', $object->itemmodule).'"/>';
543 } else if ($object->itemtype == 'manual') {
544 //TODO: add manual grading icon
545 $icon = '<img src="'.$CFG->pixpath.'/t/edit.gif" class="icon" alt="'
546 .$this->get_lang_string('manualgrade', 'grades') .'"/>';
549 $headerlink = $this->get_module_link($element['object']->get_name(), $itemmodule, $iteminstance);
550 $headerhtml .= '<th class="header '.$columnclass.' '.$type.$catlevel.$dimmed.'" scope="col">'. $headerlink . $arrow;
551 $headerhtml .= $this->get_icons($element) . '</th>';
553 $this->items[$element['object']->sortorder] =& $element['object'];
558 $headerhtml .= '</tr>';
560 return $headerhtml;
564 * Builds and return the HTML rows of the table (grades headed by student).
565 * @return string HTML
567 function get_studentshtml() {
568 global $CFG, $USER;
569 $studentshtml = '';
570 $strfeedback = $this->get_lang_string("feedback");
571 $strgrade = $this->get_lang_string('grade');
572 $gradetabindex = 1;
573 $showuserimage = $this->get_pref('showuserimage');
574 $numusers = count($this->users);
576 // Preload scale objects for items with a scaleid
577 $scales_list = '';
578 $tabindices = array();
579 foreach ($this->items as $item) {
580 if (!empty($item->scaleid)) {
581 $scales_list .= "$item->scaleid,";
583 $tabindices[$item->id]['grade'] = $gradetabindex;
584 $tabindices[$item->id]['feedback'] = $gradetabindex + $numusers;
585 $gradetabindex += $numusers * 2;
587 $scales_array = array();
589 if (!empty($scales_list)) {
590 $scales_list = substr($scales_list, 0, -1);
591 $scales_array = get_records_list('scale', 'id', $scales_list);
594 $canviewhidden = has_capability('moodle/grade:viewhidden', get_context_instance(CONTEXT_COURSE, $this->course->id));
596 foreach ($this->users as $userid => $user) {
597 $columncount = 0;
598 // Student name and link
599 $user_pic = null;
600 if ($showuserimage) {
601 $user_pic = '<div class="userpic">' . print_user_picture($user->id, $this->courseid, true, 0, true) . '</div>';
604 $studentshtml .= '<tr class="r'.$this->rowcount++.'"><th class="header c'.$columncount++.' user" scope="row">' . $user_pic
605 . '<a href="' . $CFG->wwwroot . '/user/view.php?id='
606 . $user->id . '">' . fullname($user) . '</a></th>';
608 foreach ($this->items as $itemid=>$item) {
609 // Get the decimal points preference for this item
610 $decimalpoints = $item->get_decimals();
612 if (isset($this->finalgrades[$userid][$item->id])) {
613 $gradeval = $this->finalgrades[$userid][$item->id]->finalgrade;
614 $grade = new grade_grade($this->finalgrades[$userid][$item->id], false);
615 $grade->feedback = stripslashes_safe($this->finalgrades[$userid][$item->id]->feedback);
616 $grade->feedbackformat = $this->finalgrades[$userid][$item->id]->feedbackformat;
618 } else {
619 $gradeval = null;
620 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$item->id), false);
621 $grade->feedback = '';
624 // MDL-11274
625 // Hide grades in the grader report if the current grader doesn't have 'moodle/grade:viewhidden'
626 if ($grade->is_hidden() && !$canviewhidden) {
627 if (isset($grade->finalgrade)) {
628 $studentshtml .= '<td class="cell c'.$columncount++.'">'.userdate($grade->timecreated,get_string('strftimedatetimeshort')).'</td>'; } else {
629 $studentshtml .= '<td class="cell c'.$columncount++.'">-</td>';
631 continue;
634 $grade->courseid = $this->courseid;
635 $grade->grade_item =& $this->items[$itemid]; // this speedsup is_hidden() and other grade_grade methods
637 // emulate grade element
638 $eid = $this->gtree->get_grade_eid($grade);
639 $element = array('eid'=>$eid, 'object'=>$grade, 'type'=>'grade');
641 if ($grade->is_overridden()) {
642 $studentshtml .= '<td class="overridden cell c'.$columncount++.'">';
643 } else {
644 $studentshtml .= '<td class="cell c'.$columncount++.'">';
647 if ($grade->is_excluded()) {
648 $studentshtml .= get_string('excluded', 'grades'); // TODO: improve visual representation of excluded grades
651 // Do not show any icons if no grade (no record in DB to match)
652 if (!$item->needsupdate and $USER->gradeediting[$this->courseid]) {
653 $studentshtml .= $this->get_icons($element);
656 // if in editting mode, we need to print either a text box
657 // or a drop down (for scales)
658 // grades in item of type grade category or course are not directly editable
659 if ($item->needsupdate) {
660 $studentshtml .= '<span class="gradingerror">'.get_string('error').'</span>';
662 } else if ($USER->gradeediting[$this->courseid]) {
663 // We need to retrieve each grade_grade object from DB in order to
664 // know if they are hidden/locked
666 if ($item->scaleid && !empty($scales_array[$item->scaleid])) {
667 $scale = $scales_array[$item->scaleid];
669 $scales = explode(",", $scale->scale);
670 // reindex because scale is off 1
671 $i = 0;
672 foreach ($scales as $scaleoption) {
673 $i++;
674 $scaleopt[$i] = $scaleoption;
677 if ($this->get_pref('quickgrading') and $grade->is_editable()) {
678 $oldval = empty($gradeval) ? -1 : $gradeval;
679 if (empty($item->outcomeid)) {
680 $nogradestr = $this->get_lang_string('nograde');
681 } else {
682 $nogradestr = $this->get_lang_string('nooutcome', 'grades');
684 $studentshtml .= '<input type="hidden" name="oldgrade_'.$userid.'_'
685 .$item->id.'" value="'.$oldval.'"/>';
686 $studentshtml .= choose_from_menu($scaleopt, 'grade_'.$userid.'_'.$item->id,
687 $gradeval, $nogradestr, '', '-1',
688 true, false, $tabindices[$item->id]['grade']);
689 } elseif(!empty($scale)) {
690 $scales = explode(",", $scale->scale);
692 // invalid grade if gradeval < 1
693 if ((int) $gradeval < 1) {
694 $studentshtml .= '-';
695 } else {
696 $gradeval = (int)bounded_number($grade->grade_item->grademin, $gradeval, $grade->grade_item->grademax); //just in case somebody changes scale
697 $studentshtml .= $scales[$gradeval-1];
699 } else {
700 // no such scale, throw error?
703 } else if ($item->gradetype != GRADE_TYPE_TEXT) { // Value type
704 if ($this->get_pref('quickgrading') and $grade->is_editable()) {
705 $value = format_float($gradeval, $decimalpoints);
706 $studentshtml .= '<input type="hidden" name="oldgrade_'.$userid.'_'.$item->id.'" value="'.$value.'" />';
707 $studentshtml .= '<input size="6" tabindex="' . $tabindices[$item->id]['grade']
708 . '" type="text" title="'. $strgrade .'" name="grade_'
709 .$userid.'_' .$item->id.'" value="'.$value.'" />';
710 } else {
711 $studentshtml .= format_float($gradeval, $decimalpoints);
716 // If quickfeedback is on, print an input element
717 if ($this->get_pref('quickfeedback') and $grade->is_editable()) {
718 if ($this->get_pref('quickgrading')) {
719 $studentshtml .= '<br />';
721 $studentshtml .= '<input type="hidden" name="oldfeedback_'
722 .$userid.'_'.$item->id.'" value="' . s($grade->feedback) . '" />';
723 $studentshtml .= '<input class="quickfeedback" tabindex="' . $tabindices[$item->id]['feedback']
724 . '" size="6" title="' . $strfeedback . '" type="text" name="feedback_'
725 .$userid.'_'.$item->id.'" value="' . s($grade->feedback) . '" />';
728 } else {
729 // Percentage format if specified by user (check each item for a set preference)
730 $gradedisplaytype = $item->get_displaytype();
732 $percentsign = '';
733 $grademin = $item->grademin;
734 $grademax = $item->grademax;
736 if ($gradedisplaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
737 if (!is_null($gradeval)) {
738 $gradeval = grade_to_percentage($gradeval, $grademin, $grademax);
740 $percentsign = '%';
743 // If feedback present, surround grade with feedback tooltip
744 if (!empty($grade->feedback)) {
745 $overlib = '';
746 if ($grade->feedbackformat == 1) {
747 $overlib = "return overlib('" . s(ltrim($grade->feedback)) . "', FULLHTML);";
748 } else {
749 $overlib = "return overlib('" . s($grade->feedback) . "', BORDER, 0, FGCLASS, 'feedback', "
750 . "CAPTIONFONTCLASS, 'caption', CAPTION, '$strfeedback');";
753 $studentshtml .= '<span onmouseover="' . $overlib . '" onmouseout="return nd();">';
756 if ($item->needsupdate) {
757 $studentshtml .= '<span class="gradingerror">'.get_string('error').'</span>';
759 } else if ($gradedisplaytype == GRADE_DISPLAY_TYPE_LETTER) {
760 $letters = grade_report::get_grade_letters();
761 if (!is_null($gradeval)) {
762 $studentshtml .= grade_grade::get_letter($letters, $gradeval, $grademin, $grademax);
764 } else if ($item->scaleid && !empty($scales_array[$item->scaleid])
765 && $gradedisplaytype == GRADE_DISPLAY_TYPE_REAL) {
766 $scale = $scales_array[$item->scaleid];
767 $scales = explode(",", $scale->scale);
769 // invalid grade if gradeval < 1
770 if ((int) $gradeval < 1) {
771 $studentshtml .= '-';
772 } else {
773 $studentshtml .= $scales[$gradeval-1];
775 } else {
776 if (is_null($gradeval)) {
777 $studentshtml .= '-';
778 } else {
779 $studentshtml .= format_float($gradeval, $decimalpoints). $percentsign;
782 if (!empty($grade->feedback)) {
783 $studentshtml .= '</span>';
787 if (!empty($this->gradeserror[$item->id][$userid])) {
788 $studentshtml .= $this->gradeserror[$item->id][$userid];
791 $studentshtml .= '</td>' . "\n";
793 $studentshtml .= '</tr>';
795 return $studentshtml;
799 * Builds and return the HTML row of column totals.
800 * @param bool $grouponly Whether to return only group averages or all averages.
801 * @return string HTML
803 function get_avghtml($grouponly=false) {
804 global $CFG, $USER;
806 $averagesdisplaytype = $this->get_pref('averagesdisplaytype');
807 $averagesdecimalpoints = $this->get_pref('averagesdecimalpoints');
808 $meanselection = $this->get_pref('meanselection');
809 $shownumberofgrades = $this->get_pref('shownumberofgrades');
811 $canviewhidden = has_capability('moodle/grade:viewhidden', get_context_instance(CONTEXT_COURSE, $this->course->id));
813 $avghtml = '';
814 $avgcssclass = 'avg';
816 if ($grouponly) {
817 $straverage = get_string('groupavg', 'grades');
818 $showaverages = $this->currentgroup && $this->get_pref('showgroups');
819 $groupsql = $this->groupsql;
820 $groupwheresql = $this->groupwheresql;
821 $avgcssclass = 'groupavg';
822 } else {
823 $straverage = get_string('overallaverage', 'grades');
824 $showaverages = $this->get_pref('showaverages');
825 $groupsql = null;
826 $groupwheresql = null;
829 if ($shownumberofgrades) {
830 $straverage .= ' (' . get_string('submissions', 'grades') . ') ';
833 $totalcount = $this->get_numusers($grouponly);
835 if ($showaverages) {
837 // the first join on user is needed for groupsql
838 $SQL = "SELECT g.itemid, SUM(g.finalgrade) as sum
839 FROM {$CFG->prefix}grade_items gi LEFT JOIN
840 {$CFG->prefix}grade_grades g ON gi.id = g.itemid LEFT JOIN
841 {$CFG->prefix}user u ON g.userid = u.id
842 $groupsql
843 WHERE gi.courseid = $this->courseid
844 $groupwheresql
845 AND g.userid IN (
846 SELECT DISTINCT(u.id)
847 FROM {$CFG->prefix}user u LEFT JOIN
848 {$CFG->prefix}role_assignments ra ON u.id = ra.userid
849 WHERE ra.roleid in ($this->gradebookroles)
850 AND ra.contextid ".get_related_contexts_string($this->context)."
852 GROUP BY g.itemid";
853 $sum_array = array();
854 if ($sums = get_records_sql($SQL)) {
855 foreach ($sums as $itemid => $csum) {
856 $sum_array[$itemid] = $csum->sum;
860 $avghtml = '<tr class="' . $avgcssclass . ' r'.$this->rowcount++.'"><th class="header c0" scope="row">'.$straverage.'</th>';
862 $columncount=1;
863 foreach ($this->items as $item) {
864 // If the user shouldn't see this grade_item, hide the average as well
865 if ($item->is_hidden() && !$canviewhidden) {
866 $avghtml .= '<td class="cell c' . $columncount++.'"> - </td>';
867 continue;
870 if (empty($sum_array[$item->id])) {
871 $sum_array[$item->id] = 0;
873 if ($grouponly) {
874 $groupsql = $this->groupsql;
875 $groupwheresql = $this->groupwheresql;
876 } else {
877 $groupsql = '';
878 $groupwheresql = '';
880 // MDL-10875 Empty grades must be evaluated as grademin, NOT always 0
881 // This query returns a count of ungraded grades (NULL finalgrade OR no matching record in grade_grades table)
882 $SQL = "SELECT COUNT(*) AS count FROM {$CFG->prefix}user u
883 WHERE u.id NOT IN
884 (SELECT userid FROM {$CFG->prefix}grade_grades
885 WHERE itemid = $item->id
886 AND finalgrade IS NOT NULL
888 AND u.id IN (
889 SELECT DISTINCT(u.id)
890 FROM {$CFG->prefix}user u LEFT JOIN
891 {$CFG->prefix}role_assignments ra ON u.id = ra.userid
892 $groupsql
893 WHERE ra.roleid in ($this->gradebookroles)
894 AND ra.contextid ".get_related_contexts_string($this->context)."
895 $groupwheresql
898 $ungraded_count = get_field_sql($SQL);
900 if ($meanselection == GRADE_REPORT_MEAN_GRADED) {
901 $mean_count = $totalcount - $ungraded_count;
902 } else { // Bump up the sum by the number of ungraded items * grademin
903 if (isset($sum_array[$item->id])) {
904 $sum_array[$item->id] += $ungraded_count * $item->grademin;
906 $mean_count = $totalcount;
909 $decimalpoints = $item->get_decimals();
911 // Determine which display type to use for this average
912 $gradedisplaytype = $item->get_displaytype();
914 if ($USER->gradeediting[$this->courseid]) {
915 $displaytype = GRADE_DISPLAY_TYPE_REAL;
916 } elseif ($averagesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // Inherit specific column or general preference
917 $displaytype = $gradedisplaytype;
918 } else { // General preference overrides specific column display type
919 $displaytype = $averagesdisplaytype;
922 // Override grade_item setting if a display preference (not inherit) was set for the averages
923 if ($averagesdecimalpoints != GRADE_REPORT_PREFERENCE_INHERIT) {
924 $decimalpoints = $averagesdecimalpoints;
927 if (!isset($sum_array[$item->id]) || $mean_count == 0) {
928 $avghtml .= '<td class="cell c' . $columncount++.'">-</td>';
929 } else {
930 $sum = $sum_array[$item->id];
932 if ($item->scaleid) {
933 if ($grouponly) {
934 $finalsum = $sum_array[$item->id];
935 $finalavg = $finalsum/$mean_count;
936 } else {
937 $finalavg = $sum/$mean_count;
939 $scaleval = round($finalavg);
940 $scale_object = new grade_scale(array('id' => $item->scaleid), false);
941 $gradehtml = $scale_object->get_nearest_item($scaleval);
942 $rawvalue = $scaleval;
943 } else {
944 $rawgradeval = $sum/$mean_count;
945 $gradeval = format_float($sum/$mean_count, $decimalpoints);
946 $gradehtml = $gradeval;
949 if ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
950 $gradeval = grade_to_percentage($rawgradeval, $item->grademin, $item->grademax);
951 $gradehtml = format_float($gradeval, $decimalpoints). '%';
952 } elseif ($displaytype == GRADE_DISPLAY_TYPE_LETTER) {
953 $letters = grade_report::get_grade_letters();
954 $gradehtml = grade_grade::get_letter($letters, $rawgradeval, $item->grademin, $item->grademax);
957 $numberofgrades = '';
959 if ($shownumberofgrades) {
960 $numberofgrades = " ($mean_count)";
963 $avghtml .= '<td class="cell c' . $columncount++.'">'.$gradehtml.$numberofgrades.'</td>';
966 $avghtml .= '</tr>';
968 return $avghtml;
972 * Builds and return the HTML row of ranges for each column (i.e. range).
973 * @return string HTML
975 function get_rangehtml() {
976 global $USER;
978 $scalehtml = '';
979 if ($this->get_pref('showranges')) {
980 $rangesdisplaytype = $this->get_pref('rangesdisplaytype');
981 $rangesdecimalpoints = $this->get_pref('rangesdecimalpoints');
982 $scalehtml = '<tr class="r'.$this->rowcount++.'">'
983 . '<th class="header c0 range" scope="row">'.$this->get_lang_string('range','grades').'</th>';
985 $columncount = 1;
986 foreach ($this->items as $item) {
988 // Determine which display type to use for this range
989 $decimalpoints = $item->get_decimals();
990 $gradedisplaytype = $item->get_displaytype();
992 if ($USER->gradeediting[$this->courseid]) {
993 $displaytype = GRADE_DISPLAY_TYPE_REAL;
994 } elseif ($rangesdisplaytype == GRADE_REPORT_PREFERENCE_INHERIT) { // Inherit specific column or general preference
995 $displaytype = $gradedisplaytype;
996 } else { // General preference overrides specific column display type
997 $displaytype = $rangesdisplaytype;
1000 // If ranges decimal points pref is set (but not to inherit), override grade_item setting
1001 if ($rangesdecimalpoints != GRADE_REPORT_PREFERENCE_INHERIT) {
1002 $decimalpoints = $rangesdecimalpoints;
1005 $grademin = 0;
1006 $grademax = 100;
1008 if ($displaytype == GRADE_DISPLAY_TYPE_REAL) {
1009 $grademin = format_float($item->grademin, $decimalpoints);
1010 $grademax = format_float($item->grademax, $decimalpoints);
1011 } elseif ($displaytype == GRADE_DISPLAY_TYPE_PERCENTAGE) {
1012 $grademin = 0;
1013 $grademax = 100;
1014 } elseif ($displaytype == GRADE_DISPLAY_TYPE_LETTER) {
1015 $letters = grade_report::get_grade_letters();
1016 $grademin = end($letters);
1017 $grademax = reset($letters);
1020 $scalehtml .= '<th class="header c'.$columncount++.' range">'. $grademin.'-'. $grademax.'</th>';
1022 $scalehtml .= '</tr>';
1024 return $scalehtml;
1028 * Given a grade_category, grade_item or grade_grade, this function
1029 * figures out the state of the object and builds then returns a div
1030 * with the icons needed for the grader report.
1032 * @param object $object
1033 * @return string HTML
1035 function get_icons($element) {
1036 global $CFG, $USER;
1038 if (!$USER->gradeediting[$this->courseid]) {
1039 return '<div class="grade_icons" />';
1042 // Init all icons
1043 $edit_icon = $this->gtree->get_edit_icon($element, $this->gpr);
1044 $edit_calculation_icon = '';
1045 $show_hide_icon = '';
1046 $lock_unlock_icon = '';
1048 if (has_capability('moodle/grade:manage', $this->context)) {
1050 if ($this->get_pref('showcalculations')) {
1051 $edit_calculation_icon = $this->gtree->get_calculation_icon($element, $this->gpr);
1054 if ($this->get_pref('showeyecons')) {
1055 $show_hide_icon = $this->gtree->get_hiding_icon($element, $this->gpr);
1058 if ($this->get_pref('showlocks')) {
1059 $lock_unlock_icon = $this->gtree->get_locking_icon($element, $this->gpr);
1063 return '<div class="grade_icons">'.$edit_icon.$edit_calculation_icon.$show_hide_icon.$lock_unlock_icon.'</div>';
1067 * Given a category element returns collapsing +/- icon if available
1068 * @param object $object
1069 * @return string HTML
1071 function get_collapsing_icon($element) {
1072 global $CFG;
1074 $contract_expand_icon = '';
1075 // If object is a category, display expand/contract icon
1076 if ($element['type'] == 'category') {
1077 // Load language strings
1078 $strswitch_minus = $this->get_lang_string('aggregatesonly', 'grades');
1079 $strswitch_plus = $this->get_lang_string('gradesonly', 'grades');
1080 $strswitch_whole = $this->get_lang_string('fullmode', 'grades');
1082 $expand_contract = 'switch_minus'; // Default: expanded
1083 // $this->get_pref('aggregationview', $element['object']->id) == GRADE_REPORT_AGGREGATION_VIEW_COMPACT
1085 if (in_array($element['object']->id, $this->collapsed['aggregatesonly'])) {
1086 $expand_contract = 'switch_plus';
1087 } elseif (in_array($element['object']->id, $this->collapsed['gradesonly'])) {
1088 $expand_contract = 'switch_whole';
1090 $url = $this->gpr->get_return_url(null, array('target'=>$element['eid'], 'action'=>$expand_contract, 'sesskey'=>sesskey()));
1091 $contract_expand_icon = '<a href="'.$url.'"><img src="'.$CFG->pixpath.'/t/'.$expand_contract.'.gif" class="iconsmall" alt="'
1092 .${'str'.$expand_contract}.'" title="'.${'str'.$expand_contract}.'" /></a>';
1094 return $contract_expand_icon;
1098 * Processes a single action against a category, grade_item or grade.
1099 * @param string $target eid ({type}{id}, e.g. c4 for category4)
1100 * @param string $action Which action to take (edit, delete etc...)
1101 * @return
1103 function process_action($target, $action) {
1104 // TODO: this code should be in some grade_tree static method
1105 $targettype = substr($target, 0, 1);
1106 $targetid = substr($target, 1);
1107 // TODO: end
1109 if ($collapsed = get_user_preferences('grade_report_grader_collapsed_categories')) {
1110 $collapsed = unserialize($collapsed);
1111 } else {
1112 $collapsed = array('aggregatesonly' => array(), 'gradesonly' => array());
1115 switch ($action) {
1116 case 'switch_minus': // Add category to array of aggregatesonly
1117 if (!in_array($targetid, $collapsed['aggregatesonly'])) {
1118 $collapsed['aggregatesonly'][] = $targetid;
1119 set_user_preference('grade_report_grader_collapsed_categories', serialize($collapsed));
1121 break;
1123 case 'switch_plus': // Remove category from array of aggregatesonly, and add it to array of gradesonly
1124 $key = array_search($targetid, $collapsed['aggregatesonly']);
1125 if ($key !== false) {
1126 unset($collapsed['aggregatesonly'][$key]);
1128 if (!in_array($targetid, $collapsed['gradesonly'])) {
1129 $collapsed['gradesonly'][] = $targetid;
1131 set_user_preference('grade_report_grader_collapsed_categories', serialize($collapsed));
1132 break;
1133 case 'switch_whole': // Remove the category from the array of collapsed cats
1134 $key = array_search($targetid, $collapsed['gradesonly']);
1135 if ($key !== false) {
1136 unset($collapsed['gradesonly'][$key]);
1137 set_user_preference('grade_report_grader_collapsed_categories', serialize($collapsed));
1140 break;
1141 default:
1142 break;
1145 return true;