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