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, $cmoptions->course
);
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 return $this->compare_string_with_wildcard(stripslashes_safe($state->responses
['']),
206 $answer->answer
, !$question->options
->usecase
);
209 function compare_string_with_wildcard($string, $pattern, $ignorecase) {
210 // Break the string on non-escaped asterisks.
211 $bits = preg_split('/(?<!\\\\)\*/', $pattern);
212 // Escape regexp special characters in the bits.
213 $bits = array_map('preg_quote', $bits);
214 // Put it back together to make the regexp.
215 $regexp = '|^' . implode('.*', $bits) . '$|u';
217 // Make the match insensitive if requested to.
222 return preg_match($regexp, trim($string));
225 /// BACKUP FUNCTIONS ////////////////////////////
228 * Backup the data in the question
230 * This is used in question/backuplib.php
232 function backup($bf,$preferences,$question,$level=6) {
236 $shortanswers = get_records('question_shortanswer', 'question', $question, 'id ASC');
237 //If there are shortanswers
239 //Iterate over each shortanswer
240 foreach ($shortanswers as $shortanswer) {
241 $status = fwrite ($bf,start_tag("SHORTANSWER",$level,true));
242 //Print shortanswer contents
243 fwrite ($bf,full_tag("ANSWERS",$level+
1,false,$shortanswer->answers
));
244 fwrite ($bf,full_tag("USECASE",$level+
1,false,$shortanswer->usecase
));
245 $status = fwrite ($bf,end_tag("SHORTANSWER",$level,true));
247 //Now print question_answers
248 $status = question_backup_answers($bf,$preferences,$question);
253 /// RESTORE FUNCTIONS /////////////////
256 * Restores the data in the question
258 * This is used in question/restorelib.php
260 function restore($old_question_id,$new_question_id,$info,$restore) {
264 //Get the shortanswers array
265 $shortanswers = $info['#']['SHORTANSWER'];
267 //Iterate over shortanswers
268 for($i = 0; $i < sizeof($shortanswers); $i++
) {
269 $sho_info = $shortanswers[$i];
271 //Now, build the question_shortanswer record structure
272 $shortanswer = new stdClass
;
273 $shortanswer->question
= $new_question_id;
274 $shortanswer->answers
= backup_todb($sho_info['#']['ANSWERS']['0']['#']);
275 $shortanswer->usecase
= backup_todb($sho_info['#']['USECASE']['0']['#']);
277 //We have to recode the answers field (a list of answers id)
278 //Extracts answer id from sequence
281 $tok = strtok($shortanswer->answers
,",");
283 //Get the answer from backup_ids
284 $answer = backup_getid($restore->backup_unique_code
,"question_answers",$tok);
287 $answers_field .= $answer->new_id
;
290 $answers_field .= ",".$answer->new_id
;
296 //We have the answers field recoded to its new ids
297 $shortanswer->answers
= $answers_field;
299 //The structure is equal to the db, so insert the question_shortanswer
300 $newid = insert_record ("question_shortanswer",$shortanswer);
303 if (($i+
1) %
50 == 0) {
304 if (!defined('RESTORE_SILENTLY')) {
306 if (($i+
1) %
1000 == 0) {
323 * Prints the score obtained and maximum score available plus any penalty
326 * This function prints a summary of the scoring in the most recently
327 * graded state (the question may not have been submitted for marking at
328 * the current state). The default implementation should be suitable for most
330 * @param object $question The question for which the grading details are
331 * to be rendered. Question type specific information
332 * is included. The maximum possible grade is in
334 * @param object $state The state. In particular the grading information
335 * is in ->grade, ->raw_grade and ->penalty.
336 * @param object $cmoptions
337 * @param object $options An object describing the rendering options.
339 function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
340 /* The default implementation prints the number of marks if no attempt
341 has been made. Otherwise it displays the grade obtained out of the
342 maximum grade available and a warning if a penalty was applied for the
343 attempt and displays the overall grade obtained counting all previous
344 responses (and penalties) */
346 // MDL-7496 show correct answer after "Incorrect"
348 if ($correctanswers = $this->get_correct_responses($question, $state)) {
349 if ($options->readonly
&& $options->correct_responses
) {
351 if ($correctanswers) {
352 foreach ($correctanswers as $ca) {
353 $correctanswer .= $delimiter.$ca;
360 if (QUESTION_EVENTDUPLICATE
== $state->event
) {
362 print_string('duplicateresponse', 'quiz');
364 if (!empty($question->maxgrade
) && $options->scores
) {
365 if (question_state_is_graded($state->last_graded
)) {
366 // Display the grading details from the last graded state
367 $grade = new stdClass
;
368 $grade->cur
= round($state->last_graded
->grade
, $cmoptions->decimalpoints
);
369 $grade->max
= $question->maxgrade
;
370 $grade->raw
= round($state->last_graded
->raw_grade
, $cmoptions->decimalpoints
);
372 // let student know wether the answer was correct
373 echo '<div class="correctness ';
374 if ($state->last_graded
->raw_grade
>= $question->maxgrade
/1.01) { // We divide by 1.01 so that rounding errors dont matter.
376 print_string('correct', 'quiz');
377 } else if ($state->last_graded
->raw_grade
> 0) {
378 echo ' partiallycorrect">';
379 print_string('partiallycorrect', 'quiz');
381 if ($correctanswer) {
382 echo ('<div class="correctness">');
383 print_string('correctansweris', 'quiz', s($correctanswer));
389 print_string('incorrect', 'quiz');
390 if ($correctanswer) {
391 echo ('<div class="correctness">');
392 print_string('correctansweris', 'quiz', s($correctanswer));
398 echo '<div class="gradingdetails">';
399 // print grade for this submission
400 print_string('gradingdetails', 'quiz', $grade);
401 if ($cmoptions->penaltyscheme
) {
402 // print details of grade adjustment due to penalties
403 if ($state->last_graded
->raw_grade
> $state->last_graded
->grade
){
405 print_string('gradingdetailsadjustment', 'quiz', $grade);
407 // print info about new penalty
408 // penalty is relevant only if the answer is not correct and further attempts are possible
409 if (($state->last_graded
->raw_grade
< $question->maxgrade
) and (QUESTION_EVENTCLOSEANDGRADE
!== $state->event
)) {
410 if ('' !== $state->last_graded
->penalty
&& ((float)$state->last_graded
->penalty
) > 0.0) {
411 // A penalty was applied so display it
413 print_string('gradingdetailspenalty', 'quiz', $state->last_graded
->penalty
);
415 /* No penalty was applied even though the answer was
416 not correct (eg. a syntax error) so tell the student
417 that they were not penalised for the attempt */
419 print_string('gradingdetailszeropenalty', 'quiz');
434 //// END OF CLASS ////
436 //////////////////////////////////////////////////////////////////////////
437 //// INITIATION - Without this line the question type is not in use... ///
438 //////////////////////////////////////////////////////////////////////////
439 question_register_questiontype(new question_shortanswer_qtype());