Fixed a notice. Tidied up format class call a bit. Added provision
[moodle.git] / question / format.php
1 <?php  // $Id$ 
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 ////////////////////////////////////////////////////////////////////
9 // Included by import.php and export.php
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 $questionids = array();
21 // functions to indicate import/export functionality
22 // override to return true if implemented
24     function provide_import() {
25       return false;
26     }
28     function provide_export() {
29       return false;
30     }
32 // Accessor methods
34     /**
35      * set the category
36      * @param object category the category object
37      */
38     function setCategory( $category ) {
39         $this->category = $category;
40     }
42     /**
43      * set the course class variable
44      * @param course object Moodle course variable
45      */
46     function setCourse( $course ) {
47         $this->course = $course;
48     }
50     /**
51      * set the filename
52      * @param string filename name of file to import/export
53      */
54     function setFilename( $filename ) {
55         $this->filename = $filename;
56     }
58     /**
59      * set matchgrades
60      * @param string matchgrades error or nearest for grades
61      */
62     function setMatchgrades( $matchgrades ) {
63         $this->matchgrades = $matchgrades;
64     }
66     /**
67      * set catfromfile
68      * @param bool catfromfile allow categories embedded in import file
69      */
70     function setCatfromfile( $catfromfile ) {
71         $this->catfromfile = $catfromfile;
72     }
74 /// Importing functions
76     /**
77      * Perform any required pre-processing
78      */
79     function importpreprocess() {
80         return true;
81     }
83     /**
84      * Process the file
85      * This method should not normally be overidden
86      */
87     function importprocess() {
88         if (! $lines = $this->readdata($this->filename)) {
89             notify( get_string('cannotread','quiz') );
90             return false;
91         }
93         if (! $questions = $this->readquestions($lines)) {   // Extract all the questions
94             notify( get_string('noquestionsinfile','quiz') );
95             return false;
96         }
98         notify( get_string('importingquestions','quiz',count($questions)) );
100         // get list of valid answer grades
101         $grades = get_grade_options();
102         $gradeoptionsfull = $grades->gradeoptionsfull;
104         $count = 0;
106         foreach ($questions as $question) {   // Process and store each question
108             // check for category modifiers
109             if ($question->qtype=='category') {
110                 if ($this->catfromfile) {
111                     // find/create category object
112                     $catpath = $question->category;
113                     $newcategory = create_category_path( $catpath, '/', $this->course->id );
114                     if (!empty($newcategory)) {
115                         $this->category = $newcategory;
116                     }
117                 }
118                 continue; 
119             }
121             $count++;
123             echo "<hr /><p><b>$count</b>. ".stripslashes($question->questiontext)."</p>";
125             // check for answer grades validity (must match fixed list of grades)
126             if (!empty($question->fraction) and (is_array($question->fraction))) {
127                 $fractions = $question->fraction;
128                 $answersvalid = true; // in case they are!
129                 foreach ($fractions as $key => $fraction) {
130                     $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades);
131                     if ($newfraction===false) {
132                         $answersvalid = false;
133                     }
134                     else {
135                         $fractions[$key] = $newfraction;
136                     }
137                 }
138                 if (!$answersvalid) {
139                     notify( get_string('matcherror','quiz') );
140                     continue;
141                 }
142                 else {
143                     $question->fraction = $fractions;
144                 }
145             }
147             $question->category = $this->category->id;
148             $question->stamp = make_unique_id_code();  // Set the unique code (not to be changed)
150             if (!$question->id = insert_record("question", $question)) {
151                 error( get_string('cannotinsert','quiz') );
152             }
154             $this->questionids[] = $question->id;
156             // Now to save all the answers and type-specific options
158             global $QTYPES;
159             $result = $QTYPES[$question->qtype]
160                     ->save_question_options($question);
162             if (!empty($result->error)) {
163                 notify($result->error);
164                 return false;
165             }
167             if (!empty($result->notice)) {
168                 notify($result->notice);
169                 return true;
170             }
172             // Give the question a unique version stamp determined by question_hash()
173             set_field('question', 'version', question_hash($question), 'id', $question->id);
174         }
175         return true;
176     }
179     function readdata($filename) {
180     /// Returns complete file with an array, one item per line
182         if (is_readable($filename)) {
183             $filearray = file($filename);
185             /// Check for Macintosh OS line returns (ie file on one line), and fix
186             if (ereg("\r", $filearray[0]) AND !ereg("\n", $filearray[0])) {
187                 return explode("\r", $filearray[0]);
188             } else {
189                 return $filearray;
190             }
191         }
192         return false;
193     }
195     function readquestions($lines) {
196     /// Parses an array of lines into an array of questions, 
197     /// where each item is a question object as defined by 
198     /// readquestion().   Questions are defined as anything 
199     /// between blank lines.
200      
201         $questions = array();
202         $currentquestion = array();
204         foreach ($lines as $line) {
205             $line = trim($line);
206             if (empty($line)) {
207                 if (!empty($currentquestion)) {
208                     if ($question = $this->readquestion($currentquestion)) {
209                         $questions[] = $question;
210                     }
211                     $currentquestion = array();
212                 }
213             } else {
214                 $currentquestion[] = $line;
215             }
216         }
218         if (!empty($currentquestion)) {  // There may be a final question
219             if ($question = $this->readquestion($currentquestion)) {
220                 $questions[] = $question;
221             }
222         }
224         return $questions;
225     }
228     function defaultquestion() {
229     // returns an "empty" question
230     // Somewhere to specify question parameters that are not handled
231     // by import but are required db fields.
232     // This should not be overridden. 
233         $question = new stdClass();
234         $question->shuffleanswers = 0; 
235         $question->defaultgrade = 1;
236         $question->image = "";
237         $question->usecase = 0;
238         $question->multiplier = array();
239         $question->generalfeedback = '';
240         $question->correctfeedback = '';
241         $question->partiallycorrectfeedback = '';
242         $question->incorrectfeedback = '';
244         return $question;
245     }
247     function readquestion($lines) {
248     /// Given an array of lines known to define a question in 
249     /// this format, this function converts it into a question 
250     /// object suitable for processing and insertion into Moodle.
252         $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
253         echo "<p>$formatnotimplemented</p>";
255         return NULL;
256     }
259     function importpostprocess() {
260     /// Does any post-processing that may be desired
261     /// Argument is a simple array of question ids that 
262     /// have just been added.
264         return true;
265     }
267     function importimagefile( $path, $base64 ) {
268     /// imports an image file encoded in base64 format
269     /// This should not be overridden.
270         global $CFG;
272         // all this to get the destination directory
273         // and filename!
274         $fullpath = "{$CFG->dataroot}/{$this->course->id}/$path";
275         $path_parts = pathinfo( $fullpath );
276         $destination = $path_parts['dirname'];
277         $file = clean_filename( $path_parts['basename'] );
279         // detect and fix any filename collision - get unique filename
280         $newfiles = resolve_filename_collisions( $destination, array($file) );        
281         $newfile = $newfiles[0];
283         // convert and save file contents
284         if (!$content = base64_decode( $base64 )) {
285             return false;
286         }
287         $newfullpath = "$destination/$newfile";
288         if (!$fh = fopen( $newfullpath, 'w' )) {
289             return false;
290         }
291         if (!fwrite( $fh, $content )) {
292             return false;
293         }
294         fclose( $fh );
296         // return the (possibly) new filename
297         return $newfile;
298     }
300 //=================
301 // Export functions
302 //=================
304     function export_file_extension() {
305     /// return the files extension appropriate for this type
306     /// override if you don't want .txt
307   
308         return ".txt";
309     }
311     function exportpreprocess() {
312     /// Does any pre-processing that may be desired
314         return true;
315     }
317     function presave_process( $content ) {
318     /// enables any processing to be done on the content
319     /// just prior to the file being saved
320     /// default is to do nothing
321  
322         return $content;
323     }
325     function exportprocess() {
326     /// Exports a given category.  There's probably little need to change this
328         global $CFG;
330         // create a directory for the exports (if not already existing)
331         if (! $export_dir = make_upload_directory($this->question_get_export_dir())) {
332               error( get_string('cannotcreatepath','quiz',$export_dir) );
333         }
334         $path = $CFG->dataroot.'/'.$this->question_get_export_dir();
336         // get the questions (from database) in this category
337         // only get q's with no parents (no cloze subquestions specifically)
338         $questions = get_questions_category( $this->category, true );
340         notify( get_string('exportingquestions','quiz') );
341         if (!count($questions)) {
342             notify( get_string('noquestions','quiz') );
343             return false;
344         }
345         $count = 0;
347         // results are first written into string (and then to a file)
348         // so create/initialize the string here
349         $expout = "";
351         // iterate through questions
352         foreach($questions as $question) {
353             // do not export hidden questions
354             if (!empty($question->hidden)) {
355                 continue;
356             }
358             // do not export random questions
359             if ($question->qtype==RANDOM) {
360                 continue;
361             }
363         // export the question displaying message
364         $count++;
365         echo "<hr /><p><b>$count</b>. ".stripslashes($question->questiontext)."</p>";
366         $expout .= $this->writequestion( $question ) . "\n";
367         }
369         // final pre-process on exported data
370         $expout = $this->presave_process( $expout );
372         // write file
373         $filepath = $path."/".$this->filename . $this->export_file_extension();
374         if (!$fh=fopen($filepath,"w")) {
375             error( get_string('cannotopen','quiz',$filepath) );
376         }
377         if (!fwrite($fh, $expout)) {
378             error( get_string('cannotwrite','quiz',$filepath) );
379         }
380         fclose($fh);
382         return true;
383     }
385     function exportpostprocess() {
386     /// Does any post-processing that may be desired
388         return true;
389     }
391     function writequestion($question) {
392     /// Turns a question object into textual output in the given format 
393     /// must be overidden
395         // if not overidden, then this is an error.
396         $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
397         echo "<p>$formatnotimplemented</p>";
399         return NULL;
400     }
402     function question_get_export_dir() {
403         $dirname = get_string("exportfilename","quiz");
404         $path = $this->course->id.'/backupdata/'.$dirname; // backupdata is protected directory
405         return $path;
406     }
410 ?>