MDL-52661 gradingform_gudie: Accessibility fixes for marking guide

This commit is contained in:
Jun Pataleta 2016-01-06 12:59:48 +08:00
parent ef343c3299
commit fb43a326c2
11 changed files with 730 additions and 65 deletions

View File

@ -0,0 +1 @@
define(["jquery","core/templates","core/notification","core/yui"],function(a,b,c){return{initialise:function(d,e,f,g){function h(b,c){var e="<label>"+M.util.get_string("insertcomment","gradingform_guide")+"</label>",g="comment-chooser-"+d+"-cancel",h='<button id="'+g+'">'+M.util.get_string("cancel","moodle")+"</button>";"undefined"==typeof j&&(j=new M.core.dialogue({modal:!0,headerContent:e,bodyContent:b,footerContent:h,focusAfterHide:"#"+f,id:"comments-chooser-dialog-"+d}),a("#"+g).click(function(){"undefined"!=typeof j&&j.hide()}),a.each(c,function(b,c){var e="#comment-option-"+d+"-"+c.id;a(e).click(function(){var b=a("#"+f),d=b.val();""!==a.trim(d)&&(d+="\n"),d+=c.description,b.val(d),"undefined"!=typeof j&&j.hide()}),a(document).off("keypress",e).on("keypress",e,function(){var b=event.which||event.keyCode;(13==b||32==b)&&a(e).click()})})),j.show()}function i(){var a={criterionId:d,comments:g};b.render("gradingform_guide/comment_chooser",a).done(function(a){h(a,g)}).fail(c.exception)}var j;a("#"+e).click(function(a){a.preventDefault(),i()})}}});

View File

@ -0,0 +1,139 @@
// 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/>.
/**
* AMD code for the frequently used comments chooser for the marking guide grading form.
*
* @module gradingform_guide/comment_chooser
* @class comment_chooser
* @package core
* @copyright 2015 Jun Pataleta <jun@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
define(['jquery', 'core/templates', 'core/notification', 'core/yui'], function ($, templates, notification) {
// Private variables and functions.
return /** @alias module:gradingform_guide/comment_chooser */ {
// Public variables and functions.
/**
* Initialises the module.
*
* Basically, it performs the binding and handling of the button click event for
* the 'Insert frequently used comment' button.
*
* @param criterionId The criterion ID.
* @param buttonId The element ID of the button which the handler will be bound to.
* @param remarkId The element ID of the remark text area where the text of the selected comment will be copied to.
* @param commentOptions The array of frequently used comments to be used as options.
*/
initialise: function (criterionId, buttonId, remarkId, commentOptions) {
var chooserDialog;
/**
* Display the chooser dialog using the compiled HTML from the mustache template
* and binds onclick events for the generated comment options.
*
* @param compiledSource The compiled HTML from the mustache template
* @param comments Array containing comments.
*/
function displayChooserDialog(compiledSource, comments) {
var titleLabel = '<label>' + M.util.get_string('insertcomment', 'gradingform_guide') + '</label>';
var cancelButtonId = 'comment-chooser-' + criterionId + '-cancel';
var cancelButton = '<button id="' + cancelButtonId + '">' + M.util.get_string('cancel', 'moodle') + '</button>';
if (typeof chooserDialog === 'undefined') {
// Set dialog's body content.
chooserDialog = new M.core.dialogue({
modal: true,
headerContent: titleLabel,
bodyContent: compiledSource,
footerContent: cancelButton,
focusAfterHide: '#' + remarkId,
id: "comments-chooser-dialog-" + criterionId
});
// Bind click event to the cancel button.
$("#" + cancelButtonId).click(function() {
if (typeof chooserDialog !== 'undefined') {
chooserDialog.hide();
}
});
// Loop over each comment item and bind click events.
$.each(comments, function (index, comment) {
var commentOptionId = '#comment-option-' + criterionId + '-' + comment.id;
// Delegate click event for the generated option link.
$(commentOptionId).click(function () {
var remarkTextArea = $('#' + remarkId);
var remarkText = remarkTextArea.val();
// Add line break if the current value of the remark text is not empty.
if ($.trim(remarkText) !== '') {
remarkText += '\n';
}
remarkText += comment.description;
remarkTextArea.val(remarkText);
if (typeof chooserDialog !== 'undefined') {
chooserDialog.hide();
}
});
// Handle keypress on list items.
$(document).off('keypress', commentOptionId).on('keypress', commentOptionId, function () {
var keyCode = event.which || event.keyCode;
// Enter or space key.
if (keyCode == 13 || keyCode == 32) {
// Trigger click event.
$(commentOptionId).click();
}
});
});
}
// Show dialog.
chooserDialog.show();
}
/**
* Generates the comments chooser dialog from the grading_form/comment_chooser mustache template.
*/
function generateCommentsChooser() {
// Template context.
var context = {
criterionId: criterionId,
comments: commentOptions
};
// Render the template and display the comment chooser dialog.
templates.render('gradingform_guide/comment_chooser', context)
.done(function (compiledSource) {
displayChooserDialog(compiledSource, commentOptions);
})
.fail(notification.exception);
}
// Bind click event for the comments chooser button.
$("#" + buttonId).click(function (e) {
e.preventDefault();
generateCommentsChooser();
});
}
};
});

View File

@ -53,7 +53,7 @@ class gradingform_guide_editguide extends moodleform {
// Name.
$form->addElement('text', 'name', get_string('name', 'gradingform_guide'),
array('size' => 52, 'maxlength' => 255));
$form->addRule('name', get_string('required'), 'required');
$form->addRule('name', get_string('required'), 'required', null, 'client');
$form->setType('name', PARAM_TEXT);
$form->addRule('name', null, 'maxlength', 255, 'client');

View File

@ -137,7 +137,7 @@ class moodlequickform_guideeditor extends HTML_QuickForm_input {
$html .= $renderer->display_regrade_confirmation($this->getName(), $this->regradeconfirmation, $data['regrade']);
}
if ($this->validationerrors) {
$html .= $renderer->notification($this->validationerrors, 'error');
$html .= html_writer::div($renderer->notification($this->validationerrors, 'error'), '', array('role' => 'alert'));
}
$html .= $renderer->display_guide($data['criteria'], $data['comments'], $data['options'], $mode, $this->getName());
return $html;

View File

@ -168,14 +168,15 @@ M.gradingform_guideeditor.buttonclick = function(e, confirmed) {
return;
}
// prepare the id of the next inserted criterion
var elements_str;
if (section == 'criteria') {
elements_str = '#guide-'+name+' .criteria .criterion'
} else if (section == 'comments') {
elements_str = '#guide-'+name+' .comments .criterion'
}
var newid = 0;
if (action == 'addcriterion' || action == 'addcomment') {
var newid = M.gradingform_guideeditor.calculatenewid(elements_str)
newid = M.gradingform_guideeditor.calculatenewid(elements_str);
}
var dialog_options = {
'scope' : this,
@ -199,7 +200,14 @@ M.gradingform_guideeditor.buttonclick = function(e, confirmed) {
M.gradingform_guideeditor.addhandlers();
M.gradingform_guideeditor.disablealleditors()
M.gradingform_guideeditor.assignclasses(elements_str)
//M.gradingform_guideeditor.editmode(Y.one('#guide-'+name+' #'+name+'-'+section+'-NEWID'+newid+'-shortname'),true)
// Enable edit mode of the newly added criterion/comment entry.
var inputTarget = 'shortname';
if (action == 'addcomment') {
inputTarget = 'description';
}
var inputTargetId = '#guide-' + name + ' #' + name + '-' + section + '-NEWID' + newid + '-' + inputTarget;
M.gradingform_guideeditor.editmode(Y.one(inputTargetId), true);
} else if (chunks.length == 4 && action == 'moveup') {
// MOVE UP
el = Y.one('#'+name+'-'+section+'-'+chunks[2])

View File

@ -31,6 +31,7 @@ $string['backtoediting'] = 'Back to editing';
$string['clicktocopy'] = 'Click to copy this text into the criteria feedback';
$string['clicktoedit'] = 'Click to edit';
$string['clicktoeditname'] = 'Click to edit criterion name';
$string['comment'] = 'Comment';
$string['comments'] = 'Frequently used comments';
$string['commentsdelete'] = 'Delete comment';
$string['commentsempty'] = 'Click to edit comment';
@ -38,12 +39,13 @@ $string['commentsmovedown'] = 'Move down';
$string['commentsmoveup'] = 'Move up';
$string['confirmdeletecriterion'] = 'Are you sure you want to delete this item?';
$string['confirmdeletelevel'] = 'Are you sure you want to delete this level?';
$string['criterion'] = 'Criterion';
$string['criterion'] = 'Criterion name';
$string['criteriondelete'] = 'Delete criterion';
$string['criterionempty'] = 'Click to edit criterion';
$string['criterionmovedown'] = 'Move down';
$string['criterionmoveup'] = 'Move up';
$string['criterionname'] = 'Criterion name';
$string['criterionremark'] = '{$a} criterion remark';
$string['definemarkingguide'] = 'Define marking guide';
$string['description'] = 'Description';
$string['descriptionmarkers'] = 'Description for Markers';
@ -57,6 +59,7 @@ $string['err_noshortname'] = 'Criterion name can not be empty';
$string['err_shortnametoolong'] = 'Criterion name must be less than 256 characters';
$string['err_scoreinvalid'] = 'The score given to {$a->criterianame} is not valid, the max score is: {$a->maxscore}';
$string['gradingof'] = '{$a} grading';
$string['guide'] = 'Marking guide';
$string['guidemappingexplained'] = 'WARNING: Your marking guide has a maximum grade of <b>{$a->maxscore} points</b> but the maximum grade set in your activity is {$a->modulegrade} The maximum score set in your marking guide will be scaled to the maximum grade in the module.<br />
Intermediate scores will be converted respectively and rounded to the nearest available grade.';
$string['guidenotcompleted'] = 'Please provide a valid grade for each criterion';
@ -64,6 +67,7 @@ $string['guideoptions'] = 'Marking guide options';
$string['guidestatus'] = 'Current marking guide status';
$string['hidemarkerdesc'] = 'Hide marker criterion descriptions';
$string['hidestudentdesc'] = 'Hide student criterion descriptions';
$string['insertcomment'] = 'Insert frequently used comment';
$string['maxscore'] = 'Maximum mark';
$string['name'] = 'Name';
$string['needregrademessage'] = 'The marking guide definition was changed after this student had been graded. The student can not see this marking guide until you check the marking guide and update the grade.';

View File

@ -951,7 +951,7 @@ class gradingform_guide_instance extends gradingform_instance {
$currentinstance = $this->get_current_instance();
if ($currentinstance && $currentinstance->get_status() == gradingform_instance::INSTANCE_STATUS_NEEDUPDATE) {
$html .= html_writer::tag('div', get_string('needregrademessage', 'gradingform_guide'),
array('class' => 'gradingform_guide-regrade'));
array('class' => 'gradingform_guide-regrade', 'role' => 'alert'));
}
$haschanges = false;
if ($currentinstance) {

View File

@ -55,10 +55,13 @@ class gradingform_guide_renderer extends plugin_renderer_base {
* @param array $criterion criterion data
* @param array $value (only in view mode) teacher's feedback on this criterion
* @param array $validationerrors An array containing validation errors to be shown
* @param array $comments Array of frequently used comments.
* @return string
*/
public function criterion_template($mode, $options, $elementname = '{NAME}', $criterion = null, $value = null,
$validationerrors = null) {
$validationerrors = null, $comments = null) {
global $PAGE;
if ($criterion === null || !is_array($criterion) || !array_key_exists('id', $criterion)) {
$criterion = array('id' => '{CRITERION-id}',
'description' => '{CRITERION-description}',
@ -85,24 +88,28 @@ class gradingform_guide_renderer extends plugin_renderer_base {
$value = get_string('criterion'.$key, 'gradingform_guide');
$button = html_writer::empty_tag('input', array('type' => 'submit',
'name' => '{NAME}[criteria][{CRITERION-id}]['.$key.']',
'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value, 'tabindex' => -1));
'id' => '{NAME}-criteria-{CRITERION-id}-'.$key, 'value' => $value, 'title' => $value));
$criteriontemplate .= html_writer::tag('div', $button, array('class' => $key));
}
$criteriontemplate .= html_writer::end_tag('td'); // Controls.
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[criteria][{CRITERION-id}][sortorder]', 'value' => $criterion['sortorder']));
$shortname = html_writer::empty_tag('input', array('type'=> 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][shortname]', 'value' => $criterion['shortname'],
'id ' => '{NAME}[criteria][{CRITERION-id}][shortname]'));
$shortname = html_writer::tag('div', $shortname, array('class'=>'criterionname'));
$description = html_writer::tag('textarea', s($criterion['description']),
array('name' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '65', 'rows' => '5'));
$description = html_writer::tag('div', $description, array('class'=>'criteriondesc'));
$shortnameinput = html_writer::empty_tag('input', array('type' => 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][shortname]',
'id ' => '{NAME}-criteria-{CRITERION-id}-shortname',
'value' => $criterion['shortname'],
'aria-labelledby' => '{NAME}-criterion-name-label'));
$shortname = html_writer::tag('div', $shortnameinput, array('class' => 'criterionname'));
$descriptioninput = html_writer::tag('textarea', s($criterion['description']),
array('name' => '{NAME}[criteria][{CRITERION-id}][description]',
'id' => '{NAME}[criteria][{CRITERION-id}][description]', 'cols' => '65', 'rows' => '5'));
$description = html_writer::tag('div', $descriptioninput, array('class' => 'criteriondesc'));
$descriptionmarkers = html_writer::tag('textarea', s($criterion['descriptionmarkers']),
array('name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'cols' => '65', 'rows' => '5'));
$descriptionmarkers = html_writer::tag('div', $descriptionmarkers, array('class'=>'criteriondescmarkers'));
$descriptionmarkersinput = html_writer::tag('textarea', s($criterion['descriptionmarkers']),
array('name' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]',
'id' => '{NAME}[criteria][{CRITERION-id}][descriptionmarkers]', 'cols' => '65', 'rows' => '5'));
$descriptionmarkers = html_writer::tag('div', $descriptionmarkersinput, array('class' => 'criteriondescmarkers'));
$maxscore = html_writer::empty_tag('input', array('type'=> 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][maxscore]', 'size' => '3',
@ -125,8 +132,14 @@ class gradingform_guide_renderer extends plugin_renderer_base {
$mode == gradingform_guide_controller::DISPLAY_VIEW) {
$descriptionclass = 'descriptionreadonly';
}
$shortname = html_writer::tag('div', s($criterion['shortname']),
array('class'=>'criterionshortname', 'name' => '{NAME}[criteria][{CRITERION-id}][shortname]'));
$shortnameparams = array(
'name' => '{NAME}[criteria][{CRITERION-id}][shortname]',
'id' => '{NAME}[criteria][{CRITERION-id}][shortname]',
'aria-describedby' => '{NAME}-criterion-name-label'
);
$shortname = html_writer::div(s($criterion['shortname']), 'criterionshortname', $shortnameparams);
$descmarkerclass = '';
$descstudentclass = '';
if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
@ -155,9 +168,7 @@ class gradingform_guide_renderer extends plugin_renderer_base {
$descriptionclass .= ' error';
}
$title = html_writer::tag('label', get_string('criterion', 'gradingform_guide'),
array('for'=>'{NAME}[criteria][{CRITERION-id}][shortname]', 'class' => 'criterionnamelabel'));
$title .= $shortname;
$title = $shortname;
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL ||
$mode == gradingform_guide_controller::DISPLAY_PREVIEW) {
$title .= html_writer::tag('label', get_string('descriptionstudents', 'gradingform_guide'),
@ -173,15 +184,26 @@ class gradingform_guide_renderer extends plugin_renderer_base {
$mode == gradingform_guide_controller::DISPLAY_VIEW) {
$title .= $description;
if (!empty($options['showmarkspercriterionstudents'])) {
$title .= html_writer::tag('label', get_string('maxscore', 'gradingform_guide'),
array('for' => '{NAME}[criteria][{CRITERION-id}][maxscore]'));
$title .= html_writer::label(get_string('maxscore', 'gradingform_guide'), null);
$title .= $maxscore;
}
} else {
$title .= $description . $descriptionmarkers;
}
$criteriontemplate .= html_writer::tag('td', $title, array('class' => $descriptionclass,
'id' => '{NAME}-criteria-{CRITERION-id}-shortname'));
// Title cell params.
$titletdparams = array(
'class' => $descriptionclass,
'id' => '{NAME}-criteria-{CRITERION-id}-shortname-cell'
);
if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL &&
$mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
// Set description's cell as tab-focusable.
$titletdparams['tabindex'] = '0';
}
$criteriontemplate .= html_writer::tag('td', $title, $titletdparams);
$currentremark = '';
$currentscore = '';
@ -191,23 +213,70 @@ class gradingform_guide_renderer extends plugin_renderer_base {
if (isset($value['score'])) {
$currentscore = $value['score'];
}
// Element ID of the remark text area.
$remarkid = $elementname . '-criteria-' . $criterion['id'] . '-remark';
if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
$scoreclass = '';
if (!empty($validationerrors[$criterion['id']]['score'])) {
$scoreclass = 'error';
$currentscore = $validationerrors[$criterion['id']]['score']; // Show invalid score in form.
}
$input = html_writer::tag('textarea', s($currentremark),
array('name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'cols' => '65', 'rows' => '5',
'class' => 'markingguideremark'));
$criteriontemplate .= html_writer::tag('td', $input, array('class' => 'remark'));
$score = html_writer::tag('label', get_string('score', 'gradingform_guide'),
array('for'=>'{NAME}[criteria][{CRITERION-id}][score]', 'class' => $scoreclass));
$score .= html_writer::empty_tag('input', array('type'=> 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][score]', 'class' => $scoreclass,
'id' => '{NAME}[criteria][{CRITERION-id}][score]',
'size' => '3', 'value' => $currentscore));
$score .= '/'.$maxscore;
// Grading remark text area parameters.
$remarkparams = array(
'name' => '{NAME}[criteria][{CRITERION-id}][remark]',
'id' => $remarkid,
'cols' => '65', 'rows' => '5', 'class' => 'markingguideremark',
'aria-labelledby' => '{NAME}-remarklabel{CRITERION-id}'
);
// Grading remark text area.
$input = html_writer::tag('textarea', s($currentremark), $remarkparams);
// Frequently used comments chooser.
$chooserbuttonid = 'criteria-' . $criterion['id'] . '-commentchooser';
$commentchooserparams = array('id' => $chooserbuttonid, 'class' => 'commentchooser');
$commentchooser = html_writer::tag('button', get_string('insertcomment', 'gradingform_guide'), $commentchooserparams);
// Option items for the frequently used comments chooser dialog.
$commentoptions = array();
foreach ($comments as $id => $comment) {
$commentoption = new stdClass();
$commentoption->id = $id;
$commentoption->description = s($comment['description']);
$commentoptions[] = $commentoption;
}
// Include string for JS for the comment chooser title.
$PAGE->requires->string_for_js('insertcomment', 'gradingform_guide');
// Include comment_chooser module.
$PAGE->requires->js_call_amd('gradingform_guide/comment_chooser', 'initialise',
array($criterion['id'], $chooserbuttonid, $remarkid, $commentoptions));
// Hidden marking guide remark label.
$remarklabelparams = array(
'class' => 'hidden',
'id' => '{NAME}-remarklabel{CRITERION-id}'
);
$remarklabeltext = get_string('criterionremark', 'gradingform_guide', $criterion['shortname']);
$remarklabel = html_writer::label($remarklabeltext, $remarkid, false, $remarklabelparams);
$criteriontemplate .= html_writer::tag('td', $remarklabel . $input . $commentchooser, array('class' => 'remark'));
// Score input and max score.
$scoreinputparams = array(
'type' => 'text',
'name' => '{NAME}[criteria][{CRITERION-id}][score]',
'class' => $scoreclass,
'id' => '{NAME}-criteria-{CRITERION-id}-score',
'size' => '3',
'value' => $currentscore,
'aria-labelledby' => '{NAME}-score-label'
);
$score = html_writer::empty_tag('input', $scoreinputparams);
$score .= html_writer::div('/' . s($criterion['maxscore']));
$criteriontemplate .= html_writer::tag('td', $score, array('class' => 'score'));
} else if ($mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) {
@ -215,10 +284,34 @@ class gradingform_guide_renderer extends plugin_renderer_base {
'name' => '{NAME}[criteria][{CRITERION-id}][remark]', 'value' => $currentremark));
} else if ($mode == gradingform_guide_controller::DISPLAY_REVIEW ||
$mode == gradingform_guide_controller::DISPLAY_VIEW) {
$criteriontemplate .= html_writer::tag('td', s($currentremark), array('class' => 'remark'));
// Hidden marking guide remark description.
$remarkdescparams = array(
'id' => '{NAME}-criteria-{CRITERION-id}-remark-desc'
);
$remarkdesctext = get_string('criterionremark', 'gradingform_guide', $criterion['shortname']);
$remarkdesc = html_writer::div($remarkdesctext, 'hidden', $remarkdescparams);
// Remarks cell.
$remarkdiv = html_writer::div(s($currentremark));
$remarkcellparams = array(
'class' => 'remark',
'tabindex' => '0',
'id' => '{NAME}-criteria-{CRITERION-id}-remark',
'aria-describedby' => '{NAME}-criteria-{CRITERION-id}-remark-desc'
);
$criteriontemplate .= html_writer::tag('td', $remarkdesc . $remarkdiv, $remarkcellparams);
// Score cell.
if (!empty($options['showmarkspercriterionstudents'])) {
$criteriontemplate .= html_writer::tag('td', s($currentscore). ' / '.$maxscore,
array('class' => 'score'));
$scorecellparams = array(
'class' => 'score',
'tabindex' => '0',
'id' => '{NAME}-criteria-{CRITERION-id}-score',
'aria-describedby' => '{NAME}-score-label'
);
$scorediv = html_writer::div(s($currentscore) . ' / ' . s($criterion['maxscore']));
$criteriontemplate .= html_writer::tag('td', $scorediv, $scorecellparams);
}
}
$criteriontemplate .= html_writer::end_tag('tr'); // Criterion.
@ -262,28 +355,30 @@ class gradingform_guide_renderer extends plugin_renderer_base {
}
}
}
$criteriontemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $comment['class'],
$commenttemplate = html_writer::start_tag('tr', array('class' => 'criterion'. $comment['class'],
'id' => '{NAME}-comments-{COMMENT-id}'));
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
$criteriontemplate .= html_writer::start_tag('td', array('class' => 'controls'));
$commenttemplate .= html_writer::start_tag('td', array('class' => 'controls'));
foreach (array('moveup', 'delete', 'movedown') as $key) {
$value = get_string('comments'.$key, 'gradingform_guide');
$button = html_writer::empty_tag('input', array('type' => 'submit',
'name' => '{NAME}[comments][{COMMENT-id}]['.$key.']', 'id' => '{NAME}-comments-{COMMENT-id}-'.$key,
'value' => $value, 'title' => $value, 'tabindex' => -1));
$criteriontemplate .= html_writer::tag('div', $button, array('class' => $key));
'value' => $value, 'title' => $value));
$commenttemplate .= html_writer::tag('div', $button, array('class' => $key));
}
$criteriontemplate .= html_writer::end_tag('td'); // Controls.
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
$commenttemplate .= html_writer::end_tag('td'); // Controls.
$commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder']));
$description = html_writer::tag('textarea', s($comment['description']),
array('name' => '{NAME}[comments][{COMMENT-id}][description]', 'cols' => '65', 'rows' => '5'));
array('name' => '{NAME}[comments][{COMMENT-id}][description]',
'id' => '{NAME}-comments-{COMMENT-id}-description',
'aria-labelledby' => '{NAME}-comment-label', 'cols' => '65', 'rows' => '5'));
$description = html_writer::tag('div', $description, array('class'=>'criteriondesc'));
} else {
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
$commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[comments][{COMMENT-id}][sortorder]', 'value' => $comment['sortorder']));
$criteriontemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
$commenttemplate .= html_writer::empty_tag('input', array('type' => 'hidden',
'name' => '{NAME}[comments][{COMMENT-id}][description]', 'value' => $comment['description']));
}
if ($mode == gradingform_guide_controller::DISPLAY_EVAL) {
@ -301,13 +396,21 @@ class gradingform_guide_renderer extends plugin_renderer_base {
if (isset($comment['error_description'])) {
$descriptionclass .= ' error';
}
$criteriontemplate .= html_writer::tag('td', $description, array('class' => $descriptionclass,
'id' => '{NAME}-comments-{COMMENT-id}-description'));
$criteriontemplate .= html_writer::end_tag('tr'); // Criterion.
$descriptioncellparams = array(
'class' => $descriptionclass,
'id' => '{NAME}-comments-{COMMENT-id}-description-cell'
);
// Make description cell tab-focusable when in review mode.
if ($mode != gradingform_guide_controller::DISPLAY_EDIT_FULL &&
$mode != gradingform_guide_controller::DISPLAY_EDIT_FROZEN) {
$descriptioncellparams['tabindex'] = '0';
}
$commenttemplate .= html_writer::tag('td', $description, $descriptioncellparams);
$commenttemplate .= html_writer::end_tag('tr'); // Criterion.
$criteriontemplate = str_replace('{NAME}', $elementname, $criteriontemplate);
$criteriontemplate = str_replace('{COMMENT-id}', $comment['id'], $criteriontemplate);
return $criteriontemplate;
$commenttemplate = str_replace('{NAME}', $elementname, $commenttemplate);
$commenttemplate = str_replace('{COMMENT-id}', $comment['id'], $commenttemplate);
return $commenttemplate;
}
/**
* This function returns html code for displaying guide template (content before and after
@ -358,7 +461,27 @@ class gradingform_guide_renderer extends plugin_renderer_base {
$guidetemplate = html_writer::start_tag('div', array('id' => 'guide-{NAME}',
'class' => 'clearfix gradingform_guide'.$classsuffix));
$guidetemplate .= html_writer::tag('table', $criteriastr, array('class' => 'criteria', 'id' => '{NAME}-criteria'));
// Hidden guide label.
$guidedescparams = array(
'id' => 'guide-{NAME}-desc',
'aria-hidden' => 'true'
);
$guidetemplate .= html_writer::div(get_string('guide', 'gradingform_guide'), 'hidden', $guidedescparams);
// Hidden criterion name label/description.
$guidetemplate .= html_writer::div(get_string('criterionname', 'gradingform_guide'), 'hidden',
array('id' => '{NAME}-criterion-name-label'));
// Hidden score label/description.
$guidetemplate .= html_writer::div(get_string('score', 'gradingform_guide'), 'hidden', array('id' => '{NAME}-score-label'));
// Criteria table parameters.
$criteriatableparams = array(
'class' => 'criteria',
'id' => '{NAME}-criteria',
'aria-describedby' => 'guide-{NAME}-desc');
$guidetemplate .= html_writer::tag('table', $criteriastr, $criteriatableparams);
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
$value = get_string('addcriterion', 'gradingform_guide');
$input = html_writer::empty_tag('input', array('type' => 'submit', 'name' => '{NAME}[criteria][addcriterion]',
@ -367,9 +490,15 @@ class gradingform_guide_renderer extends plugin_renderer_base {
}
if (!empty($commentstr)) {
$guidetemplate .= html_writer::tag('label', get_string('comments', 'gradingform_guide'),
array('for' => '{NAME}-comments', 'class' => 'commentheader'));
$guidetemplate .= html_writer::tag('table', $commentstr, array('class' => 'comments', 'id' => '{NAME}-comments'));
$guidetemplate .= html_writer::div(get_string('comments', 'gradingform_guide'), 'commentheader',
array('id' => '{NAME}-comments-label'));
$guidetemplate .= html_writer::div(get_string('comment', 'gradingform_guide'), 'hidden',
array('id' => '{NAME}-comment-label', 'aria-hidden' => 'true'));
$commentstableparams = array(
'class' => 'comments',
'id' => '{NAME}-comments',
'aria-describedby' => '{NAME}-comments-label');
$guidetemplate .= html_writer::tag('table', $commentstr, $commentstableparams);
}
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL) {
$value = get_string('addcomment', 'gradingform_guide');
@ -481,21 +610,21 @@ class gradingform_guide_renderer extends plugin_renderer_base {
$criterionvalue = null;
}
$criteriastr .= $this->criterion_template($mode, $options, $elementname, $criterion, $criterionvalue,
$validationerrors);
$validationerrors, $comments);
}
$cnt = 0;
$commentstr = '';
// Check if comments should be displayed.
if ($mode == gradingform_guide_controller::DISPLAY_EDIT_FULL ||
$mode == gradingform_guide_controller::DISPLAY_EDIT_FROZEN ||
$mode == gradingform_guide_controller::DISPLAY_PREVIEW ||
$mode == gradingform_guide_controller::DISPLAY_EVAL ||
$mode == gradingform_guide_controller::DISPLAY_EVAL_FROZEN) {
foreach ($comments as $id => $comment) {
$comment['id'] = $id;
$comment['class'] = $this->get_css_class_suffix($cnt++, count($comments) -1);
$commentstr .= $this->comment_template($mode, $elementname, $comment);
$commentstr .= $this->comment_template($mode, $elementname, $comment);
}
}
$output = $this->guide_template($mode, $options, $elementname, $criteriastr, $commentstr);
@ -613,7 +742,7 @@ class gradingform_guide_renderer extends plugin_renderer_base {
* @return string
*/
public function display_regrade_confirmation($elementname, $changelevel, $value) {
$html = html_writer::start_tag('div', array('class' => 'gradingform_guide-regrade'));
$html = html_writer::start_tag('div', array('class' => 'gradingform_guide-regrade', 'role' => 'alert'));
if ($changelevel<=2) {
$html .= get_string('regrademessage1', 'gradingform_guide');
$selectoptions = array(

View File

@ -0,0 +1,59 @@
{{!
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/>.
}}
{{!
@template gradingform_guide/comment_chooser
Moodle comment chooser template for marking guide.
The purpose of this template is to render a list of frequently used comments that can be used by the comment chooser dialog.
Classes required for JS:
* none
Data attributes required for JS:
* none
Context variables required for this template:
* criterionId The criterion ID this chooser template is being generated for.
* comments Array of id / description pairs.
Example context (json):
{
"criterionId": "1",
"comments": [
{
"id": "1",
"description": "Test comment description 1"
},
{
"id": "2",
"description": "Test comment description 2"
}
]
}
}}
<div class="gradingform_guide_comment_chooser" id="comment_chooser">
<ul role="list">
{{#comments}}
<li role="listitem">
<button id="comment-option-{{criterionId}}-{{id}}" class="btn btn-link" tabindex="0">
{{description}}
</button>
</li>
{{/comments}}
</ul>
</div>

View File

@ -0,0 +1,223 @@
<?php
// 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/>.
/**
* Steps definitions for marking guides.
*
* @package gradingform_guide
* @category test
* @copyright 2015 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
require_once(__DIR__ . '/../../../../../../lib/behat/behat_base.php');
use Behat\Gherkin\Node\TableNode as TableNode,
Behat\Behat\Context\Step\Given as Given,
Behat\Behat\Context\Step\When as When,
Behat\Behat\Context\Step\Then as Then,
Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
Behat\Mink\Exception\ExpectationException as ExpectationException;
/**
* Steps definitions to help with marking guides.
*
* @package gradingform_guide
* @category test
* @copyright 2015 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class behat_gradingform_guide extends behat_base {
/**
* Defines the marking guide with the provided data, following marking guide's definition grid cells.
*
* This method fills the marking guide of the marking guide definition
* form; the provided TableNode should contain one row for
* each criterion and each cell of the row should contain:
* # Criterion name, a.k.a. shortname
* # Description for students
* # Description for markers
* # Max score
*
* Works with both JS and non-JS.
*
* @When /^I define the following marking guide:$/
* @throws ExpectationException
* @param TableNode $guide
*/
public function i_define_the_following_marking_guide(TableNode $guide) {
$steptableinfo = '| Criterion name | Description for students | Description for markers | Maximum mark |';
if ($criteria = $guide->getHash()) {
$addcriterionbutton = $this->find_button(get_string('addcriterion', 'gradingform_guide'));
foreach ($criteria as $index => $criterion) {
// Make sure the criterion array has 4 elements.
if (count($criterion) != 4) {
throw new ExpectationException(
'The criterion definition should contain name, description for students and markers, and maximum points. ' .
'Please follow this format: ' . $steptableinfo,
$this->getSession()
);
}
// On load, there's already a criterion template ready.
$shortnamevisible = false;
if ($index > 0) {
// So if the index is greater than 0, we click the Add new criterion button to add a new criterion.
$addcriterionbutton->click();
$shortnamevisible = true;
}
$criterionroot = 'guide[criteria][NEWID' . ($index + 1) . ']';
// Set the field value for the Criterion name.
$this->set_guide_field_value($criterionroot . '[shortname]', $criterion['Criterion name'], $shortnamevisible);
// Set the field value for the Description for students field.
$this->set_guide_field_value($criterionroot . '[description]', $criterion['Description for students']);
// Set the field value for the Description for markers field.
$this->set_guide_field_value($criterionroot . '[descriptionmarkers]', $criterion['Description for markers']);
// Set the field value for the Max score field.
$this->set_guide_field_value($criterionroot . '[maxscore]', $criterion['Maximum mark']);
}
}
}
/**
* Defines the marking guide with the provided data, following marking guide's definition grid cells.
*
* This method fills the table of frequently used comments of the marking guide definition form.
* The provided TableNode should contain one row for each frequently used comment.
* Each row contains:
* # Comment
*
* Works with both JS and non-JS.
*
* @When /^I define the following frequently used comments:$/
* @throws ExpectationException
* @param TableNode $commentstable
*/
public function i_define_the_following_frequently_used_comments(TableNode $commentstable) {
$steptableinfo = '| Comment |';
if ($comments = $commentstable->getRows()) {
$addcommentbutton = $this->find_button(get_string('addcomment', 'gradingform_guide'));
foreach ($comments as $index => $comment) {
// Make sure the comment array has only 1 element.
if (count($comment) != 1) {
throw new ExpectationException(
'The comment cannot be empty. Please follow this format: ' . $steptableinfo,
$this->getSession()
);
}
// On load, there's already a comment template ready.
$commentfieldvisible = false;
if ($index > 0) {
// So if the index is greater than 0, we click the Add frequently used comment button to add a new criterion.
$addcommentbutton->click();
$commentfieldvisible = true;
}
$commentroot = 'guide[comments][NEWID' . ($index + 1) . ']';
// Set the field value for the frequently used comment.
$this->set_guide_field_value($commentroot . '[description]', $comment[0], $commentfieldvisible);
}
}
}
/**
* Performs grading of the student by filling out the marking guide.
* Set one line per criterion and for each criterion set "| Criterion name | Points | Remark |".
*
* @When /^I grade by filling the marking guide with:$/
*
* @throws ExpectationException
* @param TableNode $guide
* @return void
*/
public function i_grade_by_filling_the_marking_guide_with(TableNode $guide) {
$criteria = $guide->getRowsHash();
$stepusage = '"I grade by filling the rubric with:" step needs you to provide a table where each row is a criterion' .
' and each criterion has 3 different values: | Criterion name | Number of points | Remark text |';
// To fill with the steps to execute.
$steps = array();
// First element -> name, second -> points, third -> Remark.
foreach ($criteria as $name => $criterion) {
// We only expect the points and the remark, as the criterion name is $name.
if (count($criterion) !== 2) {
throw new ExpectationException($stepusage, $this->getSession());
}
// Numeric value here.
$points = $criterion[0];
if (!is_numeric($points)) {
throw new ExpectationException($stepusage, $this->getSession());
}
$criterionid = 0;
if ($criterionnamediv = $this->find('xpath', "//div[@class='criterionshortname'][text()='$name']")) {
$criteriondivname = $criterionnamediv->getAttribute('name');
// Criterion's name is of the format "advancedgrading[criteria][ID][shortname]".
// So just explode the string with "][" as delimiter to extract the criterion ID.
if ($nameparts = explode('][', $criteriondivname)) {
$criterionid = $nameparts[1];
}
}
if ($criterionid) {
$criterionroot = 'advancedgrading[criteria]' . '[' . $criterionid . ']';
$steps[] = new Given('I set the field "' . $criterionroot . '[score]' . '" to "' . $points . '"');
$steps[] = new Given('I set the field "' . $criterionroot . '[remark]' . '" to "' . $criterion[1] . '"');
}
}
return $steps;
}
/**
* Makes a hidden marking guide field visible (if necessary) and sets a value on it.
*
* @param string $name The name of the field
* @param string $value The value to set
* @param bool $visible
* @return void
*/
protected function set_guide_field_value($name, $value, $visible = false) {
// Fields are hidden by default.
if ($this->running_javascript() && $visible === false) {
$xpath = "//*[@name='$name']/following-sibling::*[contains(concat(' ', normalize-space(@class), ' '), ' plainvalue ')]";
$textnode = $this->find('xpath', $xpath);
$textnode->click();
}
// Set the value now.
$field = $this->find_field($name);
$field->setValue($value);
}
}

View File

@ -0,0 +1,102 @@
@gradingform @gradingform_guide
Feature: Marking guides can be created and edited
In order to use and refine marking guide to grade students
As a teacher
I need to edit previously used marking guides
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | Teacher | 1 | teacher1@example.com |
| student1 | Student | 1 | student1@example.com |
And the following "courses" exist:
| fullname | shortname | format |
| Course 1 | C1 | topics |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
| student1 | C1 | student |
And I log in as "teacher1"
And I follow "Course 1"
And I turn editing mode on
And I add a "Assignment" to section "1" and I fill the form with:
| Assignment name | Test assignment 1 name |
| Description | Test assignment description |
| Grading method | Marking guide |
# Defining a marking guide
When I go to "Test assignment 1 name" advanced grading definition page
And I set the following fields to these values:
| Name | Assignment 1 marking guide |
| Description | Marking guide test description |
And I define the following marking guide:
| Criterion name | Description for students | Description for markers | Maximum mark |
| Guide criterion A | Guide A description for students | Guide A description for markers | 30 |
| Guide criterion B | Guide B description for students | Guide B description for markers | 30 |
| Guide criterion C | Guide C description for students | Guide C description for markers | 40 |
And I define the following frequently used comments:
| Comment 1 |
| Comment 2 |
| Comment 3 |
| Comment 4 |
And I press "Save marking guide and make it ready"
Then I should see "Ready for use"
And I should see "Guide criterion A"
And I should see "Guide criterion B"
And I should see "Guide criterion C"
And I should see "Comment 1"
And I should see "Comment 2"
And I should see "Comment 3"
And I should see "Comment 4"
@javascript
Scenario: Deleting criterion and comment
# Deleting criterion
When I go to "Test assignment 1 name" advanced grading definition page
And I click on "Delete criterion" "button" in the "Guide criterion B" "table_row"
And I press "Yes"
And I press "Save"
Then I should see "Guide criterion A"
And I should see "Guide criterion C"
And I should see "WARNING: Your marking guide has a maximum grade of 70 points"
But I should not see "Guide criterion B"
# Deleting a frequently used comment
When I go to "Test assignment 1 name" advanced grading definition page
And I click on "Delete comment" "button" in the "Comment 3" "table_row"
And I press "Yes"
And I press "Save"
Then I should see "Comment 1"
And I should see "Comment 2"
And I should see "Comment 4"
But I should not see "Comment 3"
@javascript
Scenario: Grading and viewing graded marking guide
# Grading a student.
When I go to "Student 1" "Test assignment 1 name" activity advanced grading page
And I grade by filling the marking guide with:
| Guide criterion A | 25 | Very good |
| Guide criterion B | 20 | |
| Guide criterion C | 35 | Nice! |
# Inserting frequently used comment.
And I click on "Insert frequently used comment" "button" in the "Guide criterion B" "table_row"
And I wait "1" seconds
And I press "Comment 4"
And I wait "1" seconds
Then the field "Guide criterion B criterion remark" matches value "Comment 4"
When I press "Save changes"
Then I should see "The grade changes were saved"
# Checking that the user grade is correct.
When I press "Continue"
Then I should see "80" in the "Student 1" "table_row"
And I log out
# Viewing it as a student.
And I log in as "student1"
And I follow "Course 1"
And I follow "Test assignment 1 name"
And I should see "80" in the ".feedback" "css_element"
And I should see "Marking guide test description" in the ".feedback" "css_element"
And I should see "Very good"
And I should see "Comment 4"
And I should see "Nice!"
Scenario: I can use marking guides to grade and edit them later updating students grades with Javascript disabled