merging MOODLE_19_QUESTIONS with HEAD
[moodle.git] / question / format.php
CommitLineData
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 11class 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?>