From fb43a326c2dcdef8433edc4f0fa2f452d1958584 Mon Sep 17 00:00:00 2001 From: Jun Pataleta Date: Wed, 6 Jan 2016 12:59:48 +0800 Subject: [PATCH] MDL-52661 gradingform_gudie: Accessibility fixes for marking guide --- .../guide/amd/build/comment_chooser.min.js | 1 + .../form/guide/amd/src/comment_chooser.js | 139 ++++++++++ grade/grading/form/guide/edit_form.php | 2 +- grade/grading/form/guide/guideeditor.php | 2 +- grade/grading/form/guide/js/guideeditor.js | 14 +- .../form/guide/lang/en/gradingform_guide.php | 6 +- grade/grading/form/guide/lib.php | 2 +- grade/grading/form/guide/renderer.php | 247 +++++++++++++----- .../guide/templates/comment_chooser.mustache | 59 +++++ .../tests/behat/behat_gradingform_guide.php | 223 ++++++++++++++++ .../form/guide/tests/behat/edit_guide.feature | 102 ++++++++ 11 files changed, 731 insertions(+), 66 deletions(-) create mode 100644 grade/grading/form/guide/amd/build/comment_chooser.min.js create mode 100644 grade/grading/form/guide/amd/src/comment_chooser.js create mode 100644 grade/grading/form/guide/templates/comment_chooser.mustache create mode 100644 grade/grading/form/guide/tests/behat/behat_gradingform_guide.php create mode 100644 grade/grading/form/guide/tests/behat/edit_guide.feature diff --git a/grade/grading/form/guide/amd/build/comment_chooser.min.js b/grade/grading/form/guide/amd/build/comment_chooser.min.js new file mode 100644 index 0000000000000..acb88091429cf --- /dev/null +++ b/grade/grading/form/guide/amd/build/comment_chooser.min.js @@ -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="",g="comment-chooser-"+d+"-cancel",h='";"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()})}}}); \ No newline at end of file diff --git a/grade/grading/form/guide/amd/src/comment_chooser.js b/grade/grading/form/guide/amd/src/comment_chooser.js new file mode 100644 index 0000000000000..fdee3cb947805 --- /dev/null +++ b/grade/grading/form/guide/amd/src/comment_chooser.js @@ -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 . + +/** + * 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 + * @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 = ''; + var cancelButtonId = 'comment-chooser-' + criterionId + '-cancel'; + var cancelButton = ''; + + 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(); + }); + } + }; +}); diff --git a/grade/grading/form/guide/edit_form.php b/grade/grading/form/guide/edit_form.php index 5437beeff27a7..e632bd1b0ae5a 100644 --- a/grade/grading/form/guide/edit_form.php +++ b/grade/grading/form/guide/edit_form.php @@ -53,7 +53,7 @@ public function definition() { // 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'); diff --git a/grade/grading/form/guide/guideeditor.php b/grade/grading/form/guide/guideeditor.php index a6e43a1973caa..e044059c0d1fe 100644 --- a/grade/grading/form/guide/guideeditor.php +++ b/grade/grading/form/guide/guideeditor.php @@ -137,7 +137,7 @@ public function toHtml() { $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; diff --git a/grade/grading/form/guide/js/guideeditor.js b/grade/grading/form/guide/js/guideeditor.js index d298b15d35ad2..1175aea84e398 100644 --- a/grade/grading/form/guide/js/guideeditor.js +++ b/grade/grading/form/guide/js/guideeditor.js @@ -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]) diff --git a/grade/grading/form/guide/lang/en/gradingform_guide.php b/grade/grading/form/guide/lang/en/gradingform_guide.php index a36eabc7df4f6..87f7a110c00eb 100644 --- a/grade/grading/form/guide/lang/en/gradingform_guide.php +++ b/grade/grading/form/guide/lang/en/gradingform_guide.php @@ -31,6 +31,7 @@ $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['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_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 {$a->maxscore} points 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.
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['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.'; diff --git a/grade/grading/form/guide/lib.php b/grade/grading/form/guide/lib.php index 4a896ea76e951..a6c17eb492763 100644 --- a/grade/grading/form/guide/lib.php +++ b/grade/grading/form/guide/lib.php @@ -951,7 +951,7 @@ public function render_grading_element($page, $gradingformelement) { $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) { diff --git a/grade/grading/form/guide/renderer.php b/grade/grading/form/guide/renderer.php index c99219dcce964..caa4e193689a1 100644 --- a/grade/grading/form/guide/renderer.php +++ b/grade/grading/form/guide/renderer.php @@ -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 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr $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')); - - $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')); + $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')); + + $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 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr $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 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr $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 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr $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 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr 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 @@ public function criterion_template($mode, $options, $elementname = '{NAME}', $cr '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 @@ public function comment_template($mode, $elementname = '{NAME}', $comment = null } } } - $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 @@ public function comment_template($mode, $elementname = '{NAME}', $comment = null 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 @@ protected function guide_template($mode, $options, $elementname, $criteriastr, $ $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 @@ protected function guide_template($mode, $options, $elementname, $criteriastr, $ } 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 @@ public function display_guide($criteria, $comments, $options, $mode, $elementnam $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 @@ public function display_instance(gradingform_guide_instance $instance, $idx, $ca * @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( diff --git a/grade/grading/form/guide/templates/comment_chooser.mustache b/grade/grading/form/guide/templates/comment_chooser.mustache new file mode 100644 index 0000000000000..883121c38c6db --- /dev/null +++ b/grade/grading/form/guide/templates/comment_chooser.mustache @@ -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 . +}} +{{! + @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" + } + ] + } +}} +
+
    + {{#comments}} +
  • + +
  • + {{/comments}} +
+
diff --git a/grade/grading/form/guide/tests/behat/behat_gradingform_guide.php b/grade/grading/form/guide/tests/behat/behat_gradingform_guide.php new file mode 100644 index 0000000000000..7ce443c64ad10 --- /dev/null +++ b/grade/grading/form/guide/tests/behat/behat_gradingform_guide.php @@ -0,0 +1,223 @@ +. + +/** + * 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); + } +} diff --git a/grade/grading/form/guide/tests/behat/edit_guide.feature b/grade/grading/form/guide/tests/behat/edit_guide.feature new file mode 100644 index 0000000000000..c7ee03d2668f9 --- /dev/null +++ b/grade/grading/form/guide/tests/behat/edit_guide.feature @@ -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