3 * File in which the grader_report class is defined.
7 require_once($CFG->dirroot
. '/grade/report/lib.php');
8 require_once($CFG->libdir
.'/tablelib.php');
11 * Class providing an API for the grader report building and displaying.
15 class grade_report_grader
extends grade_report
{
18 * @var array $finalgrades
29 * Array of errors for bulk grades updating.
30 * @var array $gradeserror
32 var $gradeserror = array();
37 * The id of the grade_item by which this report will be sorted.
38 * @var int $sortitemid
43 * Sortorder used in the SQL selections.
49 * An SQL fragment affecting the search for users.
50 * @var string $userselect
55 * List of collapsed categories from user preference
56 * @var array $collapsed
61 * A count of the rows, used for css classes.
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) {
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);
82 $this->collapsed
= array('aggregatesonly' => array(), 'gradesonly' => array());
85 if (empty($CFG->enableoutcomes
)) {
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');
101 if (!empty($studentsperpage)) {
102 $perpage = '&perpage='.$studentsperpage;
103 $curpage = '&page='.$this->page
;
105 $this->baseurl
= 'index.php?id='.$this->courseid
. $perpage.$curpage.'&';
107 $this->pbarurl
= 'index.php?id='.$this->courseid
.$perpage.'&';
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).
120 * @return bool Success or Failure (array of errors).
122 function process_data($data) {
124 if (!has_capability('moodle/grade:edit', $this->context
)) {
128 // always initialize all arrays
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';
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) {
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');
160 if ($data_type == 'grade') {
162 $feedbackformat = false;
163 if ($grade_item->gradetype
== GRADE_TYPE_SCALE
) {
164 if ($postedvalue == -1) { // -1 means no grade
167 $finalgrade = $postedvalue;
170 $finalgrade = unformat_float($postedvalue);
173 } else if ($data_type == 'feedback') {
175 $trimmed = trim($postedvalue);
176 if (empty($trimmed)) {
179 $feedback = stripslashes($postedvalue);
183 $grade_item->update_final_grade($userid, $finalgrade, 'gradebook', $note, $feedback);
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() {
199 if ($this->sortitemid
) {
200 if (!isset($SESSION->gradeuserreport
->sort
)) {
201 $this->sortorder
= $SESSION->gradeuserreport
->sort
= 'DESC';
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
) {
208 if ($SESSION->gradeuserreport
->sort
== 'ASC') {
209 $this->sortorder
= $SESSION->gradeuserreport
->sort
= 'DESC';
211 $this->sortorder
= $SESSION->gradeuserreport
->sort
= 'ASC';
214 $this->sortorder
= $SESSION->gradeuserreport
->sort
= 'DESC';
217 $SESSION->gradeuserreport
->sortitemid
= $this->sortitemid
;
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
;
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() {
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
248 WHERE ra.roleid in ($this->gradebookroles)
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'));
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
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();
278 $this->userselect
= 'AND g.userid in ('.implode(',', array_keys($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) {
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 ";
297 $countsql .= $this->groupsql
;
299 $countsql .= " WHERE ra.roleid in ($this->gradebookroles) ";
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() {
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() {
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);
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
371 function print_toggle($type, $return=false) {
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);
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');
399 if (array_key_exists($type, $icons)) {
400 $image_name = $icons[$type];
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
. "&toggle=$toggle_action&toggle_type=$type\">"
411 . $string . '</a></div>';
421 * Builds and returns the HTML code for the headers.
422 * @return string $headerhtml
424 function get_headerhtml() {
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);
436 $lastarrow = print_arrow('down', $strsortdesc, true);
442 if ($this->sortitemid
=== 'firstname') {
443 if ($this->sortorder
== 'ASC') {
444 $firstarrow = print_arrow('up', $strsortasc, true);
446 $firstarrow = print_arrow('down', $strsortdesc, true);
451 // Prepare Table Headers
454 $numrows = count($this->gtree
->levels
);
456 $columns_to_unset = array();
459 foreach ($this->gtree
->levels
as $key=>$row) {
462 // do not display course grade category
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
.'&sortitemid=firstname">'
470 . $strfirstname . '</a> ' //TODO: localize
471 . $firstarrow. '/ <a href="'.$this->baseurl
.'&sortitemid=lastname">' . $strlastname . '</a>'. $lastarrow .'</th>';
473 $headerhtml .= '<td class="cell c'.$columncount++
.' topleft"> </td>';
476 foreach ($row as $columnkey => $element) {
478 if (isset($element['object']->id
)) {
479 $sort_link = $this->baseurl
.'&sortitemid=' . $element['object']->id
;
482 $eid = $element['eid'];
483 $object = $element['object'];
484 $type = $element['type'];
485 $categorystate = @$element['categorystate'];
487 $iteminstance = null;
489 $columnclass = 'c' . $columncount++
;
490 if (!empty($element['colspan'])) {
491 $colspan = 'colspan="'.$element['colspan'].'"';
497 if (!empty($element['depth'])) {
498 $catlevel = ' catlevel'.$element['depth'];
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"> </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);
514 if ($USER->gradeediting
[$this->courseid
]) {
515 $headerhtml .= $this->get_icons($element);
518 $headerhtml .= '</th>';
520 // Element is a grade_item
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);
529 $arrow = $this->get_sort_arrow('down', $sort_link);
532 $arrow = $this->get_sort_arrow('move', $sort_link);
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>';
564 * Builds and return the HTML rows of the table (grades headed by student).
565 * @return string HTML
567 function get_studentshtml() {
570 $strfeedback = $this->get_lang_string("feedback");
571 $strgrade = $this->get_lang_string('grade');
573 $showuserimage = $this->get_pref('showuserimage');
574 $numusers = count($this->users
);
576 // Preload scale objects for items with a scaleid
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) {
598 // Student name and link
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
;
620 $grade = new grade_grade(array('userid'=>$userid, 'itemid'=>$item->id
), false);
621 $grade->feedback
= '';
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>';
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++
.'">';
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
672 foreach ($scales as $scaleoption) {
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');
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 .= '-';
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];
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.'" />';
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
) . '" />';
729 // Percentage format if specified by user (check each item for a set preference)
730 $gradedisplaytype = $item->get_displaytype();
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);
743 // If feedback present, surround grade with feedback tooltip
744 if (!empty($grade->feedback
)) {
746 if ($grade->feedbackformat
== 1) {
747 $overlib = "return overlib('" . s(ltrim($grade->feedback
)) . "', FULLHTML);";
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 .= '-';
773 $studentshtml .= $scales[$gradeval-1];
776 if (is_null($gradeval)) {
777 $studentshtml .= '-';
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) {
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
));
814 $avgcssclass = 'avg';
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';
823 $straverage = get_string('overallaverage', 'grades');
824 $showaverages = $this->get_pref('showaverages');
826 $groupwheresql = null;
829 if ($shownumberofgrades) {
830 $straverage .= ' (' . get_string('submissions', 'grades') . ') ';
833 $totalcount = $this->get_numusers($grouponly);
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
843 WHERE gi.courseid = $this->courseid
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
)."
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>';
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>';
870 if (empty($sum_array[$item->id
])) {
871 $sum_array[$item->id
] = 0;
874 $groupsql = $this->groupsql
;
875 $groupwheresql = $this->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
884 (SELECT userid FROM {$CFG->prefix}grade_grades
885 WHERE itemid = $item->id
886 AND finalgrade IS NOT NULL
889 SELECT DISTINCT(u.id)
890 FROM {$CFG->prefix}user u LEFT JOIN
891 {$CFG->prefix}role_assignments ra ON u.id = ra.userid
893 WHERE ra.roleid in ($this->gradebookroles)
894 AND ra.contextid ".get_related_contexts_string($this->context
)."
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>';
930 $sum = $sum_array[$item->id
];
932 if ($item->scaleid
) {
934 $finalsum = $sum_array[$item->id
];
935 $finalavg = $finalsum/$mean_count;
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;
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>';
972 * Builds and return the HTML row of ranges for each column (i.e. range).
973 * @return string HTML
975 function get_rangehtml() {
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>';
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;
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
) {
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>';
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) {
1038 if (!$USER->gradeediting
[$this->courseid
]) {
1039 return '<div class="grade_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) {
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...)
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);
1109 if ($collapsed = get_user_preferences('grade_report_grader_collapsed_categories')) {
1110 $collapsed = unserialize($collapsed);
1112 $collapsed = array('aggregatesonly' => array(), 'gradesonly' => array());
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));
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));
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));