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