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