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