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