MDL-20636 Fix a bunch of minor errors in the truefalse preview.
[moodle.git] / question / type / shortanswer / questiontype.php
CommitLineData
aeb15530 1<?php
516cf3eb 2
fe6ce234
DC
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
516cf3eb 18///////////////////
19/// SHORTANSWER ///
20///////////////////
21
22/// QUESTION TYPE CLASS //////////////////
23
24///
25/// This class contains some special features in order to make the
26/// question type embeddable within a multianswer (cloze) question
27///
41a89a07 28/**
53a4d39f 29 * @package questionbank
30 * @subpackage questiontypes
31 */
22f2f418 32require_once("$CFG->dirroot/question/type/questiontype.php");
516cf3eb 33
af3830ee 34class question_shortanswer_qtype extends default_questiontype {
516cf3eb 35
36 function name() {
37 return 'shortanswer';
38 }
39
869309b8 40 function has_wildcards_in_responses($question, $subqid) {
41 return true;
42 }
43
d001dac7 44 function extra_question_fields() {
fe6ce234 45 return array('question_shortanswer', 'answers', 'usecase');
d001dac7 46 }
516cf3eb 47
d001dac7 48 function questionid_column_name() {
49 return 'question';
516cf3eb 50 }
51
5d548d3e
TH
52 function move_files($questionid, $oldcontextid, $newcontextid) {
53 parent::move_files($questionid, $oldcontextid, $newcontextid);
54 $this->move_files_in_answers($questionid, $oldcontextid, $newcontextid);
fe6ce234
DC
55 }
56
9203b705
TH
57 protected function delete_files($questionid, $contextid) {
58 parent::delete_files($questionid, $contextid);
59 $this->delete_files_in_answers($questionid, $contextid);
60 }
61
516cf3eb 62 function save_question_options($question) {
f34488b2 63 global $DB;
5a14d563 64 $result = new stdClass;
90a36f8c 65
fe6ce234
DC
66 $context = $question->context;
67
69988ed4
TH
68 $oldanswers = $DB->get_records('question_answers',
69 array('question' => $question->id), 'id ASC');
516cf3eb 70
69988ed4 71 // Insert all the new answers
516cf3eb 72 $answers = array();
73 $maxfraction = -1;
69988ed4
TH
74 foreach ($question->answer as $key => $answerdata) {
75 // Check for, and ignore, completely blank answer from the form.
76 if (trim($answerdata) == '' && $question->fraction[$key] == 0 &&
fe6ce234 77 html_is_blank($question->feedback[$key]['text'])) {
94a6d656 78 continue;
79 }
80
69988ed4
TH
81 // Update an existing answer if possible.
82 $answer = array_shift($oldanswers);
83 if (!$answer) {
84 $answer = new stdClass();
85 $answer->question = $question->id;
86 $answer->answer = '';
87 $answer->feedback = '';
88 $answer->id = $DB->insert_record('question_answers', $answer);
cde2709a 89 }
fe6ce234 90
69988ed4
TH
91 $answer->answer = trim($answerdata);
92 $answer->fraction = $question->fraction[$key];
93 $answer->feedback = $this->import_or_save_files($question->feedback[$key],
94 $context, 'question', 'answerfeedback', $answer->id);
95 $answer->feedbackformat = $question->feedback[$key]['format'];
96 $DB->update_record('question_answers', $answer);
fe6ce234 97
94a6d656 98 $answers[] = $answer->id;
99 if ($question->fraction[$key] > $maxfraction) {
100 $maxfraction = $question->fraction[$key];
101 }
516cf3eb 102 }
103
69988ed4
TH
104 // Delete any left over old answer records.
105 $fs = get_file_storage();
106 foreach($oldanswers as $oldanswer) {
107 $fs->delete_area_files($context->id, 'question', 'answerfeedback', $oldanswer->id);
108 $DB->delete_records('question_answers', array('id' => $oldanswer->id));
109 }
110
d001dac7 111 $question->answers = implode(',', $answers);
112 $parentresult = parent::save_question_options($question);
69988ed4
TH
113 if ($parentresult !== null) {
114 // Parent function returns null if all is OK
d001dac7 115 return $parentresult;
516cf3eb 116 }
117
69988ed4 118 // Perform sanity checks on fractional grades
516cf3eb 119 if ($maxfraction != 1) {
69988ed4 120 $result->noticeyesno = get_string('fractionsnomax', 'quiz', $maxfraction * 100);
516cf3eb 121 return $result;
516cf3eb 122 }
69988ed4
TH
123
124 return true;
516cf3eb 125 }
126
516cf3eb 127 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
fe6ce234
DC
128 global $CFG;
129 $context = $this->get_context_by_category_id($question->category);
dfa47f96 130 /// This implementation is also used by question type 'numerical'
516cf3eb 131 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
5a14d563 132 $formatoptions = new stdClass;
7347c60b 133 $formatoptions->noclean = true;
134 $formatoptions->para = false;
516cf3eb 135 $nameprefix = $question->name_prefix;
136
137 /// Print question text and media
138
53a4d39f 139 $questiontext = format_text($question->questiontext,
140 $question->questiontextformat,
141 $formatoptions, $cmoptions->course);
516cf3eb 142
143 /// Print input controls
144
34018d81 145 if (isset($state->responses['']) && $state->responses['']!='') {
cdfaa838 146 $value = ' value="'.s($state->responses['']).'" ';
516cf3eb 147 } else {
148 $value = ' value="" ';
149 }
150 $inputname = ' name="'.$nameprefix.'" ';
151
152 $feedback = '';
a8404567 153 $class = '';
154 $feedbackimg = '';
e0c25647 155
516cf3eb 156 if ($options->feedback) {
a8404567 157 $class = question_get_feedback_class(0);
158 $feedbackimg = question_get_feedback_image(0);
aeb15530 159 //this is OK for the first answer with a good response
1a1293ed 160 foreach($question->options->answers as $answer) {
2b087056 161
134f2cc0 162 if ($this->test_response($question, $state, $answer)) {
163 // Answer was correct or partially correct.
2b087056 164 $class = question_get_feedback_class($answer->fraction);
165 $feedbackimg = question_get_feedback_image($answer->fraction);
134f2cc0 166 if ($answer->feedback) {
fe6ce234 167 $answer->feedback = quiz_rewrite_question_urls($answer->feedback, 'pluginfile.php', $context->id, 'question', 'answerfeedback', array($state->attempt, $state->question), $answer->id);
a9efae50 168 $feedback = format_text($answer->feedback, $answer->feedbackformat, $formatoptions, $cmoptions->course);
1a1293ed 169 }
516cf3eb 170 break;
134f2cc0 171 }
516cf3eb 172 }
173 }
90a36f8c 174
175 /// Removed correct answer, to be displayed later MDL-7496
9fb32418
TH
176 include($this->get_display_html_path());
177 }
fe6ce234 178
9fb32418
TH
179 function get_display_html_path() {
180 global $CFG;
181 return $CFG->dirroot.'/question/type/shortanswer/display.html';
516cf3eb 182 }
183
516cf3eb 184 function check_response(&$question, &$state) {
be8563b6 185 foreach($question->options->answers as $aid => $answer) {
186 if ($this->test_response($question, $state, $answer)) {
516cf3eb 187 return $aid;
188 }
189 }
190 return false;
191 }
192
5a14d563 193 function compare_responses($question, $state, $teststate) {
194 if (isset($state->responses['']) && isset($teststate->responses[''])) {
be8563b6 195 return $state->responses[''] === $teststate->responses[''];
516cf3eb 196 }
5a14d563 197 return false;
516cf3eb 198 }
199
5a14d563 200 function test_response(&$question, $state, $answer) {
90a36f8c 201 // Trim the response before it is saved in the database. See MDL-10709
202 $state->responses[''] = trim($state->responses['']);
294ce987 203 return $this->compare_string_with_wildcard($state->responses[''],
5a14d563 204 $answer->answer, !$question->options->usecase);
516cf3eb 205 }
1a1293ed 206
5a14d563 207 function compare_string_with_wildcard($string, $pattern, $ignorecase) {
208 // Break the string on non-escaped asterisks.
209 $bits = preg_split('/(?<!\\\\)\*/', $pattern);
210 // Escape regexp special characters in the bits.
3270033a 211 $excapedbits = array();
212 foreach ($bits as $bit) {
213 $excapedbits[] = preg_quote(str_replace('\*', '*', $bit));
214 }
5a14d563 215 // Put it back together to make the regexp.
3270033a 216 $regexp = '|^' . implode('.*', $excapedbits) . '$|u';
90a36f8c 217
5a14d563 218 // Make the match insensitive if requested to.
219 if ($ignorecase) {
220 $regexp .= 'i';
221 }
90a36f8c 222
5a14d563 223 return preg_match($regexp, trim($string));
1a1293ed 224 }
225
9b75adc2 226 /*
227 * Override the parent class method, to remove escaping from asterisks.
228 */
229 function get_correct_responses(&$question, &$state) {
230 $response = parent::get_correct_responses($question, $state);
231 if (is_array($response)) {
294ce987 232 $response[''] = str_replace('\*', '*', $response['']);
9b75adc2 233 }
234 return $response;
235 }
6f51ed72 236 /**
237 * @param object $question
455c3efa 238 * @return mixed either a integer score out of 1 that the average random
239 * guess by a student might give or an empty string which means will not
240 * calculate.
6f51ed72 241 */
242 function get_random_guess_score($question) {
243 $answers = &$question->options->answers;
244 foreach($answers as $aid => $answer) {
245 if ('*' == trim($answer->answer)){
246 return $answer->fraction;
247 }
248 }
249 return 0;
250 }
516cf3eb 251
7897d6fc 252 /**
93a501c1 253 * Prints the score obtained and maximum score available plus any penalty
254 * information
255 *
256 * This function prints a summary of the scoring in the most recently
257 * graded state (the question may not have been submitted for marking at
258 * the current state). The default implementation should be suitable for most
259 * question types.
260 * @param object $question The question for which the grading details are
261 * to be rendered. Question type specific information
262 * is included. The maximum possible grade is in
263 * ->maxgrade.
264 * @param object $state The state. In particular the grading information
265 * is in ->grade, ->raw_grade and ->penalty.
266 * @param object $cmoptions
267 * @param object $options An object describing the rendering options.
268 */
269 function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
270 /* The default implementation prints the number of marks if no attempt
271 has been made. Otherwise it displays the grade obtained out of the
272 maximum grade available and a warning if a penalty was applied for the
273 attempt and displays the overall grade obtained counting all previous
274 responses (and penalties) */
90a36f8c 275
20bf2c1a 276 global $QTYPES ;
93a501c1 277 // MDL-7496 show correct answer after "Incorrect"
278 $correctanswer = '';
20bf2c1a 279 if ($correctanswers = $QTYPES[$question->qtype]->get_correct_responses($question, $state)) {
93a501c1 280 if ($options->readonly && $options->correct_responses) {
281 $delimiter = '';
282 if ($correctanswers) {
283 foreach ($correctanswers as $ca) {
284 $correctanswer .= $delimiter.$ca;
285 $delimiter = ', ';
286 }
287 }
90a36f8c 288 }
93a501c1 289 }
90a36f8c 290
93a501c1 291 if (QUESTION_EVENTDUPLICATE == $state->event) {
292 echo ' ';
293 print_string('duplicateresponse', 'quiz');
294 }
26da840f 295 if ($question->maxgrade > 0 && $options->scores) {
93a501c1 296 if (question_state_is_graded($state->last_graded)) {
297 // Display the grading details from the last graded state
298 $grade = new stdClass;
f88fb62c 299 $grade->cur = question_format_grade($cmoptions, $state->last_graded->grade);
f9a2cf86 300 $grade->max = question_format_grade($cmoptions, $question->maxgrade);
f88fb62c 301 $grade->raw = question_format_grade($cmoptions, $state->last_graded->raw_grade);
93a501c1 302 // let student know wether the answer was correct
aeb15530 303 $class = question_get_feedback_class($state->last_graded->raw_grade /
b10c38a3 304 $question->maxgrade);
305 echo '<div class="correctness ' . $class . '">' . get_string($class, 'quiz');
fbe60eb3 306 if ($correctanswer != '' && ($class == 'partiallycorrect' || $class == 'incorrect')) {
b10c38a3 307 echo ('<div class="correctness">');
cdfaa838 308 print_string('correctansweris', 'quiz', s($correctanswer));
b10c38a3 309 echo ('</div>');
93a501c1 310 }
311 echo '</div>';
312
313 echo '<div class="gradingdetails">';
314 // print grade for this submission
a65fa92a 315 print_string('gradingdetails', 'quiz', $grade) ;
7897d6fc 316 // A unit penalty for numerical was applied so display it
fe6ce234 317 // a temporary solution for unit rendering in numerical
7897d6fc
PP
318 // waiting for the new question engine code for a permanent one
319 if(isset($state->options->raw_unitpenalty) && $state->options->raw_unitpenalty > 0.0 ){
320 echo ' ';
321 print_string('unitappliedpenalty','qtype_numerical',question_format_grade($cmoptions, $state->options->raw_unitpenalty ));
fe6ce234 322 }
93a501c1 323 if ($cmoptions->penaltyscheme) {
324 // print details of grade adjustment due to penalties
325 if ($state->last_graded->raw_grade > $state->last_graded->grade){
326 echo ' ';
327 print_string('gradingdetailsadjustment', 'quiz', $grade);
328 }
329 // print info about new penalty
330 // penalty is relevant only if the answer is not correct and further attempts are possible
5995f17f 331 if (($state->last_graded->raw_grade < $question->maxgrade) and (QUESTION_EVENTCLOSEANDGRADE != $state->event)) {
93a501c1 332 if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
3f42a35a 333 echo ' ' ;
71990c7c 334 print_string('gradingdetailspenalty', 'quiz', question_format_grade($cmoptions, $state->last_graded->penalty));
93a501c1 335 } else {
336 /* No penalty was applied even though the answer was
337 not correct (eg. a syntax error) so tell the student
338 that they were not penalised for the attempt */
339 echo ' ';
340 print_string('gradingdetailszeropenalty', 'quiz');
341 }
342 }
343 }
344 echo '</div>';
345 }
346 }
347 }
b9bd6da4 348
349 /**
350 * Runs all the code required to set up and save an essay question for testing purposes.
351 * Alternate DB table prefix may be used to facilitate data deletion.
352 */
353 function generate_test($name, $courseid = null) {
354 global $DB;
355 list($form, $question) = parent::generate_test($name, $courseid);
356 $question->category = $form->category;
357
358 $form->questiontext = "What is the purpose of life, the universe, and everything";
359 $form->generalfeedback = "Congratulations, you may have solved my biggest problem!";
360 $form->penalty = 0.1;
361 $form->usecase = false;
362 $form->defaultgrade = 1;
363 $form->noanswers = 3;
364 $form->answer = array('42', 'who cares?', 'Be happy');
365 $form->fraction = array(1, 0.6, 0.8);
366 $form->feedback = array('True, but what does that mean?', 'Well you do, dont you?', 'Yes, but thats not funny...');
367 $form->correctfeedback = 'Excellent!';
368 $form->incorrectfeedback = 'Nope!';
369 $form->partiallycorrectfeedback = 'Not bad';
370
371 if ($courseid) {
372 $course = $DB->get_record('course', array('id' => $courseid));
373 }
374
94dbfb3a 375 return $this->save_question($question, $form);
b9bd6da4 376 }
fe6ce234
DC
377
378 function check_file_access($question, $state, $options, $contextid, $component,
379 $filearea, $args) {
380 if ($component == 'question' && $filearea == 'answerfeedback') {
381 $answers = &$question->options->answers;
382 if (isset($state->responses[''])) {
383 $response = $state->responses[''];
384 } else {
385 $response = '';
386 }
387 $answerid = reset($args); // itemid is answer id.
388 if (empty($options->feedback)) {
389 return false;
390 }
391 foreach($answers as $answer) {
392 if ($this->test_response($question, $state, $answer)) {
393 return true;
394 }
395 }
396 return false;
397
398 } else {
399 return parent::check_file_access($question, $state, $options, $contextid, $component,
400 $filearea, $args);
401 }
402 }
403
516cf3eb 404}
405//// END OF CLASS ////
406
407//////////////////////////////////////////////////////////////////////////
408//// INITIATION - Without this line the question type is not in use... ///
409//////////////////////////////////////////////////////////////////////////
a2156789 410question_register_questiontype(new question_shortanswer_qtype());
aeb15530 411