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