Commit | Line | Data |
---|---|---|
aeb15530 | 1 | <?php |
d3603157 TH |
2 | |
3 | // This file is part of Moodle - http://moodle.org/ | |
4 | // | |
5 | // Moodle is free software: you can redistribute it and/or modify | |
6 | // it under the terms of the GNU General Public License as published by | |
7 | // the Free Software Foundation, either version 3 of the License, or | |
8 | // (at your option) any later version. | |
9 | // | |
10 | // Moodle is distributed in the hope that it will be useful, | |
11 | // but WITHOUT ANY WARRANTY; without even the implied warranty of | |
12 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
13 | // GNU General Public License for more details. | |
14 | // | |
15 | // You should have received a copy of the GNU General Public License | |
16 | // along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
17 | ||
18 | /** | |
19 | * Defines the base class for question import and export formats. | |
20 | * | |
21 | * @package moodlecore | |
22 | * @subpackage questionbank | |
23 | * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} | |
24 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
25 | */ | |
26 | ||
27 | ||
a17b297d TH |
28 | defined('MOODLE_INTERNAL') || die(); |
29 | ||
30 | ||
4323d029 | 31 | /** |
32 | * Base class for question import and export formats. | |
33 | * | |
d3603157 TH |
34 | * @copyright 1999 onwards Martin Dougiamas {@link http://moodle.com} |
35 | * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
4323d029 | 36 | */ |
f5565b69 | 37 | class qformat_default { |
aca318e1 | 38 | |
13bb604e TH |
39 | public $displayerrors = true; |
40 | public $category = NULL; | |
41 | public $questions = array(); | |
42 | public $course = NULL; | |
43 | public $filename = ''; | |
44 | public $realfilename = ''; | |
45 | public $matchgrades = 'error'; | |
46 | public $catfromfile = 0; | |
47 | public $contextfromfile = 0; | |
48 | public $cattofile = 0; | |
49 | public $contexttofile = 0; | |
50 | public $questionids = array(); | |
51 | public $importerrors = 0; | |
52 | public $stoponerror = true; | |
53 | public $translator = null; | |
54 | public $canaccessbackupdata = true; | |
271e6dec | 55 | |
cde2709a | 56 | protected $importcontext = null; |
aca318e1 | 57 | |
58 | // functions to indicate import/export functionality | |
59 | // override to return true if implemented | |
60 | ||
f7970e3c | 61 | /** @return bool whether this plugin provides import functionality. */ |
aca318e1 | 62 | function provide_import() { |
46732124 | 63 | return false; |
aca318e1 | 64 | } |
65 | ||
f7970e3c | 66 | /** @return bool whether this plugin provides export functionality. */ |
aca318e1 | 67 | function provide_export() { |
46732124 TH |
68 | return false; |
69 | } | |
70 | ||
71 | /** The string mime-type of the files that this plugin reads or writes. */ | |
72 | function mime_type() { | |
73 | return mimeinfo('type', $this->export_file_extension()); | |
74 | } | |
75 | ||
76 | /** | |
77 | * @return string the file extension (including .) that is normally used for | |
78 | * files handled by this plugin. | |
79 | */ | |
80 | function export_file_extension() { | |
81 | return '.txt'; | |
aca318e1 | 82 | } |
83 | ||
08892d5b | 84 | // Accessor methods |
aca318e1 | 85 | |
08892d5b | 86 | /** |
87 | * set the category | |
88 | * @param object category the category object | |
89 | */ | |
13bb604e TH |
90 | function setCategory($category) { |
91 | if (count($this->questions)) { | |
88bc20c3 | 92 | debugging('You shouldn\'t call setCategory after setQuestions'); |
93 | } | |
08892d5b | 94 | $this->category = $category; |
95 | } | |
aca318e1 | 96 | |
2c44a3d3 | 97 | /** |
98 | * Set the specific questions to export. Should not include questions with | |
99 | * parents (sub questions of cloze question type). | |
100 | * Only used for question export. | |
101 | * @param array of question objects | |
102 | */ | |
13bb604e TH |
103 | function setQuestions($questions) { |
104 | if ($this->category !== null) { | |
88bc20c3 | 105 | debugging('You shouldn\'t call setQuestions after setCategory'); |
106 | } | |
2c44a3d3 | 107 | $this->questions = $questions; |
108 | } | |
109 | ||
08892d5b | 110 | /** |
111 | * set the course class variable | |
112 | * @param course object Moodle course variable | |
113 | */ | |
13bb604e | 114 | function setCourse($course) { |
aca318e1 | 115 | $this->course = $course; |
08892d5b | 116 | } |
13bb604e | 117 | |
271e6dec | 118 | /** |
119 | * set an array of contexts. | |
120 | * @param array $contexts Moodle course variable | |
121 | */ | |
122 | function setContexts($contexts) { | |
123 | $this->contexts = $contexts; | |
124 | $this->translator = new context_to_string_translator($this->contexts); | |
125 | } | |
aca318e1 | 126 | |
08892d5b | 127 | /** |
128 | * set the filename | |
129 | * @param string filename name of file to import/export | |
130 | */ | |
13bb604e | 131 | function setFilename($filename) { |
08892d5b | 132 | $this->filename = $filename; |
133 | } | |
88bc20c3 | 134 | |
135 | /** | |
73b7b195 | 136 | * set the "real" filename |
137 | * (this is what the user typed, regardless of wha happened next) | |
138 | * @param string realfilename name of file as typed by user | |
139 | */ | |
13bb604e | 140 | function setRealfilename($realfilename) { |
88bc20c3 | 141 | $this->realfilename = $realfilename; |
142 | } | |
08892d5b | 143 | |
144 | /** | |
145 | * set matchgrades | |
146 | * @param string matchgrades error or nearest for grades | |
147 | */ | |
13bb604e | 148 | function setMatchgrades($matchgrades) { |
08892d5b | 149 | $this->matchgrades = $matchgrades; |
aca318e1 | 150 | } |
151 | ||
76f0a334 | 152 | /** |
08892d5b | 153 | * set catfromfile |
154 | * @param bool catfromfile allow categories embedded in import file | |
76f0a334 | 155 | */ |
13bb604e | 156 | function setCatfromfile($catfromfile) { |
08892d5b | 157 | $this->catfromfile = $catfromfile; |
158 | } | |
271e6dec | 159 | |
160 | /** | |
161 | * set contextfromfile | |
162 | * @param bool $contextfromfile allow contexts embedded in import file | |
163 | */ | |
164 | function setContextfromfile($contextfromfile) { | |
165 | $this->contextfromfile = $contextfromfile; | |
166 | } | |
167 | ||
f1abd39f | 168 | /** |
169 | * set cattofile | |
170 | * @param bool cattofile exports categories within export file | |
171 | */ | |
13bb604e | 172 | function setCattofile($cattofile) { |
f1abd39f | 173 | $this->cattofile = $cattofile; |
271e6dec | 174 | } |
13bb604e | 175 | |
271e6dec | 176 | /** |
177 | * set contexttofile | |
178 | * @param bool cattofile exports categories within export file | |
179 | */ | |
180 | function setContexttofile($contexttofile) { | |
181 | $this->contexttofile = $contexttofile; | |
182 | } | |
aca318e1 | 183 | |
f3701561 | 184 | /** |
185 | * set stoponerror | |
186 | * @param bool stoponerror stops database write if any errors reported | |
187 | */ | |
13bb604e | 188 | function setStoponerror($stoponerror) { |
f3701561 | 189 | $this->stoponerror = $stoponerror; |
190 | } | |
191 | ||
1b8b535d | 192 | /** |
f7970e3c | 193 | * @param bool $canaccess Whether the current use can access the backup data folder. Determines |
1b8b535d | 194 | * where export files are saved. |
195 | */ | |
196 | function set_can_access_backupdata($canaccess) { | |
197 | $this->canaccessbackupdata = $canaccess; | |
198 | } | |
199 | ||
f3701561 | 200 | /*********************** |
201 | * IMPORTING FUNCTIONS | |
202 | ***********************/ | |
203 | ||
204 | /** | |
205 | * Handle parsing error | |
206 | */ | |
13bb604e | 207 | function error($message, $text='', $questionname='') { |
cdeabc06 | 208 | $importerrorquestion = get_string('importerrorquestion','quiz'); |
209 | ||
f3701561 | 210 | echo "<div class=\"importerror\">\n"; |
cdeabc06 | 211 | echo "<strong>$importerrorquestion $questionname</strong>"; |
f3701561 | 212 | if (!empty($text)) { |
213 | $text = s($text); | |
214 | echo "<blockquote>$text</blockquote>\n"; | |
215 | } | |
216 | echo "<strong>$message</strong>\n"; | |
217 | echo "</div>"; | |
218 | ||
219 | $this->importerrors++; | |
220 | } | |
08892d5b | 221 | |
271e6dec | 222 | /** |
a41e3287 | 223 | * Import for questiontype plugins |
224 | * Do not override. | |
225 | * @param data mixed The segment of data containing the question | |
226 | * @param question object processed (so far) by standard import code if appropriate | |
227 | * @param extra mixed any additional format specific data that may be passed by the format | |
88bc20c3 | 228 | * @param qtypehint hint about a question type from format |
a41e3287 | 229 | * @return object question object suitable for save_options() or false if cannot handle |
230 | */ | |
49e2bba7 TH |
231 | function try_importing_using_qtypes($data, $question = null, $extra = null, |
232 | $qtypehint = '') { | |
a41e3287 | 233 | |
234 | // work out what format we are using | |
88bc20c3 | 235 | $formatname = substr(get_class($this), strlen('qformat_')); |
a41e3287 | 236 | $methodname = "import_from_$formatname"; |
237 | ||
88bc20c3 | 238 | //first try importing using a hint from format |
239 | if (!empty($qtypehint)) { | |
49e2bba7 | 240 | $qtype = question_bank::get_qtype($qtypehint, false); |
88bc20c3 | 241 | if (is_object($qtype) && method_exists($qtype, $methodname)) { |
242 | $question = $qtype->$methodname($data, $question, $this, $extra); | |
243 | if ($question) { | |
244 | return $question; | |
245 | } | |
246 | } | |
247 | } | |
248 | ||
a41e3287 | 249 | // loop through installed questiontypes checking for |
250 | // function to handle this question | |
49e2bba7 | 251 | foreach (question_bank::get_all_qtypes() as $qtype) { |
13bb604e TH |
252 | if (method_exists($qtype, $methodname)) { |
253 | if ($question = $qtype->$methodname($data, $question, $this, $extra)) { | |
a41e3287 | 254 | return $question; |
255 | } | |
256 | } | |
271e6dec | 257 | } |
258 | return false; | |
a41e3287 | 259 | } |
260 | ||
08892d5b | 261 | /** |
262 | * Perform any required pre-processing | |
f7970e3c | 263 | * @return bool success |
08892d5b | 264 | */ |
265 | function importpreprocess() { | |
266 | return true; | |
267 | } | |
268 | ||
269 | /** | |
270 | * Process the file | |
271 | * This method should not normally be overidden | |
49e2bba7 | 272 | * @param object $category |
f7970e3c | 273 | * @return bool success |
08892d5b | 274 | */ |
cde2709a | 275 | function importprocess($category) { |
4f290077 | 276 | global $USER, $CFG, $DB, $OUTPUT, $QTYPES; |
cde2709a DC |
277 | |
278 | $context = $category->context; | |
279 | $this->importcontext = $context; | |
f3701561 | 280 | |
9dd46039 TH |
281 | // reset the timer in case file upload was slow |
282 | set_time_limit(0); | |
67c12527 | 283 | |
9dd46039 | 284 | // STAGE 1: Parse the file |
f7c1dfaf | 285 | echo $OUTPUT->notification(get_string('parsingquestions','quiz'), 'notifysuccess'); |
271e6dec | 286 | |
08892d5b | 287 | if (! $lines = $this->readdata($this->filename)) { |
13bb604e | 288 | echo $OUTPUT->notification(get_string('cannotread','quiz')); |
aca318e1 | 289 | return false; |
290 | } | |
291 | ||
13bb604e TH |
292 | if (!$questions = $this->readquestions($lines, $context)) { // Extract all the questions |
293 | echo $OUTPUT->notification(get_string('noquestionsinfile','quiz')); | |
aca318e1 | 294 | return false; |
295 | } | |
296 | ||
f3701561 | 297 | // STAGE 2: Write data to database |
f7c1dfaf TH |
298 | echo $OUTPUT->notification(get_string('importingquestions', 'quiz', |
299 | $this->count_questions($questions)), 'notifysuccess'); | |
aca318e1 | 300 | |
f3701561 | 301 | // check for errors before we continue |
302 | if ($this->stoponerror and ($this->importerrors>0)) { | |
13bb604e | 303 | echo $OUTPUT->notification(get_string('importparseerror','quiz')); |
10b4a508 | 304 | return true; |
f3701561 | 305 | } |
306 | ||
76f0a334 | 307 | // get list of valid answer grades |
308 | $grades = get_grade_options(); | |
309 | $gradeoptionsfull = $grades->gradeoptionsfull; | |
310 | ||
c1828e0b | 311 | // check answer grades are valid |
312 | // (now need to do this here because of 'stop on error': MDL-10689) | |
313 | $gradeerrors = 0; | |
314 | $goodquestions = array(); | |
315 | foreach ($questions as $question) { | |
316 | if (!empty($question->fraction) and (is_array($question->fraction))) { | |
317 | $fractions = $question->fraction; | |
318 | $answersvalid = true; // in case they are! | |
319 | foreach ($fractions as $key => $fraction) { | |
320 | $newfraction = match_grade_options($gradeoptionsfull, $fraction, $this->matchgrades); | |
321 | if ($newfraction===false) { | |
322 | $answersvalid = false; | |
323 | } | |
324 | else { | |
325 | $fractions[$key] = $newfraction; | |
326 | } | |
327 | } | |
328 | if (!$answersvalid) { | |
fef8f84e | 329 | echo $OUTPUT->notification(get_string('matcherror', 'quiz')); |
c1828e0b | 330 | ++$gradeerrors; |
331 | continue; | |
332 | } | |
333 | else { | |
334 | $question->fraction = $fractions; | |
335 | } | |
336 | } | |
337 | $goodquestions[] = $question; | |
338 | } | |
339 | $questions = $goodquestions; | |
340 | ||
341 | // check for errors before we continue | |
342 | if ($this->stoponerror and ($gradeerrors>0)) { | |
343 | return false; | |
344 | } | |
345 | ||
346 | // count number of questions processed | |
aca318e1 | 347 | $count = 0; |
348 | ||
349 | foreach ($questions as $question) { // Process and store each question | |
08892d5b | 350 | |
271e6dec | 351 | // reset the php timeout |
49e2bba7 | 352 | set_time_limit(0); |
67c12527 | 353 | |
08892d5b | 354 | // check for category modifiers |
9dd46039 | 355 | if ($question->qtype == 'category') { |
08892d5b | 356 | if ($this->catfromfile) { |
357 | // find/create category object | |
40e71443 | 358 | $catpath = $question->category; |
728d60a1 | 359 | $newcategory = $this->create_category_path($catpath); |
08892d5b | 360 | if (!empty($newcategory)) { |
361 | $this->category = $newcategory; | |
362 | } | |
363 | } | |
271e6dec | 364 | continue; |
08892d5b | 365 | } |
cde2709a | 366 | $question->context = $context; |
08892d5b | 367 | |
aca318e1 | 368 | $count++; |
369 | ||
5b0dc681 | 370 | echo "<hr /><p><b>$count</b>. ".$this->format_question_text($question)."</p>"; |
aca318e1 | 371 | |
9dd46039 | 372 | $question->category = $this->category->id; |
aca318e1 | 373 | $question->stamp = make_unique_id_code(); // Set the unique code (not to be changed) |
aca318e1 | 374 | |
271e6dec | 375 | $question->createdby = $USER->id; |
376 | $question->timecreated = time(); | |
377 | ||
cde2709a DC |
378 | $question->id = $DB->insert_record('question', $question); |
379 | if (isset($question->questiontextfiles)) { | |
380 | foreach ($question->questiontextfiles as $file) { | |
381 | $QTYPES[$question->qtype]->import_file($context, 'question', 'questiontext', $question->id, $file); | |
382 | } | |
383 | } | |
384 | if (isset($question->generalfeedbackfiles)) { | |
385 | foreach ($question->generalfeedbackfiles as $file) { | |
386 | $QTYPES[$question->qtype]->import_file($context, 'question', 'generalfeedback', $question->id, $file); | |
387 | } | |
388 | } | |
aca318e1 | 389 | |
390 | $this->questionids[] = $question->id; | |
391 | ||
392 | // Now to save all the answers and type-specific options | |
393 | ||
cde2709a | 394 | $result = $QTYPES[$question->qtype]->save_question_options($question); |
aca318e1 | 395 | |
4f290077 TH |
396 | if (!empty($CFG->usetags) && isset($question->tags)) { |
397 | require_once($CFG->dirroot . '/tag/lib.php'); | |
398 | tag_set('question', $question->id, $question->tags); | |
399 | } | |
400 | ||
aca318e1 | 401 | if (!empty($result->error)) { |
fef8f84e | 402 | echo $OUTPUT->notification($result->error); |
aca318e1 | 403 | return false; |
404 | } | |
405 | ||
406 | if (!empty($result->notice)) { | |
fef8f84e | 407 | echo $OUTPUT->notification($result->notice); |
aca318e1 | 408 | return true; |
409 | } | |
cbe20043 | 410 | |
411 | // Give the question a unique version stamp determined by question_hash() | |
e5d7d1dc | 412 | $DB->set_field('question', 'version', question_hash($question), array('id'=>$question->id)); |
aca318e1 | 413 | } |
414 | return true; | |
415 | } | |
13bb604e | 416 | |
ce2df288 | 417 | /** |
418 | * Count all non-category questions in the questions array. | |
f34488b2 | 419 | * |
ce2df288 | 420 | * @param array questions An array of question objects. |
421 | * @return int The count. | |
f34488b2 | 422 | * |
ce2df288 | 423 | */ |
424 | function count_questions($questions) { | |
425 | $count = 0; | |
426 | if (!is_array($questions)) { | |
427 | return $count; | |
428 | } | |
429 | foreach ($questions as $question) { | |
430 | if (!is_object($question) || !isset($question->qtype) || ($question->qtype == 'category')) { | |
431 | continue; | |
432 | } | |
433 | $count++; | |
434 | } | |
435 | return $count; | |
436 | } | |
437 | ||
271e6dec | 438 | /** |
439 | * find and/or create the category described by a delimited list | |
440 | * e.g. $course$/tom/dick/harry or tom/dick/harry | |
441 | * | |
442 | * removes any context string no matter whether $getcontext is set | |
443 | * but if $getcontext is set then ignore the context and use selected category context. | |
444 | * | |
445 | * @param string catpath delimited category path | |
271e6dec | 446 | * @param int courseid course to search for categories |
447 | * @return mixed category object or null if fails | |
448 | */ | |
728d60a1 | 449 | function create_category_path($catpath) { |
f34488b2 | 450 | global $DB; |
728d60a1 | 451 | $catnames = $this->split_category_path($catpath); |
271e6dec | 452 | $parent = 0; |
453 | $category = null; | |
88bc20c3 | 454 | |
5ca9e32d | 455 | // check for context id in path, it might not be there in pre 1.9 exports |
456 | $matchcount = preg_match('/^\$([a-z]+)\$$/', $catnames[0], $matches); | |
49e2bba7 | 457 | if ($matchcount == 1) { |
271e6dec | 458 | $contextid = $this->translator->string_to_context($matches[1]); |
459 | array_shift($catnames); | |
460 | } else { | |
728d60a1 | 461 | $contextid = false; |
271e6dec | 462 | } |
728d60a1 TH |
463 | |
464 | if ($this->contextfromfile && $contextid !== false) { | |
271e6dec | 465 | $context = get_context_instance_by_id($contextid); |
466 | require_capability('moodle/question:add', $context); | |
467 | } else { | |
468 | $context = get_context_instance_by_id($this->category->contextid); | |
469 | } | |
728d60a1 TH |
470 | |
471 | // Now create any categories that need to be created. | |
271e6dec | 472 | foreach ($catnames as $catname) { |
13bb604e | 473 | if ($category = $DB->get_record('question_categories', array('name' => $catname, 'contextid' => $context->id, 'parent' => $parent))) { |
271e6dec | 474 | $parent = $category->id; |
475 | } else { | |
476 | require_capability('moodle/question:managecategory', $context); | |
477 | // create the new category | |
7f389342 | 478 | $category = new stdClass(); |
271e6dec | 479 | $category->contextid = $context->id; |
480 | $category->name = $catname; | |
481 | $category->info = ''; | |
482 | $category->parent = $parent; | |
483 | $category->sortorder = 999; | |
484 | $category->stamp = make_unique_id_code(); | |
bf8e93d7 | 485 | $id = $DB->insert_record('question_categories', $category); |
271e6dec | 486 | $category->id = $id; |
487 | $parent = $id; | |
488 | } | |
489 | } | |
490 | return $category; | |
491 | } | |
3f5633df | 492 | |
f3701561 | 493 | /** |
494 | * Return complete file within an array, one item per line | |
495 | * @param string filename name of file | |
496 | * @return mixed contents array or false on failure | |
497 | */ | |
aca318e1 | 498 | function readdata($filename) { |
aca318e1 | 499 | if (is_readable($filename)) { |
500 | $filearray = file($filename); | |
501 | ||
502 | /// Check for Macintosh OS line returns (ie file on one line), and fix | |
6dbcacee | 503 | if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) { |
aca318e1 | 504 | return explode("\r", $filearray[0]); |
505 | } else { | |
506 | return $filearray; | |
507 | } | |
508 | } | |
509 | return false; | |
510 | } | |
511 | ||
f3701561 | 512 | /** |
271e6dec | 513 | * Parses an array of lines into an array of questions, |
514 | * where each item is a question object as defined by | |
515 | * readquestion(). Questions are defined as anything | |
f3701561 | 516 | * between blank lines. |
517 | * | |
518 | * If your format does not use blank lines as a delimiter | |
519 | * then you will need to override this method. Even then | |
520 | * try to use readquestion for each question | |
521 | * @param array lines array of lines from readdata | |
cde2709a | 522 | * @param object $context |
f3701561 | 523 | * @return array array of question objects |
524 | */ | |
cde2709a | 525 | function readquestions($lines, $context) { |
271e6dec | 526 | |
aca318e1 | 527 | $questions = array(); |
528 | $currentquestion = array(); | |
529 | ||
530 | foreach ($lines as $line) { | |
531 | $line = trim($line); | |
532 | if (empty($line)) { | |
533 | if (!empty($currentquestion)) { | |
534 | if ($question = $this->readquestion($currentquestion)) { | |
535 | $questions[] = $question; | |
536 | } | |
537 | $currentquestion = array(); | |
538 | } | |
539 | } else { | |
540 | $currentquestion[] = $line; | |
541 | } | |
542 | } | |
543 | ||
544 | if (!empty($currentquestion)) { // There may be a final question | |
cde2709a | 545 | if ($question = $this->readquestion($currentquestion, $context)) { |
aca318e1 | 546 | $questions[] = $question; |
547 | } | |
548 | } | |
549 | ||
550 | return $questions; | |
551 | } | |
552 | ||
f3701561 | 553 | /** |
554 | * return an "empty" question | |
555 | * Somewhere to specify question parameters that are not handled | |
556 | * by import but are required db fields. | |
557 | * This should not be overridden. | |
558 | * @return object default question | |
271e6dec | 559 | */ |
aca318e1 | 560 | function defaultquestion() { |
b0679efa | 561 | global $CFG; |
0cd46577 | 562 | static $defaultshuffleanswers = null; |
563 | if (is_null($defaultshuffleanswers)) { | |
564 | $defaultshuffleanswers = get_config('quiz', 'shuffleanswers'); | |
565 | } | |
271e6dec | 566 | |
aca318e1 | 567 | $question = new stdClass(); |
0cd46577 | 568 | $question->shuffleanswers = $defaultshuffleanswers; |
49e2bba7 | 569 | $question->defaultmark = 1; |
aca318e1 | 570 | $question->image = ""; |
571 | $question->usecase = 0; | |
572 | $question->multiplier = array(); | |
172f6d95 | 573 | $question->generalfeedback = ''; |
08892d5b | 574 | $question->correctfeedback = ''; |
575 | $question->partiallycorrectfeedback = ''; | |
576 | $question->incorrectfeedback = ''; | |
5931ea94 | 577 | $question->answernumbering = 'abc'; |
49e2bba7 | 578 | $question->penalty = 0.3333333; |
3f5633df | 579 | $question->length = 1; |
aca318e1 | 580 | |
5fd8f999 | 581 | // this option in case the questiontypes class wants |
582 | // to know where the data came from | |
583 | $question->export_process = true; | |
093414d2 | 584 | $question->import_process = true; |
5fd8f999 | 585 | |
aca318e1 | 586 | return $question; |
587 | } | |
588 | ||
f3701561 | 589 | /** |
271e6dec | 590 | * Given the data known to define a question in |
591 | * this format, this function converts it into a question | |
f3701561 | 592 | * object suitable for processing and insertion into Moodle. |
593 | * | |
594 | * If your format does not use blank lines to delimit questions | |
595 | * (e.g. an XML format) you must override 'readquestions' too | |
596 | * @param $lines mixed data that represents question | |
597 | * @return object question object | |
598 | */ | |
aca318e1 | 599 | function readquestion($lines) { |
aca318e1 | 600 | |
13bb604e | 601 | $formatnotimplemented = get_string('formatnotimplemented','quiz'); |
1e3d6fd8 | 602 | echo "<p>$formatnotimplemented</p>"; |
aca318e1 | 603 | |
604 | return NULL; | |
605 | } | |
606 | ||
f3701561 | 607 | /** |
608 | * Override if any post-processing is required | |
f7970e3c | 609 | * @return bool success |
f3701561 | 610 | */ |
aca318e1 | 611 | function importpostprocess() { |
aca318e1 | 612 | return true; |
613 | } | |
614 | ||
3f5633df | 615 | |
f3701561 | 616 | /******************* |
617 | * EXPORT FUNCTIONS | |
618 | *******************/ | |
aca318e1 | 619 | |
271e6dec | 620 | /** |
a41e3287 | 621 | * Provide export functionality for plugin questiontypes |
622 | * Do not override | |
623 | * @param name questiontype name | |
271e6dec | 624 | * @param question object data to export |
a41e3287 | 625 | * @param extra mixed any addition format specific data needed |
626 | * @return string the data to append to export or false if error (or unhandled) | |
627 | */ | |
13bb604e | 628 | function try_exporting_using_qtypes($name, $question, $extra=null) { |
a41e3287 | 629 | global $QTYPES; |
630 | ||
631 | // work out the name of format in use | |
13bb604e | 632 | $formatname = substr(get_class($this), strlen('qformat_')); |
a41e3287 | 633 | $methodname = "export_to_$formatname"; |
634 | ||
13bb604e | 635 | if (array_key_exists($name, $QTYPES)) { |
a41e3287 | 636 | $qtype = $QTYPES[ $name ]; |
13bb604e TH |
637 | if (method_exists($qtype, $methodname)) { |
638 | if ($data = $qtype->$methodname($question, $this, $extra)) { | |
a41e3287 | 639 | return $data; |
640 | } | |
641 | } | |
642 | } | |
643 | return false; | |
644 | } | |
645 | ||
f3701561 | 646 | /** |
647 | * Do any pre-processing that may be required | |
f7970e3c | 648 | * @param bool success |
f3701561 | 649 | */ |
08892d5b | 650 | function exportpreprocess() { |
aca318e1 | 651 | return true; |
652 | } | |
653 | ||
f3701561 | 654 | /** |
655 | * Enable any processing to be done on the content | |
656 | * just prior to the file being saved | |
657 | * default is to do nothing | |
658 | * @param string output text | |
659 | * @param string processed output text | |
660 | */ | |
13bb604e | 661 | function presave_process($content) { |
aca318e1 | 662 | return $content; |
663 | } | |
664 | ||
f3701561 | 665 | /** |
666 | * Do the export | |
667 | * For most types this should not need to be overrided | |
cde2709a | 668 | * @return stored_file |
f3701561 | 669 | */ |
08892d5b | 670 | function exportprocess() { |
cde2709a | 671 | global $CFG, $OUTPUT, $DB, $USER; |
aca318e1 | 672 | |
673 | // get the questions (from database) in this category | |
674 | // only get q's with no parents (no cloze subquestions specifically) | |
cde2709a | 675 | if ($this->category) { |
13bb604e | 676 | $questions = get_questions_category($this->category, true); |
2c44a3d3 | 677 | } else { |
678 | $questions = $this->questions; | |
679 | } | |
aca318e1 | 680 | |
aca318e1 | 681 | $count = 0; |
682 | ||
683 | // results are first written into string (and then to a file) | |
684 | // so create/initialize the string here | |
685 | $expout = ""; | |
271e6dec | 686 | |
f1abd39f | 687 | // track which category questions are in |
688 | // if it changes we will record the category change in the output | |
689 | // file if selected. 0 means that it will get printed before the 1st question | |
690 | $trackcategory = 0; | |
aca318e1 | 691 | |
692 | // iterate through questions | |
693 | foreach($questions as $question) { | |
cde2709a DC |
694 | // used by file api |
695 | $contextid = $DB->get_field('question_categories', 'contextid', array('id'=>$question->category)); | |
696 | $question->contextid = $contextid; | |
271e6dec | 697 | |
a9b16aff | 698 | // do not export hidden questions |
699 | if (!empty($question->hidden)) { | |
700 | continue; | |
701 | } | |
702 | ||
703 | // do not export random questions | |
704 | if ($question->qtype==RANDOM) { | |
705 | continue; | |
706 | } | |
271e6dec | 707 | |
f1abd39f | 708 | // check if we need to record category change |
709 | if ($this->cattofile) { | |
710 | if ($question->category != $trackcategory) { | |
271e6dec | 711 | $trackcategory = $question->category; |
728d60a1 | 712 | $categoryname = $this->get_category_path($trackcategory, $this->contexttofile); |
271e6dec | 713 | |
f1abd39f | 714 | // create 'dummy' question for category export |
7f389342 | 715 | $dummyquestion = new stdClass(); |
f1abd39f | 716 | $dummyquestion->qtype = 'category'; |
717 | $dummyquestion->category = $categoryname; | |
728d60a1 | 718 | $dummyquestion->name = 'Switch category to ' . $categoryname; |
f1abd39f | 719 | $dummyquestion->id = 0; |
720 | $dummyquestion->questiontextformat = ''; | |
cde2709a | 721 | $dummyquestion->contextid = 0; |
728d60a1 | 722 | $expout .= $this->writequestion($dummyquestion) . "\n"; |
271e6dec | 723 | } |
724 | } | |
f1abd39f | 725 | |
726 | // export the question displaying message | |
727 | $count++; | |
cde2709a | 728 | |
cde2709a | 729 | if (question_has_capability_on($question, 'view', $question->category)) { |
cde2709a | 730 | $expout .= $this->writequestion($question, $contextid) . "\n"; |
0647e82b | 731 | } |
a9b16aff | 732 | } |
aca318e1 | 733 | |
2c6d2c88 | 734 | // continue path for following error checks |
735 | $course = $this->course; | |
2c44a3d3 | 736 | $continuepath = "$CFG->wwwroot/question/export.php?courseid=$course->id"; |
2c6d2c88 | 737 | |
738 | // did we actually process anything | |
739 | if ($count==0) { | |
49e2bba7 | 740 | print_error('noquestions', 'quiz', $continuepath); |
2c6d2c88 | 741 | } |
742 | ||
aca318e1 | 743 | // final pre-process on exported data |
cde2709a DC |
744 | $expout = $this->presave_process($expout); |
745 | return $expout; | |
aca318e1 | 746 | } |
3f5633df | 747 | |
271e6dec | 748 | /** |
749 | * get the category as a path (e.g., tom/dick/harry) | |
750 | * @param int id the id of the most nested catgory | |
271e6dec | 751 | * @return string the path |
752 | */ | |
728d60a1 | 753 | function get_category_path($id, $includecontext = true) { |
f34488b2 | 754 | global $DB; |
728d60a1 TH |
755 | |
756 | if (!$category = $DB->get_record('question_categories',array('id' =>$id))) { | |
1e7386c9 | 757 | print_error('cannotfindcategory', 'error', '', $id); |
271e6dec | 758 | } |
271e6dec | 759 | $contextstring = $this->translator->context_to_string($category->contextid); |
728d60a1 TH |
760 | |
761 | $pathsections = array(); | |
271e6dec | 762 | do { |
728d60a1 | 763 | $pathsections[] = $category->name; |
271e6dec | 764 | $id = $category->parent; |
13bb604e | 765 | } while ($category = $DB->get_record('question_categories', array('id' => $id))); |
271e6dec | 766 | |
13bb604e | 767 | if ($includecontext) { |
728d60a1 | 768 | $pathsections[] = '$' . $contextstring . '$'; |
271e6dec | 769 | } |
728d60a1 TH |
770 | |
771 | $path = $this->assemble_category_path(array_reverse($pathsections)); | |
772 | ||
271e6dec | 773 | return $path; |
774 | } | |
aca318e1 | 775 | |
728d60a1 TH |
776 | /** |
777 | * Convert a list of category names, possibly preceeded by one of the | |
778 | * context tokens like $course$, into a string representation of the | |
779 | * category path. | |
780 | * | |
781 | * Names are separated by / delimiters. And /s in the name are replaced by //. | |
782 | * | |
783 | * To reverse the process and split the paths into names, use | |
784 | * {@link split_category_path()}. | |
785 | * | |
786 | * @param array $names | |
787 | * @return string | |
788 | */ | |
789 | protected function assemble_category_path($names) { | |
790 | $escapednames = array(); | |
791 | foreach ($names as $name) { | |
792 | $escapedname = str_replace('/', '//', $name); | |
793 | if (substr($escapedname, 0, 1) == '/') { | |
794 | $escapedname = ' ' . $escapedname; | |
795 | } | |
796 | if (substr($escapedname, -1) == '/') { | |
797 | $escapedname = $escapedname . ' '; | |
798 | } | |
799 | $escapednames[] = $escapedname; | |
800 | } | |
801 | return implode('/', $escapednames); | |
802 | } | |
803 | ||
804 | /** | |
805 | * Convert a string, as returned by {@link assemble_category_path()}, | |
806 | * back into an array of category names. | |
807 | * | |
808 | * Each category name is cleaned by a call to clean_param(, PARAM_MULTILANG), | |
aab03169 | 809 | * which matches the cleaning in question/category_form.php. |
728d60a1 TH |
810 | * |
811 | * @param string $path | |
812 | * @return array of category names. | |
813 | */ | |
814 | protected function split_category_path($path) { | |
815 | $rawnames = preg_split('~(?<!/)/(?!/)~', $path); | |
816 | $names = array(); | |
817 | foreach ($rawnames as $rawname) { | |
818 | $names[] = clean_param(trim(str_replace('//', '/', $rawname)), PARAM_MULTILANG); | |
819 | } | |
820 | return $names; | |
821 | } | |
822 | ||
f3701561 | 823 | /** |
824 | * Do an post-processing that may be required | |
f7970e3c | 825 | * @return bool success |
f3701561 | 826 | */ |
aca318e1 | 827 | function exportpostprocess() { |
aca318e1 | 828 | return true; |
829 | } | |
830 | ||
f3701561 | 831 | /** |
832 | * convert a single question object into text output in the given | |
833 | * format. | |
834 | * This must be overriden | |
835 | * @param object question question object | |
836 | * @return mixed question export text or null if not implemented | |
837 | */ | |
aca318e1 | 838 | function writequestion($question) { |
1e3d6fd8 | 839 | // if not overidden, then this is an error. |
13bb604e | 840 | $formatnotimplemented = get_string('formatnotimplemented','quiz'); |
1e3d6fd8 | 841 | echo "<p>$formatnotimplemented</p>"; |
aca318e1 | 842 | return NULL; |
843 | } | |
844 | ||
f3701561 | 845 | /** |
271e6dec | 846 | * get directory into which export is going |
f3701561 | 847 | * @return string file path |
848 | */ | |
1367cb8d | 849 | function question_get_export_dir() { |
1b8b535d | 850 | global $USER; |
851 | if ($this->canaccessbackupdata) { | |
852 | $dirname = get_string("exportfilename","quiz"); | |
853 | $path = $this->course->id.'/backupdata/'.$dirname; // backupdata is protected directory | |
854 | } else { | |
855 | $path = 'temp/questionexport/' . $USER->id; | |
856 | } | |
1367cb8d | 857 | return $path; |
858 | } | |
859 | ||
f3701561 | 860 | /** |
13bb604e TH |
861 | * Convert the question text to plain text, so it can safely be displayed |
862 | * during import to let the user see roughly what is going on. | |
f3701561 | 863 | */ |
5b0dc681 | 864 | function format_question_text($question) { |
fe6ce234 | 865 | global $DB; |
0ff4bd08 | 866 | $formatoptions = new stdClass(); |
5b0dc681 | 867 | $formatoptions->noclean = true; |
868 | $formatoptions->para = false; | |
869 | if (empty($question->questiontextformat)) { | |
870 | $format = FORMAT_MOODLE; | |
871 | } else { | |
872 | $format = $question->questiontextformat; | |
873 | } | |
fe6ce234 | 874 | $text = $question->questiontext; |
13bb604e | 875 | return format_text(html_to_text($text, 0, false), $format, $formatoptions); |
5b0dc681 | 876 | } |
cde2709a DC |
877 | |
878 | /** | |
879 | * convert files into text output in the given format. | |
880 | * @param array | |
881 | * @param string encoding method | |
882 | * @return string $string | |
883 | */ | |
884 | function writefiles($files, $encoding='base64') { | |
885 | if (empty($files)) { | |
886 | return ''; | |
887 | } | |
888 | $string = ''; | |
889 | foreach ($files as $file) { | |
890 | if ($file->is_directory()) { | |
891 | continue; | |
892 | } | |
06f1bd03 | 893 | $string .= '<file name="' . $file->get_filename() . '" encoding="' . $encoding . '">'; |
cde2709a | 894 | $string .= base64_encode($file->get_content()); |
cde2709a DC |
895 | $string .= '</file>'; |
896 | } | |
897 | return $string; | |
898 | } | |
aca318e1 | 899 | } |