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