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