This commit is contained in:
Huong Nguyen
2026-03-09 09:00:27 +07:00
46 changed files with 3018 additions and 317 deletions
@@ -0,0 +1,12 @@
define("mod_assign/batch_set_marking_workflow_state",["exports"],(function(_exports){Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;
/**
* Javascript controller for the "Actions" panel at the bottom of the page.
*
* @module mod_assign/batch_set_marking_workflow_state
* @copyright 2025 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Conn Warwicker <conn.warwicker@catalyst-eu.net>
*/
const filterOptions=(context,args)=>{const options=args[context];document.getElementById("id_markingworkflowstate").children.forEach((item=>{options.includes(item.value)?item.removeAttribute("disabled"):item.setAttribute("disabled","")}))};_exports.init=args=>{const workflowcontext=document.getElementById("id_workflowcontext");filterOptions(workflowcontext.value,args),workflowcontext.addEventListener("change",(e=>{filterOptions(e.target.value,args)}))}}));
//# sourceMappingURL=batch_set_marking_workflow_state.min.js.map
@@ -0,0 +1 @@
{"version":3,"file":"batch_set_marking_workflow_state.min.js","sources":["../src/batch_set_marking_workflow_state.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see <http://www.gnu.org/licenses/>.\n\n/**\n * Javascript controller for the \"Actions\" panel at the bottom of the page.\n *\n * @module mod_assign/batch_set_marking_workflow_state\n * @copyright 2025 onwards Catalyst IT {@link http://www.catalyst-eu.net/}\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n * @author Conn Warwicker <[email protected]>\n */\n\n/**\n * Filter which options are enabled in the select menu.\n *\n * @param {object} context\n * @param {array} args\n */\nconst filterOptions = (context, args) => {\n const options = args[context];\n const workflowstate = document.getElementById('id_markingworkflowstate');\n workflowstate.children.forEach((item) => {\n if (options.includes(item.value)) {\n item.removeAttribute('disabled');\n } else {\n item.setAttribute('disabled', '');\n }\n });\n};\n\n/**\n * Initialise scripts.\n *\n * @param {array} args\n */\nexport const init = (args) => {\n const workflowcontext = document.getElementById('id_workflowcontext');\n filterOptions(workflowcontext.value, args);\n workflowcontext.addEventListener('change', (e) => {\n filterOptions(e.target.value, args);\n });\n};"],"names":["filterOptions","context","args","options","document","getElementById","children","forEach","item","includes","value","removeAttribute","setAttribute","workflowcontext","addEventListener","e","target"],"mappings":";;;;;;;;;MA8BMA,cAAgB,CAACC,QAASC,cACtBC,QAAUD,KAAKD,SACCG,SAASC,eAAe,2BAChCC,SAASC,SAASC,OACxBL,QAAQM,SAASD,KAAKE,OACtBF,KAAKG,gBAAgB,YAErBH,KAAKI,aAAa,WAAY,sBAUrBV,aACXW,gBAAkBT,SAASC,eAAe,sBAChDL,cAAca,gBAAgBH,MAAOR,MACrCW,gBAAgBC,iBAAiB,UAAWC,IACxCf,cAAce,EAAEC,OAAON,MAAOR"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -5,6 +5,6 @@
* @copyright 2015 Damyon Wiese <damyon@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define("mod_assign/participant_selector",["core/ajax","jquery","core/templates"],(function(ajax,$,templates){return{processResults:function(selector,data){return data},transport:function(selector,query,success,failure){var assignmentid=$(selector).attr("data-assignmentid"),groupid=$(selector).attr("data-groupid"),filters=$('[data-region="configure-filters"] input[type="checkbox"]'),filterstrings=[];filters.each((function(index,element){filterstrings[$(element).attr("name")]=$(element).prop("checked")})),ajax.call([{methodname:"mod_assign_list_participants",args:{assignid:assignmentid,groupid:groupid,filter:query,limit:30,includeenrolments:!1,tablesort:!0}}])[0].then((function(results){var promises=[],identityfields=$("[data-showuseridentity]").data("showuseridentity").split(",");return $.each(results,(function(index,user){var ctx=user,identity=[],show=!0;filterstrings.filter_submitted&&!user.submitted&&(show=!1),filterstrings.filter_notsubmitted&&user.submitted&&(show=!1),filterstrings.filter_requiregrading&&!user.requiregrading&&(show=!1),filterstrings.filter_grantedextension&&!user.grantedextension&&(show=!1),show&&($.each(identityfields,(function(i,k){void 0!==user[k]&&""!==user[k]&&(ctx.hasidentity=!0,identity.push(user[k]))})),ctx.identity=identity.join(", "),promises.push(templates.render("mod_assign/list_participant_user_summary",ctx).then((function(html){return{value:user.id,label:html}}))))})),$.when.apply($,promises)})).then((function(){var users=[];arguments[0]&&(users=Array.prototype.slice.call(arguments)),success(users)})).catch(failure)}}}));
define("mod_assign/participant_selector",["core/ajax","core/templates"],(function(ajax,templates){return{processResults:function(selector,data){return data},transport:function(selector,query,success,failure){const element=document.querySelector(selector);var assignmentid=element.getAttribute("data-assignmentid"),groupid=element.getAttribute("data-groupid"),filters=document.querySelectorAll('[data-region="configure-filters"] input[type="checkbox"]'),filterstrings=[];filters.forEach((e=>{let filterelement=document.querySelector(e);filterstrings[filterelement.getAttribute("name")]=filterelement.checked}));var marking=element.getAttribute("data-ismarking");ajax.call([{methodname:"mod_assign_list_participants",args:{assignid:assignmentid,groupid:groupid,filter:query,limit:30,includeenrolments:!1,tablesort:!0,marking:marking}}])[0].then((function(results){var promises=[],identityfields=document.querySelector("[data-showuseridentity]").dataset.showuseridentity.split(",");return results.forEach((user=>{var ctx=user,identity=[],show=!0;filterstrings.filter_submitted&&!user.submitted&&(show=!1),filterstrings.filter_notsubmitted&&user.submitted&&(show=!1),filterstrings.filter_requiregrading&&!user.requiregrading&&(show=!1),filterstrings.filter_grantedextension&&!user.grantedextension&&(show=!1),show&&(identityfields.forEach((k=>{void 0!==user[k]&&""!==user[k]&&(ctx.hasidentity=!0,identity.push(user[k]))})),ctx.identity=identity.join(", "),promises.push(templates.render("mod_assign/list_participant_user_summary",ctx).then((function(html){return{value:user.id,label:html}}))))})),Promise.all(promises)})).then((function(users){success(users)})).catch(failure)}}}));
//# sourceMappingURL=participant_selector.min.js.map
File diff suppressed because one or more lines are too long
@@ -0,0 +1,54 @@
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Javascript controller for the "Actions" panel at the bottom of the page.
*
* @module mod_assign/batch_set_marking_workflow_state
* @copyright 2025 onwards Catalyst IT {@link http://www.catalyst-eu.net/}
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @author Conn Warwicker <[email protected]>
*/
/**
* Filter which options are enabled in the select menu.
*
* @param {object} context
* @param {array} args
*/
const filterOptions = (context, args) => {
const options = args[context];
const workflowstate = document.getElementById('id_markingworkflowstate');
workflowstate.children.forEach((item) => {
if (options.includes(item.value)) {
item.removeAttribute('disabled');
} else {
item.setAttribute('disabled', '');
}
});
};
/**
* Initialise scripts.
*
* @param {array} args
*/
export const init = (args) => {
const workflowcontext = document.getElementById('id_workflowcontext');
filterOptions(workflowcontext.value, args);
workflowcontext.addEventListener('change', (e) => {
filterOptions(e.target.value, args);
});
};
@@ -39,6 +39,7 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete',
this._filteredUsers = [];
this._lastXofYUpdate = 0;
this._firstLoadUsers = true;
this._isMarking = false;
let url = new URL(window.location);
if (parseInt(url.searchParams.get('treset')) > 0) {
@@ -133,10 +134,11 @@ define(['jquery', 'core/notification', 'core/str', 'core/form-autocomplete',
return false;
}
this._lastFilters = filter;
this._isMarking = select.attr('data-ismarking') ?? false;
ajax.call([{
methodname: 'mod_assign_list_participants',
args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true, tablesort: true},
args: {assignid: assignmentid, groupid: groupid, filter: '', onlyids: true, tablesort: true, marking: this._isMarking},
done: this._usersLoaded.bind(this),
fail: notification.exception
}]);
+9 -2
View File
@@ -154,10 +154,13 @@ define([
var data = form.serialize();
var assignmentid = this._region.attr('data-assignmentid');
var vars = window.location.search.substring(1).split('&');
var marker = vars.includes('action=marker');
// Now we can continue...
ajax.call([{
methodname: 'mod_assign_submit_grading_form',
args: {assignmentid: assignmentid, userid: this._lastUserId, jsonformdata: JSON.stringify(data)},
args: {assignmentid: assignmentid, userid: this._lastUserId, marker: marker, jsonformdata: JSON.stringify(data)},
done: this._handleFormSubmissionResponse.bind(this, data, nextUserId, nextUser),
fail: notification.exception
}]);
@@ -336,8 +339,12 @@ define([
this._niceReplaceNodeContents(this._region, html, js).done(function() {
if (userid > 0) {
this._region.show();
var vars = window.location.search.substring(1).split('&');
var marker = vars.includes('action=marker');
// Reload the grading form "fragment" for this user.
var params = {userid: userid, attemptnumber: attemptnumber, jsonformdata: JSON.stringify(submissiondata)};
var params = {userid: userid, attemptnumber: attemptnumber,
jsonformdata: JSON.stringify(submissiondata),
marker: marker};
fragment.loadFragment('mod_assign', 'gradingpanel', contextid, params).done(function(html, js) {
// Reset whole grading page when there is a failure in retrieving the html
@@ -20,7 +20,7 @@
* @copyright 2015 Damyon Wiese <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {
define(['core/ajax', 'core/templates'], function(ajax, templates) {
return /** @alias module:mod_assign/participants_selector */ {
@@ -48,15 +48,19 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {
* @param {Function} failure Failure handler
*/
transport: function(selector, query, success, failure) {
var assignmentid = $(selector).attr('data-assignmentid');
var groupid = $(selector).attr('data-groupid');
var filters = $('[data-region="configure-filters"] input[type="checkbox"]');
const element = document.querySelector(selector);
var assignmentid = element.getAttribute('data-assignmentid');
var groupid = element.getAttribute('data-groupid');
var filters = document.querySelectorAll('[data-region="configure-filters"] input[type="checkbox"]');
var filterstrings = [];
filters.each(function(index, element) {
filterstrings[$(element).attr('name')] = $(element).prop('checked');
filters.forEach((e) => {
let filterelement = document.querySelector(e);
filterstrings[filterelement.getAttribute('name')] = filterelement.checked;
});
var marking = element.getAttribute('data-ismarking');
ajax.call([{
methodname: 'mod_assign_list_participants',
args: {
@@ -65,14 +69,15 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {
filter: query,
limit: 30,
includeenrolments: false,
tablesort: true
tablesort: true,
marking: marking,
}
}])[0].then(function(results) {
var promises = [];
var identityfields = $('[data-showuseridentity]').data('showuseridentity').split(',');
var identityfields = document.querySelector('[data-showuseridentity]').dataset.showuseridentity.split(',');
// We got the results, now we loop over them and render each one from a template.
$.each(results, function(index, user) {
results.forEach((user) => {
var ctx = user,
identity = [],
show = true;
@@ -90,7 +95,7 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {
show = false;
}
if (show) {
$.each(identityfields, function(i, k) {
identityfields.forEach((k) => {
if (typeof user[k] !== 'undefined' && user[k] !== '') {
ctx.hasidentity = true;
identity.push(user[k]);
@@ -102,17 +107,8 @@ define(['core/ajax', 'jquery', 'core/templates'], function(ajax, $, templates) {
}));
}
});
// Do the dance for $.when()
return $.when.apply($, promises);
}).then(function() {
var users = [];
// Determine if we've been passed any arguments..
if (arguments[0]) {
// Undo the $.when() dance from arguments object into an array..
users = Array.prototype.slice.call(arguments);
}
return Promise.all(promises);
}).then(function(users) {
success(users);
return;
}).catch(failure);
@@ -91,6 +91,8 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
'maxattempts',
'markingworkflow',
'markingallocation',
'markercount',
'multimarkmethod',
'markinganonymous',
'preventsubmissionnotingroup',
'activity',
@@ -101,14 +103,30 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
$userflags = new backup_nested_element('userflags');
$userflag = new backup_nested_element('userflag', array('id'),
array('userid',
'assignment',
'mailed',
'locked',
'extensionduedate',
'workflowstate',
'allocatedmarker'));
$userflag = new backup_nested_element(
'userflag',
['id'],
[
'userid',
'assignment',
'mailed',
'locked',
'extensionduedate',
'workflowstate',
]
);
$allocatedmarkers = new backup_nested_element('allocatedmarkers');
$allocatedmarker = new backup_nested_element(
'allocatedmarker',
['id'],
[
'student',
'assignment',
'marker',
]
);
$submissions = new backup_nested_element('submissions');
@@ -133,6 +151,22 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
'penalty',
'attemptnumber'));
$marks = new backup_nested_element('marks');
$mark = new backup_nested_element(
'mark',
['id'],
[
'assignment',
'gradeid',
'timecreated',
'timemodified',
'marker',
'mark',
'workflowstate',
]
);
$pluginconfigs = new backup_nested_element('plugin_configs');
$pluginconfig = new backup_nested_element('plugin_config', array('id'),
@@ -148,10 +182,14 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
// Build the tree.
$assign->add_child($userflags);
$userflags->add_child($userflag);
$assign->add_child($allocatedmarkers);
$allocatedmarkers->add_child($allocatedmarker);
$assign->add_child($submissions);
$submissions->add_child($submission);
$assign->add_child($grades);
$grades->add_child($grade);
$assign->add_child($marks);
$marks->add_child($mark);
$assign->add_child($pluginconfigs);
$pluginconfigs->add_child($pluginconfig);
$assign->add_child($overrides);
@@ -172,6 +210,15 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
if ($userinfo) {
$userflag->set_source_table('assign_user_flags',
array('assignment' => backup::VAR_PARENTID));
$allocatedmarker->set_source_table(
'assign_allocated_marker',
['assignment' => backup::VAR_PARENTID]
);
$mark->set_source_table(
'assign_mark',
['assignment' => backup::VAR_PARENTID]
);
$submissionparams = array('assignment' => backup::VAR_PARENTID);
if (!$groupinfo) {
@@ -198,7 +245,9 @@ class backup_assign_activity_structure_step extends backup_activity_structure_st
// Define id annotations.
$userflag->annotate_ids('user', 'userid');
$userflag->annotate_ids('user', 'allocatedmarker');
$allocatedmarker->annotate_ids('user', 'student');
$allocatedmarker->annotate_ids('user', 'marker');
$mark->annotate_ids('user', 'marker');
$submission->annotate_ids('user', 'userid');
$submission->annotate_ids('group', 'groupid');
$grade->annotate_ids('user', 'userid');
@@ -71,6 +71,16 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
$userflag = new restore_path_element('assign_userflag',
'/activity/assign/userflags/userflag');
$paths[] = $userflag;
$allocatedmarker = new restore_path_element(
'assign_allocatedmarker',
'/activity/assign/allocatedmarkers/allocatedmarker'
);
$paths[] = $allocatedmarker;
$mark = new restore_path_element(
'assign_mark',
'/activity/assign/marks/mark'
);
$paths[] = $mark;
}
$paths[] = new restore_path_element('assign_override', '/activity/assign/overrides/override');
@@ -131,6 +141,9 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
if (!isset($data->markingallocation)) {
$data->markingallocation = 0;
}
if (!isset($data->markercount)) {
$data->markercount = 1;
}
if (!isset($data->markinganonymous)) {
$data->markinganonymous = 0;
}
@@ -213,9 +226,6 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
$data->assignment = $this->get_new_parentid('assign');
$data->userid = $this->get_mappingid('user', $data->userid);
if (!empty($data->allocatedmarker)) {
$data->allocatedmarker = $this->get_mappingid('user', $data->allocatedmarker);
}
if (!empty($data->extensionduedate)) {
$data->extensionduedate = $this->apply_date_offset($data->extensionduedate);
} else {
@@ -226,6 +236,40 @@ class restore_assign_activity_structure_step extends restore_activity_structure_
$newitemid = $DB->insert_record('assign_user_flags', $data);
}
/**
* Process an allocated_marker restore.
*
* @param object $data The data in object form.
*/
protected function process_assign_allocatedmarker($data): void {
global $DB;
$data = (object)$data;
$data->assignment = $this->get_new_parentid('assign');
$data->student = $this->get_mappingid('user', $data->student);
$data->marker = $this->get_mappingid('user', $data->marker);
$DB->insert_record('assign_allocated_marker', $data);
}
/**
* Process mark restore.
*
* @param object $data The data in object form.
*/
protected function process_assign_mark($data): void {
global $DB;
$data = (object)$data;
$data->assignment = $this->get_new_parentid('assign');
$data->marker = $this->get_mappingid('user', $data->marker);
$data->gradeid = $this->get_mappingid('grade', $data->gradeid);
$DB->insert_record('assign_mark', $data);
}
/**
* Process a grade restore
* @param object $data The data in object form
@@ -22,6 +22,10 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
defined('MOODLE_INTERNAL') || die();
require_once($CFG->libdir . '/formslib.php');
require_once($CFG->dirroot . '/mod/assign/feedback/file/locallib.php');
/**
* Set allocated marker form.
*
@@ -32,6 +36,7 @@
class mod_assign_batch_set_allocatedmarker_form extends moodleform {
/**
* Define this form - called by the parent constructor
* @throws moodle_exception
*/
public function definition() {
$mform = $this->_form;
@@ -41,7 +46,23 @@ class mod_assign_batch_set_allocatedmarker_form extends moodleform {
$mform->addElement('static', 'userslist', get_string('selectedusers', 'assign'), $params['usershtml']);
$options = $params['markers'];
$mform->addElement('select', 'allocatedmarker', get_string('allocatedmarker', 'assign'), $options);
$markercount = (!empty($params['markercount'])) ? $params['markercount'] : 1;
$markerids = array_keys($options);
// If we do not have enough markers to meet the requested number, throw an exception with a meaningful message.
if (count($markerids) < $markercount) {
throw new \core\exception\moodle_exception('invalidmarkerallocation:notenoughmarkers', 'assign', '', [
'markers' => count($markerids),
'requested' => $markercount,
]);
}
$options = ['' => get_string('choosemarker', 'assign')] + $options;
for ($i = 1; $i <= $markercount; $i++) {
$mform->addElement('select', "allocatedmarker{$i}", get_string('allocatedmarker', 'assign') . ' ' . $i, $options);
}
$mform->addElement('hidden', 'id');
$mform->setType('id', PARAM_INT);
@@ -34,6 +34,7 @@ class mod_assign_batch_set_marking_workflow_state_form extends moodleform {
* Define this form - called by the parent constructor
*/
public function definition() {
global $PAGE;
$mform = $this->_form;
$params = $this->_customdata;
$formheader = get_string('batchsetmarkingworkflowstateforusers', 'assign', $params['userscount']);
@@ -41,8 +42,38 @@ class mod_assign_batch_set_marking_workflow_state_form extends moodleform {
$mform->addElement('header', 'general', $formheader);
$mform->addElement('static', 'userslist', get_string('selectedusers', 'assign'), $params['usershtml']);
$options = $params['markingworkflowstates'];
$mform->addElement('select', 'markingworkflowstate', get_string('markingworkflowstate', 'assign'), $options);
$states = $params['markingworkflowstates'];
// If the assignment is using multi marking, do we want to set this workflow as the overall workflow for the submissions?
// Or for our allocated mark on them?
if ($params['assignment'] && $params['assignment']->is_using_multiple_marking()) {
$options = new core\output\choicelist();
$options->add_option(
'mark',
get_string('markverb', 'assign'),
[
'description' => get_string('workflowcontext_help_mark', 'assign'),
],
);
$options->add_option(
'grade',
get_string('gradenoun'),
[
'description' => get_string('workflowcontext_help_grade', 'assign'),
],
);
$mform->addElement('choicedropdown', 'workflowcontext', get_string('workflowcontext', 'assign'), $options);
$PAGE->requires->js_call_amd('mod_assign/batch_set_marking_workflow_state', 'init', [[
'mark' => [
ASSIGN_MARKING_WORKFLOW_STATE_NOTMARKED,
ASSIGN_MARKING_WORKFLOW_STATE_INMARKING,
ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW,
],
'grade' => array_keys($states),
]]);
}
$mform->addElement('select', 'markingworkflowstate', get_string('markingworkflowstate', 'assign'), $states);
// Don't allow notification to be sent until in "Released" state.
$mform->addElement('selectyesno', 'sendstudentnotifications', get_string('sendstudentnotifications', 'assign'));
@@ -30,15 +30,20 @@ use moodle_url;
class actionmenu implements templatable, renderable {
/** @var int The course module ID. */
private $cmid;
private int $cmid;
/** @var bool Are we using marking workflow? */
private bool $markingworkflow;
/**
* Constructor for this object.
*
* @param int $cmid The course module ID.
* @param bool $markingworkflow Are we using marking workflow?
*/
public function __construct(int $cmid) {
public function __construct(int $cmid, bool $markingworkflow) {
$this->cmid = $cmid;
$this->markingworkflow = $markingworkflow;
}
/**
@@ -51,8 +56,14 @@ class actionmenu implements templatable, renderable {
$return = [];
if (has_capability('mod/assign:grade', \context_module::instance($this->cmid))) {
$gradelink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'grader']);
$return['gradelink'] = $gradelink->out(false);
// If we are using multiple markers, then we'll have marker columns which need links to the marker page.
if ($this->markingworkflow) {
$gradelink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'marker']);
$return['marklink'] = $gradelink->out(false);
} else {
$gradelink = new moodle_url('/mod/assign/view.php', ['id' => $this->cmid, 'action' => 'grader']);
$return['gradelink'] = $gradelink->out(false);
}
}
return $return;
@@ -61,18 +61,27 @@ class grading_app implements templatable, renderable {
*/
public $participants = [];
/**
* @var bool - True to show fields for marking, false to show fields for
* grading (used when there are multiple markers).
*/
public $marker = false;
/**
* Constructor for this renderable.
*
* @param int $userid The user we will open the grading app too.
* @param int $groupid If groups are enabled this is the current course group.
* @param \assign $assignment The assignment class
* @param bool $marker If assignment instance uses multiple markers $marker
* is true to show fields for marking (as opposed to grading).
*/
public function __construct($userid, $groupid, $assignment) {
public function __construct($userid, $groupid, $assignment, $marker = false) {
$this->userid = $userid;
$this->groupid = $groupid;
$this->assignment = $assignment;
$this->participants = $assignment->list_participants_with_filter_status_and_group($groupid);
$this->marker = $marker;
if (!$this->userid && count($this->participants)) {
$this->userid = reset($this->participants)->id;
}
@@ -101,6 +110,7 @@ class grading_app implements templatable, renderable {
$export->hasmarkingworkflow = count($export->markingworkflowfilters) > 0;
$export->markingallocationfilters = $this->assignment->get_marking_allocation_filters(true);
$export->hasmarkingallocation = count($export->markingallocationfilters) > 0;
$export->ismarking = (int)$this->marker;
$num = 1;
foreach ($this->participants as $idx => $record) {
@@ -108,11 +108,23 @@ class provider implements
'assignment' => 'privacy:metadata:assignmentid',
'userid' => 'privacy:metadata:userid'
];
$assignmark = [
'assignment' => 'privacy:metadata:assignmentid',
'gradeid' => 'privacy:metadata:grade',
'marker' => 'privacy:metadata:userid',
];
$assignallocatedusers = [
'assignment' => 'privacy:metadata:assignmentid',
'studentid' => 'privacy:metadata:userid',
'marker' => 'privacy:metadata:userid',
];
$collection->add_database_table('assign_grades', $assigngrades, 'privacy:metadata:assigngrades');
$collection->add_database_table('assign_overrides', $assignoverrides, 'privacy:metadata:assignoverrides');
$collection->add_database_table('assign_submission', $assignsubmission, 'privacy:metadata:assignsubmissiondetail');
$collection->add_database_table('assign_user_flags', $assignuserflags, 'privacy:metadata:assignuserflags');
$collection->add_database_table('assign_user_mapping', $assignusermapping, 'privacy:metadata:assignusermapping');
$collection->add_database_table('assign_mark', $assignmark, 'privacy:metadata:assignmark');
$collection->add_database_table('assign_allocated_marker', $assignallocatedusers, 'privacy:metadata:assignallocatedmarker');
$collection->add_user_preference('assign_perpage', 'privacy:metadata:assignperpage');
$collection->add_user_preference('assign_filter', 'privacy:metadata:assignfilter');
$collection->add_user_preference('assign_markerfilter', 'privacy:metadata:assignmarkerfilter');
@@ -194,6 +206,37 @@ class provider implements
$contextlist->add_from_sql($sql, $params);
// Contexts where the given user has assignment marks.
$sql = "SELECT ctx.id
FROM {course_modules} cm
JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
JOIN {assign} a ON cm.instance = a.id
JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
JOIN {assign_grades} ag ON a.id = ag.assignment
JOIN {assign_mark} am ON am.gradeid = ag.id
WHERE ag.userid = :userid";
$contextlist->add_from_sql($sql, $params);
// Contexts where the given user has assigned assignment marks.
$sql = "SELECT ctx.id
FROM {course_modules} cm
JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
JOIN {assign} a ON cm.instance = a.id
JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
JOIN {assign_mark} am ON am.assignment = a.id
WHERE am.marker = :userid";
$contextlist->add_from_sql($sql, $params);
// Contexts where the given user is either an allocated marker or has an allocated marker.
$sql = "SELECT ctx.id
FROM {course_modules} cm
JOIN {modules} m ON cm.module = m.id AND m.name = :modulename
JOIN {assign} a ON cm.instance = a.id
JOIN {context} ctx ON cm.id = ctx.instanceid AND ctx.contextlevel = :contextlevel
JOIN {assign_allocated_marker} am ON am.assignment = a.id
WHERE am.marker = :graderid OR am.student = :userid";
$contextlist->add_from_sql($sql, $params);
manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_INTERFACE,
'get_context_for_userid_within_feedback', [$userid, $contextlist]);
manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_INTERFACE,
@@ -266,6 +309,29 @@ class provider implements
WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
$userlist->add_from_sql('userid', $sql, $params);
// Users who are either a student with a mark, or the teacher who has given a mark.
$sql = "SELECT g.userid, am.marker
FROM {context} ctx
JOIN {course_modules} cm ON cm.id = ctx.instanceid
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
JOIN {assign} a ON a.id = cm.instance
JOIN {assign_grades} g ON a.id = g.assignment
JOIN {assign_mark} am ON am.gradeid = g.id
WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
$userlist->add_from_sql('userid', $sql, $params);
$userlist->add_from_sql('marker', $sql, $params);
// Users who are either a student with an allocated marker, or the allocated marker.
$sql = "SELECT am.student, am.marker
FROM {context} ctx
JOIN {course_modules} cm ON cm.id = ctx.instanceid
JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
JOIN {assign} a ON a.id = cm.instance
JOIN {assign_allocated_marker} am ON am.assignment = a.id
WHERE ctx.id = :contextid AND ctx.contextlevel = :contextlevel";
$userlist->add_from_sql('student', $sql, $params);
$userlist->add_from_sql('marker', $sql, $params);
manager::plugintype_class_callback('assignsubmission', self::ASSIGNSUBMISSION_USER_INTERFACE,
'get_userids_from_context', [$userlist]);
manager::plugintype_class_callback('assignfeedback', self::ASSIGNFEEDBACK_USER_INTERFACE,
@@ -302,6 +368,8 @@ class provider implements
static::export_overrides($context, $assign, $user);
static::export_submission($assign, $user, $context, []);
static::export_marks($assign, $user, $context);
static::export_allocations($assign, $user, $context);
// Meta data.
self::store_assign_user_flags($context, $assign, $user->id);
if ($assign->is_blind_marking()) {
@@ -349,6 +417,8 @@ class provider implements
$DB->delete_records('assign_submission', ['assignment' => $assign->get_instance()->id]);
$DB->delete_records('assign_user_flags', ['assignment' => $assign->get_instance()->id]);
$DB->delete_records('assign_user_mapping', ['assignment' => $assign->get_instance()->id]);
static::delete_allocated_markers_for_users($assign);
static::delete_marks_for_users($assign);
}
}
}
@@ -394,6 +464,8 @@ class provider implements
static::delete_overrides_for_users($assign, [$user->id]);
$DB->delete_records('assign_user_flags', ['assignment' => $assignid, 'userid' => $user->id]);
$DB->delete_records('assign_user_mapping', ['assignment' => $assignid, 'userid' => $user->id]);
static::delete_allocated_markers_for_users($assign, [$user->id]);
static::delete_marks_for_users($assign, [$user->id]);
$DB->delete_records('assign_grades', ['assignment' => $assignid, 'userid' => $user->id]);
$DB->delete_records('assign_submission', ['assignment' => $assignid, 'userid' => $user->id]);
}
@@ -440,6 +512,8 @@ class provider implements
$params['assignment'] = $assignid;
$DB->delete_records_select('assign_user_flags', "assignment = :assignment AND userid $sql", $params);
$DB->delete_records_select('assign_user_mapping', "assignment = :assignment AND userid $sql", $params);
static::delete_allocated_markers_for_users($assign, $userids);
static::delete_marks_for_users($assign, $userids);
$DB->delete_records_select('assign_grades', "assignment = :assignment AND userid $sql", $params);
$DB->delete_records_select('assign_submission', "assignment = :assignment AND userid $sql", $params);
}
@@ -674,4 +748,127 @@ class provider implements
}
}
}
/**
* Delete all allocated markers for an array of users, for the given assignment.
*
* @param \assign $assign The assignment object.
* @param array $userids If empty, delete all marks for the assignment.
*/
protected static function delete_allocated_markers_for_users(\assign $assign, array $userids = []): void {
global $DB;
if ($userids) {
[$insql, $inparams] = $DB->get_in_or_equal($userids);
$params = [$assign->get_instance()->id];
// We need this twice as we're using the same $insql for student and marker.
$params = array_merge($params, $inparams);
$params = array_merge($params, $inparams);
$sql = "
SELECT id
FROM {assign_allocated_marker}
WHERE assignment = ?
AND (student {$insql} OR marker {$insql})
";
$DB->delete_records_subquery('assign_allocated_marker', 'id', 'id', $sql, $params);
} else {
$DB->delete_records('assign_allocated_marker', ['assignment' => $assign->get_instance()->id]);
}
}
/**
* Delete all assignment marks for an array of users, for the given assignment.
*
* @param \assign $assign The assignment object.
* @param array $userids If empty, delete all marks for the assignment.
*/
protected static function delete_marks_for_users(\assign $assign, array $userids = []): void {
global $DB;
if ($userids) {
[$insql, $inparams] = $DB->get_in_or_equal($userids);
$params = [$assign->get_instance()->id];
// We need this twice as we're using the same $insql for student and marker.
$params = array_merge($params, $inparams);
$params = array_merge($params, $inparams);
$sql = "
SELECT am.id
FROM {assign_mark} am
LEFT JOIN {assign_grades} ag ON ag.id = am.gradeid
WHERE am.assignment = ?
AND (ag.userid {$insql} OR am.marker {$insql})
";
$DB->delete_records_subquery('assign_mark', 'id', 'id', $sql, $params);
} else {
$DB->delete_records('assign_mark', ['assignment' => $assign->get_instance()->id]);
}
}
/**
* Export the user's marks for the given assignment.
*
* @param \assign $assign The assignment object
* @param \stdClass $user The user object
* @param \context $context The context
*/
protected static function export_marks(\assign $assign, \stdClass $user, \context $context): void {
global $DB;
$records = $DB->get_records_sql(
"
SELECT am.*
FROM {assign_mark} am
LEFT JOIN {assign_grades} ag on ag.id = am.gradeid
WHERE am.assignment = :assignment
AND (ag.userid = :uid1 OR am.marker = :uid2)
",
[
'assignment' => $assign->get_instance()->id,
'uid1' => $user->id,
'uid2' => $user->id,
]
);
$data = [];
foreach ($records as $row) {
$row = (array)$row;
unset($row['id']);
$data[] = $row;
}
writer::with_context($context)->export_data([get_string('marks', 'mod_assign')], (object) ['data' => $data]);
}
/**
* Export the user's marker allocations for the given assignment.
*
* @param \assign $assign The assignment object
* @param \stdClass $user The user object
* @param \context $context The context
*/
protected static function export_allocations(\assign $assign, \stdClass $user, \context $context): void {
global $DB;
$records = $DB->get_records_sql(
"
SELECT *
FROM {assign_allocated_marker}
WHERE assignment = :assignment
AND (student = :uid1 OR marker = :uid2)
",
[
'assignment' => $assign->get_instance()->id,
'uid1' => $user->id,
'uid2' => $user->id,
]
);
$data = [];
foreach ($records as $row) {
$row = (array)$row;
unset($row['id']);
$data[] = $row;
}
writer::with_context($context)->export_data([get_string('markerallocations', 'mod_assign')], (object) ['data' => $data]);
}
}
+35 -1
View File
@@ -34,6 +34,9 @@
<FIELD NAME="maxattempts" TYPE="int" LENGTH="6" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="What is the maximum number of student attempts allowed for this assignment? -1 means unlimited."/>
<FIELD NAME="markingworkflow" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If enabled, marking workflow features will be used in this assignment."/>
<FIELD NAME="markingallocation" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If enabled, marking allocation features will be used in this assignment"/>
<FIELD NAME="markercount" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Number of markers for this assignment, for example, 2 for double marking"/>
<FIELD NAME="multimarkmethod" TYPE="char" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="How to calculate grade when markercount greater than 1"/>
<FIELD NAME="multimarkrounding" TYPE="int" LENGTH="2" NOTNULL="false" SEQUENCE="false" COMMENT="How to round the grade calculation if using an average of marks"/>
<FIELD NAME="markinganonymous" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If enabled, marking anonymous features will be used in this assignment"/>
<FIELD NAME="sendstudentnotifications" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Default for send student notifications checkbox when grading."/>
<FIELD NAME="preventsubmissionnotingroup" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="If enabled a user will be unable to make a submission unless they are a member of a group."/>
@@ -138,7 +141,6 @@
<FIELD NAME="mailed" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Has the student been sent a notification about this grade update?"/>
<FIELD NAME="extensionduedate" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="An extension date assigned to an individual student."/>
<FIELD NAME="workflowstate" TYPE="char" LENGTH="20" NOTNULL="false" SEQUENCE="false" COMMENT="The current workflow state of the grade"/>
<FIELD NAME="allocatedmarker" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The allocated marker to this submission"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -168,5 +170,37 @@
<KEY NAME="userid" TYPE="foreign" FIELDS="userid" REFTABLE="user" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="assign_mark" COMMENT="Marks from multiple markers for a single submission">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="assignment" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The ID of the assignment this mark applies to"/>
<FIELD NAME="gradeid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The ID of an assign_grades row"/>
<FIELD NAME="timecreated" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time this mark was created"/>
<FIELD NAME="timemodified" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The time this mark was modified"/>
<FIELD NAME="marker" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The user ID of the marker"/>
<FIELD NAME="mark" TYPE="number" LENGTH="10" NOTNULL="false" SEQUENCE="false" DECIMALS="5" COMMENT="Mark given by marker"/>
<FIELD NAME="workflowstate" TYPE="char" LENGTH="20" NOTNULL="false" SEQUENCE="false" COMMENT="The current workflow state of the mark"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="assignment" TYPE="foreign" FIELDS="assignment" REFTABLE="assign" REFFIELDS="id" COMMENT="The assignment id these marks apply to"/>
<KEY NAME="gradeid" TYPE="foreign" FIELDS="gradeid" REFTABLE="assign_grades" REFFIELDS="id" COMMENT="Final grade record that this mark relates to"/>
<KEY NAME="marker" TYPE="foreign" FIELDS="marker" REFTABLE="user" REFFIELDS="id" COMMENT="Marker who gave this mark"/>
</KEYS>
</TABLE>
<TABLE NAME="assign_allocated_marker" COMMENT="One or more teachers allocated to mark individual submissions">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="true"/>
<FIELD NAME="student" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="User ID of student this marker is allocated to"/>
<FIELD NAME="assignment" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The id of the assignment this marker is allocated to"/>
<FIELD NAME="marker" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="User ID of teacher allocated to mark this student"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
<KEY NAME="student" TYPE="foreign" FIELDS="student" REFTABLE="user" REFFIELDS="id" COMMENT="The id field of the user table"/>
<KEY NAME="assignment" TYPE="foreign" FIELDS="assignment" REFTABLE="assign" REFFIELDS="id" COMMENT="The assignment id these flags apply to"/>
<KEY NAME="marker" TYPE="foreign" FIELDS="marker" REFTABLE="user" REFFIELDS="id" COMMENT="The id field of the user table"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
+86
View File
@@ -131,5 +131,91 @@ function xmldb_assign_upgrade($oldversion) {
upgrade_mod_savepoint(true, 2026022300, 'assign');
}
if ($oldversion < 2026022301) {
// Define field markercount to be added to assign.
$table = new xmldb_table('assign');
$field = new xmldb_field('markercount', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '1', 'markingallocation');
// Conditionally launch add field markercount.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field multimarkmethod to be added to assign.
$field = new xmldb_field('multimarkmethod', XMLDB_TYPE_CHAR, '10', null, false, false, null, 'markercount');
// Conditionally launch add field multimarkmethod.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define table assign_mark to be created.
$table = new xmldb_table('assign_mark');
// Adding fields to table assign_mark.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('assignment', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('gradeid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('timecreated', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('marker', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('mark', XMLDB_TYPE_NUMBER, '10,5', null, false, null, null);
$table->add_field('workflowstate', XMLDB_TYPE_CHAR, '20', null, null, null, null);
// Adding keys to table assign_grades_mark.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('assignment', XMLDB_KEY_FOREIGN, ['assignment'], 'assign', ['id']);
$table->add_key('gradeid', XMLDB_KEY_FOREIGN, ['gradeid'], 'assign_grades', ['id']);
$table->add_key('marker', XMLDB_KEY_FOREIGN, ['marker'], 'user', ['id']);
// Conditionally launch create table for assign_mark.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Define table assign_allocated_marker to be created.
$table = new xmldb_table('assign_allocated_marker');
// Adding fields to table assign_allocated_marker.
$table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null);
$table->add_field('student', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('assignment', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
$table->add_field('marker', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null);
// Adding keys to table assign_allocated_marker.
$table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']);
$table->add_key('student', XMLDB_KEY_FOREIGN, ['student'], 'user', ['id']);
$table->add_key('assignment', XMLDB_KEY_FOREIGN, ['assignment'], 'assign', ['id']);
$table->add_key('marker', XMLDB_KEY_FOREIGN, ['marker'], 'user', ['id']);
// Conditionally launch create table for assign_allocated_marker.
if (!$dbman->table_exists($table)) {
$dbman->create_table($table);
}
// Define field allocatedmarker to be dropped from assign_user_flags.
$table = new xmldb_table('assign_user_flags');
$field = new xmldb_field('allocatedmarker');
// Populate assign_allocated_marker before the allocatedmarker field is dropped.
if ($dbman->field_exists($table, $field)) {
$DB->execute(
"INSERT INTO {assign_allocated_marker} (assignment, student, marker)
SELECT assignment, userid, allocatedmarker
FROM {assign_user_flags}"
);
$dbman->drop_field($table, $field);
}
// Define field multimarkrounding to be added to assign.
$table = new xmldb_table('assign');
$field = new xmldb_field('multimarkrounding', XMLDB_TYPE_INTEGER, '2', null, false, false, null, 'multimarkmethod');
// Conditionally launch add field multimarkrounding.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Assign savepoint reached.
upgrade_mod_savepoint(true, 2026022301, 'assign');
}
return true;
}
+40 -14
View File
@@ -358,6 +358,9 @@ class mod_assign_external extends \mod_assign\external\external_api {
'm.maxattempts, ' .
'm.markingworkflow, ' .
'm.markingallocation, ' .
'm.markercount, ' .
'm.multimarkmethod, ' .
'm.multimarkrounding, ' .
'm.markinganonymous, ' .
'm.requiresubmissionstatement, '.
'm.preventsubmissionnotingroup, '.
@@ -438,6 +441,9 @@ class mod_assign_external extends \mod_assign\external\external_api {
'maxattempts' => $module->maxattempts,
'markingworkflow' => $module->markingworkflow,
'markingallocation' => $module->markingallocation,
'markercount' => $module->markercount,
'multimarkmethod' => $module->multimarkmethod,
'multimarkrounding' => $module->multimarkrounding,
'markinganonymous' => $module->markinganonymous,
'requiresubmissionstatement' => $module->requiresubmissionstatement,
'preventsubmissionnotingroup' => $module->preventsubmissionnotingroup,
@@ -558,6 +564,9 @@ class mod_assign_external extends \mod_assign\external\external_api {
'maxattempts' => new external_value(PARAM_INT, 'maximum number of attempts allowed'),
'markingworkflow' => new external_value(PARAM_INT, 'enable marking workflow'),
'markingallocation' => new external_value(PARAM_INT, 'enable marking allocation'),
'markercount' => new external_value(PARAM_INT, 'number of markers'),
'multimarkmethod' => new external_value(PARAM_TEXT, 'method to use when combining marks to a final grade'),
'multimarkrounding' => new external_value(PARAM_INT, 'rounding direction to use when handling decimal marks'),
'markinganonymous' => new external_value(PARAM_INT, 'enable marking anonymous'),
'requiresubmissionstatement' => new external_value(PARAM_INT, 'student must accept submission statement'),
'preventsubmissionnotingroup' => new external_value(PARAM_INT, 'Prevent submission not in group', VALUE_OPTIONAL),
@@ -1127,7 +1136,7 @@ class mod_assign_external extends \mod_assign\external\external_api {
list($inorequalsql, $placeholders) = $DB->get_in_or_equal($requestedassignmentids, SQL_PARAMS_NAMED);
$sql = "SELECT auf.id,auf.assignment,auf.userid,auf.locked,auf.mailed,".
"auf.extensionduedate,auf.workflowstate,auf.allocatedmarker ".
"auf.extensionduedate,auf.workflowstate " .
"FROM {assign_user_flags} auf ".
"WHERE auf.assignment ".$inorequalsql.
" ORDER BY auf.assignment, auf.id";
@@ -1143,7 +1152,6 @@ class mod_assign_external extends \mod_assign\external\external_api {
$userflag['mailed'] = $rd->mailed;
$userflag['extensionduedate'] = $rd->extensionduedate;
$userflag['workflowstate'] = $rd->workflowstate;
$userflag['allocatedmarker'] = $rd->allocatedmarker;
if (is_null($currentassignmentid) || ($rd->assignment != $currentassignmentid )) {
if (!is_null($assignment)) {
@@ -1197,7 +1205,6 @@ class mod_assign_external extends \mod_assign\external\external_api {
'mailed' => new external_value(PARAM_INT, 'mailed'),
'extensionduedate' => new external_value(PARAM_INT, 'extension due date'),
'workflowstate' => new external_value(PARAM_ALPHA, 'marking workflow state', VALUE_OPTIONAL),
'allocatedmarker' => new external_value(PARAM_INT, 'allocated marker')
)
)
)
@@ -1545,7 +1552,8 @@ class mod_assign_external extends \mod_assign\external\external_api {
array(
'assignmentid' => new external_value(PARAM_INT, 'The assignment id to operate on'),
'userid' => new external_value(PARAM_INT, 'The user id the submission belongs to'),
'jsonformdata' => new external_value(PARAM_RAW, 'The data from the grading form, encoded as a json array')
'jsonformdata' => new external_value(PARAM_RAW, 'The data from the grading form, encoded as a json array'),
'marker' => new external_value(PARAM_BOOL, 'Flag, false if grading, true if marking', VALUE_DEFAULT, false),
)
);
}
@@ -1556,12 +1564,12 @@ class mod_assign_external extends \mod_assign\external\external_api {
* @param int $assignmentid The id of the assignment
* @param int $userid The id of the user the submission belongs to.
* @param string $jsonformdata The data from the form, encoded as a json array.
* @param bool $marker Are we marking instead of grading?
* @return array of warnings to indicate any errors.
* @since Moodle 3.1
*/
public static function submit_grading_form($assignmentid, $userid, $jsonformdata) {
public static function submit_grading_form($assignmentid, $userid, $jsonformdata, $marker = false) {
global $CFG, $USER;
require_once($CFG->dirroot . '/mod/assign/locallib.php');
require_once($CFG->dirroot . '/mod/assign/gradeform.php');
@@ -1569,7 +1577,8 @@ class mod_assign_external extends \mod_assign\external\external_api {
array(
'assignmentid' => $assignmentid,
'userid' => $userid,
'jsonformdata' => $jsonformdata
'marker' => $marker,
'jsonformdata' => $jsonformdata,
));
list($assignment, $course, $cm, $context) = self::validate_assign($params['assignmentid']);
@@ -1585,7 +1594,8 @@ class mod_assign_external extends \mod_assign\external\external_api {
'userid' => $params['userid'],
'attemptnumber' => $data['attemptnumber'],
'rownum' => 0,
'gradingpanel' => true
'gradingpanel' => true,
'marker' => $marker,
);
if (WS_SERVER) {
@@ -1600,7 +1610,6 @@ class mod_assign_external extends \mod_assign\external\external_api {
// Data is injected into the form by the last param for the constructor.
$mform = new mod_assign_grade_form(null, $formparams, 'post', '', null, true, $data);
$validateddata = $mform->get_data();
if ($validateddata) {
$assignment->save_grade($params['userid'], $validateddata);
} else {
@@ -2637,8 +2646,13 @@ class mod_assign_external extends \mod_assign\external\external_api {
'onlyids' => new external_value(PARAM_BOOL, 'Do not return all user fields', VALUE_DEFAULT, false),
'includeenrolments' => new external_value(PARAM_BOOL, 'Do return courses where the user is enrolled',
VALUE_DEFAULT, true),
'tablesort' => new external_value(PARAM_BOOL, 'Apply current user table sorting preferences.',
VALUE_DEFAULT, false)
'tablesort' => new external_value(
PARAM_BOOL,
'Apply current user table sorting preferences.',
VALUE_DEFAULT,
false
),
'marking' => new external_value(PARAM_BOOL, 'Are we marking instead of grading?', VALUE_DEFAULT, false),
)
);
}
@@ -2654,12 +2668,22 @@ class mod_assign_external extends \mod_assign\external\external_api {
* @param bool $onlyids Only return user ids.
* @param bool $includeenrolments Return courses where the user is enrolled.
* @param bool $tablesort Apply current user table sorting params from the grading table.
* @param bool $marking Are we marking instead of grading?
* @return array of warnings and status result
* @since Moodle 3.1
* @throws moodle_exception
*/
public static function list_participants($assignid, $groupid, $filter, $skip,
$limit, $onlyids, $includeenrolments, $tablesort) {
public static function list_participants(
$assignid,
$groupid,
$filter,
$skip,
$limit,
$onlyids,
$includeenrolments,
$tablesort,
$marking
) {
global $DB, $CFG;
require_once($CFG->dirroot . "/mod/assign/locallib.php");
require_once($CFG->dirroot . "/user/lib.php");
@@ -2674,7 +2698,8 @@ class mod_assign_external extends \mod_assign\external\external_api {
'limit' => $limit,
'onlyids' => $onlyids,
'includeenrolments' => $includeenrolments,
'tablesort' => $tablesort
'tablesort' => $tablesort,
'marking' => $marking,
));
$warnings = array();
@@ -2682,6 +2707,7 @@ class mod_assign_external extends \mod_assign\external\external_api {
require_capability('mod/assign:view', $context);
$assign->set_is_marking($marking);
$assign->require_view_grades();
$participants = array();
+245 -50
View File
@@ -151,8 +151,7 @@ class assign_grading_table extends table_sql implements renderable {
$fields .= 'uf.mailed as mailed, ';
$fields .= 'uf.locked as locked, ';
$fields .= 'uf.extensionduedate as extensionduedate, ';
$fields .= 'uf.workflowstate as workflowstate, ';
$fields .= 'uf.allocatedmarker as allocatedmarker';
$fields .= 'uf.workflowstate as workflowstate';
$from = '{user} u
LEFT JOIN {assign_submission} s
@@ -346,10 +345,14 @@ class assign_grading_table extends table_sql implements renderable {
// Check to see if marker filter is set.
$markerfilter = (int)get_user_preferences('assign_markerfilter', '');
if (!empty($markerfilter)) {
$from .= 'LEFT JOIN {assign_allocated_marker} am
ON u.id = am.student
AND am.assignment = :assignmentid4 ';
$params['assignmentid4'] = (int)$this->assignment->get_instance()->id;
if ($markerfilter == ASSIGN_MARKER_FILTER_NO_MARKER) {
$where .= ' AND (uf.allocatedmarker IS NULL OR uf.allocatedmarker = 0)';
$where .= ' AND am.marker IS NULL';
} else {
$where .= ' AND uf.allocatedmarker = :markerid';
$where .= " AND am.marker = :markerid";
$params['markerid'] = $markerfilter;
}
}
@@ -453,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');
@@ -665,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);
@@ -696,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.
*
@@ -774,9 +821,10 @@ class assign_grading_table extends table_sql implements renderable {
* @param int $userid The user id of the user this grade belongs to
* @param int $modified Timestamp showing when the grade was last modified
* @param float $deductedmark The deducted mark if penalty is applied
* @param int|null $markerid The allocated marker id if we are displaying a mark instead of an overall grade
* @return string The formatted grade
*/
public function display_grade($grade, $editable, $userid, $modified, float $deductedmark = 0) {
public function display_grade($grade, $editable, $userid, $modified, float $deductedmark = 0, ?int $markerid = null) {
if ($this->is_downloading()) {
if ($this->assignment->get_instance()->grade >= 0) {
if ($grade == -1 || $grade === null) {
@@ -793,7 +841,7 @@ class assign_grading_table extends table_sql implements renderable {
return $scale;
}
}
return $this->assignment->display_grade($grade, $editable, $userid, $modified, $deductedmark);
return $this->assignment->display_grade($grade, $editable, $userid, $modified, $deductedmark, $markerid);
}
/**
@@ -1003,10 +1051,29 @@ class assign_grading_table extends table_sql implements renderable {
* @return string
*/
public function col_grade(stdClass $row): string {
global $DB, $USER;
$gradingdisabled = $this->assignment->grading_disabled($row->id, true, $this->gradinginfo);
$displaygrade = $this->display_grade($row->grade, $this->quickgrading && !$gradingdisabled, $row->userid, $row->timemarked);
if (!$this->is_downloading() && $this->hasgrade) {
// If assignment uses multiple markers with the manual (aka agreement)
// method then the grader must be an existing marker so only show the
// action menu if they are.
$multimarkcangrade = true;
if (
$this->assignment->is_using_multiple_marking() &&
$this->assignment->get_instance()->multimarkmethod === 'manual' &&
!$DB->get_record('assign_mark', ['gradeid' => $row->gradeid, 'marker' => $USER->id]) &&
!$DB->get_record('assign_allocated_marker', [
'assignment' => $this->assignment->get_instance()->id,
'marker' => $USER->id,
'student' => $row->userid,
])
) {
$multimarkcangrade = false;
}
if (!$this->is_downloading() && $this->hasgrade && $multimarkcangrade) {
$urlparams = [
'id' => $this->assignment->get_course_module()->id,
'rownum' => 0,
@@ -1045,6 +1112,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.
*
@@ -1641,6 +1830,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);
+34
View File
@@ -374,6 +374,7 @@ $string['introattachments'] = 'Additional files';
$string['introattachments_help'] = 'Additional files for use in the assignment, such as answer templates, may be added.';
$string['invalidgradeforscale'] = 'The grade supplied was not valid for the current scale';
$string['invalidfloatforgrade'] = 'The grade provided could not be understood: {$a}';
$string['invalidmarkerallocation:notenoughmarkers'] = 'There are not enough markers ({$a->markers}) to meet the required number for this assignment ({$a->requested}).';
$string['invalidoverrideid'] = 'Invalid override id';
$string['lastmodifiedsubmission'] = 'Last modified (submission)';
$string['lastmodifiedgrade'] = 'Last modified (grade)';
@@ -384,9 +385,20 @@ $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['markerallocations'] = 'Marker allocations';
$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';
$string['markgrademanual_help'] = 'Grade will need to be manually entered, it won\'t be calculated automatically.';
$string['markgrademaximum'] = 'Maximum mark';
$string['markgrademaximum_help'] = 'Grade will be calculated as the highest of the marks given.';
$string['markingallocation'] = 'Use marking allocation';
$string['markingallocation_help'] = 'If enabled together with marking workflow, markers can be allocated to particular students.';
$string['markinganonymous'] = 'Allow partial release of grades while marking anonymously';
@@ -409,6 +421,22 @@ $string['markingworkflowstatenotmarked'] = 'Not marked';
$string['markingworkflowstatereadyforreview'] = 'Marking completed';
$string['markingworkflowstatereadyforrelease'] = 'Ready for release';
$string['markingworkflowstatereleased'] = 'Released';
$string['markoutof'] = 'Mark out of {$a}';
$string['markoutof_help'] = 'Enter the mark for the student\'s submission here. You may include decimals.';
$string['marks'] = 'Marks';
$string['markverb'] = 'Mark';
$string['multimarkmethod'] = 'Calculate grade';
$string['multimarkmethod_help'] = 'How the grade will be calculated when there are multiple markers';
$string['multimarkrounding'] = 'Rounding';
$string['multimarkrounding_help'] = 'Whether to round up, down, naturally, or not at all when calculating an average mark';
$string['multimarkrounding:down'] = 'Round down';
$string['multimarkrounding:down_help'] = 'Average mark will be rounded down to the nearest whole number.';
$string['multimarkrounding:natural'] = 'Natural rounding';
$string['multimarkrounding:natural_help'] = 'Average mark will be rounded up or down naturally to the nearest whole number.';
$string['multimarkrounding:none'] = 'No rounding';
$string['multimarkrounding:none_help'] = 'Average mark will not be rounded.';
$string['multimarkrounding:up'] = 'Round up';
$string['multimarkrounding:up_help'] = 'Average mark will be rounded up to the nearest whole number.';
$string['maxattempts'] = 'Allowed attempts';
$string['maxattempts_help'] = 'The maximum number of submission attempts that can be made by a student. After this number has been reached, the submission can no longer be reopened.';
$string['maxgrade'] = 'Maximum grade';
@@ -500,10 +528,12 @@ $string['previoususer'] = 'Previous user';
$string['privacy:attemptpath'] = 'attempt {$a}';
$string['privacy:blindmarkingidentifier'] = 'The identifier used for anonymous submissions';
$string['privacy:gradepath'] = 'grade';
$string['privacy:metadata:assignallocatedmarker'] = 'Stores mappings between allocated markers and student assignments';
$string['privacy:metadata:assigndownloadasfolders'] = 'A user preference for whether multiple file submissions should be downloaded into folders';
$string['privacy:metadata:assignfeedbackpluginsummary'] = 'Feedback data for the assignment.';
$string['privacy:metadata:assignfilter'] = 'Filter options such as \'Submitted\', \'Not submitted\', \'Requires grading\', and \'Granted extension\'';
$string['privacy:metadata:assigngrades'] = 'Stores user grades for the assignment';
$string['privacy:metadata:assignmark'] = 'Stores user marks for the assignment';
$string['privacy:metadata:assignmarkerfilter'] = 'Filter the assign summary by the assigned marker.';
$string['privacy:metadata:assignmentid'] = 'Assignment ID';
$string['privacy:metadata:assignmessageexplanation'] = 'Messages are sent to students through the messaging system.';
@@ -709,6 +739,7 @@ $string['usersnone'] = 'There are no users enrolled in this course or no users c
$string['usersubmissioncannotberemoved'] = 'The submission of {$a} cannot be removed.';
$string['userswhoneedtosubmit'] = 'Users who need to submit: {$a}';
$string['usergrade'] = 'User grade';
$string['usermark'] = 'User mark';
$string['validmarkingworkflowstates'] = 'Valid marking workflow states';
$string['viewadifferentattempt'] = 'View a different attempt';
$string['viewbatchsetmarkingworkflowstate'] = 'View batch set marking workflow state page.';
@@ -727,6 +758,9 @@ $string['viewfull'] = 'View full';
$string['viewsummary'] = 'View summary';
$string['viewsubmissiongradingtable'] = 'View submission grading table.';
$string['viewrevealidentitiesconfirm'] = 'View reveal student identities confirmation page.';
$string['workflowcontext'] = 'Workflow context';
$string['workflowcontext_help_grade'] = 'Apply the workflow state to the overall grade for the submission.';
$string['workflowcontext_help_mark'] = 'Apply the workflow state to your mark, where you are an allocated marker.';
$string['workflowfilter'] = 'Workflow filter';
$string['xofy'] = '{$a->x} of {$a->y}';
+2 -1
View File
@@ -1461,7 +1461,8 @@ function mod_assign_output_fragment_gradingpanel($args) {
$viewargs = array(
'userid' => $userid,
'attemptnumber' => $attemptnumber,
'formdata' => $formdata
'formdata' => $formdata,
'marker' => $args['marker'],
);
return $assign->view('gradingpanel', $viewargs);
File diff suppressed because it is too large Load Diff
+76
View File
@@ -243,6 +243,82 @@ class mod_assign_mod_form extends moodleform_mod {
$mform->addHelpButton('markingallocation', 'markingallocation', 'assign');
$mform->hideIf('markingallocation', 'markingworkflow', 'eq', 0);
$name = get_string('markercount', 'assign');
$markercount = range(1, ASSIGN_MULTIMARKING_MAX_MARKERS);
$mform->addElement('select', 'markercount', $name, array_combine($markercount, $markercount));
$mform->addHelpButton('markercount', 'markercount', 'assign');
$mform->disabledIf('markercount', 'advancedgradingmethod_submissions', 'neq', '');
$mform->hideIf('markercount', 'advancedgradingmethod_submissions', 'neq', '');
$mform->hideIf('markercount', 'markingallocation', 'neq', '1');
$mform->hideIf('markercount', 'markingworkflow', 'neq', '1');
$name = get_string('multimarkmethod', 'assign');
$options = new core\output\choicelist();
$options->add_option(
ASSIGN_MULTIMARKING_METHOD_MANUAL,
get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MANUAL, 'assign'),
[
'description' => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MANUAL . '_help', 'assign'),
]
);
$options->add_option(
ASSIGN_MULTIMARKING_METHOD_MAX,
get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MAX, 'assign'),
[
'description' => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MAX . '_help', 'assign'),
]
);
$options->add_option(
ASSIGN_MULTIMARKING_METHOD_AVERAGE,
get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_AVERAGE, 'assign'),
[
'description' => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_AVERAGE . '_help', 'assign'),
]
);
$mform->addElement('choicedropdown', 'multimarkmethod', $name, $options);
$mform->hideIf('multimarkmethod', 'markingallocation', 'eq', '0');
$mform->hideIf('multimarkmethod', 'markercount', 'eq', '1');
$mform->disabledIf('multimarkmethod', 'advancedgradingmethod_submissions', 'neq', '');
$mform->hideIf('multimarkmethod', 'advancedgradingmethod_submissions', 'neq', '');
$name = get_string('multimarkrounding', 'assign');
$options = new core\output\choicelist();
$options->add_option(
ASSIGN_MULTIMARKING_AVERAGE_ROUND_NONE,
get_string('multimarkrounding:none', 'assign'),
[
'description' => get_string('multimarkrounding:none_help', 'assign'),
]
);
$options->add_option(
ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL,
get_string('multimarkrounding:natural', 'assign'),
[
'description' => get_string('multimarkrounding:natural_help', 'assign'),
]
);
$options->add_option(
ASSIGN_MULTIMARKING_AVERAGE_ROUND_DOWN,
get_string('multimarkrounding:down', 'assign'),
[
'description' => get_string('multimarkrounding:down_help', 'assign'),
]
);
$options->add_option(
ASSIGN_MULTIMARKING_AVERAGE_ROUND_UP,
get_string('multimarkrounding:up', 'assign'),
[
'description' => get_string('multimarkrounding:up_help', 'assign'),
]
);
$mform->addElement('choicedropdown', 'multimarkrounding', $name, $options);
$mform->hideIf('multimarkrounding', 'markingallocation', 'eq', '0');
$mform->hideIf('multimarkrounding', 'markercount', 'eq', '1');
$mform->hideIf('multimarkrounding', 'multimarkmethod', 'neq', 'average');
$mform->disabledIf('multimarkrounding', 'multimarkmethod', 'neq', 'average');
$mform->disabledIf('multimarkrounding', 'advancedgradingmethod_submissions', 'neq', '');
$mform->hideIf('multimarkrounding', 'advancedgradingmethod_submissions', 'neq', '');
$name = get_string('markinganonymous', 'assign');
$mform->addElement('selectyesno', 'markinganonymous', $name);
$mform->addHelpButton('markinganonymous', 'markinganonymous', 'assign');
+56
View File
@@ -360,6 +360,62 @@ if ($ADMIN->fulltree) {
$setting->set_locked_flag_options(admin_setting_flag::ENABLED, false);
$settings->add($setting);
// Default setting for number of markers.
$name = new lang_string('markercount', 'mod_assign');
$description = new lang_string('markercount_help', 'mod_assign');
$setting = new admin_setting_configselect(
'assign/markercount',
$name,
$description,
ASSIGN_MULTIMARKING_DEFAULT_MARKERS,
array_combine(
range(1, ASSIGN_MULTIMARKING_MAX_MARKERS),
range(1, ASSIGN_MULTIMARKING_MAX_MARKERS)
),
);
$setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false);
$setting->set_locked_flag_options(admin_setting_flag::ENABLED, false);
$settings->add($setting);
// Default setting for multi-marking method (e.g. manual, max, etc...).
$name = new lang_string('multimarkmethod', 'mod_assign');
$description = new lang_string('multimarkmethod_help', 'mod_assign');
$options = [
ASSIGN_MULTIMARKING_METHOD_MANUAL => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MANUAL, 'assign'),
ASSIGN_MULTIMARKING_METHOD_MAX => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_MAX, 'assign'),
ASSIGN_MULTIMARKING_METHOD_AVERAGE => get_string('markgrade' . ASSIGN_MULTIMARKING_METHOD_AVERAGE, 'assign'),
];
$setting = new admin_setting_configselect(
'assign/multimarkmethod',
$name,
$description,
ASSIGN_MULTIMARKING_METHOD_MANUAL,
$options,
);
$setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false);
$setting->set_locked_flag_options(admin_setting_flag::ENABLED, false);
$settings->add($setting);
// Default setting for multi-marking rounding (e.g. up, down, none).
$name = new lang_string('multimarkrounding', 'mod_assign');
$description = new lang_string('multimarkrounding_help', 'mod_assign');
$options = [
ASSIGN_MULTIMARKING_AVERAGE_ROUND_NONE => get_string('multimarkrounding:none', 'assign'),
ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL => get_string('multimarkrounding:natural', 'assign'),
ASSIGN_MULTIMARKING_AVERAGE_ROUND_DOWN => get_string('multimarkrounding:down', 'assign'),
ASSIGN_MULTIMARKING_AVERAGE_ROUND_UP => get_string('multimarkrounding:up', 'assign'),
];
$setting = new admin_setting_configselect(
'assign/multimarkrounding',
$name,
$description,
ASSIGN_MULTIMARKING_AVERAGE_ROUND_NONE,
$options,
);
$setting->set_advanced_flag_options(admin_setting_flag::ENABLED, false);
$setting->set_locked_flag_options(admin_setting_flag::ENABLED, false);
$settings->add($setting);
$name = new lang_string('markinganonymous', 'mod_assign');
$description = new lang_string('markinganonymous_help', 'mod_assign');
$setting = new admin_setting_configcheckbox('assign/markinganonymous',
@@ -36,7 +36,7 @@
<a href="#previous" data-action="previous-user" aria-label="{{#str}} previoususer, mod_assign {{/str}}" title="{{#str}} previoususer, mod_assign {{/str}}">{{{larrow}}}</a>
<span data-region="input-field">
<label for="change-user-select" class="visually-hidden">{{#str}} changeuser, mod_assign {{/str}}</label>
<select id="change-user-select" data-action="change-user" data-currentuserid="{{currentuserid}}" data-assignmentid="{{assignmentid}}" data-groupid="{{groupid}}"></select>
<select id="change-user-select" data-action="change-user" data-currentuserid="{{currentuserid}}" data-assignmentid="{{assignmentid}}" data-groupid="{{groupid}}" data-ismarking="{{ismarking}}"></select>
</span>
<a href="#next" data-action="next-user" aria-label="{{#str}} nextuser, mod_assign {{/str}}" title="{{#str}} nextuser, mod_assign {{/str}}">{{{rarrow}}}</a>
@@ -27,7 +27,8 @@
Example context (json):
{
"gradelink": "https://moodle.org"
"gradelink": "https://moodle.org",
"marklink": "https://moodle.org"
}
}}
@@ -38,5 +39,10 @@
<a class="btn btn-primary" href="{{gradelink}}">{{#str}}gradeverb, core{{/str}}</a>
</div>
{{/gradelink}}
{{#marklink}}
<div class="navitem">
<a class="btn btn-primary" href="{{marklink}}">{{#str}}markverb, mod_assign{{/str}}</a>
</div>
{{/marklink}}
</div>
</div>
@@ -127,6 +127,9 @@ final class restore_date_test extends \restore_date_testcase {
'timelimit' => DAYSECS,
'submissionattachments' => 1,
'gradepenalty' => 1,
'markercount' => 5,
'multimarkmethod' => 'average',
'multimarkrounding' => 0,
];
$assign = $this->getDataGenerator()->create_module('assign', $record);
@@ -0,0 +1,95 @@
@mod @mod_assign @javascript
Feature: Allocate markers to student submissions
In order to use the marking workflow and record provisional marks before calculating a final grade
As a teacher
I need to allocate markers to student submissions
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | One | student1@example.com |
| student2 | Student | Two | student2@example.com |
| teacher1 | Teacher | One | teacher1@example.com |
| teacher2 | Teacher | Two | teacher2@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
And the following "activity" exists:
| activity | assign |
| course | C1 |
| idnumber | A1 |
| name | Assignment 1 |
| section | 1 |
| completion | 1 |
| markingworkflow | 1 |
| markingallocation | 1 |
| markercount | 2 |
| grade[modgrade_type] | point |
| grade[modgrade_point] | 100 |
Scenario: Allocate markers to students via the Allocate Markers page
Given I am on the "A1" "assign activity" page logged in as teacher1
And I navigate to "Submissions" in current page administration
And I set the field "selectall" to "1"
And I click on "Allocate marker" "button" in the "sticky-footer" "region"
And I click on "Allocate marker" "button" in the ".modal-footer" "css_element"
When I select "Teacher One" from the "Allocated marker 1" singleselect
And I select "Teacher Two" from the "Allocated marker 2" singleselect
And I press "Save changes"
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 |
| Student One | Teacher One | Teacher Two |
| Student Two | Teacher One | Teacher Two |
Scenario: Allocate markers to students via the Quick Grading page
Given I am on the "A1" "assign activity" page logged in as teacher1
And I navigate to "Submissions" in current page administration
And I click on "Quick grading" "checkbox"
When I set the field "Allocated marker 1" in the "Student One" "table_row" to "Teacher One"
And I set the field "Allocated marker 1" in the "Student Two" "table_row" to "Teacher One"
And I set the field "Allocated marker 2" in the "Student One" "table_row" to "Teacher Two"
And I set the field "Allocated marker 2" in the "Student Two" "table_row" to "Teacher Two"
And I click on "Save" "button" in the "sticky-footer" "region"
And I press "Continue"
And I click on "Quick grading" "checkbox"
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 |
| Student One | Teacher One | Teacher Two |
| Student Two | Teacher One | Teacher Two |
Scenario: Allocate markers to students via the Grader page
Given I am on the "A1" "assign activity" page logged in as teacher1
And I go to "Student One" "Assignment 1" activity advanced grading page
When I set the field "Marker 1" to "Teacher One"
And I set the field "Marker 2" to "Teacher Two"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I navigate to "Submissions" in current page administration
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 |
| Student One | Teacher One | Teacher Two |
| Student Two | | |
Scenario: Allocate a duplicate marker to a student
Given I am on the "A1" "assign activity" page logged in as teacher1
And I navigate to "Submissions" in current page administration
And I set the field "selectall" to "1"
And I click on "Allocate marker" "button" in the "sticky-footer" "region"
And I click on "Allocate marker" "button" in the ".modal-footer" "css_element"
When I select "Teacher One" from the "Allocated marker 1" singleselect
And I select "Teacher One" from the "Allocated marker 2" singleselect
And I press "Save changes"
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 |
| Student One | Teacher One | |
| Student Two | Teacher One | |
And the following should not exist in the "submissions" table:
| Marker 2 |
| Teacher One |
| Teacher One |
@@ -0,0 +1,168 @@
@mod @mod_assign @javascript
Feature: Allocate marks to student submissions
In order to assess a submission with multiple markers
As a teacher
I need to allocate marks to student submissions
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | One | student1@example.com |
| student2 | Student | Two | student2@example.com |
| teacher1 | Teacher | One | teacher1@example.com |
| teacher2 | Teacher | Two | teacher2@example.com |
| teacher3 | Teacher | Three | teacher3@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
| teacher3 | C1 | editingteacher |
And the following "activity" exists:
| activity | assign |
| course | C1 |
| idnumber | A1 |
| name | Assignment 1 |
| section | 1 |
| completion | 1 |
| markingworkflow | 1 |
| markingallocation | 1 |
| markercount | 2 |
| multimarkmethod | maximum |
| grade[modgrade_type] | point |
| grade[modgrade_point] | 100 |
| assignfeedback_comments_enabled | 1 |
And the following "mod_assign > marker_allocations" exist:
| assign | user | marker |
| Assignment 1 | student1 | teacher1 |
| Assignment 1 | student1 | teacher2 |
| Assignment 1 | student2 | teacher1 |
| Assignment 1 | student2 | teacher2 |
Scenario: Allocate marks to students via the Quick Grading page
Given I am on the "A1" "assign activity" page logged in as teacher1
And I navigate to "Submissions" in current page administration
And I click on "Quick grading" "checkbox"
When I set the field "User mark" in the "Student One" "table_row" to "99"
And I click on "Save" "button" in the "sticky-footer" "region"
And I press "Continue"
And I click on "Quick grading" "checkbox"
Then the following should exist in the "submissions" table:
| First name | Marker 1 |
| Student One | 99 |
Scenario: Allocate marks to students via the Advanced Marker window
Given I am on the "A1" "assign activity" page logged in as teacher1
And I go to "Student One" "Assignment 1" activity advanced marking page
When I set the field "Mark out of 100" to "50"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I navigate to "Submissions" in current page administration
Then the following should exist in the "submissions" table:
| First name | Marker 1 |
| Student One | 50 |
Scenario: Set workflow state for an allocated mark via Advanced Marker window
Given I am on the "A1" "assign activity" page logged in as teacher1
And I go to "Student One" "Assignment 1" activity advanced marking page
When I set the field "Mark out of 100" to "42"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I navigate to "Submissions" in current page administration
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | Marking completed | Not marked | In marking |
| Student Two | | | Not marked |
Scenario: Bulk set workflow state as allocated markers
Given I am on the "A1" "assign activity" page logged in as teacher1
And I navigate to "Submissions" in current page administration
And I set the field "selectall" to "1"
And I click on "Change marking state" "button" in the "sticky-footer" "region"
And I click on "Change marking state" "button" in the ".modal-footer" "css_element"
And I select "Mark" from the "Workflow context" singleselect
When I select "Marking completed" from the "Marking workflow state" singleselect
And I press "Save changes"
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | Marking completed | Not marked | In marking |
| Student Two | Marking completed | Not marked | In marking |
# Log in as the other teacher and change their workflow status too.
And I am on the "A1" "assign activity" page logged in as teacher2
And I navigate to "Submissions" in current page administration
And I set the field "selectall" to "1"
And I click on "Change marking state" "button" in the "sticky-footer" "region"
And I click on "Change marking state" "button" in the ".modal-footer" "css_element"
And I select "Mark" from the "Workflow context" singleselect
And I select "Marking completed" from the "Marking workflow state" singleselect
And I press "Save changes"
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | Marking completed | Marking completed | Marking completed |
| Student Two | Marking completed | Marking completed | Marking completed |
Scenario: Grades are only calculated after all marks are given
Given I am on the "A1" "assign activity" page logged in as teacher1
And I go to "Student One" "Assignment 1" activity advanced marking page
When I set the field "Mark out of 100" to "42"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I navigate to "Submissions" in current page administration
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | Marking completed | Not marked | In marking |
And the following should not exist in the "submissions" table:
| Grade |
| 42 |
# Now, provide marks as teacher2.
And I am on the "A1" "assign activity" page logged in as teacher2
And I go to "Student One" "Assignment 1" activity advanced marking page
And I set the field "Mark out of 100" to "55"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I navigate to "Submissions" in current page administration
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status | Grade |
| Student One | Marking completed | Marking completed | Marking completed | 55 |
Scenario: Grade updates when new allocated marker provides mark
Given I am on the "A1" "assign activity" page logged in as teacher1
And I change window size to "large"
And I go to "Student One" "Assignment 1" activity advanced marking page
When I set the field "Mark out of 100" to "50"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
# Now, provide marks as teacher2.
And I am on the "A1" "assign activity" page logged in as teacher2
And I go to "Student One" "Assignment 1" activity advanced marking page
And I set the field "Mark out of 100" to "51"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I navigate to "Submissions" in current page administration
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status | Grade |
| Student One | Marking completed | Marking completed | Marking completed | 51 |
# Swap teacher2 for teacher3 and provide a new mark.
And I am on the "A1" "assign activity" page logged in as teacher3
And I go to "Student One" "Assignment 1" activity advanced grading page
And I set the field "Marking workflow state" to "In marking"
And I set the field "Marker 2" to "Teacher Three"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I go to "Student One" "Assignment 1" activity advanced marking page
And I set the field "Mark out of 100" to "52"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I navigate to "Submissions" in current page administration
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status | Grade |
| Student One | Marking completed | Marking completed | Marking completed | 52 |
@@ -86,4 +86,31 @@ class behat_mod_assign extends behat_base {
$rule = ['contextid' => 1, 'overdueby' => DAYSECS, 'penalty' => 10, 'sortorder' => 0];
$DB->insert_record('gradepenalty_duedate_rule', (object) $rule);
}
/**
* Goes to the student's advanced marking page.
*
* @Given /^I go to "(?P<user_fullname>(?:[^"]|\\")*)" "(?P<activity_name>(?:[^"]|\\")*)" activity advanced marking page$/
* @param string $userfullname The user's full name including firstname and lastname.
* @param string $activityname The activity name
*/
public function i_go_to_activity_advanced_marking_page(string $userfullname, string $activityname): void {
// Step to access the user grade page from the grading page.
$this->execute('behat_navigation::go_to_breadcrumb_location', $this->escape($activityname));
$this->execute('behat_general::click_link', get_string('gradeitem:submissions', 'mod_assign'));
$this->execute(
'behat_general::i_click_on_in_the',
[
$this->escape(get_string('markactions', 'assign')),
'actionmenu',
$this->escape($userfullname),
'table_row',
]
);
$this->execute('behat_action_menu::i_choose_in_the_open_action_menu', get_string('markverb', 'mod_assign'));
}
}
@@ -57,7 +57,7 @@ Feature: In an assignment, teachers can filter displayed submissions by assigned
And I press "Save and display"
And I am on the "Test assignment name" "assign activity" page
And I go to "Student 1" "Test assignment name" activity advanced grading page
And I set the field "allocatedmarker" to "Marker 1"
And I set the field "Marker 1" to "Marker 1"
And I set the field "Notify student" to "0"
And I press "Save changes"
And I am on the "Test assignment name" Activity page
@@ -130,7 +130,7 @@ Feature: In an assignment, teachers can filter displayed submissions by assigned
And I change window size to "large"
And I go to "Student 1" "Test assignment name" activity advanced grading page
# Allocate Marker 1 as the marker of Student 1.
And I set the field "allocatedmarker" to "Marker 1"
And I set the field "Marker 1" to "Marker 1"
And I set the field "Notify student" to "0"
And I press "Save changes"
And I follow "View all submissions"
@@ -32,6 +32,7 @@ Feature: In an assignment, teachers can change filters in the grading app
| assignsubmission_file_enabled | 0 |
| markingworkflow | 1 |
| markingallocation | 1 |
| markercount | 1 |
@javascript
Scenario: Set filters in the grading table and see them in the grading app
@@ -40,7 +41,7 @@ Feature: In an assignment, teachers can change filters in the grading app
And I should not see "Course 1 &amp;"
And the "title" attribute of "a[title='Course: Course 1 &']" "css_element" should not contain "&amp;"
And I should not see "Test assignment name &amp;"
And I set the field "allocatedmarker" to "Marker 1"
And I set the field "Marker 1" to "Marker 1"
And I set the field "workflowstate" to "In marking"
And I set the field "Notify student" to "0"
And I press "Save changes"
@@ -64,7 +65,7 @@ Feature: In an assignment, teachers can change filters in the grading app
Scenario: Set filters in the grading app and see them in the grading table
Given I am on the "Test assignment name &" Activity page logged in as teacher1
And I go to "Student 1" "Test assignment name &" activity advanced grading page
And I set the field "allocatedmarker" to "Marker 1"
And I set the field "Marker 1" to "Marker 1"
And I set the field "workflowstate" to "In marking"
And I set the field "Notify student" to "0"
And I press "Save changes"
@@ -0,0 +1,241 @@
@mod @mod_assign @javascript
Feature: Complete multi-marking workflow
In order to conduct an assignment with multi-marking
As a teacher
I need to:
- Allocate markers to the student(s)
- Allocate marks to the student(s) submission(s)
- Calculate a final grade based on the configured multi-marking agreement method
- Release the final grade to the student(s)
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| student1 | Student | One | student1@example.com |
| student2 | Student | Two | student2@example.com |
| student3 | Student | Three | student3@example.com |
| student4 | Student | Four | student4@example.com |
| teacher1 | Teacher | One | teacher1@example.com |
| teacher2 | Teacher | Two | teacher2@example.com |
| teacher3 | Teacher | Three | teacher3@example.com |
And the following "courses" exist:
| fullname | shortname |
| Course 1 | C1 |
And the following "course enrolments" exist:
| user | course | role |
| student1 | C1 | student |
| student2 | C1 | student |
| student3 | C1 | student |
| student4 | C1 | student |
| teacher1 | C1 | editingteacher |
| teacher2 | C1 | editingteacher |
| teacher3 | C1 | editingteacher |
And the following "groups" exist:
| name | course | idnumber | participation |
| Group 1 | C1 | G1 | 1 |
| Group 2 | C1 | G2 | 1 |
And the following "group members" exist:
| user | group |
| student1 | G1 |
| student2 | G1 |
| student3 | G2 |
| student4 | G2 |
And the following "activity" exists:
| activity | assign |
| course | C1 |
| idnumber | A1 |
| name | Assignment 1 |
| section | 1 |
| completion | 1 |
| markingworkflow | 1 |
| markingallocation | 1 |
| markercount | 2 |
| multimarkmethod | average |
| multimarkrounding | 1 |
| grade[modgrade_type] | point |
| grade[modgrade_point] | 100 |
And the following "activity" exists:
| activity | assign |
| course | C1 |
| idnumber | A2 |
| name | Assignment 2 |
| section | 1 |
| completion | 1 |
| markingworkflow | 1 |
| markingallocation | 1 |
| markercount | 2 |
| multimarkmethod | maximum |
| multimarkrounding | 1 |
| teamsubmission | 1 |
| grade[modgrade_type] | point |
| grade[modgrade_point] | 100 |
Scenario: Complete workflow of multi-marking
# Firstly, allocate the markers to the students.
Given I am on the "A1" "assign activity" page logged in as teacher1
And I change window size to "large"
And I navigate to "Submissions" in current page administration
And I set the field "selectall" to "1"
When I click on "Allocate marker" "button" in the "sticky-footer" "region"
And I click on "Allocate marker" "button" in the ".modal-footer" "css_element"
And I select "Teacher One" from the "Allocated marker 1" singleselect
And I select "Teacher Two" from the "Allocated marker 2" singleselect
And I press "Save changes"
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | Teacher One | Teacher Two | Not marked |
| Student Two | Teacher One | Teacher Two | Not marked |
| Student Three | | | Not marked |
| Student Four | | | Not marked |
# Then allocate marks to the student submissions as teacher1.
And I go to "Student One" "Assignment 1" activity advanced marking page
And I set the field "Mark out of 100" to "99"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I go to "Student Two" "Assignment 1" activity advanced marking page
And I set the field "Mark out of 100" to "11"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I navigate to "Submissions" in current page administration
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | 99 | | In marking |
| Student Two | 11 | | In marking |
| Student Three | | | Not marked |
| Student Four | | | Not marked |
# Then allocate marks to the student submissions as teacher2.
And I am on the "A1" "assign activity" page logged in as teacher2
And I go to "Student One" "Assignment 1" activity advanced marking page
And I set the field "Mark out of 100" to "88"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I go to "Student Two" "Assignment 1" activity advanced marking page
And I set the field "Mark out of 100" to "22"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A1" "assign activity" page
And I navigate to "Submissions" in current page administration
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status | Grade |
| Student One | 99 | 88 | Marking completed | 94 |
| Student Two | 11 | 22 | Marking completed | 17 |
| Student Three | | | Not marked | |
| Student Four | | | Not marked | |
# Then we check the calculated final grade and release them to the students.
And I am on the "A1" "assign activity" page logged in as teacher1
And I navigate to "Submissions" in current page administration
And I set the field "selectall" to "1"
And I click on "Change marking state" "button" in the "sticky-footer" "region"
And I click on "Change marking state" "button" in the "Set marking workflow state" "dialogue"
And I set the field "Workflow context" to "Grade"
And I set the field "Marking workflow state" to "Released"
And I press "Save changes"
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status | Final grade |
| Student One | 99 | 88 | Released | 94 |
| Student Two | 11 | 22 | Released | 17 |
| Student Three | | | Released | |
| Student Four | | | Released | |
Scenario: Complete workflow of multi-marking with group submissions
# Firstly, allocate the markers to the students (to test thoroughly, one group will have the same marker
# for both students. The other group won't).
Given I am on the "A2" "assign activity" page logged in as teacher1
And I change window size to "large"
And I navigate to "Submissions" in current page administration
And I click on "Quick grading" "checkbox"
When I set the field "Allocated marker 1" in the "Student One" "table_row" to "Teacher One"
And I set the field "Allocated marker 1" in the "Student Two" "table_row" to "Teacher One"
And I set the field "Allocated marker 1" in the "Student Three" "table_row" to "Teacher One"
And I set the field "Allocated marker 1" in the "Student Four" "table_row" to "Teacher One"
And I set the field "Allocated marker 2" in the "Student One" "table_row" to "Teacher Two"
And I set the field "Allocated marker 2" in the "Student Two" "table_row" to "Teacher Two"
And I set the field "Allocated marker 2" in the "Student Three" "table_row" to "Teacher Two"
And I set the field "Allocated marker 2" in the "Student Four" "table_row" to "Teacher Three"
And I click on "Save" "button" in the "sticky-footer" "region"
And I press "Continue"
And I click on "Quick grading" "checkbox"
Then the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | Teacher One | Teacher Two | |
| Student Two | Teacher One | Teacher Two | |
| Student Three | Teacher One | Teacher Two | |
| Student Four | Teacher One | Teacher Three | |
# Next we test adding a mark as Marker 1 (teacher1) to a student in Group 1. This should populate to the other
# student in Group 1, but not the student with this same marker, who is not in Group 1.
And I go to "Student One" "Assignment 2" activity advanced marking page
And I set the field "Mark out of 100" to "50"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I go to "Student Four" "Assignment 2" activity advanced marking page
And I set the field "Mark out of 100" to "60"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A2" "assign activity" page
And I navigate to "Submissions" in current page administration
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | 50 | | In marking |
| Student Two | 50 | | In marking |
| Student Three | 60 | | In marking |
| Student Four | 60 | | In marking |
# Next we add a mark as teacher2 to a student in both groups. The mark given to the student in Group 1 should
# populate to the other student in Group 1. The mark given to the student in Group 2 should not, as they are not
# an allocated marker for that final student.
And I am on the "A2" "assign activity" page logged in as teacher2
And I go to "Student One" "Assignment 2" activity advanced marking page
And I set the field "Mark out of 100" to "30"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A2" "assign activity" page
And I go to "Student Three" "Assignment 2" activity advanced marking page
And I set the field "Mark out of 100" to "15"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A2" "assign activity" page
And I navigate to "Submissions" in current page administration
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | 50 | 30 | Marking completed |
| Student Two | 50 | 30 | Marking completed |
| Student Three | 60 | 15 | Marking completed |
| Student Four | 60 | | In marking |
# Then we login as teacher3 and add that last mark to the final student.
And I am on the "A2" "assign activity" page logged in as teacher3
And I go to "Student Four" "Assignment 2" activity advanced marking page
And I set the field "Mark out of 100" to "99"
And I set the field "Marking workflow state" to "Marking completed"
And I press "Save changes"
And I am on the "A2" "assign activity" page
And I navigate to "Submissions" in current page administration
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status |
| Student One | 50 | 30 | Marking completed |
| Student Two | 50 | 30 | Marking completed |
| Student Three | 60 | 15 | Marking completed |
| Student Four | 60 | 99 | Marking completed |
# Then we check that the grades have been calculated correctly.
And I am on the "A2" "assign activity" page logged in as teacher1
And I navigate to "Submissions" in current page administration
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status | Grade |
| Student One | 50 | 30 | Marking completed | 50 |
| Student Two | 50 | 30 | Marking completed | 50 |
| Student Three | 60 | 15 | Marking completed | 60 |
| Student Four | 60 | 99 | Marking completed | 99 |
# Then we release them all and check the final grade column.
And I am on the "A2" "assign activity" page logged in as teacher1
And I navigate to "Submissions" in current page administration
And I set the field "selectall" to "1"
And I click on "Change marking state" "button" in the "sticky-footer" "region"
And I click on "Change marking state" "button" in the "Set marking workflow state" "dialogue"
And I set the field "Workflow context" to "Grade"
And I set the field "Marking workflow state" to "Released"
And I press "Save changes"
And the following should exist in the "submissions" table:
| First name | Marker 1 | Marker 2 | Status | Final grade |
| Student One | 50 | 30 | Released | 50 |
| Student Two | 50 | 30 | Released | 50 |
| Student Three | 60 | 15 | Released | 60 |
| Student Four | 60 | 100 | Released | 99 |
@@ -1076,7 +1076,8 @@ final class events_test extends \advanced_testcase {
$course = $this->getDataGenerator()->create_course();
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$assign = $this->create_instance($course);
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
$assign = $this->create_instance($course, ['markercount' => 1]);
// Trigger and capture the event.
$sink = $this->redirectEvents();
+3 -8
View File
@@ -735,7 +735,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
$userflag->mailed = 0;
$userflag->extensionduedate = 0;
$userflag->workflowstate = 'inmarking';
$userflag->allocatedmarker = $USER->id;
$DB->insert_record('assign_user_flags', $userflag);
@@ -757,7 +756,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
$this->assertEquals(0, $userflag['mailed']);
$this->assertEquals(0, $userflag['extensionduedate']);
$this->assertEquals('inmarking', $userflag['workflowstate']);
$this->assertEquals($USER->id, $userflag['allocatedmarker']);
}
/**
@@ -1742,7 +1740,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
$userflags = array();
$userflag['userid'] = $student->id;
$userflag['workflowstate'] = 'inmarking';
$userflag['allocatedmarker'] = $USER->id;
$userflags = array($userflag);
$createduserflags = mod_assign_external::set_user_flags($assign->id, $userflags);
@@ -1759,7 +1756,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
$this->assertEquals(2, $createduserflag->mailed);
$this->assertEquals(0, $createduserflag->extensionduedate);
$this->assertEquals('inmarking', $createduserflag->workflowstate);
$this->assertEquals($USER->id, $createduserflag->allocatedmarker);
// Create update data.
$userflags = array();
@@ -1781,7 +1777,6 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
$this->assertEquals(2, $updateduserflag->mailed);
$this->assertEquals(0, $updateduserflag->extensionduedate);
$this->assertEquals('readyforreview', $updateduserflag->workflowstate);
$this->assertEquals($USER->id, $updateduserflag->allocatedmarker);
}
/**
@@ -2824,7 +2819,7 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
$DB->update_record('user', $student);
$this->setUser($teacher);
$participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, true, true);
$participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, true, true, false);
$participants = external_api::clean_returnvalue(mod_assign_external::list_participants_returns(), $participants);
$this->assertCount(1, $participants);
@@ -2843,7 +2838,7 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
$this->assertEquals('', $participant['submissionstatus']);
$this->assertArrayHasKey('enrolledcourses', $participant);
$participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, false, true);
$participants = mod_assign_external::list_participants($assignment->id, 0, '', 0, 0, false, false, true, false);
$participants = external_api::clean_returnvalue(mod_assign_external::list_participants_returns(), $participants);
// Check that the list of courses the participant is enrolled is not returned.
$participant = $participants[0];
@@ -2919,7 +2914,7 @@ final class externallib_test extends \mod_assign\externallib_advanced_testcase {
$this->setUser($teacher);
// Test mod_assign_external::list_participants.
$participants = mod_assign_external::list_participants($assignmodule->id, $group->id, '', 0, 0, false, true, true);
$participants = mod_assign_external::list_participants($assignmodule->id, $group->id, '', 0, 0, false, true, true, false);
$participants = external_api::clean_returnvalue(mod_assign_external::list_participants_returns(), $participants);
$this->assertEquals($group->id, $participants[0]['groupid']);
$this->assertEquals(format_string($gname, true), $participants[0]['groupname']);
+1 -1
View File
@@ -75,7 +75,7 @@ class mod_assign_testable_assign extends assign {
$data = array();
$data['id'] = $this->get_course_module()->id;
$data['selectedusers'] = $selectedusers;
$data['allocatedmarker'] = $markerid;
$data['allocatedmarker1'] = $markerid;
$data['action'] = 'setbatchmarkingallocation';
mod_assign_batch_set_allocatedmarker_form::mock_submit($data);
@@ -43,6 +43,12 @@ class behat_mod_assign_generator extends behat_generator_base {
'required' => ['assign', 'user', 'extensionduedate'],
'switchids' => ['assign' => 'cmid', 'user' => 'userid'],
],
'marker_allocations' => [
'singular' => 'marker_allocation',
'datagenerator' => 'marker_allocation',
'required' => ['assign', 'user', 'marker'],
'switchids' => ['assign' => 'cmid', 'user' => 'userid', 'marker' => 'markerid'],
],
];
}
@@ -55,4 +61,15 @@ class behat_mod_assign_generator extends behat_generator_base {
protected function get_assign_id(string $identifier): int {
return $this->get_cm_by_activity_name('assign', $identifier)->id;
}
/**
* Get the user id for the marker based on their username.
*
* @param string $username The user's username
* @return int The user's ID
*/
protected function get_marker_id(string $username): int {
return $this->get_user_id($username);
}
}
+35
View File
@@ -59,6 +59,9 @@ class mod_assign_generator extends testing_module_generator {
'maxattempts' => 1,
'markingworkflow' => 0,
'markingallocation' => 0,
'markercount' => 0,
'multimarkmethod' => null,
'multimarkrounding' => null,
'markinganonymous' => 0,
'activityformat' => 0,
'timelimit' => 0,
@@ -203,4 +206,36 @@ class mod_assign_generator extends testing_module_generator {
$DB->insert_record('assign_overrides', (object) $data);
}
/**
* Create a marker allocation record.
*
* @param array $data Array containing: ['assignid', 'userid', 'markerid']
*/
public function create_marker_allocation(array $data): void {
global $DB;
if (!isset($data['cmid'])) {
throw new coding_exception('Must specify assign when creating a marker allocation.');
}
if (!isset($data['userid'])) {
throw new coding_exception('Must specify user when creating a marker allocation.');
}
if (!isset($data['markerid'])) {
throw new coding_exception('Must specify marker when creating a marker allocation.');
}
[$course, $cm] = get_course_and_cm_from_cmid($data['cmid'], 'assign');
$context = context_module::instance($cm->id);
$assign = new assign($context, $cm, $course);
$DB->insert_record('assign_allocated_marker', [
'student' => $data['userid'],
'assignment' => $assign->get_instance()->id,
'marker' => $data['markerid'],
]);
}
}
-40
View File
@@ -3349,46 +3349,6 @@ You can see it appended to your <a href="' . $assignurl .
$this->assertEquals(50, (int)$grades[$student->id]->rawgrade);
}
/**
* Test that a student allocated a specific marker is only shown to that marker.
*/
public function test_markerallocation(): void {
global $PAGE;
$this->resetAfterTest();
$course = $this->getDataGenerator()->create_course();
$student = $this->getDataGenerator()->create_and_enrol($course, 'student');
$teacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
$otherteacher = $this->getDataGenerator()->create_and_enrol($course, 'teacher');
$assign = $this->create_instance($course, [
'markingworkflow' => 1,
'markingallocation' => 1,
]);
$PAGE->set_url(new \moodle_url('/mod/assign/view.php', ['id' => $assign->get_course_module()->id]));
// Allocate marker to submission.
$this->mark_submission($teacher, $assign, $student, null, [
'allocatedmarker' => $teacher->id,
]);
// Check the allocated marker can view the submission.
$this->setUser($teacher);
$users = $assign->list_participants(0, true);
$this->assertEquals(1, count($users));
$this->assertTrue(isset($users[$student->id]));
$cm = get_coursemodule_from_instance('assign', $assign->get_instance()->id);
$context = \context_module::instance($cm->id);
$assign = new mod_assign_testable_assign($context, $cm, $course);
// Check that other teachers can't view this submission.
$this->setUser($otherteacher);
$users = $assign->list_participants(0, true);
$this->assertEquals(0, count($users));
}
/**
* Ensure that a teacher cannot submit for students as standard.
*/
+508 -34
View File
@@ -16,11 +16,14 @@
namespace mod_assign;
use assign;
defined('MOODLE_INTERNAL') || die();
global $CFG;
require_once($CFG->dirroot . '/lib/accesslib.php');
require_once($CFG->dirroot . '/course/lib.php');
require_once($CFG->dirroot . '/mod/assign/locallib.php');
/**
* Unit tests for (some of) mod/assign/markerallocaion_test.php.
@@ -29,6 +32,7 @@ require_once($CFG->dirroot . '/course/lib.php');
* @category test
* @copyright 2017 Andrés Melo <[email protected]>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @coversDefaultClass \assign
*/
final class markerallocation_test extends \advanced_testcase {
@@ -36,20 +40,24 @@ final class markerallocation_test extends \advanced_testcase {
private $course;
/**
* Create all the needed elements to test the difference between both functions.
* @var array Generated users
*/
public function test_markerusers(): void {
$this->resetAfterTest();
global $DB;
private array $users = [];
// Create a course, by default it is created with 5 sections.
$this->course = $this->getDataGenerator()->create_course();
// Setting assing module, markingworkflow and markingallocation set to 1 to enable marker allocation.
$record = new \stdClass();
$record->course = $this->course;
/**
* @var array Generated groups
*/
private array $groups = [];
/**
* Create the assignment object for testing.
*
* @param array $args Array of options that can be overwritten.
* @return assign
*/
private function create_assignment(array $args = []): assign {
$modulesettings = [
'course' => $this->course->id,
'alwaysshowdescription' => 1,
'submissiondrafts' => 1,
'requiresubmissionstatement' => 0,
@@ -58,63 +66,206 @@ final class markerallocation_test extends \advanced_testcase {
'sendlatenotifications' => 0,
'duedate' => 0,
'allowsubmissionsfromdate' => 0,
'grade' => 100,
'grade' => (!isset($args['scale'])) ? 100 : null,
'cutoffdate' => 0,
'teamsubmission' => 0,
'teamsubmission' => ($args['teamsubmission']) ?? 0,
'requireallteammemberssubmit' => 0,
'teamsubmissiongroupingid' => 0,
'blindmarking' => 0,
'attemptreopenmethod' => 'untilpass',
'maxattempts' => 1,
'markingworkflow' => 1,
'markingallocation' => 1,
'markercount' => ($args['markercount']) ?? ASSIGN_MULTIMARKING_DEFAULT_MARKERS,
'multimarkmethod' => ($args['multimarkmethod']) ?? ASSIGN_MULTIMARKING_METHOD_MANUAL,
'multimarkrounding' => ($args['multimarkrounding']) ?? null,
];
$assignelement = $this->getDataGenerator()->create_module('assign', $record, $modulesettings);
if (isset($args['scale'])) {
$scale = $this->getDataGenerator()->create_scale();
$modulesettings['gradetype'] = GRADE_TYPE_SCALE;
$modulesettings['gradescale'] = $scale->id;
}
$coursesectionid = course_add_cm_to_section($this->course->id, $assignelement->id, 1, null, 'assign');
$generator = $this->getDataGenerator()->get_plugin_generator('mod_assign');
$instance = $generator->create_instance($modulesettings);
[$course, $cm] = get_course_and_cm_from_instance($instance->id, 'assign');
$context = \core\context\module::instance($cm->id);
$assignment = new assign($context, $cm, $course);
return $assignment;
}
/**
* Setup all required test data.
*/
private function setup_data(): void {
global $DB;
$this->resetAfterTest();
// Create a course, by default it is created with 5 sections.
$this->course = $this->getDataGenerator()->create_course();
// Adding users to the course.
$userdata = array();
$userdata['firstname'] = 'teacher1';
$userdata['lasttname'] = 'lastname_teacher1';
$user1 = $this->getDataGenerator()->create_user($userdata);
$this->getDataGenerator()->enrol_user($user1->id, $this->course->id, 'teacher');
$this->users[0] = $this->getDataGenerator()->create_user($userdata);
$this->getDataGenerator()->enrol_user($this->users[0]->id, $this->course->id, 'editingteacher');
$userdata = array();
$userdata['firstname'] = 'teacher2';
$userdata['lasttname'] = 'lastname_teacher2';
$user2 = $this->getDataGenerator()->create_user($userdata);
$this->getDataGenerator()->enrol_user($user2->id, $this->course->id, 'teacher');
$this->users[1] = $this->getDataGenerator()->create_user($userdata);
$this->getDataGenerator()->enrol_user($this->users[1]->id, $this->course->id, 'editingteacher');
$userdata = array();
$userdata['firstname'] = 'student';
$userdata['lasttname'] = 'lastname_student';
$user3 = $this->getDataGenerator()->create_user($userdata);
$this->getDataGenerator()->enrol_user($user3->id, $this->course->id, 'student');
$this->users[2] = $this->getDataGenerator()->create_user($userdata);
$this->getDataGenerator()->enrol_user($this->users[2]->id, $this->course->id, 'student');
// Adding manager to the system.
$userdata = array();
$userdata['firstname'] = 'Manager';
$userdata['lasttname'] = 'lastname_Manager';
$user4 = $this->getDataGenerator()->create_user($userdata);
// Getting id of manager role.
$this->users[3] = $this->getDataGenerator()->create_user($userdata);
$managerrole = $DB->get_record('role', array('shortname' => 'manager'));
if (!empty($managerrole)) {
// By default the context of the system is assigned.
$idassignment = $this->getDataGenerator()->role_assign($managerrole->id, $user4->id);
$this->getDataGenerator()->role_assign($managerrole->id, $this->users[3]->id);
}
}
/**
* Setup group data for teamsubmission tests.
*/
private function setup_group_data(): void {
$this->resetAfterTest(false);
// Create a course, by default it is created with 5 sections.
$this->course = $this->getDataGenerator()->create_course();
// Split users into seaprate arrays for easier use here.
$teachers = [];
$students = [];
// Adding teachers to the course.
for ($i = 1; $i <= 2; $i++) {
$userdata = [];
$userdata['firstname'] = 'teacher' . $i;
$userdata['lasttname'] = 'lastname_teacher' . $i;
$teachers[$i] = $this->getDataGenerator()->create_user($userdata);
$this->getDataGenerator()->enrol_user($teachers[$i]->id, $this->course->id, 'teacher');
}
$oldusers = array($user1, $user2, $user4);
$newusers = array($user1, $user2);
// Adding students to the course.
for ($i = 1; $i <= 6; $i++) {
$userdata = [];
$userdata['firstname'] = 'student' . $i;
$userdata['lasttname'] = 'lastname_student' . $i;
$students[$i] = $this->getDataGenerator()->create_user($userdata);
$this->getDataGenerator()->enrol_user($students[$i]->id, $this->course->id, 'student');
}
// Adding students to groups.
$this->groups['A'] = $this->getDataGenerator()->create_group(['courseid' => $this->course->id, 'name' => 'A']);
$this->groups['B'] = $this->getDataGenerator()->create_group(['courseid' => $this->course->id, 'name' => 'B']);
foreach ($students as $studentnumber => $user) {
if ($studentnumber <= 3) {
groups_add_member($this->groups['A'], $user);
} else {
groups_add_member($this->groups['B'], $user);
}
}
$this->users = ['students' => $students, 'teachers' => $teachers];
}
/**
* Test marker allocation and marking with group submissions.
*
* @covers ::update_allocated_markers, ::save_grade
*/
public function test_allocated_markers_with_group_submissions(): void {
$this->setup_group_data();
$assignment = $this->create_assignment([
'teamsubmission' => 1,
]);
// To test the logic that a marker should not be able to update anyone not in their group
// we will use the "public" method `save_grade` instead of the internal `update_mark`.
// Firstly, allocate teacher1 to every student in group A.
foreach ($this->users['students'] as $studentnumber => $student) {
if ($studentnumber <= 3) {
$assignment->update_allocated_markers($student->id, [$this->users['teachers'][1]->id]);
}
}
// Allocate a mark to the first student in the group.
// This should spread out to the other students in the group as well.
$this->setUser($this->users['teachers'][1]);
// Before we save it, we need to create the submission record, which won't happen from just saving it.
// We are passing -1 as userid because it's a required argument, but if the groupid is present, then
// the `get_group_submission` function ignores it, so it just needs any value really.
$assignment->get_group_submission(-1, $this->groups['A']->id, true);
// Then save it.
$assignment->save_grade($this->users['students'][1]->id, (object)[
'mark' => 50,
'applytoall' => 1,
'attemptnumber' => -1,
]);
// All 3 students in the group should now have the same mark from this allocated marker.
foreach ($this->users['students'] as $studentnumber => $student) {
if ($studentnumber <= 3) {
$gradeobject = $assignment->get_user_grade($student->id, true);
$mark = $assignment->get_mark($gradeobject->id, $this->users['teachers'][1]->id);
$this->assertEquals(50, $mark->mark);
}
}
// Now allocate teacher2 to 2 out of 3 students in group B.
foreach ($this->users['students'] as $studentnumber => $student) {
if ($studentnumber > 3 && $studentnumber < 6) {
$assignment->update_allocated_markers($student->id, [$this->users['teachers'][2]->id]);
}
}
// Allocate a mark to the first student in the group.
$this->setUser($this->users['teachers'][2]);
$assignment->get_group_submission(-1, $this->groups['B']->id, true);
$assignment->save_grade($this->users['students'][4]->id, (object)[
'mark' => 99,
'applytoall' => 1,
'attemptnumber' => -1,
]);
// Only 2 out of 3 students should have the grade applied.
foreach ($this->users['students'] as $studentnumber => $student) {
if ($studentnumber > 3) {
$gradeobject = $assignment->get_user_grade($student->id, true);
$mark = $assignment->get_mark($gradeobject->id, $this->users['teachers'][2]->id);
if ($studentnumber < 6) {
$this->assertEquals(99, $mark->mark);
} else {
$this->assertFalse($mark);
}
}
}
}
/**
* Create all the needed elements to test the difference between both functions.
*
* @coversNothing
*/
public function test_markerusers(): void {
$this->setup_data();
$oldusers = [$this->users[0], $this->users[1], $this->users[3]];
$newusers = [$this->users[0], $this->users[1]];
list($sort, $params) = users_order_by_sql('u');
@@ -140,6 +291,329 @@ final class markerallocation_test extends \advanced_testcase {
}
$this->assertEquals(count($oldmarkers), 0);
}
/**
* Test functionality around having multiple allocated markers.
*
* @covers ::update_allocated_markers, ::update_mark
*/
public function test_multiple_marker_allocation(): void {
$this->setup_data();
$assignment = $this->create_assignment();
// To start with, confirm that no markers are allocated to the student submission.
$markers = $assignment->get_allocated_markers($this->users[2]->id);
$this->assertCount(0, $markers);
// Allocate both teachers to the student assignment.
$assignment->update_allocated_markers($this->users[2]->id, [
$this->users[0]->id,
$this->users[1]->id,
]);
$markers = $assignment->get_allocated_markers($this->users[2]->id);
$this->assertCount(2, $markers);
// Now test that we can add a mark to the submission.
// Firstly, there should be no mark currently for either marker.
$gradeobject = $assignment->get_user_grade($this->users[2]->id, true);
$mark = $assignment->get_mark($gradeobject->id, $this->users[0]->id);
$this->assertFalse($mark);
// Assign a mark as teacher1.
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, 99);
// Now check that we can find the mark.
$mark = $assignment->get_mark($gradeobject->id, $this->users[0]->id);
$this->assertEquals("99.00000", $mark->mark);
// Assign a mark as teacher2.
$gradeobject->grader = $this->users[1]->id;
$assignment->update_mark($gradeobject, 11);
// Now check that we can find the mark.
$mark = $assignment->get_mark($gradeobject->id, $this->users[1]->id);
$this->assertEquals("11.00000", $mark->mark);
}
/**
* Test manual calculation of final grade.
*
* @covers ::update_mark
*/
public function test_calculated_marker_grade_manual(): void {
$this->setup_data();
$assignment = $this->create_assignment();
// Allocate both teachers to the student assignment.
$assignment->update_allocated_markers($this->users[2]->id, [
$this->users[0]->id,
$this->users[1]->id,
]);
$gradeobject = $assignment->get_user_grade($this->users[2]->id, true);
// Assign a mark as teacher1.
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, 99);
// Assign a mark as teacher2.
$gradeobject->grader = $this->users[1]->id;
$assignment->update_mark($gradeobject, 11);
// With manual calculation, there should be no grade set yet.
$gradeobject = $assignment->get_user_grade($this->users[2]->id, false);
$this->assertEquals(-1, $gradeobject->grade);
}
/**
* Test "maximum" calculation of final grade when using scale grading.
*
* @covers ::update_mark
*/
public function test_calculated_marker_grade_maximum(): void {
$this->setup_data();
$assignment = $this->create_assignment([
'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_MAX,
]);
// Allocate both teachers to the student assignment.
$assignment->update_allocated_markers($this->users[2]->id, [
$this->users[0]->id,
$this->users[1]->id,
]);
$gradeobject = $assignment->get_user_grade($this->users[2]->id, true);
// Assign a mark as teacher1.
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, 11);
// Assign a mark as teacher2.
$gradeobject->grader = $this->users[1]->id;
$assignment->update_mark($gradeobject, 99);
// With max calculation, the grade should be the highest one.
$gradeobject = $assignment->get_user_grade($this->users[2]->id, false);
$this->assertEquals(99, $gradeobject->grade);
}
/**
* Test "average" calculation of final grade when using rounding of "none".
*
* @covers ::update_mark
*/
public function test_calculated_marker_grade_average_round_none(): void {
$this->setup_data();
$assignment = $this->create_assignment([
'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE,
'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_NONE,
]);
// Allocate both teachers to the student assignment.
$assignment->update_allocated_markers($this->users[2]->id, [
$this->users[0]->id,
$this->users[1]->id,
]);
$gradeobject = $assignment->get_user_grade($this->users[2]->id, true);
// Assign a mark as teacher1.
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, 90);
// Assign a mark as teacher2.
$gradeobject->grader = $this->users[1]->id;
$assignment->update_mark($gradeobject, 25);
// With avg calculation and no rounding, the grade should be 57.5.
$gradeobject = $assignment->get_user_grade($this->users[2]->id, false);
$this->assertEquals(57.5, $gradeobject->grade);
}
/**
* Test "average" calculation of final grade when using rounding of "down".
*
* @covers ::update_mark
*/
public function test_calculated_marker_grade_average_rounding_down(): void {
$this->setup_data();
$assignment = $this->create_assignment([
'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE,
'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_DOWN,
]);
// Allocate both teachers to the student assignment.
$assignment->update_allocated_markers($this->users[2]->id, [
$this->users[0]->id,
$this->users[1]->id,
]);
$gradeobject = $assignment->get_user_grade($this->users[2]->id, true);
// Assign a mark as teacher1.
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, 90);
// Assign a mark as teacher2.
$gradeobject->grader = $this->users[1]->id;
$assignment->update_mark($gradeobject, 25);
// With avg calculation and down rounding, the grade should be 57.
$gradeobject = $assignment->get_user_grade($this->users[2]->id, false);
$this->assertEquals(57, $gradeobject->grade);
}
/**
* Test that the grade calculation from marks using method "average" with up rounding, sets the correct grade.
*
* @covers ::update_mark
*/
public function test_calculated_marker_grade_average_round_up(): void {
$this->setup_data();
$assignment = $this->create_assignment([
'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE,
'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_UP,
]);
// Allocate both teachers to the student assignment.
$assignment->update_allocated_markers($this->users[2]->id, [
$this->users[0]->id,
$this->users[1]->id,
]);
$gradeobject = $assignment->get_user_grade($this->users[2]->id, true);
// Assign a mark as teacher1.
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, 90);
// Assign a mark as teacher2.
$gradeobject->grader = $this->users[1]->id;
$assignment->update_mark($gradeobject, 25);
// With avg calculation and up rounding, the grade should be 58.
$gradeobject = $assignment->get_user_grade($this->users[2]->id, false);
$this->assertEquals(58, $gradeobject->grade);
}
/**
* Test that the grade calculation from marks using method "average" with natural rounding, sets the correct grade.
*
* @covers ::update_mark
*/
public function test_calculated_marker_grade_average_round_natural(): void {
$this->setup_data();
$assignment = $this->create_assignment([
'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE,
'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL,
]);
// Allocate both teachers to the student assignment.
$assignment->update_allocated_markers($this->users[2]->id, [
$this->users[0]->id,
$this->users[1]->id,
]);
$gradeobject = $assignment->get_user_grade($this->users[2]->id, true);
// Assign a mark as teacher1.
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, 90);
// Assign a mark as teacher2.
$gradeobject->grader = $this->users[1]->id;
$assignment->update_mark($gradeobject, 25);
// With avg calculation and natural rounding, the grade should be 58.
$gradeobject = $assignment->get_user_grade($this->users[2]->id, false);
$this->assertEquals(58, $gradeobject->grade);
}
/**
* Test that the workflow state changes on the overall grade based on marker states.
*
* @covers ::update_mark, ::calculate_and_save_overall_workflow_state
*/
public function test_calculated_marker_workflow(): void {
$this->setup_data();
$assignment = $this->create_assignment();
// Allocate both teachers to the student assignment.
$assignment->update_allocated_markers($this->users[2]->id, [
$this->users[0]->id,
$this->users[1]->id,
]);
// First confirm that the overall grade workflow state is not set.
$flags = $assignment->get_user_flags($this->users[2]->id, true);
$this->assertEmpty($flags->workflowstate);
// One marker then sets their mark to be in the state "In Marking".
$gradeobject = $assignment->get_user_grade($this->users[2]->id, true);
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, null, ASSIGN_MARKING_WORKFLOW_STATE_INMARKING);
$assignment->calculate_and_save_overall_workflow_state($gradeobject, $flags, $flags->workflowstate);
// Re-check the overall workflow. This should now be "In Marking" as well.
$flags = $assignment->get_user_flags($this->users[2]->id, true);
$this->assertEquals(ASSIGN_MARKING_WORKFLOW_STATE_INMARKING, $flags->workflowstate);
// Now this teacher marks theirs as "Marking Complete".
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, 90, ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
$assignment->calculate_and_save_overall_workflow_state($gradeobject, $flags, $flags->workflowstate);
// Nothing should change on the overall state, that should still be In Marking.
$flags = $assignment->get_user_flags($this->users[2]->id, true);
$this->assertEquals(ASSIGN_MARKING_WORKFLOW_STATE_INMARKING, $flags->workflowstate);
// Now the second marker sets theirs as "Marking Complete".
$gradeobject->grader = $this->users[1]->id;
$assignment->update_mark($gradeobject, 70, ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW);
$assignment->calculate_and_save_overall_workflow_state($gradeobject, $flags, $flags->workflowstate);
// Now that both are complete, the overall state should be the same.
$flags = $assignment->get_user_flags($this->users[2]->id, true);
$this->assertEquals(ASSIGN_MARKING_WORKFLOW_STATE_READYFORREVIEW, $flags->workflowstate);
}
/**
* Test that when we remove a marker their marks are not counted towards anything.
*
* @covers ::update_mark
*/
public function test_unallocated_marker_not_included_in_mark_calculations(): void {
$this->setup_data();
$assignment = $this->create_assignment([
'multimarkmethod' => ASSIGN_MULTIMARKING_METHOD_AVERAGE,
'multimarkrounding' => ASSIGN_MULTIMARKING_AVERAGE_ROUND_NATURAL,
]);
// Allocate both teachers to the student assignment.
$assignment->update_allocated_markers($this->users[2]->id, [
$this->users[0]->id,
$this->users[1]->id,
]);
$gradeobject = $assignment->get_user_grade($this->users[2]->id, true);
// Assign a mark as teacher1.
$gradeobject->grader = $this->users[0]->id;
$assignment->update_mark($gradeobject, 90);
// Now we remove teacher1 and add manager instead. So we have manager and teacher2 as the markers.
$assignment->update_allocated_markers($this->users[2]->id, [$this->users[3]->id, $this->users[1]->id]);
// Now add a marker from teacher2.
$gradeobject->grader = $this->users[1]->id;
$assignment->update_mark($gradeobject, 10);
// At this point, though we've had 2 marks, only 1 of the allocated markers has marked.
// So the grade should not be set.
$gradeobject = $assignment->get_user_grade($this->users[2]->id, false);
$this->assertEquals(-1, $gradeobject->grade);
}
}
@@ -210,7 +210,10 @@ final class provider_test extends provider_testcase {
'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
'maxattempts' => 3,
'assignsubmission_onlinetext_enabled' => true,
'assignfeedback_comments_enabled' => true
'assignfeedback_comments_enabled' => true,
'markingworkflow' => 1,
'markingallocation' => 1,
'markercount' => 2,
]);
$context = $assign->get_context();
@@ -253,6 +256,14 @@ final class provider_test extends provider_testcase {
// Give the submission a grade.
$assign->save_grade($user->id, $data);
// Allocate the teacher as a marker on the assignment.
$assign->update_allocated_markers($user->id, [$teacher->id]);
// Give the submission a mark.
$gradeobject = $assign->get_user_grade($user->id, true);
$gradeobject->grader = $teacher->id;
$assign->update_mark($gradeobject, 99.9);
/** @var \core_privacy\tests\request\content_writer $writer */
$writer = writer::with_context($context);
$this->assertFalse($writer->has_any_data());
@@ -284,6 +295,12 @@ final class provider_test extends provider_testcase {
$overrideexport->cutoffdate);
$this->assertEquals(\core_privacy\local\request\transform::datetime($overridedata->allowsubmissionsfromdate),
$overrideexport->allowsubmissionsfromdate);
// Check allocated markers and marks are exported.
$this->assertEquals($teacher->id, $writer->get_data(['Marker allocations'])->data[0]['marker']);
$this->assertEquals($user->id, $writer->get_data(['Marker allocations'])->data[0]['student']);
$this->assertEquals($teacher->id, $writer->get_data(['Marks'])->data[0]['marker']);
$this->assertEquals(99.9, $writer->get_data(['Marks'])->data[0]['mark']);
}
/**
@@ -306,7 +323,10 @@ final class provider_test extends provider_testcase {
'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
'maxattempts' => 3,
'assignsubmission_onlinetext_enabled' => true,
'assignfeedback_comments_enabled' => true
'assignfeedback_comments_enabled' => true,
'markingworkflow' => 1,
'markingallocation' => 1,
'markercount' => 2,
]);
$context = $assign->get_context();
@@ -366,6 +386,18 @@ final class provider_test extends provider_testcase {
$flagdata->extensionduedate = $duedate;
$assign->update_user_flags($flagdata);
// Allocate the teacher as a marker on the assignment for both users.
$assign->update_allocated_markers($user1->id, [$teacher->id]);
$assign->update_allocated_markers($user2->id, [$teacher->id]);
// Give the submission a mark for both users.
$gradeobject = $assign->get_user_grade($user1->id, true);
$gradeobject->grader = $teacher->id;
$assign->update_mark($gradeobject, 11.1);
$gradeobject = $assign->get_user_grade($user2->id, true);
$gradeobject->grader = $teacher->id;
$assign->update_mark($gradeobject, 88.8);
/** @var \core_privacy\tests\request\content_writer $writer */
$writer = writer::with_context($context);
$this->assertFalse($writer->has_any_data());
@@ -393,6 +425,14 @@ final class provider_test extends provider_testcase {
'Feedback comments'])->commenttext);
$this->assertStringContainsString($teachercommenttext3, $writer->get_data(['studentsubmissions', $user2->id, 'attempt 2',
'Feedback comments'])->commenttext);
// Check for marker allocations as this teacher.
$this->assertEquals($teacher->id, $writer->get_data(['Marker allocations'])->data[0]['marker']);
$this->assertEquals($teacher->id, $writer->get_data(['Marker allocations'])->data[1]['marker']);
// And check for marks they have given.
$this->assertEquals($teacher->id, $writer->get_data(['Marks'])->data[0]['marker']);
$this->assertEquals($teacher->id, $writer->get_data(['Marks'])->data[1]['marker']);
}
/**
@@ -415,7 +455,10 @@ final class provider_test extends provider_testcase {
'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
'maxattempts' => 3,
'assignsubmission_onlinetext_enabled' => true,
'assignfeedback_comments_enabled' => true
'assignfeedback_comments_enabled' => true,
'markingworkflow' => 1,
'markingallocation' => 1,
'markercount' => 2,
]);
$context = $assign->get_context();
@@ -478,6 +521,18 @@ final class provider_test extends provider_testcase {
// Give the submission a grade.
$assign->save_grade($user2->id, $data);
// Allocate the teacher as a marker on the assignment for both users.
$assign->update_allocated_markers($user1->id, [$teacher->id]);
$assign->update_allocated_markers($user2->id, [$teacher->id]);
// Give the submission a mark for both users.
$gradeobject = $assign->get_user_grade($user1->id, true);
$gradeobject->grader = $teacher->id;
$assign->update_mark($gradeobject, 11.1);
$gradeobject = $assign->get_user_grade($user2->id, true);
$gradeobject->grader = $teacher->id;
$assign->update_mark($gradeobject, 88.8);
// Delete all user data for this assignment.
provider::delete_data_for_all_users_in_context($context);
@@ -490,6 +545,10 @@ final class provider_test extends provider_testcase {
$this->assertEmpty($records);
$records = $DB->get_records('assignfeedback_comments');
$this->assertEmpty($records);
$records = $DB->get_records('assign_mark');
$this->assertEmpty($records);
$records = $DB->get_records('assign_allocated_marker');
$this->assertEmpty($records);
// Check that overrides and the calendar events are deleted.
$records = $DB->get_records('event');
@@ -520,7 +579,10 @@ final class provider_test extends provider_testcase {
'attemptreopenmethod' => ASSIGN_ATTEMPT_REOPEN_METHOD_MANUAL,
'maxattempts' => 3,
'assignsubmission_onlinetext_enabled' => true,
'assignfeedback_comments_enabled' => true
'assignfeedback_comments_enabled' => true,
'markingworkflow' => 1,
'markingallocation' => 1,
'markercount' => 2,
]);
$context = $assign->get_context();
@@ -583,6 +645,18 @@ final class provider_test extends provider_testcase {
// Give the submission a grade.
$assign->save_grade($user2->id, $data);
// Allocate the teacher as a marker on the assignment for both users.
$assign->update_allocated_markers($user1->id, [$teacher->id]);
$assign->update_allocated_markers($user2->id, [$teacher->id]);
// Give the submission a mark for both users.
$gradeobject = $assign->get_user_grade($user1->id, true);
$gradeobject->grader = $teacher->id;
$assign->update_mark($gradeobject, 11.1);
$gradeobject = $assign->get_user_grade($user2->id, true);
$gradeobject->grader = $teacher->id;
$assign->update_mark($gradeobject, 88.8);
// Delete user 2's data.
$approvedlist = new approved_contextlist($user2, 'mod_assign', [$context->id, $coursecontext->id]);
provider::delete_data_for_user($approvedlist);
@@ -621,6 +695,12 @@ final class provider_test extends provider_testcase {
$record = array_shift($records);
// The remaining event should be for user 1.
$this->assertEquals($user1->id, $record->userid);
// Check marks and allocated markers only exist for user1 now.
$records = $DB->get_records('assign_allocated_marker');
$this->assertCount(1, $records);
$records = $DB->get_records('assign_mark');
$this->assertCount(1, $records);
}
/**
@@ -639,7 +719,7 @@ final class provider_test extends provider_testcase {
$user2 = $this->getDataGenerator()->create_user();
// User 3 made a submission.
$user3 = $this->getDataGenerator()->create_user();
// User 4 makes a submission and it is marked by the teacher.
// User 4 makes a submission and it is graded by the teacher.
$user4 = $this->getDataGenerator()->create_user();
// Grading and providing feedback as a teacher.
$user5 = $this->getDataGenerator()->create_user();
@@ -718,6 +798,25 @@ final class provider_test extends provider_testcase {
// Give the submission a grade.
$assign2->save_grade($user6->id, $data);
// Allocate the teacher as a marker on both assignments for some of the users.
$assign1->update_allocated_markers($user1->id, [$user5->id]);
$assign1->update_allocated_markers($user2->id, [$user5->id]);
$assign1->update_allocated_markers($user6->id, [$user5->id]);
$assign2->update_allocated_markers($user1->id, [$user5->id]);
$assign2->update_allocated_markers($user2->id, [$user5->id]);
$assign2->update_allocated_markers($user6->id, [$user5->id]);
// Give the submission a mark for some users on both assignments.
$gradeobject = $assign1->get_user_grade($user3->id, true);
$gradeobject->grader = $user5->id;
$assign1->update_mark($gradeobject, 10);
$gradeobject = $assign1->get_user_grade($user4->id, true);
$gradeobject->grader = $user5->id;
$assign1->update_mark($gradeobject, 100);
$gradeobject = $assign2->get_user_grade($user6->id, true);
$gradeobject->grader = $user5->id;
$assign2->update_mark($gradeobject, 59);
// Check data is in place.
$data = $DB->get_records('assign_submission');
// We should have one entry for user 3 and two entries each for user 4 and 6.
@@ -735,8 +834,8 @@ final class provider_test extends provider_testcase {
$this->assertEquals(2, $usercounts[$user6->id]);
$data = $DB->get_records('assign_grades');
// Two entries in assign_grades, one for each grade given.
$this->assertCount(2, $data);
// Three entries in assign_grades, for grades given and grade items created for setting marks.
$this->assertCount(3, $data);
$data = $DB->get_records('assign_overrides');
$this->assertCount(1, $data);
@@ -744,6 +843,12 @@ final class provider_test extends provider_testcase {
$data = $DB->get_records('comments');
$this->assertCount(1, $data);
$data = $DB->get_records('assign_allocated_marker');
$this->assertCount(6, $data);
$data = $DB->get_records('assign_mark');
$this->assertCount(3, $data);
$userlist = new \core_privacy\local\request\approved_userlist($context, 'assign', [$user1->id, $user2->id]);
provider::delete_data_for_users($userlist);
@@ -757,6 +862,14 @@ final class provider_test extends provider_testcase {
// No change here.
$this->assertCount(5, $data);
// Should be 4 records left - user6 on assign1, and everyone on assign2.
$data = $DB->get_records('assign_allocated_marker');
$this->assertCount(4, $data);
// Should be unchanged.
$data = $DB->get_records('assign_mark');
$this->assertCount(3, $data);
$userlist = new \core_privacy\local\request\approved_userlist($context, 'assign', [$user3->id, $user5->id]);
provider::delete_data_for_users($userlist);
@@ -767,5 +880,13 @@ final class provider_test extends provider_testcase {
$data = $DB->get_records('assign_grades');
// Grades should be unchanged.
$this->assertCount(2, $data);
// Should be 3 left, all for assign2.
$data = $DB->get_records('assign_allocated_marker');
$this->assertCount(3, $data);
// Should be 1 left, for assign2.
$data = $DB->get_records('assign_mark');
$this->assertCount(1, $data);
}
}
+1 -1
View File
@@ -25,5 +25,5 @@
defined('MOODLE_INTERNAL') || die();
$plugin->component = 'mod_assign'; // Full name of the plugin (used for diagnostics).
$plugin->version = 2026022300; // The current module version (Date: YYYYMMDDXX).
$plugin->version = 2026022301; // The current module version (Date: YYYYMMDDXX).
$plugin->requires = 2025092600; // Requires this Moodle version.