Skip to content

Commit

Permalink
Merge branch 'MDL-76849-400-5' of https://github.com/junpataleta/moodle
Browse files Browse the repository at this point in the history
… into MOODLE_400_STABLE
  • Loading branch information
andrewnicols committed Mar 10, 2023
2 parents e851069 + aa7d3fa commit 5ba62ce
Show file tree
Hide file tree
Showing 17 changed files with 317 additions and 51 deletions.
3 changes: 3 additions & 0 deletions lang/en/question.php
Expand Up @@ -330,6 +330,7 @@
$string['addanotherhint'] = 'Add another hint';
$string['answer'] = 'Answer';
$string['answersaved'] = 'Answer saved';
$string['answerx'] = 'Answer {$a}';
$string['attemptfinished'] = 'Attempt finished';
$string['attemptfinishedsubmitting'] = 'Attempt finished submitting: {$a}';
$string['behaviourbeingused'] = 'Behaviour being used: {$a}';
Expand Down Expand Up @@ -357,6 +358,8 @@
$string['defaultmark'] = 'Default mark';
$string['errorsavingflags'] = 'Error saving the flag state.';
$string['feedback'] = 'Feedback';
$string['fieldinquestion'] = '{$a->fieldname} {$a->questionindentifier}';
$string['fieldinquestionpre'] = '{$a->questionindentifier} {$a->fieldname}';
$string['fillincorrect'] = 'Fill in correct responses';
$string['generalfeedback'] = 'General feedback';
$string['generalfeedback_help'] = 'General feedback is shown to the student after they have completed the question. Unlike specific feedback, which depends on the question type and what response the student gave, the same general feedback text is shown to all students.
Expand Down
46 changes: 46 additions & 0 deletions question/engine/lib.php
Expand Up @@ -639,6 +639,20 @@ class question_display_options {
*/
public $userinfoinhistory = self::HIDDEN;

/**
* This identifier should be added to the labels of all input fields in the question.
*
* This is so people using assistive technology can easily tell which input belong to
* which question. The helper {@see self::add_question_identifier_to_label() makes this easier.
*
* If not set before the question is rendered, then it defaults to 'Question N'.
* (lang string)
*
* @var string The identifier that the question being rendered is associated with.
* E.g. The question number when it is rendered on a quiz.
*/
public $questionidentifier = null;

/**
* Set all the feedback-related fields {@link $feedback}, {@link generalfeedback},
* {@link rightanswer} and {@link manualcomment} to
Expand Down Expand Up @@ -669,6 +683,38 @@ public static function get_dp_options() {
}
return $options;
}

/**
* Helper to add the question identify (if there is one) to the label of an input field in a question.
*
* @param string $label The plain field label. E.g. 'Answer 1'
* @param bool $sridentifier If true, the question identifier, if added, will be wrapped in a sr-only span. Default false.
* @param bool $addbefore If true, the question identifier will be added before the label.
* @return string The amended label. For example 'Answer 1, Question 1'.
*/
public function add_question_identifier_to_label(string $label, bool $sridentifier = false, bool $addbefore = false): string {
if (!$this->has_question_identifier()) {
return $label;
}
$identifier = $this->questionidentifier;
if ($sridentifier) {
$identifier = html_writer::span($identifier, 'sr-only');
}
$fieldlang = 'fieldinquestion';
if ($addbefore) {
$fieldlang = 'fieldinquestionpre';
}
return get_string($fieldlang, 'question', (object)['fieldname' => $label, 'questionindentifier' => $identifier]);
}

/**
* Whether a question number has been provided for the question that is being displayed.
*
* @return bool
*/
public function has_question_identifier(): bool {
return $this->questionidentifier !== null && trim($this->questionidentifier) !== '';
}
}


Expand Down
29 changes: 27 additions & 2 deletions question/engine/renderer.php
Expand Up @@ -80,6 +80,12 @@ public function question_preview_link($questionid, context $context, $showlabel)
public function question(question_attempt $qa, qbehaviour_renderer $behaviouroutput,
qtype_renderer $qtoutput, question_display_options $options, $number) {

// If not already set, record the questionidentifier.
$options = clone($options);
if (!$options->has_question_identifier()) {
$options->questionidentifier = $this->question_number_text($number);
}

$output = '';
$output .= html_writer::start_tag('div', array(
'id' => $qa->get_outer_question_div_unique_id(),
Expand Down Expand Up @@ -152,16 +158,35 @@ protected function number($number) {
if (trim($number) === '') {
return '';
}
$numbertext = '';
if (trim($number) === 'i') {
$numbertext = get_string('information', 'question');
} else {
$numbertext = get_string('questionx', 'question',
html_writer::tag('span', $number, array('class' => 'qno')));
html_writer::tag('span', s($number), array('class' => 'qno')));
}
return html_writer::tag('h3', $numbertext, array('class' => 'no'));
}

/**
* Get the question number as a string.
*
* @param string|null $number e.g. '123' or 'i'. null or '' means do not display anything number-related.
* @return string e.g. 'Question 123' or 'Information' or ''.
*/
protected function question_number_text(?string $number): string {
$number = $number ?? '';
// Trim the question number of whitespace, including  .
$trimmed = trim(html_entity_decode($number), " \n\r\t\v\x00\xC2\xA0");
if ($trimmed === '') {
return '';
}
if (trim($number) === 'i') {
return get_string('information', 'question');
} else {
return get_string('questionx', 'question', s($number));
}
}

/**
* Add an invisible heading like 'question text', 'feebdack' at the top of
* a section's contents, but only if the section has some content.
Expand Down
97 changes: 97 additions & 0 deletions question/engine/tests/question_display_options_test.php
@@ -0,0 +1,97 @@
<?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/>.

namespace core_question;

/**
* Unit tests for {@see \question_display_options}.
*
* @coversDefaultClass \question_display_options
* @package core_question
* @category test
* @copyright 2023 Jun Pataleta
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class question_display_options_test extends \advanced_testcase {

/**
* Data provider for {@see self::test_has_question_identifier()}
*
* @return array[]
*/
public function has_question_identifier_provider(): array {
return [
'Empty string' => ['', false],
'Empty space' => [' ', false],
'Null' => [null, false],
'Non-empty string' => ["Hello!", true],
];
}

/**
* Tests for {@see \question_display_options::has_question_identifier}
*
* @covers ::has_question_identifier
* @dataProvider has_question_identifier_provider
* @param string|null $identifier The question identifier
* @param bool $expected The expected return value
* @return void
*/
public function test_has_question_identifier(?string $identifier, bool $expected): void {
$options = new \question_display_options();
$options->questionidentifier = $identifier;
$this->assertEquals($expected, $options->has_question_identifier());
}

/**
* Data provider for {@see self::test_add_question_identifier_to_label()
*
* @return array[]
*/
public function add_question_identifier_to_label_provider(): array {
return [
'Empty string identifier' => ['Hello', '', false, false, "Hello"],
'Null identifier' => ['Hello', null, false, false, "Hello"],
'With identifier' => ['Hello', 'World', false, false, "Hello World"],
'With identifier, sr-only' => ['Hello', 'World', true, false, 'Hello <span class="sr-only">World</span>'],
'With identifier, prepend' => ['Hello', 'World', false, true, "World Hello"],
];
}

/**
* Tests for {@see \question_display_options::add_question_identifier_to_label()}
*
* @covers ::add_question_identifier_to_label
* @dataProvider add_question_identifier_to_label_provider
* @param string $label The label string.
* @param string|null $identifier The question identifier.
* @param bool $sronly Whether to render the question identifier in a sr-only container
* @param bool $addbefore Whether to render the question identifier before the label.
* @param string $expected The expected return value.
* @return void
*/
public function test_add_question_identifier_to_label(
string $label,
?string $identifier,
bool $sronly,
bool $addbefore,
string $expected
): void {
$options = new \question_display_options();
$options->questionidentifier = $identifier;
$this->assertEquals($expected, $options->add_question_identifier_to_label($label, $sronly, $addbefore));
}
}
10 changes: 10 additions & 0 deletions question/engine/upgrade.txt
@@ -1,5 +1,15 @@
This files describes API changes for the core question engine.

=== 4.0.7 ===

* A `$questionidentifier` property has been added to `\question_display_options` to enable question type plugins to associate the
question number to the question that is being rendered. This can be used to improve the accessibility of rendered
questions and can be especially be helpful for screen reader users as adding the question number on the answer field(s) labels
will allow them to distinguish between answer fields as they navigate through a quiz.
* Question type plugins can use \question_display_options::add_question_identifier_to_label() to add the question number to the
label of the answer field(s) of the question that is being rendered. The question number may be redundant when displayed, so the
function allows for it to be enclosed within an sr-only container.

=== 4.0 ===
1) A new optional parameter $extraselect has been added as a part of load_questions_usages_where_question_in_state()
method in question/engine/datalib.php, anything passed here will be added to the SELECT list, use this to return extra data.
Expand Down
1 change: 1 addition & 0 deletions question/type/ddwtos/lang/en/qtype_ddwtos.php
Expand Up @@ -25,6 +25,7 @@
$string['addmorechoiceblanks'] = 'Blanks for {no} more choices';
$string['answer'] = 'Answer';
$string['blank'] = 'blank';
$string['blanknumber'] = 'Blank {$a}';
$string['correctansweris'] = 'The correct answer is: {$a}';
$string['errorlimitedchoice'] = 'Choice [[{$a}]] has been used more than once without being set to "Unlimited". Please recheck this question.';
$string['infinite'] = 'Unlimited';
Expand Down
4 changes: 2 additions & 2 deletions question/type/ddwtos/renderer.php
Expand Up @@ -76,8 +76,8 @@ protected function embedded_element(question_attempt $qa, $place,
question_display_options $options) {
$question = $qa->get_question();
$group = $question->places[$place];
$boxcontents = '&#160;' . html_writer::tag('span',
get_string('blank', 'qtype_ddwtos'), array('class' => 'accesshide'));
$label = $options->add_question_identifier_to_label(get_string('blanknumber', 'qtype_ddwtos', $place));
$boxcontents = '&#160;' . html_writer::tag('span', $label, array('class' => 'accesshide'));

$value = $qa->get_last_qt_var($question->field($place));

Expand Down
40 changes: 31 additions & 9 deletions question/type/essay/renderer.php
Expand Up @@ -38,7 +38,10 @@ public function formulation_and_controls(question_attempt $qa,
question_display_options $options) {
global $CFG;
$question = $qa->get_question();

/** @var qtype_essay_format_renderer_base $responseoutput */
$responseoutput = $question->get_format_renderer($this->page);
$responseoutput->set_displayoptions($options);

// Answer field.
$step = $qa->get_last_step_with_qt_var('answer');
Expand Down Expand Up @@ -131,7 +134,8 @@ public function files_read_only(question_attempt $qa, question_display_options $

$labelbyid = $qa->get_qt_field_name('attachments') . '_label';

$output = html_writer::tag('h4', get_string('answerfiles', 'qtype_essay'), ['id' => $labelbyid, 'class' => 'sr-only']);
$fileslabel = $options->add_question_identifier_to_label(get_string('answerfiles', 'qtype_essay'));
$output = html_writer::tag('h4', $fileslabel, ['id' => $labelbyid, 'class' => 'sr-only']);
$output .= html_writer::tag('ul', implode($filelist), [
'aria-labelledby' => $labelbyid,
'class' => 'list-unstyled m-0',
Expand Down Expand Up @@ -182,7 +186,8 @@ public function files_input(question_attempt $qa, $numallowed,
}

$output = html_writer::start_tag('fieldset');
$output .= html_writer::tag('legend', get_string('answerfiles', 'qtype_essay'), ['class' => 'sr-only']);
$fileslabel = $options->add_question_identifier_to_label(get_string('answerfiles', 'qtype_essay'));
$output .= html_writer::tag('legend', $fileslabel, ['class' => 'sr-only']);
$output .= $filesrenderer->render($fm);
$output .= html_writer::empty_tag('input', [
'type' => 'hidden',
Expand Down Expand Up @@ -216,6 +221,19 @@ public function manual_comment(question_attempt $qa, question_display_options $o
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
abstract class qtype_essay_format_renderer_base extends plugin_renderer_base {

/** @var question_display_options Question display options instance for any necessary information for rendering the question. */
protected $displayoptions;

/**
* Question number setter.
*
* @param question_display_options $displayoptions
*/
public function set_displayoptions(question_display_options $displayoptions): void {
$this->displayoptions = $displayoptions;
}

/**
* Render the students respone when the question is in read-only mode.
* @param string $name the variable name this input edits.
Expand Down Expand Up @@ -253,7 +271,7 @@ protected abstract function class_name();
* @copyright 2013 Binghamton University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_essay_format_noinline_renderer extends plugin_renderer_base {
class qtype_essay_format_noinline_renderer extends qtype_essay_format_renderer_base {

protected function class_name() {
return 'qtype_essay_noinline';
Expand All @@ -276,15 +294,16 @@ public function response_area_input($name, $qa, $step, $lines, $context) {
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_essay_format_editor_renderer extends plugin_renderer_base {
class qtype_essay_format_editor_renderer extends qtype_essay_format_renderer_base {
protected function class_name() {
return 'qtype_essay_editor';
}

public function response_area_read_only($name, $qa, $step, $lines, $context) {
$labelbyid = $qa->get_qt_field_name($name) . '_label';

$output = html_writer::tag('h4', get_string('answertext', 'qtype_essay'), ['id' => $labelbyid, 'class' => 'sr-only']);
$responselabel = $this->displayoptions->add_question_identifier_to_label(get_string('answertext', 'qtype_essay'));
$output = html_writer::tag('h4', $responselabel, ['id' => $labelbyid, 'class' => 'sr-only']);
$output .= html_writer::tag('div', $this->prepare_response($name, $qa, $step, $context), [
'role' => 'textbox',
'aria-readonly' => 'true',
Expand Down Expand Up @@ -320,7 +339,8 @@ public function response_area_input($name, $qa, $step, $lines, $context) {
$editor->use_editor($id, $this->get_editor_options($context),
$this->get_filepicker_options($context, $draftitemid));

$output = html_writer::tag('label', get_string('answertext', 'qtype_essay'), [
$responselabel = $this->displayoptions->add_question_identifier_to_label(get_string('answertext', 'qtype_essay'));
$output = html_writer::tag('label', $responselabel, [
'class' => 'sr-only',
'for' => $id,
]);
Expand Down Expand Up @@ -514,7 +534,7 @@ protected function filepicker_html($inputname, $draftitemid) {
* @copyright 2011 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
class qtype_essay_format_plain_renderer extends plugin_renderer_base {
class qtype_essay_format_plain_renderer extends qtype_essay_format_renderer_base {
/**
* @return string the HTML for the textarea.
*/
Expand All @@ -532,7 +552,8 @@ protected function class_name() {
public function response_area_read_only($name, $qa, $step, $lines, $context) {
$id = $qa->get_qt_field_name($name) . '_id';

$output = html_writer::tag('label', get_string('answertext', 'qtype_essay'), ['class' => 'sr-only', 'for' => $id]);
$responselabel = $this->displayoptions->add_question_identifier_to_label(get_string('answertext', 'qtype_essay'));
$output = html_writer::tag('label', $responselabel, ['class' => 'sr-only', 'for' => $id]);
$output .= $this->textarea($step->get_qt_var($name), $lines, ['id' => $id, 'readonly' => 'readonly']);
return $output;
}
Expand All @@ -541,7 +562,8 @@ public function response_area_input($name, $qa, $step, $lines, $context) {
$inputname = $qa->get_qt_field_name($name);
$id = $inputname . '_id';

$output = html_writer::tag('label', get_string('answertext', 'qtype_essay'), ['class' => 'sr-only', 'for' => $id]);
$responselabel = $this->displayoptions->add_question_identifier_to_label(get_string('answertext', 'qtype_essay'));
$output = html_writer::tag('label', $responselabel, ['class' => 'sr-only', 'for' => $id]);
$output .= $this->textarea($step->get_qt_var($name), $lines, ['name' => $inputname, 'id' => $id]);
$output .= html_writer::empty_tag('input', ['type' => 'hidden', 'name' => $inputname . 'format', 'value' => FORMAT_PLAIN]);

Expand Down

0 comments on commit 5ba62ce

Please sign in to comment.