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