Fixed a notice. Tidied up format class call a bit. Added provision
[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
08892d5b 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;
aca318e1 19 var $questionids = array();
20
21// functions to indicate import/export functionality
22// override to return true if implemented
23
24 function provide_import() {
25 return false;
26 }
27
28 function provide_export() {
29 return false;
30 }
31
08892d5b 32// Accessor methods
aca318e1 33
08892d5b 34 /**
35 * set the category
36 * @param object category the category object
37 */
38 function setCategory( $category ) {
39 $this->category = $category;
40 }
aca318e1 41
08892d5b 42 /**
43 * set the course class variable
44 * @param course object Moodle course variable
45 */
46 function setCourse( $course ) {
aca318e1 47 $this->course = $course;
08892d5b 48 }
aca318e1 49
08892d5b 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 }
57
58 /**
59 * set matchgrades
60 * @param string matchgrades error or nearest for grades
61 */
62 function setMatchgrades( $matchgrades ) {
63 $this->matchgrades = $matchgrades;
aca318e1 64 }
65
76f0a334 66 /**
08892d5b 67 * set catfromfile
68 * @param bool catfromfile allow categories embedded in import file
76f0a334 69 */
08892d5b 70 function setCatfromfile( $catfromfile ) {
71 $this->catfromfile = $catfromfile;
72 }
aca318e1 73
08892d5b 74/// Importing functions
75
76 /**
77 * Perform any required pre-processing
78 */
79 function importpreprocess() {
80 return true;
81 }
82
83 /**
84 * Process the file
85 * This method should not normally be overidden
86 */
87 function importprocess() {
88 if (! $lines = $this->readdata($this->filename)) {
1e3d6fd8 89 notify( get_string('cannotread','quiz') );
aca318e1 90 return false;
91 }
92
93 if (! $questions = $this->readquestions($lines)) { // Extract all the questions
1e3d6fd8 94 notify( get_string('noquestionsinfile','quiz') );
aca318e1 95 return false;
96 }
97
1e3d6fd8 98 notify( get_string('importingquestions','quiz',count($questions)) );
aca318e1 99
76f0a334 100 // get list of valid answer grades
101 $grades = get_grade_options();
102 $gradeoptionsfull = $grades->gradeoptionsfull;
103
aca318e1 104 $count = 0;
105
106 foreach ($questions as $question) { // Process and store each question
08892d5b 107
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 }
120
aca318e1 121 $count++;
122
123 echo "<hr /><p><b>$count</b>. ".stripslashes($question->questiontext)."</p>";
124
76f0a334 125 // check for answer grades validity (must match fixed list of grades)
08892d5b 126 if (!empty($question->fraction) and (is_array($question->fraction))) {
656ee8c6 127 $fractions = $question->fraction;
2d52056f 128 $answersvalid = true; // in case they are!
129 foreach ($fractions as $key => $fraction) {
08892d5b 130 $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades);
2d52056f 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;
76f0a334 141 }
142 else {
2d52056f 143 $question->fraction = $fractions;
76f0a334 144 }
145 }
76f0a334 146
aca318e1 147 $question->category = $this->category->id;
148 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
aca318e1 149
062f1125 150 if (!$question->id = insert_record("question", $question)) {
1e3d6fd8 151 error( get_string('cannotinsert','quiz') );
aca318e1 152 }
153
154 $this->questionids[] = $question->id;
155
156 // Now to save all the answers and type-specific options
157
158 global $QTYPES;
159 $result = $QTYPES[$question->qtype]
160 ->save_question_options($question);
161
162 if (!empty($result->error)) {
163 notify($result->error);
164 return false;
165 }
166
167 if (!empty($result->notice)) {
168 notify($result->notice);
169 return true;
170 }
cbe20043 171
172 // Give the question a unique version stamp determined by question_hash()
173 set_field('question', 'version', question_hash($question), 'id', $question->id);
aca318e1 174 }
175 return true;
176 }
177
178
179 function readdata($filename) {
180 /// Returns complete file with an array, one item per line
181
182 if (is_readable($filename)) {
183 $filearray = file($filename);
184
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 }
194
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();
203
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 }
217
218 if (!empty($currentquestion)) { // There may be a final question
219 if ($question = $this->readquestion($currentquestion)) {
220 $questions[] = $question;
221 }
222 }
223
224 return $questions;
225 }
226
227
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();
172f6d95 239 $question->generalfeedback = '';
08892d5b 240 $question->correctfeedback = '';
241 $question->partiallycorrectfeedback = '';
242 $question->incorrectfeedback = '';
aca318e1 243
244 return $question;
245 }
246
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.
251
1e3d6fd8 252 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
253 echo "<p>$formatnotimplemented</p>";
aca318e1 254
255 return NULL;
256 }
257
258
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.
263
264 return true;
265 }
266
d08e16b2 267 function importimagefile( $path, $base64 ) {
268 /// imports an image file encoded in base64 format
269 /// This should not be overridden.
270 global $CFG;
271
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'] );
278
279 // detect and fix any filename collision - get unique filename
280 $newfiles = resolve_filename_collisions( $destination, array($file) );
281 $newfile = $newfiles[0];
282
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 );
295
296 // return the (possibly) new filename
297 return $newfile;
298 }
299
36e2232e 300//=================
aca318e1 301// Export functions
36e2232e 302//=================
aca318e1 303
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 }
310
08892d5b 311 function exportpreprocess() {
aca318e1 312 /// Does any pre-processing that may be desired
313
aca318e1 314 return true;
315 }
316
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 }
324
08892d5b 325 function exportprocess() {
aca318e1 326 /// Exports a given category. There's probably little need to change this
327
328 global $CFG;
329
330 // create a directory for the exports (if not already existing)
1367cb8d 331 if (! $export_dir = make_upload_directory($this->question_get_export_dir())) {
332 error( get_string('cannotcreatepath','quiz',$export_dir) );
aca318e1 333 }
1367cb8d 334 $path = $CFG->dataroot.'/'.$this->question_get_export_dir();
aca318e1 335
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 );
339
1e3d6fd8 340 notify( get_string('exportingquestions','quiz') );
aca318e1 341 if (!count($questions)) {
1e3d6fd8 342 notify( get_string('noquestions','quiz') );
343 return false;
aca318e1 344 }
345 $count = 0;
346
347 // results are first written into string (and then to a file)
348 // so create/initialize the string here
349 $expout = "";
350
351 // iterate through questions
352 foreach($questions as $question) {
a9b16aff 353 // do not export hidden questions
354 if (!empty($question->hidden)) {
355 continue;
356 }
357
358 // do not export random questions
359 if ($question->qtype==RANDOM) {
360 continue;
361 }
362
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 }
aca318e1 368
369 // final pre-process on exported data
370 $expout = $this->presave_process( $expout );
371
372 // write file
08892d5b 373 $filepath = $path."/".$this->filename . $this->export_file_extension();
aca318e1 374 if (!$fh=fopen($filepath,"w")) {
1e3d6fd8 375 error( get_string('cannotopen','quiz',$filepath) );
aca318e1 376 }
377 if (!fwrite($fh, $expout)) {
1e3d6fd8 378 error( get_string('cannotwrite','quiz',$filepath) );
aca318e1 379 }
380 fclose($fh);
381
382 return true;
383 }
384
385 function exportpostprocess() {
386 /// Does any post-processing that may be desired
387
388 return true;
389 }
390
391 function writequestion($question) {
392 /// Turns a question object into textual output in the given format
393 /// must be overidden
394
1e3d6fd8 395 // if not overidden, then this is an error.
396 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
397 echo "<p>$formatnotimplemented</p>";
aca318e1 398
399 return NULL;
400 }
401
1367cb8d 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 }
407
aca318e1 408}
409
410?>