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