Commit | Line | Data |
---|---|---|
aeb15530 | 1 | <?php |
d3603157 TH |
2 | // This file is part of Moodle - http://moodle.org/ |
3 | // | |
4 | // Moodle is free software: you can redistribute it and/or modify | |
5 | // it under the terms of the GNU General Public License as published by | |
6 | // the Free Software Foundation, either version 3 of the License, or | |
7 | // (at your option) any later version. | |
8 | // | |
9 | // Moodle is distributed in the hope that it will be useful, | |
10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
12 | // GNU General Public License for more details. | |
13 | // | |
14 | // You should have received a copy of the GNU General Public License | |
15 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
16 | ||
17 | /** | |
18 | * Defines the base class for question import and export formats. | |
19 | * | |
20 | * @package moodlecore | |
21 | * @subpackage questionbank | |
22 | * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} | |
23 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
24 | */ | |
25 | ||
26 | ||
a17b297d TH |
27 | defined('MOODLE_INTERNAL') || die(); |
28 | ||
29 | ||
4323d029 | 30 | /** |
31 | * Base class for question import and export formats. | |
32 | * | |
d3603157 TH |
33 | * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} |
34 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
4323d029 | 35 | */ |
f5565b69 | 36 | class qformat_default { |
aca318e1 | 37 | |
13bb604e | 38 | public $displayerrors = true; |
e0736817 | 39 | public $category = null; |
13bb604e | 40 | public $questions = array(); |
e0736817 | 41 | public $course = null; |
13bb604e TH |
42 | public $filename = ''; |
43 | public $realfilename = ''; | |
44 | public $matchgrades = 'error'; | |
45 | public $catfromfile = 0; | |
46 | public $contextfromfile = 0; | |
47 | public $cattofile = 0; | |
48 | public $contexttofile = 0; | |
49 | public $questionids = array(); | |
50 | public $importerrors = 0; | |
51 | public $stoponerror = true; | |
52 | public $translator = null; | |
53 | public $canaccessbackupdata = true; | |
cde2709a | 54 | protected $importcontext = null; |
1dab8faa JB |
55 | /** @var bool $displayprogress Whether to display progress. */ |
56 | public $displayprogress = true; | |
aca318e1 | 57 | |
e198992b TH |
58 | // functions to indicate import/export functionality |
59 | // override to return true if implemented | |
aca318e1 | 60 | |
f7970e3c | 61 | /** @return bool whether this plugin provides import functionality. */ |
c7df5006 | 62 | public function provide_import() { |
46732124 | 63 | return false; |
aca318e1 | 64 | } |
65 | ||
f7970e3c | 66 | /** @return bool whether this plugin provides export functionality. */ |
c7df5006 | 67 | public function provide_export() { |
46732124 TH |
68 | return false; |
69 | } | |
70 | ||
71 | /** The string mime-type of the files that this plugin reads or writes. */ | |
c7df5006 | 72 | public function mime_type() { |
46732124 TH |
73 | return mimeinfo('type', $this->export_file_extension()); |
74 | } | |
75 | ||
76 | /** | |
77 | * @return string the file extension (including .) that is normally used for | |
78 | * files handled by this plugin. | |
79 | */ | |
c7df5006 | 80 | public function export_file_extension() { |
46732124 | 81 | return '.txt'; |
aca318e1 | 82 | } |
a1ef8c6d | 83 | |
7da7bfa1 JF |
84 | /** |
85 | * Check if the given file is capable of being imported by this plugin. | |
86 | * | |
87 | * Note that expensive or detailed integrity checks on the file should | |
88 | * not be performed by this method. Simple file type or magic-number tests | |
89 | * would be suitable. | |
90 | * | |
a1ef8c6d | 91 | * @param stored_file $file the file to check |
7da7bfa1 JF |
92 | * @return bool whether this plugin can import the file |
93 | */ | |
94 | public function can_import_file($file) { | |
95 | return ($file->get_mimetype() == $this->mime_type()); | |
96 | } | |
aca318e1 | 97 | |
e198992b | 98 | // Accessor methods |
aca318e1 | 99 | |
08892d5b | 100 | /** |
101 | * set the category | |
102 | * @param object category the category object | |
103 | */ | |
c7df5006 | 104 | public function setCategory($category) { |
13bb604e | 105 | if (count($this->questions)) { |
88bc20c3 | 106 | debugging('You shouldn\'t call setCategory after setQuestions'); |
107 | } | |
08892d5b | 108 | $this->category = $category; |
d197ea43 | 109 | $this->importcontext = context::instance_by_id($this->category->contextid); |
08892d5b | 110 | } |
aca318e1 | 111 | |
2c44a3d3 | 112 | /** |
113 | * Set the specific questions to export. Should not include questions with | |
114 | * parents (sub questions of cloze question type). | |
115 | * Only used for question export. | |
116 | * @param array of question objects | |
117 | */ | |
c7df5006 | 118 | public function setQuestions($questions) { |
13bb604e | 119 | if ($this->category !== null) { |
88bc20c3 | 120 | debugging('You shouldn\'t call setQuestions after setCategory'); |
121 | } | |
2c44a3d3 | 122 | $this->questions = $questions; |
123 | } | |
124 | ||
08892d5b | 125 | /** |
126 | * set the course class variable | |
127 | * @param course object Moodle course variable | |
128 | */ | |
c7df5006 | 129 | public function setCourse($course) { |
aca318e1 | 130 | $this->course = $course; |
08892d5b | 131 | } |
13bb604e | 132 | |
271e6dec | 133 | /** |
134 | * set an array of contexts. | |
135 | * @param array $contexts Moodle course variable | |
136 | */ | |
c7df5006 | 137 | public function setContexts($contexts) { |
271e6dec | 138 | $this->contexts = $contexts; |
139 | $this->translator = new context_to_string_translator($this->contexts); | |
140 | } | |
aca318e1 | 141 | |
08892d5b | 142 | /** |
143 | * set the filename | |
144 | * @param string filename name of file to import/export | |
145 | */ | |
c7df5006 | 146 | public function setFilename($filename) { |
08892d5b | 147 | $this->filename = $filename; |
148 | } | |
88bc20c3 | 149 | |
150 | /** | |
73b7b195 | 151 | * set the "real" filename |
152 | * (this is what the user typed, regardless of wha happened next) | |
153 | * @param string realfilename name of file as typed by user | |
154 | */ | |
c7df5006 | 155 | public function setRealfilename($realfilename) { |
88bc20c3 | 156 | $this->realfilename = $realfilename; |
157 | } | |
08892d5b | 158 | |
159 | /** | |
160 | * set matchgrades | |
161 | * @param string matchgrades error or nearest for grades | |
162 | */ | |
c7df5006 | 163 | public function setMatchgrades($matchgrades) { |
08892d5b | 164 | $this->matchgrades = $matchgrades; |
aca318e1 | 165 | } |
166 | ||
76f0a334 | 167 | /** |
08892d5b | 168 | * set catfromfile |
169 | * @param bool catfromfile allow categories embedded in import file | |
76f0a334 | 170 | */ |
c7df5006 | 171 | public function setCatfromfile($catfromfile) { |
08892d5b | 172 | $this->catfromfile = $catfromfile; |
173 | } | |
271e6dec | 174 | |
175 | /** | |
176 | * set contextfromfile | |
177 | * @param bool $contextfromfile allow contexts embedded in import file | |
178 | */ | |
c7df5006 | 179 | public function setContextfromfile($contextfromfile) { |
271e6dec | 180 | $this->contextfromfile = $contextfromfile; |
181 | } | |
182 | ||
f1abd39f | 183 | /** |
184 | * set cattofile | |
185 | * @param bool cattofile exports categories within export file | |
186 | */ | |
c7df5006 | 187 | public function setCattofile($cattofile) { |
f1abd39f | 188 | $this->cattofile = $cattofile; |
271e6dec | 189 | } |
13bb604e | 190 | |
271e6dec | 191 | /** |
192 | * set contexttofile | |
193 | * @param bool cattofile exports categories within export file | |
194 | */ | |
c7df5006 | 195 | public function setContexttofile($contexttofile) { |
271e6dec | 196 | $this->contexttofile = $contexttofile; |
197 | } | |
aca318e1 | 198 | |
f3701561 | 199 | /** |
200 | * set stoponerror | |
201 | * @param bool stoponerror stops database write if any errors reported | |
202 | */ | |
c7df5006 | 203 | public function setStoponerror($stoponerror) { |
f3701561 | 204 | $this->stoponerror = $stoponerror; |
205 | } | |
206 | ||
1b8b535d | 207 | /** |
f7970e3c | 208 | * @param bool $canaccess Whether the current use can access the backup data folder. Determines |
1b8b535d | 209 | * where export files are saved. |
210 | */ | |
c7df5006 | 211 | public function set_can_access_backupdata($canaccess) { |
1b8b535d | 212 | $this->canaccessbackupdata = $canaccess; |
213 | } | |
214 | ||
1dab8faa JB |
215 | /** |
216 | * Change whether to display progress messages. | |
217 | * There is normally no need to use this function as the | |
218 | * default for $displayprogress is true. | |
219 | * Set to false for unit tests. | |
220 | * @param bool $displayprogress | |
221 | */ | |
222 | public function set_display_progress($displayprogress) { | |
223 | $this->displayprogress = $displayprogress; | |
224 | } | |
225 | ||
e198992b TH |
226 | /*********************** |
227 | * IMPORTING FUNCTIONS | |
228 | ***********************/ | |
f3701561 | 229 | |
230 | /** | |
231 | * Handle parsing error | |
232 | */ | |
c7df5006 | 233 | protected function error($message, $text='', $questionname='') { |
5e8a85aa | 234 | $importerrorquestion = get_string('importerrorquestion', 'question'); |
cdeabc06 | 235 | |
f3701561 | 236 | echo "<div class=\"importerror\">\n"; |
f4fe3968 | 237 | echo "<strong>{$importerrorquestion} {$questionname}</strong>"; |
f3701561 | 238 | if (!empty($text)) { |
239 | $text = s($text); | |
f4fe3968 | 240 | echo "<blockquote>{$text}</blockquote>\n"; |
f3701561 | 241 | } |
f4fe3968 | 242 | echo "<strong>{$message}</strong>\n"; |
f3701561 | 243 | echo "</div>"; |
244 | ||
57944b7a | 245 | $this->importerrors++; |
f3701561 | 246 | } |
08892d5b | 247 | |
271e6dec | 248 | /** |
a41e3287 | 249 | * Import for questiontype plugins |
250 | * Do not override. | |
251 | * @param data mixed The segment of data containing the question | |
252 | * @param question object processed (so far) by standard import code if appropriate | |
253 | * @param extra mixed any additional format specific data that may be passed by the format | |
88bc20c3 | 254 | * @param qtypehint hint about a question type from format |
a41e3287 | 255 | * @return object question object suitable for save_options() or false if cannot handle |
256 | */ | |
c7df5006 | 257 | public function try_importing_using_qtypes($data, $question = null, $extra = null, |
49e2bba7 | 258 | $qtypehint = '') { |
a41e3287 | 259 | |
260 | // work out what format we are using | |
88bc20c3 | 261 | $formatname = substr(get_class($this), strlen('qformat_')); |
f4fe3968 | 262 | $methodname = "import_from_{$formatname}"; |
a41e3287 | 263 | |
88bc20c3 | 264 | //first try importing using a hint from format |
265 | if (!empty($qtypehint)) { | |
49e2bba7 | 266 | $qtype = question_bank::get_qtype($qtypehint, false); |
88bc20c3 | 267 | if (is_object($qtype) && method_exists($qtype, $methodname)) { |
268 | $question = $qtype->$methodname($data, $question, $this, $extra); | |
269 | if ($question) { | |
270 | return $question; | |
271 | } | |
272 | } | |
273 | } | |
274 | ||
a41e3287 | 275 | // loop through installed questiontypes checking for |
276 | // function to handle this question | |
49e2bba7 | 277 | foreach (question_bank::get_all_qtypes() as $qtype) { |
13bb604e TH |
278 | if (method_exists($qtype, $methodname)) { |
279 | if ($question = $qtype->$methodname($data, $question, $this, $extra)) { | |
a41e3287 | 280 | return $question; |
281 | } | |
282 | } | |
271e6dec | 283 | } |
284 | return false; | |
a41e3287 | 285 | } |
286 | ||
08892d5b | 287 | /** |
288 | * Perform any required pre-processing | |
f7970e3c | 289 | * @return bool success |
08892d5b | 290 | */ |
7348402f | 291 | public function importpreprocess() { |
08892d5b | 292 | return true; |
293 | } | |
294 | ||
295 | /** | |
296 | * Process the file | |
297 | * This method should not normally be overidden | |
f7970e3c | 298 | * @return bool success |
08892d5b | 299 | */ |
d0a60444 JB |
300 | public function importprocess() { |
301 | global $USER, $DB, $OUTPUT; | |
cde2709a | 302 | |
cef7621e | 303 | // Raise time and memory, as importing can be quite intensive. |
3ef7279f | 304 | core_php_time_limit::raise(); |
cef7621e | 305 | raise_memory_limit(MEMORY_EXTRA); |
67c12527 | 306 | |
9dd46039 | 307 | // STAGE 1: Parse the file |
1dab8faa JB |
308 | if ($this->displayprogress) { |
309 | echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess'); | |
310 | } | |
271e6dec | 311 | |
08892d5b | 312 | if (! $lines = $this->readdata($this->filename)) { |
5e8a85aa | 313 | echo $OUTPUT->notification(get_string('cannotread', 'question')); |
aca318e1 | 314 | return false; |
315 | } | |
316 | ||
4d188926 | 317 | if (!$questions = $this->readquestions($lines)) { // Extract all the questions |
5e8a85aa | 318 | echo $OUTPUT->notification(get_string('noquestionsinfile', 'question')); |
aca318e1 | 319 | return false; |
320 | } | |
321 | ||
f3701561 | 322 | // STAGE 2: Write data to database |
1dab8faa JB |
323 | if ($this->displayprogress) { |
324 | echo $OUTPUT->notification(get_string('importingquestions', 'question', | |
325 | $this->count_questions($questions)), 'notifysuccess'); | |
326 | } | |
aca318e1 | 327 | |
f3701561 | 328 | // check for errors before we continue |
329 | if ($this->stoponerror and ($this->importerrors>0)) { | |
5e8a85aa | 330 | echo $OUTPUT->notification(get_string('importparseerror', 'question')); |
10b4a508 | 331 | return true; |
f3701561 | 332 | } |
333 | ||
76f0a334 | 334 | // get list of valid answer grades |
92111e8d | 335 | $gradeoptionsfull = question_bank::fraction_options_full(); |
76f0a334 | 336 | |
c1828e0b | 337 | // check answer grades are valid |
338 | // (now need to do this here because of 'stop on error': MDL-10689) | |
339 | $gradeerrors = 0; | |
340 | $goodquestions = array(); | |
341 | foreach ($questions as $question) { | |
342 | if (!empty($question->fraction) and (is_array($question->fraction))) { | |
343 | $fractions = $question->fraction; | |
b5d09003 | 344 | $invalidfractions = array(); |
c1828e0b | 345 | foreach ($fractions as $key => $fraction) { |
e198992b TH |
346 | $newfraction = match_grade_options($gradeoptionsfull, $fraction, |
347 | $this->matchgrades); | |
348 | if ($newfraction === false) { | |
b5d09003 | 349 | $invalidfractions[] = $fraction; |
e198992b | 350 | } else { |
c1828e0b | 351 | $fractions[$key] = $newfraction; |
352 | } | |
353 | } | |
b5d09003 TH |
354 | if ($invalidfractions) { |
355 | echo $OUTPUT->notification(get_string('invalidgrade', 'question', | |
356 | implode(', ', $invalidfractions))); | |
c1828e0b | 357 | ++$gradeerrors; |
358 | continue; | |
e198992b | 359 | } else { |
c1828e0b | 360 | $question->fraction = $fractions; |
361 | } | |
362 | } | |
363 | $goodquestions[] = $question; | |
364 | } | |
365 | $questions = $goodquestions; | |
366 | ||
367 | // check for errors before we continue | |
e198992b | 368 | if ($this->stoponerror && $gradeerrors > 0) { |
c1828e0b | 369 | return false; |
370 | } | |
371 | ||
372 | // count number of questions processed | |
aca318e1 | 373 | $count = 0; |
374 | ||
375 | foreach ($questions as $question) { // Process and store each question | |
66de66fe | 376 | $transaction = $DB->start_delegated_transaction(); |
08892d5b | 377 | |
271e6dec | 378 | // reset the php timeout |
3ef7279f | 379 | core_php_time_limit::raise(); |
67c12527 | 380 | |
08892d5b | 381 | // check for category modifiers |
9dd46039 | 382 | if ($question->qtype == 'category') { |
08892d5b | 383 | if ($this->catfromfile) { |
384 | // find/create category object | |
40e71443 | 385 | $catpath = $question->category; |
1dab8faa | 386 | $newcategory = $this->create_category_path($catpath, $question); |
08892d5b | 387 | if (!empty($newcategory)) { |
388 | $this->category = $newcategory; | |
389 | } | |
390 | } | |
539a25ff | 391 | $transaction->allow_commit(); |
271e6dec | 392 | continue; |
08892d5b | 393 | } |
4d188926 | 394 | $question->context = $this->importcontext; |
08892d5b | 395 | |
aca318e1 | 396 | $count++; |
397 | ||
1dab8faa JB |
398 | if ($this->displayprogress) { |
399 | echo "<hr /><p><b>{$count}</b>. " . $this->format_question_text($question) . "</p>"; | |
400 | } | |
aca318e1 | 401 | |
9dd46039 | 402 | $question->category = $this->category->id; |
aca318e1 | 403 | $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) |
aca318e1 | 404 | |
271e6dec | 405 | $question->createdby = $USER->id; |
406 | $question->timecreated = time(); | |
08f53286 TH |
407 | $question->modifiedby = $USER->id; |
408 | $question->timemodified = time(); | |
2d696f8f TH |
409 | if (isset($question->idnumber)) { |
410 | if ((string) $question->idnumber === '') { | |
411 | // Id number not really set. Get rid of it. | |
6189fda4 | 412 | unset($question->idnumber); |
2d696f8f TH |
413 | } else { |
414 | if ($DB->record_exists('question', | |
415 | ['idnumber' => $question->idnumber, 'category' => $question->category])) { | |
416 | // We cannot have duplicate idnumbers in a category. Just remove it. | |
417 | unset($question->idnumber); | |
418 | } | |
6189fda4 JB |
419 | } |
420 | } | |
421 | ||
7ace84e0 | 422 | $fileoptions = array( |
624ff5ba | 423 | 'subdirs' => true, |
7ace84e0 JMV |
424 | 'maxfiles' => -1, |
425 | 'maxbytes' => 0, | |
426 | ); | |
271e6dec | 427 | |
cde2709a | 428 | $question->id = $DB->insert_record('question', $question); |
4ca60a56 V |
429 | $event = \core\event\question_created::create_from_question_instance($question, $this->importcontext); |
430 | $event->trigger(); | |
7ace84e0 | 431 | |
7980a381 JMV |
432 | if (isset($question->questiontextitemid)) { |
433 | $question->questiontext = file_save_draft_area_files($question->questiontextitemid, | |
7ace84e0 JMV |
434 | $this->importcontext->id, 'question', 'questiontext', $question->id, |
435 | $fileoptions, $question->questiontext); | |
436 | } else if (isset($question->questiontextfiles)) { | |
cde2709a | 437 | foreach ($question->questiontextfiles as $file) { |
d649fb02 | 438 | question_bank::get_qtype($question->qtype)->import_file( |
4d188926 | 439 | $this->importcontext, 'question', 'questiontext', $question->id, $file); |
cde2709a DC |
440 | } |
441 | } | |
7980a381 JMV |
442 | if (isset($question->generalfeedbackitemid)) { |
443 | $question->generalfeedback = file_save_draft_area_files($question->generalfeedbackitemid, | |
7ace84e0 JMV |
444 | $this->importcontext->id, 'question', 'generalfeedback', $question->id, |
445 | $fileoptions, $question->generalfeedback); | |
446 | } else if (isset($question->generalfeedbackfiles)) { | |
cde2709a | 447 | foreach ($question->generalfeedbackfiles as $file) { |
d649fb02 | 448 | question_bank::get_qtype($question->qtype)->import_file( |
4d188926 | 449 | $this->importcontext, 'question', 'generalfeedback', $question->id, $file); |
cde2709a DC |
450 | } |
451 | } | |
7ace84e0 | 452 | $DB->update_record('question', $question); |
aca318e1 | 453 | |
454 | $this->questionids[] = $question->id; | |
455 | ||
456 | // Now to save all the answers and type-specific options | |
457 | ||
d649fb02 | 458 | $result = question_bank::get_qtype($question->qtype)->save_question_options($question); |
aca318e1 | 459 | |
bbd655b4 SL |
460 | if (core_tag_tag::is_enabled('core_question', 'question')) { |
461 | // Is the current context we're importing in a course context? | |
462 | $importingcontext = $this->importcontext; | |
463 | $importingcoursecontext = $importingcontext->get_course_context(false); | |
464 | $isimportingcontextcourseoractivity = !empty($importingcoursecontext); | |
465 | ||
466 | if (!empty($question->coursetags)) { | |
467 | if ($isimportingcontextcourseoractivity) { | |
468 | $mergedtags = array_merge($question->coursetags, $question->tags); | |
469 | ||
470 | core_tag_tag::set_item_tags('core_question', 'question', $question->id, | |
471 | $question->context, $mergedtags); | |
472 | } else { | |
473 | core_tag_tag::set_item_tags('core_question', 'question', $question->id, | |
474 | context_course::instance($this->course->id), $question->coursetags); | |
13596866 | 475 | |
bbd655b4 SL |
476 | if (!empty($question->tags)) { |
477 | core_tag_tag::set_item_tags('core_question', 'question', $question->id, | |
478 | $importingcontext, $question->tags); | |
479 | } | |
480 | } | |
f6bafa92 | 481 | } else if (!empty($question->tags)) { |
bbd655b4 SL |
482 | core_tag_tag::set_item_tags('core_question', 'question', $question->id, |
483 | $question->context, $question->tags); | |
484 | } | |
4f290077 TH |
485 | } |
486 | ||
aca318e1 | 487 | if (!empty($result->error)) { |
fef8f84e | 488 | echo $OUTPUT->notification($result->error); |
66de66fe TH |
489 | // Can't use $transaction->rollback(); since it requires an exception, |
490 | // and I don't want to rewrite this code to change the error handling now. | |
491 | $DB->force_transaction_rollback(); | |
aca318e1 | 492 | return false; |
493 | } | |
494 | ||
66de66fe TH |
495 | $transaction->allow_commit(); |
496 | ||
aca318e1 | 497 | if (!empty($result->notice)) { |
fef8f84e | 498 | echo $OUTPUT->notification($result->notice); |
aca318e1 | 499 | return true; |
500 | } | |
cbe20043 | 501 | |
502 | // Give the question a unique version stamp determined by question_hash() | |
e198992b TH |
503 | $DB->set_field('question', 'version', question_hash($question), |
504 | array('id' => $question->id)); | |
aca318e1 | 505 | } |
506 | return true; | |
507 | } | |
13bb604e | 508 | |
ce2df288 | 509 | /** |
510 | * Count all non-category questions in the questions array. | |
f34488b2 | 511 | * |
ce2df288 | 512 | * @param array questions An array of question objects. |
513 | * @return int The count. | |
f34488b2 | 514 | * |
ce2df288 | 515 | */ |
c7df5006 | 516 | protected function count_questions($questions) { |
ce2df288 | 517 | $count = 0; |
518 | if (!is_array($questions)) { | |
519 | return $count; | |
520 | } | |
521 | foreach ($questions as $question) { | |
e198992b TH |
522 | if (!is_object($question) || !isset($question->qtype) || |
523 | ($question->qtype == 'category')) { | |
ce2df288 | 524 | continue; |
525 | } | |
526 | $count++; | |
527 | } | |
528 | return $count; | |
529 | } | |
530 | ||
271e6dec | 531 | /** |
532 | * find and/or create the category described by a delimited list | |
533 | * e.g. $course$/tom/dick/harry or tom/dick/harry | |
534 | * | |
535 | * removes any context string no matter whether $getcontext is set | |
536 | * but if $getcontext is set then ignore the context and use selected category context. | |
537 | * | |
538 | * @param string catpath delimited category path | |
1dab8faa | 539 | * @param object $lastcategoryinfo Contains category information |
271e6dec | 540 | * @return mixed category object or null if fails |
541 | */ | |
1dab8faa | 542 | protected function create_category_path($catpath, $lastcategoryinfo = null) { |
f34488b2 | 543 | global $DB; |
728d60a1 | 544 | $catnames = $this->split_category_path($catpath); |
271e6dec | 545 | $parent = 0; |
546 | $category = null; | |
88bc20c3 | 547 | |
5ca9e32d | 548 | // check for context id in path, it might not be there in pre 1.9 exports |
549 | $matchcount = preg_match('/^\$([a-z]+)\$$/', $catnames[0], $matches); | |
49e2bba7 | 550 | if ($matchcount == 1) { |
271e6dec | 551 | $contextid = $this->translator->string_to_context($matches[1]); |
552 | array_shift($catnames); | |
553 | } else { | |
728d60a1 | 554 | $contextid = false; |
271e6dec | 555 | } |
728d60a1 | 556 | |
f86f8c85 SR |
557 | // Before 3.5, question categories could be created at top level. |
558 | // From 3.5 onwards, all question categories should be a child of a special category called the "top" category. | |
559 | if (isset($catnames[0]) && (($catnames[0] != 'top') || (count($catnames) < 3))) { | |
560 | array_unshift($catnames, 'top'); | |
561 | } | |
562 | ||
728d60a1 | 563 | if ($this->contextfromfile && $contextid !== false) { |
d197ea43 | 564 | $context = context::instance_by_id($contextid); |
271e6dec | 565 | require_capability('moodle/question:add', $context); |
566 | } else { | |
d197ea43 | 567 | $context = context::instance_by_id($this->category->contextid); |
271e6dec | 568 | } |
4d188926 | 569 | $this->importcontext = $context; |
728d60a1 TH |
570 | |
571 | // Now create any categories that need to be created. | |
1dab8faa | 572 | foreach ($catnames as $key => $catname) { |
f86f8c85 SR |
573 | if ($parent == 0) { |
574 | $category = question_get_top_category($context->id, true); | |
575 | $parent = $category->id; | |
576 | } else if ($category = $DB->get_record('question_categories', | |
e198992b | 577 | array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) { |
e247068d TH |
578 | // If this category is now the last one in the path we are processing ... |
579 | if ($key == (count($catnames) - 1) && $lastcategoryinfo) { | |
580 | // Do nothing unless the child category appears before the parent category | |
581 | // in the imported xml file. Because the parent was created without info being available | |
582 | // at that time, this allows the info to be added from the xml data. | |
583 | if (isset($lastcategoryinfo->info) && $lastcategoryinfo->info !== '' | |
584 | && $category->info === '') { | |
585 | $category->info = $lastcategoryinfo->info; | |
586 | if (isset($lastcategoryinfo->infoformat) && $lastcategoryinfo->infoformat !== '') { | |
587 | $category->infoformat = $lastcategoryinfo->infoformat; | |
588 | } | |
589 | } | |
590 | // Same for idnumber. | |
591 | if (isset($lastcategoryinfo->idnumber) && $lastcategoryinfo->idnumber !== '' | |
592 | && $category->idnumber === '') { | |
593 | $category->idnumber = $lastcategoryinfo->idnumber; | |
1dab8faa JB |
594 | } |
595 | $DB->update_record('question_categories', $category); | |
596 | } | |
271e6dec | 597 | $parent = $category->id; |
598 | } else { | |
d0a60444 JB |
599 | if ($catname == 'top') { |
600 | // Should not happen, but if it does just move on. | |
601 | // Occurs when there has been some import/export that has created | |
602 | // multiple nested 'top' categories (due to old bug solved by MDL-63165). | |
1dab8faa | 603 | // This basically silently cleans up old errors. Not throwing an exception here. |
d0a60444 JB |
604 | continue; |
605 | } | |
271e6dec | 606 | require_capability('moodle/question:managecategory', $context); |
1dab8faa JB |
607 | // Create the new category. This will create all the categories in the catpath, |
608 | // though only the final category will have any info added if available. | |
7f389342 | 609 | $category = new stdClass(); |
271e6dec | 610 | $category->contextid = $context->id; |
611 | $category->name = $catname; | |
612 | $category->info = ''; | |
1dab8faa | 613 | // Only add info (category description) for the final category in the catpath. |
e247068d TH |
614 | if ($key == (count($catnames) - 1) && $lastcategoryinfo) { |
615 | if (isset($lastcategoryinfo->info) && $lastcategoryinfo->info !== '') { | |
616 | $category->info = $lastcategoryinfo->info; | |
617 | if (isset($lastcategoryinfo->infoformat) && $lastcategoryinfo->infoformat !== '') { | |
618 | $category->infoformat = $lastcategoryinfo->infoformat; | |
619 | } | |
620 | } | |
621 | // Same for idnumber. | |
622 | if (isset($lastcategoryinfo->idnumber) && $lastcategoryinfo->idnumber !== '') { | |
623 | $category->idnumber = $lastcategoryinfo->idnumber; | |
1dab8faa JB |
624 | } |
625 | } | |
271e6dec | 626 | $category->parent = $parent; |
627 | $category->sortorder = 999; | |
628 | $category->stamp = make_unique_id_code(); | |
d0a60444 JB |
629 | $category->id = $DB->insert_record('question_categories', $category); |
630 | $parent = $category->id; | |
4ca60a56 V |
631 | $event = \core\event\question_category_created::create_from_question_category_instance($category, $context); |
632 | $event->trigger(); | |
271e6dec | 633 | } |
634 | } | |
635 | return $category; | |
636 | } | |
3f5633df | 637 | |
f3701561 | 638 | /** |
639 | * Return complete file within an array, one item per line | |
640 | * @param string filename name of file | |
641 | * @return mixed contents array or false on failure | |
642 | */ | |
c7df5006 | 643 | protected function readdata($filename) { |
aca318e1 | 644 | if (is_readable($filename)) { |
645 | $filearray = file($filename); | |
646 | ||
77c1f160 | 647 | // If the first line of the file starts with a UTF-8 BOM, remove it. |
2f1e464a | 648 | $filearray[0] = core_text::trim_utf8_bom($filearray[0]); |
77c1f160 TH |
649 | |
650 | // Check for Macintosh OS line returns (ie file on one line), and fix. | |
6dbcacee | 651 | if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) { |
aca318e1 | 652 | return explode("\r", $filearray[0]); |
653 | } else { | |
654 | return $filearray; | |
655 | } | |
656 | } | |
657 | return false; | |
658 | } | |
659 | ||
f3701561 | 660 | /** |
271e6dec | 661 | * Parses an array of lines into an array of questions, |
662 | * where each item is a question object as defined by | |
663 | * readquestion(). Questions are defined as anything | |
f3701561 | 664 | * between blank lines. |
665 | * | |
4d188926 TH |
666 | * NOTE this method used to take $context as a second argument. However, at |
667 | * the point where this method was called, it was impossible to know what | |
668 | * context the quetsions were going to be saved into, so the value could be | |
669 | * wrong. Also, none of the standard question formats were using this argument, | |
670 | * so it was removed. See MDL-32220. | |
671 | * | |
f3701561 | 672 | * If your format does not use blank lines as a delimiter |
673 | * then you will need to override this method. Even then | |
674 | * try to use readquestion for each question | |
675 | * @param array lines array of lines from readdata | |
676 | * @return array array of question objects | |
677 | */ | |
4d188926 | 678 | protected function readquestions($lines) { |
271e6dec | 679 | |
aca318e1 | 680 | $questions = array(); |
681 | $currentquestion = array(); | |
682 | ||
683 | foreach ($lines as $line) { | |
684 | $line = trim($line); | |
685 | if (empty($line)) { | |
686 | if (!empty($currentquestion)) { | |
687 | if ($question = $this->readquestion($currentquestion)) { | |
688 | $questions[] = $question; | |
689 | } | |
690 | $currentquestion = array(); | |
691 | } | |
692 | } else { | |
693 | $currentquestion[] = $line; | |
694 | } | |
695 | } | |
696 | ||
697 | if (!empty($currentquestion)) { // There may be a final question | |
4d188926 | 698 | if ($question = $this->readquestion($currentquestion)) { |
aca318e1 | 699 | $questions[] = $question; |
700 | } | |
701 | } | |
702 | ||
703 | return $questions; | |
704 | } | |
705 | ||
f3701561 | 706 | /** |
707 | * return an "empty" question | |
708 | * Somewhere to specify question parameters that are not handled | |
709 | * by import but are required db fields. | |
710 | * This should not be overridden. | |
711 | * @return object default question | |
271e6dec | 712 | */ |
c7df5006 | 713 | protected function defaultquestion() { |
b0679efa | 714 | global $CFG; |
0cd46577 | 715 | static $defaultshuffleanswers = null; |
716 | if (is_null($defaultshuffleanswers)) { | |
717 | $defaultshuffleanswers = get_config('quiz', 'shuffleanswers'); | |
718 | } | |
271e6dec | 719 | |
aca318e1 | 720 | $question = new stdClass(); |
0cd46577 | 721 | $question->shuffleanswers = $defaultshuffleanswers; |
49e2bba7 | 722 | $question->defaultmark = 1; |
3a8cde29 | 723 | $question->image = ''; |
aca318e1 | 724 | $question->usecase = 0; |
725 | $question->multiplier = array(); | |
45bdcf11 | 726 | $question->questiontextformat = FORMAT_MOODLE; |
172f6d95 | 727 | $question->generalfeedback = ''; |
45bdcf11 | 728 | $question->generalfeedbackformat = FORMAT_MOODLE; |
5931ea94 | 729 | $question->answernumbering = 'abc'; |
49e2bba7 | 730 | $question->penalty = 0.3333333; |
3f5633df | 731 | $question->length = 1; |
aca318e1 | 732 | |
5fd8f999 | 733 | // this option in case the questiontypes class wants |
734 | // to know where the data came from | |
735 | $question->export_process = true; | |
093414d2 | 736 | $question->import_process = true; |
5fd8f999 | 737 | |
d47af580 MG |
738 | $this->add_blank_combined_feedback($question); |
739 | ||
aca318e1 | 740 | return $question; |
741 | } | |
742 | ||
cacb8fa0 TH |
743 | /** |
744 | * Construct a reasonable default question name, based on the start of the question text. | |
745 | * @param string $questiontext the question text. | |
746 | * @param string $default default question name to use if the constructed one comes out blank. | |
747 | * @return string a reasonable question name. | |
748 | */ | |
749 | public function create_default_question_name($questiontext, $default) { | |
750 | $name = $this->clean_question_name(shorten_text($questiontext, 80)); | |
751 | if ($name) { | |
752 | return $name; | |
753 | } else { | |
754 | return $default; | |
755 | } | |
756 | } | |
757 | ||
758 | /** | |
759 | * Ensure that a question name does not contain anything nasty, and will fit in the DB field. | |
760 | * @param string $name the raw question name. | |
761 | * @return string a safe question name. | |
762 | */ | |
763 | public function clean_question_name($name) { | |
764 | $name = clean_param($name, PARAM_TEXT); // Matches what the question editing form does. | |
765 | $name = trim($name); | |
766 | $trimlength = 251; | |
2f1e464a | 767 | while (core_text::strlen($name) > 255 && $trimlength > 0) { |
cacb8fa0 TH |
768 | $name = shorten_text($name, $trimlength); |
769 | $trimlength -= 10; | |
770 | } | |
771 | return $name; | |
772 | } | |
773 | ||
7ace84e0 JMV |
774 | /** |
775 | * Add a blank combined feedback to a question object. | |
776 | * @param object question | |
777 | * @return object question | |
778 | */ | |
779 | protected function add_blank_combined_feedback($question) { | |
d47af580 MG |
780 | $question->correctfeedback = [ |
781 | 'text' => '', | |
782 | 'format' => $question->questiontextformat, | |
783 | 'files' => [] | |
784 | ]; | |
785 | $question->partiallycorrectfeedback = [ | |
786 | 'text' => '', | |
787 | 'format' => $question->questiontextformat, | |
788 | 'files' => [] | |
789 | ]; | |
790 | $question->incorrectfeedback = [ | |
791 | 'text' => '', | |
792 | 'format' => $question->questiontextformat, | |
793 | 'files' => [] | |
794 | ]; | |
7ace84e0 JMV |
795 | return $question; |
796 | } | |
797 | ||
f3701561 | 798 | /** |
271e6dec | 799 | * Given the data known to define a question in |
800 | * this format, this function converts it into a question | |
f3701561 | 801 | * object suitable for processing and insertion into Moodle. |
802 | * | |
803 | * If your format does not use blank lines to delimit questions | |
804 | * (e.g. an XML format) you must override 'readquestions' too | |
805 | * @param $lines mixed data that represents question | |
806 | * @return object question object | |
807 | */ | |
c7df5006 | 808 | protected function readquestion($lines) { |
762ee139 JMV |
809 | // We should never get there unless the qformat plugin is broken. |
810 | throw new coding_exception('Question format plugin is missing important code: readquestion.'); | |
aca318e1 | 811 | |
e0736817 | 812 | return null; |
aca318e1 | 813 | } |
814 | ||
f3701561 | 815 | /** |
816 | * Override if any post-processing is required | |
f7970e3c | 817 | * @return bool success |
f3701561 | 818 | */ |
7348402f | 819 | public function importpostprocess() { |
aca318e1 | 820 | return true; |
821 | } | |
822 | ||
e198992b TH |
823 | /******************* |
824 | * EXPORT FUNCTIONS | |
825 | *******************/ | |
aca318e1 | 826 | |
271e6dec | 827 | /** |
a41e3287 | 828 | * Provide export functionality for plugin questiontypes |
829 | * Do not override | |
830 | * @param name questiontype name | |
271e6dec | 831 | * @param question object data to export |
a41e3287 | 832 | * @param extra mixed any addition format specific data needed |
833 | * @return string the data to append to export or false if error (or unhandled) | |
834 | */ | |
c7df5006 | 835 | protected function try_exporting_using_qtypes($name, $question, $extra=null) { |
a41e3287 | 836 | // work out the name of format in use |
13bb604e | 837 | $formatname = substr(get_class($this), strlen('qformat_')); |
f4fe3968 | 838 | $methodname = "export_to_{$formatname}"; |
a41e3287 | 839 | |
d649fb02 TH |
840 | $qtype = question_bank::get_qtype($name, false); |
841 | if (method_exists($qtype, $methodname)) { | |
842 | return $qtype->$methodname($question, $this, $extra); | |
a41e3287 | 843 | } |
844 | return false; | |
845 | } | |
846 | ||
f3701561 | 847 | /** |
848 | * Do any pre-processing that may be required | |
f7970e3c | 849 | * @param bool success |
f3701561 | 850 | */ |
c7df5006 | 851 | public function exportpreprocess() { |
aca318e1 | 852 | return true; |
853 | } | |
854 | ||
f3701561 | 855 | /** |
856 | * Enable any processing to be done on the content | |
857 | * just prior to the file being saved | |
858 | * default is to do nothing | |
859 | * @param string output text | |
860 | * @param string processed output text | |
861 | */ | |
c7df5006 | 862 | protected function presave_process($content) { |
aca318e1 | 863 | return $content; |
864 | } | |
865 | ||
f3701561 | 866 | /** |
cc98914e AN |
867 | * Perform the export. |
868 | * For most types this should not need to be overrided. | |
869 | * | |
870 | * @param bool $checkcapabilities Whether to check capabilities when exporting the questions. | |
871 | * @return string The content of the export. | |
f3701561 | 872 | */ |
cc98914e | 873 | public function exportprocess($checkcapabilities = true) { |
d0a60444 JB |
874 | global $CFG, $DB; |
875 | ||
b65db96d TH |
876 | // Raise time and memory, as exporting can be quite intensive. |
877 | core_php_time_limit::raise(); | |
878 | raise_memory_limit(MEMORY_EXTRA); | |
879 | ||
d0a60444 JB |
880 | // Get the parents (from database) for this category. |
881 | $parents = []; | |
882 | if ($this->category) { | |
883 | $parents = question_categorylist_parents($this->category->id); | |
884 | } | |
aca318e1 | 885 | |
886 | // get the questions (from database) in this category | |
887 | // only get q's with no parents (no cloze subquestions specifically) | |
cde2709a | 888 | if ($this->category) { |
13bb604e | 889 | $questions = get_questions_category($this->category, true); |
2c44a3d3 | 890 | } else { |
891 | $questions = $this->questions; | |
892 | } | |
aca318e1 | 893 | |
aca318e1 | 894 | $count = 0; |
895 | ||
896 | // results are first written into string (and then to a file) | |
897 | // so create/initialize the string here | |
3a8cde29 | 898 | $expout = ''; |
271e6dec | 899 | |
f1abd39f | 900 | // track which category questions are in |
901 | // if it changes we will record the category change in the output | |
902 | // file if selected. 0 means that it will get printed before the 1st question | |
903 | $trackcategory = 0; | |
aca318e1 | 904 | |
d0a60444 JB |
905 | // Array of categories written to file. |
906 | $writtencategories = []; | |
907 | ||
e198992b | 908 | foreach ($questions as $question) { |
cde2709a | 909 | // used by file api |
e198992b TH |
910 | $contextid = $DB->get_field('question_categories', 'contextid', |
911 | array('id' => $question->category)); | |
cde2709a | 912 | $question->contextid = $contextid; |
271e6dec | 913 | |
a9b16aff | 914 | // do not export hidden questions |
915 | if (!empty($question->hidden)) { | |
916 | continue; | |
917 | } | |
918 | ||
919 | // do not export random questions | |
55190d7e | 920 | if ($question->qtype == 'random') { |
a9b16aff | 921 | continue; |
922 | } | |
271e6dec | 923 | |
f1abd39f | 924 | // check if we need to record category change |
925 | if ($this->cattofile) { | |
d0a60444 | 926 | $addnewcat = false; |
f1abd39f | 927 | if ($question->category != $trackcategory) { |
d0a60444 | 928 | $addnewcat = true; |
271e6dec | 929 | $trackcategory = $question->category; |
d0a60444 JB |
930 | } |
931 | $trackcategoryparents = question_categorylist_parents($trackcategory); | |
932 | // Check if we need to record empty parents categories. | |
933 | foreach ($trackcategoryparents as $trackcategoryparent) { | |
934 | // If parent wasn't written. | |
935 | if (!in_array($trackcategoryparent, $writtencategories)) { | |
936 | // If parent is empty. | |
937 | if (!count($DB->get_records('question', array('category' => $trackcategoryparent)))) { | |
938 | $categoryname = $this->get_category_path($trackcategoryparent, $this->contexttofile); | |
1dab8faa | 939 | $categoryinfo = $DB->get_record('question_categories', array('id' => $trackcategoryparent), |
e247068d | 940 | 'name, info, infoformat, idnumber', MUST_EXIST); |
1dab8faa JB |
941 | if ($categoryinfo->name != 'top') { |
942 | // Create 'dummy' question for parent category. | |
943 | $dummyquestion = $this->create_dummy_question_representing_category($categoryname, $categoryinfo); | |
944 | $expout .= $this->writequestion($dummyquestion) . "\n"; | |
945 | $writtencategories[] = $trackcategoryparent; | |
946 | } | |
d0a60444 JB |
947 | } |
948 | } | |
949 | } | |
950 | if ($addnewcat && !in_array($trackcategory, $writtencategories)) { | |
951 | $categoryname = $this->get_category_path($trackcategory, $this->contexttofile); | |
1dab8faa | 952 | $categoryinfo = $DB->get_record('question_categories', array('id' => $trackcategory), |
e247068d | 953 | 'info, infoformat, idnumber', MUST_EXIST); |
d0a60444 | 954 | // Create 'dummy' question for category. |
1dab8faa | 955 | $dummyquestion = $this->create_dummy_question_representing_category($categoryname, $categoryinfo); |
728d60a1 | 956 | $expout .= $this->writequestion($dummyquestion) . "\n"; |
d0a60444 | 957 | $writtencategories[] = $trackcategory; |
271e6dec | 958 | } |
959 | } | |
f1abd39f | 960 | |
5dd1cf33 | 961 | // Add the question to result. |
cc98914e | 962 | if (!$checkcapabilities || question_has_capability_on($question, 'view')) { |
5dd1cf33 JMV |
963 | $expquestion = $this->writequestion($question, $contextid); |
964 | // Don't add anything if witequestion returned nothing. | |
965 | // This will permit qformat plugins to exclude some questions. | |
966 | if ($expquestion !== null) { | |
967 | $expout .= $expquestion . "\n"; | |
968 | $count++; | |
969 | } | |
0647e82b | 970 | } |
a9b16aff | 971 | } |
aca318e1 | 972 | |
2c6d2c88 | 973 | // continue path for following error checks |
974 | $course = $this->course; | |
f4fe3968 | 975 | $continuepath = "{$CFG->wwwroot}/question/export.php?courseid={$course->id}"; |
2c6d2c88 | 976 | |
977 | // did we actually process anything | |
978 | if ($count==0) { | |
5e8a85aa | 979 | print_error('noquestions', 'question', $continuepath); |
2c6d2c88 | 980 | } |
981 | ||
aca318e1 | 982 | // final pre-process on exported data |
cde2709a DC |
983 | $expout = $this->presave_process($expout); |
984 | return $expout; | |
aca318e1 | 985 | } |
3f5633df | 986 | |
d0a60444 JB |
987 | /** |
988 | * Create 'dummy' question for category export. | |
989 | * @param string $categoryname the name of the category | |
1dab8faa | 990 | * @param object $categoryinfo description of the category |
d0a60444 JB |
991 | * @return stdClass 'dummy' question for category |
992 | */ | |
1dab8faa | 993 | protected function create_dummy_question_representing_category(string $categoryname, $categoryinfo) { |
d0a60444 JB |
994 | $dummyquestion = new stdClass(); |
995 | $dummyquestion->qtype = 'category'; | |
996 | $dummyquestion->category = $categoryname; | |
997 | $dummyquestion->id = 0; | |
998 | $dummyquestion->questiontextformat = ''; | |
999 | $dummyquestion->contextid = 0; | |
1dab8faa JB |
1000 | $dummyquestion->info = $categoryinfo->info; |
1001 | $dummyquestion->infoformat = $categoryinfo->infoformat; | |
e247068d | 1002 | $dummyquestion->idnumber = $categoryinfo->idnumber; |
d0a60444 JB |
1003 | $dummyquestion->name = 'Switch category to ' . $categoryname; |
1004 | return $dummyquestion; | |
1005 | } | |
1006 | ||
271e6dec | 1007 | /** |
1008 | * get the category as a path (e.g., tom/dick/harry) | |
1009 | * @param int id the id of the most nested catgory | |
271e6dec | 1010 | * @return string the path |
1011 | */ | |
c7df5006 | 1012 | protected function get_category_path($id, $includecontext = true) { |
f34488b2 | 1013 | global $DB; |
728d60a1 | 1014 | |
e198992b | 1015 | if (!$category = $DB->get_record('question_categories', array('id' => $id))) { |
1e7386c9 | 1016 | print_error('cannotfindcategory', 'error', '', $id); |
271e6dec | 1017 | } |
271e6dec | 1018 | $contextstring = $this->translator->context_to_string($category->contextid); |
728d60a1 TH |
1019 | |
1020 | $pathsections = array(); | |
271e6dec | 1021 | do { |
728d60a1 | 1022 | $pathsections[] = $category->name; |
271e6dec | 1023 | $id = $category->parent; |
13bb604e | 1024 | } while ($category = $DB->get_record('question_categories', array('id' => $id))); |
271e6dec | 1025 | |
13bb604e | 1026 | if ($includecontext) { |
728d60a1 | 1027 | $pathsections[] = '$' . $contextstring . '$'; |
271e6dec | 1028 | } |
728d60a1 TH |
1029 | |
1030 | $path = $this->assemble_category_path(array_reverse($pathsections)); | |
1031 | ||
271e6dec | 1032 | return $path; |
1033 | } | |
aca318e1 | 1034 | |
728d60a1 TH |
1035 | /** |
1036 | * Convert a list of category names, possibly preceeded by one of the | |
1037 | * context tokens like $course$, into a string representation of the | |
1038 | * category path. | |
1039 | * | |
1040 | * Names are separated by / delimiters. And /s in the name are replaced by //. | |
1041 | * | |
1042 | * To reverse the process and split the paths into names, use | |
1043 | * {@link split_category_path()}. | |
1044 | * | |
1045 | * @param array $names | |
1046 | * @return string | |
1047 | */ | |
1048 | protected function assemble_category_path($names) { | |
1049 | $escapednames = array(); | |
1050 | foreach ($names as $name) { | |
1051 | $escapedname = str_replace('/', '//', $name); | |
1052 | if (substr($escapedname, 0, 1) == '/') { | |
1053 | $escapedname = ' ' . $escapedname; | |
1054 | } | |
1055 | if (substr($escapedname, -1) == '/') { | |
1056 | $escapedname = $escapedname . ' '; | |
1057 | } | |
1058 | $escapednames[] = $escapedname; | |
1059 | } | |
1060 | return implode('/', $escapednames); | |
1061 | } | |
1062 | ||
1063 | /** | |
1064 | * Convert a string, as returned by {@link assemble_category_path()}, | |
1065 | * back into an array of category names. | |
1066 | * | |
071e68f9 | 1067 | * Each category name is cleaned by a call to clean_param(, PARAM_TEXT), |
aab03169 | 1068 | * which matches the cleaning in question/category_form.php. |
728d60a1 TH |
1069 | * |
1070 | * @param string $path | |
1071 | * @return array of category names. | |
1072 | */ | |
1073 | protected function split_category_path($path) { | |
1074 | $rawnames = preg_split('~(?<!/)/(?!/)~', $path); | |
1075 | $names = array(); | |
1076 | foreach ($rawnames as $rawname) { | |
071e68f9 | 1077 | $names[] = clean_param(trim(str_replace('//', '/', $rawname)), PARAM_TEXT); |
728d60a1 TH |
1078 | } |
1079 | return $names; | |
1080 | } | |
1081 | ||
f3701561 | 1082 | /** |
1083 | * Do an post-processing that may be required | |
f7970e3c | 1084 | * @return bool success |
f3701561 | 1085 | */ |
c7df5006 | 1086 | protected function exportpostprocess() { |
aca318e1 | 1087 | return true; |
1088 | } | |
1089 | ||
f3701561 | 1090 | /** |
1091 | * convert a single question object into text output in the given | |
1092 | * format. | |
1093 | * This must be overriden | |
1094 | * @param object question question object | |
1095 | * @return mixed question export text or null if not implemented | |
1096 | */ | |
c7df5006 | 1097 | protected function writequestion($question) { |
1e3d6fd8 | 1098 | // if not overidden, then this is an error. |
762ee139 | 1099 | throw new coding_exception('Question format plugin is missing important code: writequestion.'); |
e0736817 | 1100 | return null; |
aca318e1 | 1101 | } |
1102 | ||
f3701561 | 1103 | /** |
13bb604e TH |
1104 | * Convert the question text to plain text, so it can safely be displayed |
1105 | * during import to let the user see roughly what is going on. | |
f3701561 | 1106 | */ |
c7df5006 | 1107 | protected function format_question_text($question) { |
0b18d0c9 TH |
1108 | return s(question_utils::to_plain_text($question->questiontext, |
1109 | $question->questiontextformat)); | |
5b0dc681 | 1110 | } |
aca318e1 | 1111 | } |
17ab0e74 JMV |
1112 | |
1113 | class qformat_based_on_xml extends qformat_default { | |
1114 | ||
7ace84e0 JMV |
1115 | /** |
1116 | * A lot of imported files contain unwanted entities. | |
1117 | * This method tries to clean up all known problems. | |
1118 | * @param string str string to correct | |
1119 | * @return string the corrected string | |
1120 | */ | |
1121 | public function cleaninput($str) { | |
1122 | ||
1123 | $html_code_list = array( | |
1124 | "'" => "'", | |
1125 | "’" => "'", | |
1126 | "“" => "\"", | |
1127 | "”" => "\"", | |
1128 | "–" => "-", | |
1129 | "—" => "-", | |
1130 | ); | |
1131 | $str = strtr($str, $html_code_list); | |
2f1e464a PS |
1132 | // Use core_text entities_to_utf8 function to convert only numerical entities. |
1133 | $str = core_text::entities_to_utf8($str, false); | |
7ace84e0 JMV |
1134 | return $str; |
1135 | } | |
1136 | ||
17ab0e74 JMV |
1137 | /** |
1138 | * Return the array moodle is expecting | |
1139 | * for an HTML text. No processing is done on $text. | |
1140 | * qformat classes that want to process $text | |
1141 | * for instance to import external images files | |
1142 | * and recode urls in $text must overwrite this method. | |
1143 | * @param array $text some HTML text string | |
1144 | * @return array with keys text, format and files. | |
1145 | */ | |
1146 | public function text_field($text) { | |
1147 | return array( | |
1148 | 'text' => trim($text), | |
1149 | 'format' => FORMAT_HTML, | |
1150 | 'files' => array(), | |
1151 | ); | |
1152 | } | |
1153 | ||
1154 | /** | |
1155 | * Return the value of a node, given a path to the node | |
1156 | * if it doesn't exist return the default value. | |
1157 | * @param array xml data to read | |
1158 | * @param array path path to node expressed as array | |
1159 | * @param mixed default | |
1160 | * @param bool istext process as text | |
1161 | * @param string error if set value must exist, return false and issue message if not | |
1162 | * @return mixed value | |
1163 | */ | |
1164 | public function getpath($xml, $path, $default, $istext=false, $error='') { | |
1165 | foreach ($path as $index) { | |
1166 | if (!isset($xml[$index])) { | |
1167 | if (!empty($error)) { | |
1168 | $this->error($error); | |
1169 | return false; | |
1170 | } else { | |
1171 | return $default; | |
1172 | } | |
1173 | } | |
1174 | ||
1175 | $xml = $xml[$index]; | |
1176 | } | |
1177 | ||
1178 | if ($istext) { | |
1179 | if (!is_string($xml)) { | |
1180 | $this->error(get_string('invalidxml', 'qformat_xml')); | |
1181 | } | |
1182 | $xml = trim($xml); | |
1183 | } | |
1184 | ||
1185 | return $xml; | |
1186 | } | |
1187 | } |