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