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