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