MDL-20636 Finish off converting question import.
[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 *
f29aeb5a
TH
29 * @package moodlecore
30 * @subpackage questionbank
31 * @copyright 1999 onwards Martin Dougiamas and others {@link http://moodle.com}
32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
6b11a0e8 33 */
720be6f2 34
8b92c1e3 35
f29aeb5a
TH
36require_once($CFG->dirroot . '/question/engine/lib.php');
37require_once($CFG->dirroot . '/question/type/questiontype.php');
8b92c1e3 38
8b92c1e3 39
f29aeb5a 40defined('MOODLE_INTERNAL') || die();
b55797b8 41
f29aeb5a 42/// CONSTANTS ///////////////////////////////////
e56a08dc 43
44/**#@+
a2156789 45 * The core question types.
6b11a0e8 46 */
60407982 47define("SHORTANSWER", "shortanswer");
48define("TRUEFALSE", "truefalse");
49define("MULTICHOICE", "multichoice");
50define("RANDOM", "random");
51define("MATCH", "match");
52define("RANDOMSAMATCH", "randomsamatch");
53define("DESCRIPTION", "description");
54define("NUMERICAL", "numerical");
55define("MULTIANSWER", "multianswer");
56define("CALCULATED", "calculated");
60407982 57define("ESSAY", "essay");
e56a08dc 58/**#@-*/
59
6b11a0e8 60/**
61 * Constant determines the number of answer boxes supplied in the editing
62 * form for multiple choice and similar question types.
63 */
f29aeb5a 64define("QUESTION_NUMANS", 10);
e56a08dc 65
271ffe3f 66/**
67 * Constant determines the number of answer boxes supplied in the editing
68 * form for multiple choice and similar question types to start with, with
69 * the option of adding QUESTION_NUMANS_ADD more answers.
70 */
71define("QUESTION_NUMANS_START", 3);
72
73/**
74 * Constant determines the number of answer boxes to add in the editing
75 * form for multiple choice and similar question types when the user presses
76 * 'add form fields button'.
77 */
78define("QUESTION_NUMANS_ADD", 3);
79
1b8a7434 80/**
81 * The options used when popping up a question preview window in Javascript.
82 */
f29aeb5a 83define('QUESTION_PREVIEW_POPUP_OPTIONS', 'scrollbars=yes,resizable=yes,width=800,height=600');
6b11a0e8 84
50fcb1d8 85/**
f29aeb5a
TH
86 * @global array holding question type objects
87 * @deprecated
50fcb1d8 88 */
f24493ec 89global $QTYPES;
f29aeb5a
TH
90$QTYPES = question_bank::get_all_qtypes();
91function question_register_questiontype() {
92 // TODO kill this.
a2156789 93}
f29aeb5a
TH
94// TODO kill this.
95class default_questiontype {
96 function plugin_dir() {
97 return '';
516cf3eb 98 }
99}
100
e2ae84a2 101/**
102 * An array of question type names translated to the user's language, suitable for use when
103 * creating a drop-down menu of options.
104 *
105 * Long-time Moodle programmers will realise that this replaces the old $QTYPE_MENU array.
106 * The array returned will only hold the names of all the question types that the user should
107 * be able to create directly. Some internal question types like random questions are excluded.
b9bd6da4 108 *
e2ae84a2 109 * @return array an array of question type names translated to the user's language.
110 */
111function question_type_menu() {
7f92f0ad 112 static $menuoptions = null;
113 if (is_null($menuoptions)) {
af52ecee 114 $config = get_config('question');
7f92f0ad 115 $menuoptions = array();
f29aeb5a 116 foreach (question_bank::get_all_qtypes() as $name => $qtype) {
e2ae84a2 117 $menuname = $qtype->menu_name();
af52ecee 118 $enabledvar = $name . '_disabled';
119 if ($menuname && !isset($config->$enabledvar)) {
7f92f0ad 120 $menuoptions[$name] = $menuname;
e2ae84a2 121 }
122 }
af52ecee 123
124 $menuoptions = question_sort_qtype_array($menuoptions, $config);
e2ae84a2 125 }
7f92f0ad 126 return $menuoptions;
e2ae84a2 127}
128
af52ecee 129/**
130 * Sort an array of question type names according to the question type sort order stored in
131 * config_plugins. Entries for which there is no xxx_sortorder defined will go
c6947ba7 132 * at the end, sorted according to textlib_get_instance()->asort($inarray).
f9b0500f 133 * @param $inarray an array $qtypename => $qtype->local_name().
af52ecee 134 * @param $config get_config('question'), if you happen to have it around, to save one DB query.
135 * @return array the sorted version of $inarray.
136 */
137function question_sort_qtype_array($inarray, $config = null) {
138 if (is_null($config)) {
139 $config = get_config('question');
140 }
141
142 $sortorder = array();
143 foreach ($inarray as $name => $notused) {
144 $sortvar = $name . '_sortorder';
145 if (isset($config->$sortvar)) {
146 $sortorder[$config->$sortvar] = $name;
147 }
148 }
149
150 ksort($sortorder);
151 $outarray = array();
152 foreach ($sortorder as $name) {
153 $outarray[$name] = $inarray[$name];
154 unset($inarray[$name]);
155 }
c6947ba7 156 textlib_get_instance()->asort($inarray);
af52ecee 157 return array_merge($outarray, $inarray);
158}
159
160/**
161 * Move one question type in a list of question types. If you try to move one element
162 * off of the end, nothing will change.
117bd748 163 *
af52ecee 164 * @param array $sortedqtypes An array $qtype => anything.
165 * @param string $tomove one of the keys from $sortedqtypes
166 * @param integer $direction +1 or -1
167 * @return array an array $index => $qtype, with $index from 0 to n in order, and
168 * the $qtypes in the same order as $sortedqtypes, except that $tomove will
169 * have been moved one place.
170 */
171function question_reorder_qtypes($sortedqtypes, $tomove, $direction) {
172 $neworder = array_keys($sortedqtypes);
173 // Find the element to move.
174 $key = array_search($tomove, $neworder);
175 if ($key === false) {
176 return $neworder;
177 }
178 // Work out the other index.
179 $otherkey = $key + $direction;
180 if (!isset($neworder[$otherkey])) {
181 return $neworder;
182 }
183 // Do the swap.
184 $swap = $neworder[$otherkey];
185 $neworder[$otherkey] = $neworder[$key];
186 $neworder[$key] = $swap;
187 return $neworder;
188}
189
190/**
191 * Save a new question type order to the config_plugins table.
50fcb1d8 192 * @global object
af52ecee 193 * @param $neworder An arra $index => $qtype. Indices should start at 0 and be in order.
194 * @param $config get_config('question'), if you happen to have it around, to save one DB query.
195 */
196function question_save_qtype_order($neworder, $config = null) {
197 global $DB;
198
199 if (is_null($config)) {
200 $config = get_config('question');
201 }
202
203 foreach ($neworder as $index => $qtype) {
204 $sortvar = $qtype . '_sortorder';
205 if (!isset($config->$sortvar) || $config->$sortvar != $index + 1) {
206 set_config($sortvar, $index + 1, 'question');
207 }
208 }
209}
210
516cf3eb 211/// FUNCTIONS //////////////////////////////////////////////////////
212
90c3f310 213/**
f67172b6 214 * Returns an array of names of activity modules that use this question
90c3f310 215 *
f29aeb5a
TH
216 * @deprecated since Moodle 2.1. Use {@link questions_in_use} instead.
217
f67172b6 218 * @param object $questionid
219 * @return array of strings
90c3f310 220 */
f67172b6 221function question_list_instances($questionid) {
f29aeb5a
TH
222 throw new coding_exception('question_list_instances has been deprectated. Please use questions_in_use instead.');
223}
224
225/**
226 * @param array $questionids of question ids.
227 * @return boolean whether any of these questions are being used by any part of Moodle.
228 */
229function questions_in_use($questionids) {
230 global $CFG;
231
232 if (question_engine::questions_in_use($questionids)) {
233 return true;
234 }
235
236 foreach (get_plugin_list('mod') as $module => $path) {
237 $lib = $path . '/lib.php';
238 if (is_readable($lib)) {
239 include_once($lib);
240
241 $fn = $module . '_questions_in_use';
7453df70 242 if (function_exists($fn)) {
f29aeb5a
TH
243 if ($fn($questionids)) {
244 return true;
245 }
246 } else {
247
248 // Fallback for legacy modules.
249 $fn = $module . '_question_list_instances';
250 if (function_exists($fn)) {
251 foreach ($questionids as $questionid) {
252 $instances = $fn($questionid);
253 if (!empty($instances)) {
254 return true;
255 }
256 }
257 }
7453df70 258 }
90c3f310 259 }
260 }
f29aeb5a
TH
261
262 return false;
90c3f310 263}
516cf3eb 264
e2b347e9 265/**
266 * Determine whether there arey any questions belonging to this context, that is whether any of its
267 * question categories contain any questions. This will return true even if all the questions are
268 * hidden.
269 *
270 * @param mixed $context either a context object, or a context id.
271 * @return boolean whether any of the question categories beloning to this context have
272 * any questions in them.
273 */
274function question_context_has_any_questions($context) {
eb84a826 275 global $DB;
e2b347e9 276 if (is_object($context)) {
277 $contextid = $context->id;
278 } else if (is_numeric($context)) {
279 $contextid = $context;
280 } else {
281 print_error('invalidcontextinhasanyquestions', 'question');
282 }
eb84a826 283 return $DB->record_exists_sql("SELECT *
284 FROM {question} q
285 JOIN {question_categories} qc ON qc.id = q.category
286 WHERE qc.contextid = ? AND q.parent = 0", array($contextid));
e2b347e9 287}
288
271ffe3f 289/**
e9de4366 290 * Returns list of 'allowed' grades for grade selection
291 * formatted suitably for dropdown box function
292 * @return object ->gradeoptionsfull full array ->gradeoptions +ve only
293 */
294function get_grade_options() {
30f09308 295 // define basic array of grades. This list comprises all fractions of the form:
296 // a. p/q for q <= 6, 0 <= p <= q
297 // b. p/10 for 0 <= p <= 10
298 // c. 1/q for 1 <= q <= 10
299 // d. 1/20
e9de4366 300 $grades = array(
30f09308 301 1.0000000,
302 0.9000000,
303 0.8333333,
304 0.8000000,
305 0.7500000,
306 0.7000000,
307 0.6666667,
308 0.6000000,
309 0.5000000,
310 0.4000000,
311 0.3333333,
312 0.3000000,
313 0.2500000,
314 0.2000000,
315 0.1666667,
316 0.1428571,
317 0.1250000,
318 0.1111111,
319 0.1000000,
320 0.0500000,
321 0.0000000);
e9de4366 322
323 // iterate through grades generating full range of options
324 $gradeoptionsfull = array();
325 $gradeoptions = array();
326 foreach ($grades as $grade) {
327 $percentage = 100 * $grade;
f29aeb5a
TH
328 $gradeoptions["$grade"] = $percentage . '%';
329 $gradeoptionsfull["$grade"] = $percentage . '%';
330 $gradeoptionsfull['' . (-$grade)] = (-$percentage) . '%';
e9de4366 331 }
f29aeb5a 332 $gradeoptionsfull['0'] = $gradeoptions['0'] = get_string('none');
e9de4366 333
334 // sort lists
335 arsort($gradeoptions, SORT_NUMERIC);
336 arsort($gradeoptionsfull, SORT_NUMERIC);
337
338 // construct return object
339 $grades = new stdClass;
340 $grades->gradeoptions = $gradeoptions;
341 $grades->gradeoptionsfull = $gradeoptionsfull;
342
343 return $grades;
344}
345
8511669c 346/**
347 * match grade options
348 * if no match return error or match nearest
349 * @param array $gradeoptionsfull list of valid options
350 * @param int $grade grade to be tested
351 * @param string $matchgrades 'error' or 'nearest'
352 * @return mixed either 'fixed' value or false if erro
353 */
354function match_grade_options($gradeoptionsfull, $grade, $matchgrades='error') {
355 // if we just need an error...
356 if ($matchgrades=='error') {
357 foreach($gradeoptionsfull as $value => $option) {
2784b982 358 // slightly fuzzy test, never check floats for equality :-)
359 if (abs($grade-$value)<0.00001) {
8511669c 360 return $grade;
361 }
362 }
363 // didn't find a match so that's an error
364 return false;
365 }
366 // work out nearest value
367 else if ($matchgrades=='nearest') {
368 $hownear = array();
369 foreach($gradeoptionsfull as $value => $option) {
370 if ($grade==$value) {
371 return $grade;
372 }
373 $hownear[ $value ] = abs( $grade - $value );
374 }
375 // reverse sort list of deltas and grab the last (smallest)
376 asort( $hownear, SORT_NUMERIC );
377 reset( $hownear );
378 return key( $hownear );
379 }
380 else {
381 return false;
271ffe3f 382 }
8511669c 383}
384
f67172b6 385/**
f29aeb5a
TH
386 * @deprecated Since Moodle 2.1. Use {@link question_category_in_use} instead.
387 * @param integer $categoryid a question category id.
388 * @param boolean $recursive whether to check child categories too.
389 * @return boolean whether any question in this category is in use.
f67172b6 390 */
391function question_category_isused($categoryid, $recursive = false) {
f29aeb5a
TH
392 throw new coding_exception('question_category_isused has been deprectated. Please use question_category_in_use instead.');
393}
394
395/**
396 * Tests whether any question in a category is used by any part of Moodle.
397 *
398 * @param integer $categoryid a question category id.
399 * @param boolean $recursive whether to check child categories too.
400 * @return boolean whether any question in this category is in use.
401 */
402function question_category_in_use($categoryid, $recursive = false) {
eb84a826 403 global $DB;
f67172b6 404
405 //Look at each question in the category
f29aeb5a
TH
406 if ($questions = $DB->get_records_menu('question', array('category' => $categoryid), '', 'id,1')) {
407 if (questions_in_use(array_keys($questions))) {
408 return true;
f67172b6 409 }
410 }
f29aeb5a
TH
411 if (!$recursive) {
412 return false;
413 }
f67172b6 414
415 //Look under child categories recursively
f29aeb5a
TH
416 if ($children = $DB->get_records('question_categories', array('parent' => $categoryid), '', 'id,1')) {
417 foreach ($children as $child) {
418 if (question_category_in_use($child->id, $recursive)) {
419 return true;
f67172b6 420 }
421 }
422 }
423
424 return false;
425}
426
516cf3eb 427/**
6b11a0e8 428 * Deletes question and all associated data from the database
429 *
90c3f310 430 * It will not delete a question if it is used by an activity module
6b11a0e8 431 * @param object $question The question being deleted
432 */
f29aeb5a 433function question_delete_question($questionid) {
f9b0500f 434 global $DB;
271ffe3f 435
9203b705
TH
436 $question = $DB->get_record_sql('
437 SELECT q.*, qc.contextid
438 FROM {question} q
439 JOIN {question_categories} qc ON qc.id = q.category
440 WHERE q.id = ?', array($questionid));
441 if (!$question) {
3c573960 442 // In some situations, for example if this was a child of a
443 // Cloze question that was previously deleted, the question may already
444 // have gone. In this case, just do nothing.
445 return;
446 }
447
90c3f310 448 // Do not delete a question if it is used by an activity module
f29aeb5a 449 if (questions_in_use(array($questionid))) {
90c3f310 450 return;
451 }
452
f29aeb5a 453 // Check permissions.
3bee1ead 454 question_require_capability_on($question, 'edit');
271ffe3f 455
f29aeb5a
TH
456 $dm = new question_engine_data_mapper();
457 $dm->delete_previews($questionid);
0429cd86 458
f29aeb5a
TH
459 // delete questiontype-specific data
460 question_bank::get_qtype($question->qtype, false)->delete_question(
461 $questionid, $question->contextid);
90c3f310 462
463 // Now recursively delete all child questions
d11de005 464 if ($children = $DB->get_records('question', array('parent' => $questionid), '', 'id,qtype')) {
516cf3eb 465 foreach ($children as $child) {
1d723a16 466 if ($child->id != $questionid) {
467 delete_question($child->id);
468 }
516cf3eb 469 }
470 }
271ffe3f 471
90c3f310 472 // Finally delete the question record itself
f29aeb5a 473 $DB->delete_records('question', array('id' => $questionid));
516cf3eb 474}
475
f67172b6 476/**
3bee1ead 477 * All question categories and their questions are deleted for this course.
f67172b6 478 *
3bee1ead 479 * @param object $mod an object representing the activity
f67172b6 480 * @param boolean $feedback to specify if the process must output a summary of its work
481 * @return boolean
482 */
483function question_delete_course($course, $feedback=true) {
642816a6 484 global $DB, $OUTPUT;
eb84a826 485
f67172b6 486 //To store feedback to be showed at the end of the process
487 $feedbackdata = array();
488
489 //Cache some strings
f67172b6 490 $strcatdeleted = get_string('unusedcategorydeleted', 'quiz');
3bee1ead 491 $coursecontext = get_context_instance(CONTEXT_COURSE, $course->id);
855f9c39 492 $categoriescourse = $DB->get_records('question_categories', array('contextid'=>$coursecontext->id), 'parent', 'id, parent, name, contextid');
f67172b6 493
3bee1ead 494 if ($categoriescourse) {
f67172b6 495
496 //Sort categories following their tree (parent-child) relationships
3bee1ead 497 //this will make the feedback more readable
498 $categoriescourse = sort_categories_by_tree($categoriescourse);
499
500 foreach ($categoriescourse as $category) {
501
502 //Delete it completely (questions and category itself)
503 //deleting questions
d11de005 504 if ($questions = $DB->get_records('question', array('category' => $category->id), '', 'id,qtype')) {
3bee1ead 505 foreach ($questions as $question) {
506 delete_question($question->id);
f67172b6 507 }
eb84a826 508 $DB->delete_records("question", array("category"=>$category->id));
3bee1ead 509 }
510 //delete the category
eb84a826 511 $DB->delete_records('question_categories', array('id'=>$category->id));
f67172b6 512
3bee1ead 513 //Fill feedback
514 $feedbackdata[] = array($category->name, $strcatdeleted);
515 }
516 //Inform about changes performed if feedback is enabled
517 if ($feedback) {
642816a6 518 $table = new html_table();
3bee1ead 519 $table->head = array(get_string('category','quiz'), get_string('action'));
520 $table->data = $feedbackdata;
16be8974 521 echo html_writer::table($table);
3bee1ead 522 }
523 }
524 return true;
525}
f67172b6 526
e2b347e9 527/**
528 * Category is about to be deleted,
529 * 1/ All question categories and their questions are deleted for this course category.
530 * 2/ All questions are moved to new category
531 *
532 * @param object $category course category object
533 * @param object $newcategory empty means everything deleted, otherwise id of category where content moved
534 * @param boolean $feedback to specify if the process must output a summary of its work
535 * @return boolean
536 */
537function question_delete_course_category($category, $newcategory, $feedback=true) {
aa9a6867 538 global $DB, $OUTPUT;
534792cd 539
e2b347e9 540 $context = get_context_instance(CONTEXT_COURSECAT, $category->id);
541 if (empty($newcategory)) {
542 $feedbackdata = array(); // To store feedback to be showed at the end of the process
543 $rescueqcategory = null; // See the code around the call to question_save_from_deletion.
544 $strcatdeleted = get_string('unusedcategorydeleted', 'quiz');
545
546 // Loop over question categories.
eb84a826 547 if ($categories = $DB->get_records('question_categories', array('contextid'=>$context->id), 'parent', 'id, parent, name')) {
e2b347e9 548 foreach ($categories as $category) {
b9bd6da4 549
e2b347e9 550 // Deal with any questions in the category.
d11de005 551 if ($questions = $DB->get_records('question', array('category' => $category->id), '', 'id,qtype')) {
e2b347e9 552
553 // Try to delete each question.
554 foreach ($questions as $question) {
555 delete_question($question->id);
556 }
557
558 // Check to see if there were any questions that were kept because they are
559 // still in use somehow, even though quizzes in courses in this category will
560 // already have been deteted. This could happen, for example, if questions are
561 // added to a course, and then that course is moved to another category (MDL-14802).
534792cd 562 $questionids = $DB->get_records_menu('question', array('category'=>$category->id), '', 'id,1');
e2b347e9 563 if (!empty($questionids)) {
5d548d3e 564 if (!$rescueqcategory = question_save_from_deletion(array_keys($questionids),
e2b347e9 565 get_parent_contextid($context), print_context_name($context), $rescueqcategory)) {
566 return false;
567 }
568 $feedbackdata[] = array($category->name, get_string('questionsmovedto', 'question', $rescueqcategory->name));
569 }
570 }
571
572 // Now delete the category.
eb84a826 573 if (!$DB->delete_records('question_categories', array('id'=>$category->id))) {
e2b347e9 574 return false;
575 }
576 $feedbackdata[] = array($category->name, $strcatdeleted);
577
578 } // End loop over categories.
579 }
580
581 // Output feedback if requested.
582 if ($feedback and $feedbackdata) {
642816a6 583 $table = new html_table();
e2b347e9 584 $table->head = array(get_string('questioncategory','question'), get_string('action'));
585 $table->data = $feedbackdata;
16be8974 586 echo html_writer::table($table);
e2b347e9 587 }
588
589 } else {
590 // Move question categories ot the new context.
591 if (!$newcontext = get_context_instance(CONTEXT_COURSECAT, $newcategory->id)) {
592 return false;
593 }
f685e830 594 $DB->set_field('question_categories', 'contextid', $newcontext->id, array('contextid'=>$context->id));
e2b347e9 595 if ($feedback) {
596 $a = new stdClass;
597 $a->oldplace = print_context_name($context);
598 $a->newplace = print_context_name($newcontext);
aa9a6867 599 echo $OUTPUT->notification(get_string('movedquestionsandcategories', 'question', $a), 'notifysuccess');
e2b347e9 600 }
601 }
602
603 return true;
604}
605
606/**
607 * Enter description here...
608 *
609 * @param string $questionids list of questionids
610 * @param object $newcontext the context to create the saved category in.
b9bd6da4 611 * @param string $oldplace a textual description of the think being deleted, e.g. from get_context_name
e2b347e9 612 * @param object $newcategory
b9bd6da4 613 * @return mixed false on
e2b347e9 614 */
615function question_save_from_deletion($questionids, $newcontextid, $oldplace, $newcategory = null) {
eb84a826 616 global $DB;
617
e2b347e9 618 // Make a category in the parent context to move the questions to.
619 if (is_null($newcategory)) {
365a5941 620 $newcategory = new stdClass();
e2b347e9 621 $newcategory->parent = 0;
622 $newcategory->contextid = $newcontextid;
eb84a826 623 $newcategory->name = get_string('questionsrescuedfrom', 'question', $oldplace);
624 $newcategory->info = get_string('questionsrescuedfrominfo', 'question', $oldplace);
e2b347e9 625 $newcategory->sortorder = 999;
626 $newcategory->stamp = make_unique_id_code();
a8f3a651 627 $newcategory->id = $DB->insert_record('question_categories', $newcategory);
e2b347e9 628 }
629
630 // Move any remaining questions to the 'saved' category.
631 if (!question_move_questions_to_category($questionids, $newcategory->id)) {
632 return false;
633 }
634 return $newcategory;
635}
636
3bee1ead 637/**
638 * All question categories and their questions are deleted for this activity.
639 *
640 * @param object $cm the course module object representing the activity
641 * @param boolean $feedback to specify if the process must output a summary of its work
642 * @return boolean
643 */
644function question_delete_activity($cm, $feedback=true) {
642816a6 645 global $DB, $OUTPUT;
eb84a826 646
3bee1ead 647 //To store feedback to be showed at the end of the process
648 $feedbackdata = array();
f67172b6 649
3bee1ead 650 //Cache some strings
651 $strcatdeleted = get_string('unusedcategorydeleted', 'quiz');
652 $modcontext = get_context_instance(CONTEXT_MODULE, $cm->id);
855f9c39 653 if ($categoriesmods = $DB->get_records('question_categories', array('contextid'=>$modcontext->id), 'parent', 'id, parent, name, contextid')){
3bee1ead 654 //Sort categories following their tree (parent-child) relationships
655 //this will make the feedback more readable
656 $categoriesmods = sort_categories_by_tree($categoriesmods);
f67172b6 657
3bee1ead 658 foreach ($categoriesmods as $category) {
f67172b6 659
3bee1ead 660 //Delete it completely (questions and category itself)
661 //deleting questions
d11de005 662 if ($questions = $DB->get_records('question', array('category' => $category->id), '', 'id,qtype')) {
3bee1ead 663 foreach ($questions as $question) {
664 delete_question($question->id);
665 }
eb84a826 666 $DB->delete_records("question", array("category"=>$category->id));
f67172b6 667 }
3bee1ead 668 //delete the category
eb84a826 669 $DB->delete_records('question_categories', array('id'=>$category->id));
3bee1ead 670
671 //Fill feedback
672 $feedbackdata[] = array($category->name, $strcatdeleted);
f67172b6 673 }
674 //Inform about changes performed if feedback is enabled
675 if ($feedback) {
642816a6 676 $table = new html_table();
f67172b6 677 $table->head = array(get_string('category','quiz'), get_string('action'));
678 $table->data = $feedbackdata;
16be8974 679 echo html_writer::table($table);
f67172b6 680 }
681 }
682 return true;
683}
7fb1b88d 684
685/**
686 * This function should be considered private to the question bank, it is called from
687 * question/editlib.php question/contextmoveq.php and a few similar places to to the work of
688 * acutally moving questions and associated data. However, callers of this function also have to
689 * do other work, which is why you should not call this method directly from outside the questionbank.
690 *
691 * @param string $questionids a comma-separated list of question ids.
fe6ce234 692 * @param integer $newcategoryid the id of the category to move to.
7fb1b88d 693 */
fe6ce234
DC
694function question_move_questions_to_category($questionids, $newcategoryid) {
695 global $DB, $QTYPES;
f685e830 696
5d548d3e
TH
697 $newcontextid = $DB->get_field('question_categories', 'contextid',
698 array('id' => $newcategoryid));
699 list($questionidcondition, $params) = $DB->get_in_or_equal($questionids);
700 $questions = $DB->get_records_sql("
701 SELECT q.id, q.qtype, qc.contextid
702 FROM {question} q
703 JOIN {question_categories} qc ON q.category = qc.id
704 WHERE q.id $questionidcondition", $params);
705 foreach ($questions as $question) {
706 if ($newcontextid != $question->contextid) {
707 $QTYPES[$question->qtype]->move_files($question->id,
708 $question->contextid, $newcontextid);
709 }
fe6ce234
DC
710 }
711
7fb1b88d 712 // Move the questions themselves.
5d548d3e 713 $DB->set_field_select('question', 'category', $newcategoryid, "id $questionidcondition", $params);
7fb1b88d 714
715 // Move any subquestions belonging to them.
5d548d3e 716 $DB->set_field_select('question', 'category', $newcategoryid, "parent $questionidcondition", $params);
7fb1b88d 717
718 // TODO Deal with datasets.
719
f685e830 720 return true;
7fb1b88d 721}
722
5d548d3e
TH
723/**
724 * This function helps move a question cateogry to a new context by moving all
725 * the files belonging to all the questions to the new context.
726 * Also moves subcategories.
727 * @param integer $categoryid the id of the category being moved.
728 * @param integer $oldcontextid the old context id.
729 * @param integer $newcontextid the new context id.
730 */
731function question_move_category_to_context($categoryid, $oldcontextid, $newcontextid) {
732 global $DB, $QTYPES;
733
734 $questionids = $DB->get_records_menu('question',
735 array('category' => $categoryid), '', 'id,qtype');
736 foreach ($questionids as $questionid => $qtype) {
737 $QTYPES[$qtype]->move_files($questionid, $oldcontextid, $newcontextid);
738 }
739
740 $subcatids = $DB->get_records_menu('question_categories',
741 array('parent' => $categoryid), '', 'id,1');
742 foreach ($subcatids as $subcatid => $notused) {
743 $DB->set_field('question_categories', 'contextid', $newcontextid, array('id' => $subcatid));
744 question_move_category_to_context($subcatid, $oldcontextid, $newcontextid);
745 }
746}
747
f29aeb5a
TH
748/**
749 * Generate the URL for starting a new preview of a given question with the given options.
750 * @param integer $questionid the question to preview.
751 * @param string $preferredbehaviour the behaviour to use for the preview.
752 * @param float $maxmark the maximum to mark the question out of.
753 * @param question_display_options $displayoptions the display options to use.
754 * @return string the URL.
755 */
756function question_preview_url($questionid, $preferredbehaviour, $maxmark, $displayoptions) {
757 return new moodle_url('/question/preview.php', array(
758 'id' => $questionid,
759 'behaviour' => $preferredbehaviour,
760 'maxmark' => $maxmark,
761 'correctness' => $displayoptions->correctness,
762 'marks' => $displayoptions->marks,
763 'markdp' => $displayoptions->markdp,
764 'feedback' => (bool) $displayoptions->feedback,
765 'generalfeedback' => (bool) $displayoptions->generalfeedback,
766 'rightanswer' => (bool) $displayoptions->rightanswer,
767 'history' => (bool) $displayoptions->history));
768}
769
5a2a5331 770/**
78e7a3dd 771 * Given a list of ids, load the basic information about a set of questions from the questions table.
772 * The $join and $extrafields arguments can be used together to pull in extra data.
773 * See, for example, the usage in mod/quiz/attemptlib.php, and
774 * read the code below to see how the SQL is assembled. Throws exceptions on error.
5a2a5331 775 *
50fcb1d8 776 * @global object
777 * @global object
78e7a3dd 778 * @param array $questionids array of question ids.
779 * @param string $extrafields extra SQL code to be added to the query.
780 * @param string $join extra SQL code to be added to the query.
781 * @param array $extraparams values for any placeholders in $join.
782 * You are strongly recommended to use named placeholder.
5a2a5331 783 *
78e7a3dd 784 * @return array partially complete question objects. You need to call get_question_options
785 * on them before they can be properly used.
5a2a5331 786 */
78e7a3dd 787function question_preload_questions($questionids, $extrafields = '', $join = '', $extraparams = array()) {
f29aeb5a 788 global $DB;
4089d9ec 789 if (empty($questionids)) {
790 return array();
791 }
5a2a5331 792 if ($join) {
78e7a3dd 793 $join = ' JOIN '.$join;
5a2a5331 794 }
795 if ($extrafields) {
796 $extrafields = ', ' . $extrafields;
797 }
78e7a3dd 798 list($questionidcondition, $params) = $DB->get_in_or_equal(
799 $questionids, SQL_PARAMS_NAMED, 'qid0000');
56e82d99
TH
800 $sql = 'SELECT q.*, qc.contextid' . $extrafields . ' FROM {question} q
801 JOIN {question_categories} qc ON q.category = qc.id' .
802 $join .
803 ' WHERE q.id ' . $questionidcondition;
5a2a5331 804
805 // Load the questions
78e7a3dd 806 if (!$questions = $DB->get_records_sql($sql, $extraparams + $params)) {
f29aeb5a 807 return array();
5a2a5331 808 }
809
78e7a3dd 810 foreach ($questions as $question) {
811 $question->_partiallyloaded = true;
812 }
813
b55797b8 814 // Note, a possible optimisation here would be to not load the TEXT fields
815 // (that is, questiontext and generalfeedback) here, and instead load them in
816 // question_load_questions. That would add one DB query, but reduce the amount
817 // of data transferred most of the time. I am not going to do this optimisation
818 // until it is shown to be worthwhile.
819
78e7a3dd 820 return $questions;
821}
822
823/**
824 * Load a set of questions, given a list of ids. The $join and $extrafields arguments can be used
825 * together to pull in extra data. See, for example, the usage in mod/quiz/attempt.php, and
826 * read the code below to see how the SQL is assembled. Throws exceptions on error.
827 *
828 * @param array $questionids array of question ids.
829 * @param string $extrafields extra SQL code to be added to the query.
830 * @param string $join extra SQL code to be added to the query.
831 * @param array $extraparams values for any placeholders in $join.
832 * You are strongly recommended to use named placeholder.
833 *
834 * @return array question objects.
835 */
836function question_load_questions($questionids, $extrafields = '', $join = '') {
837 $questions = question_preload_questions($questionids, $extrafields, $join);
838
5a2a5331 839 // Load the question type specific information
840 if (!get_question_options($questions)) {
841 return 'Could not load the question options';
842 }
843
844 return $questions;
845}
846
516cf3eb 847/**
d7444d44 848 * Private function to factor common code out of get_question_options().
271ffe3f 849 *
d7444d44 850 * @param object $question the question to tidy.
c599a682 851 * @param boolean $loadtags load the question tags from the tags table. Optional, default false.
d7444d44 852 */
c76145d3 853function _tidy_question($question, $loadtags = false) {
c599a682 854 global $CFG, $QTYPES;
d7444d44 855 if (!array_key_exists($question->qtype, $QTYPES)) {
856 $question->qtype = 'missingtype';
857 $question->questiontext = '<p>' . get_string('warningmissingtype', 'quiz') . '</p>' . $question->questiontext;
858 }
a8a8ec51
TH
859 $QTYPES[$question->qtype]->get_question_options($question);
860 if (isset($question->_partiallyloaded)) {
861 unset($question->_partiallyloaded);
78e7a3dd 862 }
c599a682 863 if ($loadtags && !empty($CFG->usetags)) {
864 require_once($CFG->dirroot . '/tag/lib.php');
865 $question->tags = tag_get_tags_array('question', $question->id);
866 }
d7444d44 867}
516cf3eb 868
d7444d44 869/**
870 * Updates the question objects with question type specific
871 * information by calling {@link get_question_options()}
872 *
873 * Can be called either with an array of question objects or with a single
874 * question object.
271ffe3f 875 *
d7444d44 876 * @param mixed $questions Either an array of question objects to be updated
877 * or just a single question object
c599a682 878 * @param boolean $loadtags load the question tags from the tags table. Optional, default false.
d7444d44 879 * @return bool Indicates success or failure.
880 */
c599a682 881function get_question_options(&$questions, $loadtags = false) {
516cf3eb 882 if (is_array($questions)) { // deal with an array of questions
d7444d44 883 foreach ($questions as $i => $notused) {
a8a8ec51 884 _tidy_question($questions[$i], $loadtags);
516cf3eb 885 }
516cf3eb 886 } else { // deal with single question
a8a8ec51 887 _tidy_question($questions, $loadtags);
516cf3eb 888 }
a8a8ec51 889 return true;
516cf3eb 890}
891
892/**
f29aeb5a
TH
893* Print the icon for the question type
894*
895* @param object $question The question object for which the icon is required.
896* Only $question->qtype is used.
897* @return string the HTML for the img tag.
898*/
899function print_question_icon($question) {
900 global $OUTPUT;
516cf3eb 901
f29aeb5a
TH
902 $qtype = question_bank::get_qtype($question->qtype, false);
903 $namestr = $qtype->menu_name();
81d833ad 904
f29aeb5a
TH
905 // TODO convert to return a moodle_icon object, or whatever the class is.
906 $html = '<img src="' . $OUTPUT->pix_url('icon', $qtype->plugin_name()) . '" alt="' .
907 $namestr . '" title="' . $namestr . '" />';
81d833ad 908
f29aeb5a 909 return $html;
81d833ad 910}
911
912/**
f29aeb5a 913 * Creates a stamp that uniquely identifies this version of the question
81d833ad 914 *
f29aeb5a
TH
915 * In future we want this to use a hash of the question data to guarantee that
916 * identical versions have the same version stamp.
917 *
918 * @param object $question
919 * @return string A unique version stamp
81d833ad 920 */
f29aeb5a
TH
921function question_hash($question) {
922 return make_unique_id_code();
81d833ad 923}
924
f29aeb5a 925/// FUNCTIONS THAT SIMPLY WRAP QUESTIONTYPE METHODS //////////////////////////////////
b55797b8 926/**
f29aeb5a
TH
927 * Get anything that needs to be included in the head of the question editing page
928 * for a particular question type. This function is called by question/question.php.
b55797b8 929 *
f29aeb5a
TH
930 * @param $question A question object. Only $question->qtype is used.
931 * @return string Deprecated. Some HTML code that can go inside the head tag.
b55797b8 932 */
f29aeb5a
TH
933function question_get_editing_head_contributions($question) {
934 question_bank::get_qtype($question->qtype, false)->get_editing_head_contributions();
b55797b8 935}
516cf3eb 936
937/**
f29aeb5a 938 * Saves question options
50fcb1d8 939 *
f29aeb5a
TH
940 * Simply calls the question type specific save_question_options() method.
941 */
942function save_question_options($question) {
f02c6f01 943 global $QTYPES;
516cf3eb 944
f29aeb5a 945 $QTYPES[$question->qtype]->save_question_options($question);
516cf3eb 946}
947
f29aeb5a
TH
948/// CATEGORY FUNCTIONS /////////////////////////////////////////////////////////////////
949
516cf3eb 950/**
f29aeb5a
TH
951 * returns the categories with their names ordered following parent-child relationships
952 * finally it tries to return pending categories (those being orphaned, whose parent is
953 * incorrect) to avoid missing any category from original array.
954 */
955function sort_categories_by_tree(&$categories, $id = 0, $level = 1) {
956 global $DB;
5e9170b4 957
f29aeb5a
TH
958 $children = array();
959 $keys = array_keys($categories);
516cf3eb 960
f29aeb5a
TH
961 foreach ($keys as $key) {
962 if (!isset($categories[$key]->processed) && $categories[$key]->parent == $id) {
963 $children[$key] = $categories[$key];
964 $categories[$key]->processed = true;
965 $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1);
516cf3eb 966 }
967 }
f29aeb5a
TH
968 //If level = 1, we have finished, try to look for non processed categories (bad parent) and sort them too
969 if ($level == 1) {
970 foreach ($keys as $key) {
971 // If not processed and it's a good candidate to start (because its parent doesn't exist in the course)
972 if (!isset($categories[$key]->processed) && !$DB->record_exists(
973 'question_categories', array('contextid'=>$categories[$key]->contextid, 'id'=>$categories[$key]->parent))) {
974 $children[$key] = $categories[$key];
975 $categories[$key]->processed = true;
976 $children = $children + sort_categories_by_tree($categories, $children[$key]->id, $level+1);
977 }
978 }
516cf3eb 979 }
f29aeb5a 980 return $children;
6cb4910c 981}
982
983/**
062a7522 984 * Private method, only for the use of add_indented_names().
271ffe3f 985 *
062a7522 986 * Recursively adds an indentedname field to each category, starting with the category
271ffe3f 987 * with id $id, and dealing with that category and all its children, and
062a7522 988 * return a new array, with those categories in the right order.
989 *
271ffe3f 990 * @param array $categories an array of categories which has had childids
062a7522 991 * fields added by flatten_category_tree(). Passed by reference for
992 * performance only. It is not modfied.
993 * @param int $id the category to start the indenting process from.
994 * @param int $depth the indent depth. Used in recursive calls.
995 * @return array a new array of categories, in the right order for the tree.
6cb4910c 996 */
3bee1ead 997function flatten_category_tree(&$categories, $id, $depth = 0, $nochildrenof = -1) {
271ffe3f 998
062a7522 999 // Indent the name of this category.
1000 $newcategories = array();
1001 $newcategories[$id] = $categories[$id];
1002 $newcategories[$id]->indentedname = str_repeat('&nbsp;&nbsp;&nbsp;', $depth) . $categories[$id]->name;
271ffe3f 1003
062a7522 1004 // Recursively indent the children.
1005 foreach ($categories[$id]->childids as $childid) {
3bee1ead 1006 if ($childid != $nochildrenof){
1007 $newcategories = $newcategories + flatten_category_tree($categories, $childid, $depth + 1, $nochildrenof);
1008 }
6cb4910c 1009 }
271ffe3f 1010
062a7522 1011 // Remove the childids array that were temporarily added.
1012 unset($newcategories[$id]->childids);
271ffe3f 1013
062a7522 1014 return $newcategories;
6cb4910c 1015}
1016
1017/**
062a7522 1018 * Format categories into an indented list reflecting the tree structure.
271ffe3f 1019 *
062a7522 1020 * @param array $categories An array of category objects, for example from the.
1021 * @return array The formatted list of categories.
6cb4910c 1022 */
3bee1ead 1023function add_indented_names($categories, $nochildrenof = -1) {
6cb4910c 1024
271ffe3f 1025 // Add an array to each category to hold the child category ids. This array will be removed
062a7522 1026 // again by flatten_category_tree(). It should not be used outside these two functions.
1027 foreach (array_keys($categories) as $id) {
1028 $categories[$id]->childids = array();
6cb4910c 1029 }
1030
062a7522 1031 // Build the tree structure, and record which categories are top-level.
271ffe3f 1032 // We have to be careful, because the categories array may include published
062a7522 1033 // categories from other courses, but not their parents.
1034 $toplevelcategoryids = array();
1035 foreach (array_keys($categories) as $id) {
1036 if (!empty($categories[$id]->parent) && array_key_exists($categories[$id]->parent, $categories)) {
1037 $categories[$categories[$id]->parent]->childids[] = $id;
1038 } else {
1039 $toplevelcategoryids[] = $id;
6cb4910c 1040 }
1041 }
1042
062a7522 1043 // Flatten the tree to and add the indents.
1044 $newcategories = array();
1045 foreach ($toplevelcategoryids as $id) {
3bee1ead 1046 $newcategories = $newcategories + flatten_category_tree($categories, $id, 0, $nochildrenof);
6cb4910c 1047 }
1048
062a7522 1049 return $newcategories;
6cb4910c 1050}
2dd6d66b 1051
516cf3eb 1052/**
062a7522 1053 * Output a select menu of question categories.
271ffe3f 1054 *
062a7522 1055 * Categories from this course and (optionally) published categories from other courses
271ffe3f 1056 * are included. Optionally, only categories the current user may edit can be included.
062a7522 1057 *
1058 * @param integer $courseid the id of the course to get the categories for.
1059 * @param integer $published if true, include publised categories from other courses.
1060 * @param integer $only_editable if true, exclude categories this user is not allowed to edit.
1061 * @param integer $selected optionally, the id of a category to be selected by default in the dropdown.
1062 */
3bee1ead 1063function question_category_select_menu($contexts, $top = false, $currentcat = 0, $selected = "", $nochildrenof = -1) {
641454c6 1064 global $OUTPUT;
3bee1ead 1065 $categoriesarray = question_category_options($contexts, $top, $currentcat, false, $nochildrenof);
3ed998f3 1066 if ($selected) {
6770330d 1067 $choose = '';
3ed998f3 1068 } else {
6770330d 1069 $choose = 'choosedots';
516cf3eb 1070 }
6770330d
PS
1071 $options = array();
1072 foreach($categoriesarray as $group=>$opts) {
1073 $options[] = array($group=>$opts);
1074 }
1075
1076 echo html_writer::select($options, 'category', $selected, $choose);
516cf3eb 1077}
1078
cca6e300 1079/**
1080 * @param integer $contextid a context id.
1081 * @return object the default question category for that context, or false if none.
1082 */
1083function question_get_default_category($contextid) {
1084 global $DB;
1085 $category = $DB->get_records('question_categories', array('contextid' => $contextid),'id','*',0,1);
1086 if (!empty($category)) {
1087 return reset($category);
1088 } else {
1089 return false;
1090 }
1091}
1092
8f0f605d 1093/**
1094* Gets the default category in the most specific context.
1095* If no categories exist yet then default ones are created in all contexts.
1096*
1097* @param array $contexts The context objects for this context and all parent contexts.
1098* @return object The default category - the category in the course context
1099*/
1100function question_make_default_categories($contexts) {
eb84a826 1101 global $DB;
353b2d70 1102 static $preferredlevels = array(
1103 CONTEXT_COURSE => 4,
1104 CONTEXT_MODULE => 3,
1105 CONTEXT_COURSECAT => 2,
1106 CONTEXT_SYSTEM => 1,
1107 );
eb84a826 1108
8f0f605d 1109 $toreturn = null;
353b2d70 1110 $preferredness = 0;
8f0f605d 1111 // If it already exists, just return it.
1112 foreach ($contexts as $key => $context) {
353b2d70 1113 if (!$exists = $DB->record_exists("question_categories", array('contextid'=>$context->id))) {
eb84a826 1114 // Otherwise, we need to make one
1115 $category = new stdClass;
1116 $contextname = print_context_name($context, false, true);
1117 $category->name = get_string('defaultfor', 'question', $contextname);
1118 $category->info = get_string('defaultinfofor', 'question', $contextname);
1119 $category->contextid = $context->id;
1120 $category->parent = 0;
1121 $category->sortorder = 999; // By default, all categories get this number, and are sorted alphabetically.
1122 $category->stamp = make_unique_id_code();
bf8e93d7 1123 $category->id = $DB->insert_record('question_categories', $category);
b9bd6da4 1124 } else {
cca6e300 1125 $category = question_get_default_category($context->id);
8f0f605d 1126 }
80f7bafd 1127 if ($preferredlevels[$context->contextlevel] > $preferredness &&
1128 has_any_capability(array('moodle/question:usemine', 'moodle/question:useall'), $context)) {
353b2d70 1129 $toreturn = $category;
1130 $preferredness = $preferredlevels[$context->contextlevel];
8f0f605d 1131 }
1132 }
1133
353b2d70 1134 if (!is_null($toreturn)) {
1135 $toreturn = clone($toreturn);
1136 }
8f0f605d 1137 return $toreturn;
1138}
1139
375ed78a 1140/**
3bee1ead 1141 * Get all the category objects, including a count of the number of questions in that category,
1142 * for all the categories in the lists $contexts.
375ed78a 1143 *
3bee1ead 1144 * @param mixed $contexts either a single contextid, or a comma-separated list of context ids.
1145 * @param string $sortorder used as the ORDER BY clause in the select statement.
1146 * @return array of category objects.
375ed78a 1147 */
3bee1ead 1148function get_categories_for_contexts($contexts, $sortorder = 'parent, sortorder, name ASC') {
eb84a826 1149 global $DB;
1150 return $DB->get_records_sql("
630a3dc3 1151 SELECT c.*, (SELECT count(1) FROM {question} q
eb84a826 1152 WHERE c.id = q.category AND q.hidden='0' AND q.parent='0') AS questioncount
1153 FROM {question_categories} c
1154 WHERE c.contextid IN ($contexts)
1155 ORDER BY $sortorder");
3bee1ead 1156}
3a3c454e 1157
3bee1ead 1158/**
1159 * Output an array of question categories.
1160 */
1161function question_category_options($contexts, $top = false, $currentcat = 0, $popupform = false, $nochildrenof = -1) {
1162 global $CFG;
1163 $pcontexts = array();
1164 foreach($contexts as $context){
1165 $pcontexts[] = $context->id;
61d9b9fc 1166 }
3bee1ead 1167 $contextslist = join($pcontexts, ', ');
1168
1169 $categories = get_categories_for_contexts($contextslist);
375ed78a 1170
3bee1ead 1171 $categories = question_add_context_in_key($categories);
1172
1173 if ($top){
1174 $categories = question_add_tops($categories, $pcontexts);
1175 }
1176 $categories = add_indented_names($categories, $nochildrenof);
3ed998f3 1177
3bee1ead 1178 //sort cats out into different contexts
375ed78a 1179 $categoriesarray = array();
3bee1ead 1180 foreach ($pcontexts as $pcontext){
1181 $contextstring = print_context_name(get_context_instance_by_id($pcontext), true, true);
1182 foreach ($categories as $category) {
1183 if ($category->contextid == $pcontext){
1184 $cid = $category->id;
1185 if ($currentcat!= $cid || $currentcat==0) {
1e05bf54 1186 $countstring = (!empty($category->questioncount))?" ($category->questioncount)":'';
3bee1ead 1187 $categoriesarray[$contextstring][$cid] = $category->indentedname.$countstring;
1188 }
1189 }
375ed78a 1190 }
1191 }
3bee1ead 1192 if ($popupform){
1193 $popupcats = array();
1194 foreach ($categoriesarray as $contextstring => $optgroup){
94fa6be4
PS
1195 $group = array();
1196 foreach ($optgroup as $key=>$value) {
78bfb562
PS
1197 $key = str_replace($CFG->wwwroot, '', $key);
1198 $group[$key] = $value;
94fa6be4
PS
1199 }
1200 $popupcats[] = array($contextstring=>$group);
3bee1ead 1201 }
1202 return $popupcats;
1203 } else {
1204 return $categoriesarray;
1205 }
375ed78a 1206}
1207
3bee1ead 1208function question_add_context_in_key($categories){
1209 $newcatarray = array();
1210 foreach ($categories as $id => $category) {
1211 $category->parent = "$category->parent,$category->contextid";
1212 $category->id = "$category->id,$category->contextid";
1213 $newcatarray["$id,$category->contextid"] = $category;
516cf3eb 1214 }
3bee1ead 1215 return $newcatarray;
1216}
f29aeb5a 1217
3bee1ead 1218function question_add_tops($categories, $pcontexts){
1219 $topcats = array();
1220 foreach ($pcontexts as $context){
365a5941 1221 $newcat = new stdClass();
3bee1ead 1222 $newcat->id = "0,$context";
1223 $newcat->name = get_string('top');
1224 $newcat->parent = -1;
1225 $newcat->contextid = $context;
1226 $topcats["0,$context"] = $newcat;
1227 }
1228 //put topcats in at beginning of array - they'll be sorted into different contexts later.
1229 return array_merge($topcats, $categories);
516cf3eb 1230}
1231
516cf3eb 1232/**
b20ea669 1233 * Returns a comma separated list of ids of the category and all subcategories
1234 */
dc1f00de 1235function question_categorylist($categoryid) {
eb84a826 1236 global $DB;
1237
516cf3eb 1238 // returns a comma separated list of ids of the category and all subcategories
1239 $categorylist = $categoryid;
f29aeb5a 1240 if ($subcategories = $DB->get_records('question_categories', array('parent' => $categoryid), 'sortorder ASC', 'id, 1')) {
516cf3eb 1241 foreach ($subcategories as $subcategory) {
dc1f00de 1242 $categorylist .= ','. question_categorylist($subcategory->id);
516cf3eb 1243 }
1244 }
1245 return $categorylist;
1246}
1247
947217d7 1248//===========================
1249// Import/Export Functions
1250//===========================
1251
ff4b6492 1252/**
1253 * Get list of available import or export formats
1254 * @param string $type 'import' if import list, otherwise export list assumed
271ffe3f 1255 * @return array sorted list of import/export formats available
50fcb1d8 1256 */
f29aeb5a 1257function get_import_export_formats($type) {
ff4b6492 1258 global $CFG;
f7c1dfaf 1259
f29aeb5a 1260 $fileformats = get_plugin_list('qformat');
ff4b6492 1261
f7c1dfaf
TH
1262 $fileformatname = array();
1263 require_once($CFG->dirroot . '/question/format.php');
f29aeb5a
TH
1264 foreach ($fileformats as $fileformat => $fdir) {
1265 $formatfile = $fdir . '/format.php';
1266 if (is_readable($formatfile)) {
1267 include_once($formatfile);
1268 } else {
ff4b6492 1269 continue;
1270 }
f29aeb5a 1271
f7c1dfaf 1272 $classname = 'qformat_' . $fileformat;
f29aeb5a
TH
1273 $formatclass = new $classname();
1274 if ($type == 'import') {
f7c1dfaf 1275 $provided = $formatclass->provide_import();
f29aeb5a 1276 } else {
f7c1dfaf 1277 $provided = $formatclass->provide_export();
ff4b6492 1278 }
f29aeb5a 1279
ff4b6492 1280 if ($provided) {
f7c1dfaf 1281 $fileformatnames[$fileformat] = get_string($fileformat, 'qformat_' . $fileformat);
ff4b6492 1282 }
1283 }
ff4b6492 1284
f7c1dfaf 1285 textlib_get_instance()->asort($fileformatnames);
ff4b6492 1286 return $fileformatnames;
1287}
feb60a07 1288
1289
1290/**
b80d424c
TH
1291* Create a reasonable default file name for exporting questions from a particular
1292* category.
1293* @param object $course the course the questions are in.
1294* @param object $category the question category.
1295* @return string the filename.
feb60a07 1296*/
b80d424c
TH
1297function question_default_export_filename($course, $category) {
1298 // We build a string that is an appropriate name (questions) from the lang pack,
1299 // then the corse shortname, then the question category name, then a timestamp.
1300
1301 $base = clean_filename(get_string('exportfilename', 'question'));
1302
1303 $dateformat = str_replace(' ', '_', get_string('exportnameformat', 'question'));
1304 $timestamp = clean_filename(userdate(time(), $dateformat, 99, false));
1305
1306 $shortname = clean_filename($course->shortname);
1307 if ($shortname == '' || $shortname == '_' ) {
1308 $shortname = $course->id;
1309 }
1310
1311 $categoryname = clean_filename(format_string($category->name));
1312
1313 return "{$base}-{$shortname}-{$categoryname}-{$timestamp}";
feb60a07 1314
1315 return $export_name;
1316}
50fcb1d8 1317
1318/**
f29aeb5a
TH
1319 * Converts contextlevels to strings and back to help with reading/writing contexts
1320 * to/from import/export files.
1321 *
50fcb1d8 1322 * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com}
1323 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1324 */
3bee1ead 1325class context_to_string_translator{
1326 /**
1327 * @var array used to translate between contextids and strings for this context.
1328 */
f29aeb5a 1329 protected $contexttostringarray = array();
3bee1ead 1330
f29aeb5a 1331 public function __construct($contexts) {
3bee1ead 1332 $this->generate_context_to_string_array($contexts);
1333 }
1334
f29aeb5a 1335 public function context_to_string($contextid) {
3bee1ead 1336 return $this->contexttostringarray[$contextid];
1337 }
1338
f29aeb5a 1339 public function string_to_context($contextname) {
3bee1ead 1340 $contextid = array_search($contextname, $this->contexttostringarray);
1341 return $contextid;
1342 }
1343
f29aeb5a 1344 protected function generate_context_to_string_array($contexts) {
3bee1ead 1345 if (!$this->contexttostringarray){
1346 $catno = 1;
1347 foreach ($contexts as $context){
f29aeb5a 1348 switch ($context->contextlevel){
3bee1ead 1349 case CONTEXT_MODULE :
1350 $contextstring = 'module';
1351 break;
1352 case CONTEXT_COURSE :
1353 $contextstring = 'course';
1354 break;
1355 case CONTEXT_COURSECAT :
1356 $contextstring = "cat$catno";
1357 $catno++;
1358 break;
1359 case CONTEXT_SYSTEM :
1360 $contextstring = 'system';
1361 break;
1362 }
1363 $this->contexttostringarray[$context->id] = $contextstring;
1364 }
1365 }
1366 }
1367
1368}
feb60a07 1369
3bee1ead 1370/**
1371 * Check capability on category
50fcb1d8 1372 *
3bee1ead 1373 * @param mixed $question object or id
1374 * @param string $cap 'add', 'edit', 'view', 'use', 'move'
1375 * @param integer $cachecat useful to cache all question records in a category
1376 * @return boolean this user has the capability $cap for this question $question?
1377 */
1378function question_has_capability_on($question, $cap, $cachecat = -1){
eb84a826 1379 global $USER, $DB;
1380
3bee1ead 1381 // these are capabilities on existing questions capabilties are
1382 //set per category. Each of these has a mine and all version. Append 'mine' and 'all'
1383 $question_questioncaps = array('edit', 'view', 'use', 'move');
3bee1ead 1384 static $questions = array();
1385 static $categories = array();
1386 static $cachedcat = array();
9203b705
TH
1387 if ($cachecat != -1 && array_search($cachecat, $cachedcat) === false) {
1388 $questions += $DB->get_records('question', array('category' => $cachecat));
3bee1ead 1389 $cachedcat[] = $cachecat;
1390 }
1391 if (!is_object($question)){
1392 if (!isset($questions[$question])){
9203b705 1393 if (!$questions[$question] = $DB->get_record('question', array('id' => $question), 'id,category,createdby')) {
0f6475b3 1394 print_error('questiondoesnotexist', 'question');
3bee1ead 1395 }
1396 }
1397 $question = $questions[$question];
1398 }
1399 if (!isset($categories[$question->category])){
eb84a826 1400 if (!$categories[$question->category] = $DB->get_record('question_categories', array('id'=>$question->category))) {
3bee1ead 1401 print_error('invalidcategory', 'quiz');
1402 }
1403 }
1404 $category = $categories[$question->category];
9203b705 1405 $context = get_context_instance_by_id($category->contextid);
3bee1ead 1406
1407 if (array_search($cap, $question_questioncaps)!== FALSE){
9203b705 1408 if (!has_capability('moodle/question:'.$cap.'all', $context)){
3bee1ead 1409 if ($question->createdby == $USER->id){
9203b705 1410 return has_capability('moodle/question:'.$cap.'mine', $context);
3bee1ead 1411 } else {
1412 return false;
1413 }
1414 } else {
1415 return true;
1416 }
1417 } else {
9203b705 1418 return has_capability('moodle/question:'.$cap, $context);
3bee1ead 1419 }
1420
1421}
1422
1423/**
1424 * Require capability on question.
1425 */
1426function question_require_capability_on($question, $cap){
1427 if (!question_has_capability_on($question, $cap)){
1428 print_error('nopermissions', '', '', $cap);
1429 }
1430 return true;
1431}
1432
4f5ffac0 1433/**
43ec99aa 1434 * Get the real state - the correct question id and answer - for a random
1435 * question.
4f5ffac0 1436 * @param object $state with property answer.
1437 * @return mixed return integer real question id or false if there was an
1438 * error..
1439 */
5d548d3e 1440function question_get_real_state($state) {
aa9a6867 1441 global $OUTPUT;
43ec99aa 1442 $realstate = clone($state);
4f5ffac0 1443 $matches = array();
91ec2c42 1444 if (!preg_match('|^random([0-9]+)-(.*)|', $state->answer, $matches)){
aa9a6867 1445 echo $OUTPUT->notification(get_string('errorrandom', 'quiz_statistics'));
4f5ffac0 1446 return false;
1447 } else {
43ec99aa 1448 $realstate->question = $matches[1];
1449 $realstate->answer = $matches[2];
1450 return $realstate;
4f5ffac0 1451 }
1452}
43ec99aa 1453
62e76c67 1454/**
f29aeb5a
TH
1455 * @param object $context a context
1456 * @return string A URL for editing questions in this context.
f62040ed 1457 */
f29aeb5a
TH
1458function question_edit_url($context) {
1459 global $CFG, $SITE;
1460 if (!has_any_capability(question_get_question_capabilities(), $context)) {
1461 return false;
f62040ed 1462 }
f29aeb5a
TH
1463 $baseurl = $CFG->wwwroot . '/question/edit.php?';
1464 $defaultcategory = question_get_default_category($context->id);
1465 if ($defaultcategory) {
1466 $baseurl .= 'cat=' . $defaultcategory->id . ',' . $context->id . '&amp;';
f62040ed 1467 }
f29aeb5a
TH
1468 switch ($context->contextlevel) {
1469 case CONTEXT_SYSTEM:
1470 return $baseurl . 'courseid=' . $SITE->id;
1471 case CONTEXT_COURSECAT:
1472 // This is nasty, becuase we can only edit questions in a course
1473 // context at the moment, so for now we just return false.
1474 return false;
1475 case CONTEXT_COURSE:
1476 return $baseurl . 'courseid=' . $context->instanceid;
1477 case CONTEXT_MODULE:
1478 return $baseurl . 'cmid=' . $context->instanceid;
62e76c67 1479 }
f29aeb5a 1480
62e76c67 1481}
56ed242b
SH
1482
1483/**
792881f0 1484 * Adds question bank setting links to the given navigation node if caps are met.
56ed242b
SH
1485 *
1486 * @param navigation_node $navigationnode The navigation node to add the question branch to
1487 * @param stdClass $context
1488 * @return navigation_node Returns the question branch that was added
1489 */
1490function question_extend_settings_navigation(navigation_node $navigationnode, $context) {
1491 global $PAGE;
1492
1493 if ($context->contextlevel == CONTEXT_COURSE) {
1494 $params = array('courseid'=>$context->instanceid);
1495 } else if ($context->contextlevel == CONTEXT_MODULE) {
1496 $params = array('cmid'=>$context->instanceid);
1497 } else {
1498 return;
1499 }
1500
1501 $questionnode = $navigationnode->add(get_string('questionbank','question'), new moodle_url('/question/edit.php', $params), navigation_node::TYPE_CONTAINER);
1502
1503 $contexts = new question_edit_contexts($context);
1504 if ($contexts->have_one_edit_tab_cap('questions')) {
1505 $questionnode->add(get_string('questions', 'quiz'), new moodle_url('/question/edit.php', $params), navigation_node::TYPE_SETTING);
1506 }
1507 if ($contexts->have_one_edit_tab_cap('categories')) {
1508 $questionnode->add(get_string('categories', 'quiz'), new moodle_url('/question/category.php', $params), navigation_node::TYPE_SETTING);
1509 }
1510 if ($contexts->have_one_edit_tab_cap('import')) {
1511 $questionnode->add(get_string('import', 'quiz'), new moodle_url('/question/import.php', $params), navigation_node::TYPE_SETTING);
1512 }
1513 if ($contexts->have_one_edit_tab_cap('export')) {
1514 $questionnode->add(get_string('export', 'quiz'), new moodle_url('/question/export.php', $params), navigation_node::TYPE_SETTING);
1515 }
1516
1517 return $questionnode;
1518}
1519
f29aeb5a
TH
1520/**
1521 * @return array all the capabilities that relate to accessing particular questions.
1522 */
1523function question_get_question_capabilities() {
1524 return array(
1525 'moodle/question:add',
1526 'moodle/question:editmine',
1527 'moodle/question:editall',
1528 'moodle/question:viewmine',
1529 'moodle/question:viewall',
1530 'moodle/question:usemine',
1531 'moodle/question:useall',
1532 'moodle/question:movemine',
1533 'moodle/question:moveall',
1534 );
1535}
1536
1537/**
1538 * @return array all the question bank capabilities.
1539 */
1540function question_get_all_capabilities() {
1541 $caps = question_get_question_capabilities();
1542 $caps[] = 'moodle/question:managecategory';
1543 $caps[] = 'moodle/question:flag';
1544 return $caps;
1545}
1546
56ed242b
SH
1547class question_edit_contexts {
1548
1549 public static $CAPS = array(
1550 'editq' => array('moodle/question:add',
1551 'moodle/question:editmine',
1552 'moodle/question:editall',
1553 'moodle/question:viewmine',
1554 'moodle/question:viewall',
1555 'moodle/question:usemine',
1556 'moodle/question:useall',
1557 'moodle/question:movemine',
1558 'moodle/question:moveall'),
1559 'questions'=>array('moodle/question:add',
1560 'moodle/question:editmine',
1561 'moodle/question:editall',
1562 'moodle/question:viewmine',
1563 'moodle/question:viewall',
1564 'moodle/question:movemine',
1565 'moodle/question:moveall'),
1566 'categories'=>array('moodle/question:managecategory'),
1567 'import'=>array('moodle/question:add'),
1568 'export'=>array('moodle/question:viewall', 'moodle/question:viewmine'));
1569
1570 protected $allcontexts;
1571
1572 /**
1573 * @param current context
1574 */
f29aeb5a 1575 public function question_edit_contexts($thiscontext) {
56ed242b
SH
1576 $pcontextids = get_parent_contexts($thiscontext);
1577 $contexts = array($thiscontext);
1578 foreach ($pcontextids as $pcontextid){
1579 $contexts[] = get_context_instance_by_id($pcontextid);
1580 }
1581 $this->allcontexts = $contexts;
1582 }
1583 /**
1584 * @return array all parent contexts
1585 */
f29aeb5a 1586 public function all() {
56ed242b
SH
1587 return $this->allcontexts;
1588 }
1589 /**
1590 * @return object lowest context which must be either the module or course context
1591 */
f29aeb5a 1592 public function lowest() {
56ed242b
SH
1593 return $this->allcontexts[0];
1594 }
1595 /**
1596 * @param string $cap capability
1597 * @return array parent contexts having capability, zero based index
1598 */
f29aeb5a 1599 public function having_cap($cap) {
56ed242b
SH
1600 $contextswithcap = array();
1601 foreach ($this->allcontexts as $context){
1602 if (has_capability($cap, $context)){
1603 $contextswithcap[] = $context;
1604 }
1605 }
1606 return $contextswithcap;
1607 }
1608 /**
1609 * @param array $caps capabilities
1610 * @return array parent contexts having at least one of $caps, zero based index
1611 */
f29aeb5a 1612 public function having_one_cap($caps) {
56ed242b
SH
1613 $contextswithacap = array();
1614 foreach ($this->allcontexts as $context){
1615 foreach ($caps as $cap){
1616 if (has_capability($cap, $context)){
1617 $contextswithacap[] = $context;
1618 break; //done with caps loop
1619 }
1620 }
1621 }
1622 return $contextswithacap;
1623 }
1624 /**
1625 * @param string $tabname edit tab name
1626 * @return array parent contexts having at least one of $caps, zero based index
1627 */
f29aeb5a 1628 public function having_one_edit_tab_cap($tabname) {
56ed242b
SH
1629 return $this->having_one_cap(self::$CAPS[$tabname]);
1630 }
1631 /**
1632 * Has at least one parent context got the cap $cap?
1633 *
1634 * @param string $cap capability
1635 * @return boolean
1636 */
f29aeb5a 1637 public function have_cap($cap) {
56ed242b
SH
1638 return (count($this->having_cap($cap)));
1639 }
1640
1641 /**
1642 * Has at least one parent context got one of the caps $caps?
1643 *
1644 * @param array $caps capability
1645 * @return boolean
1646 */
f29aeb5a 1647 public function have_one_cap($caps) {
56ed242b
SH
1648 foreach ($caps as $cap) {
1649 if ($this->have_cap($cap)) {
1650 return true;
1651 }
1652 }
1653 return false;
1654 }
f29aeb5a 1655
56ed242b
SH
1656 /**
1657 * Has at least one parent context got one of the caps for actions on $tabname
1658 *
1659 * @param string $tabname edit tab name
1660 * @return boolean
1661 */
1662 public function have_one_edit_tab_cap($tabname){
1663 return $this->have_one_cap(self::$CAPS[$tabname]);
1664 }
f29aeb5a 1665
56ed242b
SH
1666 /**
1667 * Throw error if at least one parent context hasn't got the cap $cap
1668 *
1669 * @param string $cap capability
1670 */
1671 public function require_cap($cap){
1672 if (!$this->have_cap($cap)){
1673 print_error('nopermissions', '', '', $cap);
1674 }
1675 }
f29aeb5a 1676
56ed242b
SH
1677 /**
1678 * Throw error if at least one parent context hasn't got one of the caps $caps
1679 *
1680 * @param array $cap capabilities
1681 */
1682 public function require_one_cap($caps) {
1683 if (!$this->have_one_cap($caps)) {
1684 $capsstring = join($caps, ', ');
1685 print_error('nopermissions', '', '', $capsstring);
1686 }
1687 }
1688
1689 /**
1690 * Throw error if at least one parent context hasn't got one of the caps $caps
1691 *
1692 * @param string $tabname edit tab name
1693 */
1694 public function require_one_edit_tab_cap($tabname){
1695 if (!$this->have_one_edit_tab_cap($tabname)) {
1696 print_error('nopermissions', '', '', 'access question edit tab '.$tabname);
1697 }
1698 }
fe6ce234
DC
1699}
1700
1701/**
1702 * Rewrite question url, file_rewrite_pluginfile_urls always build url by
1703 * $file/$contextid/$component/$filearea/$itemid/$pathname_in_text, so we cannot add
1704 * extra questionid and attempted in url by it, so we create quiz_rewrite_question_urls
1705 * to build url here
1706 *
1707 * @param string $text text being processed
1708 * @param string $file the php script used to serve files
1709 * @param int $contextid
1710 * @param string $component component
1711 * @param string $filearea filearea
1712 * @param array $ids other IDs will be used to check file permission
1713 * @param int $itemid
1714 * @param array $options
1715 * @return string
1716 */
2b7da645 1717function question_rewrite_question_urls($text, $file, $contextid, $component, $filearea, array $ids, $itemid, array $options=null) {
fe6ce234
DC
1718 global $CFG;
1719
1720 $options = (array)$options;
1721 if (!isset($options['forcehttps'])) {
1722 $options['forcehttps'] = false;
1723 }
1724
1725 if (!$CFG->slasharguments) {
1726 $file = $file . '?file=';
1727 }
1728
1729 $baseurl = "$CFG->wwwroot/$file/$contextid/$component/$filearea/";
1730
1731 if (!empty($ids)) {
1732 $baseurl .= (implode('/', $ids) . '/');
1733 }
1734
1735 if ($itemid !== null) {
1736 $baseurl .= "$itemid/";
1737 }
1738
1739 if ($options['forcehttps']) {
1740 $baseurl = str_replace('http://', 'https://', $baseurl);
1741 }
1742
1743 return str_replace('@@PLUGINFILE@@/', $baseurl, $text);
1744}
1745
1746/**
1747 * Called by pluginfile.php to serve files related to the 'question' core
1748 * component and for files belonging to qtypes.
1749 *
1750 * For files that relate to questions in a question_attempt, then we delegate to
1751 * a function in the component that owns the attempt (for example in the quiz,
1752 * or in core question preview) to get necessary inforation.
1753 *
1754 * (Note that, at the moment, all question file areas relate to questions in
1755 * attempts, so the If at the start of the last paragraph is always true.)
1756 *
1757 * Does not return, either calls send_file_not_found(); or serves the file.
1758 *
1759 * @param object $course course settings object
1760 * @param object $context context object
1761 * @param string $component the name of the component we are serving files for.
1762 * @param string $filearea the name of the file area.
1763 * @param array $args the remaining bits of the file path.
1764 * @param bool $forcedownload whether the user must be forced to download the file.
1765 */
1766function question_pluginfile($course, $context, $component, $filearea, $args, $forcedownload) {
1767 global $DB, $CFG;
1768
cde2709a
DC
1769 list($context, $course, $cm) = get_context_info_array($context->id);
1770 require_login($course, false, $cm);
1771
1772 if ($filearea === 'export') {
1773 require_once($CFG->dirroot . '/question/editlib.php');
1774 $contexts = new question_edit_contexts($context);
1775 // check export capability
1776 $contexts->require_one_edit_tab_cap('export');
1777 $category_id = (int)array_shift($args);
1778 $format = array_shift($args);
1779 $cattofile = array_shift($args);
1780 $contexttofile = array_shift($args);
1781 $filename = array_shift($args);
1782
1783 // load parent class for import/export
1784 require_once($CFG->dirroot . '/question/format.php');
1785 require_once($CFG->dirroot . '/question/editlib.php');
1786 require_once($CFG->dirroot . '/question/format/' . $format . '/format.php');
1787
1788 $classname = 'qformat_' . $format;
1789 if (!class_exists($classname)) {
1790 send_file_not_found();
1791 }
1792
1793 $qformat = new $classname();
1794
1795 if (!$category = $DB->get_record('question_categories', array('id' => $category_id))) {
1796 send_file_not_found();
1797 }
1798
1799 $qformat->setCategory($category);
1800 $qformat->setContexts($contexts->having_one_edit_tab_cap('export'));
1801 $qformat->setCourse($course);
1802
1803 if ($cattofile == 'withcategories') {
1804 $qformat->setCattofile(true);
1805 } else {
1806 $qformat->setCattofile(false);
1807 }
1808
1809 if ($contexttofile == 'withcontexts') {
1810 $qformat->setContexttofile(true);
1811 } else {
1812 $qformat->setContexttofile(false);
1813 }
1814
1815 if (!$qformat->exportpreprocess()) {
1816 send_file_not_found();
1817 print_error('exporterror', 'question', $thispageurl->out());
1818 }
1819
1820 // export data to moodle file pool
1821 if (!$content = $qformat->exportprocess(true)) {
1822 send_file_not_found();
1823 }
1824
1825 //DEBUG
1826 //echo '<textarea cols=90 rows=20>';
1827 //echo $content;
1828 //echo '</textarea>';
1829 //die;
46732124 1830 send_file($content, $filename, 0, 0, true, true, $qformat->mime_type());
cde2709a
DC
1831 }
1832
7a719748
TH
1833 $qubaid = (int)array_shift($args);
1834 $slot = (int)array_shift($args);
fe6ce234 1835
7a719748
TH
1836 $module = $DB->get_field('question_usages', 'component',
1837 array('id' => $qubaid));
fe6ce234 1838
7a719748 1839 if ($module === 'core_question_preview') {
fe6ce234
DC
1840 require_once($CFG->dirroot . '/question/previewlib.php');
1841 return question_preview_question_pluginfile($course, $context,
7a719748 1842 $component, $filearea, $qubaid, $slot, $args, $forcedownload);
fe6ce234
DC
1843
1844 } else {
fe6ce234
DC
1845 $dir = get_component_directory($module);
1846 if (!file_exists("$dir/lib.php")) {
1847 send_file_not_found();
1848 }
1849 include_once("$dir/lib.php");
1850
1851 $filefunction = $module . '_question_pluginfile';
1852 if (!function_exists($filefunction)) {
1853 send_file_not_found();
1854 }
1855
7a719748 1856 $filefunction($course, $context, $component, $filearea, $qubaid, $slot,
fe6ce234
DC
1857 $args, $forcedownload);
1858
1859 send_file_not_found();
1860 }
1861}
1862
cde2709a
DC
1863/**
1864 * Create url for question export
1865 *
1866 * @param int $contextid, current context
1867 * @param int $categoryid, categoryid
1868 * @param string $format
1869 * @param string $withcategories
1870 * @param string $ithcontexts
1871 * @param moodle_url export file url
1872 */
b80d424c 1873function question_make_export_url($contextid, $categoryid, $format, $withcategories, $withcontexts, $filename) {
cde2709a
DC
1874 global $CFG;
1875 $urlbase = "$CFG->httpswwwroot/pluginfile.php";
b80d424c 1876 return moodle_url::make_file_url($urlbase, "/$contextid/question/export/{$categoryid}/{$format}/{$withcategories}/{$withcontexts}/{$filename}", true);
cde2709a 1877}