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