Commit | Line | Data |
---|---|---|
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 | 11 | class 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> '; | |
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 | } |