aca318e1 |
1 | <?php // $Id$ |
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; |
15 | var $course = NULL; |
08892d5b |
16 | var $filename = ''; |
17 | var $matchgrades = 'error'; |
18 | var $catfromfile = 0; |
f1abd39f |
19 | var $cattofile = 0; |
aca318e1 |
20 | var $questionids = array(); |
f3701561 |
21 | var $importerrors = 0; |
22 | var $stoponerror = true; |
aca318e1 |
23 | |
24 | // functions to indicate import/export functionality |
25 | // override to return true if implemented |
26 | |
27 | function provide_import() { |
28 | return false; |
29 | } |
30 | |
31 | function provide_export() { |
32 | return false; |
33 | } |
34 | |
08892d5b |
35 | // Accessor methods |
aca318e1 |
36 | |
08892d5b |
37 | /** |
38 | * set the category |
39 | * @param object category the category object |
40 | */ |
41 | function setCategory( $category ) { |
42 | $this->category = $category; |
43 | } |
aca318e1 |
44 | |
08892d5b |
45 | /** |
46 | * set the course class variable |
47 | * @param course object Moodle course variable |
48 | */ |
49 | function setCourse( $course ) { |
aca318e1 |
50 | $this->course = $course; |
08892d5b |
51 | } |
aca318e1 |
52 | |
08892d5b |
53 | /** |
54 | * set the filename |
55 | * @param string filename name of file to import/export |
56 | */ |
57 | function setFilename( $filename ) { |
58 | $this->filename = $filename; |
59 | } |
60 | |
61 | /** |
62 | * set matchgrades |
63 | * @param string matchgrades error or nearest for grades |
64 | */ |
65 | function setMatchgrades( $matchgrades ) { |
66 | $this->matchgrades = $matchgrades; |
aca318e1 |
67 | } |
68 | |
76f0a334 |
69 | /** |
08892d5b |
70 | * set catfromfile |
71 | * @param bool catfromfile allow categories embedded in import file |
76f0a334 |
72 | */ |
08892d5b |
73 | function setCatfromfile( $catfromfile ) { |
74 | $this->catfromfile = $catfromfile; |
75 | } |
f1abd39f |
76 | |
77 | /** |
78 | * set cattofile |
79 | * @param bool cattofile exports categories within export file |
80 | */ |
81 | function setCattofile( $cattofile ) { |
82 | $this->cattofile = $cattofile; |
83 | } |
aca318e1 |
84 | |
f3701561 |
85 | /** |
86 | * set stoponerror |
87 | * @param bool stoponerror stops database write if any errors reported |
88 | */ |
89 | function setStoponerror( $stoponerror ) { |
90 | $this->stoponerror = $stoponerror; |
91 | } |
92 | |
93 | /*********************** |
94 | * IMPORTING FUNCTIONS |
95 | ***********************/ |
96 | |
97 | /** |
98 | * Handle parsing error |
99 | */ |
100 | function error( $message, $text='', $questionname='' ) { |
cdeabc06 |
101 | $importerrorquestion = get_string('importerrorquestion','quiz'); |
102 | |
f3701561 |
103 | echo "<div class=\"importerror\">\n"; |
cdeabc06 |
104 | echo "<strong>$importerrorquestion $questionname</strong>"; |
f3701561 |
105 | if (!empty($text)) { |
106 | $text = s($text); |
107 | echo "<blockquote>$text</blockquote>\n"; |
108 | } |
109 | echo "<strong>$message</strong>\n"; |
110 | echo "</div>"; |
111 | |
112 | $this->importerrors++; |
113 | } |
08892d5b |
114 | |
a41e3287 |
115 | /** |
116 | * Import for questiontype plugins |
117 | * Do not override. |
118 | * @param data mixed The segment of data containing the question |
119 | * @param question object processed (so far) by standard import code if appropriate |
120 | * @param extra mixed any additional format specific data that may be passed by the format |
121 | * @return object question object suitable for save_options() or false if cannot handle |
122 | */ |
123 | function try_importing_using_qtypes( $data, $question=null, $extra=null ) { |
124 | global $QTYPES; |
125 | |
126 | // work out what format we are using |
127 | $formatname = substr( get_class( $this ), strlen('qformat_')); |
128 | $methodname = "import_from_$formatname"; |
129 | |
130 | // loop through installed questiontypes checking for |
131 | // function to handle this question |
132 | foreach ($QTYPES as $qtype) { |
133 | if (method_exists( $qtype, $methodname)) { |
134 | if ($question = $qtype->$methodname( $data, $question, $this, $extra )) { |
135 | return $question; |
136 | } |
137 | } |
138 | } |
139 | return false; |
140 | } |
141 | |
08892d5b |
142 | /** |
143 | * Perform any required pre-processing |
f3701561 |
144 | * @return boolean success |
08892d5b |
145 | */ |
146 | function importpreprocess() { |
147 | return true; |
148 | } |
149 | |
150 | /** |
151 | * Process the file |
152 | * This method should not normally be overidden |
f3701561 |
153 | * @return boolean success |
08892d5b |
154 | */ |
155 | function importprocess() { |
f3701561 |
156 | |
67c12527 |
157 | // reset the timer in case file upload was slow |
158 | @set_time_limit(); |
159 | |
f3701561 |
160 | // STAGE 1: Parse the file |
161 | notify( get_string('parsingquestions','quiz') ); |
162 | |
08892d5b |
163 | if (! $lines = $this->readdata($this->filename)) { |
1e3d6fd8 |
164 | notify( get_string('cannotread','quiz') ); |
aca318e1 |
165 | return false; |
166 | } |
167 | |
168 | if (! $questions = $this->readquestions($lines)) { // Extract all the questions |
1e3d6fd8 |
169 | notify( get_string('noquestionsinfile','quiz') ); |
aca318e1 |
170 | return false; |
171 | } |
172 | |
f3701561 |
173 | // STAGE 2: Write data to database |
1e3d6fd8 |
174 | notify( get_string('importingquestions','quiz',count($questions)) ); |
aca318e1 |
175 | |
f3701561 |
176 | // check for errors before we continue |
177 | if ($this->stoponerror and ($this->importerrors>0)) { |
178 | return false; |
179 | } |
180 | |
76f0a334 |
181 | // get list of valid answer grades |
182 | $grades = get_grade_options(); |
183 | $gradeoptionsfull = $grades->gradeoptionsfull; |
184 | |
aca318e1 |
185 | $count = 0; |
186 | |
187 | foreach ($questions as $question) { // Process and store each question |
08892d5b |
188 | |
67c12527 |
189 | // reset the php timeout |
190 | @set_time_limit(); |
191 | |
08892d5b |
192 | // check for category modifiers |
193 | if ($question->qtype=='category') { |
194 | if ($this->catfromfile) { |
195 | // find/create category object |
196 | $catpath = $question->category; |
197 | $newcategory = create_category_path( $catpath, '/', $this->course->id ); |
198 | if (!empty($newcategory)) { |
199 | $this->category = $newcategory; |
200 | } |
201 | } |
202 | continue; |
203 | } |
204 | |
aca318e1 |
205 | $count++; |
206 | |
5b0dc681 |
207 | echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>"; |
aca318e1 |
208 | |
76f0a334 |
209 | // check for answer grades validity (must match fixed list of grades) |
08892d5b |
210 | if (!empty($question->fraction) and (is_array($question->fraction))) { |
656ee8c6 |
211 | $fractions = $question->fraction; |
2d52056f |
212 | $answersvalid = true; // in case they are! |
213 | foreach ($fractions as $key => $fraction) { |
08892d5b |
214 | $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades); |
2d52056f |
215 | if ($newfraction===false) { |
216 | $answersvalid = false; |
217 | } |
218 | else { |
219 | $fractions[$key] = $newfraction; |
220 | } |
221 | } |
222 | if (!$answersvalid) { |
eedb2494 |
223 | notify(get_string('matcherror', 'quiz')); |
2d52056f |
224 | continue; |
76f0a334 |
225 | } |
226 | else { |
2d52056f |
227 | $question->fraction = $fractions; |
76f0a334 |
228 | } |
229 | } |
76f0a334 |
230 | |
aca318e1 |
231 | $question->category = $this->category->id; |
232 | $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) |
aca318e1 |
233 | |
062f1125 |
234 | if (!$question->id = insert_record("question", $question)) { |
1e3d6fd8 |
235 | error( get_string('cannotinsert','quiz') ); |
aca318e1 |
236 | } |
237 | |
238 | $this->questionids[] = $question->id; |
239 | |
240 | // Now to save all the answers and type-specific options |
241 | |
242 | global $QTYPES; |
243 | $result = $QTYPES[$question->qtype] |
244 | ->save_question_options($question); |
245 | |
246 | if (!empty($result->error)) { |
247 | notify($result->error); |
248 | return false; |
249 | } |
250 | |
251 | if (!empty($result->notice)) { |
252 | notify($result->notice); |
253 | return true; |
254 | } |
cbe20043 |
255 | |
256 | // Give the question a unique version stamp determined by question_hash() |
257 | set_field('question', 'version', question_hash($question), 'id', $question->id); |
aca318e1 |
258 | } |
259 | return true; |
260 | } |
261 | |
f3701561 |
262 | /** |
263 | * Return complete file within an array, one item per line |
264 | * @param string filename name of file |
265 | * @return mixed contents array or false on failure |
266 | */ |
aca318e1 |
267 | function readdata($filename) { |
aca318e1 |
268 | if (is_readable($filename)) { |
269 | $filearray = file($filename); |
270 | |
271 | /// Check for Macintosh OS line returns (ie file on one line), and fix |
272 | if (ereg("\r", $filearray[0]) AND !ereg("\n", $filearray[0])) { |
273 | return explode("\r", $filearray[0]); |
274 | } else { |
275 | return $filearray; |
276 | } |
277 | } |
278 | return false; |
279 | } |
280 | |
f3701561 |
281 | /** |
282 | * Parses an array of lines into an array of questions, |
283 | * where each item is a question object as defined by |
284 | * readquestion(). Questions are defined as anything |
285 | * between blank lines. |
286 | * |
287 | * If your format does not use blank lines as a delimiter |
288 | * then you will need to override this method. Even then |
289 | * try to use readquestion for each question |
290 | * @param array lines array of lines from readdata |
291 | * @return array array of question objects |
292 | */ |
aca318e1 |
293 | function readquestions($lines) { |
aca318e1 |
294 | |
295 | $questions = array(); |
296 | $currentquestion = array(); |
297 | |
298 | foreach ($lines as $line) { |
299 | $line = trim($line); |
300 | if (empty($line)) { |
301 | if (!empty($currentquestion)) { |
302 | if ($question = $this->readquestion($currentquestion)) { |
303 | $questions[] = $question; |
304 | } |
305 | $currentquestion = array(); |
306 | } |
307 | } else { |
308 | $currentquestion[] = $line; |
309 | } |
310 | } |
311 | |
312 | if (!empty($currentquestion)) { // There may be a final question |
313 | if ($question = $this->readquestion($currentquestion)) { |
314 | $questions[] = $question; |
315 | } |
316 | } |
317 | |
318 | return $questions; |
319 | } |
320 | |
321 | |
f3701561 |
322 | /** |
323 | * return an "empty" question |
324 | * Somewhere to specify question parameters that are not handled |
325 | * by import but are required db fields. |
326 | * This should not be overridden. |
327 | * @return object default question |
328 | */ |
aca318e1 |
329 | function defaultquestion() { |
b0679efa |
330 | global $CFG; |
331 | |
aca318e1 |
332 | $question = new stdClass(); |
b0679efa |
333 | $question->shuffleanswers = $CFG->quiz_shuffleanswers; |
aca318e1 |
334 | $question->defaultgrade = 1; |
335 | $question->image = ""; |
336 | $question->usecase = 0; |
337 | $question->multiplier = array(); |
172f6d95 |
338 | $question->generalfeedback = ''; |
08892d5b |
339 | $question->correctfeedback = ''; |
340 | $question->partiallycorrectfeedback = ''; |
341 | $question->incorrectfeedback = ''; |
5931ea94 |
342 | $question->answernumbering = 'abc'; |
46013523 |
343 | $question->penalty = 0.1; |
aca318e1 |
344 | |
5fd8f999 |
345 | // this option in case the questiontypes class wants |
346 | // to know where the data came from |
347 | $question->export_process = true; |
093414d2 |
348 | $question->import_process = true; |
5fd8f999 |
349 | |
aca318e1 |
350 | return $question; |
351 | } |
352 | |
f3701561 |
353 | /** |
354 | * Given the data known to define a question in |
355 | * this format, this function converts it into a question |
356 | * object suitable for processing and insertion into Moodle. |
357 | * |
358 | * If your format does not use blank lines to delimit questions |
359 | * (e.g. an XML format) you must override 'readquestions' too |
360 | * @param $lines mixed data that represents question |
361 | * @return object question object |
362 | */ |
aca318e1 |
363 | function readquestion($lines) { |
aca318e1 |
364 | |
1e3d6fd8 |
365 | $formatnotimplemented = get_string( 'formatnotimplemented','quiz' ); |
366 | echo "<p>$formatnotimplemented</p>"; |
aca318e1 |
367 | |
368 | return NULL; |
369 | } |
370 | |
f3701561 |
371 | /** |
372 | * Override if any post-processing is required |
373 | * @return boolean success |
374 | */ |
aca318e1 |
375 | function importpostprocess() { |
aca318e1 |
376 | return true; |
377 | } |
378 | |
f3701561 |
379 | /** |
380 | * Import an image file encoded in base64 format |
381 | * @param string path path (in course data) to store picture |
382 | * @param string base64 encoded picture |
383 | * @return string filename (nb. collisions are handled) |
384 | */ |
d08e16b2 |
385 | function importimagefile( $path, $base64 ) { |
d08e16b2 |
386 | global $CFG; |
387 | |
388 | // all this to get the destination directory |
389 | // and filename! |
390 | $fullpath = "{$CFG->dataroot}/{$this->course->id}/$path"; |
391 | $path_parts = pathinfo( $fullpath ); |
392 | $destination = $path_parts['dirname']; |
393 | $file = clean_filename( $path_parts['basename'] ); |
394 | |
71735794 |
395 | // check if path exists |
396 | check_dir_exists($destination, true, true ); |
397 | |
d08e16b2 |
398 | // detect and fix any filename collision - get unique filename |
399 | $newfiles = resolve_filename_collisions( $destination, array($file) ); |
400 | $newfile = $newfiles[0]; |
401 | |
402 | // convert and save file contents |
403 | if (!$content = base64_decode( $base64 )) { |
46013523 |
404 | return ''; |
d08e16b2 |
405 | } |
406 | $newfullpath = "$destination/$newfile"; |
407 | if (!$fh = fopen( $newfullpath, 'w' )) { |
46013523 |
408 | return ''; |
d08e16b2 |
409 | } |
410 | if (!fwrite( $fh, $content )) { |
46013523 |
411 | return ''; |
d08e16b2 |
412 | } |
413 | fclose( $fh ); |
414 | |
415 | // return the (possibly) new filename |
71735794 |
416 | $newfile = ereg_replace("{$CFG->dataroot}/{$this->course->id}/", '',$newfullpath); |
d08e16b2 |
417 | return $newfile; |
418 | } |
419 | |
f3701561 |
420 | /******************* |
421 | * EXPORT FUNCTIONS |
422 | *******************/ |
aca318e1 |
423 | |
a41e3287 |
424 | /** |
425 | * Provide export functionality for plugin questiontypes |
426 | * Do not override |
427 | * @param name questiontype name |
428 | * @param question object data to export |
429 | * @param extra mixed any addition format specific data needed |
430 | * @return string the data to append to export or false if error (or unhandled) |
431 | */ |
432 | function try_exporting_using_qtypes( $name, $question, $extra=null ) { |
433 | global $QTYPES; |
434 | |
435 | // work out the name of format in use |
436 | $formatname = substr( get_class( $this ), strlen( 'qformat_' )); |
437 | $methodname = "export_to_$formatname"; |
438 | |
439 | if (array_key_exists( $name, $QTYPES )) { |
440 | $qtype = $QTYPES[ $name ]; |
441 | if (method_exists( $qtype, $methodname )) { |
442 | if ($data = $qtype->$methodname( $question, $this, $extra )) { |
443 | return $data; |
444 | } |
445 | } |
446 | } |
447 | return false; |
448 | } |
449 | |
f3701561 |
450 | /** |
451 | * Return the files extension appropriate for this type |
452 | * override if you don't want .txt |
453 | * @return string file extension |
454 | */ |
aca318e1 |
455 | function export_file_extension() { |
aca318e1 |
456 | return ".txt"; |
457 | } |
458 | |
f3701561 |
459 | /** |
460 | * Do any pre-processing that may be required |
461 | * @param boolean success |
462 | */ |
08892d5b |
463 | function exportpreprocess() { |
aca318e1 |
464 | return true; |
465 | } |
466 | |
f3701561 |
467 | /** |
468 | * Enable any processing to be done on the content |
469 | * just prior to the file being saved |
470 | * default is to do nothing |
471 | * @param string output text |
472 | * @param string processed output text |
473 | */ |
aca318e1 |
474 | function presave_process( $content ) { |
aca318e1 |
475 | return $content; |
476 | } |
477 | |
f3701561 |
478 | /** |
479 | * Do the export |
480 | * For most types this should not need to be overrided |
481 | * @return boolean success |
482 | */ |
08892d5b |
483 | function exportprocess() { |
aca318e1 |
484 | global $CFG; |
485 | |
486 | // create a directory for the exports (if not already existing) |
1367cb8d |
487 | if (! $export_dir = make_upload_directory($this->question_get_export_dir())) { |
488 | error( get_string('cannotcreatepath','quiz',$export_dir) ); |
aca318e1 |
489 | } |
1367cb8d |
490 | $path = $CFG->dataroot.'/'.$this->question_get_export_dir(); |
aca318e1 |
491 | |
492 | // get the questions (from database) in this category |
493 | // only get q's with no parents (no cloze subquestions specifically) |
494 | $questions = get_questions_category( $this->category, true ); |
495 | |
1e3d6fd8 |
496 | notify( get_string('exportingquestions','quiz') ); |
aca318e1 |
497 | if (!count($questions)) { |
1e3d6fd8 |
498 | notify( get_string('noquestions','quiz') ); |
499 | return false; |
aca318e1 |
500 | } |
501 | $count = 0; |
502 | |
503 | // results are first written into string (and then to a file) |
504 | // so create/initialize the string here |
505 | $expout = ""; |
f1abd39f |
506 | |
507 | // track which category questions are in |
508 | // if it changes we will record the category change in the output |
509 | // file if selected. 0 means that it will get printed before the 1st question |
510 | $trackcategory = 0; |
aca318e1 |
511 | |
512 | // iterate through questions |
513 | foreach($questions as $question) { |
f1abd39f |
514 | |
a9b16aff |
515 | // do not export hidden questions |
516 | if (!empty($question->hidden)) { |
517 | continue; |
518 | } |
519 | |
520 | // do not export random questions |
521 | if ($question->qtype==RANDOM) { |
522 | continue; |
523 | } |
f1abd39f |
524 | |
525 | // check if we need to record category change |
526 | if ($this->cattofile) { |
527 | if ($question->category != $trackcategory) { |
528 | $trackcategory = $question->category; |
529 | $categoryname = get_category_path( $trackcategory ); |
530 | |
531 | // create 'dummy' question for category export |
532 | $dummyquestion = new object; |
533 | $dummyquestion->qtype = 'category'; |
534 | $dummyquestion->category = $categoryname; |
535 | $dummyquestion->name = "switch category to $categoryname"; |
536 | $dummyquestion->id = 0; |
537 | $dummyquestion->questiontextformat = ''; |
538 | $expout .= $this->writequestion( $dummyquestion ) . "\n"; |
539 | } |
540 | } |
541 | |
542 | // export the question displaying message |
543 | $count++; |
5b0dc681 |
544 | echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>"; |
f1abd39f |
545 | $expout .= $this->writequestion( $question ) . "\n"; |
a9b16aff |
546 | } |
aca318e1 |
547 | |
548 | // final pre-process on exported data |
549 | $expout = $this->presave_process( $expout ); |
f1abd39f |
550 | |
aca318e1 |
551 | // write file |
08892d5b |
552 | $filepath = $path."/".$this->filename . $this->export_file_extension(); |
aca318e1 |
553 | if (!$fh=fopen($filepath,"w")) { |
1e3d6fd8 |
554 | error( get_string('cannotopen','quiz',$filepath) ); |
aca318e1 |
555 | } |
f1abd39f |
556 | if (!fwrite($fh, $expout, strlen($expout) )) { |
1e3d6fd8 |
557 | error( get_string('cannotwrite','quiz',$filepath) ); |
aca318e1 |
558 | } |
559 | fclose($fh); |
aca318e1 |
560 | return true; |
561 | } |
562 | |
f3701561 |
563 | /** |
564 | * Do an post-processing that may be required |
565 | * @return boolean success |
566 | */ |
aca318e1 |
567 | function exportpostprocess() { |
aca318e1 |
568 | return true; |
569 | } |
570 | |
f3701561 |
571 | /** |
572 | * convert a single question object into text output in the given |
573 | * format. |
574 | * This must be overriden |
575 | * @param object question question object |
576 | * @return mixed question export text or null if not implemented |
577 | */ |
aca318e1 |
578 | function writequestion($question) { |
1e3d6fd8 |
579 | // if not overidden, then this is an error. |
580 | $formatnotimplemented = get_string( 'formatnotimplemented','quiz' ); |
581 | echo "<p>$formatnotimplemented</p>"; |
aca318e1 |
582 | |
583 | return NULL; |
584 | } |
585 | |
f3701561 |
586 | /** |
587 | * get directory into which export is going |
588 | * @return string file path |
589 | */ |
1367cb8d |
590 | function question_get_export_dir() { |
591 | $dirname = get_string("exportfilename","quiz"); |
592 | $path = $this->course->id.'/backupdata/'.$dirname; // backupdata is protected directory |
593 | return $path; |
594 | } |
595 | |
f3701561 |
596 | /** |
597 | * where question specifies a moodle (text) format this |
598 | * performs the conversion. |
599 | */ |
5b0dc681 |
600 | function format_question_text($question) { |
601 | $formatoptions = new stdClass; |
602 | $formatoptions->noclean = true; |
603 | $formatoptions->para = false; |
604 | if (empty($question->questiontextformat)) { |
605 | $format = FORMAT_MOODLE; |
606 | } else { |
607 | $format = $question->questiontextformat; |
608 | } |
609 | return format_text(stripslashes($question->questiontext), $format, $formatoptions); |
610 | } |
aca318e1 |
611 | } |
612 | |
613 | ?> |