See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 39 and 401]
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 /** 18 * Essay question definition class. 19 * 20 * @package qtype 21 * @subpackage essay 22 * @copyright 2009 The Open University 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 27 defined('MOODLE_INTERNAL') || die(); 28 29 require_once($CFG->dirroot . '/question/type/questionbase.php'); 30 31 /** 32 * Represents an essay question. 33 * 34 * @copyright 2009 The Open University 35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 36 */ 37 class qtype_essay_question extends question_with_responses { 38 39 public $responseformat; 40 41 /** @var int Indicates whether an inline response is required ('0') or optional ('1') */ 42 public $responserequired; 43 44 public $responsefieldlines; 45 46 /** @var int indicates whether the minimum number of words required */ 47 public $minwordlimit; 48 49 /** @var int indicates whether the maximum number of words required */ 50 public $maxwordlimit; 51 52 public $attachments; 53 54 /** @var int maximum file size in bytes */ 55 public $maxbytes; 56 57 /** @var int The number of attachments required for a response to be complete. */ 58 public $attachmentsrequired; 59 60 public $graderinfo; 61 public $graderinfoformat; 62 public $responsetemplate; 63 public $responsetemplateformat; 64 65 /** @var array The string array of file types accepted upon file submission. */ 66 public $filetypeslist; 67 68 public function make_behaviour(question_attempt $qa, $preferredbehaviour) { 69 return question_engine::make_behaviour('manualgraded', $qa, $preferredbehaviour); 70 } 71 72 /** 73 * @param moodle_page the page we are outputting to. 74 * @return qtype_essay_format_renderer_base the response-format-specific renderer. 75 */ 76 public function get_format_renderer(moodle_page $page) { 77 return $page->get_renderer('qtype_essay', 'format_' . $this->responseformat); 78 } 79 80 public function get_expected_data() { 81 if ($this->responseformat == 'editorfilepicker') { 82 $expecteddata = array('answer' => question_attempt::PARAM_RAW_FILES); 83 } else { 84 $expecteddata = array('answer' => PARAM_RAW); 85 } 86 $expecteddata['answerformat'] = PARAM_ALPHANUMEXT; 87 if ($this->attachments != 0) { 88 $expecteddata['attachments'] = question_attempt::PARAM_FILES; 89 } 90 return $expecteddata; 91 } 92 93 public function summarise_response(array $response) { 94 $output = null; 95 96 if (isset($response['answer'])) { 97 $output .= question_utils::to_plain_text($response['answer'], 98 $response['answerformat'], array('para' => false)); 99 } 100 101 if (isset($response['attachments']) && $response['attachments']) { 102 $attachedfiles = []; 103 foreach ($response['attachments']->get_files() as $file) { 104 $attachedfiles[] = $file->get_filename() . ' (' . display_size($file->get_filesize()) . ')'; 105 } 106 if ($attachedfiles) { 107 $output .= get_string('attachedfiles', 'qtype_essay', implode(', ', $attachedfiles)); 108 } 109 } 110 return $output; 111 } 112 113 public function un_summarise_response(string $summary) { 114 if (empty($summary)) { 115 return []; 116 } 117 118 if (str_contains($this->responseformat, 'editor')) { 119 return ['answer' => text_to_html($summary), 'answerformat' => FORMAT_HTML]; 120 } else { 121 return ['answer' => $summary, 'answerformat' => FORMAT_PLAIN]; 122 } 123 } 124 125 public function get_correct_response() { 126 return null; 127 } 128 129 public function is_complete_response(array $response) { 130 // Determine if the given response has online text and attachments. 131 $hasinlinetext = array_key_exists('answer', $response) && ($response['answer'] !== ''); 132 133 // If there is a response and min/max word limit is set in the form then validate the number of words in response. 134 if ($hasinlinetext) { 135 if ($this->check_input_word_count($response['answer'])) { 136 return false; 137 } 138 } 139 $hasattachments = array_key_exists('attachments', $response) 140 && $response['attachments'] instanceof question_response_files; 141 142 // Determine the number of attachments present. 143 if ($hasattachments) { 144 // Check the filetypes. 145 $filetypesutil = new \core_form\filetypes_util(); 146 $allowlist = $filetypesutil->normalize_file_types($this->filetypeslist); 147 $wrongfiles = array(); 148 foreach ($response['attachments']->get_files() as $file) { 149 if (!$filetypesutil->is_allowed_file_type($file->get_filename(), $allowlist)) { 150 $wrongfiles[] = $file->get_filename(); 151 } 152 } 153 if ($wrongfiles) { // At least one filetype is wrong. 154 return false; 155 } 156 $attachcount = count($response['attachments']->get_files()); 157 } else { 158 $attachcount = 0; 159 } 160 161 // Determine if we have /some/ content to be graded. 162 $hascontent = $hasinlinetext || ($attachcount > 0); 163 164 // Determine if we meet the optional requirements. 165 $meetsinlinereq = $hasinlinetext || (!$this->responserequired) || ($this->responseformat == 'noinline'); 166 $meetsattachmentreq = ($attachcount >= $this->attachmentsrequired); 167 168 // The response is complete iff all of our requirements are met. 169 return $hascontent && $meetsinlinereq && $meetsattachmentreq; 170 } 171 172 /** 173 * Return null if is_complete_response() returns true 174 * otherwise, return the minmax-limit error message 175 * 176 * @param array $response 177 * @return string 178 */ 179 public function get_validation_error(array $response) { 180 if ($this->is_complete_response($response)) { 181 return ''; 182 } 183 return $this->check_input_word_count($response['answer']); 184 } 185 186 public function is_gradable_response(array $response) { 187 // Determine if the given response has online text and attachments. 188 if (array_key_exists('answer', $response) && ($response['answer'] !== '')) { 189 return true; 190 } else if (array_key_exists('attachments', $response) 191 && $response['attachments'] instanceof question_response_files) { 192 return true; 193 } else { 194 return false; 195 } 196 } 197 198 public function is_same_response(array $prevresponse, array $newresponse) { 199 if (array_key_exists('answer', $prevresponse) && $prevresponse['answer'] !== $this->responsetemplate) { 200 $value1 = (string) $prevresponse['answer']; 201 } else { 202 $value1 = ''; 203 } 204 if (array_key_exists('answer', $newresponse) && $newresponse['answer'] !== $this->responsetemplate) { 205 $value2 = (string) $newresponse['answer']; 206 } else { 207 $value2 = ''; 208 } 209 return $value1 === $value2 && ($this->attachments == 0 || 210 question_utils::arrays_same_at_key_missing_is_blank( 211 $prevresponse, $newresponse, 'attachments')); 212 } 213 214 public function check_file_access($qa, $options, $component, $filearea, $args, $forcedownload) { 215 if ($component == 'question' && $filearea == 'response_attachments') { 216 // Response attachments visible if the question has them. 217 return $this->attachments != 0; 218 219 } else if ($component == 'question' && $filearea == 'response_answer') { 220 // Response attachments visible if the question has them. 221 return $this->responseformat === 'editorfilepicker'; 222 223 } else if ($component == 'qtype_essay' && $filearea == 'graderinfo') { 224 return $options->manualcomment && $args[0] == $this->id; 225 226 } else { 227 return parent::check_file_access($qa, $options, $component, 228 $filearea, $args, $forcedownload); 229 } 230 } 231 232 /** 233 * Return the question settings that define this question as structured data. 234 * 235 * @param question_attempt $qa the current attempt for which we are exporting the settings. 236 * @param question_display_options $options the question display options which say which aspects of the question 237 * should be visible. 238 * @return mixed structure representing the question settings. In web services, this will be JSON-encoded. 239 */ 240 public function get_question_definition_for_external_rendering(question_attempt $qa, question_display_options $options) { 241 // This is a partial implementation, returning only the most relevant question settings for now, 242 // ideally, we should return as much as settings as possible (depending on the state and display options). 243 244 $settings = [ 245 'responseformat' => $this->responseformat, 246 'responserequired' => $this->responserequired, 247 'responsefieldlines' => $this->responsefieldlines, 248 'attachments' => $this->attachments, 249 'attachmentsrequired' => $this->attachmentsrequired, 250 'maxbytes' => $this->maxbytes, 251 'filetypeslist' => $this->filetypeslist, 252 'responsetemplate' => $this->responsetemplate, 253 'responsetemplateformat' => $this->responsetemplateformat, 254 'minwordlimit' => $this->minwordlimit, 255 'maxwordlimit' => $this->maxwordlimit, 256 ]; 257 258 return $settings; 259 } 260 261 /** 262 * Check the input word count and return a message to user 263 * when the number of words are outside the boundary settings. 264 * 265 * @param string $responsestring 266 * @return string|null 267 .*/ 268 private function check_input_word_count($responsestring) { 269 if (!$this->responserequired) { 270 return null; 271 } 272 if (!$this->minwordlimit && !$this->maxwordlimit) { 273 // This question does not care about the word count. 274 return null; 275 } 276 277 // Count the number of words in the response string. 278 $count = count_words($responsestring); 279 if ($this->maxwordlimit && $count > $this->maxwordlimit) { 280 return get_string('maxwordlimitboundary', 'qtype_essay', 281 ['limit' => $this->maxwordlimit, 'count' => $count]); 282 } else if ($count < $this->minwordlimit) { 283 return get_string('minwordlimitboundary', 'qtype_essay', 284 ['limit' => $this->minwordlimit, 'count' => $count]); 285 } else { 286 return null; 287 } 288 } 289 290 /** 291 * If this question uses word counts, then return a display of the current 292 * count, and whether it is within limit, for when the question is being reviewed. 293 * 294 * @param array $response responses, as returned by 295 * {@see question_attempt_step::get_qt_data()}. 296 * @return string If relevant to this question, a display of the word count. 297 */ 298 public function get_word_count_message_for_review(array $response): string { 299 if (!$this->minwordlimit && !$this->maxwordlimit) { 300 // This question does not care about the word count. 301 return ''; 302 } 303 304 if (!array_key_exists('answer', $response) || ($response['answer'] === '')) { 305 // No response. 306 return ''; 307 } 308 309 $count = count_words($response['answer']); 310 if ($this->maxwordlimit && $count > $this->maxwordlimit) { 311 return get_string('wordcounttoomuch', 'qtype_essay', 312 ['limit' => $this->maxwordlimit, 'count' => $count]); 313 } else if ($count < $this->minwordlimit) { 314 return get_string('wordcounttoofew', 'qtype_essay', 315 ['limit' => $this->minwordlimit, 'count' => $count]); 316 } else { 317 return get_string('wordcount', 'qtype_essay', $count); 318 } 319 } 320 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body