Merege from stable. New strings for xml import error messages.
[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
115 /**
116 * Perform any required pre-processing
f3701561 117 * @return boolean success
08892d5b 118 */
119 function importpreprocess() {
120 return true;
121 }
122
123 /**
124 * Process the file
125 * This method should not normally be overidden
f3701561 126 * @return boolean success
08892d5b 127 */
128 function importprocess() {
f3701561 129
130 // STAGE 1: Parse the file
131 notify( get_string('parsingquestions','quiz') );
132
08892d5b 133 if (! $lines = $this->readdata($this->filename)) {
1e3d6fd8 134 notify( get_string('cannotread','quiz') );
aca318e1 135 return false;
136 }
137
138 if (! $questions = $this->readquestions($lines)) { // Extract all the questions
1e3d6fd8 139 notify( get_string('noquestionsinfile','quiz') );
aca318e1 140 return false;
141 }
142
f3701561 143 // STAGE 2: Write data to database
1e3d6fd8 144 notify( get_string('importingquestions','quiz',count($questions)) );
aca318e1 145
f3701561 146 // check for errors before we continue
147 if ($this->stoponerror and ($this->importerrors>0)) {
148 return false;
149 }
150
76f0a334 151 // get list of valid answer grades
152 $grades = get_grade_options();
153 $gradeoptionsfull = $grades->gradeoptionsfull;
154
aca318e1 155 $count = 0;
156
157 foreach ($questions as $question) { // Process and store each question
08892d5b 158
159 // check for category modifiers
160 if ($question->qtype=='category') {
161 if ($this->catfromfile) {
162 // find/create category object
163 $catpath = $question->category;
164 $newcategory = create_category_path( $catpath, '/', $this->course->id );
165 if (!empty($newcategory)) {
166 $this->category = $newcategory;
167 }
168 }
169 continue;
170 }
171
aca318e1 172 $count++;
173
5b0dc681 174 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
aca318e1 175
76f0a334 176 // check for answer grades validity (must match fixed list of grades)
08892d5b 177 if (!empty($question->fraction) and (is_array($question->fraction))) {
656ee8c6 178 $fractions = $question->fraction;
2d52056f 179 $answersvalid = true; // in case they are!
180 foreach ($fractions as $key => $fraction) {
08892d5b 181 $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades);
2d52056f 182 if ($newfraction===false) {
183 $answersvalid = false;
184 }
185 else {
186 $fractions[$key] = $newfraction;
187 }
188 }
189 if (!$answersvalid) {
cdeabc06 190 notify( get_string('matcherror','quiz') );
2d52056f 191 continue;
76f0a334 192 }
193 else {
2d52056f 194 $question->fraction = $fractions;
76f0a334 195 }
196 }
76f0a334 197
aca318e1 198 $question->category = $this->category->id;
199 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
aca318e1 200
062f1125 201 if (!$question->id = insert_record("question", $question)) {
1e3d6fd8 202 error( get_string('cannotinsert','quiz') );
aca318e1 203 }
204
205 $this->questionids[] = $question->id;
206
207 // Now to save all the answers and type-specific options
208
209 global $QTYPES;
210 $result = $QTYPES[$question->qtype]
211 ->save_question_options($question);
212
213 if (!empty($result->error)) {
214 notify($result->error);
215 return false;
216 }
217
218 if (!empty($result->notice)) {
219 notify($result->notice);
220 return true;
221 }
cbe20043 222
223 // Give the question a unique version stamp determined by question_hash()
224 set_field('question', 'version', question_hash($question), 'id', $question->id);
aca318e1 225 }
226 return true;
227 }
228
f3701561 229 /**
230 * Return complete file within an array, one item per line
231 * @param string filename name of file
232 * @return mixed contents array or false on failure
233 */
aca318e1 234 function readdata($filename) {
aca318e1 235 if (is_readable($filename)) {
236 $filearray = file($filename);
237
238 /// Check for Macintosh OS line returns (ie file on one line), and fix
239 if (ereg("\r", $filearray[0]) AND !ereg("\n", $filearray[0])) {
240 return explode("\r", $filearray[0]);
241 } else {
242 return $filearray;
243 }
244 }
245 return false;
246 }
247
f3701561 248 /**
249 * Parses an array of lines into an array of questions,
250 * where each item is a question object as defined by
251 * readquestion(). Questions are defined as anything
252 * between blank lines.
253 *
254 * If your format does not use blank lines as a delimiter
255 * then you will need to override this method. Even then
256 * try to use readquestion for each question
257 * @param array lines array of lines from readdata
258 * @return array array of question objects
259 */
aca318e1 260 function readquestions($lines) {
aca318e1 261
262 $questions = array();
263 $currentquestion = array();
264
265 foreach ($lines as $line) {
266 $line = trim($line);
267 if (empty($line)) {
268 if (!empty($currentquestion)) {
269 if ($question = $this->readquestion($currentquestion)) {
270 $questions[] = $question;
271 }
272 $currentquestion = array();
273 }
274 } else {
275 $currentquestion[] = $line;
276 }
277 }
278
279 if (!empty($currentquestion)) { // There may be a final question
280 if ($question = $this->readquestion($currentquestion)) {
281 $questions[] = $question;
282 }
283 }
284
285 return $questions;
286 }
287
288
f3701561 289 /**
290 * return an "empty" question
291 * Somewhere to specify question parameters that are not handled
292 * by import but are required db fields.
293 * This should not be overridden.
294 * @return object default question
295 */
aca318e1 296 function defaultquestion() {
b0679efa 297 global $CFG;
298
aca318e1 299 $question = new stdClass();
b0679efa 300 $question->shuffleanswers = $CFG->quiz_shuffleanswers;
aca318e1 301 $question->defaultgrade = 1;
302 $question->image = "";
303 $question->usecase = 0;
304 $question->multiplier = array();
172f6d95 305 $question->generalfeedback = '';
08892d5b 306 $question->correctfeedback = '';
307 $question->partiallycorrectfeedback = '';
308 $question->incorrectfeedback = '';
5931ea94 309 $question->answernumbering = 'abc';
aca318e1 310
5fd8f999 311 // this option in case the questiontypes class wants
312 // to know where the data came from
313 $question->export_process = true;
314
aca318e1 315 return $question;
316 }
317
f3701561 318 /**
319 * Given the data known to define a question in
320 * this format, this function converts it into a question
321 * object suitable for processing and insertion into Moodle.
322 *
323 * If your format does not use blank lines to delimit questions
324 * (e.g. an XML format) you must override 'readquestions' too
325 * @param $lines mixed data that represents question
326 * @return object question object
327 */
aca318e1 328 function readquestion($lines) {
aca318e1 329
1e3d6fd8 330 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
331 echo "<p>$formatnotimplemented</p>";
aca318e1 332
333 return NULL;
334 }
335
f3701561 336 /**
337 * Override if any post-processing is required
338 * @return boolean success
339 */
aca318e1 340 function importpostprocess() {
aca318e1 341 return true;
342 }
343
f3701561 344 /**
345 * Import an image file encoded in base64 format
346 * @param string path path (in course data) to store picture
347 * @param string base64 encoded picture
348 * @return string filename (nb. collisions are handled)
349 */
d08e16b2 350 function importimagefile( $path, $base64 ) {
d08e16b2 351 global $CFG;
352
353 // all this to get the destination directory
354 // and filename!
355 $fullpath = "{$CFG->dataroot}/{$this->course->id}/$path";
356 $path_parts = pathinfo( $fullpath );
357 $destination = $path_parts['dirname'];
358 $file = clean_filename( $path_parts['basename'] );
359
360 // detect and fix any filename collision - get unique filename
361 $newfiles = resolve_filename_collisions( $destination, array($file) );
362 $newfile = $newfiles[0];
363
364 // convert and save file contents
365 if (!$content = base64_decode( $base64 )) {
366 return false;
367 }
368 $newfullpath = "$destination/$newfile";
369 if (!$fh = fopen( $newfullpath, 'w' )) {
370 return false;
371 }
372 if (!fwrite( $fh, $content )) {
373 return false;
374 }
375 fclose( $fh );
376
377 // return the (possibly) new filename
378 return $newfile;
379 }
380
f3701561 381/*******************
382 * EXPORT FUNCTIONS
383 *******************/
aca318e1 384
f3701561 385 /**
386 * Return the files extension appropriate for this type
387 * override if you don't want .txt
388 * @return string file extension
389 */
aca318e1 390 function export_file_extension() {
aca318e1 391 return ".txt";
392 }
393
f3701561 394 /**
395 * Do any pre-processing that may be required
396 * @param boolean success
397 */
08892d5b 398 function exportpreprocess() {
aca318e1 399 return true;
400 }
401
f3701561 402 /**
403 * Enable any processing to be done on the content
404 * just prior to the file being saved
405 * default is to do nothing
406 * @param string output text
407 * @param string processed output text
408 */
aca318e1 409 function presave_process( $content ) {
aca318e1 410 return $content;
411 }
412
f3701561 413 /**
414 * Do the export
415 * For most types this should not need to be overrided
416 * @return boolean success
417 */
08892d5b 418 function exportprocess() {
aca318e1 419 global $CFG;
420
421 // create a directory for the exports (if not already existing)
1367cb8d 422 if (! $export_dir = make_upload_directory($this->question_get_export_dir())) {
423 error( get_string('cannotcreatepath','quiz',$export_dir) );
aca318e1 424 }
1367cb8d 425 $path = $CFG->dataroot.'/'.$this->question_get_export_dir();
aca318e1 426
427 // get the questions (from database) in this category
428 // only get q's with no parents (no cloze subquestions specifically)
429 $questions = get_questions_category( $this->category, true );
430
1e3d6fd8 431 notify( get_string('exportingquestions','quiz') );
aca318e1 432 if (!count($questions)) {
1e3d6fd8 433 notify( get_string('noquestions','quiz') );
434 return false;
aca318e1 435 }
436 $count = 0;
437
438 // results are first written into string (and then to a file)
439 // so create/initialize the string here
440 $expout = "";
f1abd39f 441
442 // track which category questions are in
443 // if it changes we will record the category change in the output
444 // file if selected. 0 means that it will get printed before the 1st question
445 $trackcategory = 0;
aca318e1 446
447 // iterate through questions
448 foreach($questions as $question) {
f1abd39f 449
a9b16aff 450 // do not export hidden questions
451 if (!empty($question->hidden)) {
452 continue;
453 }
454
455 // do not export random questions
456 if ($question->qtype==RANDOM) {
457 continue;
458 }
f1abd39f 459
460 // check if we need to record category change
461 if ($this->cattofile) {
462 if ($question->category != $trackcategory) {
463 $trackcategory = $question->category;
464 $categoryname = get_category_path( $trackcategory );
465
466 // create 'dummy' question for category export
467 $dummyquestion = new object;
468 $dummyquestion->qtype = 'category';
469 $dummyquestion->category = $categoryname;
470 $dummyquestion->name = "switch category to $categoryname";
471 $dummyquestion->id = 0;
472 $dummyquestion->questiontextformat = '';
473 $expout .= $this->writequestion( $dummyquestion ) . "\n";
474 }
475 }
476
477 // export the question displaying message
478 $count++;
5b0dc681 479 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
f1abd39f 480 $expout .= $this->writequestion( $question ) . "\n";
a9b16aff 481 }
aca318e1 482
483 // final pre-process on exported data
484 $expout = $this->presave_process( $expout );
f1abd39f 485
aca318e1 486 // write file
08892d5b 487 $filepath = $path."/".$this->filename . $this->export_file_extension();
aca318e1 488 if (!$fh=fopen($filepath,"w")) {
1e3d6fd8 489 error( get_string('cannotopen','quiz',$filepath) );
aca318e1 490 }
f1abd39f 491 if (!fwrite($fh, $expout, strlen($expout) )) {
1e3d6fd8 492 error( get_string('cannotwrite','quiz',$filepath) );
aca318e1 493 }
494 fclose($fh);
aca318e1 495 return true;
496 }
497
f3701561 498 /**
499 * Do an post-processing that may be required
500 * @return boolean success
501 */
aca318e1 502 function exportpostprocess() {
aca318e1 503 return true;
504 }
505
f3701561 506 /**
507 * convert a single question object into text output in the given
508 * format.
509 * This must be overriden
510 * @param object question question object
511 * @return mixed question export text or null if not implemented
512 */
aca318e1 513 function writequestion($question) {
1e3d6fd8 514 // if not overidden, then this is an error.
515 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
516 echo "<p>$formatnotimplemented</p>";
aca318e1 517
518 return NULL;
519 }
520
f3701561 521 /**
522 * get directory into which export is going
523 * @return string file path
524 */
1367cb8d 525 function question_get_export_dir() {
526 $dirname = get_string("exportfilename","quiz");
527 $path = $this->course->id.'/backupdata/'.$dirname; // backupdata is protected directory
528 return $path;
529 }
530
f3701561 531 /**
532 * where question specifies a moodle (text) format this
533 * performs the conversion.
534 */
5b0dc681 535 function format_question_text($question) {
536 $formatoptions = new stdClass;
537 $formatoptions->noclean = true;
538 $formatoptions->para = false;
539 if (empty($question->questiontextformat)) {
540 $format = FORMAT_MOODLE;
541 } else {
542 $format = $question->questiontextformat;
543 }
544 return format_text(stripslashes($question->questiontext), $format, $formatoptions);
545 }
aca318e1 546}
547
548?>