3 /// This creates and handles the whole grader report interface, sans header and footer
5 require_once($CFG->libdir
.'/tablelib.php');
6 require_once($CFG->libdir
.'/gradelib.php');
7 require_once($CFG->dirroot
.'/grade/report/lib.php');
8 $gradeserror = array();
11 * Shortcut function for printing the grader report toggles.
12 * @param string $type The type of toggle
13 * @param string $baseurl The base of the URL the toggles will link to
14 * @param bool $return Whether to return the HTML string rather than printing it
17 function grader_report_print_toggle($type, $baseurl, $return=false) {
20 $icons = array('eyecons' => 'hide',
21 'calculations' => 'calc',
23 'grandtotals' => 'sigma');
25 $pref_name = 'grade_report_show' . $type;
26 $show_pref = get_user_preferences($pref_name, $CFG->$pref_name);
28 $strshow = get_string('show' . $type, 'grades');
29 $strhide = get_string('hide' . $type, 'grades');
39 if (array_key_exists($type, $icons)) {
40 $image_name = $icons[$type];
45 $string = $
{'str' . $show_hide};
47 $img = '<img src="'.$CFG->pixpath
.'/t/'.$image_name.'.gif" class="iconsmall" alt="'
48 .$string.'" title="'.$string.'" />'. "\n";
50 $retval = '<div class="gradertoggle">' . $img . '<a href="' . $baseurl . "&toggle=$toggle_action&toggle_type=$type\">"
51 . $string . '</a></div>';
61 /// processing posted grades here
63 if ($data = data_submitted() and confirm_sesskey()) {
65 // always initialize all arrays
68 foreach ($data as $varname => $postedgrade) {
69 // this is a bit tricky - we have to first load all grades into memory,
70 // check if changed and only then start updating the final grades because
71 // columns might depend one on another - the result would be overriden calculated and category grades
74 if (!strstr($varname, 'grade')) {
78 $gradeinfo = explode("_", $varname);
80 $userid = clean_param($gradeinfo[1], PARAM_INT
);
81 $itemid = clean_param($gradeinfo[2], PARAM_INT
);
83 if (!$grade_item = grade_item
::fetch(array('id'=>$itemid, 'courseid'=>$course->id
))) { // we must verify course id here!
84 error('Incorrect grade item id');
87 if ($grade_item->gradetype
== GRADE_TYPE_SCALE
) {
88 if ($postedgrade == -1) { // -1 means no grade
91 $finalgrade = (float)$postedgrade;
94 if ($postedgrade == '') { // empty string means no grade
97 $finalgrade = format_grade($postedgrade);
101 if (!is_null($finalgrade) and ($finalgrade < $grade_item->grademin
or $finalgrade > $grade_item->grademax
)) {
102 $gradeserror[$grade_item->id
][$userid] = 'outofrange'; //TODO: localize
103 // another possiblity is to use bounded number instead
107 if ($grade = grade_grades
::fetch(array('userid'=>$userid, 'itemid'=>$grade_item->id
))) {
108 if (!is_null($grade->finalgrade
)) {
109 $grade->finalgrade
= (float)$grade->finalgrade
;
111 if ($grade->finalgrade
=== $finalgrade) {
112 // we must not update all grades, only changed ones - we do not want to mark everything as overriden
117 $gradedata = new object();
118 $gradedata->grade_item
= $grade_item;
119 $gradedata->finalgrade
= $finalgrade;
120 $gradedata->userid
= $userid;
122 $queue[] = $gradedata;
125 // now we update the new final grade for each changed grade
126 foreach ($queue as $gradedata) {
127 $gradedata->grade_item
->update_final_grade($gradedata->userid
, $gradedata->finalgrade
, 'gradebook');
132 $courseid = required_param('id', PARAM_INT
);
133 $context = get_context_instance(CONTEXT_COURSE
, $courseid);
134 $page = optional_param('page', 0, PARAM_INT
);
135 $sortitemid = optional_param('sortitemid', 0, PARAM_ALPHANUM
); // sort by which grade item
136 $report = optional_param('report', 0, PARAM_ALPHANUM
);
137 $action = optional_param('action', 0, PARAM_ALPHA
);
138 $move = optional_param('move', 0, PARAM_INT
);
139 $type = optional_param('type', 0, PARAM_ALPHA
);
140 $target = optional_param('target', 0, PARAM_ALPHANUM
);
141 $toggle = optional_param('toggle', NULL, PARAM_INT
);
142 $toggle_type = optional_param('toggle_type', 0, PARAM_ALPHANUM
);
144 // Handle toggle change request
145 // TODO print visual feedback
146 if (!is_null($toggle) && !empty($toggle_type)) {
147 set_user_preferences(array('grade_report_show' . $toggle_type => $toggle));
150 // Get the user preferences
151 $perpage = get_user_preferences('grade_report_studentsperpage', $CFG->grade_report_studentsperpage
); // number of users on a page
152 $decimals = get_user_preferences('grade_report_decimalpoints', $CFG->grade_report_decimalpoints
); // decimals in grades
153 $showgrandtotals = get_user_preferences('grade_report_showgrandtotals', $CFG->grade_report_showgrandtotals
);
154 $showgroups = get_user_preferences('grade_report_showgroups', $CFG->grade_report_showgroups
);
155 $aggregation_position = get_user_preferences('grade_report_aggregationposition', $CFG->grade_report_aggregationposition
);
156 $showscales = get_user_preferences('grade_report_showscales', $CFG->grade_report_showscales
);
157 $quickgrading = get_user_preferences('grade_report_quickgrading', $CFG->grade_report_quickgrading
);
158 $quickfeedback = get_user_preferences('grade_report_quickfeedback', $CFG->grade_report_quickfeedback
);
160 // Override perpage if set in URL
161 if ($perpageurl = optional_param('perpage', 0, PARAM_INT
)) {
162 $perpage = $perpageurl;
165 // Prepare language strings
166 $strsortasc = get_string('sortasc', 'grades');
167 $strsortdesc = get_string('sortdesc', 'grades');
168 $strfeedback = get_string("feedback");
170 // base url for sorting by first/last name
171 $baseurl = 'report.php?id='.$courseid.'&perpage='.$perpage.'&report=grader&page='.$page;
172 // base url for paging
173 $pbarurl = 'report.php?id='.$courseid.'&perpage='.$perpage.'&report=grader&';
175 /// setting up groups
178 $group_selector = null;
179 $currentgroup = null;
182 /// find out current groups mode
183 $course = get_record('course', 'id', $courseid);
184 $groupmode = $course->groupmode
;
186 $currentgroup = setup_and_print_groups($course, $groupmode, $baseurl);
187 $group_selector = ob_get_clean();
189 // update paging after group
190 $baseurl .= 'group='.$currentgroup.'&';
191 $pbarurl .= 'group='.$currentgroup.'&';
194 $groupsql = " LEFT JOIN {$CFG->prefix}groups_members gm ON gm.userid = u.id ";
195 $groupwheresql = " AND gm.groupid = $currentgroup ";
199 // Grab the grade_tree for this course
200 $gtree = new grade_tree($courseid, true, false, $aggregation_position);
202 // setting the sort order, this depends on last state
203 // all this should be in the new table class that we might need to use
204 // for displaying grades
206 // already in not requesting sort, i.e. normal paging
209 if (!isset($SESSION->gradeuserreport
->sort
)) {
210 $sortorder = $SESSION->gradeuserreport
->sort
= 'ASC';
212 // this is the first sort, i.e. by last name
213 if (!isset($SESSION->gradeuserreport
->sortitemid
)) {
214 $sortorder = $SESSION->gradeuserreport
->sort
= 'ASC';
215 } else if ($SESSION->gradeuserreport
->sortitemid
== $sortitemid) {
217 if ($SESSION->gradeuserreport
->sort
== 'ASC') {
218 $sortorder = $SESSION->gradeuserreport
->sort
= 'DESC';
220 $sortorder = $SESSION->gradeuserreport
->sort
= 'ASC';
223 $sortorder = $SESSION->gradeuserreport
->sort
= 'ASC';
226 $SESSION->gradeuserreport
->sortitemid
= $sortitemid;
228 // not requesting sort, use last setting (for paging)
230 if (isset($SESSION->gradeuserreport
->sortitemid
)) {
231 $sortitemid = $SESSION->gradeuserreport
->sortitemid
;
233 if (isset($SESSION->gradeuserreport
->sort
)) {
234 $sortorder = $SESSION->gradeuserreport
->sort
;
240 /// end of setting sort order code
242 // Perform actions on categories, items and grades
243 if (!empty($target) && !empty($action) && confirm_sesskey()) {
245 $element = $gtree->locate_element($target);
251 if ($confirm == 1) { // Perform the deletion
252 //TODO: add proper delete support for grade items and categories
253 //$element['object']->delete();
254 // Print result message
256 } else { // Print confirmation dialog
257 $eid = $element['eid'];
258 $strdeletecheckfull = get_string('deletecheck', '', $element['object']->get_name());
259 $linkyes = "category.php?target=$eid&action=delete&confirm=1$gtree->commonvars";
260 $linkno = "category.php?$gtree->commonvars";
261 notice_yesno($strdeletecheckfull, $linkyes, $linkno);
266 // TODO Implement calendar for selection of a date to hide element until
267 $element['object']->set_hidden(1);
268 $gtree = new grade_tree($courseid);
271 $element['object']->set_hidden(0);
272 $gtree = new grade_tree($courseid);
275 // TODO Implement calendar for selection of a date to lock element after
276 if (!$element['object']->set_locked(1)) {
277 debugging("Could not update the element's locked state!");
279 $gtree = new grade_tree($courseid);
282 if (!$element['object']->set_locked(0)) {
283 debugging("Could not update the element's locked state!");
285 $gtree = new grade_tree($courseid);
292 // first make sure we have all final grades
293 // TODO: check that no grade_item has needsupdate set
294 grade_regrade_final_grades($courseid);
296 // roles to be displaye in the gradebook
297 $gradebookroles = $CFG->gradebookroles
;
300 * pulls out the userids of the users to be display, and sort them
301 * the right outer join is needed because potentially, it is possible not
302 * to have the corresponding entry in grade_grades table for some users
303 * this is check for user roles because there could be some users with grades
304 * but not supposed to be displayed
306 if (is_numeric($sortitemid)) {
307 $sql = "SELECT u.id, u.firstname, u.lastname
308 FROM {$CFG->prefix}grade_grades g RIGHT OUTER JOIN
309 {$CFG->prefix}user u ON (u.id = g.userid AND g.itemid = $sortitemid)
310 LEFT JOIN {$CFG->prefix}role_assignments ra ON u.id = ra.userid
312 WHERE ra.roleid in ($gradebookroles)
314 AND ra.contextid ".get_related_contexts_string($context)."
315 ORDER BY g.finalgrade $sortorder";
316 $users = get_records_sql($sql, $perpage * $page, $perpage);
319 // get users sorted by lastname
320 $users = get_role_users(@implode
(',', $CFG->gradebookroles
), $context, false, 'u.id, u.firstname, u.lastname', 'u.'.$sortitemid .' '. $sortorder, false, $page * $perpage, $perpage, $currentgroup);
321 // need to cut users down by groups
325 /// count total records for paging
327 $countsql = "SELECT COUNT(DISTINCT u.id)
328 FROM {$CFG->prefix}grade_grades g RIGHT OUTER JOIN
329 {$CFG->prefix}user u ON (u.id = g.userid AND g.itemid = $sortitemid)
330 LEFT JOIN {$CFG->prefix}role_assignments ra ON u.id = ra.userid
332 WHERE ra.roleid in ($gradebookroles)
334 AND ra.contextid ".get_related_contexts_string($context);
335 $numusers = count_records_sql($countsql);
337 // print_object($users); // debug
343 $userselect = 'AND g.userid in ('.implode(',', array_keys($users)).')';
347 // phase 2 sql, we supply the userids in this query, and get all the grades
348 // pulls out all the grades, this does not need to worry about paging
349 $sql = "SELECT g.id, g.itemid, g.userid, g.finalgrade, g.hidden, g.locked, g.locktime, g.overridden, gt.feedback
350 FROM {$CFG->prefix}grade_items gi,
351 {$CFG->prefix}grade_grades g
352 LEFT JOIN {$CFG->prefix}grade_grades_text gt ON g.id = gt.gradeid
353 WHERE g.itemid = gi.id
354 AND gi.courseid = $courseid $userselect";
356 ///print_object($grades); //debug
358 $finalgrades = array();
359 // needs to be formatted into an array for easy retrival
361 if ($grades = get_records_sql($sql)) {
362 foreach ($grades as $grade) {
363 $finalgrades[$grade->userid
][$grade->itemid
] = $grade;
367 /// With the users in an sorted array and grades fetched, we can not print the main html table
369 // 1. Fetch all top-level categories for this course, with all children preloaded, sorted by sortorder
371 // Fetch array of students enroled in this course
372 if (!$context = get_context_instance(CONTEXT_COURSE
, $gtree->courseid
)) {
375 //$users = get_role_users(@implode(',', $CFG->gradebookroles), $context);
377 if ($sortitemid === 'lastname') {
378 if ($sortorder == 'ASC') {
379 $lastarrow = print_arrow('up', $strsortasc, true);
381 $lastarrow = print_arrow('down', $strsortdesc, true);
387 if ($sortitemid === 'firstname') {
388 if ($sortorder == 'ASC') {
389 $firstarrow = print_arrow('up', $strsortasc, true);
391 $firstarrow = print_arrow('down', $strsortdesc, true);
397 /********* BEGIN OUTPUT *********/
399 print_heading('Grader Report');
402 $currenttab = 'graderreport';
405 // Group selection drop-down
406 echo $group_selector;
409 echo '<div id="grade-report-toggles">';
410 if ($USER->gradeediting
) {
411 grader_report_print_toggle('eyecons', $baseurl);
412 grader_report_print_toggle('locks', $baseurl);
413 grader_report_print_toggle('calculations', $baseurl);
416 grader_report_print_toggle('grandtotals', $baseurl);
417 grader_report_print_toggle('groups', $baseurl);
418 grader_report_print_toggle('scales', $baseurl);
422 print_paging_bar($numusers, $page, $perpage, $pbarurl);
426 // Prepare Table Headers
429 $numrows = count($gtree->levels
);
431 foreach ($gtree->levels
as $key=>$row) {
433 // do not diplay course grade category
437 $headerhtml .= '<tr class="heading">';
439 if ($key == $numrows - 1) {
440 $headerhtml .= '<th class="user"><a href="'.$baseurl.'&sortitemid=firstname">Firstname</a> ' //TODO: localize
441 . $firstarrow. '/ <a href="'.$baseurl.'&sortitemid=lastname">Lastname </a>'. $lastarrow .'</th>';
443 $headerhtml .= '<td class="topleft"> </td>';
446 foreach ($row as $element) {
447 $eid = $element['eid'];
448 $object = $element['object'];
449 $type = $element['type'];
451 if (!empty($element['colspan'])) {
452 $colspan = 'colspan="'.$element['colspan'].'"';
457 if (!empty($element['depth'])) {
458 $catlevel = ' catlevel'.$element['depth'];
464 if ($type == 'filler' or $type == 'fillerfirst' or $type == 'fillerlast') {
465 $headerhtml .= '<td class="'.$type.$catlevel.'" '.$colspan.'> </td>';
466 } else if ($type == 'category') {
467 $headerhtml .= '<td class="category'.$catlevel.'" '.$colspan.'>'.$element['object']->get_name();
470 if ($USER->gradeediting
) {
471 $headerhtml .= grade_get_icons($element, $gtree);
474 $headerhtml .= '</td>';
476 if ($element['object']->id
== $sortitemid) {
477 if ($sortorder == 'ASC') {
478 $arrow = print_arrow('up', $strsortasc, true);
480 $arrow = print_arrow('down', $strsortdesc, true);
487 if ($element['object']->is_hidden()) {
488 $dimmed = ' dimmed_text ';
491 if ($object->itemtype
== 'mod') {
492 $icon = '<img src="'.$CFG->modpixpath
.'/'.$object->itemmodule
.'/icon.gif" class="icon" alt="'
493 .get_string('modulename', $object->itemmodule
).'"/>';
494 } else if ($object->itemtype
== 'manual') {
495 //TODO: add manual grading icon
496 $icon = '<img src="'.$CFG->pixpath
.'/t/edit.gif" class="icon" alt="'.get_string('manualgrade', 'grades')
501 $headerhtml .= '<th class="'.$type.$catlevel.$dimmed.'"><a href="'.$baseurl.'&sortitemid='
502 . $element['object']->id
.'">'. $element['object']->get_name()
505 $headerhtml .= grade_get_icons($element, $gtree) . '</th>';
507 $items[$element['object']->sortorder
] =& $element['object'];
512 $headerhtml .= '</tr>';
515 // Prepare Table Rows
518 foreach ($users as $userid => $user) {
519 // Student name and link
520 $studentshtml .= '<tr><th class="user"><a href="' . $CFG->wwwroot
. '/user/view.php?id='
521 . $user->id
. '">' . fullname($user) . '</a></th>';
522 foreach ($items as $item) {
524 if (isset($finalgrades[$userid][$item->id
])) {
525 $gradeval = $finalgrades[$userid][$item->id
]->finalgrade
;
526 $grade = new grade_grades($finalgrades[$userid][$item->id
], false);
527 $grade->feedback
= $finalgrades[$userid][$item->id
]->feedback
;
531 $grade = new grade_grades(array('userid' => $userid, 'itemid' => $item->id
), false);
532 $grade->feedback
= '';
535 if ($grade->is_overridden()) {
536 $studentshtml .= '<td class="overridden">';
538 $studentshtml .= '<td>';
541 // Do not show any icons if no grade (no record in DB to match)
542 if (!empty($grade->id
)) {
543 // emulate grade element
544 $grade->courseid
= $course->id
;
545 $grade->grade_item
= $item; // this may speedup is_hidden() and other grade_grades methods
546 $element = array ('eid'=>'g'.$grade->id
, 'object'=>$grade, 'type'=>'grade');
547 $studentshtml .= grade_get_icons($element, $gtree);
551 // if in editting mode, we need to print either a text box
552 // or a drop down (for scales)
554 // grades in item of type grade category or course are not directly editable
555 if ($USER->gradeediting
) {
556 // We need to retrieve each grade_grade object from DB in order to
557 // know if they are hidden/locked
559 if ($item->scaleid
) {
560 if ($scale = get_record('scale', 'id', $item->scaleid
)) {
561 $scales = explode(",", $scale->scale
);
562 // reindex because scale is off 1
564 foreach ($scales as $scaleoption) {
566 $scaleopt[$i] = $scaleoption;
570 $studentshtml .= choose_from_menu($scaleopt, 'grade_'.$userid.'_'.$item->id
,
571 $gradeval, get_string('nograde'), '', -1, true);
572 } elseif ($scale = get_record('scale', 'id', $item->scaleid
)) {
573 $scales = explode(",", $scale->scale
);
575 // invalid grade if gradeval < 1
576 if ((int) $gradeval < 1) {
577 $studentshtml .= '-';
579 $studentshtml .= $scales[$gradeval-1];
582 // no such scale, throw error?
587 $studentshtml .= '<input size="6" type="text" name="grade_'.$userid.'_'.$item->id
.'" value="'.get_grade_clean($gradeval).'"/>';
589 $studentshtml .= get_grade_clean($gradeval);
594 // If quickfeedback is on, print an input element
595 if ($quickfeedback) {
596 $studentshtml .= '<input size="6" type="text" name="feedback_'.$userid.'_'.$item->id
.'" value="'. s($grade->feedback
) . '"/>';
599 $studentshtml .= '<div class="grade_icons">' . grade_get_icons($element, $gtree, array('edit')) . '</div>';
601 // If feedback present, surround grade with feedback tooltip
602 if (!empty($grade->feedback
)) {
603 $studentshtml .= '<span onmouseover="return overlib(\''.$grade->feedback
.'\', CAPTION, \''
604 . $strfeedback.'\');" onmouseout="return nd();">';
607 // finalgrades[$userid][$itemid] could be null because of the outer join
608 // in this case it's different than a 0
609 if ($item->scaleid
) {
610 if ($scale = get_record('scale', 'id', $item->scaleid
)) {
611 $scales = explode(",", $scale->scale
);
613 // invalid grade if gradeval < 1
614 if ((int) $gradeval < 1) {
615 $studentshtml .= '-';
617 $studentshtml .= $scales[$gradeval-1];
620 // no such scale, throw error?
623 if (is_null($gradeval)) {
624 $studentshtml .= '-';
626 $studentshtml .= get_grade_clean($gradeval);
629 if (!empty($grade->feedback
)) {
630 $studentshtml .= '</span>';
634 if (!empty($gradeserror[$item->id
][$userid])) {
635 $studentshtml .= $gradeserror[$item->id
][$userid];
638 $studentshtml .= '</td>' . "\n";
640 $studentshtml .= '</tr>';
643 // if user preference to display group sum
646 if ($currentgroup && $showgroups) {
648 /** SQL for finding group sum */
649 $SQL = "SELECT g.itemid, SUM(g.finalgrade) as sum
650 FROM {$CFG->prefix}grade_items gi LEFT JOIN
651 {$CFG->prefix}grade_grades g ON gi.id = g.itemid RIGHT OUTER JOIN
652 {$CFG->prefix}user u ON u.id = g.userid LEFT JOIN
653 {$CFG->prefix}role_assignments ra ON u.id = ra.userid
655 WHERE gi.courseid = $courseid
657 AND ra.roleid in ($gradebookroles)
658 AND ra.contextid ".get_related_contexts_string($context)."
662 $sums = get_records_sql($SQL);
663 foreach ($sums as $itemid => $csum) {
664 $groupsum[$itemid] = $csum;
667 $groupsumhtml = '<tr><th>Group total</th>';
668 foreach ($items as $item) {
669 if (!isset($groupsum[$item->id
])) {
670 $groupsumhtml .= '<td>-</td>';
672 $sum = $groupsum[$item->id
];
673 $groupsumhtml .= '<td>'.get_grade_clean($sum->sum
).'</td>';
676 $groupsumhtml .= '</tr>';
681 if ($showgrandtotals) {
683 /** SQL for finding the SUM grades of all visible users ($CFG->gradebookroles) */
685 $SQL = "SELECT g.itemid, SUM(g.finalgrade) as sum
686 FROM {$CFG->prefix}grade_items gi LEFT JOIN
687 {$CFG->prefix}grade_grades g ON gi.id = g.itemid RIGHT OUTER JOIN
688 {$CFG->prefix}user u ON u.id = g.userid LEFT JOIN
689 {$CFG->prefix}role_assignments ra ON u.id = ra.userid
690 WHERE gi.courseid = $courseid
691 AND ra.roleid in ($gradebookroles)
692 AND ra.contextid ".get_related_contexts_string($context)."
696 $sums = get_records_sql($SQL);
697 foreach ($sums as $itemid => $csum) {
698 $classsum[$itemid] = $csum;
701 $gradesumhtml = '<tr><th>Total</th>';
702 foreach ($items as $item) {
703 if (!isset($classsum[$item->id
])) {
704 $gradesumhtml .= '<td>-</td>';
706 $sum = $classsum[$item->id
];
707 $gradesumhtml .= '<td>'.get_grade_clean($sum->sum
).'</td>';
710 $gradesumhtml .= '</tr>';
713 // finding the ranges of each gradeitem
716 $scalehtml = '<tr><td>'.get_string('range','grades').'</td>';
717 foreach ($items as $item) {
718 $scalehtml .= '<td>'. get_grade_clean($item->grademin
).'-'. get_grade_clean($item->grademax
).'</td>';
720 $scalehtml .= '</tr>';
723 $reporthtml = "<table class=\"boxaligncenter\">$headerhtml";
724 $reporthtml .= $scalehtml;
725 $reporthtml .= $studentshtml;
726 $reporthtml .= $groupsumhtml;
727 $reporthtml .= $gradesumhtml;
728 $reporthtml .= "</table>";
730 // print submit button
731 if ($USER->gradeediting
) {
732 echo '<form action="report.php" method="post">';
734 echo '<input type="hidden" value="'.$courseid.'" name="id" />';
735 echo '<input type="hidden" value="'.sesskey().'" name="sesskey" />';
736 echo '<input type="hidden" value="grader" name="report"/>';
741 // print submit button
742 if ($USER->gradeediting
&& ($quickfeedback ||
$quickgrading)) {
743 echo '<div class="submit"><input type="submit" value="'.get_string('update').'" /></div>';
744 echo '</div></form>';