MDL-8450\rAdded support for including question categories in a quiz export.
[moodle.git] / question / format.php
CommitLineData
b0679efa 1
aca318e1 2<?php // $Id$
3
4////////////////////////////////////////////////////////////////////
5/// format.php - Default format class for file imports/exports. //
6/// //
7/// Doesn't do everything on it's own -- it needs to be extended. //
8////////////////////////////////////////////////////////////////////
9
10// Included by import.php and export.php
11
f5565b69 12class qformat_default {
aca318e1 13
08892d5b 14 // var $displayerrors = true;
aca318e1 15 var $category = NULL;
16 var $course = NULL;
08892d5b 17 var $filename = '';
18 var $matchgrades = 'error';
19 var $catfromfile = 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 }
aca318e1 74
08892d5b 75/// Importing functions
76
77 /**
78 * Perform any required pre-processing
79 */
80 function importpreprocess() {
81 return true;
82 }
83
84 /**
85 * Process the file
86 * This method should not normally be overidden
87 */
88 function importprocess() {
89 if (! $lines = $this->readdata($this->filename)) {
1e3d6fd8 90 notify( get_string('cannotread','quiz') );
aca318e1 91 return false;
92 }
93
94 if (! $questions = $this->readquestions($lines)) { // Extract all the questions
1e3d6fd8 95 notify( get_string('noquestionsinfile','quiz') );
aca318e1 96 return false;
97 }
98
1e3d6fd8 99 notify( get_string('importingquestions','quiz',count($questions)) );
aca318e1 100
76f0a334 101 // get list of valid answer grades
102 $grades = get_grade_options();
103 $gradeoptionsfull = $grades->gradeoptionsfull;
104
aca318e1 105 $count = 0;
106
107 foreach ($questions as $question) { // Process and store each question
08892d5b 108
109 // check for category modifiers
110 if ($question->qtype=='category') {
111 if ($this->catfromfile) {
112 // find/create category object
113 $catpath = $question->category;
114 $newcategory = create_category_path( $catpath, '/', $this->course->id );
115 if (!empty($newcategory)) {
116 $this->category = $newcategory;
117 }
118 }
119 continue;
120 }
121
aca318e1 122 $count++;
123
124 echo "<hr /><p><b>$count</b>. ".stripslashes($question->questiontext)."</p>";
125
76f0a334 126 // check for answer grades validity (must match fixed list of grades)
08892d5b 127 if (!empty($question->fraction) and (is_array($question->fraction))) {
656ee8c6 128 $fractions = $question->fraction;
2d52056f 129 $answersvalid = true; // in case they are!
130 foreach ($fractions as $key => $fraction) {
08892d5b 131 $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades);
2d52056f 132 if ($newfraction===false) {
133 $answersvalid = false;
134 }
135 else {
136 $fractions[$key] = $newfraction;
137 }
138 }
139 if (!$answersvalid) {
140 notify( get_string('matcherror','quiz') );
141 continue;
76f0a334 142 }
143 else {
2d52056f 144 $question->fraction = $fractions;
76f0a334 145 }
146 }
76f0a334 147
aca318e1 148 $question->category = $this->category->id;
149 $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed)
aca318e1 150
062f1125 151 if (!$question->id = insert_record("question", $question)) {
1e3d6fd8 152 error( get_string('cannotinsert','quiz') );
aca318e1 153 }
154
155 $this->questionids[] = $question->id;
156
157 // Now to save all the answers and type-specific options
158
159 global $QTYPES;
160 $result = $QTYPES[$question->qtype]
161 ->save_question_options($question);
162
163 if (!empty($result->error)) {
164 notify($result->error);
165 return false;
166 }
167
168 if (!empty($result->notice)) {
169 notify($result->notice);
170 return true;
171 }
cbe20043 172
173 // Give the question a unique version stamp determined by question_hash()
174 set_field('question', 'version', question_hash($question), 'id', $question->id);
aca318e1 175 }
176 return true;
177 }
178
179
180 function readdata($filename) {
181 /// Returns complete file with an array, one item per line
182
183 if (is_readable($filename)) {
184 $filearray = file($filename);
185
186 /// Check for Macintosh OS line returns (ie file on one line), and fix
187 if (ereg("\r", $filearray[0]) AND !ereg("\n", $filearray[0])) {
188 return explode("\r", $filearray[0]);
189 } else {
190 return $filearray;
191 }
192 }
193 return false;
194 }
195
196 function readquestions($lines) {
197 /// Parses an array of lines into an array of questions,
198 /// where each item is a question object as defined by
199 /// readquestion(). Questions are defined as anything
200 /// between blank lines.
201
202 $questions = array();
203 $currentquestion = array();
204
205 foreach ($lines as $line) {
206 $line = trim($line);
207 if (empty($line)) {
208 if (!empty($currentquestion)) {
209 if ($question = $this->readquestion($currentquestion)) {
210 $questions[] = $question;
211 }
212 $currentquestion = array();
213 }
214 } else {
215 $currentquestion[] = $line;
216 }
217 }
218
219 if (!empty($currentquestion)) { // There may be a final question
220 if ($question = $this->readquestion($currentquestion)) {
221 $questions[] = $question;
222 }
223 }
224
225 return $questions;
226 }
227
228
229 function defaultquestion() {
230 // returns an "empty" question
231 // Somewhere to specify question parameters that are not handled
232 // by import but are required db fields.
233 // This should not be overridden.
b0679efa 234 global $CFG;
235
aca318e1 236 $question = new stdClass();
b0679efa 237 $question->shuffleanswers = $CFG->quiz_shuffleanswers;
aca318e1 238 $question->defaultgrade = 1;
239 $question->image = "";
240 $question->usecase = 0;
241 $question->multiplier = array();
172f6d95 242 $question->generalfeedback = '';
08892d5b 243 $question->correctfeedback = '';
244 $question->partiallycorrectfeedback = '';
245 $question->incorrectfeedback = '';
aca318e1 246
247 return $question;
248 }
249
250 function readquestion($lines) {
251 /// Given an array of lines known to define a question in
252 /// this format, this function converts it into a question
253 /// object suitable for processing and insertion into Moodle.
254
1e3d6fd8 255 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
256 echo "<p>$formatnotimplemented</p>";
aca318e1 257
258 return NULL;
259 }
260
261
262 function importpostprocess() {
263 /// Does any post-processing that may be desired
264 /// Argument is a simple array of question ids that
265 /// have just been added.
266
267 return true;
268 }
269
d08e16b2 270 function importimagefile( $path, $base64 ) {
271 /// imports an image file encoded in base64 format
272 /// This should not be overridden.
273 global $CFG;
274
275 // all this to get the destination directory
276 // and filename!
277 $fullpath = "{$CFG->dataroot}/{$this->course->id}/$path";
278 $path_parts = pathinfo( $fullpath );
279 $destination = $path_parts['dirname'];
280 $file = clean_filename( $path_parts['basename'] );
281
282 // detect and fix any filename collision - get unique filename
283 $newfiles = resolve_filename_collisions( $destination, array($file) );
284 $newfile = $newfiles[0];
285
286 // convert and save file contents
287 if (!$content = base64_decode( $base64 )) {
288 return false;
289 }
290 $newfullpath = "$destination/$newfile";
291 if (!$fh = fopen( $newfullpath, 'w' )) {
292 return false;
293 }
294 if (!fwrite( $fh, $content )) {
295 return false;
296 }
297 fclose( $fh );
298
299 // return the (possibly) new filename
300 return $newfile;
301 }
302
36e2232e 303//=================
aca318e1 304// Export functions
36e2232e 305//=================
aca318e1 306
307 function export_file_extension() {
308 /// return the files extension appropriate for this type
309 /// override if you don't want .txt
310
311 return ".txt";
312 }
313
08892d5b 314 function exportpreprocess() {
aca318e1 315 /// Does any pre-processing that may be desired
316
aca318e1 317 return true;
318 }
319
320 function presave_process( $content ) {
321 /// enables any processing to be done on the content
322 /// just prior to the file being saved
323 /// default is to do nothing
324
325 return $content;
326 }
327
08892d5b 328 function exportprocess() {
aca318e1 329 /// Exports a given category. There's probably little need to change this
330
331 global $CFG;
332
333 // create a directory for the exports (if not already existing)
1367cb8d 334 if (! $export_dir = make_upload_directory($this->question_get_export_dir())) {
335 error( get_string('cannotcreatepath','quiz',$export_dir) );
aca318e1 336 }
1367cb8d 337 $path = $CFG->dataroot.'/'.$this->question_get_export_dir();
aca318e1 338
339 // get the questions (from database) in this category
340 // only get q's with no parents (no cloze subquestions specifically)
341 $questions = get_questions_category( $this->category, true );
342
1e3d6fd8 343 notify( get_string('exportingquestions','quiz') );
aca318e1 344 if (!count($questions)) {
1e3d6fd8 345 notify( get_string('noquestions','quiz') );
346 return false;
aca318e1 347 }
348 $count = 0;
349
350 // results are first written into string (and then to a file)
351 // so create/initialize the string here
352 $expout = "";
353
354 // iterate through questions
355 foreach($questions as $question) {
a9b16aff 356 // do not export hidden questions
357 if (!empty($question->hidden)) {
358 continue;
359 }
360
361 // do not export random questions
362 if ($question->qtype==RANDOM) {
363 continue;
364 }
365
366 // export the question displaying message
367 $count++;
368 echo "<hr /><p><b>$count</b>. ".stripslashes($question->questiontext)."</p>";
369 $expout .= $this->writequestion( $question ) . "\n";
370 }
aca318e1 371
372 // final pre-process on exported data
373 $expout = $this->presave_process( $expout );
374
375 // write file
08892d5b 376 $filepath = $path."/".$this->filename . $this->export_file_extension();
aca318e1 377 if (!$fh=fopen($filepath,"w")) {
1e3d6fd8 378 error( get_string('cannotopen','quiz',$filepath) );
aca318e1 379 }
380 if (!fwrite($fh, $expout)) {
1e3d6fd8 381 error( get_string('cannotwrite','quiz',$filepath) );
aca318e1 382 }
383 fclose($fh);
384
385 return true;
386 }
387
388 function exportpostprocess() {
389 /// Does any post-processing that may be desired
390
391 return true;
392 }
393
394 function writequestion($question) {
395 /// Turns a question object into textual output in the given format
396 /// must be overidden
397
1e3d6fd8 398 // if not overidden, then this is an error.
399 $formatnotimplemented = get_string( 'formatnotimplemented','quiz' );
400 echo "<p>$formatnotimplemented</p>";
aca318e1 401
402 return NULL;
403 }
404
1367cb8d 405 function question_get_export_dir() {
406 $dirname = get_string("exportfilename","quiz");
407 $path = $this->course->id.'/backupdata/'.$dirname; // backupdata is protected directory
408 return $path;
409 }
410
aca318e1 411}
412
413?>