7 /// QUESTION TYPE CLASS //////////////////
10 /// This class contains some special features in order to make the
11 /// question type embeddable within a multianswer (cloze) question
14 * @package questionbank
15 * @subpackage questiontypes
17 require_once("$CFG->dirroot/question/type/questiontype.php");
19 class question_shortanswer_qtype
extends default_questiontype
{
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!');
33 if (!$question->options
->answers
= get_records('question_answers', 'question',
34 $question->id
, 'id ASC')) {
35 notify('Error: Missing question answers!');
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();
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
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)";
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!";
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)";
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!";
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);
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);
127 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
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).'" ';
148 $value = ' value="" ';
150 $inputname = ' name="'.$nameprefix.'" ';
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
);
173 /// Removed correct answer, to be displayed later MDL-7496
174 include("$CFG->dirroot/question/type/shortanswer/display.html");
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)) {
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;
196 $textlib = textlib_get_instance();
197 return strcmp($textlib->strtolower($state->responses
['']),
198 $textlib->strtolower($teststate->responses
[''])) == 0;
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.
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) {
238 $shortanswers = get_records('question_shortanswer', 'question', $question, 'id ASC');
239 //If there are 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);
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) {
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
283 $tok = strtok($shortanswer->answers
,",");
285 //Get the answer from backup_ids
286 $answer = backup_getid($restore->backup_unique_code
,"question_answers",$tok);
289 $answers_field .= $answer->new_id
;
292 $answers_field .= ",".$answer->new_id
;
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);
305 if (($i+
1) %
50 == 0) {
306 if (!defined('RESTORE_SILENTLY')) {
308 if (($i+
1) %
1000 == 0) {
325 * Prints the score obtained and maximum score available plus any penalty
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
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
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"
350 if ($correctanswers = $this->get_correct_responses($question, $state)) {
351 if ($options->readonly
&& $options->correct_responses
) {
353 if ($correctanswers) {
354 foreach ($correctanswers as $ca) {
355 $correctanswer .= $delimiter.$ca;
362 if (QUESTION_EVENTDUPLICATE
== $state->event
) {
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.
378 print_string('correct', 'quiz');
379 } else if ($state->last_graded
->raw_grade
> 0) {
380 echo ' partiallycorrect">';
381 print_string('partiallycorrect', 'quiz');
383 if ($correctanswer) {
384 echo ('<div class="correctness">');
385 print_string('correctansweris', 'quiz', s($correctanswer));
391 print_string('incorrect', 'quiz');
392 if ($correctanswer) {
393 echo ('<div class="correctness">');
394 print_string('correctansweris', 'quiz', s($correctanswer));
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
){
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
415 print_string('gradingdetailspenalty', 'quiz', $state->last_graded
->penalty
);
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 */
421 print_string('gradingdetailszeropenalty', 'quiz');
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());