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