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