From eaf4a6d4256813aaba3ef19f85424160dbcd5fac Mon Sep 17 00:00:00 2001 From: Conn Warwicker Date: Fri, 16 Jan 2026 15:00:05 +0000 Subject: [PATCH] MDL-86006 mod_assign: Display markers on grading table --- public/mod/assign/gradingtable.php | 258 ++++++++++++++++++++++----- public/mod/assign/lang/en/assign.php | 3 + 2 files changed, 218 insertions(+), 43 deletions(-) diff --git a/public/mod/assign/gradingtable.php b/public/mod/assign/gradingtable.php index 6e9dfae70b2..705eaefa2a0 100644 --- a/public/mod/assign/gradingtable.php +++ b/public/mod/assign/gradingtable.php @@ -456,14 +456,21 @@ class assign_grading_table extends table_sql implements renderable { $columns[] = 'team'; $headers[] = get_string('submissionteam', 'assign'); } - // Allocated marker. + + // Multiple markers. if ($this->assignment->get_instance()->markingworkflow && $this->assignment->get_instance()->markingallocation && - has_capability('mod/assign:manageallocations', $this->assignment->get_context())) { - // Add a column for the allocated marker. - $columns[] = 'allocatedmarker'; - $headers[] = get_string('marker', 'assign'); + property_exists( + $assignment->get_instance(), + 'markercount', + ) + ) { + for ($i = 1; $i <= $assignment->get_instance()->markercount; $i++) { + $columns[] = "marker$i"; + $headers[] = get_string('markernumber', 'assign', $i); + } } + // Grade. $columns[] = 'grade'; $headers[] = get_string('gradenoun'); @@ -668,15 +675,6 @@ class assign_grading_table extends table_sql implements renderable { $o .= html_writer::div($allworkflowstates[$workflowstate]); } else { $o .= html_writer::select($workflowstates, $name, $workflowstate, ['' => $notmarked]); - // Check if this user is a marker that can't manage allocations and doesn't have the marker column added. - if ($this->assignment->get_instance()->markingworkflow && - $this->assignment->get_instance()->markingallocation && - !has_capability('mod/assign:manageallocations', $this->assignment->get_context())) { - - $name = 'quickgrade_' . $row->id . '_allocatedmarker'; - $o .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => $name, - 'value' => $row->allocatedmarker]); - } } } else { $o .= $this->output->container(get_string('markingworkflowstate' . $workflowstate, 'assign'), $workflowstate); @@ -699,56 +697,102 @@ class assign_grading_table extends table_sql implements renderable { return get_string('markingworkflowstate' . $state, 'assign'); } + /** + * Get the user object for the marker of a given student and marker number. + * + * @param int $studentid ID of the student. + * @param int $number Marker index number. + * @return stdClass|false User object or false if not found. + */ + protected function get_marker_number(int $studentid, int $number): stdClass|false { + global $DB; + $multimarkers = $DB->get_fieldset('assign_allocated_marker', 'marker', [ + 'student' => $studentid, 'assignment' => $this->assignment->get_instance()->id, + ]); + if (!empty($multimarkers) && count($multimarkers) >= $number) { + // Then get the name of the one at the column position requested, e.g. marker1, marker2, etc... + $index = $number - 1; + return \core_user::get_user($multimarkers[$index]); + } + return false; + } + /** * list current marker * * @param stdClass $row - The row of data - * @return id the user->id of the marker. + * @param int $markerpos - Marker position number - related to the markercount + * @return string The name of the allocated marker */ - public function col_allocatedmarker(stdClass $row) { + public function col_allocatedmarker(stdClass $row, int $markerpos = 1) { static $markers = null; static $markerlist = array(); - if ($markers === null) { - list($sort, $params) = users_order_by_sql('u'); - // Only enrolled users could be assigned as potential markers. - $markers = get_enrolled_users($this->assignment->get_context(), 'mod/assign:grade', 0, 'u.*', $sort); - $markerlist[0] = get_string('choosemarker', 'assign'); - $viewfullnames = has_capability('moodle/site:viewfullnames', $this->assignment->get_context()); - foreach ($markers as $marker) { - $markerlist[$marker->id] = fullname($marker, $viewfullnames); + + // Get the allocated markers that have been assigned to this student, if we are using multi-marking. + $allocatedmarker = $this->get_marker_number($row->userid, $markerpos); + + if ($this->is_downloading()) { + if ($allocatedmarker) { + return fullname( + $allocatedmarker, + has_capability('moodle/site:viewfullnames', $this->assignment->get_context()) + ); } - } - if (empty($markerlist)) { - // TODO: add some form of notification here that no markers are available. return ''; } - if ($this->is_downloading()) { - if (isset($markers[$row->allocatedmarker])) { - return fullname($markers[$row->allocatedmarker], - has_capability('moodle/site:viewfullnames', $this->assignment->get_context())); - } else { + + if ( + $this->quickgrading && + has_capability('mod/assign:manageallocations', $this->assignment->get_context()) && + ( + empty($row->workflowstate) || + $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INMARKING || + $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED + ) + ) { + // Get the potential users who could be assigned as an allocated marker. + if ($markers === null) { + [$sort, $params] = users_order_by_sql('u'); + // Only enrolled users could be assigned as potential markers. + $markers = get_enrolled_users($this->assignment->get_context(), 'mod/assign:grade', 0, 'u.*', $sort); + $markerlist[0] = get_string('choosemarker', 'assign'); + $viewfullnames = has_capability('moodle/site:viewfullnames', $this->assignment->get_context()); + foreach ($markers as $marker) { + $markerlist[$marker->id] = fullname($marker, $viewfullnames); + } + } + + if (empty($markerlist)) { return ''; } + + if (count($markers) > 1) { + $label = get_string('allocatedmarker', 'assign') . ' ' . $markerpos; + } else { + $label = get_string('allocatedmarker', 'assign'); + } + + $name = 'quickgrade_' . $row->id . '_allocatedmarker_' . $markerpos; + return html_writer::label( + $label, + 'menu' . $name + ) . html_writer::select($markerlist, $name, ($allocatedmarker) ? $allocatedmarker->id : '', false); } - if ($this->quickgrading && has_capability('mod/assign:manageallocations', $this->assignment->get_context()) && - (empty($row->workflowstate) || - $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_INMARKING || - $row->workflowstate == ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED)) { - - $name = 'quickgrade_' . $row->id . '_allocatedmarker'; - return html_writer::select($markerlist, $name, $row->allocatedmarker, false); - } else if (!empty($row->allocatedmarker)) { + if ($allocatedmarker) { $output = ''; if ($this->quickgrading) { // Add hidden field for quickgrading page. - $name = 'quickgrade_' . $row->id . '_allocatedmarker'; - $attributes = ['type' => 'hidden', 'name' => $name, 'value' => $row->allocatedmarker]; + $name = 'quickgrade_' . $row->id . '_allocatedmarker_' . $markerpos; + $attributes = ['type' => 'hidden', 'name' => $name, 'value' => $allocatedmarker->id]; $output .= html_writer::empty_tag('input', $attributes); } - $output .= $markerlist[$row->allocatedmarker]; + $output .= html_writer::tag('strong', fullname($allocatedmarker)); return $output; } + + return ''; } + /** * For download only - list all the valid options for this custom scale. * @@ -1048,6 +1092,128 @@ class assign_grading_table extends table_sql implements renderable { return $displaygrade; } + /** + * Format the marker column's data for display. + * + * @param stdClass $row Row object. + * @param int $col Marker number column. + * @return string + */ + public function col_marker(stdClass $row, int $col): string { + global $USER, $DB; + $allocatedmarker = ""; + $index = $col - 1; + + if ( + $this->assignment->get_instance()->markingworkflow && + $this->assignment->get_instance()->markingallocation + ) { + $allocatedmarker = $this->col_allocatedmarker($row, $col); + } + + $gradingdisabled = $this->assignment->grading_disabled($row->id, true, $this->gradinginfo); + $displaymark = ""; + + if (!$this->is_downloading() && $this->hasgrade) { + if ( + $this->assignment->get_instance()->markingworkflow && + $this->assignment->get_instance()->markingallocation + ) { + // Allocated markers are enabled: get the mark corresponding to + // the marker for this column. + $markers = array_values($DB->get_records('assign_allocated_marker', [ + 'student' => $row->userid, + 'assignment' => $this->assignment->get_instance()->id, + ], 'id')); + if (count($markers) > $index) { + $mark = $DB->get_record('assign_mark', ['gradeid' => $row->gradeid, 'marker' => $markers[$index]->marker]); + // Mark is only editable if we are quick grading, grading is not disabled, and if we are either + // the marker for this column, or we have manageallocations permissions. + $editable = ( + ($this->quickgrading) && + (!$gradingdisabled) && + ($USER->id == $markers[$index]->marker) + ); + $displaymark = $this->display_grade( + $mark->mark ?? null, + $editable, + $row->userid, + $row->timemarked, + 0, + $markers[$index]->marker, + ); + // Display the workflow state for this mark. + if ($markers[$index]->marker > 0) { + $displaymark .= html_writer::div( + get_string('markingworkflowstate' . ($mark->workflowstate ?? 'notmarked'), 'assign'), + 'badge bg-info d-block' + ); + } + } + } + + $urlparams = [ + 'id' => $this->assignment->get_course_module()->id, + 'rownum' => 0, + 'action' => 'marker', + ]; + + if ($this->assignment->is_blind_marking()) { + if (empty($row->recordid)) { + $row->recordid = $this->assignment->get_uniqueid_for_user($row->userid); + } + $urlparams['blindid'] = $row->recordid; + } else { + $urlparams['userid'] = $row->userid; + } + $url = new moodle_url('/mod/assign/view.php', $urlparams); + + // The container with the grade information. + $gradecontainer = $this->output->container($displaymark, 'w-100'); + + // Should this user get the 'Mark' action menu item? + $isallocatedmarker = false; + + if ( + $this->assignment->get_instance()->markingworkflow && + $this->assignment->get_instance()->markingallocation + ) { + // If allocated marking is enabled is this user the marker for this column? + if ( + $markers = $DB->get_fieldset('assign_allocated_marker', 'marker', [ + 'student' => $row->userid, + 'assignment' => $this->assignment->get_instance()->id, + ]) + ) { + $isallocatedmarker = (array_key_exists($index, $markers) && $markers[$index] == $USER->id); + } + } + + if ($isallocatedmarker) { + $menu = new action_menu(); + $menu->set_owner_selector('.gradingtable-actionmenu'); + $menu->set_boundary('window'); + $menu->set_kebab_trigger(get_string('markactions', 'assign')); + $menu->set_additional_classes('ps-2 ms-auto'); + // Prioritise the menu ahead of all other actions. + $menu->prioritise = true; + // Add the 'Mark' action item to the contextual menu. + $menu->add(new action_menu_link_secondary($url, null, get_string('markverb', 'assign'))); + // The contextual menu container. + $contextualmenucontainer = $this->output->container($this->output->render($menu), 'd-flex'); + return $allocatedmarker . + $this->output->container($gradecontainer . $contextualmenucontainer, ['class' => 'd-flex']); + } + } + // The table data is being downloaded, or the user cannot grade; therefore, only the formatted grade for display + // is returned. + if ($this->is_downloading()) { + return $displaymark; + } else { + return $allocatedmarker . $this->output->container($gradecontainer, ['class' => 'd-flex']); + } + } + /** * Format a column of data for display. * @@ -1644,6 +1810,12 @@ class assign_grading_table extends table_sql implements renderable { * @return mixed string or NULL */ public function other_cols($colname, $row) { + // If the column name is formatted as "marker" and a number, e.g. "marker1", this is a Marker column + // and so we need to call col_marker() instead with the marker number provided in the name. In this case "1". + if (str_starts_with($colname, 'marker') && ($col = substr($colname, 6)) && ctype_digit($col)) { + return $this->col_marker($row, $col); + } + // For extra user fields the result is already in $row. if (empty($this->plugincache[$colname])) { return parent::other_cols($colname, $row); diff --git a/public/mod/assign/lang/en/assign.php b/public/mod/assign/lang/en/assign.php index 3101a5de8e9..025ad8f0ff4 100644 --- a/public/mod/assign/lang/en/assign.php +++ b/public/mod/assign/lang/en/assign.php @@ -384,11 +384,13 @@ $string['locksubmissionforstudent'] = 'Prevent any more submissions for student: $string['locksubmissions'] = 'Lock submissions'; $string['manageassignfeedbackplugins'] = 'Manage assignment feedback plugins'; $string['manageassignsubmissionplugins'] = 'Manage assignment submission plugins'; +$string['markactions'] = 'Mark actions'; $string['marker'] = 'Marker'; $string['markercount'] = 'Markers'; $string['markercount_help'] = 'Number of markers for the assignment.'; $string['markerfilter'] = 'Marker filter'; $string['markerfilternomarker'] = 'No marker'; +$string['markernumber'] = 'Marker {$a}'; $string['markgradeaverage'] = 'Average mark'; $string['markgradeaverage_help'] = 'Grade will be calculated as the average of all marks given.'; $string['markgrademanual'] = 'Manual'; @@ -417,6 +419,7 @@ $string['markingworkflowstatenotmarked'] = 'Not marked'; $string['markingworkflowstatereadyforreview'] = 'Marking completed'; $string['markingworkflowstatereadyforrelease'] = 'Ready for release'; $string['markingworkflowstatereleased'] = 'Released'; +$string['markverb'] = 'Mark'; $string['multimarkmethod'] = 'Calculate grade'; $string['multimarkmethod_help'] = 'How the grade will be calculated when there are multiple markers'; $string['multimarkrounding'] = 'Rounding';