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