PageRenderTime 51ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/question/type/shortanswer/question.php

http://github.com/moodle/moodle
PHP | 187 lines | 109 code | 32 blank | 46 comment | 27 complexity | f40588f584ec208ee47ac3d39903979e MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Short answer question definition class.
  18. *
  19. * @package qtype
  20. * @subpackage shortanswer
  21. * @copyright 2009 The Open University
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. require_once($CFG->dirroot . '/question/type/questionbase.php');
  26. /**
  27. * Represents a short answer question.
  28. *
  29. * @copyright 2009 The Open University
  30. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  31. */
  32. class qtype_shortanswer_question extends question_graded_by_strategy
  33. implements question_response_answer_comparer {
  34. /** @var boolean whether answers should be graded case-sensitively. */
  35. public $usecase;
  36. /** @var array of question_answer. */
  37. public $answers = array();
  38. public function __construct() {
  39. parent::__construct(new question_first_matching_answer_grading_strategy($this));
  40. }
  41. public function get_expected_data() {
  42. return array('answer' => PARAM_RAW_TRIMMED);
  43. }
  44. public function summarise_response(array $response) {
  45. if (isset($response['answer'])) {
  46. return $response['answer'];
  47. } else {
  48. return null;
  49. }
  50. }
  51. public function un_summarise_response(string $summary) {
  52. if (!empty($summary)) {
  53. return ['answer' => $summary];
  54. } else {
  55. return [];
  56. }
  57. }
  58. public function is_complete_response(array $response) {
  59. return array_key_exists('answer', $response) &&
  60. ($response['answer'] || $response['answer'] === '0');
  61. }
  62. public function get_validation_error(array $response) {
  63. if ($this->is_gradable_response($response)) {
  64. return '';
  65. }
  66. return get_string('pleaseenterananswer', 'qtype_shortanswer');
  67. }
  68. public function is_same_response(array $prevresponse, array $newresponse) {
  69. return question_utils::arrays_same_at_key_missing_is_blank(
  70. $prevresponse, $newresponse, 'answer');
  71. }
  72. public function get_answers() {
  73. return $this->answers;
  74. }
  75. public function compare_response_with_answer(array $response, question_answer $answer) {
  76. if (!array_key_exists('answer', $response) || is_null($response['answer'])) {
  77. return false;
  78. }
  79. return self::compare_string_with_wildcard(
  80. $response['answer'], $answer->answer, !$this->usecase);
  81. }
  82. public static function compare_string_with_wildcard($string, $pattern, $ignorecase) {
  83. // Normalise any non-canonical UTF-8 characters before we start.
  84. $pattern = self::safe_normalize($pattern);
  85. $string = self::safe_normalize($string);
  86. // Break the string on non-escaped runs of asterisks.
  87. // ** is equivalent to *, but people were doing that, and with many *s it breaks preg.
  88. $bits = preg_split('/(?<!\\\\)\*+/', $pattern);
  89. // Escape regexp special characters in the bits.
  90. $escapedbits = array();
  91. foreach ($bits as $bit) {
  92. $escapedbits[] = preg_quote(str_replace('\*', '*', $bit), '|');
  93. }
  94. // Put it back together to make the regexp.
  95. $regexp = '|^' . implode('.*', $escapedbits) . '$|u';
  96. // Make the match insensitive if requested to.
  97. if ($ignorecase) {
  98. $regexp .= 'i';
  99. }
  100. return preg_match($regexp, trim($string));
  101. }
  102. /**
  103. * Normalise a UTf-8 string to FORM_C, avoiding the pitfalls in PHP's
  104. * normalizer_normalize function.
  105. * @param string $string the input string.
  106. * @return string the normalised string.
  107. */
  108. protected static function safe_normalize($string) {
  109. if ($string === '') {
  110. return '';
  111. }
  112. if (!function_exists('normalizer_normalize')) {
  113. return $string;
  114. }
  115. $normalised = normalizer_normalize($string, Normalizer::FORM_C);
  116. if (is_null($normalised)) {
  117. // An error occurred in normalizer_normalize, but we have no idea what.
  118. debugging('Failed to normalise string: ' . $string, DEBUG_DEVELOPER);
  119. return $string; // Return the original string, since it is the best we have.
  120. }
  121. return $normalised;
  122. }
  123. public function get_correct_response() {
  124. $response = parent::get_correct_response();
  125. if ($response) {
  126. $response['answer'] = $this->clean_response($response['answer']);
  127. }
  128. return $response;
  129. }
  130. public function clean_response($answer) {
  131. // Break the string on non-escaped asterisks.
  132. $bits = preg_split('/(?<!\\\\)\*/', $answer);
  133. // Unescape *s in the bits.
  134. $cleanbits = array();
  135. foreach ($bits as $bit) {
  136. $cleanbits[] = str_replace('\*', '*', $bit);
  137. }
  138. // Put it back together with spaces to look nice.
  139. return trim(implode(' ', $cleanbits));
  140. }
  141. public function check_file_access($qa, $options, $component, $filearea,
  142. $args, $forcedownload) {
  143. if ($component == 'question' && $filearea == 'answerfeedback') {
  144. $currentanswer = $qa->get_last_qt_var('answer');
  145. $answer = $this->get_matching_answer(array('answer' => $currentanswer));
  146. $answerid = reset($args); // Itemid is answer id.
  147. return $options->feedback && $answer && $answerid == $answer->id;
  148. } else if ($component == 'question' && $filearea == 'hint') {
  149. return $this->check_hint_file_access($qa, $options, $args);
  150. } else {
  151. return parent::check_file_access($qa, $options, $component, $filearea,
  152. $args, $forcedownload);
  153. }
  154. }
  155. }