/** * Builds and returns the rows that will make up the right part of the grader report * @param boolean $displayaverages whether to display average rows in the table * @return array Array of html_table_row objects */ public function get_right_rows($displayaverages) { global $CFG, $USER, $OUTPUT, $DB, $PAGE; $rows = array(); $this->rowcount = 0; $numrows = count($this->gtree->get_levels()); $numusers = count($this->users); $gradetabindex = 1; $columnstounset = array(); $strgrade = $this->get_lang_string('grade'); $strfeedback = $this->get_lang_string("feedback"); $arrows = $this->get_sort_arrows(); $jsarguments = array('cfg' => array('ajaxenabled' => false), 'items' => array(), 'users' => array(), 'feedback' => array(), 'grades' => array()); $jsscales = array(); // Get preferences once. $showactivityicons = $this->get_pref('showactivityicons'); $quickgrading = $this->get_pref('quickgrading'); $showquickfeedback = $this->get_pref('showquickfeedback'); $enableajax = $this->get_pref('enableajax'); $showanalysisicon = $this->get_pref('showanalysisicon'); // Get strings which are re-used inside the loop. $strftimedatetimeshort = get_string('strftimedatetimeshort'); $strexcludedgrades = get_string('excluded', 'grades'); $strerror = get_string('error'); foreach ($this->gtree->get_levels() as $key => $row) { $headingrow = new html_table_row(); $headingrow->attributes['class'] = 'heading_name_row'; foreach ($row as $columnkey => $element) { $sortlink = clone $this->baseurl; if (isset($element['object']->id)) { $sortlink->param('sortitemid', $element['object']->id); } $eid = $element['eid']; $object = $element['object']; $type = $element['type']; $categorystate = @$element['categorystate']; if (!empty($element['colspan'])) { $colspan = $element['colspan']; } else { $colspan = 1; } if (!empty($element['depth'])) { $catlevel = 'catlevel' . $element['depth']; } else { $catlevel = ''; } // Element is a filler if ($type == 'filler' or $type == 'fillerfirst' or $type == 'fillerlast') { $fillercell = new html_table_cell(); $fillercell->attributes['class'] = $type . ' ' . $catlevel; $fillercell->colspan = $colspan; $fillercell->text = ' '; // This is a filler cell; don't use a <th>, it'll confuse screen readers. $fillercell->header = false; $headingrow->cells[] = $fillercell; } else { if ($type == 'category') { // Make sure the grade category has a grade total or at least has child grade items. if (grade_tree::can_output_item($element)) { // Element is a category. $categorycell = new html_table_cell(); $categorycell->attributes['class'] = 'category ' . $catlevel; $categorycell->colspan = $colspan; $categorycell->text = $this->get_course_header($element); $categorycell->header = true; $categorycell->scope = 'col'; // Print icons. if ($USER->gradeediting[$this->courseid]) { $categorycell->text .= $this->get_icons($element); } $headingrow->cells[] = $categorycell; } } else { // Element is a grade_item if ($element['object']->id == $this->sortitemid) { if ($this->sortorder == 'ASC') { $arrow = $this->get_sort_arrow('up', $sortlink); } else { $arrow = $this->get_sort_arrow('down', $sortlink); } } else { $arrow = $this->get_sort_arrow('move', $sortlink); } $headerlink = $this->gtree->get_element_header($element, true, $showactivityicons, false, false, true); $itemcell = new html_table_cell(); $itemcell->attributes['class'] = $type . ' ' . $catlevel . ' highlightable' . ' i' . $element['object']->id; $itemcell->attributes['data-itemid'] = $element['object']->id; if ($element['object']->is_hidden()) { $itemcell->attributes['class'] .= ' dimmed_text'; } $singleview = ''; // FIXME: MDL-52678 This is extremely hacky we should have an API for inserting grade column links. if (get_capability_info('gradereport/singleview:view')) { if (has_all_capabilities(array('gradereport/singleview:view', 'moodle/grade:viewall', 'moodle/grade:edit'), $this->context)) { $url = new moodle_url('/grade/report/singleview/index.php', array('id' => $this->course->id, 'item' => 'grade', 'itemid' => $element['object']->id)); $singleview = $OUTPUT->action_icon($url, new pix_icon('t/editstring', get_string('singleview', 'grades', $element['object']->get_name()))); } } $itemcell->colspan = $colspan; $itemcell->text = shorten_text($headerlink) . $arrow . $singleview; $itemcell->header = true; $itemcell->scope = 'col'; $headingrow->cells[] = $itemcell; } } } $rows[] = $headingrow; } $rows = $this->get_right_icons_row($rows); // Preload scale objects for items with a scaleid and initialize tab indices $scaleslist = array(); $tabindices = array(); foreach ($this->gtree->get_items() as $itemid => $item) { $scale = null; if (!empty($item->scaleid)) { $scaleslist[] = $item->scaleid; $jsarguments['items'][$itemid] = array('id' => $itemid, 'name' => $item->get_name(true), 'type' => 'scale', 'scale' => $item->scaleid, 'decimals' => $item->get_decimals()); } else { $jsarguments['items'][$itemid] = array('id' => $itemid, 'name' => $item->get_name(true), 'type' => 'value', 'scale' => false, 'decimals' => $item->get_decimals()); } $tabindices[$item->id]['grade'] = $gradetabindex; $tabindices[$item->id]['feedback'] = $gradetabindex + $numusers; $gradetabindex += $numusers * 2; } $scalesarray = array(); if (!empty($scaleslist)) { $scalesarray = $DB->get_records_list('scale', 'id', $scaleslist); } $jsscales = $scalesarray; // Get all the grade items if the user can not view hidden grade items. // It is possible that the user is simply viewing the 'Course total' by switching to the 'Aggregates only' view // and that this user does not have the ability to view hidden items. In this case we still need to pass all the // grade items (in case one has been hidden) as the course total shown needs to be adjusted for this particular // user. if (!$this->canviewhidden) { $allgradeitems = grade_item::fetch_all(array('courseid' => $this->courseid)); } foreach ($this->users as $userid => $user) { if ($this->canviewhidden) { $altered = array(); $unknown = array(); } else { $usergrades = $this->allgrades[$userid]; $hidingaffected = grade_grade::get_hiding_affected($usergrades, $allgradeitems); $altered = $hidingaffected['altered']; $unknown = $hidingaffected['unknown']; unset($hidingaffected); } $itemrow = new html_table_row(); $itemrow->id = 'user_' . $userid; $fullname = fullname($user); $jsarguments['users'][$userid] = $fullname; foreach ($this->gtree->items as $itemid => $unused) { $item =& $this->gtree->items[$itemid]; $grade = $this->grades[$userid][$item->id]; $itemcell = new html_table_cell(); $itemcell->id = 'u' . $userid . 'i' . $itemid; $itemcell->attributes['data-itemid'] = $itemid; // Get the decimal points preference for this item $decimalpoints = $item->get_decimals(); if (in_array($itemid, $unknown)) { $gradeval = null; } else { if (array_key_exists($itemid, $altered)) { $gradeval = $altered[$itemid]; } else { $gradeval = $grade->finalgrade; } } if (!empty($grade->finalgrade)) { $gradevalforjs = null; if ($item->scaleid && !empty($scalesarray[$item->scaleid])) { $gradevalforjs = (int) $gradeval; } else { $gradevalforjs = format_float($gradeval, $decimalpoints); } $jsarguments['grades'][] = array('user' => $userid, 'item' => $itemid, 'grade' => $gradevalforjs); } // MDL-11274 // Hide grades in the grader report if the current grader doesn't have 'moodle/grade:viewhidden' if (!$this->canviewhidden and $grade->is_hidden()) { if (!empty($CFG->grade_hiddenasdate) and $grade->get_datesubmitted() and !$item->is_category_item() and !$item->is_course_item()) { // the problem here is that we do not have the time when grade value was modified, 'timemodified' is general modification date for grade_grades records $itemcell->text = "<span class='datesubmitted'>" . userdate($grade->get_datesubmitted(), $strftimedatetimeshort) . "</span>"; } else { $itemcell->text = '-'; } $itemrow->cells[] = $itemcell; continue; } // emulate grade element $eid = $this->gtree->get_grade_eid($grade); $element = array('eid' => $eid, 'object' => $grade, 'type' => 'grade'); $itemcell->attributes['class'] .= ' grade i' . $itemid; if ($item->is_category_item()) { $itemcell->attributes['class'] .= ' cat'; } if ($item->is_course_item()) { $itemcell->attributes['class'] .= ' course'; } if ($grade->is_overridden()) { $itemcell->attributes['class'] .= ' overridden'; $itemcell->attributes['aria-label'] = get_string('overriddengrade', 'gradereport_grader'); } if (!empty($grade->feedback)) { $feedback = wordwrap(trim(format_string($grade->feedback, $grade->feedbackformat)), 34, '<br>'); $itemcell->attributes['data-feedback'] = $feedback; $jsarguments['feedback'][] = array('user' => $userid, 'item' => $itemid, 'content' => $feedback); } if ($grade->is_excluded()) { // Adding white spaces before and after to prevent a screenreader from // thinking that the words are attached to the next/previous <span> or text. $itemcell->text .= " <span class='excludedfloater'>" . $strexcludedgrades . "</span> "; } // Do not show any icons if no grade (no record in DB to match) if (!$item->needsupdate and $USER->gradeediting[$this->courseid]) { $itemcell->text .= $this->get_icons($element); } $hidden = ''; if ($grade->is_hidden()) { $hidden = ' dimmed_text '; } $gradepass = '******'; if ($grade->is_passed($item)) { $gradepass = '******'; } else { if (is_null($grade->is_passed($item))) { $gradepass = ''; } } // if in editing mode, we need to print either a text box // or a drop down (for scales) // grades in item of type grade category or course are not directly editable if ($item->needsupdate) { $itemcell->text .= "<span class='gradingerror{$hidden}'>" . $strerror . "</span>"; } else { if ($USER->gradeediting[$this->courseid]) { if ($item->scaleid && !empty($scalesarray[$item->scaleid])) { $itemcell->attributes['class'] .= ' grade_type_scale'; } else { if ($item->gradetype == GRADE_TYPE_VALUE) { $itemcell->attributes['class'] .= ' grade_type_value'; } else { if ($item->gradetype == GRADE_TYPE_TEXT) { $itemcell->attributes['class'] .= ' grade_type_text'; } } } if ($item->scaleid && !empty($scalesarray[$item->scaleid])) { $scale = $scalesarray[$item->scaleid]; $gradeval = (int) $gradeval; // scales use only integers $scales = explode(",", $scale->scale); // reindex because scale is off 1 // MDL-12104 some previous scales might have taken up part of the array // so this needs to be reset $scaleopt = array(); $i = 0; foreach ($scales as $scaleoption) { $i++; $scaleopt[$i] = $scaleoption; } if ($quickgrading and $grade->is_editable()) { $oldval = empty($gradeval) ? -1 : $gradeval; if (empty($item->outcomeid)) { $nogradestr = $this->get_lang_string('nograde'); } else { $nogradestr = $this->get_lang_string('nooutcome', 'grades'); } $attributes = array('tabindex' => $tabindices[$item->id]['grade'], 'id' => 'grade_' . $userid . '_' . $item->id); $gradelabel = $fullname . ' ' . $item->itemname; $itemcell->text .= html_writer::label(get_string('useractivitygrade', 'gradereport_grader', $gradelabel), $attributes['id'], false, array('class' => 'accesshide')); $itemcell->text .= html_writer::select($scaleopt, 'grade[' . $userid . '][' . $item->id . ']', $gradeval, array(-1 => $nogradestr), $attributes); } else { if (!empty($scale)) { $scales = explode(",", $scale->scale); // invalid grade if gradeval < 1 if ($gradeval < 1) { $itemcell->text .= "<span class='gradevalue{$hidden}{$gradepass}'>-</span>"; } else { $gradeval = $grade->grade_item->bounded_grade($gradeval); //just in case somebody changes scale $itemcell->text .= "<span class='gradevalue{$hidden}{$gradepass}'>{$scales[$gradeval - 1]}</span>"; } } } } else { if ($item->gradetype != GRADE_TYPE_TEXT) { // Value type if ($quickgrading and $grade->is_editable()) { $value = format_float($gradeval, $decimalpoints); $gradelabel = $fullname . ' ' . $item->itemname; $itemcell->text .= '<label class="accesshide" for="grade_' . $userid . '_' . $item->id . '">' . get_string('useractivitygrade', 'gradereport_grader', $gradelabel) . '</label>'; $itemcell->text .= '<input size="6" tabindex="' . $tabindices[$item->id]['grade'] . '" type="text" class="text" title="' . $strgrade . '" name="grade[' . $userid . '][' . $item->id . ']" id="grade_' . $userid . '_' . $item->id . '" value="' . $value . '" />'; } else { $itemcell->text .= "<span class='gradevalue{$hidden}{$gradepass}'>" . format_float($gradeval, $decimalpoints) . "</span>"; } } } // If quickfeedback is on, print an input element if ($showquickfeedback and $grade->is_editable()) { $feedbacklabel = $fullname . ' ' . $item->itemname; $itemcell->text .= '<label class="accesshide" for="feedback_' . $userid . '_' . $item->id . '">' . get_string('useractivityfeedback', 'gradereport_grader', $feedbacklabel) . '</label>'; $itemcell->text .= '<input class="quickfeedback" tabindex="' . $tabindices[$item->id]['feedback'] . '" id="feedback_' . $userid . '_' . $item->id . '" size="6" title="' . $strfeedback . '" type="text" name="feedback[' . $userid . '][' . $item->id . ']" value="' . s($grade->feedback) . '" />'; } } else { // Not editing $gradedisplaytype = $item->get_displaytype(); if ($item->scaleid && !empty($scalesarray[$item->scaleid])) { $itemcell->attributes['class'] .= ' grade_type_scale'; } else { if ($item->gradetype == GRADE_TYPE_VALUE) { $itemcell->attributes['class'] .= ' grade_type_value'; } else { if ($item->gradetype == GRADE_TYPE_TEXT) { $itemcell->attributes['class'] .= ' grade_type_text'; } } } // Only allow edting if the grade is editable (not locked, not in a unoverridable category, etc). if ($enableajax && $grade->is_editable()) { // If a grade item is type text, and we don't have show quick feedback on, it can't be edited. if ($item->gradetype != GRADE_TYPE_TEXT || $showquickfeedback) { $itemcell->attributes['class'] .= ' clickable'; } } if ($item->needsupdate) { $itemcell->text .= "<span class='gradingerror{$hidden}{$gradepass}'>" . $error . "</span>"; } else { // The max and min for an aggregation may be different to the grade_item. if (!is_null($gradeval)) { $item->grademax = $grade->get_grade_max(); $item->grademin = $grade->get_grade_min(); } $itemcell->text .= "<span class='gradevalue{$hidden}{$gradepass}'>" . grade_format_gradevalue($gradeval, $item, true, $gradedisplaytype, null) . "</span>"; if ($showanalysisicon) { $itemcell->text .= $this->gtree->get_grade_analysis_icon($grade); } } } } // Enable keyboard navigation if the grade is editable (not locked, not in a unoverridable category, etc). if ($enableajax && $grade->is_editable()) { // If a grade item is type text, and we don't have show quick feedback on, it can't be edited. if ($item->gradetype != GRADE_TYPE_TEXT || $showquickfeedback) { $itemcell->attributes['class'] .= ' gbnavigable'; } } if (!empty($this->gradeserror[$item->id][$userid])) { $itemcell->text .= $this->gradeserror[$item->id][$userid]; } $itemrow->cells[] = $itemcell; } $rows[] = $itemrow; } if ($enableajax) { $jsarguments['cfg']['ajaxenabled'] = true; $jsarguments['cfg']['scales'] = array(); foreach ($jsscales as $scale) { // Trim the scale values, as they may have a space that is ommitted from values later. $jsarguments['cfg']['scales'][$scale->id] = array_map('trim', explode(',', $scale->scale)); } $jsarguments['cfg']['feedbacktrunclength'] = $this->feedback_trunc_length; // Student grades and feedback are already at $jsarguments['feedback'] and $jsarguments['grades'] } $jsarguments['cfg']['isediting'] = (bool) $USER->gradeediting[$this->courseid]; $jsarguments['cfg']['courseid'] = $this->courseid; $jsarguments['cfg']['studentsperpage'] = $this->get_students_per_page(); $jsarguments['cfg']['showquickfeedback'] = (bool) $showquickfeedback; $module = array('name' => 'gradereport_grader', 'fullpath' => '/grade/report/grader/module.js', 'requires' => array('base', 'dom', 'event', 'event-mouseenter', 'event-key', 'io-queue', 'json-parse', 'overlay')); $PAGE->requires->js_init_call('M.gradereport_grader.init_report', $jsarguments, false, $module); $PAGE->requires->strings_for_js(array('addfeedback', 'feedback', 'grade'), 'grades'); $PAGE->requires->strings_for_js(array('ajaxchoosescale', 'ajaxclicktoclose', 'ajaxerror', 'ajaxfailedupdate', 'ajaxfieldchanged'), 'gradereport_grader'); if (!$enableajax && $USER->gradeediting[$this->courseid]) { $PAGE->requires->yui_module('moodle-core-formchangechecker', 'M.core_formchangechecker.init', array(array('formid' => 'gradereport_grader'))); $PAGE->requires->string_for_js('changesmadereallygoaway', 'moodle'); } $rows = $this->get_right_range_row($rows); if ($displayaverages) { $rows = $this->get_right_avg_row($rows, true); $rows = $this->get_right_avg_row($rows); } return $rows; }
/** * Test can_output_item. */ public function test_can_output_item() { $this->resetAfterTest(); $generator = $this->getDataGenerator(); // Course level grade category. $course = $generator->create_course(); // Grade tree looks something like: // - Test course (Rendered). $gradetree = grade_category::fetch_course_tree($course->id); $this->assertTrue(grade_tree::can_output_item($gradetree)); // Add a grade category with default settings. $generator->create_grade_category(array('courseid' => $course->id)); // Grade tree now looks something like: // - Test course n (Rendered). // -- Grade category n (Rendered). $gradetree = grade_category::fetch_course_tree($course->id); $this->assertNotEmpty($gradetree['children']); foreach ($gradetree['children'] as $child) { $this->assertTrue(grade_tree::can_output_item($child)); } // Add a grade category with grade type = None. $nototalcategory = 'No total category'; $nototalparams = ['courseid' => $course->id, 'fullname' => $nototalcategory, 'aggregation' => GRADE_AGGREGATE_WEIGHTED_MEAN]; $nototal = $generator->create_grade_category($nototalparams); $catnototal = grade_category::fetch(array('id' => $nototal->id)); // Set the grade type of the grade item associated to the grade category. $catitemnototal = $catnototal->load_grade_item(); $catitemnototal->gradetype = GRADE_TYPE_NONE; $catitemnototal->update(); // Grade tree looks something like: // - Test course n (Rendered). // -- Grade category n (Rendered). // -- No total category (Not rendered). $gradetree = grade_category::fetch_course_tree($course->id); foreach ($gradetree['children'] as $child) { if ($child['object']->fullname == $nototalcategory) { $this->assertFalse(grade_tree::can_output_item($child)); } else { $this->assertTrue(grade_tree::can_output_item($child)); } } // Add another grade category with default settings under 'No total category'. $normalinnototalparams = ['courseid' => $course->id, 'fullname' => 'Normal category in no total category', 'parent' => $nototal->id]; $generator->create_grade_category($normalinnototalparams); // Grade tree looks something like: // - Test course n (Rendered). // -- Grade category n (Rendered). // -- No total category (Rendered). // --- Normal category in no total category (Rendered). $gradetree = grade_category::fetch_course_tree($course->id); foreach ($gradetree['children'] as $child) { // All children are now visible. $this->assertTrue(grade_tree::can_output_item($child)); if (!empty($child['children'])) { foreach ($child['children'] as $grandchild) { $this->assertTrue(grade_tree::can_output_item($grandchild)); } } } // Add a grade category with grade type = None. $nototalcategory2 = 'No total category 2'; $nototal2params = ['courseid' => $course->id, 'fullname' => $nototalcategory2, 'aggregation' => GRADE_AGGREGATE_WEIGHTED_MEAN]; $nototal2 = $generator->create_grade_category($nototal2params); $catnototal2 = grade_category::fetch(array('id' => $nototal2->id)); // Set the grade type of the grade item associated to the grade category. $catitemnototal2 = $catnototal2->load_grade_item(); $catitemnototal2->gradetype = GRADE_TYPE_NONE; $catitemnototal2->update(); // Add a category with no total under 'No total category'. $nototalinnototalcategory = 'Category with no total in no total category'; $nototalinnototalparams = ['courseid' => $course->id, 'fullname' => $nototalinnototalcategory, 'aggregation' => GRADE_AGGREGATE_WEIGHTED_MEAN, 'parent' => $nototal2->id]; $nototalinnototal = $generator->create_grade_category($nototalinnototalparams); $catnototalinnototal = grade_category::fetch(array('id' => $nototalinnototal->id)); // Set the grade type of the grade item associated to the grade category. $catitemnototalinnototal = $catnototalinnototal->load_grade_item(); $catitemnototalinnototal->gradetype = GRADE_TYPE_NONE; $catitemnototalinnototal->update(); // Grade tree looks something like: // - Test course n (Rendered). // -- Grade category n (Rendered). // -- No total category (Rendered). // --- Normal category in no total category (Rendered). // -- No total category 2 (Not rendered). // --- Category with no total in no total category (Not rendered). $gradetree = grade_category::fetch_course_tree($course->id); foreach ($gradetree['children'] as $child) { if ($child['object']->fullname == $nototalcategory2) { $this->assertFalse(grade_tree::can_output_item($child)); } else { $this->assertTrue(grade_tree::can_output_item($child)); } if (!empty($child['children'])) { foreach ($child['children'] as $grandchild) { if ($grandchild['object']->fullname == $nototalinnototalcategory) { $this->assertFalse(grade_tree::can_output_item($grandchild)); } else { $this->assertTrue(grade_tree::can_output_item($grandchild)); } } } } }