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