Skip to content

Commit

Permalink
Crossword: must support apostrophes in the answer
Browse files Browse the repository at this point in the history
  • Loading branch information
danghieu1407 authored and AnupamaSarjoshi committed Feb 26, 2025
1 parent 5068a30 commit 5bb96ee
Show file tree
Hide file tree
Showing 16 changed files with 109 additions and 59 deletions.
2 changes: 1 addition & 1 deletion amd/build/crossword_grid.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion amd/build/crossword_grid.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion amd/build/crossword_question.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion amd/build/crossword_question.min.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion amd/src/crossword_grid.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export class CrosswordGrid extends CrosswordQuestion {
return;
}
for (let i = 0; i < words.length; i++) {
const answer = words[i].answer.trim().replace(/-|\s/g, '');
const answer = words[i].answer.trim().replace(/-|\s|\'||/g, '');
let row = words[i].startrow + 1;
let column = words[i].startcolumn + 1;
let answerLength = answer.length;
Expand Down
10 changes: 8 additions & 2 deletions amd/src/crossword_question.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,13 @@ export class CrosswordQuestion {
coordinates: '',
maxSizeCell: 50,
minSizeCell: 30,
specialCharacters: {hyphen: '-', space: ' '},
specialCharacters: {
hyphen: '-',
space: ' ',
straightsinglequote: '\'',
openingsinglequote: '‘',
closingsinglequote: '’',
},
};
// Merge options.
defaultOption = {...defaultOption, ...options};
Expand Down Expand Up @@ -220,7 +226,7 @@ export class CrosswordQuestion {
* Calculate and retreive the letter index.
*
* @param {Number} letterIndex The current letter index.
* @param {Array} ignoreList The ignore list; If the letter contains space or hyphen
* @param {Array} ignoreList The ignore list; If the letter contains space or hyphen or apostrophes.
* @param {Number} wordLength The word length.
* characters. We have to ignore it.
* @return {Number} The new letter index.
Expand Down
14 changes: 10 additions & 4 deletions classes/answer.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,14 @@ public function generate_answer_hint(): array {
$count = 0;
$answerhint = '';
$ignorecharcterindex = [];
// Allow space and hyphen only.
$listspecialcharacters = ['space' => ' ', 'hyphen' => '-'];
// Allow space, hyphen and apostrophes only.
$listspecialcharacters = [
'space' => ' ',
'hyphen' => '-',
'straightsinglequote' => '\'',
'openingsinglequote' => '',
'closingsinglequote' => '',
];
// Retrieve the answer length (answers that still contain spaces and hyphens).
$length = \core_text::strlen($this->answer);
// Loop the answer by letter.
Expand All @@ -134,14 +140,14 @@ public function generate_answer_hint(): array {
// In case the character is a space or a hyphen, we need to handle it further.
if (in_array($letter, array_values($listspecialcharacters))) {
// Get type of the special character.
// It should return 'space' or 'hyphen'.
// It should return 'space' or 'hyphen' or 'apostrophes'.
$character = array_search($letter, $listspecialcharacters);
if ($character < -1) {
continue;
}
// Store index of special character.
$ignorecharcterindex[$character][] = $index;
// Prevents the value 0 when double spaces/hyphen exist.
// Prevents the value 0 when double spaces/hyphen/apostrophes exist.
// E.g: The result should be 1, 2 instead of 1, 0, 2.
if ($count > 0) {
// Generate answer hint.
Expand Down
31 changes: 28 additions & 3 deletions classes/util.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ public static function safe_normalize(string $string, int $normalizeform = Norma
}

/**
* Remove the work-break characters '-' and ' ' from an answer.
* Remove the work-break characters '-' and ' ' and apostrophes from an answer.
*
* @param string $text Full answer.
* @return string Answer with just the letters remaining.
*/
public static function remove_break_characters(string $text): string {
// Remove hyphen and space from text.
return preg_replace('/-|\s/', '', $text);
// Remove hyphen, space and apostrophes from text.
return preg_replace('/-|\s|\'|‘|’/', '', $text);
}

/**
Expand Down Expand Up @@ -135,4 +135,29 @@ public static function update_answer_list(array $answers): array {

return $answerresponse;
}

/**
* Convert smart quotes to straight quotes, handling recursion for arrays.
*
* @param mixed $input Form input data can be a string / number / array.
* @return mixed
*/
public static function convert_quote_to_straight_quote(mixed $input): mixed {
if (is_array($input)) {
// If input is an array, process each element recursively.
foreach ($input as $key => $subvalue) {
$input[$key] = self::convert_quote_to_straight_quote($subvalue);
}
} else if (is_string($input)) {
// If input is a string, convert quotes.
// Replace smart quotes with straight quotes.
$input = str_replace(
['&lsquo;', '&rsquo;', '&ldquo;', '&rdquo;', '', '', '', ''], // HTML entities and smart quotes.
["'", "'", '"', '"', "'", "'", '"', '"'], // Corresponding straight quotes.
$input
);
}

return $input;
}
}
6 changes: 5 additions & 1 deletion edit_crossword_form.php
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ protected function data_preprocessing_words(stdClass $question): stdClass {
$question->numrows = $question->options->numrows;
$question->numcolumns = $question->options->numcolumns;
$question->accentgradingtype = $question->options->accentgradingtype;
$question->quotematching = $question->options->quotematching;
$question->accentpenalty = $question->options->accentpenalty;
}
$question->answer = $answer;
Expand All @@ -332,8 +333,11 @@ public function validation($data, $files): array {
$answercount = 0;
$answers = $data['answer'];
$clues = $data['clue'];
if (isset($data['quotematching']) && $data['quotematching'] == 0) {
$data = util::convert_quote_to_straight_quote($data);
}
// phpcs:ignore
$regex = '/([^\p{L}\p{N}\-\s]+)/u';
$regex = '/([^\p{L}\p{N}\s\-‘’\']+)/u';
$except = [];
for ($i = 0; $i < count($answers); $i++) {
// Skip the invalid word.
Expand Down
2 changes: 1 addition & 1 deletion lang/en/qtype_crossword.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
$string['down'] = 'Down';
$string['inputlabel'] = '{$a->number} {$a->orientation}. {$a->clue} Answer length {$a->length}';
$string['missingresponse'] = '-';
$string['mustbealphanumeric'] = 'The answer must be alphanumeric characters only';
$string['mustbealphanumeric'] = 'The answer must contain alphanumeric characters. Special characters allowed are hyphens and apostrophes.';
$string['notenoughwords'] = 'This type of question requires at least {$a} word';
$string['numberofcolumns'] = 'Number of columns';
$string['numberofrows'] = 'Number of rows';
Expand Down
27 changes: 1 addition & 26 deletions questiontype.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,31 +94,6 @@ protected function create_default_options($question): object {
return $options;
}

/**
* Convert smart quotes to straight quotes, handling recursion for arrays.
*
* @param mixed $input Form input data can be a string / number / array.
* @return mixed
*/
public function convert_quote_to_straight_quote(mixed $input): mixed {
if (is_array($input)) {
// If input is an array, process each element recursively.
foreach ($input as $key => $subvalue) {
$input[$key] = $this->convert_quote_to_straight_quote($subvalue);
}
} else if (is_string($input)) {
// If input is a string, convert quotes.
// Replace smart quotes with straight quotes.
$input = str_replace(
['&lsquo;', '&rsquo;', '&ldquo;', '&rdquo;', '', '', '', ''], // HTML entities and smart quotes.
["'", "'", '"', '"', "'", "'", '"', '"'], // Corresponding straight quotes.
$input
);
}

return $input;
}

#[\Override]
public function save_question($question, $form) {
// For MVP version, default mark will be set automatically.
Expand All @@ -133,7 +108,7 @@ public function save_question($question, $form) {
if (!$form->quotematching) {
foreach ($form as $property => $value) {
if (isset($value)) {
$form->{$property} = $this->convert_quote_to_straight_quote($value);
$form->{$property} = util::convert_quote_to_straight_quote($value);
}
}
}
Expand Down
9 changes: 5 additions & 4 deletions tests/answer_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ final class answer_test extends \advanced_testcase {
*/
public function test_is_correct(array $answerdata): void {
// Create a normal crossword question.
$q = \test_question_maker::make_question('crossword', 'normal_with_hyphen_and_space');
$q = \test_question_maker::make_question('crossword', 'normal_with_hyphen_space_and_apostrophes');
foreach ($q->answers as $key => $answer) {
$this->assertTrue($answer->is_correct($answerdata[$key]));
}
Expand All @@ -52,10 +52,10 @@ public function test_is_correct(array $answerdata): void {
public static function is_correct_test_provider(): array {
return [
'Normal case' => [
['TIM BERNERS-LEE', 'GORDON BROWN', 'DAVID ATTENBOROUGH'],
['TIM BERNERS-LEE', 'GORDON BROWN', 'DAVID ATTENBOROUGH', "ALBERT EINSTEIN'S THEORY"],
],
'With Underscore' => [
['TIM_BERNERS-LEE', 'GORDON_BROWN', 'DAVID_ATTENBOROUGH'],
['TIM_BERNERS-LEE', 'GORDON_BROWN', 'DAVID_ATTENBOROUGH', "ALBERT_EINSTEIN'S_THEORY"],
],
];
}
Expand All @@ -67,11 +67,12 @@ public static function is_correct_test_provider(): array {
*/
public function test_generate_answer_hint(): void {
// Create a normal crossword question.
$q = \test_question_maker::make_question('crossword', 'normal_with_hyphen_and_space');
$q = \test_question_maker::make_question('crossword', 'normal_with_hyphen_space_and_apostrophes');
$expecteddata = [
['3, 7-3', ['space' => [3], 'hyphen' => [11]]],
['6, 5', ['space' => [6]]],
['5, 12', ['space' => [5]]],
['6, 8\'1, 6', ['space' => [6, 17], 'straightsinglequote' => [15]]],
];
foreach ($q->answers as $key => $answer) {
$this->assertEquals($expecteddata[$key], $answer->generate_answer_hint());
Expand Down
10 changes: 8 additions & 2 deletions tests/behat/preview.feature
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Feature: Preview a Crossword question
| Test questions | crossword | crossword-003 | different_codepoint |
| Test questions | crossword | crossword-004 | sampleimage |
| Test questions | crossword | crossword-005 | clear_incorrect_response |
| Test questions | crossword | crossword-006 | normal_with_hyphen_and_space |
| Test questions | crossword | crossword-006 | normal_with_hyphen_space_and_apostrophes |
| Test questions | crossword | crossword-007 | accept_wrong_accents_but_subtract_point |
| Test questions | crossword | crossword-008 | accept_wrong_accents_but_not_subtract_point |
| Test questions | crossword | crossword-009 | not_accept_wrong_accents |
Expand Down Expand Up @@ -226,14 +226,16 @@ Feature: Preview a Crossword question
And the field "3 Across. Where is the Leaning Tower of Pisa? Answer length 5" matches value "ITALY"

@javascript
Scenario: For answers that contain spaces or hyphens, the answer hint will not count those characters.
Scenario: For answers that contain spaces or hyphens or apostrophes, the answer hint will not count those characters.
When I am on the "crossword-006" "core_question > preview" page logged in as teacher
And I expand all fieldsets
And I set the field "How questions behave" to "Interactive with multiple tries"
And I press "id_saverestart"
Then I should see "(5, 12)"
And I should see "(6, 5)"
And I should see "(3, 7-3)"
And I should see "(6, 8'1, 6)"
And I should see "(7, 7’1, 4)"

@javascript
Scenario: Preview a Crossword question and submit a correct response with mobile input.
Expand Down Expand Up @@ -329,6 +331,10 @@ Feature: Preview a Crossword question
And I set the field "1 Down. Engineer, computer scientist and inventor of the World Wide Web? Answer length 3, 7-3" to "TIMBERNERSLEE"
And I set the field "2 Down. Former Prime Minister of the United Kingdom? Answer length 6, 5" to "GORDONBROWN"
And I set the field "3 Across. British broadcaster and naturalist, famous for his voice-overs of nature programmes? Answer length 5, 12" to "DAVIDATTENBOROUGH"
And I set the field "4 Down. Physicist known for black hole research and author of \"A Brief History of Time\"? Answer length 7, 7’1, 4" to "STEPHENHAWKINGSWORK"
And I set the field "5 Down. Famous physicist known for his theory of relativity? Answer length 6, 8'1, 6" to "ALBERTEINSTEINSTHEORY"
Then the field "1 Down. Engineer, computer scientist and inventor of the World Wide Web? Answer length 3, 7-3" matches value "TIM BERNERS-LEE"
And the field "2 Down. Former Prime Minister of the United Kingdom? Answer length 6, 5" matches value "GORDON BROWN"
And the field "3 Across. British broadcaster and naturalist, famous for his voice-overs of nature programmes? Answer length 5, 12" matches value "DAVID ATTENBOROUGH"
And the field "4 Down. Physicist known for black hole research and author of \"A Brief History of Time\"? Answer length 7, 7’1, 4" matches value "STEPHEN HAWKING’S WORK"
And the field "5 Down. Famous physicist known for his theory of relativity? Answer length 6, 8'1, 6" matches value "ALBERT EINSTEIN'S THEORY"
2 changes: 1 addition & 1 deletion tests/form_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public static function form_validation_testcases(): array {
'answer[0]' => get_string('overflowposition', 'qtype_crossword'),
],
],
'The answer must be alphanumeric characters only' => [
'The answer must contain alphanumeric characters' => [
[
'noanswers' => 3,
'answer' => ['Speci@al char*', 'BBB', 'CCC'],
Expand Down
45 changes: 36 additions & 9 deletions tests/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class qtype_crossword_test_helper extends question_test_helper {
public function get_test_questions(): array {
// phpcs:disable NormalizedArrays.Arrays.CommaAfterLast.MissingMultiLine
return ['normal', 'unicode', 'different_codepoint', 'sampleimage',
'clear_incorrect_response', 'normal_with_hyphen_and_space',
'clear_incorrect_response', 'normal_with_hyphen_space_and_apostrophes',
'not_accept_wrong_accents', 'accept_wrong_accents_but_subtract_point',
'accept_wrong_accents_but_not_subtract_point'];
// phpcs:enable
Expand Down Expand Up @@ -488,7 +488,7 @@ public static function question_edit_contexts(\context $context): object {
*
* @return qtype_crossword_question
*/
public function make_crossword_question_normal_with_hyphen_and_space() {
public function make_crossword_question_normal_with_hyphen_space_and_apostrophes() {
question_bank::load_question_definition_classes('crossword');
$cw = new qtype_crossword_question();
test_question_maker::initialise_a_question($cw);
Expand Down Expand Up @@ -544,6 +544,19 @@ public function make_crossword_question_normal_with_hyphen_and_space() {
'feedbackformat' => FORMAT_HTML,
'answernumber' => 3,
],
(object) [
'id' => 4,
'questionid' => 1,
'clue' => "Famous physicist known for his theory of relativity?",
'clueformat' => FORMAT_HTML,
'answer' => "ALBERT EINSTEIN'S THEORY",
'startcolumn' => 5,
'startrow' => 2,
'orientation' => 1,
'feedback' => '',
'feedbackformat' => FORMAT_HTML,
'answernumber' => 4,
],
];

foreach ($answerslist as $answer) {
Expand All @@ -566,7 +579,7 @@ public function make_crossword_question_normal_with_hyphen_and_space() {
/**
* Makes a normal crossword question with answer contains hyphen and space.
*/
public function get_crossword_question_form_data_normal_with_hyphen_and_space() {
public function get_crossword_question_form_data_normal_with_hyphen_space_and_apostrophes() {
$fromform = new stdClass();
$fromform->name = 'Cross word question';
$fromform->questiontext = ['text' => 'Crossword question text', 'format' => FORMAT_HTML];
Expand All @@ -575,7 +588,13 @@ public function get_crossword_question_form_data_normal_with_hyphen_and_space()
$fromform->incorrectfeedback = ['text' => 'Incorrect feedback.', 'format' => FORMAT_HTML];
$fromform->penalty = 1;
$fromform->defaultmark = 1;
$fromform->answer = ['TIM BERNERS-LEE', 'GORDON BROWN', 'DAVID ATTENBOROUGH'];
$fromform->answer = [
'TIM BERNERS-LEE',
'GORDON BROWN',
'DAVID ATTENBOROUGH',
'ALBERT EINSTEIN\'S THEORY',
'STEPHEN HAWKING’S WORK',
];
$fromform->clue = [
[
'text' => 'Engineer, computer scientist and inventor of the World Wide Web?',
Expand All @@ -589,15 +608,23 @@ public function get_crossword_question_form_data_normal_with_hyphen_and_space()
'text' => 'British broadcaster and naturalist, famous for his voice-overs of nature programmes?',
'format' => FORMAT_HTML,
],
[
'text' => 'Famous physicist known for his theory of relativity?',
'format' => FORMAT_HTML,
],
[
'text' => 'Physicist known for black hole research and author of "A Brief History of Time"?',
'format' => FORMAT_HTML,
],
];
$fromform->orientation = [1, 1, 0];
$fromform->startrow = [0, 0, 1];
$fromform->startcolumn = [3, 11, 0];
$fromform->numrows = 13;
$fromform->orientation = [1, 1, 0, 1, 1];
$fromform->startrow = [0, 0, 1, 2, 2];
$fromform->startcolumn = [3, 11, 0, 5, 2];
$fromform->numrows = 19;
$fromform->numcolumns = 17;
$fromform->accentgradingtype = qtype_crossword::ACCENT_GRADING_STRICT;
$fromform->accentpenalty = 0;
$fromform->quotematching = 0;
$fromform->quotematching = 1;
return $fromform;
}

Expand Down
2 changes: 1 addition & 1 deletion tests/question_type_test.php
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public function test_convert_quote_to_straight_quote(): void {
'arrayelement' => ['hasrecursion' => '‘ single smart quote ’ and “ double smart quote ”'],
'test' => '&lsquo; HTML entities single quote &rsquo; and &ldquo; HTML entities double quote &rdquo;',
];
$result = $this->qtype->convert_quote_to_straight_quote($data);
$result = util::convert_quote_to_straight_quote($data);
$this->assertEquals($result['arrayelement']['hasrecursion'], "' single smart quote ' and " . '" double smart quote "');
$this->assertEquals($result['test'], "' HTML entities single quote ' and " . '" HTML entities double quote "');
}
Expand Down

0 comments on commit 5bb96ee

Please sign in to comment.