MDL-9115 Added new strings to lang/en_utf8/group.php (where in roles.php before!...
[moodle-pu.git] / question / format.php
blobcce13b5b11070b6e2ec78a6635ba9b17a1b175f9
1 <?php // $Id$
2 /**
3 * Base class for question import and export formats.
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
11 class qformat_default {
13 var $displayerrors = true;
14 var $category = NULL;
15 var $course = NULL;
16 var $filename = '';
17 var $matchgrades = 'error';
18 var $catfromfile = 0;
19 var $cattofile = 0;
20 var $questionids = array();
21 var $importerrors = 0;
22 var $stoponerror = true;
24 // functions to indicate import/export functionality
25 // override to return true if implemented
27 function provide_import() {
28 return false;
31 function provide_export() {
32 return false;
35 // Accessor methods
37 /**
38 * set the category
39 * @param object category the category object
41 function setCategory( $category ) {
42 $this->category = $category;
45 /**
46 * set the course class variable
47 * @param course object Moodle course variable
49 function setCourse( $course ) {
50 $this->course = $course;
53 /**
54 * set the filename
55 * @param string filename name of file to import/export
57 function setFilename( $filename ) {
58 $this->filename = $filename;
61 /**
62 * set matchgrades
63 * @param string matchgrades error or nearest for grades
65 function setMatchgrades( $matchgrades ) {
66 $this->matchgrades = $matchgrades;
69 /**
70 * set catfromfile
71 * @param bool catfromfile allow categories embedded in import file
73 function setCatfromfile( $catfromfile ) {
74 $this->catfromfile = $catfromfile;
77 /**
78 * set cattofile
79 * @param bool cattofile exports categories within export file
81 function setCattofile( $cattofile ) {
82 $this->cattofile = $cattofile;
85 /**
86 * set stoponerror
87 * @param bool stoponerror stops database write if any errors reported
89 function setStoponerror( $stoponerror ) {
90 $this->stoponerror = $stoponerror;
93 /***********************
94 * IMPORTING FUNCTIONS
95 ***********************/
97 /**
98 * Handle parsing error
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";
107 echo "<strong>$message</strong>\n";
108 echo "</div>";
110 $this->importerrors++;
114 * Perform any required pre-processing
115 * @return boolean success
117 function importpreprocess() {
118 return true;
122 * Process the file
123 * This method should not normally be overidden
124 * @return boolean success
126 function importprocess() {
128 // STAGE 1: Parse the file
129 notify( get_string('parsingquestions','quiz') );
131 if (! $lines = $this->readdata($this->filename)) {
132 notify( get_string('cannotread','quiz') );
133 return false;
136 if (! $questions = $this->readquestions($lines)) { // Extract all the questions
137 notify( get_string('noquestionsinfile','quiz') );
138 return false;
141 // STAGE 2: Write data to database
142 notify( get_string('importingquestions','quiz',count($questions)) );
144 // check for errors before we continue
145 if ($this->stoponerror and ($this->importerrors>0)) {
146 return false;
149 // get list of valid answer grades
150 $grades = get_grade_options();
151 $gradeoptionsfull = $grades->gradeoptionsfull;
153 $count = 0;
155 foreach ($questions as $question) { // Process and store each question
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;
167 continue;
170 $count++;
172 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
174 // check for answer grades validity (must match fixed list of grades)
175 if (!empty($question->fraction) and (is_array($question->fraction))) {
176 $fractions = $question->fraction;
177 $answersvalid = true; // in case they are!
178 foreach ($fractions as $key => $fraction) {
179 $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades);
180 if ($newfraction===false) {
181 $answersvalid = false;
183 else {
184 $fractions[$key] = $newfraction;
187 if (!$answersvalid) {
188 notify( get_string('matcherror','quiz') );
189 continue;
191 else {
192 $question->fraction = $fractions;
196 $question->category = $this->category->id;
197 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
199 if (!$question->id = insert_record("question", $question)) {
200 error( get_string('cannotinsert','quiz') );
203 $this->questionids[] = $question->id;
205 // Now to save all the answers and type-specific options
207 global $QTYPES;
208 $result = $QTYPES[$question->qtype]
209 ->save_question_options($question);
211 if (!empty($result->error)) {
212 notify($result->error);
213 return false;
216 if (!empty($result->notice)) {
217 notify($result->notice);
218 return true;
221 // Give the question a unique version stamp determined by question_hash()
222 set_field('question', 'version', question_hash($question), 'id', $question->id);
224 return true;
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
232 function readdata($filename) {
233 if (is_readable($filename)) {
234 $filearray = file($filename);
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;
243 return false;
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.
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
258 function readquestions($lines) {
260 $questions = array();
261 $currentquestion = array();
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;
270 $currentquestion = array();
272 } else {
273 $currentquestion[] = $line;
277 if (!empty($currentquestion)) { // There may be a final question
278 if ($question = $this->readquestion($currentquestion)) {
279 $questions[] = $question;
283 return $questions;
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
294 function defaultquestion() {
295 global $CFG;
297 $question = new stdClass();
298 $question->shuffleanswers = $CFG->quiz_shuffleanswers;
299 $question->defaultgrade = 1;
300 $question->image = "";
301 $question->usecase = 0;
302 $question->multiplier = array();
303 $question->generalfeedback = '';
304 $question->correctfeedback = '';
305 $question->partiallycorrectfeedback = '';
306 $question->incorrectfeedback = '';
308 // this option in case the questiontypes class wants
309 // to know where the data came from
310 $question->export_process = true;
312 return $question;
316 * Given the data known to define a question in
317 * this format, this function converts it into a question
318 * object suitable for processing and insertion into Moodle.
320 * If your format does not use blank lines to delimit questions
321 * (e.g. an XML format) you must override 'readquestions' too
322 * @param $lines mixed data that represents question
323 * @return object question object
325 function readquestion($lines) {
327 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
328 echo "<p>$formatnotimplemented</p>";
330 return NULL;
334 * Override if any post-processing is required
335 * @return boolean success
337 function importpostprocess() {
338 return true;
342 * Import an image file encoded in base64 format
343 * @param string path path (in course data) to store picture
344 * @param string base64 encoded picture
345 * @return string filename (nb. collisions are handled)
347 function importimagefile( $path, $base64 ) {
348 global $CFG;
350 // all this to get the destination directory
351 // and filename!
352 $fullpath = "{$CFG->dataroot}/{$this->course->id}/$path";
353 $path_parts = pathinfo( $fullpath );
354 $destination = $path_parts['dirname'];
355 $file = clean_filename( $path_parts['basename'] );
357 // detect and fix any filename collision - get unique filename
358 $newfiles = resolve_filename_collisions( $destination, array($file) );
359 $newfile = $newfiles[0];
361 // convert and save file contents
362 if (!$content = base64_decode( $base64 )) {
363 return false;
365 $newfullpath = "$destination/$newfile";
366 if (!$fh = fopen( $newfullpath, 'w' )) {
367 return false;
369 if (!fwrite( $fh, $content )) {
370 return false;
372 fclose( $fh );
374 // return the (possibly) new filename
375 return $newfile;
378 /*******************
379 * EXPORT FUNCTIONS
380 *******************/
383 * Return the files extension appropriate for this type
384 * override if you don't want .txt
385 * @return string file extension
387 function export_file_extension() {
388 return ".txt";
392 * Do any pre-processing that may be required
393 * @param boolean success
395 function exportpreprocess() {
396 return true;
400 * Enable any processing to be done on the content
401 * just prior to the file being saved
402 * default is to do nothing
403 * @param string output text
404 * @param string processed output text
406 function presave_process( $content ) {
407 return $content;
411 * Do the export
412 * For most types this should not need to be overrided
413 * @return boolean success
415 function exportprocess() {
416 global $CFG;
418 // create a directory for the exports (if not already existing)
419 if (! $export_dir = make_upload_directory($this->question_get_export_dir())) {
420 error( get_string('cannotcreatepath','quiz',$export_dir) );
422 $path = $CFG->dataroot.'/'.$this->question_get_export_dir();
424 // get the questions (from database) in this category
425 // only get q's with no parents (no cloze subquestions specifically)
426 $questions = get_questions_category( $this->category, true );
428 notify( get_string('exportingquestions','quiz') );
429 if (!count($questions)) {
430 notify( get_string('noquestions','quiz') );
431 return false;
433 $count = 0;
435 // results are first written into string (and then to a file)
436 // so create/initialize the string here
437 $expout = "";
439 // track which category questions are in
440 // if it changes we will record the category change in the output
441 // file if selected. 0 means that it will get printed before the 1st question
442 $trackcategory = 0;
444 // iterate through questions
445 foreach($questions as $question) {
447 // do not export hidden questions
448 if (!empty($question->hidden)) {
449 continue;
452 // do not export random questions
453 if ($question->qtype==RANDOM) {
454 continue;
457 // check if we need to record category change
458 if ($this->cattofile) {
459 if ($question->category != $trackcategory) {
460 $trackcategory = $question->category;
461 $categoryname = get_category_path( $trackcategory );
463 // create 'dummy' question for category export
464 $dummyquestion = new object;
465 $dummyquestion->qtype = 'category';
466 $dummyquestion->category = $categoryname;
467 $dummyquestion->name = "switch category to $categoryname";
468 $dummyquestion->id = 0;
469 $dummyquestion->questiontextformat = '';
470 $expout .= $this->writequestion( $dummyquestion ) . "\n";
474 // export the question displaying message
475 $count++;
476 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
477 $expout .= $this->writequestion( $question ) . "\n";
480 // final pre-process on exported data
481 $expout = $this->presave_process( $expout );
483 // write file
484 $filepath = $path."/".$this->filename . $this->export_file_extension();
485 if (!$fh=fopen($filepath,"w")) {
486 error( get_string('cannotopen','quiz',$filepath) );
488 if (!fwrite($fh, $expout, strlen($expout) )) {
489 error( get_string('cannotwrite','quiz',$filepath) );
491 fclose($fh);
492 return true;
496 * Do an post-processing that may be required
497 * @return boolean success
499 function exportpostprocess() {
500 return true;
504 * convert a single question object into text output in the given
505 * format.
506 * This must be overriden
507 * @param object question question object
508 * @return mixed question export text or null if not implemented
510 function writequestion($question) {
511 // if not overidden, then this is an error.
512 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
513 echo "<p>$formatnotimplemented</p>";
515 return NULL;
519 * get directory into which export is going
520 * @return string file path
522 function question_get_export_dir() {
523 $dirname = get_string("exportfilename","quiz");
524 $path = $this->course->id.'/backupdata/'.$dirname; // backupdata is protected directory
525 return $path;
529 * where question specifies a moodle (text) format this
530 * performs the conversion.
532 function format_question_text($question) {
533 $formatoptions = new stdClass;
534 $formatoptions->noclean = true;
535 $formatoptions->para = false;
536 if (empty($question->questiontextformat)) {
537 $format = FORMAT_MOODLE;
538 } else {
539 $format = $question->questiontextformat;
541 return format_text(stripslashes($question->questiontext), $format, $formatoptions);