fixed typo for parameter value from treu to true.
[moodle.git] / lib / questionlib.php
CommitLineData
50fcb1d8 1<?php
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/**
6b11a0e8 19 * Code for handling and processing questions
20 *
21 * This is code that is module independent, i.e., can be used by any module that
22 * uses questions, like quiz, lesson, ..
23 * This script also loads the questiontype classes
24 * Code for handling the editing of questions is in {@link question/editlib.php}
25 *
26 * TODO: separate those functions which form part of the API
27 * from the helper functions.
28 *
50fcb1d8 29 * Major Contributors
30 * - Alex Smith, Julian Sedding and Gustav Delius {@link http://maths.york.ac.uk/serving_maths}
31 *
32 * @package moodlecore
33 * @subpackage question
34 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6b11a0e8 36 */
37
38/// CONSTANTS ///////////////////////////////////
516cf3eb 39
e56a08dc 40/**#@+
6b11a0e8 41 * The different types of events that can create question states
42 */
f30bbcaf 43define('QUESTION_EVENTOPEN', '0'); // The state was created by Moodle
44define('QUESTION_EVENTNAVIGATE', '1'); // The responses were saved because the student navigated to another page (this is not currently used)
45define('QUESTION_EVENTSAVE', '2'); // The student has requested that the responses should be saved but not submitted or validated
46define('QUESTION_EVENTGRADE', '3'); // Moodle has graded the responses. A SUBMIT event can be changed to a GRADE event by Moodle.
47define('QUESTION_EVENTDUPLICATE', '4'); // The responses submitted were the same as previously
48define('QUESTION_EVENTVALIDATE', '5'); // The student has requested a validation. This causes the responses to be saved as well, but not graded.
49define('QUESTION_EVENTCLOSEANDGRADE', '6'); // Moodle has graded the responses. A CLOSE event can be changed to a CLOSEANDGRADE event by Moodle.
50define('QUESTION_EVENTSUBMIT', '7'); // The student response has been submitted but it has not yet been marked
51define('QUESTION_EVENTCLOSE', '8'); // The response has been submitted and the session has been closed, either because the student requested it or because Moodle did it (e.g. because of a timelimit). The responses have not been graded.
bc2feba3 52define('QUESTION_EVENTMANUALGRADE', '9'); // Grade was entered by teacher
720be6f2 53
54define('QUESTION_EVENTS_GRADED', QUESTION_EVENTGRADE.','.
55 QUESTION_EVENTCLOSEANDGRADE.','.
56 QUESTION_EVENTMANUALGRADE);
8b92c1e3 57
58
59define('QUESTION_EVENTS_CLOSED', QUESTION_EVENTCLOSE.','.
60 QUESTION_EVENTCLOSEANDGRADE.','.
61 QUESTION_EVENTMANUALGRADE);
62
63define('QUESTION_EVENTS_CLOSED_OR_GRADED', QUESTION_EVENTGRADE.','.
64 QUESTION_EVENTS_CLOSED);
b55797b8 65
e56a08dc 66/**#@-*/
67
68/**#@+
a2156789 69 * The core question types.
6b11a0e8 70 */
60407982 71define("SHORTANSWER", "shortanswer");
72define("TRUEFALSE", "truefalse");
73define("MULTICHOICE", "multichoice");
74define("RANDOM", "random");
75define("MATCH", "match");
76define("RANDOMSAMATCH", "randomsamatch");
77define("DESCRIPTION", "description");
78define("NUMERICAL", "numerical");
79define("MULTIANSWER", "multianswer");
80define("CALCULATED", "calculated");
60407982 81define("ESSAY", "essay");
e56a08dc 82/**#@-*/
83
6b11a0e8 84/**
85 * Constant determines the number of answer boxes supplied in the editing
86 * form for multiple choice and similar question types.
87 */
4f48fb42 88define("QUESTION_NUMANS", "10");
e56a08dc 89
271ffe3f 90/**
91 * Constant determines the number of answer boxes supplied in the editing
92 * form for multiple choice and similar question types to start with, with
93 * the option of adding QUESTION_NUMANS_ADD more answers.
94 */
95define("QUESTION_NUMANS_START", 3);
96
97/**
98 * Constant determines the number of answer boxes to add in the editing
99 * form for multiple choice and similar question types when the user presses
100 * 'add form fields button'.
101 */
102define("QUESTION_NUMANS_ADD", 3);
103
1b8a7434 104/**
105 * The options used when popping up a question preview window in Javascript.
106 */
3bd6b994 107define('QUESTION_PREVIEW_POPUP_OPTIONS', 'scrollbars=true&resizable=true&width=700&height=540');
516cf3eb 108
6b11a0e8 109/**#@+
110 * Option flags for ->optionflags
111 * The options are read out via bitwise operation using these constants
112 */
113/**
114 * Whether the questions is to be run in adaptive mode. If this is not set then
115 * a question closes immediately after the first submission of responses. This
116 * is how question is Moodle always worked before version 1.5
117 */
118define('QUESTION_ADAPTIVE', 1);
62e76c67 119/**#@-*/
6b11a0e8 120
62e76c67 121/**#@+
122 * Options used in forms that move files.
3bee1ead 123 */
124define('QUESTION_FILENOTHINGSELECTED', 0);
125define('QUESTION_FILEDONOTHING', 1);
126define('QUESTION_FILECOPY', 2);
127define('QUESTION_FILEMOVE', 3);
128define('QUESTION_FILEMOVELINKSONLY', 4);
62e76c67 129/**#@-*/
3bee1ead 130
62e76c67 131/**#@+
132 * Options for whether flags are shown/editable when rendering questions.
133 */
134define('QUESTION_FLAGSHIDDEN', 0);
135define('QUESTION_FLAGSSHOWN', 1);
136define('QUESTION_FLAGSEDITABLE', 2);
6b11a0e8 137/**#@-*/
138
50fcb1d8 139/**
140 * GLOBAL VARAIBLES
141 * @global array $QTYPES
142 * @name $QTYPES
143 */
f24493ec 144global $QTYPES;
516cf3eb 145/**
f24493ec 146 * Array holding question type objects. Initialised via calls to
147 * question_register_questiontype as the question type classes are included.
6b11a0e8 148 */
a2156789 149$QTYPES = array();
a2156789 150
151/**
152 * Add a new question type to the various global arrays above.
271ffe3f 153 *
50fcb1d8 154 * @global object
a2156789 155 * @param object $qtype An instance of the new question type class.
156 */
157function question_register_questiontype($qtype) {
f24493ec 158 global $QTYPES;
271ffe3f 159
a2156789 160 $name = $qtype->name();
161 $QTYPES[$name] = $qtype;
a2156789 162}
516cf3eb 163
643ec47d 164require_once("$CFG->dirroot/question/type/questiontype.php");
516cf3eb 165
a2156789 166// Load the questiontype.php file for each question type
271ffe3f 167// These files in turn call question_register_questiontype()
a2156789 168// with a new instance of each qtype class.
17da2e6f 169$qtypenames = get_plugin_list('qtype');
170foreach($qtypenames as $qtypename => $qdir) {
f02c6f01 171 // Instanciates all plug-in question types
17da2e6f 172 $qtypefilepath= "$qdir/questiontype.php";
f02c6f01 173
174 // echo "Loading $qtypename<br/>"; // Uncomment for debugging
175 if (is_readable($qtypefilepath)) {
176 require_once($qtypefilepath);
516cf3eb 177 }
178}
179
e2ae84a2 180/**
181 * An array of question type names translated to the user's language, suitable for use when
182 * creating a drop-down menu of options.
183 *
184 * Long-time Moodle programmers will realise that this replaces the old $QTYPE_MENU array.
185 * The array returned will only hold the names of all the question types that the user should
186 * be able to create directly. Some internal question types like random questions are excluded.
b9bd6da4 187 *
50fcb1d8 188 * @global object
e2ae84a2 189 * @return array an array of question type names translated to the user's language.
190 */
191function question_type_menu() {
192 global $QTYPES;
7f92f0ad 193 static $menuoptions = null;
194 if (is_null($menuoptions)) {
af52ecee 195 $config = get_config('question');
7f92f0ad 196 $menuoptions = array();
e2ae84a2 197 foreach ($QTYPES as $name => $qtype) {
af52ecee 198 // Get the name if this qtype is enabled.
e2ae84a2 199 $menuname = $qtype->menu_name();
af52ecee 200 $enabledvar = $name . '_disabled';
201 if ($menuname && !isset($config->$enabledvar)) {
7f92f0ad 202 $menuoptions[$name] = $menuname;
e2ae84a2 203 }
204 }
af52ecee 205
206 $menuoptions = question_sort_qtype_array($menuoptions, $config);
e2ae84a2 207 }
7f92f0ad 208 return $menuoptions;
e2ae84a2 209}
210
af52ecee 211/**
212 * Sort an array of question type names according to the question type sort order stored in
213 * config_plugins. Entries for which there is no xxx_sortorder defined will go
214 * at the end, sorted according to asort($inarray, SORT_LOCALE_STRING).
215 * @param $inarray an array $qtype => $QTYPES[$qtype]->local_name().
216 * @param $config get_config('question'), if you happen to have it around, to save one DB query.
217 * @return array the sorted version of $inarray.
218 */
219function question_sort_qtype_array($inarray, $config = null) {
220 if (is_null($config)) {
221 $config = get_config('question');
222 }
223
224 $sortorder = array();
225 foreach ($inarray as $name => $notused) {
226 $sortvar = $name . '_sortorder';
227 if (isset($config->$sortvar)) {
228 $sortorder[$config->$sortvar] = $name;
229 }
230 }
231
232 ksort($sortorder);
233 $outarray = array();
234 foreach ($sortorder as $name) {
235 $outarray[$name] = $inarray[$name];
236 unset($inarray[$name]);
237 }
238 asort($inarray, SORT_LOCALE_STRING);
239 return array_merge($outarray, $inarray);
240}
241
242/**
243 * Move one question type in a list of question types. If you try to move one element
244 * off of the end, nothing will change.
117bd748 245 *
af52ecee 246 * @param array $sortedqtypes An array $qtype => anything.
247 * @param string $tomove one of the keys from $sortedqtypes
248 * @param integer $direction +1 or -1
249 * @return array an array $index => $qtype, with $index from 0 to n in order, and
250 * the $qtypes in the same order as $sortedqtypes, except that $tomove will
251 * have been moved one place.
252 */
253function question_reorder_qtypes($sortedqtypes, $tomove, $direction) {
254 $neworder = array_keys($sortedqtypes);
255 // Find the element to move.
256 $key = array_search($tomove, $neworder);
257 if ($key === false) {
258 return $neworder;
259 }
260 // Work out the other index.
261 $otherkey = $key + $direction;
262 if (!isset($neworder[$otherkey])) {
263 return $neworder;
264 }
265 // Do the swap.
266 $swap = $neworder[$otherkey];
267 $neworder[$otherkey] = $neworder[$key];
268 $neworder[$key] = $swap;
269 return $neworder;
270}
271
272/**
273 * Save a new question type order to the config_plugins table.
50fcb1d8 274 * @global object
af52ecee 275 * @param $neworder An arra $index => $qtype. Indices should start at 0 and be in order.
276 * @param $config get_config('question'), if you happen to have it around, to save one DB query.
277 */
278function question_save_qtype_order($neworder, $config = null) {
279 global $DB;
280
281 if (is_null($config)) {
282 $config = get_config('question');
283 }
284
285 foreach ($neworder as $index => $qtype) {
286 $sortvar = $qtype . '_sortorder';
287 if (!isset($config->$sortvar) || $config->$sortvar != $index + 1) {
288 set_config($sortvar, $index + 1, 'question');
289 }
290 }
291}
292
516cf3eb 293/// OTHER CLASSES /////////////////////////////////////////////////////////
294
295/**
6b11a0e8 296 * This holds the options that are set by the course module
50fcb1d8 297 *
298 * @package moodlecore
299 * @subpackage question
300 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
301 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6b11a0e8 302 */
516cf3eb 303class cmoptions {
304 /**
305 * Whether a new attempt should be based on the previous one. If true
306 * then a new attempt will start in a state where all responses are set
307 * to the last responses from the previous attempt.
308 */
309 var $attemptonlast = false;
310
311 /**
312 * Various option flags. The flags are accessed via bitwise operations
313 * using the constants defined in the CONSTANTS section above.
314 */
6b11a0e8 315 var $optionflags = QUESTION_ADAPTIVE;
516cf3eb 316
317 /**
318 * Determines whether in the calculation of the score for a question
319 * penalties for earlier wrong responses within the same attempt will
320 * be subtracted.
321 */
322 var $penaltyscheme = true;
323
324 /**
325 * The maximum time the user is allowed to answer the questions withing
326 * an attempt. This is measured in minutes so needs to be multiplied by
327 * 60 before compared to timestamps. If set to 0 no timelimit will be applied
328 */
329 var $timelimit = 0;
330
331 /**
332 * Timestamp for the closing time. Responses submitted after this time will
333 * be saved but no credit will be given for them.
334 */
335 var $timeclose = 9999999999;
336
337 /**
338 * The id of the course from withing which the question is currently being used
339 */
340 var $course = SITEID;
341
342 /**
343 * Whether the answers in a multiple choice question should be randomly
344 * shuffled when a new attempt is started.
345 */
c4c11af8 346 var $shuffleanswers = true;
516cf3eb 347
348 /**
349 * The number of decimals to be shown when scores are printed
350 */
351 var $decimalpoints = 2;
516cf3eb 352}
353
354
516cf3eb 355/// FUNCTIONS //////////////////////////////////////////////////////
356
90c3f310 357/**
f67172b6 358 * Returns an array of names of activity modules that use this question
90c3f310 359 *
50fcb1d8 360 * @global object
361 * @global object
f67172b6 362 * @param object $questionid
363 * @return array of strings
90c3f310 364 */
f67172b6 365function question_list_instances($questionid) {
eb84a826 366 global $CFG, $DB;
90c3f310 367 $instances = array();
eb84a826 368 $modules = $DB->get_records('modules');
90c3f310 369 foreach ($modules as $module) {
7453df70 370 $fullmod = $CFG->dirroot . '/mod/' . $module->name;
371 if (file_exists($fullmod . '/lib.php')) {
372 include_once($fullmod . '/lib.php');
373 $fn = $module->name.'_question_list_instances';
374 if (function_exists($fn)) {
375 $instances = $instances + $fn($questionid);
376 }
90c3f310 377 }
378 }
379 return $instances;
380}
516cf3eb 381
e2b347e9 382/**
383 * Determine whether there arey any questions belonging to this context, that is whether any of its
384 * question categories contain any questions. This will return true even if all the questions are
385 * hidden.
386 *
50fcb1d8 387 * @global object
e2b347e9 388 * @param mixed $context either a context object, or a context id.
389 * @return boolean whether any of the question categories beloning to this context have
390 * any questions in them.
391 */
392function question_context_has_any_questions($context) {
eb84a826 393 global $DB;
e2b347e9 394 if (is_object($context)) {
395 $contextid = $context->id;
396 } else if (is_numeric($context)) {
397 $contextid = $context;
398 } else {
399 print_error('invalidcontextinhasanyquestions', 'question');
400 }
eb84a826 401 return $DB->record_exists_sql("SELECT *
402 FROM {question} q
403 JOIN {question_categories} qc ON qc.id = q.category
404 WHERE qc.contextid = ? AND q.parent = 0", array($contextid));
e2b347e9 405}
406
271ffe3f 407/**
e9de4366 408 * Returns list of 'allowed' grades for grade selection
409 * formatted suitably for dropdown box function
410 * @return object ->gradeoptionsfull full array ->gradeoptions +ve only
411 */
412function get_grade_options() {
30f09308 413 // define basic array of grades. This list comprises all fractions of the form:
414 // a. p/q for q <= 6, 0 <= p <= q
415 // b. p/10 for 0 <= p <= 10
416 // c. 1/q for 1 <= q <= 10
417 // d. 1/20
e9de4366 418 $grades = array(
30f09308 419 1.0000000,
420 0.9000000,
421 0.8333333,
422 0.8000000,
423 0.7500000,
424 0.7000000,
425 0.6666667,
426 0.6000000,
427 0.5000000,
428 0.4000000,
429 0.3333333,
430 0.3000000,
431 0.2500000,
432 0.2000000,
433 0.1666667,
434 0.1428571,
435 0.1250000,
436 0.1111111,
437 0.1000000,
438 0.0500000,
439 0.0000000);
e9de4366 440
441 // iterate through grades generating full range of options
442 $gradeoptionsfull = array();
443 $gradeoptions = array();
444 foreach ($grades as $grade) {
445 $percentage = 100 * $grade;
446 $neggrade = -$grade;
447 $gradeoptions["$grade"] = "$percentage %";
448 $gradeoptionsfull["$grade"] = "$percentage %";
449 $gradeoptionsfull["$neggrade"] = -$percentage." %";
450 }
451 $gradeoptionsfull["0"] = $gradeoptions["0"] = get_string("none");
452
453 // sort lists
454 arsort($gradeoptions, SORT_NUMERIC);
455 arsort($gradeoptionsfull, SORT_NUMERIC);
456
457 // construct return object
458 $grades = new stdClass;
459 $grades->gradeoptions = $gradeoptions;
460 $grades->gradeoptionsfull = $gradeoptionsfull;
461
462 return $grades;
463}
464
8511669c 465/**
466 * match grade options
467 * if no match return error or match nearest
468 * @param array $gradeoptionsfull list of valid options
469 * @param int $grade grade to be tested
470 * @param string $matchgrades 'error' or 'nearest'
471 * @return mixed either 'fixed' value or false if erro
472 */
473function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') {
474 // if we just need an error...
475 if ($matchgrades=='error') {
476 foreach($gradeoptionsfull as $value => $option) {
2784b982 477 // slightly fuzzy test, never check floats for equality :-)
478 if (abs($grade-$value)<0.00001) {
8511669c 479 return $grade;
480 }
481 }
482 // didn't find a match so that's an error
483 return false;
484 }
485 // work out nearest value
486 else if ($matchgrades=='nearest') {
487 $hownear = array();
488 foreach($gradeoptionsfull as $value => $option) {
489 if ($grade==$value) {
490 return $grade;
491 }
492 $hownear[ $value ] = abs( $grade - $value );
493 }
494 // reverse sort list of deltas and grab the last (smallest)
495 asort( $hownear, SORT_NUMERIC );
496 reset( $hownear );
497 return key( $hownear );
498 }
499 else {
500 return false;
271ffe3f 501 }
8511669c 502}
503
f67172b6 504/**
505 * Tests whether a category is in use by any activity module
506 *
50fcb1d8 507 * @global object
f67172b6 508 * @return boolean
271ffe3f 509 * @param integer $categoryid
f67172b6 510 * @param boolean $recursive Whether to examine category children recursively
511 */
512function question_category_isused($categoryid, $recursive = false) {
eb84a826 513 global $DB;
f67172b6 514
515 //Look at each question in the category
d11de005 516 if ($questions = $DB->get_records('question', array('category'=>$categoryid), '', 'id,qtype')) {
f67172b6 517 foreach ($questions as $question) {
518 if (count(question_list_instances($question->id))) {
519 return true;
520 }
521 }
522 }
523
524 //Look under child categories recursively
525 if ($recursive) {
eb84a826 526 if ($children = $DB->get_records('question_categories', array('parent'=>$categoryid))) {
f67172b6 527 foreach ($children as $child) {
528 if (question_category_isused($child->id, $recursive)) {
529 return true;
530 }
531 }
532 }
533 }
534
535 return false;
536}
537
0429cd86 538/**
539 * Deletes all data associated to an attempt from the database
540 *
50fcb1d8 541 * @global object
542 * @global object
93eb0ea3 543 * @param integer $attemptid The id of the attempt being deleted
0429cd86 544 */
545function delete_attempt($attemptid) {
eb84a826 546 global $QTYPES, $DB;
0429cd86 547
eb84a826 548 $states = $DB->get_records('question_states', array('attempt'=>$attemptid));
9523cbfc 549 if ($states) {
550 $stateslist = implode(',', array_keys($states));
3bee1ead 551
9523cbfc 552 // delete question-type specific data
553 foreach ($QTYPES as $qtype) {
554 $qtype->delete_states($stateslist);
555 }
0429cd86 556 }
557
558 // delete entries from all other question tables
559 // It is important that this is done only after calling the questiontype functions
eb84a826 560 $DB->delete_records("question_states", array("attempt"=>$attemptid));
561 $DB->delete_records("question_sessions", array("attemptid"=>$attemptid));
562 $DB->delete_records("question_attempts", array("id"=>$attemptid));
0429cd86 563}
f67172b6 564
516cf3eb 565/**
6b11a0e8 566 * Deletes question and all associated data from the database
567 *
90c3f310 568 * It will not delete a question if it is used by an activity module
50fcb1d8 569 *
570 * @global object
571 * @global object
6b11a0e8 572 * @param object $question The question being deleted
573 */
90c3f310 574function delete_question($questionid) {
eb84a826 575 global $QTYPES, $DB;
271ffe3f 576
3c573960 577 if (!$question = $DB->get_record('question', array('id'=>$questionid))) {
578 // In some situations, for example if this was a child of a
579 // Cloze question that was previously deleted, the question may already
580 // have gone. In this case, just do nothing.
581 return;
582 }
583
90c3f310 584 // Do not delete a question if it is used by an activity module
f67172b6 585 if (count(question_list_instances($questionid))) {
90c3f310 586 return;
587 }
588
589 // delete questiontype-specific data
3bee1ead 590 question_require_capability_on($question, 'edit');
591 if ($question) {
cf60a28a 592 if (isset($QTYPES[$question->qtype])) {
593 $QTYPES[$question->qtype]->delete_question($questionid);
594 }
595 } else {
596 echo "Question with id $questionid does not exist.<br />";
516cf3eb 597 }
90c3f310 598
eb84a826 599 if ($states = $DB->get_records('question_states', array('question'=>$questionid))) {
5cb9076a 600 $stateslist = implode(',', array_keys($states));
271ffe3f 601
5cb9076a 602 // delete questiontype-specific data
603 foreach ($QTYPES as $qtype) {
604 $qtype->delete_states($stateslist);
605 }
0429cd86 606 }
607
90c3f310 608 // delete entries from all other question tables
609 // It is important that this is done only after calling the questiontype functions
eb84a826 610 $DB->delete_records("question_answers", array("question"=>$questionid));
611 $DB->delete_records("question_states", array("question"=>$questionid));
612 $DB->delete_records("question_sessions", array("questionid"=>$questionid));
90c3f310 613
614 // Now recursively delete all child questions
d11de005 615 if ($children = $DB->get_records('question', array('parent' => $questionid), '', 'id,qtype')) {
516cf3eb 616 foreach ($children as $child) {
1d723a16 617 if ($child->id != $questionid) {
618 delete_question($child->id);
619 }
516cf3eb 620 }
621 }
271ffe3f 622
90c3f310 623 // Finally delete the question record itself
eb84a826 624 $DB->delete_records('question', array('id'=>$questionid));
90c3f310 625
626 return;
516cf3eb 627}
628
f67172b6 629/**
3bee1ead 630 * All question categories and their questions are deleted for this course.
f67172b6 631 *
50fcb1d8 632 * @global object
3bee1ead 633 * @param object $mod an object representing the activity
f67172b6 634 * @param boolean $feedback to specify if the process must output a summary of its work
635 * @return boolean
636 */
637function question_delete_course($course, $feedback=true) {
642816a6 638 global $DB, $OUTPUT;
eb84a826 639
f67172b6 640 //To store feedback to be showed at the end of the process
641 $feedbackdata = array();
642
643 //Cache some strings
f67172b6 644 $strcatdeleted = get_string('unusedcategorydeleted', 'quiz');
3bee1ead 645 $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
eb84a826 646 $categoriescourse = $DB->get_records('question_categories', array('contextid'=>$coursecontext->id), 'parent', 'id, parent, name');
f67172b6 647
3bee1ead 648 if ($categoriescourse) {
f67172b6 649
650 //Sort categories following their tree (parent-child) relationships
3bee1ead 651 //this will make the feedback more readable
652 $categoriescourse = sort_categories_by_tree($categoriescourse);
653
654 foreach ($categoriescourse as $category) {
655
656 //Delete it completely (questions and category itself)
657 //deleting questions
d11de005 658 if ($questions = $DB->get_records('question', array('category' => $category->id), '', 'id,qtype')) {
3bee1ead 659 foreach ($questions as $question) {
660 delete_question($question->id);
f67172b6 661 }
eb84a826 662 $DB->delete_records("question", array("category"=>$category->id));
3bee1ead 663 }
664 //delete the category
eb84a826 665 $DB->delete_records('question_categories', array('id'=>$category->id));
f67172b6 666
3bee1ead 667 //Fill feedback
668 $feedbackdata[] = array($category->name, $strcatdeleted);
669 }
670 //Inform about changes performed if feedback is enabled
671 if ($feedback) {
642816a6 672 $table = new html_table();
3bee1ead 673 $table->head = array(get_string('category','quiz'), get_string('action'));
674 $table->data = $feedbackdata;
16be8974 675 echo html_writer::table($table);
3bee1ead 676 }
677 }
678 return true;
679}
f67172b6 680
e2b347e9 681/**
682 * Category is about to be deleted,
683 * 1/ All question categories and their questions are deleted for this course category.
684 * 2/ All questions are moved to new category
685 *
50fcb1d8 686 * @global object
e2b347e9 687 * @param object $category course category object
688 * @param object $newcategory empty means everything deleted, otherwise id of category where content moved
689 * @param boolean $feedback to specify if the process must output a summary of its work
690 * @return boolean
691 */
692function question_delete_course_category($category, $newcategory, $feedback=true) {
aa9a6867 693 global $DB, $OUTPUT;
534792cd 694
e2b347e9 695 $context = get_context_instance(CONTEXT_COURSECAT, $category->id);
696 if (empty($newcategory)) {
697 $feedbackdata = array(); // To store feedback to be showed at the end of the process
698 $rescueqcategory = null; // See the code around the call to question_save_from_deletion.
699 $strcatdeleted = get_string('unusedcategorydeleted', 'quiz');
700
701 // Loop over question categories.
eb84a826 702 if ($categories = $DB->get_records('question_categories', array('contextid'=>$context->id), 'parent', 'id, parent, name')) {
e2b347e9 703 foreach ($categories as $category) {
b9bd6da4 704
e2b347e9 705 // Deal with any questions in the category.
d11de005 706 if ($questions = $DB->get_records('question', array('category' => $category->id), '', 'id,qtype')) {
e2b347e9 707
708 // Try to delete each question.
709 foreach ($questions as $question) {
710 delete_question($question->id);
711 }
712
713 // Check to see if there were any questions that were kept because they are
714 // still in use somehow, even though quizzes in courses in this category will
715 // already have been deteted. This could happen, for example, if questions are
716 // added to a course, and then that course is moved to another category (MDL-14802).
534792cd 717 $questionids = $DB->get_records_menu('question', array('category'=>$category->id), '', 'id,1');
e2b347e9 718 if (!empty($questionids)) {
719 if (!$rescueqcategory = question_save_from_deletion(implode(',', array_keys($questionids)),
720 get_parent_contextid($context), print_context_name($context), $rescueqcategory)) {
721 return false;
722 }
723 $feedbackdata[] = array($category->name, get_string('questionsmovedto', 'question', $rescueqcategory->name));
724 }
725 }
726
727 // Now delete the category.
eb84a826 728 if (!$DB->delete_records('question_categories', array('id'=>$category->id))) {
e2b347e9 729 return false;
730 }
731 $feedbackdata[] = array($category->name, $strcatdeleted);
732
733 } // End loop over categories.
734 }
735
736 // Output feedback if requested.
737 if ($feedback and $feedbackdata) {
642816a6 738 $table = new html_table();
e2b347e9 739 $table->head = array(get_string('questioncategory','question'), get_string('action'));
740 $table->data = $feedbackdata;
16be8974 741 echo html_writer::table($table);
e2b347e9 742 }
743
744 } else {
745 // Move question categories ot the new context.
746 if (!$newcontext = get_context_instance(CONTEXT_COURSECAT, $newcategory->id)) {
747 return false;
748 }
eb84a826 749 if (!$DB->set_field('question_categories', 'contextid', $newcontext->id, array('contextid'=>$context->id))) {
e2b347e9 750 return false;
751 }
752 if ($feedback) {
753 $a = new stdClass;
754 $a->oldplace = print_context_name($context);
755 $a->newplace = print_context_name($newcontext);
aa9a6867 756 echo $OUTPUT->notification(get_string('movedquestionsandcategories', 'question', $a), 'notifysuccess');
e2b347e9 757 }
758 }
759
760 return true;
761}
762
763/**
764 * Enter description here...
765 *
50fcb1d8 766 * @global object
e2b347e9 767 * @param string $questionids list of questionids
768 * @param object $newcontext the context to create the saved category in.
b9bd6da4 769 * @param string $oldplace a textual description of the think being deleted, e.g. from get_context_name
e2b347e9 770 * @param object $newcategory
b9bd6da4 771 * @return mixed false on
e2b347e9 772 */
773function question_save_from_deletion($questionids, $newcontextid, $oldplace, $newcategory = null) {
eb84a826 774 global $DB;
775
e2b347e9 776 // Make a category in the parent context to move the questions to.
777 if (is_null($newcategory)) {
778 $newcategory = new object();
779 $newcategory->parent = 0;
780 $newcategory->contextid = $newcontextid;
eb84a826 781 $newcategory->name = get_string('questionsrescuedfrom', 'question', $oldplace);
782 $newcategory->info = get_string('questionsrescuedfrominfo', 'question', $oldplace);
e2b347e9 783 $newcategory->sortorder = 999;
784 $newcategory->stamp = make_unique_id_code();
a8f3a651 785 $newcategory->id = $DB->insert_record('question_categories', $newcategory);
e2b347e9 786 }
787
788 // Move any remaining questions to the 'saved' category.
789 if (!question_move_questions_to_category($questionids, $newcategory->id)) {
790 return false;
791 }
792 return $newcategory;
793}
794
3bee1ead 795/**
796 * All question categories and their questions are deleted for this activity.
797 *
50fcb1d8 798 * @global object
3bee1ead 799 * @param object $cm the course module object representing the activity
800 * @param boolean $feedback to specify if the process must output a summary of its work
801 * @return boolean
802 */
803function question_delete_activity($cm, $feedback=true) {
642816a6 804 global $DB, $OUTPUT;
eb84a826 805
3bee1ead 806 //To store feedback to be showed at the end of the process
807 $feedbackdata = array();
f67172b6 808
3bee1ead 809 //Cache some strings
810 $strcatdeleted = get_string('unusedcategorydeleted', 'quiz');
811 $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
eb84a826 812 if ($categoriesmods = $DB->get_records('question_categories', array('contextid'=>$modcontext->id), 'parent', 'id, parent, name')){
3bee1ead 813 //Sort categories following their tree (parent-child) relationships
814 //this will make the feedback more readable
815 $categoriesmods = sort_categories_by_tree($categoriesmods);
f67172b6 816
3bee1ead 817 foreach ($categoriesmods as $category) {
f67172b6 818
3bee1ead 819 //Delete it completely (questions and category itself)
820 //deleting questions
d11de005 821 if ($questions = $DB->get_records('question', array('category' => $category->id), '', 'id,qtype')) {
3bee1ead 822 foreach ($questions as $question) {
823 delete_question($question->id);
824 }
eb84a826 825 $DB->delete_records("question", array("category"=>$category->id));
f67172b6 826 }
3bee1ead 827 //delete the category
eb84a826 828 $DB->delete_records('question_categories', array('id'=>$category->id));
3bee1ead 829
830 //Fill feedback
831 $feedbackdata[] = array($category->name, $strcatdeleted);
f67172b6 832 }
833 //Inform about changes performed if feedback is enabled
834 if ($feedback) {
642816a6 835 $table = new html_table();
f67172b6 836 $table->head = array(get_string('category','quiz'), get_string('action'));
837 $table->data = $feedbackdata;
16be8974 838 echo html_writer::table($table);
f67172b6 839 }
840 }
841 return true;
842}
7fb1b88d 843
844/**
845 * This function should be considered private to the question bank, it is called from
846 * question/editlib.php question/contextmoveq.php and a few similar places to to the work of
847 * acutally moving questions and associated data. However, callers of this function also have to
848 * do other work, which is why you should not call this method directly from outside the questionbank.
849 *
50fcb1d8 850 * @global object
7fb1b88d 851 * @param string $questionids a comma-separated list of question ids.
852 * @param integer $newcategory the id of the category to move to.
853 */
854function question_move_questions_to_category($questionids, $newcategory) {
1c69b885 855 global $DB;
856
7fb1b88d 857 $result = true;
858
859 // Move the questions themselves.
1c69b885 860 $result = $result && $DB->set_field_select('question', 'category', $newcategory, "id IN ($questionids)");
7fb1b88d 861
862 // Move any subquestions belonging to them.
1c69b885 863 $result = $result && $DB->set_field_select('question', 'category', $newcategory, "parent IN ($questionids)");
7fb1b88d 864
865 // TODO Deal with datasets.
866
867 return $result;
868}
869
3bee1ead 870/**
50fcb1d8 871 * @global object
872 * @global object
3bee1ead 873 * @param array $row tab objects
874 * @param question_edit_contexts $contexts object representing contexts available from this context
875 * @param string $querystring to append to urls
876 * */
8afba50b 877function questionbank_navigation_tabs(&$row, $contexts, array $params) {
3bee1ead 878 global $CFG, $QUESTION_EDITTABCAPS;
879 $tabs = array(
8afba50b
PS
880 'questions' =>array(new moodle_url('/question/edit.php', $params), get_string('questions', 'quiz'), get_string('editquestions', 'quiz')),
881 'categories' =>array(new moodle_url('/question/category.php', $params), get_string('categories', 'quiz'), get_string('editqcats', 'quiz')),
882 'import' =>array(new moodle_url('/question/import.php', $params), get_string('import', 'quiz'), get_string('importquestions', 'quiz')),
883 'export' =>array(new moodle_url('/question/export.php', $params), get_string('export', 'quiz'), get_string('exportquestions', 'quiz')));
3bee1ead 884 foreach ($tabs as $tabname => $tabparams){
885 if ($contexts->have_one_edit_tab_cap($tabname)) {
886 $row[] = new tabobject($tabname, $tabparams[0], $tabparams[1], $tabparams[2]);
887 }
d187f660 888 }
889}
890
5a2a5331 891/**
78e7a3dd 892 * Given a list of ids, load the basic information about a set of questions from the questions table.
893 * The $join and $extrafields arguments can be used together to pull in extra data.
894 * See, for example, the usage in mod/quiz/attemptlib.php, and
895 * read the code below to see how the SQL is assembled. Throws exceptions on error.
5a2a5331 896 *
50fcb1d8 897 * @global object
898 * @global object
78e7a3dd 899 * @param array $questionids array of question ids.
900 * @param string $extrafields extra SQL code to be added to the query.
901 * @param string $join extra SQL code to be added to the query.
902 * @param array $extraparams values for any placeholders in $join.
903 * You are strongly recommended to use named placeholder.
5a2a5331 904 *
78e7a3dd 905 * @return array partially complete question objects. You need to call get_question_options
906 * on them before they can be properly used.
5a2a5331 907 */
78e7a3dd 908function question_preload_questions($questionids, $extrafields = '', $join = '', $extraparams = array()) {
eb84a826 909 global $CFG, $DB;
4089d9ec 910 if (empty($questionids)) {
911 return array();
912 }
5a2a5331 913 if ($join) {
78e7a3dd 914 $join = ' JOIN '.$join;
5a2a5331 915 }
916 if ($extrafields) {
917 $extrafields = ', ' . $extrafields;
918 }
78e7a3dd 919 list($questionidcondition, $params) = $DB->get_in_or_equal(
920 $questionids, SQL_PARAMS_NAMED, 'qid0000');
eb84a826 921 $sql = 'SELECT q.*' . $extrafields . ' FROM {question} q' . $join .
78e7a3dd 922 ' WHERE q.id ' . $questionidcondition;
5a2a5331 923
924 // Load the questions
78e7a3dd 925 if (!$questions = $DB->get_records_sql($sql, $extraparams + $params)) {
5a2a5331 926 return 'Could not load questions.';
927 }
928
78e7a3dd 929 foreach ($questions as $question) {
930 $question->_partiallyloaded = true;
931 }
932
b55797b8 933 // Note, a possible optimisation here would be to not load the TEXT fields
934 // (that is, questiontext and generalfeedback) here, and instead load them in
935 // question_load_questions. That would add one DB query, but reduce the amount
936 // of data transferred most of the time. I am not going to do this optimisation
937 // until it is shown to be worthwhile.
938
78e7a3dd 939 return $questions;
940}
941
942/**
943 * Load a set of questions, given a list of ids. The $join and $extrafields arguments can be used
944 * together to pull in extra data. See, for example, the usage in mod/quiz/attempt.php, and
945 * read the code below to see how the SQL is assembled. Throws exceptions on error.
946 *
947 * @param array $questionids array of question ids.
948 * @param string $extrafields extra SQL code to be added to the query.
949 * @param string $join extra SQL code to be added to the query.
950 * @param array $extraparams values for any placeholders in $join.
951 * You are strongly recommended to use named placeholder.
952 *
953 * @return array question objects.
954 */
955function question_load_questions($questionids, $extrafields = '', $join = '') {
956 $questions = question_preload_questions($questionids, $extrafields, $join);
957
5a2a5331 958 // Load the question type specific information
959 if (!get_question_options($questions)) {
960 return 'Could not load the question options';
961 }
962
963 return $questions;
964}
965
516cf3eb 966/**
d7444d44 967 * Private function to factor common code out of get_question_options().
271ffe3f 968 *
50fcb1d8 969 * @global object
970 * @global object
d7444d44 971 * @param object $question the question to tidy.
c599a682 972 * @param boolean $loadtags load the question tags from the tags table. Optional, default false.
271ffe3f 973 * @return boolean true if successful, else false.
d7444d44 974 */
c599a682 975function _tidy_question(&$question, $loadtags = false) {
976 global $CFG, $QTYPES;
d7444d44 977 if (!array_key_exists($question->qtype, $QTYPES)) {
978 $question->qtype = 'missingtype';
979 $question->questiontext = '<p>' . get_string('warningmissingtype', 'quiz') . '</p>' . $question->questiontext;
980 }
981 $question->name_prefix = question_make_name_prefix($question->id);
78e7a3dd 982 if ($success = $QTYPES[$question->qtype]->get_question_options($question)) {
983 if (isset($question->_partiallyloaded)) {
984 unset($question->_partiallyloaded);
985 }
986 }
c599a682 987 if ($loadtags && !empty($CFG->usetags)) {
988 require_once($CFG->dirroot . '/tag/lib.php');
989 $question->tags = tag_get_tags_array('question', $question->id);
990 }
78e7a3dd 991 return $success;
d7444d44 992}
516cf3eb 993
d7444d44 994/**
995 * Updates the question objects with question type specific
996 * information by calling {@link get_question_options()}
997 *
998 * Can be called either with an array of question objects or with a single
999 * question object.
271ffe3f 1000 *
d7444d44 1001 * @param mixed $questions Either an array of question objects to be updated
1002 * or just a single question object
c599a682 1003 * @param boolean $loadtags load the question tags from the tags table. Optional, default false.
d7444d44 1004 * @return bool Indicates success or failure.
1005 */
c599a682 1006function get_question_options(&$questions, $loadtags = false) {
516cf3eb 1007 if (is_array($questions)) { // deal with an array of questions
d7444d44 1008 foreach ($questions as $i => $notused) {
c599a682 1009 if (!_tidy_question($questions[$i], $loadtags)) {
516cf3eb 1010 return false;
d7444d44 1011 }
516cf3eb 1012 }
1013 return true;
1014 } else { // deal with single question
c599a682 1015 return _tidy_question($questions, $loadtags);
516cf3eb 1016 }
1017}
1018
1019/**
117bd748 1020 * Load the basic state information for
81d833ad 1021 *
50fcb1d8 1022 * @global object
81d833ad 1023 * @param integer $attemptid the attempt id to load the states for.
1024 * @return array an array of state data from the database, you will subsequently
1025 * need to call question_load_states to get fully loaded states that can be
117bd748 1026 * used by the question types. The states here should be sufficient for
81d833ad 1027 * basic tasks like rendering navigation.
1028 */
1029function question_preload_states($attemptid) {
1030 global $DB;
766df8f7 1031 // Note, changes here probably also need to be reflected in
1032 // regrade_question_in_attempt and question_load_specific_state.
516cf3eb 1033
81d833ad 1034 // The questionid field must be listed first so that it is used as the
eb84a826 1035 // array index in the array returned by $DB->get_records_sql
59be14de
TH
1036 $statefields = 'n.questionid as question, s.id, s.attempt, ' .
1037 's.seq_number, s.answer, s.timestamp, s.event, s.grade, s.raw_grade, ' .
1038 's.penalty, n.sumpenalty, n.manualcomment, n.flagged, n.id as questionsessionid';
81d833ad 1039
516cf3eb 1040 // Load the newest states for the questions
eb84a826 1041 $sql = "SELECT $statefields
1042 FROM {question_states} s, {question_sessions} n
81d833ad 1043 WHERE s.id = n.newest AND n.attemptid = ?";
1044 $states = $DB->get_records_sql($sql, array($attemptid));
1045 if (!$states) {
1046 return false;
1047 }
516cf3eb 1048
1049 // Load the newest graded states for the questions
eb84a826 1050 $sql = "SELECT $statefields
1051 FROM {question_states} s, {question_sessions} n
81d833ad 1052 WHERE s.id = n.newgraded AND n.attemptid = ?";
1053 $gradedstates = $DB->get_records_sql($sql, array($attemptid));
1054
1055 // Hook the two together.
1056 foreach ($states as $questionid => $state) {
1057 $states[$questionid]->_partiallyloaded = true;
1058 if ($gradedstates[$questionid]) {
81d833ad 1059 $states[$questionid]->last_graded = $gradedstates[$questionid];
40d1feeb 1060 $states[$questionid]->last_graded->_partiallyloaded = true;
81d833ad 1061 }
1062 }
1063
1064 return $states;
1065}
1066
1067/**
1068 * Finish loading the question states that were extracted from the database with
1069 * question_preload_states, creating new states for any question where there
1070 * is not a state in the database.
1071 *
50fcb1d8 1072 * @global object
1073 * @global object
81d833ad 1074 * @param array $questions the questions to load state for.
40d1feeb 1075 * @param array $states the partially loaded states this array is updated.
81d833ad 1076 * @param object $cmoptions options from the module we are loading the states for. E.g. $quiz.
1077 * @param object $attempt The attempt for which the question sessions are
1078 * to be restored or created.
1079 * @param mixed either the id of a previous attempt, if this attmpt is
1080 * building on a previous one, or false for a clean attempt.
40d1feeb 1081 * @return true or false for success or failure.
81d833ad 1082 */
1083function question_load_states(&$questions, &$states, $cmoptions, $attempt, $lastattemptid = false) {
1084 global $QTYPES, $DB;
516cf3eb 1085
1086 // loop through all questions and set the last_graded states
81d833ad 1087 foreach (array_keys($questions) as $qid) {
1088 if (isset($states[$qid])) {
1089 restore_question_state($questions[$qid], $states[$qid]);
1090 if (isset($states[$qid]->_partiallyloaded)) {
1091 unset($states[$qid]->_partiallyloaded);
1092 }
40d1feeb 1093 if (isset($states[$qid]->last_graded)) {
1094 restore_question_state($questions[$qid], $states[$qid]->last_graded);
1095 if (isset($states[$qid]->last_graded->_partiallyloaded)) {
1096 unset($states[$qid]->last_graded->_partiallyloaded);
1097 }
1098 } else {
1099 $states[$qid]->last_graded = clone($states[$qid]);
516cf3eb 1100 }
1101 } else {
fb708c11 1102 if ($lastattemptid) {
47ceac47 1103 // If the new attempt is to be based on this previous attempt.
1104 // Find the responses from the previous attempt and save them to the new session
fb708c11 1105
59be14de
TH
1106 // Load the last graded state for the question. Note, $statefields is
1107 // the same as above, except that we don't want n.manualcomment.
1108 $statefields = 'n.questionid as question, s.id, s.attempt, ' .
1109 's.seq_number, s.answer, s.timestamp, s.event, s.grade, s.raw_grade, ' .
1110 's.penalty, n.sumpenalty';
eb84a826 1111 $sql = "SELECT $statefields
1112 FROM {question_states} s, {question_sessions} n
0b8506ec 1113 WHERE s.id = n.newest
eb84a826 1114 AND n.attemptid = ?
1115 AND n.questionid = ?";
81d833ad 1116 if (!$laststate = $DB->get_record_sql($sql, array($lastattemptid, $qid))) {
fb708c11 1117 // Only restore previous responses that have been graded
1118 continue;
1119 }
1120 // Restore the state so that the responses will be restored
81d833ad 1121 restore_question_state($questions[$qid], $laststate);
1122 $states[$qid] = clone($laststate);
1123 unset($states[$qid]->id);
fb708c11 1124 } else {
d402153a 1125 // create a new empty state
81d833ad 1126 $states[$qid] = new object;
1127 $states[$qid]->question = $qid;
1128 $states[$qid]->responses = array('' => '');
1129 $states[$qid]->raw_grade = 0;
fb708c11 1130 }
1131
1132 // now fill/overide initial values
81d833ad 1133 $states[$qid]->attempt = $attempt->uniqueid;
1134 $states[$qid]->seq_number = 0;
1135 $states[$qid]->timestamp = $attempt->timestart;
1136 $states[$qid]->event = ($attempt->timefinish) ? QUESTION_EVENTCLOSE : QUESTION_EVENTOPEN;
1137 $states[$qid]->grade = 0;
1138 $states[$qid]->penalty = 0;
1139 $states[$qid]->sumpenalty = 0;
1140 $states[$qid]->manualcomment = '';
1141 $states[$qid]->flagged = 0;
fb708c11 1142
d23e3e11 1143 // Prevent further changes to the session from incrementing the
1144 // sequence number
81d833ad 1145 $states[$qid]->changed = true;
d23e3e11 1146
fb708c11 1147 if ($lastattemptid) {
1148 // prepare the previous responses for new processing
1149 $action = new stdClass;
1150 $action->responses = $laststate->responses;
1151 $action->timestamp = $laststate->timestamp;
1152 $action->event = QUESTION_EVENTSAVE; //emulate save of questions from all pages MDL-7631
1153
1154 // Process these responses ...
81d833ad 1155 question_process_responses($questions[$qid], $states[$qid], $action, $cmoptions, $attempt);
fb708c11 1156
1157 // Fix for Bug #5506: When each attempt is built on the last one,
3bee1ead 1158 // preserve the options from any previous attempt.
fb708c11 1159 if ( isset($laststate->options) ) {
81d833ad 1160 $states[$qid]->options = $laststate->options;
fb708c11 1161 }
1162 } else {
1163 // Create the empty question type specific information
81d833ad 1164 if (!$QTYPES[$questions[$qid]->qtype]->create_session_and_responses(
1165 $questions[$qid], $states[$qid], $cmoptions, $attempt)) {
fb708c11 1166 return false;
1167 }
516cf3eb 1168 }
81d833ad 1169 $states[$qid]->last_graded = clone($states[$qid]);
516cf3eb 1170 }
1171 }
40d1feeb 1172 return true;
516cf3eb 1173}
1174
81d833ad 1175/**
1176* Loads the most recent state of each question session from the database
1177* or create new one.
1178*
1179* For each question the most recent session state for the current attempt
1180* is loaded from the question_states table and the question type specific data and
1181* responses are added by calling {@link restore_question_state()} which in turn
1182* calls {@link restore_session_and_responses()} for each question.
1183* If no states exist for the question instance an empty state object is
1184* created representing the start of a session and empty question
1185* type specific information and responses are created by calling
1186* {@link create_session_and_responses()}.
1187*
1188* @return array An array of state objects representing the most recent
1189* states of the question sessions.
1190* @param array $questions The questions for which sessions are to be restored or
1191* created.
1192* @param object $cmoptions
1193* @param object $attempt The attempt for which the question sessions are
1194* to be restored or created.
1195* @param mixed either the id of a previous attempt, if this attmpt is
1196* building on a previous one, or false for a clean attempt.
1197*/
1198function get_question_states(&$questions, $cmoptions, $attempt, $lastattemptid = false) {
81d833ad 1199 // Preload the states.
1200 $states = question_preload_states($attempt->uniqueid);
1201 if (!$states) {
1202 $states = array();
1203 }
1204
1205 // Then finish the job.
40d1feeb 1206 if (!question_load_states($questions, $states, $cmoptions, $attempt, $lastattemptid)) {
1207 return false;
1208 }
81d833ad 1209
1210 return $states;
1211}
1212
b55797b8 1213/**
1214 * Load a particular previous state of a question.
1215 *
50fcb1d8 1216 * @global object
b55797b8 1217 * @param array $question The question to load the state for.
1218 * @param object $cmoptions Options from the specifica activity module, e.g. $quiz.
1219 * @param object $attempt The attempt for which the question sessions are to be loaded.
1220 * @param integer $stateid The id of a specific state of this question.
1221 * @return object the requested state. False on error.
1222 */
1223function question_load_specific_state($question, $cmoptions, $attempt, $stateid) {
8b92c1e3 1224 global $DB;
b55797b8 1225 // Load specified states for the question.
766df8f7 1226 // sess.sumpenalty is probably wrong here shoul really be a sum of penalties from before the one we are asking for.
1227 $sql = 'SELECT st.*, sess.sumpenalty, sess.manualcomment, sess.flagged, sess.id as questionsessionid
b55797b8 1228 FROM {question_states} st, {question_sessions} sess
1229 WHERE st.id = ?
1230 AND st.attempt = ?
1231 AND sess.attemptid = st.attempt
1232 AND st.question = ?
1233 AND sess.questionid = st.question';
1234 $state = $DB->get_record_sql($sql, array($stateid, $attempt->id, $question->id));
1235 if (!$state) {
1236 return false;
1237 }
1238 restore_question_state($question, $state);
1239
1240 // Load the most recent graded states for the questions before the specified one.
766df8f7 1241 $sql = 'SELECT st.*, sess.sumpenalty, sess.manualcomment, sess.flagged, sess.id as questionsessionid
b55797b8 1242 FROM {question_states} st, {question_sessions} sess
1243 WHERE st.seq_number <= ?
1244 AND st.attempt = ?
1245 AND sess.attemptid = st.attempt
1246 AND st.question = ?
1247 AND sess.questionid = st.question
8b92c1e3 1248 AND st.event IN ('.QUESTION_EVENTS_GRADED.') '.
b55797b8 1249 'ORDER BY st.seq_number DESC';
8b92c1e3 1250 $gradedstates = $DB->get_records_sql($sql, array($state->seq_number, $attempt->id, $question->id), 0, 1);
b55797b8 1251 if (empty($gradedstates)) {
1252 $state->last_graded = clone($state);
1253 } else {
1254 $gradedstate = reset($gradedstates);
1255 restore_question_state($question, $gradedstate);
1256 $state->last_graded = $gradedstate;
1257 }
1258 return $state;
1259}
516cf3eb 1260
1261/**
1262* Creates the run-time fields for the states
1263*
1264* Extends the state objects for a question by calling
1265* {@link restore_session_and_responses()}
50fcb1d8 1266 *
1267 * @global object
516cf3eb 1268* @param object $question The question for which the state is needed
865b7534 1269* @param object $state The state as loaded from the database
1270* @return boolean Represents success or failure
516cf3eb 1271*/
4f48fb42 1272function restore_question_state(&$question, &$state) {
f02c6f01 1273 global $QTYPES;
516cf3eb 1274
1275 // initialise response to the value in the answer field
294ce987 1276 $state->responses = array('' => $state->answer);
516cf3eb 1277 unset($state->answer);
294ce987 1278 $state->manualcomment = isset($state->manualcomment) ? $state->manualcomment : '';
516cf3eb 1279
1280 // Set the changed field to false; any code which changes the
1281 // question session must set this to true and must increment
4f48fb42 1282 // ->seq_number. The save_question_session
516cf3eb 1283 // function will save the new state object to the database if the field is
1284 // set to true.
1285 $state->changed = false;
1286
1287 // Load the question type specific data
f02c6f01 1288 return $QTYPES[$question->qtype]
865b7534 1289 ->restore_session_and_responses($question, $state);
516cf3eb 1290
1291}
1292
1293/**
1294* Saves the current state of the question session to the database
1295*
1296* The state object representing the current state of the session for the
4f48fb42 1297* question is saved to the question_states table with ->responses[''] saved
516cf3eb 1298* to the answer field of the database table. The information in the
1299* question_sessions table is updated.
1300* The question type specific data is then saved.
50fcb1d8 1301 *
1302 * @global array
1303 * @global object
bc2feba3 1304* @return mixed The id of the saved or updated state or false
516cf3eb 1305* @param object $question The question for which session is to be saved.
1306* @param object $state The state information to be saved. In particular the
1307* most recent responses are in ->responses. The object
1308* is updated to hold the new ->id.
1309*/
66d07f81 1310function save_question_session($question, $state) {
eb84a826 1311 global $QTYPES, $DB;
5e9170b4 1312
516cf3eb 1313 // Check if the state has changed
1314 if (!$state->changed && isset($state->id)) {
5e9170b4 1315 if (isset($state->newflaggedstate) && $state->flagged != $state->newflaggedstate) {
1316 // If this fails, don't worry too much, it is not critical data.
1317 question_update_flag($state->questionsessionid, $state->newflaggedstate);
1318 }
bc2feba3 1319 return $state->id;
516cf3eb 1320 }
1321 // Set the legacy answer field
1322 $state->answer = isset($state->responses['']) ? $state->responses[''] : '';
1323
1324 // Save the state
36be25f6 1325 if (!empty($state->update)) { // this forces the old state record to be overwritten
eb84a826 1326 $DB->update_record('question_states', $state);
516cf3eb 1327 } else {
a8f3a651 1328 $state->id = $DB->insert_record('question_states', $state);
b6e907a2 1329 }
516cf3eb 1330
b6e907a2 1331 // create or update the session
b9bd6da4 1332 if (!$session = $DB->get_record('question_sessions', array('attemptid' => $state->attempt, 'questionid' => $question->id))) {
5e9170b4 1333 $session = new stdClass;
2e9b6d15 1334 $session->attemptid = $state->attempt;
1335 $session->questionid = $question->id;
1336 $session->newest = $state->id;
1337 // The following may seem weird, but the newgraded field needs to be set
1338 // already even if there is no graded state yet.
1339 $session->newgraded = $state->id;
1340 $session->sumpenalty = $state->sumpenalty;
3e3e5a40 1341 $session->manualcomment = $state->manualcomment;
5e9170b4 1342 $session->flagged = !empty($state->newflaggedstate);
fc29e51b 1343 $DB->insert_record('question_sessions', $session);
b6e907a2 1344 } else {
2e9b6d15 1345 $session->newest = $state->id;
1346 if (question_state_is_graded($state) or $state->event == QUESTION_EVENTOPEN) {
1347 // this state is graded or newly opened, so it goes into the lastgraded field as well
1348 $session->newgraded = $state->id;
1349 $session->sumpenalty = $state->sumpenalty;
3e3e5a40 1350 $session->manualcomment = $state->manualcomment;
ef822868 1351 } else {
eb84a826 1352 $session->manualcomment = $session->manualcomment;
516cf3eb 1353 }
5e9170b4 1354 $session->flagged = !empty($state->newflaggedstate);
bb4b6010 1355 $DB->update_record('question_sessions', $session);
516cf3eb 1356 }
1357
1358 unset($state->answer);
1359
1360 // Save the question type specific state information and responses
5e9170b4 1361 if (!$QTYPES[$question->qtype]->save_session_and_responses($question, $state)) {
516cf3eb 1362 return false;
1363 }
5e9170b4 1364
516cf3eb 1365 // Reset the changed flag
1366 $state->changed = false;
bc2feba3 1367 return $state->id;
516cf3eb 1368}
1369
1370/**
1371* Determines whether a state has been graded by looking at the event field
1372*
1373* @return boolean true if the state has been graded
1374* @param object $state
1375*/
4f48fb42 1376function question_state_is_graded($state) {
8b92c1e3 1377 static $question_events_graded = array();
1378 if (!$question_events_graded){
1379 $question_events_graded = explode(',', QUESTION_EVENTS_GRADED);
1380 }
1381 return (in_array($state->event, $question_events_graded));
f30bbcaf 1382}
1383
1384/**
1385* Determines whether a state has been closed by looking at the event field
1386*
1387* @return boolean true if the state has been closed
1388* @param object $state
1389*/
1390function question_state_is_closed($state) {
8b92c1e3 1391 static $question_events_closed = array();
1392 if (!$question_events_closed){
1393 $question_events_closed = explode(',', QUESTION_EVENTS_CLOSED);
1394 }
1395 return (in_array($state->event, $question_events_closed));
516cf3eb 1396}
1397
1398
1399/**
4dca7e51 1400 * Extracts responses from submitted form
1401 *
1402 * This can extract the responses given to one or several questions present on a page
1403 * It returns an array with one entry for each question, indexed by question id
1404 * Each entry is an object with the properties
1405 * ->event The event that has triggered the submission. This is determined by which button
1406 * the user has pressed.
1407 * ->responses An array holding the responses to an individual question, indexed by the
1408 * name of the corresponding form element.
1409 * ->timestamp A unix timestamp
1410 * @return array array of action objects, indexed by question ids.
1411 * @param array $questions an array containing at least all questions that are used on the form
1412 * @param array $formdata the data submitted by the form on the question page
1413 * @param integer $defaultevent the event type used if no 'mark' or 'validate' is submitted
1414 */
1415function question_extract_responses($questions, $formdata, $defaultevent=QUESTION_EVENTSAVE) {
516cf3eb 1416
4dca7e51 1417 $time = time();
516cf3eb 1418 $actions = array();
4dca7e51 1419 foreach ($formdata as $key => $response) {
516cf3eb 1420 // Get the question id from the response name
4f48fb42 1421 if (false !== ($quid = question_get_id_from_name_prefix($key))) {
516cf3eb 1422 // check if this is a valid id
1423 if (!isset($questions[$quid])) {
d89ec019 1424 print_error('formquestionnotinids', 'question');
516cf3eb 1425 }
1426
1427 // Remove the name prefix from the name
1428 //decrypt trying
1429 $key = substr($key, strlen($questions[$quid]->name_prefix));
1430 if (false === $key) {
1431 $key = '';
1432 }
1433 // Check for question validate and mark buttons & set events
1434 if ($key === 'validate') {
4f48fb42 1435 $actions[$quid]->event = QUESTION_EVENTVALIDATE;
4dca7e51 1436 } else if ($key === 'submit') {
f30bbcaf 1437 $actions[$quid]->event = QUESTION_EVENTSUBMIT;
516cf3eb 1438 } else {
1439 $actions[$quid]->event = $defaultevent;
1440 }
516cf3eb 1441 // Update the state with the new response
1442 $actions[$quid]->responses[$key] = $response;
271ffe3f 1443
4dca7e51 1444 // Set the timestamp
1445 $actions[$quid]->timestamp = $time;
516cf3eb 1446 }
1447 }
3ebdddf7 1448 foreach ($actions as $quid => $notused) {
1449 ksort($actions[$quid]->responses);
1450 }
516cf3eb 1451 return $actions;
1452}
1453
1454
bc75cc52 1455/**
1456 * Returns the html for question feedback image.
50fcb1d8 1457 *
1458 * @global object
bc75cc52 1459 * @param float $fraction value representing the correctness of the user's
1460 * response to a question.
1461 * @param boolean $selected whether or not the answer is the one that the
1462 * user picked.
1463 * @return string
1464 */
1465function question_get_feedback_image($fraction, $selected=true) {
d4a03c00 1466 global $CFG, $OUTPUT;
b10c38a3 1467 static $icons = array('correct' => 'tick_green', 'partiallycorrect' => 'tick_amber',
1468 'incorrect' => 'cross_red');
bc75cc52 1469
b10c38a3 1470 if ($selected) {
1471 $size = 'big';
bc75cc52 1472 } else {
b10c38a3 1473 $size = 'small';
bc75cc52 1474 }
b10c38a3 1475 $class = question_get_feedback_class($fraction);
b5d0cafc 1476 return '<img src="' . $OUTPUT->pix_url('i/' . $icons[$class] . '_' . $size) .
ddedf979 1477 '" alt="' . get_string($class, 'quiz') . '" class="icon" />';
bc75cc52 1478}
1479
bc75cc52 1480/**
1481 * Returns the class name for question feedback.
1482 * @param float $fraction value representing the correctness of the user's
1483 * response to a question.
1484 * @return string
1485 */
1486function question_get_feedback_class($fraction) {
b10c38a3 1487 if ($fraction >= 1/1.01) {
1488 return 'correct';
1489 } else if ($fraction > 0.0) {
1490 return 'partiallycorrect';
bc75cc52 1491 } else {
b10c38a3 1492 return 'incorrect';
bc75cc52 1493 }
bc75cc52 1494}
1495
516cf3eb 1496
1497/**
1498* For a given question in an attempt we walk the complete history of states
1499* and recalculate the grades as we go along.
1500*
1501* This is used when a question is changed and old student
1502* responses need to be marked with the new version of a question.
1503*
50fcb1d8 1504* @todo Make sure this is not quiz-specific
4f48fb42 1505*
50fcb1d8 1506 * @global object
0a5b58af 1507* @return boolean Indicates whether the grade has changed
516cf3eb 1508* @param object $question A question object
1509* @param object $attempt The attempt, in which the question needs to be regraded.
1510* @param object $cmoptions
1511* @param boolean $verbose Optional. Whether to print progress information or not.
98f38217 1512* @param boolean $dryrun Optional. Whether to make changes to grades records
1513* or record that changes need to be made for a later regrade.
516cf3eb 1514*/
98f38217 1515function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose=false, $dryrun=false) {
aa9a6867 1516 global $DB, $OUTPUT;
516cf3eb 1517
1518 // load all states for this question in this attempt, ordered in sequence
eb84a826 1519 if ($states = $DB->get_records('question_states',
1520 array('attempt'=>$attempt->uniqueid, 'question'=>$question->id),
865b7534 1521 'seq_number ASC')) {
516cf3eb 1522 $states = array_values($states);
1523
1524 // Subtract the grade for the latest state from $attempt->sumgrades to get the
271ffe3f 1525 // sumgrades for the attempt without this question.
516cf3eb 1526 $attempt->sumgrades -= $states[count($states)-1]->grade;
1527
1528 // Initialise the replaystate
766df8f7 1529 $replaystate = question_load_specific_state($question, $cmoptions, $attempt, $states[0]->id);
1530 $replaystate->sumpenalty = 0;
1531 $replaystate->last_graded->sumpenalty = 0;
516cf3eb 1532
0a5b58af 1533 $changed = false;
516cf3eb 1534 for($j = 1; $j < count($states); $j++) {
4f48fb42 1535 restore_question_state($question, $states[$j]);
516cf3eb 1536 $action = new stdClass;
1537 $action->responses = $states[$j]->responses;
1538 $action->timestamp = $states[$j]->timestamp;
1539
f30bbcaf 1540 // Change event to submit so that it will be reprocessed
766df8f7 1541 if (in_array($states[$j]->event, array(QUESTION_EVENTCLOSE,
1542 QUESTION_EVENTGRADE, QUESTION_EVENTCLOSEANDGRADE))) {
f30bbcaf 1543 $action->event = QUESTION_EVENTSUBMIT;
516cf3eb 1544
1545 // By default take the event that was saved in the database
1546 } else {
1547 $action->event = $states[$j]->event;
1548 }
36be25f6 1549
5e60643e 1550 if ($action->event == QUESTION_EVENTMANUALGRADE) {
b9bd6da4 1551 // Ensure that the grade is in range - in the past this was not checked,
994c8c35 1552 // but now it is (MDL-14835) - so we need to ensure the data is valid before
1553 // proceeding.
1554 if ($states[$j]->grade < 0) {
1555 $states[$j]->grade = 0;
98f38217 1556 $changed = true;
994c8c35 1557 } else if ($states[$j]->grade > $question->maxgrade) {
1558 $states[$j]->grade = $question->maxgrade;
98f38217 1559 $changed = true;
117bd748 1560
994c8c35 1561 }
98f38217 1562 if (!$dryrun){
1563 $error = question_process_comment($question, $replaystate, $attempt,
1564 $replaystate->manualcomment, $states[$j]->grade);
1565 if (is_string($error)) {
aa9a6867 1566 echo $OUTPUT->notification($error);
98f38217 1567 }
1568 } else {
1569 $replaystate->grade = $states[$j]->grade;
994c8c35 1570 }
36be25f6 1571 } else {
36be25f6 1572 // Reprocess (regrade) responses
865b7534 1573 if (!question_process_responses($question, $replaystate,
766df8f7 1574 $action, $cmoptions, $attempt) && $verbose) {
1575 $a = new stdClass;
1576 $a->qid = $question->id;
1577 $a->stateid = $states[$j]->id;
aa9a6867 1578 echo $OUTPUT->notification(get_string('errorduringregrade', 'question', $a));
36be25f6 1579 }
98f38217 1580 // We need rounding here because grades in the DB get truncated
1581 // e.g. 0.33333 != 0.3333333, but we want them to be equal here
1582 if ((round((float)$replaystate->raw_grade, 5) != round((float)$states[$j]->raw_grade, 5))
1583 or (round((float)$replaystate->penalty, 5) != round((float)$states[$j]->penalty, 5))
1584 or (round((float)$replaystate->grade, 5) != round((float)$states[$j]->grade, 5))) {
1585 $changed = true;
1586 }
766df8f7 1587 // If this was previously a closed state, and it has been knoced back to
1588 // graded, then fix up the state again.
1589 if ($replaystate->event == QUESTION_EVENTGRADE &&
1590 ($states[$j]->event == QUESTION_EVENTCLOSE ||
1591 $states[$j]->event == QUESTION_EVENTCLOSEANDGRADE)) {
1592 $replaystate->event = $states[$j]->event;
1593 }
516cf3eb 1594 }
516cf3eb 1595
1596 $replaystate->id = $states[$j]->id;
271ffe3f 1597 $replaystate->changed = true;
d23e3e11 1598 $replaystate->update = true; // This will ensure that the existing database entry is updated rather than a new one created
98f38217 1599 if (!$dryrun){
1600 save_question_session($question, $replaystate);
1601 }
516cf3eb 1602 }
0a5b58af 1603 if ($changed) {
98f38217 1604 if (!$dryrun){
1605 // TODO, call a method in quiz to do this, where 'quiz' comes from
1606 // the question_attempts table.
1607 $DB->update_record('quiz_attempts', $attempt);
1608 }
1609 }
1610 if ($changed){
1611 $toinsert = new object();
1612 $toinsert->oldgrade = round((float)$states[count($states)-1]->grade, 5);
1613 $toinsert->newgrade = round((float)$replaystate->grade, 5);
1614 $toinsert->attemptid = $attempt->uniqueid;
1615 $toinsert->questionid = $question->id;
1616 //the grade saved is the old grade if the new grade is saved
1617 //it is the new grade if this is a dry run.
1618 $toinsert->regraded = $dryrun?0:1;
1619 $toinsert->timemodified = time();
1620 $DB->insert_record('quiz_question_regrade', $toinsert);
1621 return true;
1622 } else {
1623 return false;
516cf3eb 1624 }
516cf3eb 1625 }
0a5b58af 1626 return false;
516cf3eb 1627}
1628
1629/**
1630* Processes an array of student responses, grading and saving them as appropriate
1631*
50fcb1d8 1632 * @global array
516cf3eb 1633* @param object $question Full question object, passed by reference
1634* @param object $state Full state object, passed by reference
1635* @param object $action object with the fields ->responses which
1636* is an array holding the student responses,
4f48fb42 1637* ->action which specifies the action, e.g., QUESTION_EVENTGRADE,
516cf3eb 1638* and ->timestamp which is a timestamp from when the responses
1639* were submitted by the student.
1640* @param object $cmoptions
1641* @param object $attempt The attempt is passed by reference so that
1642* during grading its ->sumgrades field can be updated
373f0afd 1643* @return boolean Indicates success/failure
516cf3eb 1644*/
66d07f81 1645function question_process_responses($question, &$state, $action, $cmoptions, &$attempt) {
f02c6f01 1646 global $QTYPES;
516cf3eb 1647
1648 // if no responses are set initialise to empty response
1649 if (!isset($action->responses)) {
1650 $action->responses = array('' => '');
1651 }
1652
5e9170b4 1653 $state->newflaggedstate = !empty($action->responses['_flagged']);
1654
516cf3eb 1655 // make sure these are gone!
5e9170b4 1656 unset($action->responses['submit'], $action->responses['validate'], $action->responses['_flagged']);
516cf3eb 1657
1658 // Check the question session is still open
f30bbcaf 1659 if (question_state_is_closed($state)) {
516cf3eb 1660 return true;
1661 }
f30bbcaf 1662
516cf3eb 1663 // If $action->event is not set that implies saving
1664 if (! isset($action->event)) {
a6b691d8 1665 debugging('Ambiguous action in question_process_responses.' , DEBUG_DEVELOPER);
4f48fb42 1666 $action->event = QUESTION_EVENTSAVE;
516cf3eb 1667 }
f30bbcaf 1668 // If submitted then compare against last graded
516cf3eb 1669 // responses, not last given responses in this case
4f48fb42 1670 if (question_isgradingevent($action->event)) {
516cf3eb 1671 $state->responses = $state->last_graded->responses;
1672 }
271ffe3f 1673
516cf3eb 1674 // Check for unchanged responses (exactly unchanged, not equivalent).
1675 // We also have to catch questions that the student has not yet attempted
47cdbd1f 1676 $sameresponses = $QTYPES[$question->qtype]->compare_responses($question, $action, $state);
4f2c86ad 1677 if (!empty($state->last_graded) && $state->last_graded->event == QUESTION_EVENTOPEN &&
1678 question_isgradingevent($action->event)) {
47cdbd1f 1679 $sameresponses = false;
1680 }
516cf3eb 1681
f30bbcaf 1682 // If the response has not been changed then we do not have to process it again
1683 // unless the attempt is closing or validation is requested
4f48fb42 1684 if ($sameresponses and QUESTION_EVENTCLOSE != $action->event
5a14d563 1685 and QUESTION_EVENTVALIDATE != $action->event) {
516cf3eb 1686 return true;
1687 }
1688
1689 // Roll back grading information to last graded state and set the new
1690 // responses
1691 $newstate = clone($state->last_graded);
1692 $newstate->responses = $action->responses;
1693 $newstate->seq_number = $state->seq_number + 1;
1694 $newstate->changed = true; // will assure that it gets saved to the database
ca56222d 1695 $newstate->last_graded = clone($state->last_graded);
516cf3eb 1696 $newstate->timestamp = $action->timestamp;
5e9170b4 1697 $newstate->newflaggedstate = $state->newflaggedstate;
1698 $newstate->flagged = $state->flagged;
1699 $newstate->questionsessionid = $state->questionsessionid;
516cf3eb 1700 $state = $newstate;
1701
1702 // Set the event to the action we will perform. The question type specific
4f48fb42 1703 // grading code may override this by setting it to QUESTION_EVENTCLOSE if the
516cf3eb 1704 // attempt at the question causes the session to close
1705 $state->event = $action->event;
1706
4f48fb42 1707 if (!question_isgradingevent($action->event)) {
516cf3eb 1708 // Grade the response but don't update the overall grade
373f0afd 1709 if (!$QTYPES[$question->qtype]->grade_responses($question, $state, $cmoptions)) {
1710 return false;
1711 }
3bee1ead 1712
47e16978 1713 // Temporary hack because question types are not given enough control over what is going
1714 // on. Used by Opaque questions.
1715 // TODO fix this code properly.
1716 if (!empty($state->believeevent)) {
1717 // If the state was graded we need to ...
1718 if (question_state_is_graded($state)) {
1719 question_apply_penalty_and_timelimit($question, $state, $attempt, $cmoptions);
1720
1721 // update the attempt grade
1722 $attempt->sumgrades -= (float)$state->last_graded->grade;
1723 $attempt->sumgrades += (float)$state->grade;
3bee1ead 1724
47e16978 1725 // and update the last_graded field.
1726 unset($state->last_graded);
1727 $state->last_graded = clone($state);
1728 unset($state->last_graded->changed);
1729 }
1730 } else {
1731 // Don't allow the processing to change the event type
1732 $state->event = $action->event;
1733 }
3bee1ead 1734
ca56222d 1735 } else { // grading event
516cf3eb 1736
271ffe3f 1737 // Unless the attempt is closing, we want to work out if the current responses
1738 // (or equivalent responses) were already given in the last graded attempt.
5a14d563 1739 if(QUESTION_EVENTCLOSE != $action->event && QUESTION_EVENTOPEN != $state->last_graded->event &&
1740 $QTYPES[$question->qtype]->compare_responses($question, $state, $state->last_graded)) {
f30bbcaf 1741 $state->event = QUESTION_EVENTDUPLICATE;
516cf3eb 1742 }
f30bbcaf 1743
ca56222d 1744 // If we did not find a duplicate or if the attempt is closing, perform grading
5a14d563 1745 if ((!$sameresponses and QUESTION_EVENTDUPLICATE != $state->event) or
1746 QUESTION_EVENTCLOSE == $action->event) {
373f0afd 1747 if (!$QTYPES[$question->qtype]->grade_responses($question, $state, $cmoptions)) {
1748 return false;
1749 }
f30bbcaf 1750
f30bbcaf 1751 // Calculate overall grade using correct penalty method
1752 question_apply_penalty_and_timelimit($question, $state, $attempt, $cmoptions);
516cf3eb 1753 }
516cf3eb 1754
47e16978 1755 // If the state was graded we need to ...
ca56222d 1756 if (question_state_is_graded($state)) {
47e16978 1757 // update the attempt grade
1758 $attempt->sumgrades -= (float)$state->last_graded->grade;
1759 $attempt->sumgrades += (float)$state->grade;
1760
1761 // and update the last_graded field.
ca56222d 1762 unset($state->last_graded);
1763 $state->last_graded = clone($state);
1764 unset($state->last_graded->changed);
1765 }
516cf3eb 1766 }
1767 $attempt->timemodified = $action->timestamp;
1768
1769 return true;
1770}
1771
1772/**
1773* Determine if event requires grading
1774*/
4f48fb42 1775function question_isgradingevent($event) {
f30bbcaf 1776 return (QUESTION_EVENTSUBMIT == $event || QUESTION_EVENTCLOSE == $event);
516cf3eb 1777}
1778
516cf3eb 1779/**
1780* Applies the penalty from the previous graded responses to the raw grade
1781* for the current responses
1782*
1783* The grade for the question in the current state is computed by subtracting the
1784* penalty accumulated over the previous graded responses at the question from the
1785* raw grade. If the timestamp is more than 1 minute beyond the end of the attempt
1786* the grade is set to zero. The ->grade field of the state object is modified to
1787* reflect the new grade but is never allowed to decrease.
1788* @param object $question The question for which the penalty is to be applied.
1789* @param object $state The state for which the grade is to be set from the
1790* raw grade and the cumulative penalty from the last
1791* graded state. The ->grade field is updated by applying
1792* the penalty scheme determined in $cmoptions to the ->raw_grade and
1793* ->last_graded->penalty fields.
1794* @param object $cmoptions The options set by the course module.
1795* The ->penaltyscheme field determines whether penalties
1796* for incorrect earlier responses are subtracted.
1797*/
4f48fb42 1798function question_apply_penalty_and_timelimit(&$question, &$state, $attempt, $cmoptions) {
b423cff1 1799 // TODO. Quiz dependancy. The fact that the attempt that is passed in here
1800 // is from quiz_attempts, and we use things like $cmoptions->timelimit.
3bee1ead 1801
f30bbcaf 1802 // deal with penalty
516cf3eb 1803 if ($cmoptions->penaltyscheme) {
47e16978 1804 $state->grade = $state->raw_grade - $state->sumpenalty;
1805 $state->sumpenalty += (float) $state->penalty;
516cf3eb 1806 } else {
1807 $state->grade = $state->raw_grade;
1808 }
1809
1810 // deal with timelimit
1811 if ($cmoptions->timelimit) {
1812 // We allow for 5% uncertainty in the following test
84e628a0 1813 if ($state->timestamp - $attempt->timestart > $cmoptions->timelimit * 1.05) {
c7318911 1814 $cm = get_coursemodule_from_instance('quiz', $cmoptions->id);
1815 if (!has_capability('mod/quiz:ignoretimelimits', get_context_instance(CONTEXT_MODULE, $cm->id),
1816 $attempt->userid, false)) {
1817 $state->grade = 0;
1818 }
516cf3eb 1819 }
1820 }
1821
1822 // deal with closing time
1823 if ($cmoptions->timeclose and $state->timestamp > ($cmoptions->timeclose + 60) // allowing 1 minute lateness
1824 and !$attempt->preview) { // ignore closing time for previews
1825 $state->grade = 0;
1826 }
1827
1828 // Ensure that the grade does not go down
1829 $state->grade = max($state->grade, $state->last_graded->grade);
1830}
1831
516cf3eb 1832/**
1833* Print the icon for the question type
1834*
50fcb1d8 1835 * @global array
1836 * @global object
3843d274 1837* @param object $question The question object for which the icon is required
1838* only $question->qtype is used.
1839* @param boolean $return If true the functions returns the link as a string
516cf3eb 1840*/
dcc2ffde 1841function print_question_icon($question, $return = false) {
dc1f00de 1842 global $QTYPES, $CFG;
516cf3eb 1843
b9d4b031 1844 if (array_key_exists($question->qtype, $QTYPES)) {
05efb0e5 1845 $namestr = $QTYPES[$question->qtype]->local_name();
b9d4b031 1846 } else {
1847 $namestr = 'missingtype';
1848 }
dcc2ffde 1849 $html = '<img src="' . $CFG->wwwroot . '/question/type/' .
1850 $question->qtype . '/icon.gif" alt="' .
1851 $namestr . '" title="' . $namestr . '" />';
516cf3eb 1852 if ($return) {
1853 return $html;
1854 } else {
1855 echo $html;
1856 }
1857}
1858
516cf3eb 1859/**
1860* Returns a html link to the question image if there is one
1861*
50fcb1d8 1862 * @global object
1863 * @global object
516cf3eb 1864* @return string The html image tag or the empy string if there is no image.
1865* @param object $question The question object
1866*/
9fc3100f 1867function get_question_image($question) {
eb84a826 1868 global $CFG, $DB;
516cf3eb 1869 $img = '';
1870
eb84a826 1871 if (!$category = $DB->get_record('question_categories', array('id'=>$question->category))) {
d89ec019 1872 print_error('invalidcategory');
9fc3100f 1873 }
1874 $coursefilesdir = get_filesdir_from_context(get_context_instance_by_id($category->contextid));
1875
516cf3eb 1876 if ($question->image) {
1877
1878 if (substr(strtolower($question->image), 0, 7) == 'http://') {
1879 $img .= $question->image;
1880
516cf3eb 1881 } else {
5a254a29 1882 require_once($CFG->libdir .'/filelib.php');
bbf795df 1883 $img = get_file_url("$coursefilesdir/{$question->image}");
117bd748 1884 }
516cf3eb 1885 }
1886 return $img;
1887}
b6e907a2 1888
50fcb1d8 1889/**
1890 * @global array
1891 */
e8f99abc 1892function question_print_comment_fields($question, $state, $prefix, $cmoptions, $caption = '') {
83f0fef5 1893 global $QTYPES;
766df8f7 1894 $idprefix = preg_replace('/[^-_a-zA-Z0-9]/', '', $prefix);
574c3750 1895 $otherquestionsinuse = '';
1896 if (!empty($cmoptions->questions)) {
1897 $otherquestionsinuse = $cmoptions->questions;
1898 }
1899 if (!question_state_is_graded($state) && $QTYPES[$question->qtype]->is_question_manual_graded($question, $otherquestionsinuse)) {
83f0fef5 1900 $grade = '';
1901 } else {
1902 $grade = question_format_grade($cmoptions, $state->last_graded->grade);
1903 }
766df8f7 1904 $maxgrade = question_format_grade($cmoptions, $question->maxgrade);
1905 $fieldsize = strlen($maxgrade) - 1;
e8f99abc 1906 if (empty($caption)) {
1907 $caption = format_string($question->name);
1908 }
1909 ?>
1910<fieldset class="que comment clearfix">
1911 <legend class="ftoggler"><?php echo $caption; ?></legend>
1912 <div class="fcontainer clearfix">
1913 <div class="fitem">
1914 <div class="fitemtitle">
1915 <label for="<?php echo $idprefix; ?>_comment_box"><?php print_string('comment', 'quiz'); ?></label>
1916 </div>
1917 <div class="felement fhtmleditor">
1918 <?php print_textarea(can_use_html_editor(), 15, 60, 630, 300, $prefix . '[comment]',
1919 $state->manualcomment, 0, false, $idprefix . '_comment_box'); ?>
1920 </div>
1921 </div>
1922 <div class="fitem">
1923 <div class="fitemtitle">
1924 <label for="<?php echo $idprefix; ?>_grade_field"><?php print_string('grade', 'quiz'); ?></label>
1925 </div>
1926 <div class="felement ftext">
1927 <input type="text" name="<?php echo $prefix; ?>[grade]" size="<?php echo $fieldsize; ?>" id="<?php echo $idprefix; ?>_grade_field" value="<?php echo $grade; ?>" /> / <?php echo $maxgrade; ?>
1928 </div>
1929 </div>
1930 </div>
1931</fieldset>
1932 <?php
b6e907a2 1933}
1934
994c8c35 1935/**
1936 * Process a manual grading action. That is, use $comment and $grade to update
1937 * $state and $attempt. The attempt and the comment text are stored in the
1938 * database. $state is only updated in memory, it is up to the call to store
1939 * that, if appropriate.
1940 *
50fcb1d8 1941 * @global object
994c8c35 1942 * @param object $question the question
1943 * @param object $state the state to be updated.
1944 * @param object $attempt the attempt the state belongs to, to be updated.
83f0fef5 1945 * @param string $comment the new comment from the teacher.
1946 * @param mixed $grade the grade the teacher assigned, or '' to not change the grade.
994c8c35 1947 * @return mixed true on success, a string error message if a problem is detected
1948 * (for example score out of range).
1949 */
b6e907a2 1950function question_process_comment($question, &$state, &$attempt, $comment, $grade) {
eb84a826 1951 global $DB;
1952
83f0fef5 1953 $grade = trim($grade);
994c8c35 1954 if ($grade < 0 || $grade > $question->maxgrade) {
1955 $a = new stdClass;
1956 $a->grade = $grade;
1957 $a->maxgrade = $question->maxgrade;
1958 $a->name = $question->name;
1959 return get_string('errormanualgradeoutofrange', 'question', $a);
1960 }
b6e907a2 1961
1962 // Update the comment and save it in the database
fca490bc 1963 $comment = trim($comment);
3e3e5a40 1964 $state->manualcomment = $comment;
eb84a826 1965 if (!$DB->set_field('question_sessions', 'manualcomment', $comment, array('attemptid'=>$attempt->uniqueid, 'questionid'=>$question->id))) {
994c8c35 1966 return get_string('errorsavingcomment', 'question', $question);
b6e907a2 1967 }
95ac8a40 1968
1969 // Update the attempt if the score has changed.
83f0fef5 1970 if ($grade !== '' && (abs($state->last_graded->grade - $grade) > 0.002 || $state->last_graded->event != QUESTION_EVENTMANUALGRADE)) {
b6e907a2 1971 $attempt->sumgrades = $attempt->sumgrades - $state->last_graded->grade + $grade;
1972 $attempt->timemodified = time();
bb4b6010 1973 $DB->update_record('quiz_attempts', $attempt);
84bf852c 1974
1975 // We want to update existing state (rather than creating new one) if it
1976 // was itself created by a manual grading event.
83f0fef5 1977 $state->update = $state->event == QUESTION_EVENTMANUALGRADE;
84bf852c 1978
1979 // Update the other parts of the state object.
b6e907a2 1980 $state->raw_grade = $grade;
1981 $state->grade = $grade;
1982 $state->penalty = 0;
1983 $state->timestamp = time();
36be25f6 1984 $state->seq_number++;
95ac8a40 1985 $state->event = QUESTION_EVENTMANUALGRADE;
1986
b6e907a2 1987 // Update the last graded state (don't simplify!)
1988 unset($state->last_graded);
1989 $state->last_graded = clone($state);
95ac8a40 1990
1991 // We need to indicate that the state has changed in order for it to be saved.
1992 $state->changed = 1;
b6e907a2 1993 }
1994
994c8c35 1995 return true;
b6e907a2 1996}
1997
516cf3eb 1998/**
1999* Construct name prefixes for question form element names
2000*
2001* Construct the name prefix that should be used for example in the
2002* names of form elements created by questions.
4f48fb42 2003* This is called by {@link get_question_options()}
516cf3eb 2004* to set $question->name_prefix.
2005* This name prefix includes the question id which can be
4f48fb42 2006* extracted from it with {@link question_get_id_from_name_prefix()}.
516cf3eb 2007*
2008* @return string
2009* @param integer $id The question id
2010*/
4f48fb42 2011function question_make_name_prefix($id) {
516cf3eb 2012 return 'resp' . $id . '_';
2013}
2014
2015/**
f62040ed 2016 * Extract question id from the prefix of form element names
2017 *
2018 * @return integer The question id
2019 * @param string $name The name that contains a prefix that was
2020 * constructed with {@link question_make_name_prefix()}
2021 */
4f48fb42 2022function question_get_id_from_name_prefix($name) {
f62040ed 2023 if (!preg_match('/^resp([0-9]+)_/', $name, $matches)) {
516cf3eb 2024 return false;
f62040ed 2025 }
516cf3eb 2026 return (integer) $matches[1];
2027}
2028
f62040ed 2029/**
2030 * Extract question id from the prefix of form element names
2031 *
2032 * @return integer The question id
2033 * @param string $name The name that contains a prefix that was
2034 * constructed with {@link question_make_name_prefix()}
2035 */
2036function question_id_and_key_from_post_name($name) {
2037 if (!preg_match('/^resp([0-9]+)_(.*)$/', $name, $matches)) {
2038 return array(false, false);
2039 }
2040 return array((integer) $matches[1], $matches[2]);
2041}
2042
4f48fb42 2043/**
4dca7e51 2044 * Returns the unique id for a new attempt
2045 *
2046 * Every module can keep their own attempts table with their own sequential ids but
2047 * the question code needs to also have a unique id by which to identify all these
2048 * attempts. Hence a module, when creating a new attempt, calls this function and
2049 * stores the return value in the 'uniqueid' field of its attempts table.
50fcb1d8 2050 *
2051 * @global object
4f48fb42 2052 */
36be25f6 2053function question_new_attempt_uniqueid($modulename='quiz') {
eb84a826 2054 global $DB;
2055
d7444d44 2056 $attempt = new stdClass;
36be25f6 2057 $attempt->modulename = $modulename;
a8d6ef8c 2058 $id = $DB->insert_record('question_attempts', $attempt);
36be25f6 2059 return $id;
516cf3eb 2060}
2061
36be25f6 2062/**
2063 * Creates a stamp that uniquely identifies this version of the question
cbe20043 2064 *
2065 * In future we want this to use a hash of the question data to guarantee that
2066 * identical versions have the same version stamp.
2067 *
2068 * @param object $question
2069 * @return string A unique version stamp
2070 */
2071function question_hash($question) {
2072 return make_unique_id_code();
2073}
2074
f88fb62c 2075/**
2076 * Round a grade to to the correct number of decimal places, and format it for display.
84e628a0 2077 * If $cmoptions->questiondecimalpoints is set, that is used, otherwise
2078 * else if $cmoptions->decimalpoints is used,
2079 * otherwise a default of 2 is used, but this should not be relied upon, and generated a developer debug warning.
2080 * However, if $cmoptions->questiondecimalpoints is -1, the means use $cmoptions->decimalpoints.
f88fb62c 2081 *
84e628a0 2082 * @param object $cmoptions The modules settings.
f88fb62c 2083 * @param float $grade The grade to round.
2084 */
2085function question_format_grade($cmoptions, $grade) {
84e628a0 2086 if (isset($cmoptions->questiondecimalpoints) && $cmoptions->questiondecimalpoints != -1) {
2087 $decimalplaces = $cmoptions->questiondecimalpoints;
2088 } else if (isset($cmoptions->decimalpoints)) {
2089 $decimalplaces = $cmoptions->decimalpoints;
2090 } else {
2091 $decimalplaces = 2;
2092 debugging('Code that leads to question_format_grade being called should set ' .
2093 '$cmoptions->questiondecimalpoints or $cmoptions->decimalpoints', DEBUG_DEVELOPER);
2094 }
2095 return format_float($grade, $decimalplaces);
f88fb62c 2096}
516cf3eb 2097
62e76c67 2098/**
2099 * @return string An inline script that creates a JavaScript object storing
2100 * various strings and bits of configuration that the scripts in qengine.js need
2101 * to get from PHP.
2102 */
ddedf979 2103function question_init_qengine_js() {
2104 global $CFG, $PAGE, $OUTPUT;
62e76c67 2105 $config = array(
ddedf979 2106 'actionurl' => $CFG->wwwroot . '/question/toggleflag.php',
06f87d2f
TH
2107 'flagicon' => '' . $OUTPUT->pix_url('i/flagged'),
2108 'unflagicon' => '' . $OUTPUT->pix_url('i/unflagged'),
62e76c67 2109 'flagtooltip' => get_string('clicktoflag', 'question'),
2110 'unflagtooltip' => get_string('clicktounflag', 'question'),
2111 'flaggedalt' => get_string('flagged', 'question'),
2112 'unflaggedalt' => get_string('notflagged', 'question'),
2113 );
cf615522 2114 $PAGE->requires->data_for_js('qengine_config', $config);
62e76c67 2115}
2116
516cf3eb 2117/// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS //////////////////////////////////
99ba746d 2118/**
25ddb7ef 2119 * Give the questions in $questionlist a chance to request the CSS or JavaScript
2120 * they need, before the header is printed.
2121 *
2122 * If your code is going to call the print_question function, it must call this
2123 * funciton before print_header.
99ba746d 2124 *
2125 * @param array $questionlist a list of questionids of the questions what will appear on this page.
2126 * @param array $questions an array of question objects, whose keys are question ids.
2127 * Must contain all the questions in $questionlist
2128 * @param array $states an array of question state objects, whose keys are question ids.
2129 * Must contain the state of all the questions in $questionlist
99ba746d 2130 */
e1dc480c 2131function get_html_head_contributions($questionlist, &$questions, &$states) {
c28bf5c9 2132 global $CFG, $PAGE, $QTYPES;
62e76c67 2133
2134 // The question engine's own JavaScript.
f44b10ed 2135 $PAGE->requires->yui2_lib('connection');
9dec75db 2136 $PAGE->requires->js('/question/qengine.js');
ddedf979 2137 question_init_qengine_js();
516cf3eb 2138
62e76c67 2139 // Anything that questions on this page need.
99ba746d 2140 foreach ($questionlist as $questionid) {
2141 $question = $questions[$questionid];
34a2777c 2142 $QTYPES[$question->qtype]->get_html_head_contributions($question, $states[$questionid]);
99ba746d 2143 }
99ba746d 2144}
2145
50da63eb 2146/**
25ddb7ef 2147 * Like {@link get_html_head_contributions()} but for the editing page
50da63eb 2148 * question/question.php.
2149 *
2150 * @param $question A question object. Only $question->qtype is used.
25ddb7ef 2151 * @return string Deprecated. Some HTML code that can go inside the head tag.
50da63eb 2152 */
2153function get_editing_head_contributions($question) {
2154 global $QTYPES;
34a2777c 2155 $QTYPES[$question->qtype]->get_editing_head_contributions();
50da63eb 2156}
2157
99ba746d 2158/**
4dca7e51 2159 * Prints a question
2160 *
2161 * Simply calls the question type specific print_question() method.
50fcb1d8 2162 *
2163 * @global array
4dca7e51 2164 * @param object $question The question to be rendered.
2165 * @param object $state The state to render the question in.
2166 * @param integer $number The number for this question.
2167 * @param object $cmoptions The options specified by the course module
2168 * @param object $options An object specifying the rendering options.
2169 */
4f48fb42 2170function print_question(&$question, &$state, $number, $cmoptions, $options=null) {
f02c6f01 2171 global $QTYPES;
426091ee 2172 $QTYPES[$question->qtype]->print_question($question, $state, $number, $cmoptions, $options);
516cf3eb 2173}
f5565b69 2174/**
99ba746d 2175 * Saves question options
2176 *
2177 * Simply calls the question type specific save_question_options() method.
50fcb1d8 2178 *
2179 * @global array
99ba746d 2180 */
f5565b69 2181function save_question_options($question) {
2182 global $QTYPES;
2183
2184 $QTYPES[$question->qtype]->save_question_options($question);
2185}
516cf3eb 2186
2187/**
2188* Gets all teacher stored answers for a given question
2189*
2190* Simply calls the question type specific get_all_responses() method.
50fcb1d8 2191 *
2192 * @global array
516cf3eb 2193*/
2194// ULPGC ecastro
4f48fb42 2195function get_question_responses($question, $state) {
f02c6f01 2196 global $QTYPES;
2197 $r = $QTYPES[$question->qtype]->get_all_responses($question, $state);
516cf3eb 2198 return $r;
2199}
2200
516cf3eb 2201/**
dc1f00de 2202* Gets the response given by the user in a particular state
516cf3eb 2203*
2204* Simply calls the question type specific get_actual_response() method.
50fcb1d8 2205 *
2206 * @global array
516cf3eb 2207*/
2208// ULPGC ecastro
4f48fb42 2209function get_question_actual_response($question, $state) {
f02c6f01 2210 global $QTYPES;
516cf3eb 2211
f02c6f01 2212 $r = $QTYPES[$question->qtype]->get_actual_response($question, $state);
516cf3eb 2213 return $r;
2214}
2215
2216/**
dc1f00de 2217* TODO: document this
50fcb1d8 2218 *
2219 * @global array
516cf3eb 2220*/
2221// ULPGc ecastro
4f48fb42 2222function get_question_fraction_grade($question, $state) {
f02c6f01 2223 global $QTYPES;
516cf3eb 2224
f02c6f01 2225 $r = $QTYPES[$question->qtype]->get_fractional_grade($question, $state);
516cf3eb 2226 return $r;
2227}
6f51ed72 2228/**
50fcb1d8 2229 * @global array
6f51ed72 2230* @return integer grade out of 1 that a random guess by a student might score.
2231*/
2232// ULPGc ecastro
455c3efa 2233function question_get_random_guess_score($question) {
6f51ed72 2234 global $QTYPES;
117bd748 2235
6f51ed72 2236 $r = $QTYPES[$question->qtype]->get_random_guess_score($question);
2237 return $r;
2238}
516cf3eb 2239/// CATEGORY FUNCTIONS /////////////////////////////////////////////////////////////////
2240
6cb4910c 2241/**
2242 * returns the categories with their names ordered following parent-child relationships
2243 * finally it tries to return pending categories (those being orphaned, whose parent is
2244 * incorrect) to avoid missing any category from original array.
50fcb1d8 2245 *
2246 * @global object
6cb4910c 2247 */
2248function sort_categories_by_tree(&$categories, $id = 0, $level = 1) {
eb84a826 2249 global $DB;
2250
6cb4910c 2251 $children = array();
2252 $keys = array_keys($categories);
2253
2254 foreach ($keys as $key) {
2255 if (!isset($categories[$key]->processed) && $categories[$key]->parent == $id) {
2256 $children[$key] = $categories[$key];
2257 $categories[$key]->processed = true;
2258 $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1);
2259 }
2260 }
2261 //If level = 1, we have finished, try to look for non processed categories (bad parent) and sort them too
2262 if ($level == 1) {
2263 foreach ($keys as $key) {
2264 //If not processed and it's a good candidate to start (because its parent doesn't exist in the course)
eb84a826 2265 if (!isset($categories[$key]->processed) && !$DB->record_exists('question_categories', array('course'=>$categories[$key]->course, 'id'=>$categories[$key]->parent))) {
6cb4910c 2266 $children[$key] = $categories[$key];
2267 $categories[$key]->processed = true;
2268 $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1);
2269 }
2270 }
2271 }
2272 return $children;
2273}
2274
2275/**
062a7522 2276 * Private method, only for the use of add_indented_names().
271ffe3f 2277 *
062a7522 2278 * Recursively adds an indentedname field to each category, starting with the category
271ffe3f 2279 * with id $id, and dealing with that category and all its children, and
062a7522 2280 * return a new array, with those categories in the right order.
2281 *
271ffe3f 2282 * @param array $categories an array of categories which has had childids
062a7522 2283 * fields added by flatten_category_tree(). Passed by reference for
2284 * performance only. It is not modfied.
2285 * @param int $id the category to start the indenting process from.
2286 * @param int $depth the indent depth. Used in recursive calls.
2287 * @return array a new array of categories, in the right order for the tree.
6cb4910c 2288 */
3bee1ead 2289function flatten_category_tree(&$categories, $id, $depth = 0, $nochildrenof = -1) {
271ffe3f 2290
062a7522 2291 // Indent the name of this category.
2292 $newcategories = array();
2293 $newcategories[$id] = $categories[$id];
2294 $newcategories[$id]->indentedname = str_repeat('&nbsp;&nbsp;&nbsp;', $depth) . $categories[$id]->name;
271ffe3f 2295
062a7522 2296 // Recursively indent the children.
2297 foreach ($categories[$id]->childids as $childid) {
3bee1ead 2298 if ($childid != $nochildrenof){
2299 $newcategories = $newcategories + flatten_category_tree($categories, $childid, $depth + 1, $nochildrenof);
2300 }
6cb4910c 2301 }
271ffe3f 2302
062a7522 2303 // Remove the childids array that were temporarily added.
2304 unset($newcategories[$id]->childids);
271ffe3f 2305
062a7522 2306 return $newcategories;
6cb4910c 2307}
2308
2309/**
062a7522 2310 * Format categories into an indented list reflecting the tree structure.
271ffe3f 2311 *
062a7522 2312 * @param array $categories An array of category objects, for example from the.
2313 * @return array The formatted list of categories.
6cb4910c 2314 */
3bee1ead 2315function add_indented_names($categories, $nochildrenof = -1) {
6cb4910c 2316
271ffe3f 2317 // Add an array to each category to hold the child category ids. This array will be removed
062a7522 2318 // again by flatten_category_tree(). It should not be used outside these two functions.
2319 foreach (array_keys($categories) as $id) {
2320 $categories[$id]->childids = array();
6cb4910c 2321 }
2322
062a7522 2323 // Build the tree structure, and record which categories are top-level.
271ffe3f 2324 // We have to be careful, because the categories array may include published
062a7522 2325 // categories from other courses, but not their parents.
2326 $toplevelcategoryids = array();
2327 foreach (array_keys($categories) as $id) {
2328 if (!empty($categories[$id]->parent) && array_key_exists($categories[$id]->parent, $categories)) {
2329 $categories[$categories[$id]->parent]->childids[] = $id;
2330 } else {
2331 $toplevelcategoryids[] = $id;
6cb4910c 2332 }
2333 }
2334
062a7522 2335 // Flatten the tree to and add the indents.
2336 $newcategories = array();
2337 foreach ($toplevelcategoryids as $id) {
3bee1ead 2338 $newcategories = $newcategories + flatten_category_tree($categories, $id, 0, $nochildrenof);
6cb4910c 2339 }
2340
062a7522 2341 return $newcategories;
6cb4910c 2342}
2dd6d66b 2343
516cf3eb 2344/**
062a7522 2345 * Output a select menu of question categories.
271ffe3f 2346 *
062a7522 2347 * Categories from this course and (optionally) published categories from other courses
271ffe3f 2348 * are included. Optionally, only categories the current user may edit can be included.
062a7522 2349 *
2350 * @param integer $courseid the id of the course to get the categories for.
2351 * @param integer $published if true, include publised categories from other courses.
2352 * @param integer $only_editable if true, exclude categories this user is not allowed to edit.
2353 * @param integer $selected optionally, the id of a category to be selected by default in the dropdown.
2354 */
3bee1ead 2355function question_category_select_menu($contexts, $top = false, $currentcat = 0, $selected = "", $nochildrenof = -1) {
641454c6 2356 global $OUTPUT;
3bee1ead 2357 $categoriesarray = question_category_options($contexts, $top, $currentcat, false, $nochildrenof);
3ed998f3 2358 if ($selected) {
6770330d 2359 $choose = '';
3ed998f3 2360 } else {
6770330d 2361 $choose = 'choosedots';
516cf3eb 2362 }
6770330d
PS
2363 $options = array();
2364 foreach($categoriesarray as $group=>$opts) {
2365 $options[] = array($group=>$opts);
2366 }
2367
2368 echo html_writer::select($options, 'category', $selected, $choose);
516cf3eb 2369}
2370
cca6e300 2371/**
50fcb1d8 2372 * @global object
cca6e300 2373 * @param integer $contextid a context id.
2374 * @return object the default question category for that context, or false if none.
2375 */
2376function question_get_default_category($contextid) {
2377 global $DB;
2378 $category = $DB->get_records('question_categories', array('contextid' => $contextid),'id','*',0,1);
2379 if (!empty($category)) {
2380 return reset($category);
2381 } else {
2382 return false;
2383 }
2384}
2385
2386/**
50fcb1d8 2387 * @global object
2388 * @global object
cca6e300 2389 * @param object $context a context
2390 * @return string A URL for editing questions in this context.
2391 */
2392function question_edit_url($context) {
2393 global $CFG, $SITE;
2394 if (!has_any_capability(question_get_question_capabilities(), $context)) {
2395 return false;
2396 }
2397 $baseurl = $CFG->wwwroot . '/question/edit.php?';
2398 $defaultcategory = question_get_default_category($context->id);
2399 if ($defaultcategory) {
bdff6b85 2400 $baseurl .= 'cat=' . $defaultcategory->id . ',' . $context->id . '&amp;';
cca6e300 2401 }
2402 switch ($context->contextlevel) {
2403 case CONTEXT_SYSTEM:
2404 return $baseurl . 'courseid=' . $SITE->id;
2405 case CONTEXT_COURSECAT:
2406 // This is nasty, becuase we can only edit questions in a course
2407 // context at the moment, so for now we just return false.
2408 return false;
2409 case CONTEXT_COURSE:
2410 return $baseurl . 'courseid=' . $context->instanceid;
2411 case CONTEXT_MODULE:
2412 return $baseurl . 'cmid=' . $context->instanceid;
2413 }
117bd748 2414
cca6e300 2415}
2416
8f0f605d 2417/**
2418* Gets the default category in the most specific context.
2419* If no categories exist yet then default ones are created in all contexts.
2420*
50fcb1d8 2421 * @global object
8f0f605d 2422* @param array $contexts The context objects for this context and all parent contexts.
2423* @return object The default category - the category in the course context
2424*/
2425function question_make_default_categories($contexts) {
eb84a826 2426 global $DB;
353b2d70 2427 static $preferredlevels = array(
2428 CONTEXT_COURSE => 4,
2429 CONTEXT_MODULE => 3,
2430 CONTEXT_COURSECAT => 2,
2431 CONTEXT_SYSTEM => 1,
2432 );
eb84a826 2433
8f0f605d 2434 $toreturn = null;
353b2d70 2435 $preferredness = 0;
8f0f605d 2436 // If it already exists, just return it.
2437 foreach ($contexts as $key => $context) {
353b2d70 2438 if (!$exists = $DB->record_exists("question_categories", array('contextid'=>$context->id))) {
eb84a826 2439 // Otherwise, we need to make one
2440 $category = new stdClass;
2441 $contextname = print_context_name($context, false, true);
2442 $category->name = get_string('defaultfor', 'question', $contextname);
2443 $category->info = get_string('defaultinfofor', 'question', $contextname);
2444 $category->contextid = $context->id;
2445 $category->parent = 0;
2446 $category->sortorder = 999; // By default, all categories get this number, and are sorted alphabetically.
2447 $category->stamp = make_unique_id_code();
bf8e93d7 2448 $category->id = $DB->insert_record('question_categories', $category);
b9bd6da4 2449 } else {
cca6e300 2450 $category = question_get_default_category($context->id);
8f0f605d 2451 }
80f7bafd 2452 if ($preferredlevels[$context->contextlevel] > $preferredness &&
2453 has_any_capability(array('moodle/question:usemine', 'moodle/question:useall'), $context)) {
353b2d70 2454 $toreturn = $category;
2455 $preferredness = $preferredlevels[$context->contextlevel];
8f0f605d 2456 }
2457 }
2458
353b2d70 2459 if (!is_null($toreturn)) {
2460 $toreturn = clone($toreturn);
2461 }
8f0f605d 2462 return $toreturn;
2463}
2464
375ed78a 2465/**
3bee1ead 2466 * Get all the category objects, including a count of the number of questions in that category,
2467 * for all the categories in the lists $contexts.
375ed78a 2468 *
50fcb1d8 2469 * @global object
3bee1ead 2470 * @param mixed $contexts either a single contextid, or a comma-separated list of context ids.
2471 * @param string $sortorder used as the ORDER BY clause in the select statement.
2472 * @return array of category objects.
375ed78a 2473 */
3bee1ead 2474function get_categories_for_contexts($contexts, $sortorder = 'parent, sortorder, name ASC') {
eb84a826 2475 global $DB;
2476 return $DB->get_records_sql("
630a3dc3 2477 SELECT c.*, (SELECT count(1) FROM {question} q
eb84a826 2478 WHERE c.id = q.category AND q.hidden='0' AND q.parent='0') AS questioncount
2479 FROM {question_categories} c
2480 WHERE c.contextid IN ($contexts)
2481 ORDER BY $sortorder");
3bee1ead 2482}
3a3c454e 2483
3bee1ead 2484/**
2485 * Output an array of question categories.
50fcb1d8 2486 * @global object
3bee1ead 2487 */
2488function question_category_options($contexts, $top = false, $currentcat = 0, $popupform = false, $nochildrenof = -1) {
2489 global $CFG;
2490 $pcontexts = array();
2491 foreach($contexts as $context){
2492 $pcontexts[] = $context->id;
61d9b9fc 2493 }
3bee1ead 2494 $contextslist = join($pcontexts, ', ');
2495
2496 $categories = get_categories_for_contexts($contextslist);
375ed78a 2497
3bee1ead 2498 $categories = question_add_context_in_key($categories);
2499
2500 if ($top){
2501 $categories = question_add_tops($categories, $pcontexts);
2502 }
2503 $categories = add_indented_names($categories, $nochildrenof);
3ed998f3 2504
3bee1ead 2505 //sort cats out into different contexts
375ed78a 2506 $categoriesarray = array();
3bee1ead 2507 foreach ($pcontexts as $pcontext){
2508 $contextstring = print_context_name(get_context_instance_by_id($pcontext), true, true);
2509 foreach ($categories as $category) {
2510 if ($category->contextid == $pcontext){
2511 $cid = $category->id;
2512 if ($currentcat!= $cid || $currentcat==0) {
1e05bf54 2513 $countstring = (!empty($category->questioncount))?" ($category->questioncount)":'';
3bee1ead 2514 $categoriesarray[$contextstring][$cid] = $category->indentedname.$countstring;
2515 }
2516 }
375ed78a 2517 }
2518 }
3bee1ead 2519 if ($popupform){
2520 $popupcats = array();
2521 foreach ($categoriesarray as $contextstring => $optgroup){
94fa6be4
PS
2522 $group = array();
2523 foreach ($optgroup as $key=>$value) {
2524 $key = str_replace($CFG->wwwroot, '', $key);
2525 $group[$key] = $value;
2526 }
2527 $popupcats[] = array($contextstring=>$group);
3bee1ead 2528 }
2529 return $popupcats;
2530 } else {
2531 return $categoriesarray;
2532 }
375ed78a 2533}
2534
3bee1ead 2535function question_add_context_in_key($categories){
2536 $newcatarray = array();
2537 foreach ($categories as $id => $category) {
2538 $category->parent = "$category->parent,$category->contextid";
2539 $category->id = "$category->id,$category->contextid";
2540 $newcatarray["$id,$category->contextid"] = $category;
516cf3eb 2541 }
3bee1ead 2542 return $newcatarray;
2543}
2544function question_add_tops($categories, $pcontexts){
2545 $topcats = array();
2546 foreach ($pcontexts as $context){
2547 $newcat = new object();
2548 $newcat->id = "0,$context";
2549 $newcat->name = get_string('top');
2550 $newcat->parent = -1;
2551 $newcat->contextid = $context;
2552 $topcats["0,$context"] = $newcat;
2553 }
2554 //put topcats in at beginning of array - they'll be sorted into different contexts later.
2555 return array_merge($topcats, $categories);
516cf3eb 2556}
2557
516cf3eb 2558/**
b20ea669 2559 * Returns a comma separated list of ids of the category and all subcategories
50fcb1d8 2560 * @global object
b20ea669 2561 */
dc1f00de 2562function question_categorylist($categoryid) {
eb84a826 2563 global $DB;
2564
516cf3eb 2565 // returns a comma separated list of ids of the category and all subcategories
2566 $categorylist = $categoryid;
df4e2244 2567 if ($subcategories = $DB->get_records('question_categories', array('parent'=>$categoryid), 'sortorder ASC', 'id, 1')) {
516cf3eb 2568 foreach ($subcategories as $subcategory) {
dc1f00de 2569 $categorylist .= ','. question_categorylist($subcategory->id);
516cf3eb 2570 }
2571 }
2572 return $categorylist;
2573}
2574
f5c15407 2575
3bee1ead 2576
516cf3eb 2577
947217d7 2578//===========================
2579// Import/Export Functions
2580//===========================
2581
ff4b6492 2582/**
2583 * Get list of available import or export formats
50fcb1d8 2584 *
2585 * @global object
ff4b6492 2586 * @param string $type 'import' if import list, otherwise export list assumed
271ffe3f 2587 * @return array sorted list of import/export formats available
50fcb1d8 2588 */
ff4b6492 2589function get_import_export_formats( $type ) {
2590
2591 global $CFG;
17da2e6f 2592 $fileformats = get_plugin_list("qformat");
ff4b6492 2593
2594 $fileformatname=array();
947217d7 2595 require_once( "{$CFG->dirroot}/question/format.php" );
17da2e6f 2596 foreach ($fileformats as $fileformat=>$fdir) {
2597 $format_file = "$fdir/format.php";
2598 if (file_exists($format_file) ) {
2599 require_once($format_file);
ff4b6492 2600 }
2601 else {
2602 continue;
2603 }
70c01adb 2604 $classname = "qformat_$fileformat";
ff4b6492 2605 $format_class = new $classname();
2606 if ($type=='import') {
2607 $provided = $format_class->provide_import();
2608 }
2609 else {
2610 $provided = $format_class->provide_export();
2611 }
2612 if ($provided) {
2613 $formatname = get_string($fileformat, 'quiz');
2614 if ($formatname == "[[$fileformat]]") {
b0ea27b4 2615 $formatname = get_string($fileformat, 'qformat_'.$fileformat);
2616 if ($formatname == "[[$fileformat]]") {
2617 $formatname = $fileformat; // Just use the raw folder name
2618 }
ff4b6492 2619 }
2620 $fileformatnames[$fileformat] = $formatname;
2621 }
2622 }
2623 natcasesort($fileformatnames);
2624
2625 return $fileformatnames;
2626}
feb60a07 2627
2628
2629/**
2630* Create default export filename
2631*
2632* @return string default export filename
2633* @param object $course
2634* @param object $category
2635*/
2636function default_export_filename($course,$category) {
2637 //Take off some characters in the filename !!
2638 $takeoff = array(" ", ":", "/", "\\", "|");
57f1b914 2639 $export_word = str_replace($takeoff,"_",moodle_strtolower(get_string("exportfilename","quiz")));
feb60a07 2640 //If non-translated, use "export"
2641 if (substr($export_word,0,1) == "[") {
2642 $export_word= "export";
2643 }
2644
2645 //Calculate the date format string
2646 $export_date_format = str_replace(" ","_",get_string("exportnameformat","quiz"));
2647 //If non-translated, use "%Y%m%d-%H%M"
2648 if (substr($export_date_format,0,1) == "[") {
2649 $export_date_format = "%%Y%%m%%d-%%H%%M";
2650 }
2651
2652 //Calculate the shortname
2653 $export_shortname = clean_filename($course->shortname);
2654 if (empty($export_shortname) or $export_shortname == '_' ) {
2655 $export_shortname = $course->id;
2656 }
2657
2658 //Calculate the category name
2659 $export_categoryname = clean_filename($category->name);
2660
2661 //Calculate the final export filename
2662 //The export word
2663 $export_name = $export_word."-";
2664 //The shortname
57f1b914 2665 $export_name .= moodle_strtolower($export_shortname)."-";
feb60a07 2666 //The category name
57f1b914 2667 $export_name .= moodle_strtolower($export_categoryname)."-";
feb60a07 2668 //The date format
2669 $export_name .= userdate(time(),$export_date_format,99,false);
6c58e198 2670 //Extension is supplied by format later.
feb60a07 2671
2672 return $export_name;
2673}
50fcb1d8 2674
2675/**
2676 * @package moodlecore
2677 * @subpackage question
2678 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
2679 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2680 */
3bee1ead 2681class context_to_string_translator{
2682 /**
2683 * @var array used to translate between contextids and strings for this context.
2684 */
2685 var $contexttostringarray = array();
2686
2687 function context_to_string_translator($contexts){
2688 $this->generate_context_to_string_array($contexts);
2689 }
2690
2691 function context_to_string($contextid){
2692 return $this->contexttostringarray[$contextid];
2693 }
2694
2695 function string_to_context($contextname){
2696 $contextid = array_search($contextname, $this->contexttostringarray);
2697 return $contextid;
2698 }
2699
2700 function generate_context_to_string_array($contexts){
2701 if (!$this->contexttostringarray){
2702 $catno = 1;
2703 foreach ($contexts as $context){
2704 switch ($context->contextlevel){
2705 case CONTEXT_MODULE :
2706 $contextstring = 'module';
2707 break;
2708 case CONTEXT_COURSE :
2709 $contextstring = 'course';
2710 break;
2711 case CONTEXT_COURSECAT :
2712 $contextstring = "cat$catno";
2713 $catno++;
2714 break;
2715 case CONTEXT_SYSTEM :
2716 $contextstring = 'system';
2717 break;
2718 }
2719 $this->contexttostringarray[$context->id] = $contextstring;
2720 }
2721 }
2722 }
2723
2724}
feb60a07 2725
cca6e300 2726/**
2727 * @return array all the capabilities that relate to accessing particular questions.
2728 */
2729function question_get_question_capabilities() {
2730 return array(
2731 'moodle/question:add',
2732 'moodle/question:editmine',
2733 'moodle/question:editall',
2734 'moodle/question:viewmine',
2735 'moodle/question:viewall',
2736 'moodle/question:usemine',
2737 'moodle/question:useall',
2738 'moodle/question:movemine',
2739 'moodle/question:moveall',
2740 );
2741}
2742
2743/**
2744 * @return array all the question bank capabilities.
2745 */
2746function question_get_all_capabilities() {
2747 $caps = question_get_question_capabilities();
2748 $caps[] = 'moodle/question:managecategory';
2749 $caps[] = 'moodle/question:flag';
2750 return $caps;
2751}
3bee1ead 2752
2753/**
2754 * Check capability on category
50fcb1d8 2755 *
2756 * @global object
2757 * @global object
3bee1ead 2758 * @param mixed $question object or id
2759 * @param string $cap 'add', 'edit', 'view', 'use', 'move'
2760 * @param integer $cachecat useful to cache all question records in a category
2761 * @return boolean this user has the capability $cap for this question $question?
2762 */
2763function question_has_capability_on($question, $cap, $cachecat = -1){
eb84a826 2764 global $USER, $DB;
2765
415f8c3e 2766 // nicolasconnault@gmail.com In some cases I get $question === false. Since no such object exists, it can't be deleted, we can safely return true
2767 if ($question === false) {
2768 return true;
2769 }
2770
3bee1ead 2771 // these are capabilities on existing questions capabilties are
2772 //set per category. Each of these has a mine and all version. Append 'mine' and 'all'
2773 $question_questioncaps = array('edit', 'view', 'use', 'move');
3bee1ead 2774 static $questions = array();
2775 static $categories = array();
2776 static $cachedcat = array();
9f1017a0 2777 if ($cachecat != -1 && (array_search($cachecat, $cachedcat)===FALSE)){
eb84a826 2778 $questions += $DB->get_records('question', array('category'=>$cachecat));
3bee1ead 2779 $cachedcat[] = $cachecat;
2780 }
2781 if (!is_object($question)){
2782 if (!isset($questions[$question])){
f3409e53 2783 if (!$questions[$question] = $DB->get_record('question', array('id'=>$question), 'id,category,createdby')) {
0f6475b3 2784 print_error('questiondoesnotexist', 'question');
3bee1ead 2785 }
2786 }
2787 $question = $questions[$question];
2788 }
2789 if (!isset($categories[$question->category])){
eb84a826 2790 if (!$categories[$question->category] = $DB->get_record('question_categories', array('id'=>$question->category))) {
3bee1ead 2791 print_error('invalidcategory', 'quiz');
2792 }
2793 }
2794 $category = $categories[$question->category];
2795
2796 if (array_search($cap, $question_questioncaps)!== FALSE){
2797 if (!has_capability('moodle/question:'.$cap.'all', get_context_instance_by_id($category->contextid))){
2798 if ($question->createdby == $USER->id){
2799 return has_capability('moodle/question:'.$cap.'mine', get_context_instance_by_id($category->contextid));
2800 } else {
2801 return false;
2802 }
2803 } else {
2804 return true;
2805 }
2806 } else {
2807 return has_capability('moodle/question:'.$cap, get_context_instance_by_id($category->contextid));
2808 }
2809
2810}
2811
2812/**
2813 * Require capability on question.
2814 */
2815function question_require_capability_on($question, $cap){
2816 if (!question_has_capability_on($question, $cap)){
2817 print_error('nopermissions', '', '', $cap);
2818 }
2819 return true;
2820}
2821
50fcb1d8 2822/**
2823 * @global object
2824 */
3bee1ead 2825function question_file_links_base_url($courseid){
2826 global $CFG;
2827 $baseurl = preg_quote("$CFG->wwwroot/file.php", '!');
2828 $baseurl .= '('.preg_quote('?file=', '!').')?';//may or may not
2829 //be using slasharguments, accept either
2830 $baseurl .= "/$courseid/";//course directory
2831 return $baseurl;
2832}
2833
50fcb1d8 2834/**
3bee1ead 2835 * Find all course / site files linked to in a piece of html.
50fcb1d8 2836 * @global object
3bee1ead 2837 * @param string html the html to search
2838 * @param int course search for files for courseid course or set to siteid for
2839 * finding site files.
2840 * @return array files with keys being files.
2841 */
2842function question_find_file_links_from_html($html, $courseid){
2843 global $CFG;
2844 $baseurl = question_file_links_base_url($courseid);
2845 $searchfor = '!'.
2846 '(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")'.$baseurl.'([^"]*)"'.
2847 '|'.
2848 '(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')'.$baseurl.'([^\']*)\''.
2849 '!i';
2850 $matches = array();
2851 $no = preg_match_all($searchfor, $html, $matches);
2852 if ($no){
2853 $rawurls = array_filter(array_merge($matches[5], $matches[10]));//array_filter removes empty elements
2854 //remove any links that point somewhere they shouldn't
2855 foreach (array_keys($rawurls) as $rawurlkey){
2856 if (!$cleanedurl = question_url_check($rawurls[$rawurlkey])){
2857 unset($rawurls[$rawurlkey]);
2858 } else {
2859 $rawurls[$rawurlkey] = $cleanedurl;
2860 }
2861
2862 }
2863 $urls = array_flip($rawurls);// array_flip removes duplicate files
2864 // and when we merge arrays will continue to automatically remove duplicates
2865 } else {
2866 $urls = array();
2867 }
2868 return $urls;
2869}
eb84a826 2870
2871/**
3bee1ead 2872 * Check that url doesn't point anywhere it shouldn't
2873 *
50fcb1d8 2874 * @global object
3bee1ead 2875 * @param $url string relative url within course files directory
2876 * @return mixed boolean false if not OK or cleaned URL as string if OK
2877 */
2878function question_url_check($url){
2879 global $CFG;
2880 if ((substr(strtolower($url), 0, strlen($CFG->moddata)) == strtolower($CFG->moddata)) ||
2881 (substr(strtolower($url), 0, 10) == 'backupdata')){
2882 return false;
2883 } else {
2884 return clean_param($url, PARAM_PATH);
2885 }
2886}
2887
eb84a826 2888/**
3bee1ead 2889 * Find all course / site files linked to in a piece of html.
50fcb1d8 2890 *
2891 * @global object
3bee1ead 2892 * @param string html the html to search
2893 * @param int course search for files for courseid course or set to siteid for
2894 * finding site files.
2895 * @return array files with keys being files.
2896 */
2897function question_replace_file_links_in_html($html, $fromcourseid, $tocourseid, $url, $destination, &$changed){
2898 global $CFG;
5a254a29 2899 require_once($CFG->libdir .'/filelib.php');
2900 $tourl = get_file_url("$tocourseid/$destination");
3bee1ead 2901 $fromurl = question_file_links_base_url($fromcourseid).preg_quote($url, '!');
2902 $searchfor = array('!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*")'.$fromurl.'(")!i',
2903 '!(<\s*(a|img)\s[^>]*(href|src)\s*=\s*\')'.$fromurl.'(\')!i');
2904 $newhtml = preg_replace($searchfor, '\\1'.$tourl.'\\5', $html);
2905 if ($newhtml != $html){
2906 $changed = true;
2907 }
2908 return $newhtml;
2909}
2910
50fcb1d8 2911/**
2912 * @global object
2913 */
3bee1ead 2914function get_filesdir_from_context($context){
eb84a826 2915 global $DB;
2916
3bee1ead 2917 switch ($context->contextlevel){
2918 case CONTEXT_COURSE :
2919 $courseid = $context->instanceid;
2920 break;
2921 case CONTEXT_MODULE :
eb84a826 2922 $courseid = $DB->get_field('course_modules', 'course', array('id'=>$context->instanceid));
3bee1ead 2923 break;
2924 case CONTEXT_COURSECAT :
2925 case CONTEXT_SYSTEM :
2926 $courseid = SITEID;
2927 break;
2928 default :
d89ec019 2929 print_error('invalidcontext');
3bee1ead 2930 }
2931 return $courseid;
2932}
4f5ffac0 2933/**
43ec99aa 2934 * Get the real state - the correct question id and answer - for a random
2935 * question.
4f5ffac0 2936 * @param object $state with property answer.
2937 * @return mixed return integer real question id or false if there was an
2938 * error..
2939 */
43ec99aa 2940function question_get_real_state($state){
aa9a6867 2941 global $OUTPUT;
43ec99aa 2942 $realstate = clone($state);
4f5ffac0 2943 $matches = array();
91ec2c42 2944 if (!preg_match('|^random([0-9]+)-(.*)|', $state->answer, $matches)){
aa9a6867 2945 echo $OUTPUT->notification(get_string('errorrandom', 'quiz_statistics'));
4f5ffac0 2946 return false;
2947 } else {
43ec99aa 2948 $realstate->question = $matches[1];
2949 $realstate->answer = $matches[2];
2950 return $realstate;
4f5ffac0 2951 }
2952}
43ec99aa 2953
62e76c67 2954/**
2955 * Update the flagged state of a particular question session.
2956 *
50fcb1d8 2957 * @global object
62e76c67 2958 * @param integer $sessionid question_session id.
2959 * @param boolean $newstate the new state for the flag.
2960 * @return boolean success or failure.
2961 */
2962function question_update_flag($sessionid, $newstate) {
2963 global $DB;
2964 return $DB->set_field('question_sessions', 'flagged', $newstate, array('id' => $sessionid));
2965}
2966
f62040ed 2967/**
2968 * Update the flagged state of all the questions in an attempt, where a new .
2969 *
50fcb1d8 2970 * @global object
f62040ed 2971 * @param integer $sessionid question_session id.
2972 * @param boolean $newstate the new state for the flag.
2973 * @return boolean success or failure.
2974 */
2975function question_save_flags($formdata, $attemptid, $questionids) {
2976 global $DB;
2977 $donequestionids = array();
2978 foreach ($formdata as $postvariable => $value) {
2979 list($qid, $key) = question_id_and_key_from_post_name($postvariable);
2980 if ($qid !== false && in_array($qid, $questionids)) {
2981 if ($key == '_flagged') {
2982 $DB->set_field('question_sessions', 'flagged', !empty($value),
2983 array('attemptid' => $attemptid, 'questionid' => $qid));
2984 $donequestionids[$qid] = 1;
2985 }
2986 }
2987 }
2988 foreach ($questionids as $qid) {
2989 if (!isset($donequestionids[$qid])) {
2990 $DB->set_field('question_sessions', 'flagged', 0,
2991 array('attemptid' => $attemptid, 'questionid' => $qid));
2992 }
2993 }
2994}
2995
62e76c67 2996/**
50fcb1d8 2997 *
2998 * @global object
62e76c67 2999 * @param integer $attemptid the question_attempt id.
3000 * @param integer $questionid the question id.
3001 * @param integer $sessionid the question_session id.
3002 * @param object $user a user, or null to use $USER.
3003 * @return string that needs to be sent to question/toggleflag.php for it to work.
3004 */
3005function question_get_toggleflag_checksum($attemptid, $questionid, $sessionid, $user = null) {
3006 if (is_null($user)) {
3007 global $USER;
3008 $user = $USER;
3009 }
3010 return md5($attemptid . "_" . $user->secret . "_" . $questionid . "_" . $sessionid);
3011}