merged from 1.9 :: CSS color "lightgrey" does not exist. I replaced it with the ...
[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
25 function get_question_options(&$question) {
26 // Get additional information from database
27 // and attach it to the question object
32a189d6 28 if (!$question->options = get_record('question_shortanswer', 'question', $question->id)) {
516cf3eb 29 notify('Error: Missing question options!');
30 return false;
31 }
32
dc1f00de 33 if (!$question->options->answers = get_records('question_answers', 'question',
9e8dba79 34 $question->id, 'id ASC')) {
35 notify('Error: Missing question answers!');
36 return false;
516cf3eb 37 }
38 return true;
39 }
40
41 function save_question_options($question) {
5a14d563 42 $result = new stdClass;
90a36f8c 43
a4d79b0e 44 if (!$oldanswers = get_records('question_answers', 'question', $question->id, 'id ASC')) {
516cf3eb 45 $oldanswers = array();
46 }
47
48 $answers = array();
49 $maxfraction = -1;
50
51 // Insert all the new answers
52 foreach ($question->answer as $key => $dataanswer) {
53 if ($dataanswer != "") {
54 if ($oldanswer = array_shift($oldanswers)) { // Existing answer, so reuse it
55 $answer = $oldanswer;
56 $answer->answer = trim($dataanswer);
57 $answer->fraction = $question->fraction[$key];
58 $answer->feedback = $question->feedback[$key];
dc1f00de 59 if (!update_record("question_answers", $answer)) {
516cf3eb 60 $result->error = "Could not update quiz answer! (id=$answer->id)";
61 return $result;
62 }
63 } else { // This is a completely new answer
5a14d563 64 $answer = new stdClass;
516cf3eb 65 $answer->answer = trim($dataanswer);
66 $answer->question = $question->id;
67 $answer->fraction = $question->fraction[$key];
68 $answer->feedback = $question->feedback[$key];
dc1f00de 69 if (!$answer->id = insert_record("question_answers", $answer)) {
516cf3eb 70 $result->error = "Could not insert quiz answer!";
71 return $result;
72 }
73 }
74 $answers[] = $answer->id;
75 if ($question->fraction[$key] > $maxfraction) {
76 $maxfraction = $question->fraction[$key];
77 }
78 }
79 }
80
32a189d6 81 if ($options = get_record("question_shortanswer", "question", $question->id)) {
516cf3eb 82 $options->answers = implode(",",$answers);
83 $options->usecase = $question->usecase;
32a189d6 84 if (!update_record("question_shortanswer", $options)) {
516cf3eb 85 $result->error = "Could not update quiz shortanswer options! (id=$options->id)";
86 return $result;
87 }
88 } else {
89 unset($options);
90 $options->question = $question->id;
91 $options->answers = implode(",",$answers);
92 $options->usecase = $question->usecase;
32a189d6 93 if (!insert_record("question_shortanswer", $options)) {
516cf3eb 94 $result->error = "Could not insert quiz shortanswer options!";
95 return $result;
96 }
97 }
98
99 // delete old answer records
100 if (!empty($oldanswers)) {
101 foreach($oldanswers as $oa) {
dc1f00de 102 delete_records('question_answers', 'id', $oa->id);
516cf3eb 103 }
104 }
105
106 /// Perform sanity checks on fractional grades
107 if ($maxfraction != 1) {
108 $maxfraction = $maxfraction * 100;
109 $result->noticeyesno = get_string("fractionsnomax", "quiz", $maxfraction);
110 return $result;
111 } else {
112 return true;
113 }
114 }
115
116 /**
117 * Deletes question from the question-type specific tables
118 *
119 * @return boolean Success/Failure
120 * @param object $question The question being deleted
121 */
90c3f310 122 function delete_question($questionid) {
123 delete_records("question_shortanswer", "question", $questionid);
516cf3eb 124 return true;
125 }
126
127 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
128 global $CFG;
dfa47f96 129 /// This implementation is also used by question type 'numerical'
516cf3eb 130 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
5a14d563 131 $formatoptions = new stdClass;
7347c60b 132 $formatoptions->noclean = true;
133 $formatoptions->para = false;
516cf3eb 134 $nameprefix = $question->name_prefix;
135
136 /// Print question text and media
137
53a4d39f 138 $questiontext = format_text($question->questiontext,
139 $question->questiontextformat,
140 $formatoptions, $cmoptions->course);
9fc3100f 141 $image = get_question_image($question);
516cf3eb 142
143 /// Print input controls
144
145 if (isset($state->responses[''])) {
9e8dba79 146 $value = ' value="'.s($state->responses[''], true).'" ';
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);
1a1293ed 159 foreach($question->options->answers as $answer) {
2b087056 160
134f2cc0 161 if ($this->test_response($question, $state, $answer)) {
162 // Answer was correct or partially correct.
2b087056 163 $class = question_get_feedback_class($answer->fraction);
164 $feedbackimg = question_get_feedback_image($answer->fraction);
134f2cc0 165 if ($answer->feedback) {
1a1293ed 166 $feedback = format_text($answer->feedback, true, $formatoptions, $cmoptions->course);
167 }
516cf3eb 168 break;
134f2cc0 169 }
516cf3eb 170 }
171 }
90a36f8c 172
173 /// Removed correct answer, to be displayed later MDL-7496
aaae75b0 174 include("$CFG->dirroot/question/type/shortanswer/display.html");
516cf3eb 175 }
176
177 // ULPGC ecastro
178 function check_response(&$question, &$state) {
179 $answers = &$question->options->answers;
180 $testedstate = clone($state);
181 $teststate = clone($state);
182 foreach($answers as $aid => $answer) {
183 $teststate->responses[''] = trim($answer->answer);
184 if($this->compare_responses($question, $testedstate, $teststate)) {
185 return $aid;
186 }
187 }
188 return false;
189 }
190
5a14d563 191 function compare_responses($question, $state, $teststate) {
192 if (isset($state->responses['']) && isset($teststate->responses[''])) {
193 if ($question->options->usecase) {
c82f76d0 194 return strcmp($state->responses[''], $teststate->responses['']) == 0;
5a14d563 195 } else {
485349dd 196 $textlib = textlib_get_instance();
197 return strcmp($textlib->strtolower($state->responses['']),
198 $textlib->strtolower($teststate->responses[''])) == 0;
516cf3eb 199 }
200 }
5a14d563 201 return false;
516cf3eb 202 }
203
5a14d563 204 function test_response(&$question, $state, $answer) {
90a36f8c 205 // Trim the response before it is saved in the database. See MDL-10709
206 $state->responses[''] = trim($state->responses['']);
9e8dba79 207 return $this->compare_string_with_wildcard(stripslashes_safe($state->responses['']),
5a14d563 208 $answer->answer, !$question->options->usecase);
516cf3eb 209 }
1a1293ed 210
5a14d563 211 function compare_string_with_wildcard($string, $pattern, $ignorecase) {
212 // Break the string on non-escaped asterisks.
213 $bits = preg_split('/(?<!\\\\)\*/', $pattern);
214 // Escape regexp special characters in the bits.
3270033a 215 $excapedbits = array();
216 foreach ($bits as $bit) {
217 $excapedbits[] = preg_quote(str_replace('\*', '*', $bit));
218 }
5a14d563 219 // Put it back together to make the regexp.
3270033a 220 $regexp = '|^' . implode('.*', $excapedbits) . '$|u';
90a36f8c 221
5a14d563 222 // Make the match insensitive if requested to.
223 if ($ignorecase) {
224 $regexp .= 'i';
225 }
90a36f8c 226
5a14d563 227 return preg_match($regexp, trim($string));
1a1293ed 228 }
229
230 /// BACKUP FUNCTIONS ////////////////////////////
c5d94c41 231
232 /*
233 * Backup the data in the question
234 *
235 * This is used in question/backuplib.php
236 */
237 function backup($bf,$preferences,$question,$level=6) {
238
239 $status = true;
240
a4d79b0e 241 $shortanswers = get_records('question_shortanswer', 'question', $question, 'id ASC');
c5d94c41 242 //If there are shortanswers
243 if ($shortanswers) {
244 //Iterate over each shortanswer
245 foreach ($shortanswers as $shortanswer) {
246 $status = fwrite ($bf,start_tag("SHORTANSWER",$level,true));
247 //Print shortanswer contents
248 fwrite ($bf,full_tag("ANSWERS",$level+1,false,$shortanswer->answers));
249 fwrite ($bf,full_tag("USECASE",$level+1,false,$shortanswer->usecase));
250 $status = fwrite ($bf,end_tag("SHORTANSWER",$level,true));
251 }
252 //Now print question_answers
253 $status = question_backup_answers($bf,$preferences,$question);
254 }
255 return $status;
256 }
516cf3eb 257
315559d3 258/// RESTORE FUNCTIONS /////////////////
259
260 /*
261 * Restores the data in the question
262 *
263 * This is used in question/restorelib.php
264 */
265 function restore($old_question_id,$new_question_id,$info,$restore) {
266
267 $status = true;
268
269 //Get the shortanswers array
270 $shortanswers = $info['#']['SHORTANSWER'];
271
272 //Iterate over shortanswers
273 for($i = 0; $i < sizeof($shortanswers); $i++) {
274 $sho_info = $shortanswers[$i];
275
276 //Now, build the question_shortanswer record structure
5a14d563 277 $shortanswer = new stdClass;
315559d3 278 $shortanswer->question = $new_question_id;
279 $shortanswer->answers = backup_todb($sho_info['#']['ANSWERS']['0']['#']);
280 $shortanswer->usecase = backup_todb($sho_info['#']['USECASE']['0']['#']);
281
282 //We have to recode the answers field (a list of answers id)
283 //Extracts answer id from sequence
284 $answers_field = "";
285 $in_first = true;
286 $tok = strtok($shortanswer->answers,",");
287 while ($tok) {
288 //Get the answer from backup_ids
289 $answer = backup_getid($restore->backup_unique_code,"question_answers",$tok);
290 if ($answer) {
291 if ($in_first) {
292 $answers_field .= $answer->new_id;
293 $in_first = false;
294 } else {
295 $answers_field .= ",".$answer->new_id;
296 }
297 }
298 //check for next
299 $tok = strtok(",");
300 }
301 //We have the answers field recoded to its new ids
302 $shortanswer->answers = $answers_field;
303
304 //The structure is equal to the db, so insert the question_shortanswer
305 $newid = insert_record ("question_shortanswer",$shortanswer);
306
307 //Do some output
308 if (($i+1) % 50 == 0) {
309 if (!defined('RESTORE_SILENTLY')) {
310 echo ".";
311 if (($i+1) % 1000 == 0) {
312 echo "<br />";
313 }
314 }
315 backup_flush(300);
316 }
317
318 if (!$newid) {
319 $status = false;
320 }
321 }
322
323 return $status;
324 }
90a36f8c 325
326
93a501c1 327 /**
328 * Prints the score obtained and maximum score available plus any penalty
329 * information
330 *
331 * This function prints a summary of the scoring in the most recently
332 * graded state (the question may not have been submitted for marking at
333 * the current state). The default implementation should be suitable for most
334 * question types.
335 * @param object $question The question for which the grading details are
336 * to be rendered. Question type specific information
337 * is included. The maximum possible grade is in
338 * ->maxgrade.
339 * @param object $state The state. In particular the grading information
340 * is in ->grade, ->raw_grade and ->penalty.
341 * @param object $cmoptions
342 * @param object $options An object describing the rendering options.
343 */
344 function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
345 /* The default implementation prints the number of marks if no attempt
346 has been made. Otherwise it displays the grade obtained out of the
347 maximum grade available and a warning if a penalty was applied for the
348 attempt and displays the overall grade obtained counting all previous
349 responses (and penalties) */
90a36f8c 350
93a501c1 351 // MDL-7496 show correct answer after "Incorrect"
352 $correctanswer = '';
353 if ($correctanswers = $this->get_correct_responses($question, $state)) {
354 if ($options->readonly && $options->correct_responses) {
355 $delimiter = '';
356 if ($correctanswers) {
357 foreach ($correctanswers as $ca) {
358 $correctanswer .= $delimiter.$ca;
359 $delimiter = ', ';
360 }
361 }
90a36f8c 362 }
93a501c1 363 }
90a36f8c 364
93a501c1 365 if (QUESTION_EVENTDUPLICATE == $state->event) {
366 echo ' ';
367 print_string('duplicateresponse', 'quiz');
368 }
369 if (!empty($question->maxgrade) && $options->scores) {
370 if (question_state_is_graded($state->last_graded)) {
371 // Display the grading details from the last graded state
372 $grade = new stdClass;
373 $grade->cur = round($state->last_graded->grade, $cmoptions->decimalpoints);
374 $grade->max = $question->maxgrade;
375 $grade->raw = round($state->last_graded->raw_grade, $cmoptions->decimalpoints);
376
377 // let student know wether the answer was correct
378 echo '<div class="correctness ';
379 if ($state->last_graded->raw_grade >= $question->maxgrade/1.01) { // We divide by 1.01 so that rounding errors dont matter.
380 echo ' correct">';
381 print_string('correct', 'quiz');
382 } else if ($state->last_graded->raw_grade > 0) {
383 echo ' partiallycorrect">';
384 print_string('partiallycorrect', 'quiz');
385 // MDL-7496
386 if ($correctanswer) {
e0c25647 387 echo ('<div class="correctness">');
ec2692a6 388 print_string('correctansweris', 'quiz', s($correctanswer, true));
93a501c1 389 echo ('</div>');
390 }
391 } else {
392 echo ' incorrect">';
393 // MDL-7496
90a36f8c 394 print_string('incorrect', 'quiz');
93a501c1 395 if ($correctanswer) {
e0c25647 396 echo ('<div class="correctness">');
ec2692a6 397 print_string('correctansweris', 'quiz', s($correctanswer, true));
93a501c1 398 echo ('</div>');
399 }
400 }
401 echo '</div>';
402
403 echo '<div class="gradingdetails">';
404 // print grade for this submission
405 print_string('gradingdetails', 'quiz', $grade);
406 if ($cmoptions->penaltyscheme) {
407 // print details of grade adjustment due to penalties
408 if ($state->last_graded->raw_grade > $state->last_graded->grade){
409 echo ' ';
410 print_string('gradingdetailsadjustment', 'quiz', $grade);
411 }
412 // print info about new penalty
413 // penalty is relevant only if the answer is not correct and further attempts are possible
414 if (($state->last_graded->raw_grade < $question->maxgrade) and (QUESTION_EVENTCLOSEANDGRADE !== $state->event)) {
415 if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
416 // A penalty was applied so display it
417 echo ' ';
418 print_string('gradingdetailspenalty', 'quiz', $state->last_graded->penalty);
419 } else {
420 /* No penalty was applied even though the answer was
421 not correct (eg. a syntax error) so tell the student
422 that they were not penalised for the attempt */
423 echo ' ';
424 print_string('gradingdetailszeropenalty', 'quiz');
425 }
426 }
427 }
428 echo '</div>';
429 }
430 }
431 }
516cf3eb 432}
433//// END OF CLASS ////
434
435//////////////////////////////////////////////////////////////////////////
436//// INITIATION - Without this line the question type is not in use... ///
437//////////////////////////////////////////////////////////////////////////
a2156789 438question_register_questiontype(new question_shortanswer_qtype());
516cf3eb 439?>