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