question base type: Fix typo.
[moodle.git] / question / type / questiontype.php
CommitLineData
aeb15530 1<?php
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
8d205441 14require_once($CFG->libdir . '/questionlib.php');
516cf3eb 15
32b0adfa 16/**
17 * This is the base class for Moodle question types.
271ffe3f 18 *
32b0adfa 19 * There are detailed comments on each method, explaining what the method is
20 * for, and the circumstances under which you might need to override it.
271ffe3f 21 *
32b0adfa 22 * Note: the questiontype API should NOT be considered stable yet. Very few
23 * question tyeps have been produced yet, so we do not yet know all the places
271ffe3f 24 * where the current API is insufficient. I would rather learn from the
32b0adfa 25 * experiences of the first few question type implementors, and improve the
26 * interface to meet their needs, rather the freeze the API prematurely and
27 * condem everyone to working round a clunky interface for ever afterwards.
271e6dec 28 *
4323d029 29 * @package questionbank
30 * @subpackage questiontypes
32b0adfa 31 */
af3830ee 32class default_questiontype {
516cf3eb 33
34 /**
a2156789 35 * Name of the question type
36 *
37 * The name returned should coincide with the name of the directory
38 * in which this questiontype is located
271ffe3f 39 *
32b0adfa 40 * @return string the name of this question type.
a2156789 41 */
516cf3eb 42 function name() {
43 return 'default';
44 }
45
a2156789 46 /**
869309b8 47 * Returns a list of other question types that this one requires in order to
48 * work. For example, the calculated question type is a subclass of the
49 * numerical question type, which is a subclass of the shortanswer question
50 * type; and the randomsamatch question type requires the shortanswer type
51 * to be installed.
271ffe3f 52 *
869309b8 53 * @return array any other question types that this one relies on. An empty
54 * array if none.
a2156789 55 */
869309b8 56 function requires_qtypes() {
57 return array();
58 }
59
4995b9c1 60 /**
61 * @return string the name of this pluginfor passing to get_string, set/get_config, etc.
62 */
63 function plugin_name() {
64 return 'qtype_' . $this->name();
65 }
66
869309b8 67 /**
68 * @return string the name of this question type in the user's language.
69 * You should not need to override this method, the default behaviour should be fine.
70 */
71 function local_name() {
4995b9c1 72 return get_string($this->name(), $this->plugin_name());
a2156789 73 }
271ffe3f 74
869309b8 75 /**
76 * The name this question should appear as in the create new question
77 * dropdown. Override this method to return false if you don't want your
78 * question type to be createable, for example if it is an abstract base type,
79 * otherwise, you should not need to override this method.
80 *
81 * @return mixed the desired string, or false to hide this question type in the menu.
82 */
83 function menu_name() {
84 return $this->local_name();
85 }
86
b974f947 87 /**
88 * @return boolean override this to return false if this is not really a
89 * question type, for example the description question type is not
90 * really a question type.
91 */
92 function is_real_question_type() {
93 return true;
94 }
95
a2156789 96 /**
f24493ec 97 * @return boolean true if this question type may require manual grading.
a2156789 98 */
99 function is_manual_graded() {
100 return false;
101 }
102
f24493ec 103 /**
104 * @param object $question a question of this type.
105 * @param string $otherquestionsinuse comma-separate list of other question ids in this attempt.
106 * @return boolean true if a particular instance of this question requires manual grading.
107 */
108 function is_question_manual_graded($question, $otherquestionsinuse) {
109 return $this->is_manual_graded();
110 }
111
869309b8 112 /**
113 * @return boolean true if a table analyzing responses should be shown in
114 * the quiz statistics report. Usually if a question is manually graded
115 * then this analysis table won't be a good idea.
116 */
117 function show_analysis_of_responses() {
118 return !$this->is_manual_graded();
119 }
120
a2156789 121 /**
122 * @return boolean true if this question type can be used by the random question type.
123 */
124 function is_usable_by_random() {
125 return true;
126 }
127
869309b8 128 /**
129 * @param question record.
130 * @param integer subqid this is the id of the subquestion. Usually the id
131 * of the question record of the question record but this is dependent on
132 * the question type. Not relevant to some question types.
133 * @return whether the teacher supplied responses can include wildcards. Can
134 * more than one answer be equivalent to one teacher supplied response.
135 */
136 function has_wildcards_in_responses($question, $subqid) {
137 return false;
138 }
d001dac7 139
c85607f0 140 /**
271e6dec 141 * @return whether the question_answers.answer field needs to have
c85607f0 142 * restore_decode_content_links_worker called on it.
143 */
144 function has_html_answers() {
145 return false;
146 }
147
295043c2 148 /**
149 * If your question type has a table that extends the question table, and
150 * you want the base class to automatically save, backup and restore the extra fields,
151 * override this method to return an array wherer the first element is the table name,
152 * and the subsequent entries are the column names (apart from id and questionid).
153 *
154 * @return mixed array as above, or null to tell the base class to do nothing.
155 */
156 function extra_question_fields() {
157 return null;
158 }
159
d001dac7 160 /**
161 * If you use extra_question_fields, overload this function to return question id field name
162 * in case you table use another name for this column
163 */
164 function questionid_column_name() {
165 return 'questionid';
166 }
167
295043c2 168 /**
169 * If your question type has a table that extends the question_answers table,
170 * make this method return an array wherer the first element is the table name,
171 * and the subsequent entries are the column names (apart from id and answerid).
172 *
173 * @return mixed array as above, or null to tell the base class to do nothing.
174 */
175 function extra_answer_fields() {
176 return null;
177 }
178
36703ed7 179 /**
180 * Return an instance of the question editing form definition. This looks for a
181 * class called edit_{$this->name()}_question_form in the file
4568bf99 182 * {$CFG->dirroot}/question/type/{$this->name()}/edit_{$this->name()}_question_form.php
36703ed7 183 * and if it exists returns an instance of it.
184 *
185 * @param string $submiturl passed on to the constructor call.
186 * @return object an instance of the form definition, or null if one could not be found.
187 */
271e6dec 188 function create_editing_form($submiturl, $question, $category, $contexts, $formeditable) {
36703ed7 189 global $CFG;
190 require_once("{$CFG->dirroot}/question/type/edit_question_form.php");
271ffe3f 191 $definition_file = $CFG->dirroot.'/question/type/'.$this->name().'/edit_'.$this->name().'_form.php';
36703ed7 192 if (!(is_readable($definition_file) && is_file($definition_file))) {
193 return null;
194 }
195 require_once($definition_file);
271ffe3f 196 $classname = 'question_edit_'.$this->name().'_form';
36703ed7 197 if (!class_exists($classname)) {
198 return null;
199 }
271e6dec 200 return new $classname($submiturl, $question, $category, $contexts, $formeditable);
36703ed7 201 }
202
99ba746d 203 /**
204 * @return string the full path of the folder this plugin's files live in.
205 */
206 function plugin_dir() {
207 global $CFG;
208 return $CFG->dirroot . '/question/type/' . $this->name();
209 }
210
211 /**
212 * @return string the URL of the folder this plugin's files live in.
213 */
214 function plugin_baseurl() {
215 global $CFG;
216 return $CFG->wwwroot . '/question/type/' . $this->name();
217 }
218
7b41a4a9 219 /**
220 * This method should be overriden if you want to include a special heading or some other
221 * html on a question editing page besides the question editing form.
222 *
223 * @param question_edit_form $mform a child of question_edit_form
224 * @param object $question
225 * @param string $wizardnow is '' for first page.
226 */
227 function display_question_editing_page(&$mform, $question, $wizardnow){
723d610c 228 global $OUTPUT;
4995b9c1 229 $heading = $this->get_heading(empty($question->id));
1d58b567 230
4bcc5118 231 echo $OUTPUT->heading_with_help($heading, $this->name(), $this->plugin_name());
1d58b567 232
271e6dec 233 $permissionstrs = array();
234 if (!empty($question->id)){
235 if ($question->formoptions->canedit){
236 $permissionstrs[] = get_string('permissionedit', 'question');
237 }
238 if ($question->formoptions->canmove){
239 $permissionstrs[] = get_string('permissionmove', 'question');
240 }
241 if ($question->formoptions->cansaveasnew){
242 $permissionstrs[] = get_string('permissionsaveasnew', 'question');
243 }
244 }
245 if (!$question->formoptions->movecontext && count($permissionstrs)){
04a7ba52 246 echo $OUTPUT->heading(get_string('permissionto', 'question'), 3);
271e6dec 247 $html = '<ul>';
248 foreach ($permissionstrs as $permissionstr){
249 $html .= '<li>'.$permissionstr.'</li>';
250 }
251 $html .= '</ul>';
beb677cd 252 echo $OUTPUT->box($html, 'boxwidthnarrow boxaligncenter generalbox');
271e6dec 253 }
9ab75b2b 254 $mform->display();
255 }
271e6dec 256
9ab75b2b 257 /**
258 * Method called by display_question_editing_page and by question.php to get heading for breadcrumbs.
271e6dec 259 *
9ab75b2b 260 * @return array a string heading and the langmodule in which it was found.
261 */
271e6dec 262 function get_heading($adding = false){
4995b9c1 263 if ($adding) {
264 $prefix = 'adding';
271e6dec 265 } else {
4995b9c1 266 $prefix = 'editing';
c2f8c4be 267 }
4995b9c1 268 return get_string($prefix . $this->name(), $this->plugin_name());
7b41a4a9 269 }
270
516cf3eb 271 /**
24e8b9b6 272 * Saves (creates or updates) a question.
516cf3eb 273 *
274 * Given some question info and some data about the answers
275 * this function parses, organises and saves the question
276 * It is used by {@link question.php} when saving new data from
277 * a form, and also by {@link import.php} when importing questions
278 * This function in turn calls {@link save_question_options}
24e8b9b6 279 * to save question-type specific data.
280 *
281 * Whether we are saving a new question or updating an existing one can be
282 * determined by testing !empty($question->id). If it is not empty, we are updating.
283 *
284 * The question will be saved in category $form->category.
285 *
286 * @param object $question the question object which should be updated. For a new question will be mostly empty.
287 * @param object $form the object containing the information to save, as if from the question editing form.
288 * @param object $course not really used any more.
695d225c 289 * @return object On success, return the new question object. On failure,
271ffe3f 290 * return an object as follows. If the error object has an errors field,
32b0adfa 291 * display that as an error message. Otherwise, the editing form will be
695d225c 292 * redisplayed with validation errors, from validation_errors field, which
24e8b9b6 293 * is itself an object, shown next to the form fields. (I don't think this is accurate any more.)
516cf3eb 294 */
295 function save_question($question, $form, $course) {
9b59580b 296 global $USER, $DB, $OUTPUT;
516cf3eb 297 // This default implementation is suitable for most
298 // question types.
271ffe3f 299
516cf3eb 300 // First, save the basic question itself
24e8b9b6 301 $question->name = trim($form->name);
302 $question->questiontext = trim($form->questiontext);
516cf3eb 303 $question->questiontextformat = $form->questiontextformat;
24e8b9b6 304 $question->parent = isset($form->parent) ? $form->parent : 0;
516cf3eb 305 $question->length = $this->actual_number_of_questions($question);
306 $question->penalty = isset($form->penalty) ? $form->penalty : 0;
307
308 if (empty($form->image)) {
24e8b9b6 309 $question->image = '';
516cf3eb 310 } else {
311 $question->image = $form->image;
312 }
313
a4514d91 314 if (empty($form->generalfeedback)) {
315 $question->generalfeedback = '';
1b8a7434 316 } else {
a4514d91 317 $question->generalfeedback = trim($form->generalfeedback);
1b8a7434 318 }
319
516cf3eb 320 if (empty($question->name)) {
411efaee 321 $question->name = shorten_text(strip_tags($question->questiontext), 15);
516cf3eb 322 if (empty($question->name)) {
323 $question->name = '-';
324 }
325 }
326
327 if ($question->penalty > 1 or $question->penalty < 0) {
328 $question->errors['penalty'] = get_string('invalidpenalty', 'quiz');
329 }
330
331 if (isset($form->defaultgrade)) {
332 $question->defaultgrade = $form->defaultgrade;
333 }
334
24e8b9b6 335 list($question->category) = explode(',', $form->category);
336
337 if (!empty($question->id)) {
338 /// Question already exists, update.
271e6dec 339 $question->modifiedby = $USER->id;
340 $question->timemodified = time();
bb4b6010 341 $DB->update_record('question', $question);
24e8b9b6 342
343 } else {
344 /// New question.
cbe20043 345 // Set the unique code
346 $question->stamp = make_unique_id_code();
271e6dec 347 $question->createdby = $USER->id;
cb14dd63 348 $question->modifiedby = $USER->id;
271e6dec 349 $question->timecreated = time();
cb14dd63 350 $question->timemodified = time();
bb4b6010 351 $question->id = $DB->insert_record('question', $question);
516cf3eb 352 }
353
354 // Now to save all the answers and type-specific options
6459b8fc 355 $form->id = $question->id;
356 $form->qtype = $question->qtype;
516cf3eb 357 $form->category = $question->category;
6459b8fc 358 $form->questiontext = $question->questiontext;
516cf3eb 359
360 $result = $this->save_question_options($form);
361
362 if (!empty($result->error)) {
bd8ee7c1 363 print_error('questionsaveerror', 'question', '', $result->error);
516cf3eb 364 }
365
366 if (!empty($result->notice)) {
367 notice($result->notice, "question.php?id=$question->id");
368 }
369
370 if (!empty($result->noticeyesno)) {
a941387a 371 echo $OUTPUT->confirm($result->noticeyesno, "question.php?id=$question->id&courseid={$course->id}",
372 "edit.php?courseid={$course->id}");
9b59580b 373 echo $OUTPUT->footer();
516cf3eb 374 exit;
375 }
376
cbe20043 377 // Give the question a unique version stamp determined by question_hash()
bb4b6010 378 $DB->set_field('question', 'version', question_hash($question), array('id' => $question->id));
cbe20043 379
516cf3eb 380 return $question;
381 }
271ffe3f 382
516cf3eb 383 /**
384 * Saves question-type specific options
385 *
386 * This is called by {@link save_question()} to save the question-type specific data
387 * @return object $result->error or $result->noticeyesno or $result->notice
388 * @param object $question This holds the information from the editing form,
389 * it is not a standard question object.
390 */
391 function save_question_options($question) {
f34488b2 392 global $DB;
295043c2 393 $extra_question_fields = $this->extra_question_fields();
394
395 if (is_array($extra_question_fields)) {
396 $question_extension_table = array_shift($extra_question_fields);
271e6dec 397
295043c2 398 $function = 'update_record';
d001dac7 399 $questionidcolname = $this->questionid_column_name();
400 $options = $DB->get_record($question_extension_table, array($questionidcolname => $question->id));
295043c2 401 if (!$options) {
402 $function = 'insert_record';
403 $options = new stdClass;
d001dac7 404 $options->$questionidcolname = $question->id;
295043c2 405 }
406 foreach ($extra_question_fields as $field) {
407 if (!isset($question->$field)) {
408 $result = new stdClass;
409 $result->error = "No data for field $field when saving " .
410 $this->name() . ' question id ' . $question->id;
411 return $result;
412 }
413 $options->$field = $question->$field;
414 }
271e6dec 415
f34488b2 416 if (!$DB->{$function}($question_extension_table, $options)) {
295043c2 417 $result = new stdClass;
418 $result->error = 'Could not save question options for ' .
419 $this->name() . ' question id ' . $question->id;
420 return $result;
421 }
422 }
423
424 $extra_answer_fields = $this->extra_answer_fields();
425 // TODO save the answers, with any extra data.
271e6dec 426
4eda4eec 427 return null;
516cf3eb 428 }
429
516cf3eb 430 /**
431 * Loads the question type specific options for the question.
432 *
433 * This function loads any question type specific options for the
434 * question from the database into the question object. This information
435 * is placed in the $question->options field. A question type is
436 * free, however, to decide on a internal structure of the options field.
437 * @return bool Indicates success or failure.
438 * @param object $question The question object for the question. This object
439 * should be updated to include the question type
440 * specific information (it is passed by reference).
441 */
442 function get_question_options(&$question) {
fef8f84e 443 global $CFG, $DB, $OUTPUT;
295043c2 444
516cf3eb 445 if (!isset($question->options)) {
446 $question->options = new object;
447 }
295043c2 448
449 $extra_question_fields = $this->extra_question_fields();
450 if (is_array($extra_question_fields)) {
451 $question_extension_table = array_shift($extra_question_fields);
d001dac7 452 $extra_data = $DB->get_record($question_extension_table, array($this->questionid_column_name() => $question->id), implode(', ', $extra_question_fields));
295043c2 453 if ($extra_data) {
454 foreach ($extra_question_fields as $field) {
455 $question->options->$field = $extra_data->$field;
456 }
457 } else {
fef8f84e 458 echo $OUTPUT->notification("Failed to load question options from the table $question_extension_table for questionid " .
295043c2 459 $question->id);
460 return false;
461 }
462 }
463
464 $extra_answer_fields = $this->extra_answer_fields();
465 if (is_array($extra_answer_fields)) {
466 $answer_extension_table = array_shift($extra_answer_fields);
f34488b2 467 $question->options->answers = $DB->get_records_sql("
468 SELECT qa.*, qax." . implode(', qax.', $extra_answer_fields) . "
469 FROM {question_answers} qa, {$answer_extension_table} qax
470 WHERE qa.questionid = ? AND qax.answerid = qa.id", array($question->id));
295043c2 471 if (!$question->options->answers) {
fef8f84e 472 echo $OUTPUT->notification("Failed to load question answers from the table $answer_extension_table for questionid " .
295043c2 473 $question->id);
474 return false;
475 }
476 } else {
ca9000df 477 // Don't check for success or failure because some question types do not use the answers table.
f34488b2 478 $question->options->answers = $DB->get_records('question_answers', array('question' => $question->id), 'id ASC');
295043c2 479 }
480
516cf3eb 481 return true;
482 }
483
484 /**
0429cd86 485 * Deletes states from the question-type specific tables
486 *
487 * @param string $stateslist Comma separated list of state ids to be deleted
488 */
489 function delete_states($stateslist) {
490 /// The default question type does not have any tables of its own
491 // therefore there is nothing to delete
492
493 return true;
494 }
495
496 /**
497 * Deletes a question from the question-type specific tables
516cf3eb 498 *
499 * @return boolean Success/Failure
500 * @param object $question The question being deleted
501 */
90c3f310 502 function delete_question($questionid) {
f34488b2 503 global $CFG, $DB;
295043c2 504 $success = true;
516cf3eb 505
295043c2 506 $extra_question_fields = $this->extra_question_fields();
507 if (is_array($extra_question_fields)) {
508 $question_extension_table = array_shift($extra_question_fields);
d001dac7 509 $success = $success && $DB->delete_records($question_extension_table,
510 array($this->questionid_column_name() => $questionid));
295043c2 511 }
512
513 $extra_answer_fields = $this->extra_answer_fields();
514 if (is_array($extra_answer_fields)) {
515 $answer_extension_table = array_shift($extra_answer_fields);
f34488b2 516 $success = $success && $DB->delete_records_select($answer_extension_table,
517 "answerid IN (SELECT qa.id FROM {question_answers} qa WHERE qa.question = ?)", array($questionid));
295043c2 518 }
519
f34488b2 520 $success = $success && $DB->delete_records('question_answers', array('question' => $questionid));
295043c2 521
522 return $success;
516cf3eb 523 }
524
525 /**
526 * Returns the number of question numbers which are used by the question
527 *
528 * This function returns the number of question numbers to be assigned
529 * to the question. Most question types will have length one; they will be
dfa47f96 530 * assigned one number. The 'description' type, however does not use up a
516cf3eb 531 * number and so has a length of zero. Other question types may wish to
532 * handle a bundle of questions and hence return a number greater than one.
533 * @return integer The number of question numbers which should be
534 * assigned to the question.
535 * @param object $question The question whose length is to be determined.
536 * Question type specific information is included.
537 */
538 function actual_number_of_questions($question) {
539 // By default, each question is given one number
540 return 1;
541 }
542
543 /**
544 * Creates empty session and response information for the question
545 *
546 * This function is called to start a question session. Empty question type
547 * specific session data (if any) and empty response data will be added to the
548 * state object. Session data is any data which must persist throughout the
549 * attempt possibly with updates as the user interacts with the
550 * question. This function does NOT create new entries in the database for
551 * the session; a call to the {@link save_session_and_responses} member will
552 * occur to do this.
553 * @return bool Indicates success or failure.
554 * @param object $question The question for which the session is to be
555 * created. Question type specific information is
556 * included.
557 * @param object $state The state to create the session for. Note that
558 * this will not have been saved in the database so
559 * there will be no id. This object will be updated
560 * to include the question type specific information
561 * (it is passed by reference). In particular, empty
562 * responses will be created in the ->responses
563 * field.
564 * @param object $cmoptions
565 * @param object $attempt The attempt for which the session is to be
566 * started. Questions may wish to initialize the
567 * session in different ways depending on the user id
568 * or time available for the attempt.
569 */
570 function create_session_and_responses(&$question, &$state, $cmoptions, $attempt) {
571 // The default implementation should work for the legacy question types.
572 // Most question types with only a single form field for the student's response
573 // will use the empty string '' as the index for that one response. This will
574 // automatically be stored in and restored from the answer field in the
4f48fb42 575 // question_states table.
5a14d563 576 $state->responses = array(
577 '' => '',
578 );
516cf3eb 579 return true;
580 }
581
582 /**
583 * Restores the session data and most recent responses for the given state
584 *
585 * This function loads any session data associated with the question
586 * session in the given state from the database into the state object.
587 * In particular it loads the responses that have been saved for the given
588 * state into the ->responses member of the state object.
589 *
590 * Question types with only a single form field for the student's response
591 * will not need not restore the responses; the value of the answer
4f48fb42 592 * field in the question_states table is restored to ->responses['']
516cf3eb 593 * before this function is called. Question types with more response fields
594 * should override this method and set the ->responses field to an
595 * associative array of responses.
596 * @return bool Indicates success or failure.
597 * @param object $question The question object for the question including any
598 * question type specific information.
599 * @param object $state The saved state to load the session for. This
600 * object should be updated to include the question
601 * type specific session information and responses
602 * (it is passed by reference).
603 */
604 function restore_session_and_responses(&$question, &$state) {
605 // The default implementation does nothing (successfully)
606 return true;
607 }
608
609 /**
610 * Saves the session data and responses for the given question and state
611 *
612 * This function saves the question type specific session data from the
613 * state object to the database. In particular for most question types it saves the
614 * responses from the ->responses member of the state object. The question type
4f48fb42 615 * non-specific data for the state has already been saved in the question_states
516cf3eb 616 * table and the state object contains the corresponding id and
617 * sequence number which may be used to index a question type specific table.
618 *
619 * Question types with only a single form field for the student's response
620 * which is contained in ->responses[''] will not have to save this response,
4f48fb42 621 * it will already have been saved to the answer field of the question_states table.
694061ad 622 * Question types with more response fields should override this method to convert
f34488b2 623 * the data the ->responses array into a single string field, and save it in the
694061ad 624 * database. The implementation in the multichoice question type is a good model to follow.
625 * http://cvs.moodle.org/contrib/plugins/question/type/opaque/questiontype.php?view=markup
626 * has a solution that is probably quite generally applicable.
516cf3eb 627 * @return bool Indicates success or failure.
628 * @param object $question The question object for the question including
629 * the question type specific information.
630 * @param object $state The state for which the question type specific
631 * data and responses should be saved.
632 */
633 function save_session_and_responses(&$question, &$state) {
634 // The default implementation does nothing (successfully)
635 return true;
636 }
637
638 /**
639 * Returns an array of values which will give full marks if graded as
640 * the $state->responses field
641 *
642 * The correct answer to the question in the given state, or an example of
643 * a correct answer if there are many, is returned. This is used by some question
644 * types in the {@link grade_responses()} function but it is also used by the
645 * question preview screen to fill in correct responses.
646 * @return mixed A response array giving the responses corresponding
647 * to the (or a) correct answer to the question. If there is
648 * no correct answer that scores 100% then null is returned.
649 * @param object $question The question for which the correct answer is to
650 * be retrieved. Question type specific information is
651 * available.
652 * @param object $state The state of the question, for which a correct answer is
653 * needed. Question type specific information is included.
654 */
655 function get_correct_responses(&$question, &$state) {
656 /* The default implementation returns the response for the first answer
657 that gives full marks. */
4eda4eec 658 if ($question->options->answers) {
659 foreach ($question->options->answers as $answer) {
660 if (((int) $answer->fraction) === 1) {
294ce987 661 return array('' => $answer->answer);
4eda4eec 662 }
516cf3eb 663 }
664 }
665 return null;
666 }
667
668 /**
669 * Return an array of values with the texts for all possible responses stored
670 * for the question
671 *
672 * All answers are found and their text values isolated
673 * @return object A mixed object
674 * ->id question id. Needed to manage random questions:
675 * it's the id of the actual question presented to user in a given attempt
676 * ->responses An array of values giving the responses corresponding
677 * to all answers to the question. Answer ids are used as keys.
678 * The text and partial credit are the object components
679 * @param object $question The question for which the answers are to
680 * be retrieved. Question type specific information is
681 * available.
682 */
683 // ULPGC ecastro
684 function get_all_responses(&$question, &$state) {
7baff8aa 685 if (isset($question->options->answers) && is_array($question->options->answers)) {
686 $answers = array();
516cf3eb 687 foreach ($question->options->answers as $aid=>$answer) {
7baff8aa 688 $r = new stdClass;
516cf3eb 689 $r->answer = $answer->answer;
690 $r->credit = $answer->fraction;
691 $answers[$aid] = $r;
692 }
7baff8aa 693 $result = new stdClass;
694 $result->id = $question->id;
695 $result->responses = $answers;
696 return $result;
516cf3eb 697 } else {
7baff8aa 698 return null;
516cf3eb 699 }
516cf3eb 700 }
869309b8 701 /**
702 * The difference between this method an get_all_responses is that this
703 * method is not passed a state object. It is the possible answers to a
704 * question no matter what the state.
705 * This method is not called for random questions.
706 * @return array of possible answers.
707 */
708 function get_possible_responses(&$question) {
709 static $responses = array();
710 if (!isset($responses[$question->id])){
711 $responses[$question->id] = $this->get_all_responses($question, new object());
712 }
713 return array($question->id => $responses[$question->id]->responses);
714 }
beb677cd 715
6f51ed72 716 /**
717 * @param object $question
455c3efa 718 * @return mixed either a integer score out of 1 that the average random
719 * guess by a student might give or an empty string which means will not
720 * calculate.
6f51ed72 721 */
722 function get_random_guess_score($question) {
723 return 0;
724 }
2280e147 725 /**
516cf3eb 726 * Return the actual response to the question in a given state
2280e147 727 * for the question. Text is not yet formatted for output.
516cf3eb 728 *
729 * @return mixed An array containing the response or reponses (multiple answer, match)
730 * given by the user in a particular attempt.
731 * @param object $question The question for which the correct answer is to
732 * be retrieved. Question type specific information is
733 * available.
734 * @param object $state The state object that corresponds to the question,
735 * for which a correct answer is needed. Question
736 * type specific information is included.
737 */
738 // ULPGC ecastro
8cc274de 739 function get_actual_response($question, $state) {
8cc274de 740 if (!empty($state->responses)) {
294ce987 741 $responses[] = $state->responses[''];
8cc274de 742 } else {
743 $responses[] = '';
744 }
745 return $responses;
516cf3eb 746 }
747
869309b8 748 function get_actual_response_details($question, $state) {
749 $response = array_shift($this->get_actual_response($question, $state));
750 $teacherresponses = $this->get_possible_responses($question, $state);
751 //only one response
752 list($tsubqid, $tresponses) = each($teacherresponses);
753 $responsedetail = new object();
754 $responsedetail->subqid = $tsubqid;
755 $responsedetail->response = $response;
756 if ($aid = $this->check_response($question, $state)){
757 $responsedetail->aid = $aid;
758 } else {
759 foreach ($tresponses as $aid => $tresponse){
760 if ($tresponse->answer == $response){
761 $responsedetail->aid = $aid;
762 break;
763 }
764 }
765 }
766 if (isset($responsedetail->aid)){
767 $responsedetail->credit = $tresponses[$aid]->credit;
768 } else {
769 $responsedetail->aid = 0;
770 $responsedetail->credit = 0;
771 }
772 return array($responsedetail);
773 }
774
516cf3eb 775 // ULPGC ecastro
776 function get_fractional_grade(&$question, &$state) {
516cf3eb 777 $grade = $state->grade;
26da840f 778 if ($question->maxgrade > 0) {
779 return (float)($grade / $question->maxgrade);
516cf3eb 780 } else {
781 return (float)$grade;
782 }
783 }
784
785
786 /**
787 * Checks if the response given is correct and returns the id
788 *
789 * @return int The ide number for the stored answer that matches the response
790 * given by the user in a particular attempt.
791 * @param object $question The question for which the correct answer is to
792 * be retrieved. Question type specific information is
793 * available.
794 * @param object $state The state object that corresponds to the question,
795 * for which a correct answer is needed. Question
796 * type specific information is included.
797 */
798 // ULPGC ecastro
799 function check_response(&$question, &$state){
800 return false;
801 }
802
5fceb049 803 // Used by the following function, so that it only returns results once per quiz page.
50da63eb 804 private $htmlheadalreadydone = false;
516cf3eb 805 /**
25ddb7ef 806 * Hook to allow question types to include required JavaScrip or CSS on pages
807 * where they are going to be printed.
271e6dec 808 *
25ddb7ef 809 * If this question type requires extra CSS or JavaScript to function,
810 * then this method, which will be called before print_header on any page
811 * where this question is going to be printed, is a chance to call
812 * $PAGE->requires->js, $PAGE->requiers->css, and so on.
50da63eb 813 *
99ba746d 814 * The two parameters match the first two parameters of print_question.
271e6dec 815 *
99ba746d 816 * @param object $question The question object.
817 * @param object $state The state object.
271e6dec 818 *
25ddb7ef 819 * @return array Deprecated. An array of bits of HTML to add to the head of
820 * pages where this question is print_question-ed in the body. The array
821 * should use integer array keys, which have no significance.
99ba746d 822 */
823 function get_html_head_contributions(&$question, &$state) {
50da63eb 824 // We only do this once for this question type, no matter how often this
825 // method is called on one page.
826 if ($this->htmlheadalreadydone) {
827 return array();
828 }
829 $this->htmlheadalreadydone = true;
830
99ba746d 831 // By default, we link to any of the files styles.css, styles.php,
832 // script.js or script.php that exist in the plugin folder.
833 // Core question types should not use this mechanism. Their styles
834 // should be included in the standard theme.
25ddb7ef 835 $this->find_standard_scripts_and_css();
50da63eb 836 }
5fceb049 837
50da63eb 838 /**
839 * Like @see{get_html_head_contributions}, but this method is for CSS and
840 * JavaScript required on the question editing page question/question.php.
841 *
842 * @return an array of bits of HTML to add to the head of pages where
843 * this question is print_question-ed in the body. The array should use
844 * integer array keys, which have no significance.
845 */
846 function get_editing_head_contributions() {
847 // By default, we link to any of the files styles.css, styles.php,
848 // script.js or script.php that exist in the plugin folder.
849 // Core question types should not use this mechanism. Their styles
850 // should be included in the standard theme.
25ddb7ef 851 $this->find_standard_scripts_and_css();
50da63eb 852 }
271e6dec 853
50da63eb 854 /**
855 * Utility method used by @see{get_html_head_contributions} and
856 * @see{get_editing_head_contributions}. This looks for any of the files
857 * styles.css, styles.php, script.js or script.php that exist in the plugin
858 * folder and ensures they get included.
859 *
860 * @return array as required by get_html_head_contributions or get_editing_head_contributions.
861 */
862 protected function find_standard_scripts_and_css() {
25ddb7ef 863 global $PAGE;
864
99ba746d 865 $plugindir = $this->plugin_dir();
25ddb7ef 866 $plugindirrel = 'question/type/' . $this->name();
50da63eb 867
868 if (file_exists($plugindir . '/script.js')) {
9dec75db 869 $PAGE->requires->js('/' . $plugindirrel . '/script.js');
50da63eb 870 }
871 if (file_exists($plugindir . '/script.php')) {
9dec75db 872 $PAGE->requires->js('/' . $plugindirrel . '/script.php');
50da63eb 873 }
99ba746d 874 }
271e6dec 875
99ba746d 876 /**
877 * Prints the question including the number, grading details, content,
878 * feedback and interactions
879 *
880 * This function prints the question including the question number,
881 * grading details, content for the question, any feedback for the previously
882 * submitted responses and the interactions. The default implementation calls
883 * various other methods to print each of these parts and most question types
884 * will just override those methods.
885 * @param object $question The question to be rendered. Question type
886 * specific information is included. The
887 * maximum possible grade is in ->maxgrade. The name
888 * prefix for any named elements is in ->name_prefix.
889 * @param object $state The state to render the question in. The grading
890 * information is in ->grade, ->raw_grade and
891 * ->penalty. The current responses are in
892 * ->responses. This is an associative array (or the
893 * empty string or null in the case of no responses
894 * submitted). The last graded state is in
895 * ->last_graded (hence the most recently graded
896 * responses are in ->last_graded->responses). The
897 * question type specific information is also
898 * included.
899 * @param integer $number The number for this question.
900 * @param object $cmoptions
901 * @param object $options An object describing the rendering options.
902 */
516cf3eb 903 function print_question(&$question, &$state, $number, $cmoptions, $options) {
904 /* The default implementation should work for most question types
905 provided the member functions it calls are overridden where required.
37a12367 906 The layout is determined by the template question.html */
271ffe3f 907
80e7bb44 908 global $CFG, $OUTPUT;
1b8a7434 909 $isgraded = question_state_is_graded($state->last_graded);
516cf3eb 910
aafdb447 911 if (isset($question->randomquestionid)) {
912 $actualquestionid = $question->randomquestionid;
913 } else {
914 $actualquestionid = $question->id;
516cf3eb 915 }
916
aafdb447 917 // For editing teachers print a link to an editing popup window
918 $editlink = $this->get_question_edit_link($question, $cmoptions, $options);
919
a4514d91 920 $generalfeedback = '';
295043c2 921 if ($isgraded && $options->generalfeedback) {
a4514d91 922 $generalfeedback = $this->format_text($question->generalfeedback,
1b8a7434 923 $question->questiontextformat, $cmoptions);
924 }
925
516cf3eb 926 $grade = '';
26da840f 927 if ($question->maxgrade > 0 && $options->scores) {
6b11a0e8 928 if ($cmoptions->optionflags & QUESTION_ADAPTIVE) {
f88fb62c 929 if ($isgraded) {
930 $grade = question_format_grade($cmoptions, $state->last_graded->grade).'/';
931 } else {
beb677cd 932 $grade = '--/';
f88fb62c 933 }
516cf3eb 934 }
f9a2cf86 935 $grade .= question_format_grade($cmoptions, $question->maxgrade);
516cf3eb 936 }
271ffe3f 937
5ffd1421
TH
938 $formatoptions = new stdClass;
939 $formatoptions->para = false;
940 $comment = format_text($state->manualcomment, FORMAT_HTML,
941 $formatoptions, $cmoptions->course);
b6e907a2 942 $commentlink = '';
271ffe3f 943
aafdb447 944 if (!empty($options->questioncommentlink)) {
b6e907a2 945 $strcomment = get_string('commentorgrade', 'quiz');
80e7bb44 946
9bf16314 947 $link = new moodle_url("$options->questioncommentlink?attempt=$state->attempt&question=$actualquestionid");
4ba32cf1 948 $action = new popup_action('click', $link, 'commentquestion', array('height' => 480, 'width' => 750));
9bf16314 949 $commentlink = $OUTPUT->container($OUTPUT->action_link($link, $strcomment, $action), 'commentlink');
b6e907a2 950 }
516cf3eb 951
fe9b5cfd 952 $history = $this->history($question, $state, $number, $cmoptions, $options);
953
aaae75b0 954 include "$CFG->dirroot/question/type/question.html";
fe9b5cfd 955 }
956
62e76c67 957 /**
958 * Render the question flag, assuming $flagsoption allows it. You will probably
959 * never need to override this method.
960 *
961 * @param object $question the question
962 * @param object $state its current state
963 * @param integer $flagsoption the option that says whether flags should be displayed.
964 */
965 protected function print_question_flag($question, $state, $flagsoption) {
cf615522 966 global $CFG, $PAGE;
62e76c67 967 switch ($flagsoption) {
968 case QUESTION_FLAGSSHOWN:
969 $flagcontent = $this->get_question_flag_tag($state->flagged);
970 break;
971 case QUESTION_FLAGSEDITABLE:
972 $id = $question->name_prefix . '_flagged';
973 if ($state->flagged) {
974 $checked = 'checked="checked" ';
975 } else {
976 $checked = '';
977 }
978 $qsid = $state->questionsessionid;
979 $aid = $state->attempt;
980 $qid = $state->question;
981 $checksum = question_get_toggleflag_checksum($aid, $qid, $qsid);
cf615522 982 $postdata = "qsid=$qsid&aid=$aid&qid=$qid&checksum=$checksum&sesskey=" . sesskey();
62e76c67 983 $flagcontent = '<input type="checkbox" id="' . $id . '" name="' . $id .
beb677cd 984 '" value="1" ' . $checked . ' />' .
d0dfe949 985 '<label id="' . $id . 'label" for="' . $id . '">' . $this->get_question_flag_tag(
cf615522 986 $state->flagged, $id . 'img') . '</label>' . "\n";
987 $PAGE->requires->js_function_call('question_flag_changer.init_flag', array($id, $postdata));
62e76c67 988 break;
989 default:
990 $flagcontent = '';
991 }
992 if ($flagcontent) {
993 echo '<div class="questionflag">' . $flagcontent . "</div>\n";
994 }
995 }
996
997 /**
998 * Work out the actual img tag needed for the flag
999 *
1000 * @param boolean $flagged whether the question is currently flagged.
beb677cd 1001 * @param string $id an id to be added as an attribute to the img (optional).
62e76c67 1002 * @return string the img tag.
1003 */
1004 protected function get_question_flag_tag($flagged, $id = '') {
ddedf979 1005 global $OUTPUT;
62e76c67 1006 if ($id) {
1007 $id = 'id="' . $id . '" ';
1008 }
1009 if ($flagged) {
ddedf979 1010 $img = 'i/flagged';
62e76c67 1011 } else {
ddedf979 1012 $img = 'i/unflagged';
62e76c67 1013 }
b5d0cafc 1014 return '<img ' . $id . 'src="' . $OUTPUT->pix_url($img) .
62e76c67 1015 '" alt="' . get_string('flagthisquestion', 'question') . '" />';
1016 }
1017
aafdb447 1018 /**
1019 * Get a link to an edit icon for this question, if the current user is allowed
1020 * to edit it.
1021 *
1022 * @param object $question the question object.
1023 * @param object $cmoptions the options from the module. If $cmoptions->thispageurl is set
1024 * then the link will be to edit the question in this browser window, then return to
1025 * $cmoptions->thispageurl. Otherwise the link will be to edit in a popup.
1026 * @return string the HTML of the link, or nothing it the currenty user is not allowed to edit.
1027 */
1028 function get_question_edit_link($question, $cmoptions, $options) {
f2a1963c 1029 global $CFG, $OUTPUT;
aafdb447 1030
1031 /// Is this user allowed to edit this question?
1032 if (!empty($options->noeditlink) || !question_has_capability_on($question, 'edit')) {
1033 return '';
1034 }
1035
1036 /// Work out the right URL.
1037 $linkurl = '/question/question.php?id=' . $question->id;
a18ba12c 1038 if (!empty($cmoptions->cmid)) {
1039 $linkurl .= '&amp;cmid=' . $cmoptions->cmid;
aafdb447 1040 } else if (!empty($cmoptions->course)) {
1041 $linkurl .= '&amp;courseid=' . $cmoptions->course;
1042 } else {
60e40dda 1043 print_error('missingcourseorcmidtolink', 'question');
aafdb447 1044 }
1045
1046 /// Work out the contents of the link.
1047 $stredit = get_string('edit');
b5d0cafc 1048 $linktext = '<img src="' . $OUTPUT->pix_url('t/edit') . '" alt="' . $stredit . '" />';
aafdb447 1049
1050 if (!empty($cmoptions->thispageurl)) {
1051 /// The module allow editing in the same window, print an ordinary link.
6224d7a1
TH
1052 return '<a href="' . $CFG->wwwroot . $linkurl . '&amp;returnurl=' .
1053 urlencode($cmoptions->thispageurl . '#q' . $question->id) .
aafdb447 1054 '" title="' . $stredit . '">' . $linktext . '</a>';
1055 } else {
1056 /// We have to edit in a pop-up.
9bf16314
PS
1057 $link = new moodle_url($linkurl . '&inpopup=1');
1058 $action = new popup_action('click', $link, 'editquestion');
1059 return $OUTPUT->action_link($link, $linktext, $action ,array('title'=>$stredit));
aafdb447 1060 }
1061 }
1062
62e76c67 1063 /**
fe9b5cfd 1064 * Print history of responses
1065 *
1066 * Used by print_question()
1067 */
1068 function history($question, $state, $number, $cmoptions, $options) {
a941387a 1069 global $DB, $OUTPUT;
5c86dc7c
TH
1070
1071 if (empty($options->history)) {
1072 return '';
1073 }
1074
1075 $params = array('aid' => $state->attempt);
1076 if (isset($question->randomquestionid)) {
1077 $params['qid'] = $question->randomquestionid;
e14de5aa 1078 $randomprefix = 'random' . $question->id . '-';
5c86dc7c
TH
1079 } else {
1080 $params['qid'] = $question->id;
e14de5aa 1081 $randomprefix = '';
5c86dc7c
TH
1082 }
1083 if ($options->history == 'all') {
1084 $eventtest = 'event > 0';
1085 } else {
1086 $eventtest = 'event IN (' . QUESTION_EVENTS_GRADED . ')';
1087 }
2f67a9b3 1088 $states = $DB->get_records_select('question_states',
5c86dc7c 1089 'attempt = :aid AND question = :qid AND ' . $eventtest, $params, 'seq_number ASC');
e14de5aa 1090 if (count($states) <= 1) {
5c86dc7c
TH
1091 return '';
1092 }
1093
1094 $strreviewquestion = get_string('reviewresponse', 'quiz');
1095 $table = new html_table();
1096 $table->width = '100%';
1097 $table->head = array (
1098 get_string('numberabbr', 'quiz'),
1099 get_string('action', 'quiz'),
1100 get_string('response', 'quiz'),
1101 get_string('time'),
1102 );
1103 if ($options->scores) {
1104 $table->head[] = get_string('score', 'quiz');
1105 $table->head[] = get_string('grade', 'quiz');
1106 }
1107
1108 foreach ($states as $st) {
e14de5aa
TH
1109 if ($randomprefix && strpos($st->answer, $randomprefix) === 0) {
1110 $st->answer = substr($st->answer, strlen($randomprefix));
1111 }
5c86dc7c
TH
1112 $st->responses[''] = $st->answer;
1113 $this->restore_session_and_responses($question, $st);
1114
1115 if ($state->id == $st->id) {
1116 $link = '<b>' . $st->seq_number . '</b>';
1117 } else if (isset($options->questionreviewlink)) {
a5d96f93
SH
1118 $reviewlink = new moodle_url($options->questionreviewlink);
1119 $reviewlink->params(array('state'=>$st->id,'question'=>$question->id));
9bf16314
PS
1120 $link = new moodle_url($reviewlink);
1121 $action = new popup_action('click', $link, 'reviewquestion', array('height' => 450, 'width' => 650));
1122 $link = $OUTPUT->action_link($link, $st->seq_number, $action, array('title'=>$strreviewquestion));
516cf3eb 1123 } else {
5c86dc7c 1124 $link = $st->seq_number;
516cf3eb 1125 }
271ffe3f 1126
5c86dc7c
TH
1127 if ($state->id == $st->id) {
1128 $b = '<b>';
1129 $be = '</b>';
1130 } else {
1131 $b = '';
1132 $be = '';
516cf3eb 1133 }
5c86dc7c
TH
1134
1135 $data = array (
1136 $link,
1137 $b.get_string('event'.$st->event, 'quiz').$be,
1138 $b.$this->response_summary($question, $st).$be,
1139 $b.userdate($st->timestamp, get_string('timestr', 'quiz')).$be,
1140 );
1141 if ($options->scores) {
1142 $data[] = $b.question_format_grade($cmoptions, $st->raw_grade).$be;
1143 $data[] = $b.question_format_grade($cmoptions, $st->raw_grade).$be;
1144 }
1145 $table->data[] = $data;
516cf3eb 1146 }
16be8974 1147 return html_writer::table($table);
516cf3eb 1148 }
1149
516cf3eb 1150 /**
1151 * Prints the score obtained and maximum score available plus any penalty
1152 * information
1153 *
1154 * This function prints a summary of the scoring in the most recently
1155 * graded state (the question may not have been submitted for marking at
1156 * the current state). The default implementation should be suitable for most
1157 * question types.
1158 * @param object $question The question for which the grading details are
1159 * to be rendered. Question type specific information
1160 * is included. The maximum possible grade is in
1161 * ->maxgrade.
1162 * @param object $state The state. In particular the grading information
1163 * is in ->grade, ->raw_grade and ->penalty.
1164 * @param object $cmoptions
1165 * @param object $options An object describing the rendering options.
1166 */
1167 function print_question_grading_details(&$question, &$state, $cmoptions, $options) {
1168 /* The default implementation prints the number of marks if no attempt
1169 has been made. Otherwise it displays the grade obtained out of the
1170 maximum grade available and a warning if a penalty was applied for the
1171 attempt and displays the overall grade obtained counting all previous
1172 responses (and penalties) */
1173
f30bbcaf 1174 if (QUESTION_EVENTDUPLICATE == $state->event) {
516cf3eb 1175 echo ' ';
1176 print_string('duplicateresponse', 'quiz');
1177 }
26da840f 1178 if ($question->maxgrade > 0 && $options->scores) {
f30bbcaf 1179 if (question_state_is_graded($state->last_graded)) {
516cf3eb 1180 // Display the grading details from the last graded state
1b8a7434 1181 $grade = new stdClass;
f88fb62c 1182 $grade->cur = question_format_grade($cmoptions, $state->last_graded->grade);
f9a2cf86 1183 $grade->max = question_format_grade($cmoptions, $question->maxgrade);
f88fb62c 1184 $grade->raw = question_format_grade($cmoptions, $state->last_graded->raw_grade);
516cf3eb 1185
1186 // let student know wether the answer was correct
beb677cd 1187 $class = question_get_feedback_class($state->last_graded->raw_grade /
b10c38a3 1188 $question->maxgrade);
1189 echo '<div class="correctness ' . $class . '">' . get_string($class, 'quiz') . '</div>';
516cf3eb 1190
1191 echo '<div class="gradingdetails">';
1192 // print grade for this submission
1193 print_string('gradingdetails', 'quiz', $grade);
1194 if ($cmoptions->penaltyscheme) {
1195 // print details of grade adjustment due to penalties
1196 if ($state->last_graded->raw_grade > $state->last_graded->grade){
2962ad61 1197 echo ' ';
516cf3eb 1198 print_string('gradingdetailsadjustment', 'quiz', $grade);
1199 }
1200 // print info about new penalty
1201 // penalty is relevant only if the answer is not correct and further attempts are possible
c7a99084 1202 if (($state->last_graded->raw_grade < $question->maxgrade / 1.01)
5995f17f 1203 and (QUESTION_EVENTCLOSEANDGRADE != $state->event)) {
c7a99084 1204
516cf3eb 1205 if ('' !== $state->last_graded->penalty && ((float)$state->last_graded->penalty) > 0.0) {
1206 // A penalty was applied so display it
2962ad61 1207 echo ' ';
f9a2cf86 1208 print_string('gradingdetailspenalty', 'quiz', question_format_grade($cmoptions, $state->last_graded->penalty));
516cf3eb 1209 } else {
1210 /* No penalty was applied even though the answer was
1211 not correct (eg. a syntax error) so tell the student
1212 that they were not penalised for the attempt */
2962ad61 1213 echo ' ';
516cf3eb 1214 print_string('gradingdetailszeropenalty', 'quiz');
1215 }
1216 }
1217 }
1218 echo '</div>';
1219 }
1220 }
1221 }
1222
1223 /**
1224 * Prints the main content of the question including any interactions
1225 *
1226 * This function prints the main content of the question including the
1227 * interactions for the question in the state given. The last graded responses
1228 * are printed or indicated and the current responses are selected or filled in.
1229 * Any names (eg. for any form elements) are prefixed with $question->name_prefix.
1230 * This method is called from the print_question method.
1231 * @param object $question The question to be rendered. Question type
1232 * specific information is included. The name
1233 * prefix for any named elements is in ->name_prefix.
1234 * @param object $state The state to render the question in. The grading
1235 * information is in ->grade, ->raw_grade and
1236 * ->penalty. The current responses are in
1237 * ->responses. This is an associative array (or the
1238 * empty string or null in the case of no responses
1239 * submitted). The last graded state is in
1240 * ->last_graded (hence the most recently graded
1241 * responses are in ->last_graded->responses). The
1242 * question type specific information is also
1243 * included.
1244 * The state is passed by reference because some adaptive
1245 * questions may want to update it during rendering
1246 * @param object $cmoptions
1247 * @param object $options An object describing the rendering options.
1248 */
1249 function print_question_formulation_and_controls(&$question, &$state, $cmoptions, $options) {
1250 /* This default implementation prints an error and must be overridden
1251 by all question type implementations, unless the default implementation
1252 of print_question has been overridden. */
fef8f84e 1253 global $OUTPUT;
1254 echo $OUTPUT->notification('Error: Question formulation and input controls has not'
516cf3eb 1255 .' been implemented for question type '.$this->name());
1256 }
1257
1258 /**
1259 * Prints the submit button(s) for the question in the given state
1260 *
1261 * This function prints the submit button(s) for the question in the
1262 * given state. The name of any button created will be prefixed with the
1263 * unique prefix for the question in $question->name_prefix. The suffix
4dca7e51 1264 * 'submit' is reserved for the single question submit button and the suffix
516cf3eb 1265 * 'validate' is reserved for the single question validate button (for
1266 * question types which support it). Other suffixes will result in a response
1267 * of that name in $state->responses which the printing and grading methods
1268 * can then use.
1269 * @param object $question The question for which the submit button(s) are to
1270 * be rendered. Question type specific information is
1271 * included. The name prefix for any
1272 * named elements is in ->name_prefix.
1273 * @param object $state The state to render the buttons for. The
1274 * question type specific information is also
1275 * included.
1276 * @param object $cmoptions
1277 * @param object $options An object describing the rendering options.
1278 */
1279 function print_question_submit_buttons(&$question, &$state, $cmoptions, $options) {
9f9eec1e 1280 // The default implementation should be suitable for most question types.
1281 // It prints a mark button in the case where individual marking is allowed.
6b11a0e8 1282 if (($cmoptions->optionflags & QUESTION_ADAPTIVE) and !$options->readonly) {
3d3867b6 1283 echo '<input type="submit" name="', $question->name_prefix, 'submit" value="',
9f9eec1e 1284 get_string('mark', 'quiz'), '" class="submit btn" />';
516cf3eb 1285 }
1286 }
1287
516cf3eb 1288 /**
1289 * Return a summary of the student response
1290 *
1291 * This function returns a short string of no more than a given length that
1292 * summarizes the student's response in the given $state. This is used for
2280e147 1293 * example in the response history table. This string should already be
1294 * formatted for output.
516cf3eb 1295 * @return string The summary of the student response
271ffe3f 1296 * @param object $question
516cf3eb 1297 * @param object $state The state whose responses are to be summarized
1298 * @param int $length The maximum length of the returned string
1299 */
e65701ee 1300 function response_summary($question, $state, $length = 80, $formatting = true) {
516cf3eb 1301 // This should almost certainly be overridden
879caa51 1302 $responses = $this->get_actual_response($question, $state);
2280e147 1303 if ($formatting){
1304 $responses = $this->format_responses($responses, $question->questiontextformat);
879caa51 1305 }
e65701ee 1306 $responses = implode('; ', $responses);
21a4ca7d 1307 return shorten_text($responses, $length);
516cf3eb 1308 }
2280e147 1309 /**
1310 * @param array responses is an array of responses.
1311 * @return formatted responses
1312 */
1313 function format_responses($responses, $format){
1314 $toreturn = array();
1315 foreach ($responses as $response){
1316 $toreturn[] = $this->format_response($response, $format);
1317 }
1318 return $toreturn;
1319 }
1320 /**
1321 * @param string response is a response.
1322 * @return formatted response
1323 */
1324 function format_response($response, $format){
1325 return s($response);
1326 }
516cf3eb 1327 /**
1328 * Renders the question for printing and returns the LaTeX source produced
1329 *
1330 * This function should render the question suitable for a printed problem
1331 * or solution sheet in LaTeX and return the rendered output.
1332 * @return string The LaTeX output.
1333 * @param object $question The question to be rendered. Question type
1334 * specific information is included.
1335 * @param object $state The state to render the question in. The
1336 * question type specific information is also
1337 * included.
1338 * @param object $cmoptions
1339 * @param string $type Indicates if the question or the solution is to be
1340 * rendered with the values 'question' and
1341 * 'solution'.
1342 */
1343 function get_texsource(&$question, &$state, $cmoptions, $type) {
1344 // The default implementation simply returns a string stating that
1345 // the question is only available online.
1346
1347 return get_string('onlineonly', 'texsheet');
1348 }
1349
1350 /**
1351 * Compares two question states for equivalence of the student's responses
1352 *
1353 * The responses for the two states must be examined to see if they represent
1354 * equivalent answers to the question by the student. This method will be
1355 * invoked for each of the previous states of the question before grading
1356 * occurs. If the student is found to have already attempted the question
1357 * with equivalent responses then the attempt at the question is ignored;
1358 * grading does not occur and the state does not change. Thus they are not
1359 * penalized for this case.
1360 * @return boolean
1361 * @param object $question The question for which the states are to be
1362 * compared. Question type specific information is
1363 * included.
1364 * @param object $state The state of the question. The responses are in
ac249da5 1365 * ->responses. This is the only field of $state
1366 * that it is safe to use.
516cf3eb 1367 * @param object $teststate The state whose responses are to be
1368 * compared. The state will be of the same age or
271ffe3f 1369 * older than $state. If possible, the method should
bb080d20 1370 * only use the field $teststate->responses, however
1371 * any field that is set up by restore_session_and_responses
1372 * can be used.
516cf3eb 1373 */
1374 function compare_responses(&$question, $state, $teststate) {
1375 // The default implementation performs a comparison of the response
1376 // arrays. The ordering of the arrays does not matter.
1377 // Question types may wish to override this (eg. to ignore trailing
1378 // white space or to make "7.0" and "7" compare equal).
271ffe3f 1379
e7e62d45 1380 // In php neither == nor === compare arrays the way you want. The following
ceeae340 1381 // ensures that the arrays have the same keys, with the same values.
1382 $result = false;
1383 $diff1 = array_diff_assoc($state->responses, $teststate->responses);
1384 if (empty($diff1)) {
1385 $diff2 = array_diff_assoc($teststate->responses, $state->responses);
1386 $result = empty($diff2);
1387 }
1388
1389 return $result;
516cf3eb 1390 }
1391
37a12367 1392 /**
1393 * Checks whether a response matches a given answer
1394 *
1395 * This method only applies to questions that use teacher-defined answers
1396 *
1397 * @return boolean
1398 */
1399 function test_response(&$question, &$state, $answer) {
1400 $response = isset($state->responses['']) ? $state->responses[''] : '';
1401 return ($response == $answer->answer);
1402 }
1403
516cf3eb 1404 /**
1405 * Performs response processing and grading
1406 *
1407 * This function performs response processing and grading and updates
1408 * the state accordingly.
1409 * @return boolean Indicates success or failure.
1410 * @param object $question The question to be graded. Question type
1411 * specific information is included.
1412 * @param object $state The state of the question to grade. The current
1413 * responses are in ->responses. The last graded state
1414 * is in ->last_graded (hence the most recently graded
1415 * responses are in ->last_graded->responses). The
1416 * question type specific information is also
1417 * included. The ->raw_grade and ->penalty fields
1418 * must be updated. The method is able to
1419 * close the question session (preventing any further
1420 * attempts at this question) by setting
f30bbcaf 1421 * $state->event to QUESTION_EVENTCLOSEANDGRADE
516cf3eb 1422 * @param object $cmoptions
1423 */
1424 function grade_responses(&$question, &$state, $cmoptions) {
5a14d563 1425 // The default implementation uses the test_response method to
1426 // compare what the student entered against each of the possible
271ffe3f 1427 // answers stored in the question, and uses the grade from the
5a14d563 1428 // first one that matches. It also sets the marks and penalty.
1429 // This should be good enought for most simple question types.
516cf3eb 1430
5a14d563 1431 $state->raw_grade = 0;
516cf3eb 1432 foreach($question->options->answers as $answer) {
5a14d563 1433 if($this->test_response($question, $state, $answer)) {
1434 $state->raw_grade = $answer->fraction;
516cf3eb 1435 break;
1436 }
1437 }
5a14d563 1438
1439 // Make sure we don't assign negative or too high marks.
1440 $state->raw_grade = min(max((float) $state->raw_grade,
1441 0.0), 1.0) * $question->maxgrade;
271ffe3f 1442
5a14d563 1443 // Update the penalty.
1444 $state->penalty = $question->penalty * $question->maxgrade;
516cf3eb 1445
f30bbcaf 1446 // mark the state as graded
1447 $state->event = ($state->event == QUESTION_EVENTCLOSE) ? QUESTION_EVENTCLOSEANDGRADE : QUESTION_EVENTGRADE;
1448
516cf3eb 1449 return true;
1450 }
1451
516cf3eb 1452 /**
dc1f00de 1453 * Returns true if the editing wizard is finished, false otherwise.
1454 *
1455 * The default implementation returns true, which is suitable for all question-
516cf3eb 1456 * types that only use one editing form. This function is used in
1457 * question.php to decide whether we can regrade any states of the edited
1458 * question and redirect to edit.php.
1459 *
1460 * The dataset dependent question-type, which is extended by the calculated
1461 * question-type, overwrites this method because it uses multiple pages (i.e.
1462 * a wizard) to set up the question and associated datasets.
1463 *
1464 * @param object $form The data submitted by the previous page.
1465 *
1466 * @return boolean Whether the wizard's last page was submitted or not.
1467 */
1468 function finished_edit_wizard(&$form) {
1469 //In the default case there is only one edit page.
1470 return true;
1471 }
1472
1b8a7434 1473 /**
1474 * Call format_text from weblib.php with the options appropriate to question types.
271ffe3f 1475 *
1b8a7434 1476 * @param string $text the text to format.
1477 * @param integer $text the type of text. Normally $question->questiontextformat.
1478 * @param object $cmoptions the context the string is being displayed in. Only $cmoptions->course is used.
1479 * @return string the formatted text.
1480 */
08eef20d 1481 function format_text($text, $textformat, $cmoptions = NULL) {
1b8a7434 1482 $formatoptions = new stdClass;
1483 $formatoptions->noclean = true;
1484 $formatoptions->para = false;
08eef20d 1485 return format_text($text, $textformat, $formatoptions, $cmoptions === NULL ? NULL : $cmoptions->course);
516cf3eb 1486 }
271ffe3f 1487
271e6dec 1488 /*
1489 * Find all course / site files linked from a question.
1490 *
1491 * Need to check for links to files in question_answers.answer and feedback
1492 * and in question table in generalfeedback and questiontext fields. Methods
1493 * on child classes will also check extra question specific fields.
1494 *
1495 * Needs to be overriden for child classes that have extra fields containing
1496 * html.
1497 *
1498 * @param string html the html to search
1499 * @param int courseid search for files for courseid course or set to siteid for
1500 * finding site files.
1501 * @return array of url, relative url is key and array with one item = question id as value
1502 * relative url is relative to course/site files directory root.
1503 */
1504 function find_file_links($question, $courseid){
1505 $urls = array();
a588d813 1506
1507 /// Question image
271e6dec 1508 if ($question->image != ''){
1509 if (substr(strtolower($question->image), 0, 7) == 'http://') {
1510 $matches = array();
1511
1512 //support for older questions where we have a complete url in image field
1513 if (preg_match('!^'.question_file_links_base_url($courseid).'(.*)!i', $question->image, $matches)){
1514 if ($cleanedurl = question_url_check($urls[$matches[2]])){
1515 $urls[$cleanedurl] = null;
1516 }
1517 }
1518 } else {
1519 if ($question->image != ''){
1520 if ($cleanedurl = question_url_check($question->image)){
1521 $urls[$cleanedurl] = null;//will be set later
1522 }
1523 }
1524
1525 }
1526
1527 }
a588d813 1528
1529 /// Questiontext and general feedback.
271e6dec 1530 $urls += question_find_file_links_from_html($question->questiontext, $courseid);
1531 $urls += question_find_file_links_from_html($question->generalfeedback, $courseid);
a588d813 1532
1533 /// Answers, if this question uses them.
1534 if (isset($question->options->answers)){
271e6dec 1535 foreach ($question->options->answers as $answerkey => $answer){
a588d813 1536 /// URLs in the answers themselves, if appropriate.
1537 if ($this->has_html_answers()) {
1538 $urls += question_find_file_links_from_html($answer->answer, $courseid);
271e6dec 1539 }
a588d813 1540 /// URLs in the answer feedback.
1541 $urls += question_find_file_links_from_html($answer->feedback, $courseid);
271e6dec 1542 }
1543 }
a588d813 1544
1545 /// Set all the values of the array to the question object
271e6dec 1546 if ($urls){
1547 $urls = array_combine(array_keys($urls), array_fill(0, count($urls), array($question->id)));
1548 }
1549 return $urls;
1550 }
1551 /*
1552 * Find all course / site files linked from a question.
1553 *
1554 * Need to check for links to files in question_answers.answer and feedback
1555 * and in question table in generalfeedback and questiontext fields. Methods
1556 * on child classes will also check extra question specific fields.
1557 *
1558 * Needs to be overriden for child classes that have extra fields containing
1559 * html.
1560 *
1561 * @param string html the html to search
1562 * @param int course search for files for courseid course or set to siteid for
1563 * finding site files.
1564 * @return array of files, file name is key and array with one item = question id as value
1565 */
1566 function replace_file_links($question, $fromcourseid, $tocourseid, $url, $destination){
f34488b2 1567 global $CFG, $DB;
271e6dec 1568 $updateqrec = false;
a588d813 1569
1570 /// Question image
271e6dec 1571 if (!empty($question->image)){
1572 //support for older questions where we have a complete url in image field
1573 if (substr(strtolower($question->image), 0, 7) == 'http://') {
1574 $questionimage = preg_replace('!^'.question_file_links_base_url($fromcourseid).preg_quote($url, '!').'$!i', $destination, $question->image, 1);
1575 } else {
1576 $questionimage = preg_replace('!^'.preg_quote($url, '!').'$!i', $destination, $question->image, 1);
1577 }
1578 if ($questionimage != $question->image){
1579 $question->image = $questionimage;
1580 $updateqrec = true;
1581 }
1582 }
a588d813 1583
1584 /// Questiontext and general feedback.
271e6dec 1585 $question->questiontext = question_replace_file_links_in_html($question->questiontext, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
1586 $question->generalfeedback = question_replace_file_links_in_html($question->generalfeedback, $fromcourseid, $tocourseid, $url, $destination, $updateqrec);
a588d813 1587
1588 /// If anything has changed, update it in the database.
271e6dec 1589 if ($updateqrec){
bb4b6010 1590 $DB->update_record('question', $question);
271e6dec 1591 }
1592
a588d813 1593
1594 /// Answers, if this question uses them.
1595 if (isset($question->options->answers)){
271e6dec 1596 //answers that do not need updating have been unset
1597 foreach ($question->options->answers as $answer){
1598 $answerchanged = false;
a588d813 1599 /// URLs in the answers themselves, if appropriate.
1600 if ($this->has_html_answers()) {
1601 $answer->answer = question_replace_file_links_in_html($answer->answer, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1602 }
1603 /// URLs in the answer feedback.
1604 $answer->feedback = question_replace_file_links_in_html($answer->feedback, $fromcourseid, $tocourseid, $url, $destination, $answerchanged);
1605 /// If anything has changed, update it in the database.
271e6dec 1606 if ($answerchanged){
bb4b6010 1607 $DB->update_record('question_answers', $answer);
271e6dec 1608 }
1609 }
1610 }
1611 }
4995b9c1 1612
e3fa6587 1613 /**
1614 * @return the best link to pass to print_error.
1615 * @param $cmoptions as passed in from outside.
1616 */
1617 function error_link($cmoptions) {
1618 global $CFG;
1619 $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
1620 if (!empty($cm->id)) {
1621 return $CFG->wwwroot . '/mod/quiz/view.php?id=' . $cm->id;
1622 } else if (!empty($cm->course)) {
1623 return $CFG->wwwroot . '/course/view.php?id=' . $cm->course;
1624 } else {
1625 return '';
1626 }
1627 }
1628
c5d94c41 1629/// BACKUP FUNCTIONS ////////////////////////////
1630
1631 /*
1632 * Backup the data in the question
1633 *
1634 * This is used in question/backuplib.php
1635 */
1636 function backup($bf,$preferences,$question,$level=6) {
815262ca 1637 global $DB;
c51c539d 1638
1639 $status = true;
1640 $extraquestionfields = $this->extra_question_fields();
1641
1642 if (is_array($extraquestionfields)) {
1643 $questionextensiontable = array_shift($extraquestionfields);
815262ca 1644 $record = $DB->get_record($questionextensiontable, array($this->questionid_column_name() => $question));
c51c539d 1645 if ($record) {
1646 $tagname = strtoupper($this->name());
1647 $status = $status && fwrite($bf, start_tag($tagname, $level, true));
1648 foreach ($extraquestionfields as $field) {
1649 if (!isset($record->$field)) {
1650 echo "No data for field $field when backuping " .
1651 $this->name() . ' question id ' . $question;
1652 return false;
1653 }
1654 fwrite($bf, full_tag(strtoupper($field), $level + 1, false, $record->$field));
1655 }
1656 $status = $status && fwrite($bf, end_tag($tagname, $level, true));
1657 }
1658 }
1659
1660 $extraasnwersfields = $this->extra_answer_fields();
1661 if (is_array($extraasnwersfields)) {
1662 //TODO backup the answers, with any extra data.
1663 } else {
1664 $status = $status && question_backup_answers($bf, $preferences, $question);
1665 }
1666 return $status;
c5d94c41 1667 }
1668
315559d3 1669/// RESTORE FUNCTIONS /////////////////
1670
1671 /*
1672 * Restores the data in the question
1673 *
1674 * This is used in question/restorelib.php
1675 */
1676 function restore($old_question_id,$new_question_id,$info,$restore) {
c795dc78 1677 global $DB;
c51c539d 1678
1679 $status = true;
1680 $extraquestionfields = $this->extra_question_fields();
1681
1682 if (is_array($extraquestionfields)) {
1683 $questionextensiontable = array_shift($extraquestionfields);
1684 $tagname = strtoupper($this->name());
1685 $recordinfo = $info['#'][$tagname][0];
1686
1687 $record = new stdClass;
1688 $qidcolname = $this->questionid_column_name();
1689 $record->$qidcolname = $new_question_id;
1690 foreach ($extraquestionfields as $field) {
1691 $record->$field = backup_todb($recordinfo['#'][strtoupper($field)]['0']['#']);
1692 }
fc29e51b 1693 $DB->insert_record($questionextensiontable, $record);
c51c539d 1694 }
1695 //TODO restore extra data in answers
1696 return $status;
315559d3 1697 }
1698
1699 function restore_map($old_question_id,$new_question_id,$info,$restore) {
1700 // There is nothing to decode
1701 return true;
1702 }
1703
1704 function restore_recode_answer($state, $restore) {
1705 // There is nothing to decode
ba8f7ff0 1706 return $state->answer;
271ffe3f 1707 }
b9bd6da4 1708
88bc20c3 1709/// IMPORT/EXPORT FUNCTIONS /////////////////
1710
1711 /*
1712 * Imports question from the Moodle XML format
1713 *
1714 * Imports question using information from extra_question_fields function
1715 * If some of you fields contains id's you'll need to reimplement this
1716 */
1717 function import_from_xml($data, $question, $format, $extra=null) {
1718 $question_type = $data['@']['type'];
1719 if ($question_type != $this->name()) {
1720 return false;
1721 }
1722
1723 $extraquestionfields = $this->extra_question_fields();
1724 if (!is_array($extraquestionfields)) {
1725 return false;
1726 }
1727
1728 //omit table name
1729 array_shift($extraquestionfields);
1730 $qo = $format->import_headers($data);
1731 $qo->qtype = $question_type;
1732
1733 foreach ($extraquestionfields as $field) {
1734 $qo->$field = $format->getpath($data, array('#',$field,0,'#'), $qo->$field);
1735 }
1736
1737 // run through the answers
1738 $answers = $data['#']['answer'];
1739 $a_count = 0;
1740 $extraasnwersfields = $this->extra_answer_fields();
1741 if (is_array($extraasnwersfields)) {
1742 //TODO import the answers, with any extra data.
1743 } else {
1744 foreach ($answers as $answer) {
1745 $ans = $format->import_answer($answer);
1746 $qo->answer[$a_count] = $ans->answer;
1747 $qo->fraction[$a_count] = $ans->fraction;
1748 $qo->feedback[$a_count] = $ans->feedback;
1749 ++$a_count;
1750 }
1751 }
1752 return $qo;
1753 }
1754
1755 /*
1756 * Export question to the Moodle XML format
1757 *
1758 * Export question using information from extra_question_fields function
1759 * If some of you fields contains id's you'll need to reimplement this
1760 */
1761 function export_to_xml($question, $format, $extra=null) {
1762 $extraquestionfields = $this->extra_question_fields();
1763 if (!is_array($extraquestionfields)) {
1764 return false;
1765 }
1766
1767 //omit table name
1768 array_shift($extraquestionfields);
1769 $expout='';
1770 foreach ($extraquestionfields as $field) {
1771 $expout .= " <$field>{$question->options->$field}</$field>\n";
1772 }
1773
1774 $extraasnwersfields = $this->extra_answer_fields();
1775 if (is_array($extraasnwersfields)) {
1776 //TODO export answers with any extra data
1777 } else {
1778 foreach ($question->options->answers as $answer) {
1779 $percent = 100 * $answer->fraction;
1780 $expout .= " <answer fraction=\"$percent\">\n";
1781 $expout .= $format->writetext($answer->answer, 3, false);
1782 $expout .= " <feedback>\n";
1783 $expout .= $format->writetext($answer->feedback, 4, false);
1784 $expout .= " </feedback>\n";
1785 $expout .= " </answer>\n";
1786 }
1787 }
1788 return $expout;
1789 }
1790
b9bd6da4 1791 /**
1792 * Abstract function implemented by each question type. It runs all the code
1793 * required to set up and save a question of any type for testing purposes.
1794 * Alternate DB table prefix may be used to facilitate data deletion.
1795 */
1796 function generate_test($name, $courseid=null) {
1797 $form = new stdClass();
1798 $form->name = $name;
1799 $form->questiontextformat = 1;
1800 $form->questiontext = 'test question, generated by script';
1801 $form->defaultgrade = 1;
1802 $form->penalty = 0.1;
1803 $form->generalfeedback = "Well done";
1804
1805 $context = get_context_instance(CONTEXT_COURSE, $courseid);
1806 $newcategory = question_make_default_categories(array($context));
1807 $form->category = $newcategory->id . ',1';
1808
1809 $question = new stdClass();
1810 $question->courseid = $courseid;
1811 $question->qtype = $this->qtype;
1812 return array($form, $question);
1813 }
516cf3eb 1814}
aeb15530 1815