MDL-8898
[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();
21
22// functions to indicate import/export functionality
23// override to return true if implemented
24
25 function provide_import() {
26 return false;
27 }
28
29 function provide_export() {
30 return false;
31 }
32
08892d5b 33// Accessor methods
aca318e1 34
08892d5b 35 /**
36 * set the category
37 * @param object category the category object
38 */
39 function setCategory( $category ) {
40 $this->category = $category;
41 }
aca318e1 42
08892d5b 43 /**
44 * set the course class variable
45 * @param course object Moodle course variable
46 */
47 function setCourse( $course ) {
aca318e1 48 $this->course = $course;
08892d5b 49 }
aca318e1 50
08892d5b 51 /**
52 * set the filename
53 * @param string filename name of file to import/export
54 */
55 function setFilename( $filename ) {
56 $this->filename = $filename;
57 }
58
59 /**
60 * set matchgrades
61 * @param string matchgrades error or nearest for grades
62 */
63 function setMatchgrades( $matchgrades ) {
64 $this->matchgrades = $matchgrades;
aca318e1 65 }
66
76f0a334 67 /**
08892d5b 68 * set catfromfile
69 * @param bool catfromfile allow categories embedded in import file
76f0a334 70 */
08892d5b 71 function setCatfromfile( $catfromfile ) {
72 $this->catfromfile = $catfromfile;
73 }
f1abd39f 74
75 /**
76 * set cattofile
77 * @param bool cattofile exports categories within export file
78 */
79 function setCattofile( $cattofile ) {
80 $this->cattofile = $cattofile;
81 }
aca318e1 82
08892d5b 83/// Importing functions
84
85 /**
86 * Perform any required pre-processing
87 */
88 function importpreprocess() {
89 return true;
90 }
91
92 /**
93 * Process the file
94 * This method should not normally be overidden
95 */
96 function importprocess() {
97 if (! $lines = $this->readdata($this->filename)) {
1e3d6fd8 98 notify( get_string('cannotread','quiz') );
aca318e1 99 return false;
100 }
101
102 if (! $questions = $this->readquestions($lines)) { // Extract all the questions
1e3d6fd8 103 notify( get_string('noquestionsinfile','quiz') );
aca318e1 104 return false;
105 }
106
1e3d6fd8 107 notify( get_string('importingquestions','quiz',count($questions)) );
aca318e1 108
76f0a334 109 // get list of valid answer grades
110 $grades = get_grade_options();
111 $gradeoptionsfull = $grades->gradeoptionsfull;
112
aca318e1 113 $count = 0;
114
115 foreach ($questions as $question) { // Process and store each question
08892d5b 116
117 // check for category modifiers
118 if ($question->qtype=='category') {
119 if ($this->catfromfile) {
120 // find/create category object
121 $catpath = $question->category;
122 $newcategory = create_category_path( $catpath, '/', $this->course->id );
123 if (!empty($newcategory)) {
124 $this->category = $newcategory;
125 }
126 }
127 continue;
128 }
129
aca318e1 130 $count++;
131
5b0dc681 132 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
aca318e1 133
76f0a334 134 // check for answer grades validity (must match fixed list of grades)
08892d5b 135 if (!empty($question->fraction) and (is_array($question->fraction))) {
656ee8c6 136 $fractions = $question->fraction;
2d52056f 137 $answersvalid = true; // in case they are!
138 foreach ($fractions as $key => $fraction) {
08892d5b 139 $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades);
2d52056f 140 if ($newfraction===false) {
141 $answersvalid = false;
142 }
143 else {
144 $fractions[$key] = $newfraction;
145 }
146 }
147 if (!$answersvalid) {
148 notify( get_string('matcherror','quiz') );
149 continue;
76f0a334 150 }
151 else {
2d52056f 152 $question->fraction = $fractions;
76f0a334 153 }
154 }
76f0a334 155
aca318e1 156 $question->category = $this->category->id;
157 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
aca318e1 158
062f1125 159 if (!$question->id = insert_record("question", $question)) {
1e3d6fd8 160 error( get_string('cannotinsert','quiz') );
aca318e1 161 }
162
163 $this->questionids[] = $question->id;
164
165 // Now to save all the answers and type-specific options
166
167 global $QTYPES;
168 $result = $QTYPES[$question->qtype]
169 ->save_question_options($question);
170
171 if (!empty($result->error)) {
172 notify($result->error);
173 return false;
174 }
175
176 if (!empty($result->notice)) {
177 notify($result->notice);
178 return true;
179 }
cbe20043 180
181 // Give the question a unique version stamp determined by question_hash()
182 set_field('question', 'version', question_hash($question), 'id', $question->id);
aca318e1 183 }
184 return true;
185 }
186
187
188 function readdata($filename) {
189 /// Returns complete file with an array, one item per line
190
191 if (is_readable($filename)) {
192 $filearray = file($filename);
193
194 /// Check for Macintosh OS line returns (ie file on one line), and fix
195 if (ereg("\r", $filearray[0]) AND !ereg("\n", $filearray[0])) {
196 return explode("\r", $filearray[0]);
197 } else {
198 return $filearray;
199 }
200 }
201 return false;
202 }
203
204 function readquestions($lines) {
205 /// Parses an array of lines into an array of questions,
206 /// where each item is a question object as defined by
207 /// readquestion(). Questions are defined as anything
208 /// between blank lines.
209
210 $questions = array();
211 $currentquestion = array();
212
213 foreach ($lines as $line) {
214 $line = trim($line);
215 if (empty($line)) {
216 if (!empty($currentquestion)) {
217 if ($question = $this->readquestion($currentquestion)) {
218 $questions[] = $question;
219 }
220 $currentquestion = array();
221 }
222 } else {
223 $currentquestion[] = $line;
224 }
225 }
226
227 if (!empty($currentquestion)) { // There may be a final question
228 if ($question = $this->readquestion($currentquestion)) {
229 $questions[] = $question;
230 }
231 }
232
233 return $questions;
234 }
235
236
237 function defaultquestion() {
238 // returns an "empty" question
239 // Somewhere to specify question parameters that are not handled
240 // by import but are required db fields.
241 // This should not be overridden.
b0679efa 242 global $CFG;
243
aca318e1 244 $question = new stdClass();
b0679efa 245 $question->shuffleanswers = $CFG->quiz_shuffleanswers;
aca318e1 246 $question->defaultgrade = 1;
247 $question->image = "";
248 $question->usecase = 0;
249 $question->multiplier = array();
172f6d95 250 $question->generalfeedback = '';
08892d5b 251 $question->correctfeedback = '';
252 $question->partiallycorrectfeedback = '';
253 $question->incorrectfeedback = '';
aca318e1 254
255 return $question;
256 }
257
258 function readquestion($lines) {
259 /// Given an array of lines known to define a question in
260 /// this format, this function converts it into a question
261 /// object suitable for processing and insertion into Moodle.
262
1e3d6fd8 263 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
264 echo "<p>$formatnotimplemented</p>";
aca318e1 265
266 return NULL;
267 }
268
269
270 function importpostprocess() {
271 /// Does any post-processing that may be desired
272 /// Argument is a simple array of question ids that
273 /// have just been added.
274
275 return true;
276 }
277
d08e16b2 278 function importimagefile( $path, $base64 ) {
279 /// imports an image file encoded in base64 format
280 /// This should not be overridden.
281 global $CFG;
282
283 // all this to get the destination directory
284 // and filename!
285 $fullpath = "{$CFG->dataroot}/{$this->course->id}/$path";
286 $path_parts = pathinfo( $fullpath );
287 $destination = $path_parts['dirname'];
288 $file = clean_filename( $path_parts['basename'] );
289
290 // detect and fix any filename collision - get unique filename
291 $newfiles = resolve_filename_collisions( $destination, array($file) );
292 $newfile = $newfiles[0];
293
294 // convert and save file contents
295 if (!$content = base64_decode( $base64 )) {
296 return false;
297 }
298 $newfullpath = "$destination/$newfile";
299 if (!$fh = fopen( $newfullpath, 'w' )) {
300 return false;
301 }
302 if (!fwrite( $fh, $content )) {
303 return false;
304 }
305 fclose( $fh );
306
307 // return the (possibly) new filename
308 return $newfile;
309 }
310
36e2232e 311//=================
aca318e1 312// Export functions
36e2232e 313//=================
aca318e1 314
315 function export_file_extension() {
316 /// return the files extension appropriate for this type
317 /// override if you don't want .txt
318
319 return ".txt";
320 }
321
08892d5b 322 function exportpreprocess() {
aca318e1 323 /// Does any pre-processing that may be desired
324
aca318e1 325 return true;
326 }
327
328 function presave_process( $content ) {
329 /// enables any processing to be done on the content
330 /// just prior to the file being saved
331 /// default is to do nothing
332
333 return $content;
334 }
335
08892d5b 336 function exportprocess() {
aca318e1 337 /// Exports a given category. There's probably little need to change this
338
339 global $CFG;
340
341 // create a directory for the exports (if not already existing)
1367cb8d 342 if (! $export_dir = make_upload_directory($this->question_get_export_dir())) {
343 error( get_string('cannotcreatepath','quiz',$export_dir) );
aca318e1 344 }
1367cb8d 345 $path = $CFG->dataroot.'/'.$this->question_get_export_dir();
aca318e1 346
347 // get the questions (from database) in this category
348 // only get q's with no parents (no cloze subquestions specifically)
349 $questions = get_questions_category( $this->category, true );
350
1e3d6fd8 351 notify( get_string('exportingquestions','quiz') );
aca318e1 352 if (!count($questions)) {
1e3d6fd8 353 notify( get_string('noquestions','quiz') );
354 return false;
aca318e1 355 }
356 $count = 0;
357
358 // results are first written into string (and then to a file)
359 // so create/initialize the string here
360 $expout = "";
f1abd39f 361
362 // track which category questions are in
363 // if it changes we will record the category change in the output
364 // file if selected. 0 means that it will get printed before the 1st question
365 $trackcategory = 0;
aca318e1 366
367 // iterate through questions
368 foreach($questions as $question) {
f1abd39f 369
a9b16aff 370 // do not export hidden questions
371 if (!empty($question->hidden)) {
372 continue;
373 }
374
375 // do not export random questions
376 if ($question->qtype==RANDOM) {
377 continue;
378 }
f1abd39f 379
380 // check if we need to record category change
381 if ($this->cattofile) {
382 if ($question->category != $trackcategory) {
383 $trackcategory = $question->category;
384 $categoryname = get_category_path( $trackcategory );
385
386 // create 'dummy' question for category export
387 $dummyquestion = new object;
388 $dummyquestion->qtype = 'category';
389 $dummyquestion->category = $categoryname;
390 $dummyquestion->name = "switch category to $categoryname";
391 $dummyquestion->id = 0;
392 $dummyquestion->questiontextformat = '';
393 $expout .= $this->writequestion( $dummyquestion ) . "\n";
394 }
395 }
396
397 // export the question displaying message
398 $count++;
5b0dc681 399 echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>";
f1abd39f 400 $expout .= $this->writequestion( $question ) . "\n";
a9b16aff 401 }
aca318e1 402
403 // final pre-process on exported data
404 $expout = $this->presave_process( $expout );
f1abd39f 405
aca318e1 406 // write file
08892d5b 407 $filepath = $path."/".$this->filename . $this->export_file_extension();
aca318e1 408 if (!$fh=fopen($filepath,"w")) {
1e3d6fd8 409 error( get_string('cannotopen','quiz',$filepath) );
aca318e1 410 }
f1abd39f 411 if (!fwrite($fh, $expout, strlen($expout) )) {
1e3d6fd8 412 error( get_string('cannotwrite','quiz',$filepath) );
aca318e1 413 }
414 fclose($fh);
aca318e1 415 return true;
416 }
417
418 function exportpostprocess() {
419 /// Does any post-processing that may be desired
420
421 return true;
422 }
423
424 function writequestion($question) {
425 /// Turns a question object into textual output in the given format
426 /// must be overidden
427
1e3d6fd8 428 // if not overidden, then this is an error.
429 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
430 echo "<p>$formatnotimplemented</p>";
aca318e1 431
432 return NULL;
433 }
434
1367cb8d 435 function question_get_export_dir() {
436 $dirname = get_string("exportfilename","quiz");
437 $path = $this->course->id.'/backupdata/'.$dirname; // backupdata is protected directory
438 return $path;
439 }
440
5b0dc681 441 function format_question_text($question) {
442 $formatoptions = new stdClass;
443 $formatoptions->noclean = true;
444 $formatoptions->para = false;
445 if (empty($question->questiontextformat)) {
446 $format = FORMAT_MOODLE;
447 } else {
448 $format = $question->questiontextformat;
449 }
450 return format_text(stripslashes($question->questiontext), $format, $formatoptions);
451 }
aca318e1 452}
453
454?>