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