Merge branch 'MDL-85111-500' of https://github.com/muhammadarnaldo/moodle into MOODLE_500_STABLE

This commit is contained in:
Mihail Geshoski
2026-02-18 10:08:07 +08:00
4 changed files with 70 additions and 16 deletions
+11 -2
View File
@@ -7838,14 +7838,23 @@ function moodle_setlocale($locale='') {
/**
* Count words in a string.
*
* Words are defined as things between whitespace.
* Words are defined as things between whitespace. Developments have tried to ensure that this
* method gives the same results as Libre Office, MS Word, etc. However, word-counting rules are
* subtle, and not identical between languages, so there may be differences in non-English languages.
*
* @category string
* @param string $string The text to be searched for words. May be HTML.
* @param int|null $format
* @param int|null $format a FORMAT_... constant. In the API this is optional,
* but really, it is required to get accurate results, so should be passed.
* @return int The count of words in the specified string
*/
function count_words($string, $format = null) {
// If format is plain, remove < characters that are attached to non-HTML words.
if ($format === null || $format == FORMAT_PLAIN) {
// Remove < that is attached to a word but doesn't form a valid HTML tag.
// This matches < followed by word characters that don't have a closing >.
$string = preg_replace('/(\w|^|\s)<(?![^<>]*>)(?=\w)/u', '$1', $string);
}
// Before stripping tags, add a space after the close tag of anything that is not obviously inline.
// Also, br is a special case because it definitely delimits a word, but has no close tag.
$string = preg_replace('~
+6 -2
View File
@@ -3897,7 +3897,7 @@ EOF;
* @dataProvider count_words_testcases
* @param int $expectedcount number of words in $string.
* @param string $string the test string to count the words of.
* @param int|null $format
* @param int|null $format FORMAT_... constant to pass to count_words.
*/
public function test_count_words(int $expectedcount, string $string, $format = null): void {
$this->assertEquals($expectedcount, count_words($string, $format),
@@ -3957,8 +3957,12 @@ EOT;
[1, '<span>a</span><span>b</span>', FORMAT_HTML],
[1, '<span>a</span><span>b</span>', FORMAT_MOODLE],
[1, '<span>a</span><span>b</span>', FORMAT_MARKDOWN],
[1, 'aa <argh <bleh>pokus</bleh>'],
[3, 'aa <argh <bleh>pokus</bleh>'],
[2, 'aa <argh <bleh>pokus</bleh>', FORMAT_HTML],
[3, 'x < 1', FORMAT_PLAIN],
[3, 'quam justo<lectus commodo', FORMAT_PLAIN],
[5, 'lorem ipsum< dolor sit amet', FORMAT_PLAIN],
[4, 'word starting <less than', FORMAT_PLAIN],
[6, $copypasted],
[6, $copypasted, FORMAT_PLAIN],
[3, $copypasted, FORMAT_HTML],
+8 -7
View File
@@ -132,7 +132,7 @@ class qtype_essay_question extends question_with_responses {
// 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'])) {
if ($this->check_input_word_count($response['answer'], $response['answerformat'] ?? FORMAT_PLAIN)) {
return false;
}
}
@@ -180,7 +180,7 @@ class qtype_essay_question extends question_with_responses {
if ($this->is_complete_response($response)) {
return '';
}
return $this->check_input_word_count($response['answer']);
return $this->check_input_word_count($response['answer'], $response['answerformat'] ?? FORMAT_PLAIN);
}
public function is_gradable_response(array $response) {
@@ -262,10 +262,11 @@ class qtype_essay_question extends question_with_responses {
* 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
* @param string $responsestring the student's response to count the words in.
* @param int $responseformat the FORMAT_... constant for what format $responsestring is.
* @return string|null null if the word-count is in range, otherwise a string message about how it is not.
.*/
private function check_input_word_count($responsestring) {
private function check_input_word_count(string $responsestring, int $responseformat) {
if (!$this->responserequired) {
return null;
}
@@ -275,7 +276,7 @@ class qtype_essay_question extends question_with_responses {
}
// Count the number of words in the response string.
$count = count_words($responsestring);
$count = count_words($responsestring, $responseformat);
if ($this->maxwordlimit && $count > $this->maxwordlimit) {
return get_string('maxwordlimitboundary', 'qtype_essay',
['limit' => $this->maxwordlimit, 'count' => $count]);
@@ -306,7 +307,7 @@ class qtype_essay_question extends question_with_responses {
return '';
}
$count = count_words($response['answer']);
$count = count_words($response['answer'], $response['answerformat'] ?? FORMAT_PLAIN);
if ($this->maxwordlimit && $count > $this->maxwordlimit) {
return get_string('wordcounttoomuch', 'qtype_essay',
['limit' => $this->maxwordlimit, 'count' => $count]);
+45 -5
View File
@@ -26,12 +26,12 @@ require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
/**
* Unit tests for the matching question definition class.
* Unit tests for the essay question definition class.
*
* @package qtype_essay
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qtype_essay_question
* @package qtype_essay
* @copyright 2009 The Open University
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
* @covers \qtype_essay_question
*/
final class question_test extends \advanced_testcase {
public function test_get_question_summary(): void {
@@ -401,6 +401,21 @@ final class question_test extends \advanced_testcase {
$this->assertEquals($expected, $actual);
}
/**
* Test that a less-than symbol attached to a word ('<1.') in plain text
* is handled correctly and doesn't trigger HTML validation errors.
*/
public function test_get_validation_error_plain_text_with_html_chars(): void {
$question = \test_question_maker::make_an_essay_question();
$question->responseformat = 'plain';
$question->responserequired = 1;
$question->minwordlimit = 2;
$question->maxwordlimit = 2;
$response = ['answer' => 'x <1.', 'answerformat' => FORMAT_PLAIN];
$this->assertEquals('', $question->get_validation_error($response));
}
/**
* Data provider for get_validation_error test.
*
@@ -437,6 +452,31 @@ final class question_test extends \advanced_testcase {
$this->assertEquals($expected, $question->get_word_count_message_for_review($response));
}
/**
* Test word count calculation when input contains less-than symbols.
*
* Verifies that words with leading less-than symbols (like '<1.' or '<adjacent')
* are counted correctly and don't affect word boundary detection.
*/
public function test_get_word_count_message_for_review_plain_text_with_html_chars(): void {
$question = \test_question_maker::make_an_essay_question();
$question->responseformat = 'plain';
$question->responserequired = 1;
$question->minwordlimit = 2;
$question->maxwordlimit = 2;
$expected = 'Word count: 2';
$response = ['answer' => 'x <1.', 'answerformat' => FORMAT_PLAIN];
$this->assertEquals($expected, $question->get_word_count_message_for_review($response));
$expected = 'Word count: 8, more than the limit of 2 words.';
$response = [
'answer' => 'What about an <adjacent to the following word?',
'answerformat' => FORMAT_PLAIN,
];
$this->assertEquals($expected, $question->get_word_count_message_for_review($response));
}
/**
* Data provider for test_get_word_count_message_for_review.
*