Commit eceeb180 authored by Adrian Greeve's avatar Adrian Greeve

Merge branch 'MDL-68597' of https://github.com/timhunt/moodle

parents e15aaed7 c4e2b67c
......@@ -49,10 +49,9 @@ class backup_qtype_essay_plugin extends backup_qtype_plugin {
// Now create the qtype own structures.
$essay = new backup_nested_element('essay', array('id'), array(
'responseformat', 'responserequired', 'responsefieldlines',
'attachments', 'attachmentsrequired', 'graderinfo',
'graderinfoformat', 'responsetemplate', 'responsetemplateformat',
'filetypeslist', 'maxbytes'));
'responseformat', 'responserequired', 'responsefieldlines', 'minwordlimit', 'maxwordlimit',
'attachments', 'attachmentsrequired', 'graderinfo', 'graderinfoformat', 'responsetemplate',
'responsetemplateformat', 'filetypeslist', 'maxbytes'));
// Now the own qtype tree.
$pluginwrapper->add_child($essay);
......
......@@ -61,6 +61,12 @@ class restore_qtype_essay_plugin extends restore_qtype_plugin {
if (!isset($data->responserequired)) {
$data->responserequired = 1;
}
if (!isset($data->minwordlimit)) {
$data->minwordlimit = null;
}
if (!isset($data->maxwordlimit)) {
$data->maxwordlimit = null;
}
if (!isset($data->attachmentsrequired)) {
$data->attachmentsrequired = 0;
}
......@@ -111,6 +117,8 @@ class restore_qtype_essay_plugin extends restore_qtype_plugin {
$defaultoptions->responseformat = 'editor';
$defaultoptions->responserequired = 1;
$defaultoptions->responsefieldlines = 15;
$defaultoptions->minwordlimit = null;
$defaultoptions->maxwordlimit = null;
$defaultoptions->attachments = 0;
$defaultoptions->attachmentsrequired = 0;
$defaultoptions->graderinfo = '';
......
......@@ -11,6 +11,8 @@
<FIELD NAME="responseformat" TYPE="char" LENGTH="16" NOTNULL="true" DEFAULT="editor" SEQUENCE="false" COMMENT="The type of input area students should be given for their response."/>
<FIELD NAME="responserequired" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="1" SEQUENCE="false" COMMENT="Nonzero if an online text response is optional"/>
<FIELD NAME="responsefieldlines" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="15" SEQUENCE="false" COMMENT="Approximate height, in lines, of the input box the students should be given for their response."/>
<FIELD NAME="minwordlimit" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Minimum number of words"/>
<FIELD NAME="maxwordlimit" TYPE="int" LENGTH="10" NOTNULL="false" SEQUENCE="false" COMMENT="Maximum number of words"/>
<FIELD NAME="attachments" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether, and how many, attachments a student is allowed to include with their response. -1 means unlimited."/>
<FIELD NAME="attachmentsrequired" TYPE="int" LENGTH="4" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="The number of attachments that should be required"/>
<FIELD NAME="graderinfo" TYPE="text" NOTNULL="false" SEQUENCE="false" COMMENT="Information shown to people with permission to manually grade the question, when they are grading."/>
......
......@@ -63,5 +63,30 @@ function xmldb_qtype_essay_upgrade($oldversion) {
// Essay savepoint reached.
upgrade_plugin_savepoint(true, 2021052501, 'qtype', 'essay');
}
if ($oldversion < 2021052502) {
// Define field minwordlimit to be added to qtype_essay_options.
$table = new xmldb_table('qtype_essay_options');
$field = new xmldb_field('minwordlimit', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'responsefieldlines');
// Conditionally launch add field minwordlimit.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Define field maxwordlimit to be added to qtype_essay_options.
$table = new xmldb_table('qtype_essay_options');
$field = new xmldb_field('maxwordlimit', XMLDB_TYPE_INTEGER, '10', null, null, null, null, 'minwordlimit');
// Conditionally launch add field maxwordlimit.
if (!$dbman->field_exists($table, $field)) {
$dbman->add_field($table, $field);
}
// Essay savepoint reached.
upgrade_plugin_savepoint(true, 2021052502, 'qtype', 'essay');
}
return true;
}
......@@ -48,12 +48,34 @@ class qtype_essay_edit_form extends question_edit_form {
$mform->addElement('select', 'responserequired',
get_string('responserequired', 'qtype_essay'), $qtype->response_required_options());
$mform->setDefault('responserequired', 1);
$mform->disabledIf('responserequired', 'responseformat', 'eq', 'noinline');
$mform->hideIf('responserequired', 'responseformat', 'eq', 'noinline');
$mform->addElement('select', 'responsefieldlines',
get_string('responsefieldlines', 'qtype_essay'), $qtype->response_sizes());
$mform->setDefault('responsefieldlines', 15);
$mform->disabledIf('responsefieldlines', 'responseformat', 'eq', 'noinline');
$mform->hideIf('responsefieldlines', 'responseformat', 'eq', 'noinline');
// Create a text box that can be enabled/disabled for max/min word limits options.
$wordlimitoptions = ['size' => '6', 'maxlength' => '6'];
$mingrp[] = $mform->createElement('text', 'minwordlimit', '', $wordlimitoptions);
$mform->setType('minwordlimit', PARAM_INT);
$mingrp[] = $mform->createElement('checkbox', 'minwordenabled', '', get_string('enable'));
$mform->setDefault('minwordenabled', 0);
$mform->addGroup($mingrp, 'mingroup', get_string('minwordlimit', 'qtype_essay'), ' ', false);
$mform->addHelpButton('mingroup', 'minwordlimit', 'qtype_essay');
$mform->disabledIf('minwordlimit', 'minwordenabled', 'notchecked');
$mform->hideIf('mingroup', 'responserequired', 'eq', '0');
$mform->hideIf('mingroup', 'responseformat', 'eq', 'noinline');
$maxgrp[] = $mform->createElement('text', 'maxwordlimit', '', $wordlimitoptions);
$mform->setType('maxwordlimit', PARAM_INT);
$maxgrp[] = $mform->createElement('checkbox', 'maxwordenabled', '', get_string('enable'));
$mform->setDefault('maxwordenabled', 0);
$mform->addGroup($maxgrp, 'maxgroup', get_string('maxwordlimit', 'qtype_essay'), ' ', false);
$mform->addHelpButton('maxgroup', 'maxwordlimit', 'qtype_essay');
$mform->disabledIf('maxwordlimit', 'maxwordenabled', 'notchecked');
$mform->hideIf('maxgroup', 'responserequired', 'eq', '0');
$mform->hideIf('maxgroup', 'responseformat', 'eq', 'noinline');
$mform->addElement('select', 'attachments',
get_string('allowattachments', 'qtype_essay'), $qtype->attachment_options());
......@@ -63,15 +85,15 @@ class qtype_essay_edit_form extends question_edit_form {
get_string('attachmentsrequired', 'qtype_essay'), $qtype->attachments_required_options());
$mform->setDefault('attachmentsrequired', 0);
$mform->addHelpButton('attachmentsrequired', 'attachmentsrequired', 'qtype_essay');
$mform->disabledIf('attachmentsrequired', 'attachments', 'eq', 0);
$mform->hideIf('attachmentsrequired', 'attachments', 'eq', 0);
$mform->addElement('filetypes', 'filetypeslist', get_string('acceptedfiletypes', 'qtype_essay'));
$mform->addHelpButton('filetypeslist', 'acceptedfiletypes', 'qtype_essay');
$mform->disabledIf('filetypeslist', 'attachments', 'eq', 0);
$mform->hideIf('filetypeslist', 'attachments', 'eq', 0);
$mform->addElement('select', 'maxbytes', get_string('maxbytes', 'qtype_essay'), $qtype->max_file_size_options());
$mform->setDefault('maxbytes', '0');
$mform->disabledIf('maxbytes', 'attachments', 'eq', 0);
$mform->hideIf('maxbytes', 'attachments', 'eq', 0);
$mform->addElement('header', 'responsetemplateheader', get_string('responsetemplateheader', 'qtype_essay'));
$mform->addElement('editor', 'responsetemplate', get_string('responsetemplate', 'qtype_essay'),
......@@ -94,6 +116,10 @@ class qtype_essay_edit_form extends question_edit_form {
$question->responseformat = $question->options->responseformat;
$question->responserequired = $question->options->responserequired;
$question->responsefieldlines = $question->options->responsefieldlines;
$question->minwordenabled = $question->options->minwordlimit ? 1 : 0;
$question->minwordlimit = $question->options->minwordlimit;
$question->maxwordenabled = $question->options->maxwordlimit ? 1 : 0;
$question->maxwordlimit = $question->options->maxwordlimit;
$question->attachments = $question->options->attachments;
$question->attachmentsrequired = $question->options->attachmentsrequired;
$question->filetypeslist = $question->options->filetypeslist;
......@@ -142,6 +168,36 @@ class qtype_essay_edit_form extends question_edit_form {
$errors['attachmentsrequired'] = get_string('mustrequirefewer', 'qtype_essay');
}
if ($fromform['responserequired']) {
if (isset($fromform['minwordenabled'])) {
if (!is_numeric($fromform['minwordlimit'])) {
$errors['mingroup'] = get_string('err_numeric', 'form');
}
if ($fromform['minwordlimit'] < 0) {
$errors['mingroup'] = get_string('err_minwordlimitnegative', 'qtype_essay');
}
if (!$fromform['minwordlimit']) {
$errors['mingroup'] = get_string('err_minwordlimit', 'qtype_essay');
}
}
if (isset($fromform['maxwordenabled'])) {
if (!is_numeric($fromform['maxwordlimit'])) {
$errors['maxgroup'] = get_string('err_numeric', 'form');
}
if ($fromform['maxwordlimit'] < 0) {
$errors['maxgroup'] = get_string('err_maxwordlimitnegative', 'qtype_essay');
}
if (!$fromform['maxwordlimit']) {
$errors['maxgroup'] = get_string('err_maxwordlimit', 'qtype_essay');
}
}
if (isset($fromform['maxwordenabled']) && isset($fromform['minwordenabled'])) {
if ($fromform['maxwordlimit'] < $fromform['minwordlimit'] &&
$fromform['maxwordlimit'] > 0 && $fromform['minwordlimit'] > 0) {
$errors['maxgroup'] = get_string('err_maxminmismatch', 'qtype_essay');
}
}
}
return $errors;
}
......
......@@ -29,6 +29,11 @@ $string['allowattachments'] = 'Allow attachments';
$string['attachmentsoptional'] = 'Attachments are optional';
$string['attachmentsrequired'] = 'Require attachments';
$string['attachmentsrequired_help'] = 'This option specifies the minimum number of attachments required for a response to be considered gradable.';
$string['err_maxminmismatch'] = 'Maximum world limit must be greater than minimum word limit';
$string['err_maxwordlimit'] = 'Maximum word limit is enabled but is not set';
$string['err_maxwordlimitnegative'] = 'Maximum word limit cannot be a negative number';
$string['err_minwordlimit'] = 'Minimum word limit is enabled but is not set';
$string['err_minwordlimitnegative'] = 'Minimum word limit cannot be a negative number';
$string['formateditor'] = 'HTML editor';
$string['formateditorfilepicker'] = 'HTML editor with file picker';
$string['formatmonospaced'] = 'Plain text, monospaced font';
......@@ -37,6 +42,12 @@ $string['formatplain'] = 'Plain text';
$string['graderinfo'] = 'Information for graders';
$string['graderinfoheader'] = 'Grader Information';
$string['maxbytes'] = 'Maximum file size';
$string['maxwordlimit'] = 'Maximum word limit';
$string['maxwordlimit_help'] = 'If the response requires that students enter text, this is the maximum number of words that each student will be allowed to submit.';
$string['maxwordlimitboundary'] = 'The word limit for this question is {$a->limit} words and you are attempting to submit {$a->count} words. Please shorten your response and try again.';
$string['minwordlimit'] = 'Minimum word limit';
$string['minwordlimit_help'] = 'If the response requires that students enter text, this is the minimum number of words that each student will be allowed to submit.';
$string['minwordlimitboundary'] = 'This question requires a response of at least {$a->limit} words and you are attempting to submit {$a->count} words. Please expand your response and try again.';
$string['mustattach'] = 'When "No online text" is selected, or responses are optional, you must allow at least one attachment.';
$string['mustrequire'] = 'When "No online text" is selected, or responses are optional, you must require at least one attachment.';
$string['mustrequirefewer'] = 'You cannot require more attachments than you allow.';
......@@ -58,3 +69,6 @@ $string['responseisrequired'] = 'Require the student to enter text';
$string['responsetemplate'] = 'Response template';
$string['responsetemplateheader'] = 'Response Template';
$string['responsetemplate_help'] = 'Any text entered here will be displayed in the response input box when a new attempt at the question starts.';
$string['wordcount'] = 'Word count: {$a}';
$string['wordcounttoofew'] = 'Word count: {$a->count}, less than the required {$a->limit} words.';
$string['wordcounttoomuch'] = 'Word count: {$a->count}, more than the limit of {$a->limit} words.';
......@@ -42,6 +42,13 @@ class qtype_essay_question extends question_with_responses {
public $responserequired;
public $responsefieldlines;
/** @var int indicates whether the minimum number of words required */
public $minwordlimit;
/** @var int indicates whether the maximum number of words required */
public $maxwordlimit;
public $attachments;
/** @var int maximum file size in bytes */
......@@ -107,6 +114,13 @@ class qtype_essay_question extends question_with_responses {
public function is_complete_response(array $response) {
// Determine if the given response has online text and attachments.
$hasinlinetext = array_key_exists('answer', $response) && ($response['answer'] !== '');
// If there is a response and min/max word limit is set in the form then validate the number of words in response.
if ($hasinlinetext) {
if ($this->check_input_word_count($response['answer'])) {
return false;
}
}
$hasattachments = array_key_exists('attachments', $response)
&& $response['attachments'] instanceof question_response_files;
......@@ -140,6 +154,20 @@ class qtype_essay_question extends question_with_responses {
return $hascontent && $meetsinlinereq && $meetsattachmentreq;
}
/**
* Return null if is_complete_response() returns true
* otherwise, return the minmax-limit error message
*
* @param array $response
* @return string
*/
public function get_validation_error(array $response) {
if ($this->is_complete_response($response)) {
return '';
}
return $this->check_input_word_count($response['answer']);
}
public function is_gradable_response(array $response) {
// Determine if the given response has online text and attachments.
if (array_key_exists('answer', $response) && ($response['answer'] !== '')) {
......@@ -212,4 +240,64 @@ class qtype_essay_question extends question_with_responses {
return $settings;
}
/**
* Check the input word count and return a message to user
* when the number of words are outside the boundary settings.
*
* @param string $responsestring
* @return string|null
.*/
private function check_input_word_count($responsestring) {
if (!$this->responserequired) {
return null;
}
if (!$this->minwordlimit && !$this->maxwordlimit) {
// This question does not care about the word count.
return null;
}
// Count the number of words in the response string.
$count = count_words($responsestring);
if ($this->maxwordlimit && $count > $this->maxwordlimit) {
return get_string('maxwordlimitboundary', 'qtype_essay',
['limit' => $this->maxwordlimit, 'count' => $count]);
} else if ($count < $this->minwordlimit) {
return get_string('minwordlimitboundary', 'qtype_essay',
['limit' => $this->minwordlimit, 'count' => $count]);
} else {
return null;
}
}
/**
* If this question uses word counts, then return a display of the current
* count, and whether it is within limit, for when the question is being reviewed.
*
* @param array $response responses, as returned by
* {@see question_attempt_step::get_qt_data()}.
* @return string If relevant to this question, a display of the word count.
*/
public function get_word_count_message_for_review(array $response): string {
if (!$this->minwordlimit && !$this->maxwordlimit) {
// This question does not care about the word count.
return '';
}
if (!array_key_exists('answer', $response) || ($response['answer'] === '')) {
// No response.
return '';
}
$count = count_words($response['answer']);
if ($this->maxwordlimit && $count > $this->maxwordlimit) {
return get_string('wordcounttoomuch', 'qtype_essay',
['limit' => $this->maxwordlimit, 'count' => $count]);
} else if ($count < $this->minwordlimit) {
return get_string('wordcounttoofew', 'qtype_essay',
['limit' => $this->minwordlimit, 'count' => $count]);
} else {
return get_string('wordcount', 'qtype_essay', $count);
}
}
}
......@@ -65,6 +65,8 @@ class qtype_essay extends question_type {
$options->responseformat = $formdata->responseformat;
$options->responserequired = $formdata->responserequired;
$options->responsefieldlines = $formdata->responsefieldlines;
$options->minwordlimit = isset($formdata->minwordenabled) ? $formdata->minwordlimit : 0;
$options->maxwordlimit = isset($formdata->maxwordenabled) ? $formdata->maxwordlimit : 0;
$options->attachments = $formdata->attachments;
$options->attachmentsrequired = $formdata->attachmentsrequired;
if (!isset($formdata->filetypeslist)) {
......@@ -86,6 +88,8 @@ class qtype_essay extends question_type {
$question->responseformat = $questiondata->options->responseformat;
$question->responserequired = $questiondata->options->responserequired;
$question->responsefieldlines = $questiondata->options->responsefieldlines;
$question->minwordlimit = $questiondata->options->minwordlimit;
$question->maxwordlimit = $questiondata->options->maxwordlimit;
$question->attachments = $questiondata->options->attachments;
$question->attachmentsrequired = $questiondata->options->attachmentsrequired;
$question->graderinfo = $questiondata->options->graderinfo;
......
......@@ -55,6 +55,8 @@ class qtype_essay_renderer extends qtype_renderer {
} else {
$answer = $responseoutput->response_area_read_only('answer', $qa,
$step, $question->responsefieldlines, $options->context);
$answer .= html_writer::nonempty_tag('p', $question->get_word_count_message_for_review($step->get_qt_data()));
}
$files = '';
......@@ -73,6 +75,12 @@ class qtype_essay_renderer extends qtype_renderer {
$result .= html_writer::start_tag('div', array('class' => 'ablock'));
$result .= html_writer::tag('div', $answer, array('class' => 'answer'));
// If there is a response and min/max word limit is set in the form then check the response word count.
if ($qa->get_state() == question_state::$invalid) {
$result .= html_writer::nonempty_tag('div',
$question->get_validation_error($step->get_qt_data()), ['class' => 'validationerror']);
}
$result .= html_writer::tag('div', $files, array('class' => 'attachments'));
$result .= html_writer::end_tag('div');
......
@qtype @qtype_essay
Feature: In an essay question, let the question author choose the min/max number of words for input text
In order to constrain student submissions for marking
As a teacher
I need to choose the appropriate minimum and/or maximum number of words for input text
Background:
Given the following "users" exist:
| username | firstname | lastname | email |
| teacher1 | T1 | Teacher1 | teacher1@moodle.com |
And the following "courses" exist:
| fullname | shortname | category |
| Course 1 | C1 | 0 |
And the following "course enrolments" exist:
| user | course | role |
| teacher1 | C1 | editingteacher |
And the following "question categories" exist:
| contextlevel | reference | name |
| Course | C1 | Test questions |
And the following "questions" exist:
| questioncategory | qtype | name | template | minwordlimit | maxwordlimit |
| Test questions | essay | essay-min-max | editor | null | null |
Given I log in as "teacher1"
And I am on "Course 1" course homepage
And I navigate to "Question bank" in current page administration
@javascript
Scenario: Minimum/Maximum word limit are enabled but not set.
Given I choose "Edit question" action for "essay-min-max" in the question bank
When I set the field "minwordenabled" to "1"
And I click on "Save changes" "button"
Then I should see "Minimum word limit is enabled but is not set"
@javascript
Scenario: Minimum/Maximum word limit cannot be set to a negative number.
Given I choose "Edit question" action for "essay-min-max" in the question bank
And I set the field "minwordenabled" to "1"
When I set the field "id_minwordlimit" to "-10"
And I click on "Save changes" "button"
Then I should see "Minimum word limit cannot be a negative number"
@javascript
Scenario: Maximum word limit cannot be greater than minimum word limit.
Given I choose "Edit question" action for "essay-min-max" in the question bank
And I set the field "minwordenabled" to "1"
And I set the field "id_minwordlimit" to "500"
And I set the field "maxwordenabled" to "1"
When I set the field "id_maxwordlimit" to "450"
And I click on "Save changes" "button"
Then I should see "Maximum world limit must be greater than minimum word limit"
@javascript
Scenario: Modify the question to see 'Minimum word limit' and 'Maximum word limit' are hidden when 'Require text' field is set to 'Text input is optional'
Given I choose "Edit question" action for "essay-min-max" in the question bank
And I should see "Minimum word limit"
And I should see "Maximum word limit"
When I set the field "Require text" to "Text input is optional"
Then I should not see "Minimum word limit"
And I should not see "Minimum word limit"
......@@ -27,7 +27,7 @@ Feature: Preview Essay questions
And I navigate to "Question bank" in current page administration
@javascript @_switch_window
Scenario: Preview an Essay question and submit a partially correct response.
Scenario: Preview an Essay question that uses the HTML editor.
When I choose "Preview" action for "essay-001" in the question bank
And I switch to "questionpreview" window
And I set the field "How questions behave" to "Immediate feedback"
......@@ -36,7 +36,7 @@ Feature: Preview Essay questions
And I switch to the main window
@javascript @_switch_window
Scenario: Preview an Essay question and submit a partially correct response.
Scenario: Preview an Essay question that uses the HTML editor with embedded files.
When I choose "Preview" action for "essay-002" in the question bank
And I switch to "questionpreview" window
And I set the field "How questions behave" to "Immediate feedback"
......@@ -46,7 +46,7 @@ Feature: Preview Essay questions
And I switch to the main window
@javascript @_switch_window
Scenario: Preview an Essay question and submit a partially correct response.
Scenario: Preview an Essay question that uses a plain text area.
When I choose "Preview" action for "essay-003" in the question bank
And I switch to "questionpreview" window
And I set the field "How questions behave" to "Immediate feedback"
......
......@@ -25,6 +25,8 @@
<responseformat>editor</responseformat>
<responserequired>1</responserequired>
<responsefieldlines>15</responsefieldlines>
<minwordlimit>0</minwordlimit>
<maxwordlimit>0</maxwordlimit>
<attachments>0</attachments>
<attachmentsrequired>0</attachmentsrequired>
<filetypeslist></filetypeslist>
......
......@@ -51,6 +51,8 @@ class qtype_essay_test_helper extends question_test_helper {
$q->responseformat = 'editor';
$q->responserequired = 1;
$q->responsefieldlines = 10;
$q->minwordlimit = 0;
$q->maxwordlimit = 0;
$q->attachments = 0;
$q->attachmentsrequired = 0;
$q->filetypeslist = '';
......@@ -86,6 +88,8 @@ class qtype_essay_test_helper extends question_test_helper {
$fromform->responseformat = 'editor';
$fromform->responserequired = 1;
$fromform->responsefieldlines = 10;
$fromform->minwordlimit = 0;
$fromform->maxwordlimit = 0;
$fromform->attachments = 0;
$fromform->attachmentsrequired = 0;
$fromform->filetypeslist = '';
......@@ -138,6 +142,8 @@ class qtype_essay_test_helper extends question_test_helper {
$fromform->responseformat = 'editorfilepicker';
$fromform->responserequired = 1;
$fromform->responsefieldlines = 10;
$fromform->minwordlimit = 0;
$fromform->maxwordlimit = 0;
$fromform->attachments = 3;
$fromform->attachmentsrequired = 0;
$fromform->filetypeslist = '';
......@@ -175,6 +181,8 @@ class qtype_essay_test_helper extends question_test_helper {
$fromform->responseformat = 'plain';
$fromform->responserequired = 1;
$fromform->responsefieldlines = 10;
$fromform->minwordlimit = 0;
$fromform->maxwordlimit = 0;
$fromform->attachments = 0;
$fromform->attachmentsrequired = 0;
$fromform->filetypeslist = '';
......
......@@ -170,6 +170,25 @@ class qtype_essay_question_test extends advanced_testcase {
$this->assertTrue($essay->is_complete_response(array('answer' => '0 times.')));
$this->assertTrue($essay->is_complete_response(array('answer' => '0')));
// Test case for minimum and/or maximum word limit.
$response = [];
$response['answer'] = 'In this essay, I will be testing a function called check_input_word_count().';
$essay->minwordlimit = 50; // The answer is shorter than the required minimum word limit.
$this->assertFalse($essay->is_complete_response($response));
$essay->minwordlimit = 10; // The word count meets the required minimum word limit.
$this->assertTrue($essay->is_complete_response($response));
// The word count meets the required minimum and maximum word limit.
$essay->minwordlimit = 10;
$essay->maxwordlimit = 15;
$this->assertTrue($essay->is_complete_response($response));
// Unset the minwordlimit/maxwordlimit variables to avoid the extra check in is_complete_response() for further tests.
$essay->minwordlimit = null;
$essay->maxwordlimit = null;
// Test the case where two files are required.
$essay->attachmentsrequired = 2;
......@@ -236,7 +255,6 @@ class qtype_essay_question_test extends advanced_testcase {
$essay->reponserequired = 1;
$this->assertTrue($essay->is_complete_response(
array('attachments' => $attachments[1])));
}
/**
......@@ -262,4 +280,88 @@ class qtype_essay_question_test extends advanced_testcase {
$this->assertEquals('', $options['responsetemplate']);
$this->assertEquals(FORMAT_MOODLE, $options['responsetemplateformat']);
}
/**
* Test get_validation_error when users submit their input text.
*
* (The tests are done with a fixed 14-word response.)
*
* @dataProvider get_min_max_wordlimit_test_cases()
* @param int $responserequired whether response required (yes = 1, no = 0)
* @param int $minwordlimit minimum word limit
* @param int $maxwordlimit maximum word limit
* @param string $expected error message | null
*/
public function test_get_validation_error(int $responserequired,
int $minwordlimit, int $maxwordlimit, string $expected): void {
$question = test_question_maker::make_an_essay_question();
$response = ['answer' => 'In this essay, I will be testing a function called check_input_word_count().'];
$question->responserequired = $responserequired;
$question->minwordlimit = $minwordlimit;
$question->maxwordlimit = $maxwordlimit;
$actual = $question->get_validation_error($response);
$this->assertEquals($expected, $actual);
}
/**
* Data provider for get_validation_error test.
*
* @return array the test cases.
*/
public function get_min_max_wordlimit_test_cases(): array {
return [
'text input required, min/max word limit not set' => [1, 0, 0, ''],
'text input required, min/max word limit valid (within the boundaries)' => [1, 10, 25, ''],
'text input required, min word limit not reached' => [1, 15, 25,
get_string('minwordlimitboundary', 'qtype_essay', ['count' => 14, 'limit' => 15])],
'text input required, max word limit is exceeded' => [1, 5, 12,
get_string('maxwordlimitboundary', 'qtype_essay', ['count' => 14, 'limit' => 12])],
'text input not required, min/max word limit not set' => [0, 5, 12, ''],
];
}
/**
* Test get_word_count_message_for_review when users submit their input text.
*
* (The tests are done with a fixed 14-word response.)
*
* @dataProvider get_word_count_message_for_review_test_cases()
* @param int|null $minwordlimit minimum word limit
* @param int|null $maxwordlimit maximum word limit
* @param string $expected error message | null
*/
public function test_get_word_count_message_for_review(?int $minwordlimit, ?int $maxwordlimit, string $expected): void {
$question = test_question_maker::make_an_essay_question();
$question->minwordlimit = $minwordlimit;
$question->maxwordlimit = $maxwordlimit;
$response = ['answer' => 'One two three four five six seven eight nine ten eleven twelve thirteen fourteen.'];
$this->assertEquals($expected, $question->get_word_count_message_for_review($response));
}
/**
* Data provider for test_get_word_count_message_for_review.
*
* @return array the test cases.
*/
public function get_word_count_message_for_review_test_cases() {
return [
'No limit' =>
[null, null, ''],
'min and max, answer within range' =>
[10, 25, get_string('wordcount', 'qtype_essay', 14)],
'min and max, answer too short' =>
[15, 25, get_string('wordcounttoofew', 'qtype_essay', ['count' => 14, 'limit' => 15])],
'min and max, answer too long' =>
[5, 12, get_string('wordcounttoomuch', 'qtype_essay', ['count' => 14, 'limit' => 12])],
'min only, answer within range' =>
[14, null, get_string('wordcount', 'qtype_essay', 14)],
'min only, answer too short' =>
[15, null, get_string('wordcounttoofew', 'qtype_essay', ['count' => 14, 'limit' => 15])],
'max only, answer within range' =>
[null, 14, get_string('wordcount', 'qtype_essay', 14)],
'max only, answer too short' =>
[null, 13, get_string('wordcounttoomuch', 'qtype_essay', ['count' => 14, 'limit' => 13])],
];
}
}
......@@ -619,4 +619,113 @@ class qtype_essay_walkthrough_testcase extends qbehaviour_walkthrough_test_base
$this->check_step_count(3);
$this->save_quba();
}
public function test_deferred_feedback_word_limits() {
global $PAGE;
// The current text editor depends on the users profile setting - so it needs a valid user.
$this->setAdminUser();
// Required to init a text editor.
$PAGE->set_url('/');
// Create an essay question.
/** @var qtype_essay_question $q */
$q = test_question_maker::make_question('essay', 'editor');
$q->minwordlimit = 3;
$q->maxwordlimit = 7;
$this->start_attempt_at_question($q, 'deferredfeedback', 1);
// Check the initial state.
$this->check_current_state(question_state::$todo);
$this->check_current_mark(null);
$this->render();
$this->check_contains_textarea('answer', '');
$this->check_current_output(
$this->get_contains_question_text_expectation($q),
$this->get_does_not_contain_validation_error_expectation(),
$this->get_does_not_contain_feedback_expectation());
// Save a response that is too short (and give the word-count code a tricky case).
$response = '<div class="card">
<div class="card-body">
<h3 class="card-title">One</h3>
<div class="card-text">
<ul>
<li>Two</li>
</ul>
</div>
</div>
</div>';
$this->process_submission(['answer' => $response, 'answerformat' => FORMAT_HTML]);
// Verify.
$this->check_current_state(question_state::$invalid);
$this->check_current_mark(null);
$this->render();
$this->check_contains_textarea('answer', $response);
$this->check_current_output(
$this->get_contains_question_text_expectation($q),
$this->get_contains_validation_error_expectation(),
$this->get_does_not_contain_feedback_expectation());
$this->assertStringContainsString('This question requires a response of at least 3 words and you are ' .
'attempting to submit 2 words. Please expand your response and try again.',
$this->currentoutput);
// Save a response that is just long enough.
$this->process_submission(['answer' => '<p>One two three.</p>', 'answerformat' => FORMAT_HTML]);
// Verify.
$this->check_current_state(question_state::$complete);
$this->check_current_mark(null);
$this->render();
$this->check_contains_textarea('answer', '<p>One two three.</p>');