MDL-27747 questions remove all references to the recently deprecated get_grade_options()
[moodle.git] / question / format.php
CommitLineData
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
27defined('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 36class 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;
271e6dec 54
cde2709a 55 protected $importcontext = null;
aca318e1 56
57// functions to indicate import/export functionality
58// override to return true if implemented
59
f7970e3c 60 /** @return bool whether this plugin provides import functionality. */
c7df5006 61 public function provide_import() {
46732124 62 return false;
aca318e1 63 }
64
f7970e3c 65 /** @return bool whether this plugin provides export functionality. */
c7df5006 66 public function provide_export() {
46732124
TH
67 return false;
68 }
69
70 /** The string mime-type of the files that this plugin reads or writes. */
c7df5006 71 public function mime_type() {
46732124
TH
72 return mimeinfo('type', $this->export_file_extension());
73 }
74
75 /**
76 * @return string the file extension (including .) that is normally used for
77 * files handled by this plugin.
78 */
c7df5006 79 public function export_file_extension() {
46732124 80 return '.txt';
aca318e1 81 }
82
08892d5b 83// Accessor methods
aca318e1 84
08892d5b 85 /**
86 * set the category
87 * @param object category the category object
88 */
c7df5006 89 public function setCategory($category) {
13bb604e 90 if (count($this->questions)) {
88bc20c3 91 debugging('You shouldn\'t call setCategory after setQuestions');
92 }
08892d5b 93 $this->category = $category;
94 }
aca318e1 95
2c44a3d3 96 /**
97 * Set the specific questions to export. Should not include questions with
98 * parents (sub questions of cloze question type).
99 * Only used for question export.
100 * @param array of question objects
101 */
c7df5006 102 public function setQuestions($questions) {
13bb604e 103 if ($this->category !== null) {
88bc20c3 104 debugging('You shouldn\'t call setQuestions after setCategory');
105 }
2c44a3d3 106 $this->questions = $questions;
107 }
108
08892d5b 109 /**
110 * set the course class variable
111 * @param course object Moodle course variable
112 */
c7df5006 113 public function setCourse($course) {
aca318e1 114 $this->course = $course;
08892d5b 115 }
13bb604e 116
271e6dec 117 /**
118 * set an array of contexts.
119 * @param array $contexts Moodle course variable
120 */
c7df5006 121 public function setContexts($contexts) {
271e6dec 122 $this->contexts = $contexts;
123 $this->translator = new context_to_string_translator($this->contexts);
124 }
aca318e1 125
08892d5b 126 /**
127 * set the filename
128 * @param string filename name of file to import/export
129 */
c7df5006 130 public function setFilename($filename) {
08892d5b 131 $this->filename = $filename;
132 }
88bc20c3 133
134 /**
73b7b195 135 * set the "real" filename
136 * (this is what the user typed, regardless of wha happened next)
137 * @param string realfilename name of file as typed by user
138 */
c7df5006 139 public function setRealfilename($realfilename) {
88bc20c3 140 $this->realfilename = $realfilename;
141 }
08892d5b 142
143 /**
144 * set matchgrades
145 * @param string matchgrades error or nearest for grades
146 */
c7df5006 147 public function setMatchgrades($matchgrades) {
08892d5b 148 $this->matchgrades = $matchgrades;
aca318e1 149 }
150
76f0a334 151 /**
08892d5b 152 * set catfromfile
153 * @param bool catfromfile allow categories embedded in import file
76f0a334 154 */
c7df5006 155 public function setCatfromfile($catfromfile) {
08892d5b 156 $this->catfromfile = $catfromfile;
157 }
271e6dec 158
159 /**
160 * set contextfromfile
161 * @param bool $contextfromfile allow contexts embedded in import file
162 */
c7df5006 163 public function setContextfromfile($contextfromfile) {
271e6dec 164 $this->contextfromfile = $contextfromfile;
165 }
166
f1abd39f 167 /**
168 * set cattofile
169 * @param bool cattofile exports categories within export file
170 */
c7df5006 171 public function setCattofile($cattofile) {
f1abd39f 172 $this->cattofile = $cattofile;
271e6dec 173 }
13bb604e 174
271e6dec 175 /**
176 * set contexttofile
177 * @param bool cattofile exports categories within export file
178 */
c7df5006 179 public function setContexttofile($contexttofile) {
271e6dec 180 $this->contexttofile = $contexttofile;
181 }
aca318e1 182
f3701561 183 /**
184 * set stoponerror
185 * @param bool stoponerror stops database write if any errors reported
186 */
c7df5006 187 public function setStoponerror($stoponerror) {
f3701561 188 $this->stoponerror = $stoponerror;
189 }
190
1b8b535d 191 /**
f7970e3c 192 * @param bool $canaccess Whether the current use can access the backup data folder. Determines
1b8b535d 193 * where export files are saved.
194 */
c7df5006 195 public function set_can_access_backupdata($canaccess) {
1b8b535d 196 $this->canaccessbackupdata = $canaccess;
197 }
198
f3701561 199/***********************
200 * IMPORTING FUNCTIONS
201 ***********************/
202
203 /**
204 * Handle parsing error
205 */
c7df5006 206 protected function error($message, $text='', $questionname='') {
5e8a85aa 207 $importerrorquestion = get_string('importerrorquestion', 'question');
cdeabc06 208
f3701561 209 echo "<div class=\"importerror\">\n";
cdeabc06 210 echo "<strong>$importerrorquestion $questionname</strong>";
f3701561 211 if (!empty($text)) {
212 $text = s($text);
213 echo "<blockquote>$text</blockquote>\n";
214 }
215 echo "<strong>$message</strong>\n";
216 echo "</div>";
217
218 $this->importerrors++;
219 }
08892d5b 220
271e6dec 221 /**
a41e3287 222 * Import for questiontype plugins
223 * Do not override.
224 * @param data mixed The segment of data containing the question
225 * @param question object processed (so far) by standard import code if appropriate
226 * @param extra mixed any additional format specific data that may be passed by the format
88bc20c3 227 * @param qtypehint hint about a question type from format
a41e3287 228 * @return object question object suitable for save_options() or false if cannot handle
229 */
c7df5006 230 public function try_importing_using_qtypes($data, $question = null, $extra = null,
49e2bba7 231 $qtypehint = '') {
a41e3287 232
233 // work out what format we are using
88bc20c3 234 $formatname = substr(get_class($this), strlen('qformat_'));
a41e3287 235 $methodname = "import_from_$formatname";
236
88bc20c3 237 //first try importing using a hint from format
238 if (!empty($qtypehint)) {
49e2bba7 239 $qtype = question_bank::get_qtype($qtypehint, false);
88bc20c3 240 if (is_object($qtype) && method_exists($qtype, $methodname)) {
241 $question = $qtype->$methodname($data, $question, $this, $extra);
242 if ($question) {
243 return $question;
244 }
245 }
246 }
247
a41e3287 248 // loop through installed questiontypes checking for
249 // function to handle this question
49e2bba7 250 foreach (question_bank::get_all_qtypes() as $qtype) {
13bb604e
TH
251 if (method_exists($qtype, $methodname)) {
252 if ($question = $qtype->$methodname($data, $question, $this, $extra)) {
a41e3287 253 return $question;
254 }
255 }
271e6dec 256 }
257 return false;
a41e3287 258 }
259
08892d5b 260 /**
261 * Perform any required pre-processing
f7970e3c 262 * @return bool success
08892d5b 263 */
7348402f 264 public function importpreprocess() {
08892d5b 265 return true;
266 }
267
268 /**
269 * Process the file
270 * This method should not normally be overidden
49e2bba7 271 * @param object $category
f7970e3c 272 * @return bool success
08892d5b 273 */
7348402f 274 public function importprocess($category) {
d649fb02 275 global $USER, $CFG, $DB, $OUTPUT;
cde2709a
DC
276
277 $context = $category->context;
278 $this->importcontext = $context;
f3701561 279
9dd46039
TH
280 // reset the timer in case file upload was slow
281 set_time_limit(0);
67c12527 282
9dd46039 283 // STAGE 1: Parse the file
5e8a85aa 284 echo $OUTPUT->notification(get_string('parsingquestions', 'question'), 'notifysuccess');
271e6dec 285
08892d5b 286 if (! $lines = $this->readdata($this->filename)) {
5e8a85aa 287 echo $OUTPUT->notification(get_string('cannotread', 'question'));
aca318e1 288 return false;
289 }
290
13bb604e 291 if (!$questions = $this->readquestions($lines, $context)) { // Extract all the questions
5e8a85aa 292 echo $OUTPUT->notification(get_string('noquestionsinfile', 'question'));
aca318e1 293 return false;
294 }
295
f3701561 296 // STAGE 2: Write data to database
5e8a85aa 297 echo $OUTPUT->notification(get_string('importingquestions', 'question',
f7c1dfaf 298 $this->count_questions($questions)), 'notifysuccess');
aca318e1 299
f3701561 300 // check for errors before we continue
301 if ($this->stoponerror and ($this->importerrors>0)) {
5e8a85aa 302 echo $OUTPUT->notification(get_string('importparseerror', 'question'));
10b4a508 303 return true;
f3701561 304 }
305
76f0a334 306 // get list of valid answer grades
92111e8d 307 $gradeoptionsfull = question_bank::fraction_options_full();
76f0a334 308
c1828e0b 309 // check answer grades are valid
310 // (now need to do this here because of 'stop on error': MDL-10689)
311 $gradeerrors = 0;
312 $goodquestions = array();
313 foreach ($questions as $question) {
314 if (!empty($question->fraction) and (is_array($question->fraction))) {
315 $fractions = $question->fraction;
316 $answersvalid = true; // in case they are!
317 foreach ($fractions as $key => $fraction) {
318 $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades);
319 if ($newfraction===false) {
320 $answersvalid = false;
321 }
322 else {
323 $fractions[$key] = $newfraction;
324 }
325 }
326 if (!$answersvalid) {
5e8a85aa 327 echo $OUTPUT->notification(get_string('invalidgrade', 'question'));
c1828e0b 328 ++$gradeerrors;
329 continue;
330 }
331 else {
332 $question->fraction = $fractions;
333 }
334 }
335 $goodquestions[] = $question;
336 }
337 $questions = $goodquestions;
338
339 // check for errors before we continue
340 if ($this->stoponerror and ($gradeerrors>0)) {
341 return false;
342 }
343
344 // count number of questions processed
aca318e1 345 $count = 0;
346
347 foreach ($questions as $question) { // Process and store each question
08892d5b 348
271e6dec 349 // reset the php timeout
49e2bba7 350 set_time_limit(0);
67c12527 351
08892d5b 352 // check for category modifiers
9dd46039 353 if ($question->qtype == 'category') {
08892d5b 354 if ($this->catfromfile) {
355 // find/create category object
40e71443 356 $catpath = $question->category;
728d60a1 357 $newcategory = $this->create_category_path($catpath);
08892d5b 358 if (!empty($newcategory)) {
359 $this->category = $newcategory;
360 }
361 }
271e6dec 362 continue;
08892d5b 363 }
cde2709a 364 $question->context = $context;
08892d5b 365
aca318e1 366 $count++;
367
5b0dc681 368 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
aca318e1 369
9dd46039 370 $question->category = $this->category->id;
aca318e1 371 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
aca318e1 372
271e6dec 373 $question->createdby = $USER->id;
374 $question->timecreated = time();
375
cde2709a
DC
376 $question->id = $DB->insert_record('question', $question);
377 if (isset($question->questiontextfiles)) {
378 foreach ($question->questiontextfiles as $file) {
d649fb02
TH
379 question_bank::get_qtype($question->qtype)->import_file(
380 $context, 'question', 'questiontext', $question->id, $file);
cde2709a
DC
381 }
382 }
383 if (isset($question->generalfeedbackfiles)) {
384 foreach ($question->generalfeedbackfiles as $file) {
d649fb02
TH
385 question_bank::get_qtype($question->qtype)->import_file(
386 $context, 'question', 'generalfeedback', $question->id, $file);
cde2709a
DC
387 }
388 }
aca318e1 389
390 $this->questionids[] = $question->id;
391
392 // Now to save all the answers and type-specific options
393
d649fb02 394 $result = question_bank::get_qtype($question->qtype)->save_question_options($question);
aca318e1 395
4f290077
TH
396 if (!empty($CFG->usetags) && isset($question->tags)) {
397 require_once($CFG->dirroot . '/tag/lib.php');
398 tag_set('question', $question->id, $question->tags);
399 }
400
aca318e1 401 if (!empty($result->error)) {
fef8f84e 402 echo $OUTPUT->notification($result->error);
aca318e1 403 return false;
404 }
405
406 if (!empty($result->notice)) {
fef8f84e 407 echo $OUTPUT->notification($result->notice);
aca318e1 408 return true;
409 }
cbe20043 410
411 // Give the question a unique version stamp determined by question_hash()
e5d7d1dc 412 $DB->set_field('question', 'version', question_hash($question), array('id'=>$question->id));
aca318e1 413 }
414 return true;
415 }
13bb604e 416
ce2df288 417 /**
418 * Count all non-category questions in the questions array.
f34488b2 419 *
ce2df288 420 * @param array questions An array of question objects.
421 * @return int The count.
f34488b2 422 *
ce2df288 423 */
c7df5006 424 protected function count_questions($questions) {
ce2df288 425 $count = 0;
426 if (!is_array($questions)) {
427 return $count;
428 }
429 foreach ($questions as $question) {
430 if (!is_object($question) || !isset($question->qtype) || ($question->qtype == 'category')) {
431 continue;
432 }
433 $count++;
434 }
435 return $count;
436 }
437
271e6dec 438 /**
439 * find and/or create the category described by a delimited list
440 * e.g. $course$/tom/dick/harry or tom/dick/harry
441 *
442 * removes any context string no matter whether $getcontext is set
443 * but if $getcontext is set then ignore the context and use selected category context.
444 *
445 * @param string catpath delimited category path
271e6dec 446 * @param int courseid course to search for categories
447 * @return mixed category object or null if fails
448 */
c7df5006 449 protected function create_category_path($catpath) {
f34488b2 450 global $DB;
728d60a1 451 $catnames = $this->split_category_path($catpath);
271e6dec 452 $parent = 0;
453 $category = null;
88bc20c3 454
5ca9e32d 455 // check for context id in path, it might not be there in pre 1.9 exports
456 $matchcount = preg_match('/^\$([a-z]+)\$$/', $catnames[0], $matches);
49e2bba7 457 if ($matchcount == 1) {
271e6dec 458 $contextid = $this->translator->string_to_context($matches[1]);
459 array_shift($catnames);
460 } else {
728d60a1 461 $contextid = false;
271e6dec 462 }
728d60a1
TH
463
464 if ($this->contextfromfile && $contextid !== false) {
271e6dec 465 $context = get_context_instance_by_id($contextid);
466 require_capability('moodle/question:add', $context);
467 } else {
468 $context = get_context_instance_by_id($this->category->contextid);
469 }
728d60a1
TH
470
471 // Now create any categories that need to be created.
271e6dec 472 foreach ($catnames as $catname) {
13bb604e 473 if ($category = $DB->get_record('question_categories', array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) {
271e6dec 474 $parent = $category->id;
475 } else {
476 require_capability('moodle/question:managecategory', $context);
477 // create the new category
7f389342 478 $category = new stdClass();
271e6dec 479 $category->contextid = $context->id;
480 $category->name = $catname;
481 $category->info = '';
482 $category->parent = $parent;
483 $category->sortorder = 999;
484 $category->stamp = make_unique_id_code();
bf8e93d7 485 $id = $DB->insert_record('question_categories', $category);
271e6dec 486 $category->id = $id;
487 $parent = $id;
488 }
489 }
490 return $category;
491 }
3f5633df 492
f3701561 493 /**
494 * Return complete file within an array, one item per line
495 * @param string filename name of file
496 * @return mixed contents array or false on failure
497 */
c7df5006 498 protected function readdata($filename) {
aca318e1 499 if (is_readable($filename)) {
500 $filearray = file($filename);
501
502 /// Check for Macintosh OS line returns (ie file on one line), and fix
6dbcacee 503 if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) {
aca318e1 504 return explode("\r", $filearray[0]);
505 } else {
506 return $filearray;
507 }
508 }
509 return false;
510 }
511
f3701561 512 /**
271e6dec 513 * Parses an array of lines into an array of questions,
514 * where each item is a question object as defined by
515 * readquestion(). Questions are defined as anything
f3701561 516 * between blank lines.
517 *
518 * If your format does not use blank lines as a delimiter
519 * then you will need to override this method. Even then
520 * try to use readquestion for each question
521 * @param array lines array of lines from readdata
cde2709a 522 * @param object $context
f3701561 523 * @return array array of question objects
524 */
c7df5006 525 protected function readquestions($lines, $context) {
271e6dec 526
aca318e1 527 $questions = array();
528 $currentquestion = array();
529
530 foreach ($lines as $line) {
531 $line = trim($line);
532 if (empty($line)) {
533 if (!empty($currentquestion)) {
534 if ($question = $this->readquestion($currentquestion)) {
535 $questions[] = $question;
536 }
537 $currentquestion = array();
538 }
539 } else {
540 $currentquestion[] = $line;
541 }
542 }
543
544 if (!empty($currentquestion)) { // There may be a final question
cde2709a 545 if ($question = $this->readquestion($currentquestion, $context)) {
aca318e1 546 $questions[] = $question;
547 }
548 }
549
550 return $questions;
551 }
552
f3701561 553 /**
554 * return an "empty" question
555 * Somewhere to specify question parameters that are not handled
556 * by import but are required db fields.
557 * This should not be overridden.
558 * @return object default question
271e6dec 559 */
c7df5006 560 protected function defaultquestion() {
b0679efa 561 global $CFG;
0cd46577 562 static $defaultshuffleanswers = null;
563 if (is_null($defaultshuffleanswers)) {
564 $defaultshuffleanswers = get_config('quiz', 'shuffleanswers');
565 }
271e6dec 566
aca318e1 567 $question = new stdClass();
0cd46577 568 $question->shuffleanswers = $defaultshuffleanswers;
49e2bba7 569 $question->defaultmark = 1;
aca318e1 570 $question->image = "";
571 $question->usecase = 0;
572 $question->multiplier = array();
172f6d95 573 $question->generalfeedback = '';
08892d5b 574 $question->correctfeedback = '';
575 $question->partiallycorrectfeedback = '';
576 $question->incorrectfeedback = '';
5931ea94 577 $question->answernumbering = 'abc';
49e2bba7 578 $question->penalty = 0.3333333;
3f5633df 579 $question->length = 1;
aca318e1 580
5fd8f999 581 // this option in case the questiontypes class wants
582 // to know where the data came from
583 $question->export_process = true;
093414d2 584 $question->import_process = true;
5fd8f999 585
aca318e1 586 return $question;
587 }
588
f3701561 589 /**
271e6dec 590 * Given the data known to define a question in
591 * this format, this function converts it into a question
f3701561 592 * object suitable for processing and insertion into Moodle.
593 *
594 * If your format does not use blank lines to delimit questions
595 * (e.g. an XML format) you must override 'readquestions' too
596 * @param $lines mixed data that represents question
597 * @return object question object
598 */
c7df5006 599 protected function readquestion($lines) {
aca318e1 600
5e8a85aa 601 $formatnotimplemented = get_string('formatnotimplemented', 'question');
1e3d6fd8 602 echo "<p>$formatnotimplemented</p>";
aca318e1 603
e0736817 604 return null;
aca318e1 605 }
606
f3701561 607 /**
608 * Override if any post-processing is required
f7970e3c 609 * @return bool success
f3701561 610 */
7348402f 611 public function importpostprocess() {
aca318e1 612 return true;
613 }
614
3f5633df 615
f3701561 616/*******************
617 * EXPORT FUNCTIONS
618 *******************/
aca318e1 619
271e6dec 620 /**
a41e3287 621 * Provide export functionality for plugin questiontypes
622 * Do not override
623 * @param name questiontype name
271e6dec 624 * @param question object data to export
a41e3287 625 * @param extra mixed any addition format specific data needed
626 * @return string the data to append to export or false if error (or unhandled)
627 */
c7df5006 628 protected function try_exporting_using_qtypes($name, $question, $extra=null) {
a41e3287 629 // work out the name of format in use
13bb604e 630 $formatname = substr(get_class($this), strlen('qformat_'));
a41e3287 631 $methodname = "export_to_$formatname";
632
d649fb02
TH
633 $qtype = question_bank::get_qtype($name, false);
634 if (method_exists($qtype, $methodname)) {
635 return $qtype->$methodname($question, $this, $extra);
a41e3287 636 }
637 return false;
638 }
639
f3701561 640 /**
641 * Do any pre-processing that may be required
f7970e3c 642 * @param bool success
f3701561 643 */
c7df5006 644 public function exportpreprocess() {
aca318e1 645 return true;
646 }
647
f3701561 648 /**
649 * Enable any processing to be done on the content
650 * just prior to the file being saved
651 * default is to do nothing
652 * @param string output text
653 * @param string processed output text
654 */
c7df5006 655 protected function presave_process($content) {
aca318e1 656 return $content;
657 }
658
f3701561 659 /**
660 * Do the export
661 * For most types this should not need to be overrided
cde2709a 662 * @return stored_file
f3701561 663 */
c7df5006 664 public function exportprocess() {
cde2709a 665 global $CFG, $OUTPUT, $DB, $USER;
aca318e1 666
667 // get the questions (from database) in this category
668 // only get q's with no parents (no cloze subquestions specifically)
cde2709a 669 if ($this->category) {
13bb604e 670 $questions = get_questions_category($this->category, true);
2c44a3d3 671 } else {
672 $questions = $this->questions;
673 }
aca318e1 674
aca318e1 675 $count = 0;
676
677 // results are first written into string (and then to a file)
678 // so create/initialize the string here
679 $expout = "";
271e6dec 680
f1abd39f 681 // track which category questions are in
682 // if it changes we will record the category change in the output
683 // file if selected. 0 means that it will get printed before the 1st question
684 $trackcategory = 0;
aca318e1 685
686 // iterate through questions
687 foreach($questions as $question) {
cde2709a
DC
688 // used by file api
689 $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$question->category));
690 $question->contextid = $contextid;
271e6dec 691
a9b16aff 692 // do not export hidden questions
693 if (!empty($question->hidden)) {
694 continue;
695 }
696
697 // do not export random questions
698 if ($question->qtype==RANDOM) {
699 continue;
700 }
271e6dec 701
f1abd39f 702 // check if we need to record category change
703 if ($this->cattofile) {
704 if ($question->category != $trackcategory) {
271e6dec 705 $trackcategory = $question->category;
728d60a1 706 $categoryname = $this->get_category_path($trackcategory, $this->contexttofile);
271e6dec 707
f1abd39f 708 // create 'dummy' question for category export
7f389342 709 $dummyquestion = new stdClass();
f1abd39f 710 $dummyquestion->qtype = 'category';
711 $dummyquestion->category = $categoryname;
728d60a1 712 $dummyquestion->name = 'Switch category to ' . $categoryname;
f1abd39f 713 $dummyquestion->id = 0;
714 $dummyquestion->questiontextformat = '';
cde2709a 715 $dummyquestion->contextid = 0;
728d60a1 716 $expout .= $this->writequestion($dummyquestion) . "\n";
271e6dec 717 }
718 }
f1abd39f 719
720 // export the question displaying message
721 $count++;
cde2709a 722
cde2709a 723 if (question_has_capability_on($question, 'view', $question->category)) {
cde2709a 724 $expout .= $this->writequestion($question, $contextid) . "\n";
0647e82b 725 }
a9b16aff 726 }
aca318e1 727
2c6d2c88 728 // continue path for following error checks
729 $course = $this->course;
2c44a3d3 730 $continuepath = "$CFG->wwwroot/question/export.php?courseid=$course->id";
2c6d2c88 731
732 // did we actually process anything
733 if ($count==0) {
5e8a85aa 734 print_error('noquestions', 'question', $continuepath);
2c6d2c88 735 }
736
aca318e1 737 // final pre-process on exported data
cde2709a
DC
738 $expout = $this->presave_process($expout);
739 return $expout;
aca318e1 740 }
3f5633df 741
271e6dec 742 /**
743 * get the category as a path (e.g., tom/dick/harry)
744 * @param int id the id of the most nested catgory
271e6dec 745 * @return string the path
746 */
c7df5006 747 protected function get_category_path($id, $includecontext = true) {
f34488b2 748 global $DB;
728d60a1
TH
749
750 if (!$category = $DB->get_record('question_categories',array('id' =>$id))) {
1e7386c9 751 print_error('cannotfindcategory', 'error', '', $id);
271e6dec 752 }
271e6dec 753 $contextstring = $this->translator->context_to_string($category->contextid);
728d60a1
TH
754
755 $pathsections = array();
271e6dec 756 do {
728d60a1 757 $pathsections[] = $category->name;
271e6dec 758 $id = $category->parent;
13bb604e 759 } while ($category = $DB->get_record('question_categories', array('id' => $id)));
271e6dec 760
13bb604e 761 if ($includecontext) {
728d60a1 762 $pathsections[] = '$' . $contextstring . '$';
271e6dec 763 }
728d60a1
TH
764
765 $path = $this->assemble_category_path(array_reverse($pathsections));
766
271e6dec 767 return $path;
768 }
aca318e1 769
728d60a1
TH
770 /**
771 * Convert a list of category names, possibly preceeded by one of the
772 * context tokens like $course$, into a string representation of the
773 * category path.
774 *
775 * Names are separated by / delimiters. And /s in the name are replaced by //.
776 *
777 * To reverse the process and split the paths into names, use
778 * {@link split_category_path()}.
779 *
780 * @param array $names
781 * @return string
782 */
783 protected function assemble_category_path($names) {
784 $escapednames = array();
785 foreach ($names as $name) {
786 $escapedname = str_replace('/', '//', $name);
787 if (substr($escapedname, 0, 1) == '/') {
788 $escapedname = ' ' . $escapedname;
789 }
790 if (substr($escapedname, -1) == '/') {
791 $escapedname = $escapedname . ' ';
792 }
793 $escapednames[] = $escapedname;
794 }
795 return implode('/', $escapednames);
796 }
797
798 /**
799 * Convert a string, as returned by {@link assemble_category_path()},
800 * back into an array of category names.
801 *
802 * Each category name is cleaned by a call to clean_param(, PARAM_MULTILANG),
aab03169 803 * which matches the cleaning in question/category_form.php.
728d60a1
TH
804 *
805 * @param string $path
806 * @return array of category names.
807 */
808 protected function split_category_path($path) {
809 $rawnames = preg_split('~(?<!/)/(?!/)~', $path);
810 $names = array();
811 foreach ($rawnames as $rawname) {
812 $names[] = clean_param(trim(str_replace('//', '/', $rawname)), PARAM_MULTILANG);
813 }
814 return $names;
815 }
816
f3701561 817 /**
818 * Do an post-processing that may be required
f7970e3c 819 * @return bool success
f3701561 820 */
c7df5006 821 protected function exportpostprocess() {
aca318e1 822 return true;
823 }
824
f3701561 825 /**
826 * convert a single question object into text output in the given
827 * format.
828 * This must be overriden
829 * @param object question question object
830 * @return mixed question export text or null if not implemented
831 */
c7df5006 832 protected function writequestion($question) {
1e3d6fd8 833 // if not overidden, then this is an error.
5e8a85aa 834 $formatnotimplemented = get_string('formatnotimplemented', 'question');
1e3d6fd8 835 echo "<p>$formatnotimplemented</p>";
e0736817 836 return null;
aca318e1 837 }
838
f3701561 839 /**
13bb604e
TH
840 * Convert the question text to plain text, so it can safely be displayed
841 * during import to let the user see roughly what is going on.
f3701561 842 */
c7df5006 843 protected function format_question_text($question) {
fe6ce234 844 global $DB;
0ff4bd08 845 $formatoptions = new stdClass();
5b0dc681 846 $formatoptions->noclean = true;
22cebed5 847 return html_to_text(format_text($question->questiontext,
c73c9836 848 $question->questiontextformat, $formatoptions), 0, false);
5b0dc681 849 }
cde2709a
DC
850
851 /**
852 * convert files into text output in the given format.
853 * @param array
854 * @param string encoding method
855 * @return string $string
856 */
c7df5006 857 protected function writefiles($files, $encoding='base64') {
cde2709a
DC
858 if (empty($files)) {
859 return '';
860 }
861 $string = '';
862 foreach ($files as $file) {
863 if ($file->is_directory()) {
864 continue;
865 }
06f1bd03 866 $string .= '<file name="' . $file->get_filename() . '" encoding="' . $encoding . '">';
cde2709a 867 $string .= base64_encode($file->get_content());
cde2709a
DC
868 $string .= '</file>';
869 }
870 return $string;
871 }
aca318e1 872}