fix for 5438 merged
[moodle.git] / lib / questionlib.php
CommitLineData
516cf3eb 1<?php // $Id$
2/**
6b11a0e8 3 * Code for handling and processing questions
4 *
5 * This is code that is module independent, i.e., can be used by any module that
6 * uses questions, like quiz, lesson, ..
7 * This script also loads the questiontype classes
8 * Code for handling the editing of questions is in {@link question/editlib.php}
9 *
10 * TODO: separate those functions which form part of the API
11 * from the helper functions.
12 *
13 * @version $Id$
14 * @author Martin Dougiamas and many others. This has recently been completely
15 * rewritten by Alex Smith, Julian Sedding and Gustav Delius as part of
16 * the Serving Mathematics project
17 * {@link http://maths.york.ac.uk/serving_maths}
18 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
19 * @package question
20 */
21
22/// CONSTANTS ///////////////////////////////////
516cf3eb 23
e56a08dc 24/**#@+
6b11a0e8 25 * The different types of events that can create question states
26 */
f30bbcaf 27define('QUESTION_EVENTOPEN', '0'); // The state was created by Moodle
28define('QUESTION_EVENTNAVIGATE', '1'); // The responses were saved because the student navigated to another page (this is not currently used)
29define('QUESTION_EVENTSAVE', '2'); // The student has requested that the responses should be saved but not submitted or validated
30define('QUESTION_EVENTGRADE', '3'); // Moodle has graded the responses. A SUBMIT event can be changed to a GRADE event by Moodle.
31define('QUESTION_EVENTDUPLICATE', '4'); // The responses submitted were the same as previously
32define('QUESTION_EVENTVALIDATE', '5'); // The student has requested a validation. This causes the responses to be saved as well, but not graded.
33define('QUESTION_EVENTCLOSEANDGRADE', '6'); // Moodle has graded the responses. A CLOSE event can be changed to a CLOSEANDGRADE event by Moodle.
34define('QUESTION_EVENTSUBMIT', '7'); // The student response has been submitted but it has not yet been marked
35define('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 36define('QUESTION_EVENTMANUALGRADE', '9'); // Grade was entered by teacher
e56a08dc 37/**#@-*/
38
39/**#@+
6b11a0e8 40 * The core question types
41 */
60407982 42define("SHORTANSWER", "shortanswer");
43define("TRUEFALSE", "truefalse");
44define("MULTICHOICE", "multichoice");
45define("RANDOM", "random");
46define("MATCH", "match");
47define("RANDOMSAMATCH", "randomsamatch");
48define("DESCRIPTION", "description");
49define("NUMERICAL", "numerical");
50define("MULTIANSWER", "multianswer");
51define("CALCULATED", "calculated");
52define("RQP", "rqp");
53define("ESSAY", "essay");
e56a08dc 54/**#@-*/
55
6b11a0e8 56/**
57 * Constant determines the number of answer boxes supplied in the editing
58 * form for multiple choice and similar question types.
59 */
4f48fb42 60define("QUESTION_NUMANS", "10");
e56a08dc 61
516cf3eb 62
6b11a0e8 63/**#@+
64 * Option flags for ->optionflags
65 * The options are read out via bitwise operation using these constants
66 */
67/**
68 * Whether the questions is to be run in adaptive mode. If this is not set then
69 * a question closes immediately after the first submission of responses. This
70 * is how question is Moodle always worked before version 1.5
71 */
72define('QUESTION_ADAPTIVE', 1);
73
6b11a0e8 74/**#@-*/
75
f02c6f01 76/// QTYPES INITIATION //////////////////
516cf3eb 77
78/**
6b11a0e8 79 * Array holding question type objects
80 */
ccccf04f 81global $QTYPES;
6b11a0e8 82$QTYPES = array(); // This array will be populated when the questiontype.php files are loaded below
ccccf04f 83
84/**
6b11a0e8 85 * Array of question types names translated to the user's language
86 *
87 * The $QTYPE_MENU array holds the names of all the question types that the user should
88 * be able to create directly. Some internal question types like random questions are excluded.
89 * The complete list of question types can be found in {@link $QTYPES}.
90 */
ccccf04f 91$QTYPE_MENU = array(); // This array will be populated when the questiontype.php files are loaded
516cf3eb 92
643ec47d 93require_once("$CFG->dirroot/question/type/questiontype.php");
516cf3eb 94
f02c6f01 95/*
96* Load the questiontype.php file for each question type
516cf3eb 97* These files in turn instantiate the corresponding question type class
ccccf04f 98* and add them to the $QTYPES array
516cf3eb 99*/
aaae75b0 100$qtypenames= get_list_of_plugins('question/type');
f02c6f01 101foreach($qtypenames as $qtypename) {
102 // Instanciates all plug-in question types
643ec47d 103 $qtypefilepath= "$CFG->dirroot/question/type/$qtypename/questiontype.php";
f02c6f01 104
105 // echo "Loading $qtypename<br/>"; // Uncomment for debugging
106 if (is_readable($qtypefilepath)) {
107 require_once($qtypefilepath);
516cf3eb 108 }
109}
110
111/// OTHER CLASSES /////////////////////////////////////////////////////////
112
113/**
6b11a0e8 114 * This holds the options that are set by the course module
115 */
516cf3eb 116class cmoptions {
117 /**
118 * Whether a new attempt should be based on the previous one. If true
119 * then a new attempt will start in a state where all responses are set
120 * to the last responses from the previous attempt.
121 */
122 var $attemptonlast = false;
123
124 /**
125 * Various option flags. The flags are accessed via bitwise operations
126 * using the constants defined in the CONSTANTS section above.
127 */
6b11a0e8 128 var $optionflags = QUESTION_ADAPTIVE;
516cf3eb 129
130 /**
131 * Determines whether in the calculation of the score for a question
132 * penalties for earlier wrong responses within the same attempt will
133 * be subtracted.
134 */
135 var $penaltyscheme = true;
136
137 /**
138 * The maximum time the user is allowed to answer the questions withing
139 * an attempt. This is measured in minutes so needs to be multiplied by
140 * 60 before compared to timestamps. If set to 0 no timelimit will be applied
141 */
142 var $timelimit = 0;
143
144 /**
145 * Timestamp for the closing time. Responses submitted after this time will
146 * be saved but no credit will be given for them.
147 */
148 var $timeclose = 9999999999;
149
150 /**
151 * The id of the course from withing which the question is currently being used
152 */
153 var $course = SITEID;
154
155 /**
156 * Whether the answers in a multiple choice question should be randomly
157 * shuffled when a new attempt is started.
158 */
c4c11af8 159 var $shuffleanswers = true;
516cf3eb 160
161 /**
162 * The number of decimals to be shown when scores are printed
163 */
164 var $decimalpoints = 2;
516cf3eb 165}
166
167
516cf3eb 168/// FUNCTIONS //////////////////////////////////////////////////////
169
90c3f310 170/**
f67172b6 171 * Returns an array of names of activity modules that use this question
90c3f310 172 *
f67172b6 173 * @param object $questionid
174 * @return array of strings
90c3f310 175 */
f67172b6 176function question_list_instances($questionid) {
90c3f310 177 $instances = array();
178 $modules = get_records('modules');
179 foreach ($modules as $module) {
f67172b6 180 $fn = $module->name.'_question_list_instances';
90c3f310 181 if (function_exists($fn)) {
f67172b6 182 $instances = $instances + $fn($questionid);
90c3f310 183 }
184 }
185 return $instances;
186}
516cf3eb 187
e9de4366 188
189/**
190 * Returns list of 'allowed' grades for grade selection
191 * formatted suitably for dropdown box function
192 * @return object ->gradeoptionsfull full array ->gradeoptions +ve only
193 */
194function get_grade_options() {
195 // define basic array of grades
196 $grades = array(
197 1,
198 0.9,
199 0.8,
200 0.75,
201 0.70,
202 0.66666,
203 0.60,
204 0.50,
205 0.40,
206 0.33333,
207 0.30,
208 0.25,
209 0.20,
210 0.16666,
211 0.142857,
212 0.125,
213 0.11111,
214 0.10,
215 0.05,
216 0);
217
218 // iterate through grades generating full range of options
219 $gradeoptionsfull = array();
220 $gradeoptions = array();
221 foreach ($grades as $grade) {
222 $percentage = 100 * $grade;
223 $neggrade = -$grade;
224 $gradeoptions["$grade"] = "$percentage %";
225 $gradeoptionsfull["$grade"] = "$percentage %";
226 $gradeoptionsfull["$neggrade"] = -$percentage." %";
227 }
228 $gradeoptionsfull["0"] = $gradeoptions["0"] = get_string("none");
229
230 // sort lists
231 arsort($gradeoptions, SORT_NUMERIC);
232 arsort($gradeoptionsfull, SORT_NUMERIC);
233
234 // construct return object
235 $grades = new stdClass;
236 $grades->gradeoptions = $gradeoptions;
237 $grades->gradeoptionsfull = $gradeoptionsfull;
238
239 return $grades;
240}
241
8511669c 242/**
243 * match grade options
244 * if no match return error or match nearest
245 * @param array $gradeoptionsfull list of valid options
246 * @param int $grade grade to be tested
247 * @param string $matchgrades 'error' or 'nearest'
248 * @return mixed either 'fixed' value or false if erro
249 */
250function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') {
251 // if we just need an error...
252 if ($matchgrades=='error') {
253 foreach($gradeoptionsfull as $value => $option) {
2784b982 254 // slightly fuzzy test, never check floats for equality :-)
255 if (abs($grade-$value)<0.00001) {
8511669c 256 return $grade;
257 }
258 }
259 // didn't find a match so that's an error
260 return false;
261 }
262 // work out nearest value
263 else if ($matchgrades=='nearest') {
264 $hownear = array();
265 foreach($gradeoptionsfull as $value => $option) {
266 if ($grade==$value) {
267 return $grade;
268 }
269 $hownear[ $value ] = abs( $grade - $value );
270 }
271 // reverse sort list of deltas and grab the last (smallest)
272 asort( $hownear, SORT_NUMERIC );
273 reset( $hownear );
274 return key( $hownear );
275 }
276 else {
277 return false;
278 }
279}
280
f67172b6 281/**
282 * Tests whether a category is in use by any activity module
283 *
284 * @return boolean
285 * @param integer $categoryid
286 * @param boolean $recursive Whether to examine category children recursively
287 */
288function question_category_isused($categoryid, $recursive = false) {
289
290 //Look at each question in the category
291 if ($questions = get_records('question', 'category', $categoryid)) {
292 foreach ($questions as $question) {
293 if (count(question_list_instances($question->id))) {
294 return true;
295 }
296 }
297 }
298
299 //Look under child categories recursively
300 if ($recursive) {
301 if ($children = get_records('question_categories', 'parent', $categoryid)) {
302 foreach ($children as $child) {
303 if (question_category_isused($child->id, $recursive)) {
304 return true;
305 }
306 }
307 }
308 }
309
310 return false;
311}
312
0429cd86 313/**
314 * Deletes all data associated to an attempt from the database
315 *
316 * @param object $question The question being deleted
317 */
318function delete_attempt($attemptid) {
319 global $QTYPES;
320
321 $states = get_records('question_states', 'attempt', $attemptid);
322 $stateslist = implode(',', array_keys($states));
323
324 // delete questiontype-specific data
325 foreach ($QTYPES as $qtype) {
326 $qtype->delete_states($stateslist);
327 }
328
329 // delete entries from all other question tables
330 // It is important that this is done only after calling the questiontype functions
331 delete_records("question_states", "attempt", $attemptid);
332 delete_records("question_sessions", "attemptid", $attemptid);
333
334 return;
335}
f67172b6 336
516cf3eb 337/**
6b11a0e8 338 * Deletes question and all associated data from the database
339 *
90c3f310 340 * It will not delete a question if it is used by an activity module
6b11a0e8 341 * @param object $question The question being deleted
342 */
90c3f310 343function delete_question($questionid) {
f02c6f01 344 global $QTYPES;
90c3f310 345
346 // Do not delete a question if it is used by an activity module
f67172b6 347 if (count(question_list_instances($questionid))) {
90c3f310 348 return;
349 }
350
351 // delete questiontype-specific data
cf60a28a 352 if ($question = get_record('question', 'id', $questionid)) {
353 if (isset($QTYPES[$question->qtype])) {
354 $QTYPES[$question->qtype]->delete_question($questionid);
355 }
356 } else {
357 echo "Question with id $questionid does not exist.<br />";
516cf3eb 358 }
90c3f310 359
5cb9076a 360 if ($states = get_records('question_states', 'question', $questionid)) {
361 $stateslist = implode(',', array_keys($states));
362
363 // delete questiontype-specific data
364 foreach ($QTYPES as $qtype) {
365 $qtype->delete_states($stateslist);
366 }
0429cd86 367 }
368
90c3f310 369 // delete entries from all other question tables
370 // It is important that this is done only after calling the questiontype functions
371 delete_records("question_answers", "question", $questionid);
372 delete_records("question_states", "question", $questionid);
373 delete_records("question_sessions", "questionid", $questionid);
374
375 // Now recursively delete all child questions
376 if ($children = get_records('question', 'parent', $questionid)) {
516cf3eb 377 foreach ($children as $child) {
90c3f310 378 delete_question($child->id);
516cf3eb 379 }
380 }
90c3f310 381
382 // Finally delete the question record itself
383 delete_records('question', 'id', $questionid);
384
385 return;
516cf3eb 386}
387
f67172b6 388/**
389 * All non-used question categories and their questions are deleted and
390 * categories still used by other courses are moved to the site course.
391 *
392 * @param object $course an object representing the course
393 * @param boolean $feedback to specify if the process must output a summary of its work
394 * @return boolean
395 */
396function question_delete_course($course, $feedback=true) {
397
398 global $CFG, $QTYPES;
399
400 //To detect if we have created the "container category"
401 $concatid = 0;
402
403 //The "container" category we'll create if we need if
404 $contcat = new object;
405
406 //To temporary store changes performed with parents
407 $parentchanged = array();
408
409 //To store feedback to be showed at the end of the process
410 $feedbackdata = array();
411
412 //Cache some strings
413 $strcatcontainer=get_string('containercategorycreated', 'quiz');
414 $strcatmoved = get_string('usedcategorymoved', 'quiz');
415 $strcatdeleted = get_string('unusedcategorydeleted', 'quiz');
416
417 if ($categories = get_records('question_categories', 'course', $course->id, 'parent', 'id, parent, name, course')) {
418
419 //Sort categories following their tree (parent-child) relationships
420 $categories = sort_categories_by_tree($categories);
421
422 foreach ($categories as $cat) {
423
424 //Get the full record
425 $category = get_record('question_categories', 'id', $cat->id);
426
427 //Check if the category is being used anywhere
428 if(question_category_isused($category->id, true)) {
429 //It's being used. Cannot delete it, so:
430 //Create a container category in SITEID course if it doesn't exist
431 if (!$concatid) {
432 $concat->course = SITEID;
433 if (!isset($course->shortname)) {
434 $course->shortname = 'id=' . $course->id;
435 }
436 $concat->name = get_string('savedfromdeletedcourse', 'quiz', $course->shortname);
437 $concat->info = $concat->name;
438 $concat->publish = 1;
439 $concat->stamp = make_unique_id_code();
440 $concatid = insert_record('question_categories', $concat);
441
442 //Fill feedback
443 $feedbackdata[] = array($concat->name, $strcatcontainer);
444 }
445 //Move the category to the container category in SITEID course
446 $category->course = SITEID;
447 //Assign to container if the category hasn't parent or if the parent is wrong (not belongs to the course)
448 if (!$category->parent || !isset($categories[$category->parent])) {
449 $category->parent = $concatid;
450 }
451 //If it's being used, its publish field should be 1
452 $category->publish = 1;
453 //Let's update it
454 update_record('question_categories', $category);
455
456 //Save this parent change for future use
457 $parentchanged[$category->id] = $category->parent;
458
459 //Fill feedback
460 $feedbackdata[] = array($category->name, $strcatmoved);
461
462 } else {
463 //Category isn't being used so:
464 //Delete it completely (questions and category itself)
465 //deleting questions
466 if ($questions = get_records("question", "category", $category->id)) {
467 foreach ($questions as $question) {
468 delete_question($question->id);
469 }
470 delete_records("question", "category", $category->id);
471 }
472 //delete the category
473 delete_records('question_categories', 'id', $category->id);
474
475 //Save this parent change for future use
476 if (!empty($category->parent)) {
477 $parentchanged[$category->id] = $category->parent;
478 } else {
479 $parentchanged[$category->id] = $concatid;
480 }
481
482 //Update all its child categories to re-parent them to grandparent.
483 set_field ('question_categories', 'parent', $parentchanged[$category->id], 'parent', $category->id);
484
485 //Fill feedback
486 $feedbackdata[] = array($category->name, $strcatdeleted);
487 }
488 }
489 //Inform about changes performed if feedback is enabled
490 if ($feedback) {
491 $table->head = array(get_string('category','quiz'), get_string('action'));
492 $table->data = $feedbackdata;
493 print_table($table);
494 }
495 }
496 return true;
497}
498
499
516cf3eb 500/**
501* Updates the question objects with question type specific
502* information by calling {@link get_question_options()}
503*
504* Can be called either with an array of question objects or with a single
505* question object.
506* @return bool Indicates success or failure.
507* @param mixed $questions Either an array of question objects to be updated
508* or just a single question object
509*/
4f48fb42 510function get_question_options(&$questions) {
f02c6f01 511 global $QTYPES;
516cf3eb 512
513 if (is_array($questions)) { // deal with an array of questions
514 // get the keys of the input array
515 $keys = array_keys($questions);
516 // update each question object
517 foreach ($keys as $i) {
4eda4eec 518 if (!array_key_exists($questions[$i]->qtype, $QTYPES)) {
519 $questions[$i]->qtype = 'missingtype';
2b44d03f 520 $questions[$i]->questiontext = get_string('warningmissingtype').$questions[$i]->questiontext;
4eda4eec 521 }
522
516cf3eb 523 // set name prefix
4f48fb42 524 $questions[$i]->name_prefix = question_make_name_prefix($i);
516cf3eb 525
f02c6f01 526 if (!$QTYPES[$questions[$i]->qtype]->get_question_options($questions[$i]))
516cf3eb 527 return false;
528 }
529 return true;
530 } else { // deal with single question
4eda4eec 531 if (!array_key_exists($questions->qtype, $QTYPES)) {
532 $questions->qtype = 'missingtype';
2b44d03f 533 $questions[$i]->questiontext = get_string('warningmissingtype').$questions[$i]->questiontext;
4eda4eec 534 }
4f48fb42 535 $questions->name_prefix = question_make_name_prefix($questions->id);
f02c6f01 536 return $QTYPES[$questions->qtype]->get_question_options($questions);
516cf3eb 537 }
538}
539
540/**
541* Loads the most recent state of each question session from the database
542* or create new one.
543*
544* For each question the most recent session state for the current attempt
4f48fb42 545* is loaded from the question_states table and the question type specific data and
546* responses are added by calling {@link restore_question_state()} which in turn
516cf3eb 547* calls {@link restore_session_and_responses()} for each question.
548* If no states exist for the question instance an empty state object is
549* created representing the start of a session and empty question
550* type specific information and responses are created by calling
551* {@link create_session_and_responses()}.
516cf3eb 552*
553* @return array An array of state objects representing the most recent
554* states of the question sessions.
555* @param array $questions The questions for which sessions are to be restored or
556* created.
557* @param object $cmoptions
558* @param object $attempt The attempt for which the question sessions are
559* to be restored or created.
560*/
4f48fb42 561function get_question_states(&$questions, $cmoptions, $attempt) {
f02c6f01 562 global $CFG, $QTYPES;
516cf3eb 563
564 // get the question ids
565 $ids = array_keys($questions);
566 $questionlist = implode(',', $ids);
567
568 // The question field must be listed first so that it is used as the
569 // array index in the array returned by get_records_sql
3a5a6f59 570 $statefields = 'n.questionid as question, s.*, n.sumpenalty, n.comment';
516cf3eb 571 // Load the newest states for the questions
572 $sql = "SELECT $statefields".
4f48fb42 573 " FROM {$CFG->prefix}question_states s,".
516cf3eb 574 " {$CFG->prefix}question_sessions n".
575 " WHERE s.id = n.newest".
576 " AND n.attemptid = '$attempt->uniqueid'".
577 " AND n.questionid IN ($questionlist)";
578 $states = get_records_sql($sql);
579
580 // Load the newest graded states for the questions
581 $sql = "SELECT $statefields".
4f48fb42 582 " FROM {$CFG->prefix}question_states s,".
516cf3eb 583 " {$CFG->prefix}question_sessions n".
584 " WHERE s.id = n.newgraded".
585 " AND n.attemptid = '$attempt->uniqueid'".
586 " AND n.questionid IN ($questionlist)";
587 $gradedstates = get_records_sql($sql);
588
589 // loop through all questions and set the last_graded states
590 foreach ($ids as $i) {
591 if (isset($states[$i])) {
4f48fb42 592 restore_question_state($questions[$i], $states[$i]);
516cf3eb 593 if (isset($gradedstates[$i])) {
4f48fb42 594 restore_question_state($questions[$i], $gradedstates[$i]);
516cf3eb 595 $states[$i]->last_graded = $gradedstates[$i];
596 } else {
597 $states[$i]->last_graded = clone($states[$i]);
516cf3eb 598 }
599 } else {
d23e3e11 600 // create a new empty state
601 $states[$i] = new object;
602 $states[$i]->attempt = $attempt->uniqueid;
603 $states[$i]->question = (int) $i;
604 $states[$i]->seq_number = 0;
605 $states[$i]->timestamp = $attempt->timestart;
606 $states[$i]->event = ($attempt->timefinish) ? QUESTION_EVENTCLOSE : QUESTION_EVENTOPEN;
607 $states[$i]->grade = 0;
608 $states[$i]->raw_grade = 0;
609 $states[$i]->penalty = 0;
610 $states[$i]->sumpenalty = 0;
7c404f9b 611 $states[$i]->comment = '';
d23e3e11 612 $states[$i]->responses = array('' => '');
613 // Prevent further changes to the session from incrementing the
614 // sequence number
615 $states[$i]->changed = true;
616
617 // Create the empty question type specific information
618 if (!$QTYPES[$questions[$i]->qtype]
619 ->create_session_and_responses($questions[$i], $states[$i], $cmoptions, $attempt)) {
620 return false;
516cf3eb 621 }
d23e3e11 622 $states[$i]->last_graded = clone($states[$i]);
516cf3eb 623 }
624 }
625 return $states;
626}
627
628
629/**
630* Creates the run-time fields for the states
631*
632* Extends the state objects for a question by calling
633* {@link restore_session_and_responses()}
634* @return boolean Represents success or failure
635* @param object $question The question for which the state is needed
636* @param object $state The state as loaded from the database
637*/
4f48fb42 638function restore_question_state(&$question, &$state) {
f02c6f01 639 global $QTYPES;
516cf3eb 640
641 // initialise response to the value in the answer field
da1cc5a4 642 $state->answer = addslashes($state->answer);
516cf3eb 643 $state->responses = array('' => $state->answer);
644 unset($state->answer);
da1cc5a4 645 $state->comment = isset($state->comment) ? addslashes($state->comment) : '';
516cf3eb 646
647 // Set the changed field to false; any code which changes the
648 // question session must set this to true and must increment
4f48fb42 649 // ->seq_number. The save_question_session
516cf3eb 650 // function will save the new state object to the database if the field is
651 // set to true.
652 $state->changed = false;
653
654 // Load the question type specific data
f02c6f01 655 return $QTYPES[$question->qtype]
516cf3eb 656 ->restore_session_and_responses($question, $state);
657
658}
659
660/**
661* Saves the current state of the question session to the database
662*
663* The state object representing the current state of the session for the
4f48fb42 664* question is saved to the question_states table with ->responses[''] saved
516cf3eb 665* to the answer field of the database table. The information in the
666* question_sessions table is updated.
667* The question type specific data is then saved.
bc2feba3 668* @return mixed The id of the saved or updated state or false
516cf3eb 669* @param object $question The question for which session is to be saved.
670* @param object $state The state information to be saved. In particular the
671* most recent responses are in ->responses. The object
672* is updated to hold the new ->id.
673*/
4f48fb42 674function save_question_session(&$question, &$state) {
f02c6f01 675 global $QTYPES;
516cf3eb 676 // Check if the state has changed
677 if (!$state->changed && isset($state->id)) {
bc2feba3 678 return $state->id;
516cf3eb 679 }
680 // Set the legacy answer field
681 $state->answer = isset($state->responses['']) ? $state->responses[''] : '';
682
683 // Save the state
b6e907a2 684 if (isset($state->update)) { // this forces the old state record to be overwritten
4f48fb42 685 update_record('question_states', $state);
516cf3eb 686 } else {
4f48fb42 687 if (!$state->id = insert_record('question_states', $state)) {
516cf3eb 688 unset($state->id);
689 unset($state->answer);
690 return false;
691 }
b6e907a2 692 }
516cf3eb 693
b6e907a2 694 // create or update the session
2e9b6d15 695 if (!$session = get_record('question_sessions', 'attemptid',
b6e907a2 696 $state->attempt, 'questionid', $question->id)) {
2e9b6d15 697 $session->attemptid = $state->attempt;
698 $session->questionid = $question->id;
699 $session->newest = $state->id;
700 // The following may seem weird, but the newgraded field needs to be set
701 // already even if there is no graded state yet.
702 $session->newgraded = $state->id;
703 $session->sumpenalty = $state->sumpenalty;
704 $session->comment = $state->comment;
705 if (!insert_record('question_sessions', $session)) {
b6e907a2 706 error('Could not insert entry in question_sessions');
516cf3eb 707 }
b6e907a2 708 } else {
2e9b6d15 709 $session->newest = $state->id;
710 if (question_state_is_graded($state) or $state->event == QUESTION_EVENTOPEN) {
711 // this state is graded or newly opened, so it goes into the lastgraded field as well
712 $session->newgraded = $state->id;
713 $session->sumpenalty = $state->sumpenalty;
714 $session->comment = $state->comment;
516cf3eb 715 }
2e9b6d15 716 update_record('question_sessions', $session);
516cf3eb 717 }
718
719 unset($state->answer);
720
721 // Save the question type specific state information and responses
f02c6f01 722 if (!$QTYPES[$question->qtype]->save_session_and_responses(
516cf3eb 723 $question, $state)) {
724 return false;
725 }
726 // Reset the changed flag
727 $state->changed = false;
bc2feba3 728 return $state->id;
516cf3eb 729}
730
731/**
732* Determines whether a state has been graded by looking at the event field
733*
734* @return boolean true if the state has been graded
735* @param object $state
736*/
4f48fb42 737function question_state_is_graded($state) {
bc2feba3 738 return ($state->event == QUESTION_EVENTGRADE
739 or $state->event == QUESTION_EVENTCLOSEANDGRADE
740 or $state->event == QUESTION_EVENTMANUALGRADE);
f30bbcaf 741}
742
743/**
744* Determines whether a state has been closed by looking at the event field
745*
746* @return boolean true if the state has been closed
747* @param object $state
748*/
749function question_state_is_closed($state) {
b6e907a2 750 return ($state->event == QUESTION_EVENTCLOSE
751 or $state->event == QUESTION_EVENTCLOSEANDGRADE
752 or $state->event == QUESTION_EVENTMANUALGRADE);
516cf3eb 753}
754
755
756/**
4dca7e51 757 * Extracts responses from submitted form
758 *
759 * This can extract the responses given to one or several questions present on a page
760 * It returns an array with one entry for each question, indexed by question id
761 * Each entry is an object with the properties
762 * ->event The event that has triggered the submission. This is determined by which button
763 * the user has pressed.
764 * ->responses An array holding the responses to an individual question, indexed by the
765 * name of the corresponding form element.
766 * ->timestamp A unix timestamp
767 * @return array array of action objects, indexed by question ids.
768 * @param array $questions an array containing at least all questions that are used on the form
769 * @param array $formdata the data submitted by the form on the question page
770 * @param integer $defaultevent the event type used if no 'mark' or 'validate' is submitted
771 */
772function question_extract_responses($questions, $formdata, $defaultevent=QUESTION_EVENTSAVE) {
516cf3eb 773
4dca7e51 774 $time = time();
516cf3eb 775 $actions = array();
4dca7e51 776 foreach ($formdata as $key => $response) {
516cf3eb 777 // Get the question id from the response name
4f48fb42 778 if (false !== ($quid = question_get_id_from_name_prefix($key))) {
516cf3eb 779 // check if this is a valid id
780 if (!isset($questions[$quid])) {
781 error('Form contained question that is not in questionids');
782 }
783
784 // Remove the name prefix from the name
785 //decrypt trying
786 $key = substr($key, strlen($questions[$quid]->name_prefix));
787 if (false === $key) {
788 $key = '';
789 }
790 // Check for question validate and mark buttons & set events
791 if ($key === 'validate') {
4f48fb42 792 $actions[$quid]->event = QUESTION_EVENTVALIDATE;
4dca7e51 793 } else if ($key === 'submit') {
f30bbcaf 794 $actions[$quid]->event = QUESTION_EVENTSUBMIT;
516cf3eb 795 } else {
796 $actions[$quid]->event = $defaultevent;
797 }
798
799 // Update the state with the new response
800 $actions[$quid]->responses[$key] = $response;
4dca7e51 801
802 // Set the timestamp
803 $actions[$quid]->timestamp = $time;
516cf3eb 804 }
805 }
806 return $actions;
807}
808
809
810
811/**
812* For a given question in an attempt we walk the complete history of states
813* and recalculate the grades as we go along.
814*
815* This is used when a question is changed and old student
816* responses need to be marked with the new version of a question.
817*
4f48fb42 818* TODO: Make sure this is not quiz-specific
819*
0a5b58af 820* @return boolean Indicates whether the grade has changed
516cf3eb 821* @param object $question A question object
822* @param object $attempt The attempt, in which the question needs to be regraded.
823* @param object $cmoptions
824* @param boolean $verbose Optional. Whether to print progress information or not.
825*/
4f48fb42 826function regrade_question_in_attempt($question, $attempt, $cmoptions, $verbose=false) {
516cf3eb 827
828 // load all states for this question in this attempt, ordered in sequence
4f48fb42 829 if ($states = get_records_select('question_states',
516cf3eb 830 "attempt = '{$attempt->uniqueid}' AND question = '{$question->id}'", 'seq_number ASC')) {
831 $states = array_values($states);
832
833 // Subtract the grade for the latest state from $attempt->sumgrades to get the
834 // sumgrades for the attempt without this question.
835 $attempt->sumgrades -= $states[count($states)-1]->grade;
836
837 // Initialise the replaystate
838 $state = clone($states[0]);
4f48fb42 839 restore_question_state($question, $state);
516cf3eb 840 $state->sumpenalty = 0.0;
841 $replaystate = clone($state);
842 $replaystate->last_graded = $state;
843
0a5b58af 844 $changed = false;
516cf3eb 845 for($j = 1; $j < count($states); $j++) {
4f48fb42 846 restore_question_state($question, $states[$j]);
516cf3eb 847 $action = new stdClass;
848 $action->responses = $states[$j]->responses;
849 $action->timestamp = $states[$j]->timestamp;
850
f30bbcaf 851 // Change event to submit so that it will be reprocessed
0a5b58af 852 if (QUESTION_EVENTCLOSE == $states[$j]->event
f30bbcaf 853 or QUESTION_EVENTGRADE == $states[$j]->event
854 or QUESTION_EVENTCLOSEANDGRADE == $states[$j]->event) {
855 $action->event = QUESTION_EVENTSUBMIT;
516cf3eb 856
857 // By default take the event that was saved in the database
858 } else {
859 $action->event = $states[$j]->event;
860 }
861 // Reprocess (regrade) responses
4f48fb42 862 if (!question_process_responses($question, $replaystate, $action, $cmoptions,
516cf3eb 863 $attempt)) {
864 $verbose && notify("Couldn't regrade state #{$state->id}!");
865 }
866
867 // We need rounding here because grades in the DB get truncated
868 // e.g. 0.33333 != 0.3333333, but we want them to be equal here
0a5b58af 869 if ((round((float)$replaystate->raw_grade, 5) != round((float)$states[$j]->raw_grade, 5))
870 or (round((float)$replaystate->penalty, 5) != round((float)$states[$j]->penalty, 5))
871 or (round((float)$replaystate->grade, 5) != round((float)$states[$j]->grade, 5))) {
872 $changed = true;
516cf3eb 873 }
874
875 $replaystate->id = $states[$j]->id;
d23e3e11 876 $replaystate->update = true; // This will ensure that the existing database entry is updated rather than a new one created
4f48fb42 877 save_question_session($question, $replaystate);
516cf3eb 878 }
0a5b58af 879 if ($changed) {
880 update_record('quiz_attempts', $attempt);
516cf3eb 881 }
882
0a5b58af 883 return $changed;
516cf3eb 884 }
0a5b58af 885 return false;
516cf3eb 886}
887
888/**
889* Processes an array of student responses, grading and saving them as appropriate
890*
891* @return boolean Indicates success/failure
892* @param object $question Full question object, passed by reference
893* @param object $state Full state object, passed by reference
894* @param object $action object with the fields ->responses which
895* is an array holding the student responses,
4f48fb42 896* ->action which specifies the action, e.g., QUESTION_EVENTGRADE,
516cf3eb 897* and ->timestamp which is a timestamp from when the responses
898* were submitted by the student.
899* @param object $cmoptions
900* @param object $attempt The attempt is passed by reference so that
901* during grading its ->sumgrades field can be updated
902*/
4f48fb42 903function question_process_responses(&$question, &$state, $action, $cmoptions, &$attempt) {
f02c6f01 904 global $QTYPES;
516cf3eb 905
906 // if no responses are set initialise to empty response
907 if (!isset($action->responses)) {
908 $action->responses = array('' => '');
909 }
910
911 // make sure these are gone!
755bddf1 912 unset($action->responses['submit'], $action->responses['validate']);
516cf3eb 913
914 // Check the question session is still open
f30bbcaf 915 if (question_state_is_closed($state)) {
516cf3eb 916 return true;
917 }
f30bbcaf 918
516cf3eb 919 // If $action->event is not set that implies saving
920 if (! isset($action->event)) {
4f48fb42 921 $action->event = QUESTION_EVENTSAVE;
516cf3eb 922 }
f30bbcaf 923 // If submitted then compare against last graded
516cf3eb 924 // responses, not last given responses in this case
4f48fb42 925 if (question_isgradingevent($action->event)) {
516cf3eb 926 $state->responses = $state->last_graded->responses;
927 }
928 // Check for unchanged responses (exactly unchanged, not equivalent).
929 // We also have to catch questions that the student has not yet attempted
930 $sameresponses = (($state->responses == $action->responses) or
931 ($state->responses == array(''=>'') && array_keys(array_count_values($action->responses))===array('')));
932
f30bbcaf 933 // If the response has not been changed then we do not have to process it again
934 // unless the attempt is closing or validation is requested
4f48fb42 935 if ($sameresponses and QUESTION_EVENTCLOSE != $action->event
936 and QUESTION_EVENTVALIDATE != $action->event) {
516cf3eb 937 return true;
938 }
939
940 // Roll back grading information to last graded state and set the new
941 // responses
942 $newstate = clone($state->last_graded);
943 $newstate->responses = $action->responses;
944 $newstate->seq_number = $state->seq_number + 1;
945 $newstate->changed = true; // will assure that it gets saved to the database
ca56222d 946 $newstate->last_graded = clone($state->last_graded);
516cf3eb 947 $newstate->timestamp = $action->timestamp;
948 $state = $newstate;
949
950 // Set the event to the action we will perform. The question type specific
4f48fb42 951 // grading code may override this by setting it to QUESTION_EVENTCLOSE if the
516cf3eb 952 // attempt at the question causes the session to close
953 $state->event = $action->event;
954
4f48fb42 955 if (!question_isgradingevent($action->event)) {
516cf3eb 956 // Grade the response but don't update the overall grade
f02c6f01 957 $QTYPES[$question->qtype]->grade_responses(
516cf3eb 958 $question, $state, $cmoptions);
f30bbcaf 959 // Don't allow the processing to change the event type
516cf3eb 960 $state->event = $action->event;
961
ca56222d 962 } else { // grading event
516cf3eb 963
ca56222d 964 // Unless the attempt is closing, we want to work out if the current responses
965 // (or equivalent responses) were already given in the last graded attempt.
966 if((QUESTION_EVENTCLOSE != $action->event) and $QTYPES[$question->qtype]->compare_responses(
516cf3eb 967 $question, $state, $state->last_graded)) {
f30bbcaf 968 $state->event = QUESTION_EVENTDUPLICATE;
516cf3eb 969 }
f30bbcaf 970
ca56222d 971 // If we did not find a duplicate or if the attempt is closing, perform grading
972 if ((!$sameresponses and (QUESTION_EVENTDUPLICATE != $state->event)) or (QUESTION_EVENTCLOSE == $action->event)) {
f30bbcaf 973 // Decrease sumgrades by previous grade and then later add new grade
974 $attempt->sumgrades -= (float)$state->last_graded->grade;
975
976 $QTYPES[$question->qtype]->grade_responses(
977 $question, $state, $cmoptions);
978 // Calculate overall grade using correct penalty method
979 question_apply_penalty_and_timelimit($question, $state, $attempt, $cmoptions);
516cf3eb 980
1d7e0934 981 $attempt->sumgrades += (float)$state->grade;
516cf3eb 982 }
516cf3eb 983
ca56222d 984 // If the state was graded we need to update the last_graded field.
985 if (question_state_is_graded($state)) {
986 unset($state->last_graded);
987 $state->last_graded = clone($state);
988 unset($state->last_graded->changed);
989 }
516cf3eb 990 }
991 $attempt->timemodified = $action->timestamp;
992
993 return true;
994}
995
996/**
997* Determine if event requires grading
998*/
4f48fb42 999function question_isgradingevent($event) {
f30bbcaf 1000 return (QUESTION_EVENTSUBMIT == $event || QUESTION_EVENTCLOSE == $event);
516cf3eb 1001}
1002
516cf3eb 1003/**
1004* Applies the penalty from the previous graded responses to the raw grade
1005* for the current responses
1006*
1007* The grade for the question in the current state is computed by subtracting the
1008* penalty accumulated over the previous graded responses at the question from the
1009* raw grade. If the timestamp is more than 1 minute beyond the end of the attempt
1010* the grade is set to zero. The ->grade field of the state object is modified to
1011* reflect the new grade but is never allowed to decrease.
1012* @param object $question The question for which the penalty is to be applied.
1013* @param object $state The state for which the grade is to be set from the
1014* raw grade and the cumulative penalty from the last
1015* graded state. The ->grade field is updated by applying
1016* the penalty scheme determined in $cmoptions to the ->raw_grade and
1017* ->last_graded->penalty fields.
1018* @param object $cmoptions The options set by the course module.
1019* The ->penaltyscheme field determines whether penalties
1020* for incorrect earlier responses are subtracted.
1021*/
4f48fb42 1022function question_apply_penalty_and_timelimit(&$question, &$state, $attempt, $cmoptions) {
f30bbcaf 1023 // deal with penalty
516cf3eb 1024 if ($cmoptions->penaltyscheme) {
1025 $state->grade = $state->raw_grade - $state->sumpenalty;
1026 $state->sumpenalty += (float) $state->penalty;
1027 } else {
1028 $state->grade = $state->raw_grade;
1029 }
1030
1031 // deal with timelimit
1032 if ($cmoptions->timelimit) {
1033 // We allow for 5% uncertainty in the following test
1034 if (($state->timestamp - $attempt->timestart) > ($cmoptions->timelimit * 63)) {
1035 $state->grade = 0;
1036 }
1037 }
1038
1039 // deal with closing time
1040 if ($cmoptions->timeclose and $state->timestamp > ($cmoptions->timeclose + 60) // allowing 1 minute lateness
1041 and !$attempt->preview) { // ignore closing time for previews
1042 $state->grade = 0;
1043 }
1044
1045 // Ensure that the grade does not go down
1046 $state->grade = max($state->grade, $state->last_graded->grade);
1047}
1048
516cf3eb 1049/**
1050* Print the icon for the question type
1051*
1052* @param object $question The question object for which the icon is required
1053* @param boolean $editlink If true then the icon is a link to the question
1054* edit page.
1055* @param boolean $return If true the functions returns the link as a string
1056*/
4f48fb42 1057function print_question_icon($question, $editlink=true, $return = false) {
516cf3eb 1058// returns a question icon
1059
dc1f00de 1060 global $QTYPES, $CFG;
516cf3eb 1061
ca252edb 1062 $namestr = get_string($question->qtype, 'quiz');
643ec47d 1063 $html = '<img border="0" height="16" width="16" src="'.$CFG->wwwroot.'/question/type/'.
4eda4eec 1064 $question->qtype.'/icon.gif" alt="'.
ca252edb 1065 $namestr.'" title="'.$namestr.'" />';
516cf3eb 1066
1067 if ($editlink) {
e56a08dc 1068 $html = "<a href=\"$CFG->wwwroot/question/question.php?id=$question->id\" title=\""
4eda4eec 1069 .$question->qtype."\">".
516cf3eb 1070 $html."</a>\n";
1071 }
1072 if ($return) {
1073 return $html;
1074 } else {
1075 echo $html;
1076 }
1077}
1078
516cf3eb 1079/**
1080* Returns a html link to the question image if there is one
1081*
1082* @return string The html image tag or the empy string if there is no image.
1083* @param object $question The question object
1084*/
4f48fb42 1085function get_question_image($question, $courseid) {
516cf3eb 1086
1087 global $CFG;
1088 $img = '';
1089
1090 if ($question->image) {
1091
1092 if (substr(strtolower($question->image), 0, 7) == 'http://') {
1093 $img .= $question->image;
1094
1095 } else if ($CFG->slasharguments) { // Use this method if possible for better caching
1096 $img .= "$CFG->wwwroot/file.php/$courseid/$question->image";
1097
1098 } else {
1099 $img .= "$CFG->wwwroot/file.php?file=$courseid/$question->image";
1100 }
1101 }
1102 return $img;
1103}
b6e907a2 1104
1105function question_print_comment_box($question, $state, $attempt, $url) {
2a2aba27 1106 global $CFG;
1107
848d886e 1108 $prefix = 'response';
2a2aba27 1109 $usehtmleditor = can_use_richtext_editor();
1110 $grade = round($state->last_graded->grade, 3);
1111 echo '<form method="post" action="'.$url.'">';
1112 include($CFG->dirroot.'/question/comment.html');
1113 echo '<input type="hidden" name="attempt" value="'.$attempt->uniqueid.'" />';
1114 echo '<input type="hidden" name="question" value="'.$question->id.'" />';
1115 echo '<input type="hidden" name="sesskey" value="'.sesskey().'" />';
1116 echo '<input type="submit" name="submit" value="'.get_string('save', 'quiz').'" />';
1117 echo '</form>';
1118
1119 if ($usehtmleditor) {
848d886e 1120 use_html_editor();
2a2aba27 1121 }
b6e907a2 1122}
1123
1124function question_process_comment($question, &$state, &$attempt, $comment, $grade) {
1125
1126 // Update the comment and save it in the database
1127 $state->comment = $comment;
1128 if (!set_field('question_sessions', 'comment', $comment, 'attemptid', $attempt->uniqueid, 'questionid', $question->id)) {
1129 error("Cannot save comment");
1130 }
1131
1132 // If the teacher has changed the grade then update the attempt and the state
1133 // The modified attempt is stored to the database, the state not yet but the
1134 // $state->changed flag is set
1135 if (abs($state->last_graded->grade - $grade) > 0.002) {
1136 // the teacher has changed the grade
1137 $attempt->sumgrades = $attempt->sumgrades - $state->last_graded->grade + $grade;
1138 $attempt->timemodified = time();
1139 if (!update_record('quiz_attempts', $attempt)) {
1140 error('Failed to save the current quiz attempt!');
1141 }
1142
1143 $state->raw_grade = $grade;
1144 $state->grade = $grade;
1145 $state->penalty = 0;
1146 $state->timestamp = time();
1147 // We need to indicate that the state has changed in order for it to be saved
1148 $state->changed = 1;
1149 // We want to update existing state (rather than creating new one) if it
1150 // was itself created by a manual grading event
1151 $state->update = ($state->event == QUESTION_EVENTMANUALGRADE) ? 1 : 0;
1152 $state->event = QUESTION_EVENTMANUALGRADE;
1153
1154 // Update the last graded state (don't simplify!)
1155 unset($state->last_graded);
1156 $state->last_graded = clone($state);
1157 unset($state->last_graded->changed);
1158 }
1159
1160}
1161
516cf3eb 1162/**
1163* Construct name prefixes for question form element names
1164*
1165* Construct the name prefix that should be used for example in the
1166* names of form elements created by questions.
4f48fb42 1167* This is called by {@link get_question_options()}
516cf3eb 1168* to set $question->name_prefix.
1169* This name prefix includes the question id which can be
4f48fb42 1170* extracted from it with {@link question_get_id_from_name_prefix()}.
516cf3eb 1171*
1172* @return string
1173* @param integer $id The question id
1174*/
4f48fb42 1175function question_make_name_prefix($id) {
516cf3eb 1176 return 'resp' . $id . '_';
1177}
1178
1179/**
1180* Extract question id from the prefix of form element names
1181*
1182* @return integer The question id
1183* @param string $name The name that contains a prefix that was
4f48fb42 1184* constructed with {@link question_make_name_prefix()}
516cf3eb 1185*/
4f48fb42 1186function question_get_id_from_name_prefix($name) {
516cf3eb 1187 if (!preg_match('/^resp([0-9]+)_/', $name, $matches))
1188 return false;
1189 return (integer) $matches[1];
1190}
1191
4f48fb42 1192/**
4dca7e51 1193 * Returns the unique id for a new attempt
1194 *
1195 * Every module can keep their own attempts table with their own sequential ids but
1196 * the question code needs to also have a unique id by which to identify all these
1197 * attempts. Hence a module, when creating a new attempt, calls this function and
1198 * stores the return value in the 'uniqueid' field of its attempts table.
4f48fb42 1199 */
1200function question_new_attempt_uniqueid() {
516cf3eb 1201 global $CFG;
1202 set_config('attemptuniqueid', $CFG->attemptuniqueid + 1);
1203 return $CFG->attemptuniqueid;
1204}
1205
cbe20043 1206/* Creates a stamp that uniquely identifies this version of the question
1207 *
1208 * In future we want this to use a hash of the question data to guarantee that
1209 * identical versions have the same version stamp.
1210 *
1211 * @param object $question
1212 * @return string A unique version stamp
1213 */
1214function question_hash($question) {
1215 return make_unique_id_code();
1216}
1217
516cf3eb 1218
1219/// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS //////////////////////////////////
1220
4dca7e51 1221 /**
1222 * Prints a question
1223 *
1224 * Simply calls the question type specific print_question() method.
1225 * @param object $question The question to be rendered.
1226 * @param object $state The state to render the question in.
1227 * @param integer $number The number for this question.
1228 * @param object $cmoptions The options specified by the course module
1229 * @param object $options An object specifying the rendering options.
1230 */
4f48fb42 1231function print_question(&$question, &$state, $number, $cmoptions, $options=null) {
f02c6f01 1232 global $QTYPES;
516cf3eb 1233
f02c6f01 1234 $QTYPES[$question->qtype]->print_question($question, $state, $number,
516cf3eb 1235 $cmoptions, $options);
1236}
f5565b69 1237/**
1238* Saves question options
1239*
1240* Simply calls the question type specific save_question_options() method.
1241*/
1242function save_question_options($question) {
1243 global $QTYPES;
1244
1245 $QTYPES[$question->qtype]->save_question_options($question);
1246}
516cf3eb 1247
1248/**
1249* Gets all teacher stored answers for a given question
1250*
1251* Simply calls the question type specific get_all_responses() method.
1252*/
1253// ULPGC ecastro
4f48fb42 1254function get_question_responses($question, $state) {
f02c6f01 1255 global $QTYPES;
1256 $r = $QTYPES[$question->qtype]->get_all_responses($question, $state);
516cf3eb 1257 return $r;
1258}
1259
1260
1261/**
dc1f00de 1262* Gets the response given by the user in a particular state
516cf3eb 1263*
1264* Simply calls the question type specific get_actual_response() method.
1265*/
1266// ULPGC ecastro
4f48fb42 1267function get_question_actual_response($question, $state) {
f02c6f01 1268 global $QTYPES;
516cf3eb 1269
f02c6f01 1270 $r = $QTYPES[$question->qtype]->get_actual_response($question, $state);
516cf3eb 1271 return $r;
1272}
1273
1274/**
dc1f00de 1275* TODO: document this
516cf3eb 1276*/
1277// ULPGc ecastro
4f48fb42 1278function get_question_fraction_grade($question, $state) {
f02c6f01 1279 global $QTYPES;
516cf3eb 1280
f02c6f01 1281 $r = $QTYPES[$question->qtype]->get_fractional_grade($question, $state);
516cf3eb 1282 return $r;
1283}
1284
1285
1286/// CATEGORY FUNCTIONS /////////////////////////////////////////////////////////////////
1287
2dd6d66b 1288
516cf3eb 1289/**
1290* Displays a select menu of categories with appended course names
1291*
1292* Optionaly non editable categories may be excluded.
1293* @author Howard Miller June '04
1294*/
dc1f00de 1295function question_category_select_menu($courseid,$published=false,$only_editable=false,$selected="") {
516cf3eb 1296
1297 // get sql fragment for published
1298 $publishsql="";
1299 if ($published) {
1300 $publishsql = "or publish=1";
1301 }
1302
dc1f00de 1303 $categories = get_records_select("question_categories","course=$courseid $publishsql", 'parent, sortorder, name ASC');
516cf3eb 1304
1305 $categories = add_indented_names($categories);
1306
1307 echo "<select name=\"category\">\n";
1308 foreach ($categories as $category) {
1309 $cid = $category->id;
dc1f00de 1310 $cname = question_category_coursename($category, $courseid);
516cf3eb 1311 $seltxt = "";
1312 if ($cid==$selected) {
1313 $seltxt = "selected=\"selected\"";
1314 }
1315 if ((!$only_editable) || isteacheredit($category->course)) {
1316 echo " <option value=\"$cid\" $seltxt>$cname</option>\n";
1317 }
1318 }
1319 echo "</select>\n";
1320}
1321
dc1f00de 1322function question_category_coursename($category, $courseid = 0) {
516cf3eb 1323/// if the category is not from this course and is published , adds on the course
1324/// name
1325 $cname = (isset($category->indentedname)) ? $category->indentedname : $category->name;
1326 if ($category->course != $courseid && $category->publish) {
1327 if ($catcourse=get_record("course","id",$category->course)) {
1328 $cname .= " ($catcourse->shortname) ";
1329 }
1330 }
1331 return $cname;
1332}
1333
1334
1335/**
1336* Returns a comma separated list of ids of the category and all subcategories
1337*/
dc1f00de 1338function question_categorylist($categoryid) {
516cf3eb 1339 // returns a comma separated list of ids of the category and all subcategories
1340 $categorylist = $categoryid;
dc1f00de 1341 if ($subcategories = get_records('question_categories', 'parent', $categoryid, 'sortorder ASC', 'id, id')) {
516cf3eb 1342 foreach ($subcategories as $subcategory) {
dc1f00de 1343 $categorylist .= ','. question_categorylist($subcategory->id);
516cf3eb 1344 }
1345 }
1346 return $categorylist;
1347}
1348
1349
947217d7 1350//===========================
1351// Import/Export Functions
1352//===========================
1353
ff4b6492 1354/**
1355 * Get list of available import or export formats
1356 * @param string $type 'import' if import list, otherwise export list assumed
1357 * @return array sorted list of import/export formats available
1358**/
1359function get_import_export_formats( $type ) {
1360
1361 global $CFG;
dc1f00de 1362 $fileformats = get_list_of_plugins("question/format");
ff4b6492 1363
1364 $fileformatname=array();
947217d7 1365 require_once( "{$CFG->dirroot}/question/format.php" );
ff4b6492 1366 foreach ($fileformats as $key => $fileformat) {
1367 $format_file = $CFG->dirroot . "/question/format/$fileformat/format.php";
1368 if (file_exists( $format_file ) ) {
1369 require_once( $format_file );
1370 }
1371 else {
1372 continue;
1373 }
70c01adb 1374 $classname = "qformat_$fileformat";
ff4b6492 1375 $format_class = new $classname();
1376 if ($type=='import') {
1377 $provided = $format_class->provide_import();
1378 }
1379 else {
1380 $provided = $format_class->provide_export();
1381 }
1382 if ($provided) {
1383 $formatname = get_string($fileformat, 'quiz');
1384 if ($formatname == "[[$fileformat]]") {
1385 $formatname = $fileformat; // Just use the raw folder name
1386 }
1387 $fileformatnames[$fileformat] = $formatname;
1388 }
1389 }
1390 natcasesort($fileformatnames);
1391
1392 return $fileformatnames;
1393}
feb60a07 1394
1395
1396/**
1397* Create default export filename
1398*
1399* @return string default export filename
1400* @param object $course
1401* @param object $category
1402*/
1403function default_export_filename($course,$category) {
1404 //Take off some characters in the filename !!
1405 $takeoff = array(" ", ":", "/", "\\", "|");
1406 $export_word = str_replace($takeoff,"_",strtolower(get_string("exportfilename","quiz")));
1407 //If non-translated, use "export"
1408 if (substr($export_word,0,1) == "[") {
1409 $export_word= "export";
1410 }
1411
1412 //Calculate the date format string
1413 $export_date_format = str_replace(" ","_",get_string("exportnameformat","quiz"));
1414 //If non-translated, use "%Y%m%d-%H%M"
1415 if (substr($export_date_format,0,1) == "[") {
1416 $export_date_format = "%%Y%%m%%d-%%H%%M";
1417 }
1418
1419 //Calculate the shortname
1420 $export_shortname = clean_filename($course->shortname);
1421 if (empty($export_shortname) or $export_shortname == '_' ) {
1422 $export_shortname = $course->id;
1423 }
1424
1425 //Calculate the category name
1426 $export_categoryname = clean_filename($category->name);
1427
1428 //Calculate the final export filename
1429 //The export word
1430 $export_name = $export_word."-";
1431 //The shortname
1432 $export_name .= strtolower($export_shortname)."-";
1433 //The category name
1434 $export_name .= strtolower($export_categoryname)."-";
1435 //The date format
1436 $export_name .= userdate(time(),$export_date_format,99,false);
1437 //The extension - no extension, supplied by format
1438 // $export_name .= ".txt";
1439
1440 return $export_name;
1441}
1442
516cf3eb 1443?>