MDL-11350 allowvisiblecoursesinhiddencategories was not used fully in the determinati...
[moodle-pu.git] / question / type / shortanswer / questiontype.php
blob797b63b689076e94a2a9acb3e1cb225cdbcab739
1 <?php // $Id$
3 ///////////////////
4 /// SHORTANSWER ///
5 ///////////////////
7 /// QUESTION TYPE CLASS //////////////////
9 ///
10 /// This class contains some special features in order to make the
11 /// question type embeddable within a multianswer (cloze) question
12 ///
13 /**
14 * @package questionbank
15 * @subpackage questiontypes
17 require_once("$CFG->dirroot/question/type/questiontype.php");
19 class question_shortanswer_qtype extends default_questiontype {
21 function name() {
22 return 'shortanswer';
25 function get_question_options(&$question) {
26 // Get additional information from database
27 // and attach it to the question object
28 if (!$question->options = get_record('question_shortanswer', 'question', $question->id)) {
29 notify('Error: Missing question options!');
30 return false;
33 if (!$question->options->answers = get_records('question_answers', 'question',
34 $question->id, 'id ASC')) {
35 notify('Error: Missing question answers!');
36 return false;
38 return true;
41 function save_question_options($question) {
42 $result = new stdClass;
44 if (!$oldanswers = get_records('question_answers', 'question', $question->id, 'id ASC')) {
45 $oldanswers = array();
48 $answers = array();
49 $maxfraction = -1;
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];
59 if (!update_record("question_answers", $answer)) {
60 $result->error = "Could not update quiz answer! (id=$answer->id)";
61 return $result;
63 } else { // This is a completely new answer
64 $answer = new stdClass;
65 $answer->answer = trim($dataanswer);
66 $answer->question = $question->id;
67 $answer->fraction = $question->fraction[$key];
68 $answer->feedback = $question->feedback[$key];
69 if (!$answer->id = insert_record("question_answers", $answer)) {
70 $result->error = "Could not insert quiz answer!";
71 return $result;
74 $answers[] = $answer->id;
75 if ($question->fraction[$key] > $maxfraction) {
76 $maxfraction = $question->fraction[$key];
81 if ($options = get_record("question_shortanswer", "question", $question->id)) {
82 $options->answers = implode(",",$answers);
83 $options->usecase = $question->usecase;
84 if (!update_record("question_shortanswer", $options)) {
85 $result->error = "Could not update quiz shortanswer options! (id=$options->id)";
86 return $result;
88 } else {
89 unset($options);
90 $options->question = $question->id;
91 $options->answers = implode(",",$answers);
92 $options->usecase = $question->usecase;
93 if (!insert_record("question_shortanswer", $options)) {
94 $result->error = "Could not insert quiz shortanswer options!";
95 return $result;
99 // delete old answer records
100 if (!empty($oldanswers)) {
101 foreach($oldanswers as $oa) {
102 delete_records('question_answers', 'id', $oa->id);
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;
117 * Deletes question from the question-type specific tables
119 * @return boolean Success/Failure
120 * @param object $question The question being deleted
122 function delete_question($questionid) {
123 delete_records("question_shortanswer", "question", $questionid);
124 return true;
127 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
128 global $CFG;
129 /// This implementation is also used by question type 'numerical'
130 $readonly = empty($options->readonly) ? '' : 'readonly="readonly"';
131 $formatoptions = new stdClass;
132 $formatoptions->noclean = true;
133 $formatoptions->para = false;
134 $nameprefix = $question->name_prefix;
136 /// Print question text and media
138 $questiontext = format_text($question->questiontext,
139 $question->questiontextformat,
140 $formatoptions, $cmoptions->course);
141 $image = get_question_image($question);
143 /// Print input controls
145 if (isset($state->responses[''])) {
146 $value = ' value="'.s($state->responses[''], true).'" ';
147 } else {
148 $value = ' value="" ';
150 $inputname = ' name="'.$nameprefix.'" ';
152 $feedback = '';
153 $class = '';
154 $feedbackimg = '';
156 if ($options->feedback) {
157 $class = question_get_feedback_class(0);
158 $feedbackimg = question_get_feedback_image(0);
159 foreach($question->options->answers as $answer) {
161 if ($this->test_response($question, $state, $answer)) {
162 // Answer was correct or partially correct.
163 $class = question_get_feedback_class($answer->fraction);
164 $feedbackimg = question_get_feedback_image($answer->fraction);
165 if ($answer->feedback) {
166 $feedback = format_text($answer->feedback, true, $formatoptions, $cmoptions->course);
168 break;
173 /// Removed correct answer, to be displayed later MDL-7496
174 include("$CFG->dirroot/question/type/shortanswer/display.html");
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;
188 return false;
191 function compare_responses($question, $state, $teststate) {
192 if (isset($state->responses['']) && isset($teststate->responses[''])) {
193 if ($question->options->usecase) {
194 return strcmp($state->responses[''], $teststate->responses['']) == 0;
195 } else {
196 $textlib = textlib_get_instance();
197 return strcmp($textlib->strtolower($state->responses['']),
198 $textlib->strtolower($teststate->responses[''])) == 0;
201 return false;
204 function test_response(&$question, $state, $answer) {
205 // Trim the response before it is saved in the database. See MDL-10709
206 $state->responses[''] = trim($state->responses['']);
207 return $this->compare_string_with_wildcard(stripslashes_safe($state->responses['']),
208 $answer->answer, !$question->options->usecase);
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.
215 $bits = array_map('preg_quote', $bits);
216 // Put it back together to make the regexp.
217 $regexp = '|^' . implode('.*', $bits) . '$|u';
219 // Make the match insensitive if requested to.
220 if ($ignorecase) {
221 $regexp .= 'i';
224 return preg_match($regexp, trim($string));
227 /// BACKUP FUNCTIONS ////////////////////////////
230 * Backup the data in the question
232 * This is used in question/backuplib.php
234 function backup($bf,$preferences,$question,$level=6) {
236 $status = true;
238 $shortanswers = get_records('question_shortanswer', 'question', $question, 'id ASC');
239 //If there are shortanswers
240 if ($shortanswers) {
241 //Iterate over each shortanswer
242 foreach ($shortanswers as $shortanswer) {
243 $status = fwrite ($bf,start_tag("SHORTANSWER",$level,true));
244 //Print shortanswer contents
245 fwrite ($bf,full_tag("ANSWERS",$level+1,false,$shortanswer->answers));
246 fwrite ($bf,full_tag("USECASE",$level+1,false,$shortanswer->usecase));
247 $status = fwrite ($bf,end_tag("SHORTANSWER",$level,true));
249 //Now print question_answers
250 $status = question_backup_answers($bf,$preferences,$question);
252 return $status;
255 /// RESTORE FUNCTIONS /////////////////
258 * Restores the data in the question
260 * This is used in question/restorelib.php
262 function restore($old_question_id,$new_question_id,$info,$restore) {
264 $status = true;
266 //Get the shortanswers array
267 $shortanswers = $info['#']['SHORTANSWER'];
269 //Iterate over shortanswers
270 for($i = 0; $i < sizeof($shortanswers); $i++) {
271 $sho_info = $shortanswers[$i];
273 //Now, build the question_shortanswer record structure
274 $shortanswer = new stdClass;
275 $shortanswer->question = $new_question_id;
276 $shortanswer->answers = backup_todb($sho_info['#']['ANSWERS']['0']['#']);
277 $shortanswer->usecase = backup_todb($sho_info['#']['USECASE']['0']['#']);
279 //We have to recode the answers field (a list of answers id)
280 //Extracts answer id from sequence
281 $answers_field = "";
282 $in_first = true;
283 $tok = strtok($shortanswer->answers,",");
284 while ($tok) {
285 //Get the answer from backup_ids
286 $answer = backup_getid($restore->backup_unique_code,"question_answers",$tok);
287 if ($answer) {
288 if ($in_first) {
289 $answers_field .= $answer->new_id;
290 $in_first = false;
291 } else {
292 $answers_field .= ",".$answer->new_id;
295 //check for next
296 $tok = strtok(",");
298 //We have the answers field recoded to its new ids
299 $shortanswer->answers = $answers_field;
301 //The structure is equal to the db, so insert the question_shortanswer
302 $newid = insert_record ("question_shortanswer",$shortanswer);
304 //Do some output
305 if (($i+1) % 50 == 0) {
306 if (!defined('RESTORE_SILENTLY')) {
307 echo ".";
308 if (($i+1) % 1000 == 0) {
309 echo "<br />";
312 backup_flush(300);
315 if (!$newid) {
316 $status = false;
320 return $status;
325 * Prints the score obtained and maximum score available plus any penalty
326 * information
328 * This function prints a summary of the scoring in the most recently
329 * graded state (the question may not have been submitted for marking at
330 * the current state). The default implementation should be suitable for most
331 * question types.
332 * @param object $question The question for which the grading details are
333 * to be rendered. Question type specific information
334 * is included. The maximum possible grade is in
335 * ->maxgrade.
336 * @param object $state The state. In particular the grading information
337 * is in ->grade, ->raw_grade and ->penalty.
338 * @param object $cmoptions
339 * @param object $options An object describing the rendering options.
341 function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
342 /* The default implementation prints the number of marks if no attempt
343 has been made. Otherwise it displays the grade obtained out of the
344 maximum grade available and a warning if a penalty was applied for the
345 attempt and displays the overall grade obtained counting all previous
346 responses (and penalties) */
348 // MDL-7496 show correct answer after "Incorrect"
349 $correctanswer = '';
350 if ($correctanswers = $this->get_correct_responses($question, $state)) {
351 if ($options->readonly && $options->correct_responses) {
352 $delimiter = '';
353 if ($correctanswers) {
354 foreach ($correctanswers as $ca) {
355 $correctanswer .= $delimiter.$ca;
356 $delimiter = ', ';
362 if (QUESTION_EVENTDUPLICATE == $state->event) {
363 echo ' ';
364 print_string('duplicateresponse', 'quiz');
366 if (!empty($question->maxgrade) && $options->scores) {
367 if (question_state_is_graded($state->last_graded)) {
368 // Display the grading details from the last graded state
369 $grade = new stdClass;
370 $grade->cur = round($state->last_graded->grade, $cmoptions->decimalpoints);
371 $grade->max = $question->maxgrade;
372 $grade->raw = round($state->last_graded->raw_grade, $cmoptions->decimalpoints);
374 // let student know wether the answer was correct
375 echo '<div class="correctness ';
376 if ($state->last_graded->raw_grade >= $question->maxgrade/1.01) { // We divide by 1.01 so that rounding errors dont matter.
377 echo ' correct">';
378 print_string('correct', 'quiz');
379 } else if ($state->last_graded->raw_grade > 0) {
380 echo ' partiallycorrect">';
381 print_string('partiallycorrect', 'quiz');
382 // MDL-7496
383 if ($correctanswer) {
384 echo ('<div class="correctness">');
385 print_string('correctansweris', 'quiz', s($correctanswer));
386 echo ('</div>');
388 } else {
389 echo ' incorrect">';
390 // MDL-7496
391 print_string('incorrect', 'quiz');
392 if ($correctanswer) {
393 echo ('<div class="correctness">');
394 print_string('correctansweris', 'quiz', s($correctanswer));
395 echo ('</div>');
398 echo '</div>';
400 echo '<div class="gradingdetails">';
401 // print grade for this submission
402 print_string('gradingdetails', 'quiz', $grade);
403 if ($cmoptions->penaltyscheme) {
404 // print details of grade adjustment due to penalties
405 if ($state->last_graded->raw_grade > $state->last_graded->grade){
406 echo ' ';
407 print_string('gradingdetailsadjustment', 'quiz', $grade);
409 // print info about new penalty
410 // penalty is relevant only if the answer is not correct and further attempts are possible
411 if (($state->last_graded->raw_grade < $question->maxgrade) and (QUESTION_EVENTCLOSEANDGRADE !== $state->event)) {
412 if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
413 // A penalty was applied so display it
414 echo ' ';
415 print_string('gradingdetailspenalty', 'quiz', $state->last_graded->penalty);
416 } else {
417 /* No penalty was applied even though the answer was
418 not correct (eg. a syntax error) so tell the student
419 that they were not penalised for the attempt */
420 echo ' ';
421 print_string('gradingdetailszeropenalty', 'quiz');
425 echo '</div>';
430 //// END OF CLASS ////
432 //////////////////////////////////////////////////////////////////////////
433 //// INITIATION - Without this line the question type is not in use... ///
434 //////////////////////////////////////////////////////////////////////////
435 question_register_questiontype(new question_shortanswer_qtype());