MDL-20636 More progress.
[moodle.git] / question / type / questiontype.php
CommitLineData
d1b7e03d 1<?php // $Id$
516cf3eb 2/**
4323d029 3 * The default questiontype class.
4 *
5 * @author Martin Dougiamas and many others. This has recently been completely
6 * rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of
7 * the Serving Mathematics project
8 * {@link http://maths.york.ac.uk/serving_maths}
9 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
10 * @package questionbank
11 * @subpackage questiontypes
271e6dec 12 */
516cf3eb 13
d1b7e03d 14require_once($CFG->dirroot . '/question/engine/lib.php');
516cf3eb 15
d1b7e03d
TH
16// DONOTCOMMIT
17class default_questiontype {
18 function plugin_dir() {
19 return '';
20 }
21}
32b0adfa 22/**
23 * This is the base class for Moodle question types.
271ffe3f 24 *
32b0adfa 25 * There are detailed comments on each method, explaining what the method is
26 * for, and the circumstances under which you might need to override it.
271ffe3f 27 *
32b0adfa 28 * Note: the questiontype API should NOT be considered stable yet. Very few
29 * question tyeps have been produced yet, so we do not yet know all the places
271ffe3f 30 * where the current API is insufficient. I would rather learn from the
32b0adfa 31 * experiences of the first few question type implementors, and improve the
32 * interface to meet their needs, rather the freeze the API prematurely and
33 * condem everyone to working round a clunky interface for ever afterwards.
271e6dec 34 *
4323d029 35 * @package questionbank
36 * @subpackage questiontypes
32b0adfa 37 */
d1b7e03d 38class question_type {
516cf3eb 39
d1b7e03d 40 public function __construct() {
869309b8 41 }
42
4995b9c1 43 /**
d1b7e03d 44 * @return string the name of this question type.
869309b8 45 */
d1b7e03d
TH
46 public function name() {
47 return substr(get_class($this), 6);
a2156789 48 }
271ffe3f 49
869309b8 50 /**
51 * The name this question should appear as in the create new question
d1b7e03d 52 * dropdown.
869309b8 53 *
54 * @return mixed the desired string, or false to hide this question type in the menu.
55 */
d1b7e03d
TH
56 public function menu_name() {
57 $name = $this->name();
58 return get_string($name, 'qtype_' . $name);
b974f947 59 }
60
a2156789 61 /**
d1b7e03d 62 * @return boolean true if this question type sometimes requires manual grading.
a2156789 63 */
d1b7e03d 64 public function is_manual_graded() {
a2156789 65 return false;
66 }
67
f24493ec 68 /**
69 * @param object $question a question of this type.
70 * @param string $otherquestionsinuse comma-separate list of other question ids in this attempt.
71 * @return boolean true if a particular instance of this question requires manual grading.
72 */
d1b7e03d 73 public function is_question_manual_graded($question, $otherquestionsinuse) {
f24493ec 74 return $this->is_manual_graded();
75 }
76
869309b8 77 /**
d1b7e03d 78 * @return boolean true if this question type can be used by the random question type.
869309b8 79 */
d1b7e03d
TH
80 public function is_usable_by_random() {
81 return true;
869309b8 82 }
83
a2156789 84 /**
d1b7e03d
TH
85 * Whether this question type can perform a frequency analysis of student
86 * responses.
87 *
88 * If this method returns true, you must implement the get_possible_responses
89 * method, and the question_definition class must implement the
90 * classify_response method.
91 *
92 * @return boolean whether this report can analyse all the student reponses
93 * for things like the quiz statistics report.
a2156789 94 */
d1b7e03d
TH
95 public function can_analyse_responses() {
96 // This works in most cases.
97 return !$this->is_manual_graded();
a2156789 98 }
99
869309b8 100 /**
d1b7e03d
TH
101 * @return whether the question_answers.answer field needs to have
102 * restore_decode_content_links_worker called on it.
869309b8 103 */
d1b7e03d 104 public function has_html_answers() {
869309b8 105 return false;
106 }
d001dac7 107
295043c2 108 /**
109 * If your question type has a table that extends the question table, and
110 * you want the base class to automatically save, backup and restore the extra fields,
111 * override this method to return an array wherer the first element is the table name,
112 * and the subsequent entries are the column names (apart from id and questionid).
113 *
114 * @return mixed array as above, or null to tell the base class to do nothing.
115 */
d1b7e03d 116 public function extra_question_fields() {
295043c2 117 return null;
118 }
119
d001dac7 120 /**
121 * If you use extra_question_fields, overload this function to return question id field name
122 * in case you table use another name for this column
123 */
d1b7e03d 124 protected function questionid_column_name() {
d001dac7 125 return 'questionid';
126 }
127
295043c2 128 /**
129 * If your question type has a table that extends the question_answers table,
130 * make this method return an array wherer the first element is the table name,
131 * and the subsequent entries are the column names (apart from id and answerid).
132 *
133 * @return mixed array as above, or null to tell the base class to do nothing.
134 */
d1b7e03d
TH
135 protected function extra_answer_fields() {
136 return null;
137 }
295043c2 138
36703ed7 139 /**
140 * Return an instance of the question editing form definition. This looks for a
141 * class called edit_{$this->name()}_question_form in the file
d1b7e03d 142 * {$CFG->docroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php
36703ed7 143 * and if it exists returns an instance of it.
144 *
145 * @param string $submiturl passed on to the constructor call.
146 * @return object an instance of the form definition, or null if one could not be found.
147 */
d1b7e03d 148 public function create_editing_form($submiturl, $question, $category, $contexts, $formeditable) {
36703ed7 149 global $CFG;
150 require_once("{$CFG->dirroot}/question/type/edit_question_form.php");
271ffe3f 151 $definition_file = $CFG->dirroot.'/question/type/'.$this->name().'/edit_'.$this->name().'_form.php';
36703ed7 152 if (!(is_readable($definition_file) && is_file($definition_file))) {
153 return null;
154 }
155 require_once($definition_file);
271ffe3f 156 $classname = 'question_edit_'.$this->name().'_form';
36703ed7 157 if (!class_exists($classname)) {
158 return null;
159 }
271e6dec 160 return new $classname($submiturl, $question, $category, $contexts, $formeditable);
36703ed7 161 }
162
99ba746d 163 /**
164 * @return string the full path of the folder this plugin's files live in.
165 */
d1b7e03d 166 public function plugin_dir() {
99ba746d 167 global $CFG;
168 return $CFG->dirroot . '/question/type/' . $this->name();
169 }
170
171 /**
172 * @return string the URL of the folder this plugin's files live in.
173 */
d1b7e03d 174 public function plugin_baseurl() {
99ba746d 175 global $CFG;
176 return $CFG->wwwroot . '/question/type/' . $this->name();
177 }
178
7b41a4a9 179 /**
180 * This method should be overriden if you want to include a special heading or some other
181 * html on a question editing page besides the question editing form.
182 *
183 * @param question_edit_form $mform a child of question_edit_form
184 * @param object $question
185 * @param string $wizardnow is '' for first page.
186 */
d1b7e03d
TH
187 public function display_question_editing_page(&$mform, $question, $wizardnow) {
188 $name = $this->name();
189 print_heading_with_help($this->get_heading(empty($question->id)), $name, 'qtype_' . $name);
271e6dec 190 $permissionstrs = array();
191 if (!empty($question->id)){
192 if ($question->formoptions->canedit){
193 $permissionstrs[] = get_string('permissionedit', 'question');
194 }
195 if ($question->formoptions->canmove){
196 $permissionstrs[] = get_string('permissionmove', 'question');
197 }
198 if ($question->formoptions->cansaveasnew){
199 $permissionstrs[] = get_string('permissionsaveasnew', 'question');
200 }
201 }
202 if (!$question->formoptions->movecontext && count($permissionstrs)){
d1b7e03d 203 print_heading(get_string('permissionto', 'question'), 'center', 3);
271e6dec 204 $html = '<ul>';
205 foreach ($permissionstrs as $permissionstr){
206 $html .= '<li>'.$permissionstr.'</li>';
207 }
208 $html .= '</ul>';
d1b7e03d 209 print_box($html, 'boxwidthnarrow boxaligncenter generalbox');
271e6dec 210 }
9ab75b2b 211 $mform->display();
212 }
271e6dec 213
9ab75b2b 214 /**
215 * Method called by display_question_editing_page and by question.php to get heading for breadcrumbs.
271e6dec 216 *
d1b7e03d 217 * @return string the heading
9ab75b2b 218 */
d1b7e03d
TH
219 public function get_heading($adding = false){
220 $name = $this->name();
221 if ($adding){
222 $action = 'adding';
271e6dec 223 } else {
d1b7e03d 224 $action = 'editing';
c2f8c4be 225 }
d1b7e03d
TH
226 return get_string($action . $name, 'qtype_' . $name);
227 }
228
229 /**
230 * Set any missing settings for this question to the default values. This is
231 * called before displaying the question editing form.
232 *
233 * @param object $questiondata the question data, loaded from the databsae,
234 * or more likely a newly created question object that is only partially
235 * initialised.
236 */
237 public function set_default_options($questiondata) {
7b41a4a9 238 }
239
516cf3eb 240 /**
24e8b9b6 241 * Saves (creates or updates) a question.
516cf3eb 242 *
243 * Given some question info and some data about the answers
244 * this function parses, organises and saves the question
245 * It is used by {@link question.php} when saving new data from
246 * a form, and also by {@link import.php} when importing questions
247 * This function in turn calls {@link save_question_options}
24e8b9b6 248 * to save question-type specific data.
249 *
250 * Whether we are saving a new question or updating an existing one can be
251 * determined by testing !empty($question->id). If it is not empty, we are updating.
252 *
253 * The question will be saved in category $form->category.
254 *
255 * @param object $question the question object which should be updated. For a new question will be mostly empty.
256 * @param object $form the object containing the information to save, as if from the question editing form.
257 * @param object $course not really used any more.
695d225c 258 * @return object On success, return the new question object. On failure,
271ffe3f 259 * return an object as follows. If the error object has an errors field,
32b0adfa 260 * display that as an error message. Otherwise, the editing form will be
695d225c 261 * redisplayed with validation errors, from validation_errors field, which
24e8b9b6 262 * is itself an object, shown next to the form fields. (I don't think this is accurate any more.)
516cf3eb 263 */
d1b7e03d
TH
264 public function save_question($question, $form, $course) {
265 global $USER;
fe6ce234 266
516cf3eb 267 // This default implementation is suitable for most
268 // question types.
271ffe3f 269
516cf3eb 270 // First, save the basic question itself
24e8b9b6 271 $question->name = trim($form->name);
d1b7e03d
TH
272 $question->questiontext = trim($form->questiontext);
273 $question->questiontextformat = $form->questiontextformat;
24e8b9b6 274 $question->parent = isset($form->parent) ? $form->parent : 0;
516cf3eb 275 $question->length = $this->actual_number_of_questions($question);
276 $question->penalty = isset($form->penalty) ? $form->penalty : 0;
277
d1b7e03d
TH
278 if (empty($form->image)) {
279 $question->image = '';
516cf3eb 280 } else {
d1b7e03d 281 $question->image = $form->image;
516cf3eb 282 }
283
d1b7e03d 284 if (empty($form->generalfeedback)) {
a4514d91 285 $question->generalfeedback = '';
1b8a7434 286 } else {
d1b7e03d 287 $question->generalfeedback = trim($form->generalfeedback);
1b8a7434 288 }
289
516cf3eb 290 if (empty($question->name)) {
d1b7e03d 291 $question->name = shorten_text(strip_tags($question->questiontext), 15);
516cf3eb 292 if (empty($question->name)) {
293 $question->name = '-';
294 }
295 }
296
297 if ($question->penalty > 1 or $question->penalty < 0) {
298 $question->errors['penalty'] = get_string('invalidpenalty', 'quiz');
299 }
300
d1b7e03d
TH
301 if (isset($form->defaultmark)) {
302 $question->defaultmark = $form->defaultmark;
516cf3eb 303 }
304
d1b7e03d
TH
305 list($question->category) = explode(',', $form->category);
306
307 if (!empty($question->id)) {
308 /// Question already exists, update.
309 $question->modifiedby = $USER->id;
310 $question->timemodified = time();
311 if (!update_record('question', $question)) {
312 error('Could not update question!');
313 }
314
315 } else {
316 /// New question.
cbe20043 317 // Set the unique code
318 $question->stamp = make_unique_id_code();
271e6dec 319 $question->createdby = $USER->id;
d1b7e03d 320 $question->modifiedby = $USER->id;
271e6dec 321 $question->timecreated = time();
d1b7e03d
TH
322 $question->timemodified = time();
323 if (!$question->id = insert_record('question', $question)) {
324 error('Could not insert new question!');
325 }
516cf3eb 326 }
327
328 // Now to save all the answers and type-specific options
6459b8fc 329 $form->id = $question->id;
330 $form->qtype = $question->qtype;
516cf3eb 331 $form->category = $question->category;
6459b8fc 332 $form->questiontext = $question->questiontext;
516cf3eb 333
334 $result = $this->save_question_options($form);
335
336 if (!empty($result->error)) {
d1b7e03d 337 error($result->error);
516cf3eb 338 }
339
340 if (!empty($result->notice)) {
341 notice($result->notice, "question.php?id=$question->id");
342 }
343
344 if (!empty($result->noticeyesno)) {
d1b7e03d
TH
345 notice_yesno($result->noticeyesno, "question.php?id=$question->id&amp;courseid={$course->id}",
346 "edit.php?courseid={$course->id}");
347 print_footer($course);
348 exit;
516cf3eb 349 }
350
cbe20043 351 // Give the question a unique version stamp determined by question_hash()
d1b7e03d
TH
352 if (!set_field('question', 'version', question_hash($question), 'id', $question->id)) {
353 error('Could not update question version field');
354 }
cbe20043 355
516cf3eb 356 return $question;
357 }
271ffe3f 358
516cf3eb 359 /**
d1b7e03d
TH
360 * Saves question-type specific options
361 *
362 * This is called by {@link save_question()} to save the question-type specific data
363 * @return object $result->error or $result->noticeyesno or $result->notice
364 * @param object $question This holds the information from the editing form,
365 * it is not a standard question object.
366 */
367 public function save_question_options($question) {
295043c2 368 $extra_question_fields = $this->extra_question_fields();
369
370 if (is_array($extra_question_fields)) {
371 $question_extension_table = array_shift($extra_question_fields);
271e6dec 372
295043c2 373 $function = 'update_record';
d001dac7 374 $questionidcolname = $this->questionid_column_name();
d1b7e03d 375 $options = get_record($question_extension_table, $questionidcolname, $question->id);
295043c2 376 if (!$options) {
377 $function = 'insert_record';
378 $options = new stdClass;
d001dac7 379 $options->$questionidcolname = $question->id;
295043c2 380 }
381 foreach ($extra_question_fields as $field) {
382 if (!isset($question->$field)) {
383 $result = new stdClass;
384 $result->error = "No data for field $field when saving " .
385 $this->name() . ' question id ' . $question->id;
386 return $result;
387 }
388 $options->$field = $question->$field;
389 }
271e6dec 390
d1b7e03d 391 if (!$function($question_extension_table, $options)) {
295043c2 392 $result = new stdClass;
393 $result->error = 'Could not save question options for ' .
394 $this->name() . ' question id ' . $question->id;
395 return $result;
396 }
397 }
398
399 $extra_answer_fields = $this->extra_answer_fields();
400 // TODO save the answers, with any extra data.
271e6dec 401
4eda4eec 402 return null;
516cf3eb 403 }
404
d1b7e03d
TH
405 public function save_hints($formdata, $withparts = false) {
406 delete_records('question_hints', 'questionid', $formdata->id);
295043c2 407
d1b7e03d
TH
408 if (!empty($formdata->hint)) {
409 $numhints = max(array_keys($formdata->hint)) + 1;
410 } else {
411 $numhints = 0;
516cf3eb 412 }
295043c2 413
d1b7e03d
TH
414 if ($withparts) {
415 if (!empty($formdata->hintclearwrong)) {
416 $numclears = max(array_keys($formdata->hintclearwrong)) + 1;
417 } else {
418 $numclears = 0;
419 }
420 if (!empty($formdata->hintshownumcorrect)) {
421 $numshows = max(array_keys($formdata->hintshownumcorrect)) + 1;
422 } else {
423 $numshows = 0;
424 }
425 $numhints = max($numhints, $numclears, $numshows);
426 }
427
428 for ($i = 0; $i < $numhints; $i += 1) {
429 $hint = new stdClass;
430 $hint->hint = $formdata->hint[$i];
431 $hint->questionid = $formdata->id;
432
433 if (html_is_blank($hint->hint)) {
434 $hint->hint = '';
435 }
436
437 if ($withparts) {
438 $hint->clearwrong = !empty($formdata->hintclearwrong[$i]);
439 $hint->shownumcorrect = !empty($formdata->hintshownumcorrect[$i]);
440 }
441
442 if (empty($hint->hint) && empty($hint->clearwrong) && empty($hint->shownumcorrect)) {
443 continue;
444 }
445
446 insert_record('question_hints', $hint);
447 }
448 }
449
450 /**
451 * Loads the question type specific options for the question.
452 *
453 * This function loads any question type specific options for the
454 * question from the database into the question object. This information
455 * is placed in the $question->options field. A question type is
456 * free, however, to decide on a internal structure of the options field.
457 * @return bool Indicates success or failure.
458 * @param object $question The question object for the question. This object
459 * should be updated to include the question type
460 * specific information (it is passed by reference).
461 */
462 public function get_question_options($question) {
463 global $CFG;
464
295043c2 465 $extra_question_fields = $this->extra_question_fields();
466 if (is_array($extra_question_fields)) {
467 $question_extension_table = array_shift($extra_question_fields);
d1b7e03d 468 $extra_data = get_record($question_extension_table, $this->questionid_column_name(), $question->id, '', '', '', '', implode(', ', $extra_question_fields));
295043c2 469 if ($extra_data) {
470 foreach ($extra_question_fields as $field) {
471 $question->options->$field = $extra_data->$field;
472 }
473 } else {
d1b7e03d 474 notify("Failed to load question options from the table $question_extension_table for questionid " .
295043c2 475 $question->id);
476 return false;
477 }
478 }
479
480 $extra_answer_fields = $this->extra_answer_fields();
481 if (is_array($extra_answer_fields)) {
482 $answer_extension_table = array_shift($extra_answer_fields);
d1b7e03d
TH
483 $question->options->answers = get_records_sql('
484 SELECT qa.*, qax.' . implode(', qax.', $extra_answer_fields) . '
485 FROM ' . $CFG->prefix . 'question_answers qa, ' . $CFG->prefix . '$answer_extension_table qax
486 WHERE qa.questionid = ' . $question->id . ' AND qax.answerid = qa.id');
295043c2 487 if (!$question->options->answers) {
d1b7e03d 488 notify("Failed to load question answers from the table $answer_extension_table for questionid " .
295043c2 489 $question->id);
490 return false;
491 }
492 } else {
ca9000df 493 // Don't check for success or failure because some question types do not use the answers table.
d1b7e03d 494 $question->options->answers = get_records('question_answers', 'question', $question->id, 'id ASC');
295043c2 495 }
496
d1b7e03d
TH
497 $question->hints = get_records('question_hints', 'questionid', $question->id, 'id ASC');
498
516cf3eb 499 return true;
500 }
501
502 /**
d1b7e03d
TH
503 * Create an appropriate question_definition for the question of this type
504 * using data loaded from the database.
505 * @param object $questiondata the question data loaded from the database.
506 * @return question_definition the corresponding question_definition.
507 */
508 public function make_question($questiondata) {
509 $question = $this->make_question_instance($questiondata);
510 $this->initialise_question_instance($question, $questiondata);
511 return $question;
512 }
0429cd86 513
d1b7e03d
TH
514 /**
515 * Create an appropriate question_definition for the question of this type
516 * using data loaded from the database.
517 * @param object $questiondata the question data loaded from the database.
518 * @return question_definition an instance of the appropriate question_definition subclass.
519 * Still needs to be initialised.
520 */
521 protected function make_question_instance($questiondata) {
522 question_bank::load_question_definition_classes($this->name());
523 $class = 'qtype_' . $this->name() . '_question';
524 return new $class();
525 }
526
527 /**
528 * Initialise the common question_definition fields.
529 * @param question_definition $question the question_definition we are creating.
530 * @param object $questiondata the question data loaded from the database.
531 */
532 protected function initialise_question_instance(question_definition $question, $questiondata) {
533 $question->id = $questiondata->id;
534 $question->category = $questiondata->category;
535 $question->parent = $questiondata->parent;
536 $question->qtype = $this;
537 $question->name = $questiondata->name;
538 $question->questiontext = $questiondata->questiontext;
539 $question->questiontextformat = $questiondata->questiontextformat;
540 $question->generalfeedback = $questiondata->generalfeedback;
541 $question->defaultmark = $questiondata->defaultmark + 0;
542 $question->length = $questiondata->length;
543 $question->penalty = $questiondata->penalty;
544 $question->stamp = $questiondata->stamp;
545 $question->version = $questiondata->version;
546 $question->hidden = $questiondata->hidden;
547 $question->timecreated = $questiondata->timecreated;
548 $question->timemodified = $questiondata->timemodified;
549 $question->createdby = $questiondata->createdby;
550 $question->modifiedby = $questiondata->modifiedby;
551
552 $this->initialise_question_hints($question, $questiondata);
553 }
554
555 /**
556 * Initialise question_definition::hints field.
557 * @param question_definition $question the question_definition we are creating.
558 * @param object $questiondata the question data loaded from the database.
559 */
560 protected function initialise_question_hints(question_definition $question, $questiondata) {
561 if (empty($questiondata->hints)) {
562 return;
563 }
564 foreach ($questiondata->hints as $hint) {
565 $question->hints[] = $this->make_hint($hint);
566 }
0429cd86 567 }
568
569 /**
d1b7e03d
TH
570 * Create a question_hint, or an appropriate subclass for this question,
571 * from a row loaded from the database.
572 * @param object $hint the DB row from the question hints table.
573 * @return question_hint
9203b705 574 */
d1b7e03d
TH
575 protected function make_hint($hint) {
576 return question_hint::load_from_record($hint);
577 }
578
579 /**
580 * Initialise question_definition::answers field.
581 * @param question_definition $question the question_definition we are creating.
582 * @param object $questiondata the question data loaded from the database.
583 */
584 protected function initialise_question_answers(question_definition $question, $questiondata) {
585 $question->answers = array();
586 if (empty($questiondata->options->answers)) {
587 return;
588 }
589 foreach ($questiondata->options->answers as $a) {
590 $question->answers[$a->id] = new question_answer($a->answer, $a->fraction, $a->feedback);
591 }
592 }
9203b705 593
d1b7e03d
TH
594 /**
595 * Deletes a question from the question-type specific tables
596 *
597 * @return boolean Success/Failure
598 * @param object $question The question being deleted
599 */
600 public function delete_question($questionid) {
601 global $CFG;
602 $success = true;
516cf3eb 603
295043c2 604 $extra_question_fields = $this->extra_question_fields();
605 if (is_array($extra_question_fields)) {
606 $question_extension_table = array_shift($extra_question_fields);
d1b7e03d
TH
607 $success = $success && delete_records($question_extension_table,
608 $this->questionid_column_name(), $questionid);
295043c2 609 }
610
611 $extra_answer_fields = $this->extra_answer_fields();
612 if (is_array($extra_answer_fields)) {
613 $answer_extension_table = array_shift($extra_answer_fields);
d1b7e03d
TH
614 $success = $success && delete_records_select($answer_extension_table,
615 "answerid IN (SELECT qa.id FROM {$CFG->prefix}question_answers qa WHERE qa.question = $questionid)");
295043c2 616 }
617
d1b7e03d
TH
618 $success = $success && delete_records('question_answers', 'question', $questionid);
619
620 $success = $success && delete_records('question_hints', 'questionid', $questionid);
621
622 return $success;
516cf3eb 623 }
624
625 /**
626 * Returns the number of question numbers which are used by the question
627 *
628 * This function returns the number of question numbers to be assigned
629 * to the question. Most question types will have length one; they will be
dfa47f96 630 * assigned one number. The 'description' type, however does not use up a
516cf3eb 631 * number and so has a length of zero. Other question types may wish to
632 * handle a bundle of questions and hence return a number greater than one.
633 * @return integer The number of question numbers which should be
634 * assigned to the question.
635 * @param object $question The question whose length is to be determined.
636 * Question type specific information is included.
637 */
d1b7e03d 638 public function actual_number_of_questions($question) {
516cf3eb 639 // By default, each question is given one number
640 return 1;
641 }
642
6f51ed72 643 /**
644 * @param object $question
d1b7e03d
TH
645 * @return number|null either a fraction estimating what the student would
646 * score by guessing, or null, if it is not possible to estimate.
6f51ed72 647 */
d1b7e03d 648 function get_random_guess_score($questiondata) {
6f51ed72 649 return 0;
650 }
516cf3eb 651
516cf3eb 652 /**
d1b7e03d
TH
653 * This method should return all the possible types of response that are
654 * recognised for this question.
271e6dec 655 *
d1b7e03d
TH
656 * The question is modelled as comprising one or more subparts. For each
657 * subpart, there are one or more classes that that students response
658 * might fall into, each of those classes earning a certain score.
50da63eb 659 *
d1b7e03d
TH
660 * For example, in a shortanswer question, there is only one subpart, the
661 * text entry field. The response the student gave will be classified according
662 * to which of the possible $question->options->answers it matches.
271e6dec 663 *
d1b7e03d
TH
664 * For the matching question type, there will be one subpart for each
665 * question stem, and for each stem, each of the possible choices is a class
666 * of student's response.
667 *
668 * A response is an object with two fields, ->responseclass is a string
669 * presentation of that response, and ->fraction, the credit for a response
670 * in that class.
671 *
672 * Array keys have no specific meaning, but must be unique, and must be
673 * the same if this function is called repeatedly.
674 *
675 * @param object $question the question definition data.
676 * @return array keys are subquestionid, values are arrays of possible
677 * responses to that subquestion.
99ba746d 678 */
d1b7e03d
TH
679 function get_possible_responses($questiondata) {
680 return array();
50da63eb 681 }
5fceb049 682
50da63eb 683 /**
d1b7e03d
TH
684 * Return any CSS JavaScript required on the head of the question editing
685 * page question/question.php.
686 *
687 * @return an array of bits of HTML to add to the head of pages where
688 * this question is displayed in the body. The array should use
689 * integer array keys, which have no significance.
50da63eb 690 */
d1b7e03d 691 public function get_editing_head_contributions() {
50da63eb 692 // By default, we link to any of the files styles.css, styles.php,
693 // script.js or script.php that exist in the plugin folder.
694 // Core question types should not use this mechanism. Their styles
695 // should be included in the standard theme.
d1b7e03d 696 return $this->find_standard_scripts_and_css();
50da63eb 697 }
271e6dec 698
50da63eb 699 /**
d1b7e03d 700 * Utility method used by @see{get_editing_head_contributions} and
50da63eb 701 * @see{get_editing_head_contributions}. This looks for any of the files
d1b7e03d
TH
702 * styles.css, styles.php, script.js or script.php that exist in the plugin
703 * folder and ensures they get included.
704 *
705 * @return array as required by get_editing_head_contributions.
50da63eb 706 */
d1b7e03d 707 public function find_standard_scripts_and_css() {
99ba746d 708 $plugindir = $this->plugin_dir();
d1b7e03d 709 $baseurl = $this->plugin_baseurl();
50da63eb 710
711 if (file_exists($plugindir . '/script.js')) {
d1b7e03d 712 require_js($baseurl . '/script.js');
50da63eb 713 }
714 if (file_exists($plugindir . '/script.php')) {
d1b7e03d 715 require_js($baseurl . '/script.php');
50da63eb 716 }
fe6ce234 717
d1b7e03d
TH
718 $stylesheets = array();
719 if (file_exists($plugindir . '/styles.css')) {
720 $stylesheets[] = 'styles.css';
516cf3eb 721 }
d1b7e03d
TH
722 if (file_exists($plugindir . '/styles.php')) {
723 $stylesheets[] = 'styles.php';
1b8a7434 724 }
d1b7e03d
TH
725 $contributions = array();
726 foreach ($stylesheets as $stylesheet) {
727 $contributions[] = '<link rel="stylesheet" type="text/css" href="' .
728 $baseurl . '/' . $stylesheet . '" />';
516cf3eb 729 }
d1b7e03d 730 return $contributions;
fe9b5cfd 731 }
732
62e76c67 733 /**
d1b7e03d
TH
734 * Returns true if the editing wizard is finished, false otherwise.
735 *
736 * The default implementation returns true, which is suitable for all question-
737 * types that only use one editing form. This function is used in
738 * question.php to decide whether we can regrade any states of the edited
739 * question and redirect to edit.php.
740 *
741 * The dataset dependent question-type, which is extended by the calculated
742 * question-type, overwrites this method because it uses multiple pages (i.e.
743 * a wizard) to set up the question and associated datasets.
744 *
745 * @param object $form The data submitted by the previous page.
746 *
747 * @return boolean Whether the wizard's last page was submitted or not.
748 */
749 public function finished_edit_wizard(&$form) {
750 //In the default case there is only one edit page.
751 return true;
62e76c67 752 }
753
d1b7e03d
TH
754 /*
755 * Find all course / site files linked from a question.
62e76c67 756 *
d1b7e03d
TH
757 * Need to check for links to files in question_answers.answer and feedback
758 * and in question table in generalfeedback and questiontext fields. Methods
759 * on child classes will also check extra question specific fields.
760 *
761 * Needs to be overriden for child classes that have extra fields containing
762 * html.
aafdb447 763 *
d1b7e03d
TH
764 * @param string html the html to search
765 * @param int courseid search for files for courseid course or set to siteid for
766 * finding site files.
767 * @return array of url, relative url is key and array with one item = question id as value
768 * relative url is relative to course/site files directory root.
aafdb447 769 */
d1b7e03d
TH
770 public function find_file_links($question, $courseid){
771 $urls = array();
772
773 /// Question image
774 if ($question->image != ''){
775 if (substr(strtolower($question->image), 0, 7) == 'http://') {
776 $matches = array();
777
778 //support for older questions where we have a complete url in image field
779 if (preg_match('!^'.question_file_links_base_url($courseid).'(.*)!i', $question->image, $matches)){
780 if ($cleanedurl = question_url_check($urls[$matches[2]])){
781 $urls[$cleanedurl] = null;
782 }
783 }
784 } else {
785 if ($question->image != ''){
786 if ($cleanedurl = question_url_check($question->image)){
787 $urls[$cleanedurl] = null;//will be set later
788 }
789 }
aafdb447 790
d1b7e03d 791 }
aafdb447 792
aafdb447 793 }
794
d1b7e03d
TH
795 /// Questiontext and general feedback.
796 $urls += question_find_file_links_from_html($question->questiontext, $courseid);
797 $urls += question_find_file_links_from_html($question->generalfeedback, $courseid);
aafdb447 798
d1b7e03d
TH
799 /// Answers, if this question uses them.
800 if (isset($question->options->answers)){
801 foreach ($question->options->answers as $answerkey => $answer){
802 /// URLs in the answers themselves, if appropriate.
803 if ($this->has_html_answers()) {
804 $urls += question_find_file_links_from_html($answer->answer, $courseid);
805 }
806 /// URLs in the answer feedback.
807 $urls += question_find_file_links_from_html($answer->feedback, $courseid);
808 }
aafdb447 809 }
fb6dcdab 810
d1b7e03d
TH
811 /// Set all the values of the array to the question object
812 if ($urls){
813 $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id)));
814 }
815 return $urls;
aafdb447 816 }
817
d1b7e03d
TH
818 /*
819 * Find all course / site files linked from a question.
fe9b5cfd 820 *
d1b7e03d
TH
821 * Need to check for links to files in question_answers.answer and feedback
822 * and in question table in generalfeedback and questiontext fields. Methods
823 * on child classes will also check extra question specific fields.
824 *
825 * Needs to be overriden for child classes that have extra fields containing
826 * html.
827 *
828 * @param string html the html to search
829 * @param int course search for files for courseid course or set to siteid for
830 * finding site files.
831 * @return array of files, file name is key and array with one item = question id as value
fe9b5cfd 832 */
d1b7e03d
TH
833 public function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
834 global $CFG;
835 $updateqrec = false;
5c86dc7c 836
d1b7e03d
TH
837 /// Question image
838 if (!empty($question->image)){
839 //support for older questions where we have a complete url in image field
840 if (substr(strtolower($question->image), 0, 7) == 'http://') {
841 $questionimage = preg_replace('!^'.question_file_links_base_url($fromcourseid).preg_quote($url, '!').'$!i', $destination, $question->image, 1);
516cf3eb 842 } else {
d1b7e03d 843 $questionimage = preg_replace('!^'.preg_quote($url, '!').'$!i', $destination, $question->image, 1);
516cf3eb 844 }
d1b7e03d
TH
845 if ($questionimage != $question->image){
846 $question->image = $questionimage;
847 $updateqrec = true;
516cf3eb 848 }
d1b7e03d
TH
849 }
850
851 /// Questiontext and general feedback.
852 $question->questiontext = question_replace_file_links_in_html($question->questiontext, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
853 $question->generalfeedback = question_replace_file_links_in_html($question->generalfeedback, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
5c86dc7c 854
d1b7e03d
TH
855 /// If anything has changed, update it in the database.
856 if ($updateqrec){
857 if (!update_record('question', addslashes_recursive($question))){
858 error ('Couldn\'t update question '.$question->name);
5c86dc7c 859 }
516cf3eb 860 }
516cf3eb 861
d1b7e03d
TH
862
863 /// Answers, if this question uses them.
864 if (isset($question->options->answers)){
865 //answers that do not need updating have been unset
866 foreach ($question->options->answers as $answer){
867 $answerchanged = false;
868 /// URLs in the answers themselves, if appropriate.
869 if ($this->has_html_answers()) {
870 $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
871 }
872 /// URLs in the answer feedback.
873 $answer->feedback = question_replace_file_links_in_html($answer->feedback, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
874 /// If anything has changed, update it in the database.
875 if ($answerchanged){
876 if (!update_record('question_answers', addslashes_recursive($answer))){
877 error ('Couldn\'t update question ('.$question->name.') answer '.$answer->id);
516cf3eb 878 }
879 }
516cf3eb 880 }
881 }
882 }
883
d1b7e03d 884/// BACKUP FUNCTIONS ////////////////////////////
516cf3eb 885
d1b7e03d
TH
886 /*
887 * Backup the data in the question
888 *
889 * This is used in question/backuplib.php
890 */
891 public function backup($bf,$preferences,$question,$level=6) {
fe6ce234 892
d1b7e03d
TH
893 $status = true;
894 $extraquestionfields = $this->extra_question_fields();
fe6ce234 895
d1b7e03d
TH
896 if (is_array($extraquestionfields)) {
897 $questionextensiontable = array_shift($extraquestionfields);
898 $record = get_record($questionextensiontable, $this->questionid_column_name(), $question);
899 if ($record) {
900 $tagname = strtoupper($this->name());
901 $status = $status && fwrite($bf, start_tag($tagname, $level, true));
902 foreach ($extraquestionfields as $field) {
903 if (!isset($record->$field)) {
904 echo "No data for field $field when backuping " .
905 $this->name() . ' question id ' . $question;
906 return false;
907 }
908 fwrite($bf, full_tag(strtoupper($field), $level + 1, false, $record->$field));
909 }
910 $status = $status && fwrite($bf, end_tag($tagname, $level, true));
911 }
912 }
fe6ce234 913
d1b7e03d
TH
914 $extraasnwersfields = $this->extra_answer_fields();
915 if (is_array($extraasnwersfields)) {
916 //TODO backup the answers, with any extra data.
fe6ce234 917 } else {
d1b7e03d 918 $status = $status && question_backup_answers($bf, $preferences, $question);
fe6ce234 919 }
d1b7e03d 920 return $status;
fe6ce234
DC
921 }
922
d1b7e03d 923/// RESTORE FUNCTIONS /////////////////
516cf3eb 924
d1b7e03d
TH
925 /*
926 * Restores the data in the question
927 *
928 * This is used in question/restorelib.php
2280e147 929 */
d1b7e03d 930 public function restore($old_question_id,$new_question_id,$info,$restore) {
516cf3eb 931
d1b7e03d
TH
932 $status = true;
933 $extraquestionfields = $this->extra_question_fields();
516cf3eb 934
d1b7e03d
TH
935 if (is_array($extraquestionfields)) {
936 $questionextensiontable = array_shift($extraquestionfields);
937 $tagname = strtoupper($this->name());
938 $recordinfo = $info['#'][$tagname][0];
37a12367 939
d1b7e03d
TH
940 $record = new stdClass;
941 $qidcolname = $this->questionid_column_name();
942 $record->$qidcolname = $new_question_id;
943 foreach ($extraquestionfields as $field) {
944 $record->$field = backup_todb($recordinfo['#'][strtoupper($field)]['0']['#']);
945 }
946 if (!insert_record($questionextensiontable, $record)) {
947 echo "Can't insert record in $questionextensiontable when restoring " .
948 $this->name() . ' question id ' . $question;
949 $status = false;
516cf3eb 950 }
951 }
d1b7e03d
TH
952 //TODO restore extra data in answers
953 return $status;
516cf3eb 954 }
955
d1b7e03d
TH
956 public function restore_map($old_question_id,$new_question_id,$info,$restore) {
957 // There is nothing to decode
516cf3eb 958 return true;
959 }
960
d1b7e03d
TH
961 public function restore_recode_answer($state, $restore) {
962 // There is nothing to decode
963 return $state->answer;
e3fa6587 964 }
965
88bc20c3 966/// IMPORT/EXPORT FUNCTIONS /////////////////
967
968 /*
969 * Imports question from the Moodle XML format
970 *
971 * Imports question using information from extra_question_fields function
972 * If some of you fields contains id's you'll need to reimplement this
973 */
d1b7e03d 974 public function import_from_xml($data, $question, $format, $extra=null) {
88bc20c3 975 $question_type = $data['@']['type'];
976 if ($question_type != $this->name()) {
977 return false;
978 }
979
980 $extraquestionfields = $this->extra_question_fields();
981 if (!is_array($extraquestionfields)) {
982 return false;
983 }
984
985 //omit table name
986 array_shift($extraquestionfields);
987 $qo = $format->import_headers($data);
988 $qo->qtype = $question_type;
989
990 foreach ($extraquestionfields as $field) {
d1b7e03d 991 $qo->$field = addslashes($format->getpath($data, array('#',$field,0,'#'), $qo->$field));
88bc20c3 992 }
993
994 // run through the answers
995 $answers = $data['#']['answer'];
996 $a_count = 0;
997 $extraasnwersfields = $this->extra_answer_fields();
998 if (is_array($extraasnwersfields)) {
999 //TODO import the answers, with any extra data.
1000 } else {
1001 foreach ($answers as $answer) {
1002 $ans = $format->import_answer($answer);
1003 $qo->answer[$a_count] = $ans->answer;
1004 $qo->fraction[$a_count] = $ans->fraction;
1005 $qo->feedback[$a_count] = $ans->feedback;
1006 ++$a_count;
1007 }
1008 }
1009 return $qo;
1010 }
1011
1012 /*
1013 * Export question to the Moodle XML format
1014 *
1015 * Export question using information from extra_question_fields function
1016 * If some of you fields contains id's you'll need to reimplement this
1017 */
d1b7e03d 1018 public function export_to_xml($question, $format, $extra=null) {
88bc20c3 1019 $extraquestionfields = $this->extra_question_fields();
1020 if (!is_array($extraquestionfields)) {
1021 return false;
1022 }
1023
1024 //omit table name
1025 array_shift($extraquestionfields);
1026 $expout='';
1027 foreach ($extraquestionfields as $field) {
2d01a916
TH
1028 $exportedvalue = $question->options->$field;
1029 if (!empty($exportedvalue) && htmlspecialchars($exportedvalue) != $exportedvalue) {
1030 $exportedvalue = '<![CDATA[' . $exportedvalue . ']]>';
1031 }
1032 $expout .= " <$field>{$exportedvalue}</$field>\n";
88bc20c3 1033 }
1034
1035 $extraasnwersfields = $this->extra_answer_fields();
1036 if (is_array($extraasnwersfields)) {
1037 //TODO export answers with any extra data
1038 } else {
1039 foreach ($question->options->answers as $answer) {
1040 $percent = 100 * $answer->fraction;
1041 $expout .= " <answer fraction=\"$percent\">\n";
1042 $expout .= $format->writetext($answer->answer, 3, false);
1043 $expout .= " <feedback>\n";
1044 $expout .= $format->writetext($answer->feedback, 4, false);
1045 $expout .= " </feedback>\n";
1046 $expout .= " </answer>\n";
1047 }
1048 }
1049 return $expout;
1050 }
1051
b9bd6da4 1052 /**
1053 * Abstract function implemented by each question type. It runs all the code
1054 * required to set up and save a question of any type for testing purposes.
1055 * Alternate DB table prefix may be used to facilitate data deletion.
1056 */
d1b7e03d 1057 public function generate_test($name, $courseid=null) {
b9bd6da4 1058 $form = new stdClass();
1059 $form->name = $name;
1060 $form->questiontextformat = 1;
1061 $form->questiontext = 'test question, generated by script';
d1b7e03d
TH
1062 $form->defaultmark = 1;
1063 $form->penalty = 0.3333333;
b9bd6da4 1064 $form->generalfeedback = "Well done";
1065
1066 $context = get_context_instance(CONTEXT_COURSE, $courseid);
1067 $newcategory = question_make_default_categories(array($context));
1068 $form->category = $newcategory->id . ',1';
1069
1070 $question = new stdClass();
1071 $question->courseid = $courseid;
1072 $question->qtype = $this->qtype;
1073 return array($form, $question);
1074 }
d1b7e03d 1075}
aeb15530 1076
cde2709a 1077
d1b7e03d
TH
1078/**
1079 * This class is used in the return value from
1080 * {@link question_type::get_possible_responses()}.
1081 *
1082 * @copyright 2010 The Open University
1083 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1084 */
1085class question_possible_response {
9203b705 1086 /**
d1b7e03d
TH
1087 * @var string the classification of this response the student gave to this
1088 * part of the question. Must match one of the responseclasses returned by
1089 * {@link question_type::get_possible_responses()}.
9203b705 1090 */
d1b7e03d
TH
1091 public $responseclass;
1092 /** @var string the actual response the student gave to this part. */
1093 public $fraction;
9203b705 1094 /**
d1b7e03d
TH
1095 * Constructor, just an easy way to set the fields.
1096 * @param string $responseclassid see the field descriptions above.
1097 * @param string $response see the field descriptions above.
1098 * @param number $fraction see the field descriptions above.
9203b705 1099 */
d1b7e03d
TH
1100 public function __construct($responseclass, $fraction) {
1101 $this->responseclass = $responseclass;
1102 $this->fraction = $fraction;
cde2709a
DC
1103 }
1104
d1b7e03d
TH
1105 public static function no_response() {
1106 return new question_possible_response(get_string('noresponse', 'question'), 0);
cde2709a 1107 }
fe6ce234 1108}