84769fd8 |
1 | <?php // $Id$ |
2 | // |
3 | /////////////////////////////////////////////////////////////// |
4 | // XML import/export |
5 | // |
6 | ////////////////////////////////////////////////////////////////////////// |
7 | // Based on default.php, included by ../import.php |
41a89a07 |
8 | /** |
9 | * @package questionbank |
10 | * @subpackage importexport |
11 | */ |
84769fd8 |
12 | require_once( "$CFG->libdir/xmlize.php" ); |
13 | |
f5565b69 |
14 | class qformat_xml extends qformat_default { |
84769fd8 |
15 | |
16 | function provide_import() { |
17 | return true; |
18 | } |
19 | |
20 | function provide_export() { |
21 | return true; |
22 | } |
23 | |
24 | // IMPORT FUNCTIONS START HERE |
25 | |
6e557c08 |
26 | /** |
c81415c7 |
27 | * Translate human readable format name |
28 | * into internal Moodle code number |
6e557c08 |
29 | * @param string name format name from xml file |
30 | * @return int Moodle format code |
c81415c7 |
31 | */ |
84769fd8 |
32 | function trans_format( $name ) { |
84769fd8 |
33 | $name = trim($name); |
34 | |
35 | if ($name=='moodle_auto_format') { |
36 | $id = 0; |
37 | } |
38 | elseif ($name=='html') { |
39 | $id = 1; |
40 | } |
41 | elseif ($name=='plain_text') { |
42 | $id = 2; |
43 | } |
44 | elseif ($name=='wiki_like') { |
45 | $id = 3; |
46 | } |
47 | elseif ($name=='markdown') { |
48 | $id = 4; |
49 | } |
50 | else { |
51 | $id = 0; // or maybe warning required |
52 | } |
53 | return $id; |
54 | } |
55 | |
6e557c08 |
56 | /** |
c81415c7 |
57 | * Translate human readable single answer option |
58 | * to internal code number |
6e557c08 |
59 | * @param string name true/false |
60 | * @return int internal code number |
c81415c7 |
61 | */ |
84769fd8 |
62 | function trans_single( $name ) { |
2da44816 |
63 | $name = trim($name); |
64 | if ($name == "false" || !$name) { |
65 | return 0; |
66 | } else { |
67 | return 1; |
68 | } |
84769fd8 |
69 | } |
70 | |
6e557c08 |
71 | /** |
c81415c7 |
72 | * process text string from xml file |
6e557c08 |
73 | * @param array $text bit of xml tree after ['text'] |
74 | * @return string processed text |
c81415c7 |
75 | */ |
84769fd8 |
76 | function import_text( $text ) { |
17102269 |
77 | // quick sanity check |
78 | if (empty($text)) { |
79 | return ''; |
80 | } |
84769fd8 |
81 | $data = $text[0]['#']; |
294ce987 |
82 | return trim($data); |
84769fd8 |
83 | } |
84 | |
46013523 |
85 | /** |
86 | * return the value of a node, given a path to the node |
87 | * if it doesn't exist return the default value |
88 | * @param array xml data to read |
89 | * @param array path path to node expressed as array |
90 | * @param mixed default |
91 | * @param bool istext process as text |
92 | * @param string error if set value must exist, return false and issue message if not |
93 | * @return mixed value |
94 | */ |
95 | function getpath( $xml, $path, $default, $istext=false, $error='' ) { |
96 | foreach ($path as $index) { |
228b6f6b |
97 | if (!isset($xml[$index])) { |
46013523 |
98 | if (!empty($error)) { |
99 | $this->error( $error ); |
100 | return false; |
101 | } else { |
102 | return $default; |
103 | } |
104 | } |
105 | else $xml = $xml[$index]; |
106 | } |
107 | if ($istext) { |
fc22da99 |
108 | if (!is_string($xml)) { |
c95defa9 |
109 | $this->error( get_string('invalidxml','qformat_xml') ); |
fc22da99 |
110 | } |
294ce987 |
111 | $xml = trim( $xml ); |
46013523 |
112 | } |
113 | |
114 | return $xml; |
115 | } |
116 | |
117 | |
6e557c08 |
118 | /** |
c81415c7 |
119 | * import parts of question common to all types |
e7ef42f5 |
120 | * @param $question array question question array from xml tree |
6e557c08 |
121 | * @return object question object |
c81415c7 |
122 | */ |
84769fd8 |
123 | function import_headers( $question ) { |
46013523 |
124 | // get some error strings |
125 | $error_noname = get_string( 'xmlimportnoname','quiz' ); |
126 | $error_noquestion = get_string( 'xmlimportnoquestion','quiz' ); |
127 | |
84769fd8 |
128 | // this routine initialises the question object |
5bed54e1 |
129 | $qo = $this->defaultquestion(); |
84769fd8 |
130 | |
46013523 |
131 | // question name |
132 | $qo->name = $this->getpath( $question, array('#','name',0,'#','text',0,'#'), '', true, $error_noname ); |
6af98025 |
133 | $qo->questiontext = $this->getpath( $question, array('#','questiontext',0,'#','text',0,'#'), '', true ); |
46013523 |
134 | $qo->questiontextformat = $this->getpath( $question, array('#','questiontext',0,'@','format'), '' ); |
e7ef42f5 |
135 | $qo->image = $this->getpath( $question, array('#','image',0,'#'), $qo->image ); |
46013523 |
136 | $image_base64 = $this->getpath( $question, array('#','image_base64','0','#'),'' ); |
137 | if (!empty($image_base64)) { |
cdfaa838 |
138 | $qo->image = $this->importimagefile( $qo->image, $image_base64); |
46013523 |
139 | } |
140 | $qo->generalfeedback = $this->getpath( $question, array('#','generalfeedback',0,'#','text',0,'#'), $qo->generalfeedback, true ); |
141 | $qo->defaultgrade = $this->getpath( $question, array('#','defaultgrade',0,'#'), $qo->defaultgrade ); |
142 | $qo->penalty = $this->getpath( $question, array('#','penalty',0,'#'), $qo->penalty ); |
84769fd8 |
143 | |
144 | return $qo; |
145 | } |
146 | |
6e557c08 |
147 | /** |
c81415c7 |
148 | * import the common parts of a single answer |
6e557c08 |
149 | * @param array answer xml tree for single answer |
150 | * @return object answer object |
c81415c7 |
151 | */ |
84769fd8 |
152 | function import_answer( $answer ) { |
81a7a02b |
153 | $fraction = $this->getpath( $answer, array('@','fraction'),0 ); |
154 | $text = $this->getpath( $answer, array('#','text',0,'#'), '', true ); |
155 | $feedback = $this->getpath( $answer, array('#','feedback',0,'#','text',0,'#'), '', true ); |
84769fd8 |
156 | |
157 | $ans = null; |
158 | $ans->answer = $text; |
159 | $ans->fraction = $fraction / 100; |
160 | $ans->feedback = $feedback; |
84769fd8 |
161 | return $ans; |
162 | } |
163 | |
6e557c08 |
164 | /** |
c81415c7 |
165 | * import multiple choice question |
6e557c08 |
166 | * @param array question question array from xml tree |
167 | * @return object question object |
c81415c7 |
168 | */ |
84769fd8 |
169 | function import_multichoice( $question ) { |
84769fd8 |
170 | // get common parts |
171 | $qo = $this->import_headers( $question ); |
172 | |
173 | // 'header' parts particular to multichoice |
174 | $qo->qtype = MULTICHOICE; |
81a7a02b |
175 | $single = $this->getpath( $question, array('#','single',0,'#'), 'true' ); |
84769fd8 |
176 | $qo->single = $this->trans_single( $single ); |
81a7a02b |
177 | $shuffleanswers = $this->getpath( $question, array('#','shuffleanswers',0,'#'), 'false' ); |
2377f8dc |
178 | $qo->answernumbering = $this->getpath( $question, array('#','answernumbering',0,'#'), 'abc' ); |
2da44816 |
179 | $qo->shuffleanswers = $this->trans_single($shuffleanswers); |
81a7a02b |
180 | $qo->correctfeedback = $this->getpath( $question, array('#','correctfeedback',0,'#','text',0,'#'), '', true ); |
181 | $qo->partiallycorrectfeedback = $this->getpath( $question, array('#','partiallycorrectfeedback',0,'#','text',0,'#'), '', true ); |
182 | $qo->incorrectfeedback = $this->getpath( $question, array('#','incorrectfeedback',0,'#','text',0,'#'), '', true ); |
c0ffeb39 |
183 | |
184 | // There was a time on the 1.8 branch when it could output an empty answernumbering tag, so fix up any found. |
185 | if (empty($qo->answernumbering)) { |
186 | $qo->answernumbering = 'abc'; |
187 | } |
188 | |
84769fd8 |
189 | // run through the answers |
190 | $answers = $question['#']['answer']; |
191 | $a_count = 0; |
192 | foreach ($answers as $answer) { |
193 | $ans = $this->import_answer( $answer ); |
194 | $qo->answer[$a_count] = $ans->answer; |
195 | $qo->fraction[$a_count] = $ans->fraction; |
196 | $qo->feedback[$a_count] = $ans->feedback; |
197 | ++$a_count; |
198 | } |
84769fd8 |
199 | return $qo; |
200 | } |
201 | |
6e557c08 |
202 | /** |
c81415c7 |
203 | * import cloze type question |
6e557c08 |
204 | * @param array question question array from xml tree |
205 | * @return object question object |
c81415c7 |
206 | */ |
7b8bc256 |
207 | function import_multianswer( $questions ) { |
0c6b4d2e |
208 | $questiontext = $questions['#']['questiontext'][0]['#']['text']; |
209 | $qo = qtype_multianswer_extract_question($this->import_text($questiontext)); |
7b8bc256 |
210 | |
211 | // 'header' parts particular to multianswer |
212 | $qo->qtype = MULTIANSWER; |
213 | $qo->course = $this->course; |
4ab4dcb2 |
214 | $qo->generalfeedback = $this->getpath( $questions, array('#','generalfeedback',0,'#','text',0,'#'), '', true ); |
7b8bc256 |
215 | |
71ffbac2 |
216 | if (!empty($questions)) { |
7b8bc256 |
217 | $qo->name = $this->import_text( $questions['#']['name'][0]['#']['text'] ); |
218 | } |
219 | |
220 | return $qo; |
221 | } |
222 | |
6e557c08 |
223 | /** |
c81415c7 |
224 | * import true/false type question |
6e557c08 |
225 | * @param array question question array from xml tree |
226 | * @return object question object |
c81415c7 |
227 | */ |
84769fd8 |
228 | function import_truefalse( $question ) { |
84769fd8 |
229 | // get common parts |
fef8f84e |
230 | global $OUTPUT; |
84769fd8 |
231 | $qo = $this->import_headers( $question ); |
232 | |
233 | // 'header' parts particular to true/false |
234 | $qo->qtype = TRUEFALSE; |
235 | |
236 | // get answer info |
3246ed33 |
237 | // |
238 | // In the past, it used to be assumed that the two answers were in the file |
239 | // true first, then false. Howevever that was not always true. Now, we |
240 | // try to match on the answer text, but in old exports, this will be a localised |
241 | // string, so if we don't find true or false, we fall back to the old system. |
242 | $first = true; |
243 | $warning = false; |
244 | foreach ($question['#']['answer'] as $answer) { |
81a7a02b |
245 | $answertext = $this->getpath( $answer, array('#','text',0,'#'), '', true ); |
246 | $feedback = $this->getpath($answer, array('#','feedback',0,'#','text',0,'#'), '', true ); |
3246ed33 |
247 | if ($answertext != 'true' && $answertext != 'false') { |
248 | $warning = true; |
249 | $answertext = $first ? 'true' : 'false'; // Old style file, assume order is true/false. |
250 | } |
251 | if ($answertext == 'true') { |
252 | $qo->answer = ($answer['@']['fraction'] == 100); |
7939a4a0 |
253 | $qo->correctanswer = $qo->answer; |
3246ed33 |
254 | $qo->feedbacktrue = $feedback; |
255 | } else { |
256 | $qo->answer = ($answer['@']['fraction'] != 100); |
7939a4a0 |
257 | $qo->correctanswer = $qo->answer; |
3246ed33 |
258 | $qo->feedbackfalse = $feedback; |
259 | } |
260 | $first = false; |
84769fd8 |
261 | } |
3246ed33 |
262 | |
263 | if ($warning) { |
264 | $a = new stdClass; |
55c54868 |
265 | $a->questiontext = $qo->questiontext; |
3246ed33 |
266 | $a->answer = get_string($qo->answer ? 'true' : 'false', 'quiz'); |
fef8f84e |
267 | echo $OUTPUT->notification(get_string('truefalseimporterror', 'quiz', $a)); |
84769fd8 |
268 | } |
84769fd8 |
269 | return $qo; |
270 | } |
271 | |
6e557c08 |
272 | /** |
c81415c7 |
273 | * import short answer type question |
6e557c08 |
274 | * @param array question question array from xml tree |
275 | * @return object question object |
c81415c7 |
276 | */ |
84769fd8 |
277 | function import_shortanswer( $question ) { |
84769fd8 |
278 | // get common parts |
279 | $qo = $this->import_headers( $question ); |
280 | |
281 | // header parts particular to shortanswer |
282 | $qo->qtype = SHORTANSWER; |
283 | |
284 | // get usecase |
46013523 |
285 | $qo->usecase = $this->getpath($question, array('#','usecase',0,'#'), $qo->usecase ); |
84769fd8 |
286 | |
287 | // run through the answers |
288 | $answers = $question['#']['answer']; |
289 | $a_count = 0; |
290 | foreach ($answers as $answer) { |
291 | $ans = $this->import_answer( $answer ); |
292 | $qo->answer[$a_count] = $ans->answer; |
293 | $qo->fraction[$a_count] = $ans->fraction; |
294 | $qo->feedback[$a_count] = $ans->feedback; |
295 | ++$a_count; |
296 | } |
297 | |
298 | return $qo; |
299 | } |
7b8bc256 |
300 | |
6e557c08 |
301 | /** |
c81415c7 |
302 | * import description type question |
6e557c08 |
303 | * @param array question question array from xml tree |
304 | * @return object question object |
c81415c7 |
305 | */ |
7b8bc256 |
306 | function import_description( $question ) { |
307 | // get common parts |
308 | $qo = $this->import_headers( $question ); |
309 | // header parts particular to shortanswer |
310 | $qo->qtype = DESCRIPTION; |
3f5633df |
311 | $qo->defaultgrade = 0; |
312 | $qo->length = 0; |
7b8bc256 |
313 | return $qo; |
314 | } |
84769fd8 |
315 | |
6e557c08 |
316 | /** |
c81415c7 |
317 | * import numerical type question |
6e557c08 |
318 | * @param array question question array from xml tree |
319 | * @return object question object |
c81415c7 |
320 | */ |
321 | function import_numerical( $question ) { |
84769fd8 |
322 | // get common parts |
323 | $qo = $this->import_headers( $question ); |
324 | |
325 | // header parts particular to numerical |
326 | $qo->qtype = NUMERICAL; |
327 | |
328 | // get answers array |
329 | $answers = $question['#']['answer']; |
330 | $qo->answer = array(); |
331 | $qo->feedback = array(); |
332 | $qo->fraction = array(); |
333 | $qo->tolerance = array(); |
334 | foreach ($answers as $answer) { |
55c54868 |
335 | // answer outside of <text> is deprecated |
81a7a02b |
336 | $answertext = trim( $this->getpath( $answer, array('#',0), '' ) ); |
337 | $qo->answer[] = $this->getpath( $answer, array('#','text',0,'#'), $answertext, true ); |
338 | if (empty($qo->answer)) { |
339 | $qo->answer = '*'; |
55c54868 |
340 | } |
81a7a02b |
341 | $qo->feedback[] = $this->getpath( $answer, array('#','feedback',0,'#','text',0,'#'), '', true ); |
342 | $qo->tolerance[] = $this->getpath( $answer, array('#','tolerance',0,'#'), 0 ); |
55c54868 |
343 | |
344 | // fraction as a tag is deprecated |
81a7a02b |
345 | $fraction = $this->getpath( $answer, array('@','fraction'), 0 ) / 100; |
346 | $qo->fraction[] = $this->getpath( $answer, array('#','fraction',0,'#'), $fraction ); // deprecated |
84769fd8 |
347 | } |
348 | |
349 | // get units array |
84769fd8 |
350 | $qo->unit = array(); |
81a7a02b |
351 | $units = $this->getpath( $question, array('#','units',0,'#','unit'), array() ); |
352 | if (!empty($units)) { |
a0d187bf |
353 | $qo->multiplier = array(); |
354 | foreach ($units as $unit) { |
81a7a02b |
355 | $qo->multiplier[] = $this->getpath( $unit, array('#','multiplier',0,'#'), 1 ); |
356 | $qo->unit[] = $this->getpath( $unit, array('#','unit_name',0,'#'), '', true ); |
a0d187bf |
357 | } |
84769fd8 |
358 | } |
84769fd8 |
359 | return $qo; |
360 | } |
361 | |
6e557c08 |
362 | /** |
c81415c7 |
363 | * import matching type question |
6e557c08 |
364 | * @param array question question array from xml tree |
365 | * @return object question object |
c81415c7 |
366 | */ |
51bcdf28 |
367 | function import_matching( $question ) { |
51bcdf28 |
368 | // get common parts |
369 | $qo = $this->import_headers( $question ); |
370 | |
371 | // header parts particular to matching |
372 | $qo->qtype = MATCH; |
81a7a02b |
373 | $qo->shuffleanswers = $this->getpath( $question, array( '#','shuffleanswers',0,'#' ), 1 ); |
51bcdf28 |
374 | |
375 | // get subquestions |
376 | $subquestions = $question['#']['subquestion']; |
377 | $qo->subquestions = array(); |
378 | $qo->subanswers = array(); |
379 | |
380 | // run through subquestions |
381 | foreach ($subquestions as $subquestion) { |
81a7a02b |
382 | $qo->subquestions[] = $this->getpath( $subquestion, array('#','text',0,'#'), '', true ); |
383 | $qo->subanswers[] = $this->getpath( $subquestion, array('#','answer',0,'#','text',0,'#'), '', true); |
51bcdf28 |
384 | } |
51bcdf28 |
385 | return $qo; |
386 | } |
387 | |
6e557c08 |
388 | /** |
c81415c7 |
389 | * import essay type question |
6e557c08 |
390 | * @param array question question array from xml tree |
391 | * @return object question object |
c81415c7 |
392 | */ |
393 | function import_essay( $question ) { |
394 | // get common parts |
395 | $qo = $this->import_headers( $question ); |
396 | |
397 | // header parts particular to essay |
398 | $qo->qtype = ESSAY; |
399 | |
400 | // get feedback |
33ed59a2 |
401 | $qo->feedback = $this->getpath( $question, array('#','answer',0,'#','feedback',0,'#','text',0,'#'), '', true ); |
55c54868 |
402 | |
403 | // get fraction - <fraction> tag is deprecated |
6af98025 |
404 | $qo->fraction = $this->getpath( $question, array('@','fraction'), 0 ) / 100; |
405 | $q0->fraction = $this->getpath( $question, array('#','fraction',0,'#'), $qo->fraction ); |
c81415c7 |
406 | |
407 | return $qo; |
408 | } |
84769fd8 |
409 | |
725ba2a0 |
410 | function import_calculated( $question ) { |
411 | // import numerical question |
412 | |
413 | // get common parts |
414 | $qo = $this->import_headers( $question ); |
415 | |
416 | // header parts particular to numerical |
417 | $qo->qtype = CALCULATED ;//CALCULATED; |
505f7342 |
418 | $qo->synchronize = $this->getpath( $question, array( '#','synchronize',0,'#' ), 0 ); |
725ba2a0 |
419 | // get answers array |
420 | // echo "<pre> question";print_r($question);echo "</pre>"; |
421 | $answers = $question['#']['answer']; |
422 | $qo->answers = array(); |
423 | $qo->feedback = array(); |
424 | $qo->fraction = array(); |
425 | $qo->tolerance = array(); |
426 | $qo->tolerancetype = array(); |
427 | $qo->correctanswerformat = array(); |
428 | $qo->correctanswerlength = array(); |
429 | $qo->feedback = array(); |
430 | foreach ($answers as $answer) { |
431 | // answer outside of <text> is deprecated |
432 | if (!empty( $answer['#']['text'] )) { |
433 | $answertext = $this->import_text( $answer['#']['text'] ); |
434 | } |
435 | else { |
436 | $answertext = trim($answer['#'][0]); |
437 | } |
438 | if ($answertext == '') { |
439 | $qo->answers[] = '*'; |
440 | } else { |
441 | $qo->answers[] = $answertext; |
442 | } |
443 | $qo->feedback[] = $this->import_text( $answer['#']['feedback'][0]['#']['text'] ); |
444 | $qo->tolerance[] = $answer['#']['tolerance'][0]['#']; |
445 | // fraction as a tag is deprecated |
446 | if (!empty($answer['#']['fraction'][0]['#'])) { |
447 | $qo->fraction[] = $answer['#']['fraction'][0]['#']; |
448 | } |
449 | else { |
450 | $qo->fraction[] = $answer['@']['fraction'] / 100; |
451 | } |
452 | $qo->tolerancetype[] = $answer['#']['tolerancetype'][0]['#']; |
453 | $qo->correctanswerformat[] = $answer['#']['correctanswerformat'][0]['#']; |
454 | $qo->correctanswerlength[] = $answer['#']['correctanswerlength'][0]['#']; |
455 | } |
456 | // get units array |
457 | $qo->unit = array(); |
458 | if (isset($question['#']['units'][0]['#']['unit'])) { |
459 | $units = $question['#']['units'][0]['#']['unit']; |
460 | $qo->multiplier = array(); |
461 | foreach ($units as $unit) { |
462 | $qo->multiplier[] = $unit['#']['multiplier'][0]['#']; |
463 | $qo->unit[] = $unit['#']['unit_name'][0]['#']; |
464 | } |
465 | } |
466 | $datasets = $question['#']['dataset_definitions'][0]['#']['dataset_definition']; |
467 | $qo->dataset = array(); |
468 | $qo->datasetindex= 0 ; |
469 | foreach ($datasets as $dataset) { |
470 | $qo->datasetindex++; |
471 | $qo->dataset[$qo->datasetindex] = new stdClass(); |
472 | $qo->dataset[$qo->datasetindex]->status = $this->import_text( $dataset['#']['status'][0]['#']['text']); |
473 | $qo->dataset[$qo->datasetindex]->name = $this->import_text( $dataset['#']['name'][0]['#']['text']); |
474 | $qo->dataset[$qo->datasetindex]->type = $dataset['#']['type'][0]['#']; |
475 | $qo->dataset[$qo->datasetindex]->distribution = $this->import_text( $dataset['#']['distribution'][0]['#']['text']); |
476 | $qo->dataset[$qo->datasetindex]->max = $this->import_text( $dataset['#']['maximum'][0]['#']['text']); |
477 | $qo->dataset[$qo->datasetindex]->min = $this->import_text( $dataset['#']['minimum'][0]['#']['text']); |
478 | $qo->dataset[$qo->datasetindex]->length = $this->import_text( $dataset['#']['decimals'][0]['#']['text']); |
479 | $qo->dataset[$qo->datasetindex]->distribution = $this->import_text( $dataset['#']['distribution'][0]['#']['text']); |
480 | $qo->dataset[$qo->datasetindex]->itemcount = $dataset['#']['itemcount'][0]['#']; |
481 | $qo->dataset[$qo->datasetindex]->datasetitem = array(); |
482 | $qo->dataset[$qo->datasetindex]->itemindex = 0; |
483 | $qo->dataset[$qo->datasetindex]->number_of_items=$dataset['#']['number_of_items'][0]['#']; |
484 | $datasetitems = $dataset['#']['dataset_items'][0]['#']['dataset_item']; |
485 | foreach ($datasetitems as $datasetitem) { |
486 | $qo->dataset[$qo->datasetindex]->itemindex++; |
487 | $qo->dataset[$qo->datasetindex]->datasetitem[$qo->dataset[$qo->datasetindex]->itemindex] = new stdClass(); |
488 | $qo->dataset[$qo->datasetindex]->datasetitem[$qo->dataset[$qo->datasetindex]->itemindex]->itemnumber = $datasetitem['#']['number'][0]['#']; //[0]['#']['number'][0]['#'] ; // [0]['numberitems'] ;//['#']['number'][0]['#'];// $datasetitems['#']['number'][0]['#']; |
489 | $qo->dataset[$qo->datasetindex]->datasetitem[$qo->dataset[$qo->datasetindex]->itemindex]->value = $datasetitem['#']['value'][0]['#'] ;//$datasetitem['#']['value'][0]['#']; |
490 | } |
491 | } |
492 | |
5fd8f999 |
493 | // echo "<pre>loaded qo";print_r($qo);echo "</pre>"; |
725ba2a0 |
494 | |
495 | return $qo; |
496 | } |
497 | |
ee259d0c |
498 | /** |
499 | * this is not a real question type. It's a dummy type used |
500 | * to specify the import category |
501 | * format is: |
502 | * <question type="category"> |
503 | * <category>tom/dick/harry</category> |
504 | * </question> |
505 | */ |
506 | function import_category( $question ) { |
0c6b4d2e |
507 | $qo = new stdClass; |
ee259d0c |
508 | $qo->qtype = 'category'; |
86b68520 |
509 | $qo->category = $this->import_text($question['#']['category'][0]['#']['text']); |
ee259d0c |
510 | return $qo; |
511 | } |
512 | |
c81415c7 |
513 | /** |
514 | * parse the array of lines into an array of questions |
515 | * this *could* burn memory - but it won't happen that much |
516 | * so fingers crossed! |
6e557c08 |
517 | * @param array lines array of lines from the input file |
518 | * @return array (of objects) question objects |
c81415c7 |
519 | */ |
520 | function readquestions($lines) { |
84769fd8 |
521 | // we just need it as one big string |
522 | $text = implode($lines, " "); |
523 | unset( $lines ); |
524 | |
525 | // this converts xml to big nasty data structure |
526 | // the 0 means keep white space as it is (important for markdown format) |
527 | // print_r it if you want to see what it looks like! |
528 | $xml = xmlize( $text, 0 ); |
529 | |
530 | // set up array to hold all our questions |
531 | $questions = array(); |
532 | |
533 | // iterate through questions |
534 | foreach ($xml['quiz']['#']['question'] as $question) { |
535 | $question_type = $question['@']['type']; |
536 | $questiontype = get_string( 'questiontype','quiz',$question_type ); |
84769fd8 |
537 | |
538 | if ($question_type=='multichoice') { |
539 | $qo = $this->import_multichoice( $question ); |
540 | } |
541 | elseif ($question_type=='truefalse') { |
542 | $qo = $this->import_truefalse( $question ); |
543 | } |
544 | elseif ($question_type=='shortanswer') { |
545 | $qo = $this->import_shortanswer( $question ); |
546 | } |
547 | elseif ($question_type=='numerical') { |
548 | $qo = $this->import_numerical( $question ); |
549 | } |
7b8bc256 |
550 | elseif ($question_type=='description') { |
551 | $qo = $this->import_description( $question ); |
552 | } |
51bcdf28 |
553 | elseif ($question_type=='matching') { |
554 | $qo = $this->import_matching( $question ); |
555 | } |
7b8bc256 |
556 | elseif ($question_type=='cloze') { |
c81415c7 |
557 | $qo = $this->import_multianswer( $question ); |
558 | } |
559 | elseif ($question_type=='essay') { |
560 | $qo = $this->import_essay( $question ); |
7b8bc256 |
561 | } |
725ba2a0 |
562 | elseif ($question_type=='calculated') { |
563 | $qo = $this->import_calculated( $question ); |
564 | } |
9c1c6c7f |
565 | elseif ($question_type=='calculatedsimple') { |
566 | $qo = $this->import_calculated( $question ); |
567 | $qo->qtype = CALCULATEDSIMPLE ; |
568 | } |
ee259d0c |
569 | elseif ($question_type=='category') { |
570 | $qo = $this->import_category( $question ); |
571 | } |
84769fd8 |
572 | else { |
a41e3287 |
573 | // try for plugin support |
574 | // no default question, as the plugin can call |
575 | // import_headers() itself if it wants to |
576 | if (!$qo=$this->try_importing_using_qtypes( $question )) { |
577 | $notsupported = get_string( 'xmltypeunsupported','quiz',$question_type ); |
578 | $this->error( $notsupported ); |
579 | $qo = null; |
580 | } |
84769fd8 |
581 | } |
582 | |
583 | // stick the result in the $questions array |
584 | if ($qo) { |
585 | $questions[] = $qo; |
586 | } |
587 | } |
84769fd8 |
588 | return $questions; |
589 | } |
590 | |
591 | // EXPORT FUNCTIONS START HERE |
592 | |
84769fd8 |
593 | function export_file_extension() { |
594 | // override default type so extension is .xml |
595 | |
596 | return ".xml"; |
597 | } |
598 | |
c81415c7 |
599 | |
600 | /** |
601 | * Turn the internal question code into a human readable form |
602 | * (The code used to be numeric, but this remains as some of |
603 | * the names don't match the new internal format) |
6e557c08 |
604 | * @param mixed type_id Internal code |
605 | * @return string question type string |
c81415c7 |
606 | */ |
84769fd8 |
607 | function get_qtype( $type_id ) { |
84769fd8 |
608 | switch( $type_id ) { |
609 | case TRUEFALSE: |
610 | $name = 'truefalse'; |
611 | break; |
612 | case MULTICHOICE: |
613 | $name = 'multichoice'; |
614 | break; |
615 | case SHORTANSWER: |
616 | $name = 'shortanswer'; |
617 | break; |
618 | case NUMERICAL: |
619 | $name = 'numerical'; |
620 | break; |
621 | case MATCH: |
622 | $name = 'matching'; |
623 | break; |
624 | case DESCRIPTION: |
625 | $name = 'description'; |
626 | break; |
627 | case MULTIANSWER: |
628 | $name = 'cloze'; |
629 | break; |
c81415c7 |
630 | case ESSAY: |
631 | $name = 'essay'; |
632 | break; |
725ba2a0 |
633 | case CALCULATED: |
634 | $name = 'calculated'; |
635 | break; |
9c1c6c7f |
636 | case CALCULATEDSIMPLE: |
637 | $name = 'calculatedsimple'; |
638 | break; |
84769fd8 |
639 | default: |
a41e3287 |
640 | $name = false; |
84769fd8 |
641 | } |
642 | return $name; |
643 | } |
644 | |
6e557c08 |
645 | /** |
c81415c7 |
646 | * Convert internal Moodle text format code into |
647 | * human readable form |
6e557c08 |
648 | * @param int id internal code |
649 | * @return string format text |
c81415c7 |
650 | */ |
84769fd8 |
651 | function get_format( $id ) { |
84769fd8 |
652 | switch( $id ) { |
653 | case 0: |
654 | $name = "moodle_auto_format"; |
655 | break; |
656 | case 1: |
657 | $name = "html"; |
658 | break; |
659 | case 2: |
660 | $name = "plain_text"; |
661 | break; |
662 | case 3: |
663 | $name = "wiki_like"; |
664 | break; |
665 | case 4: |
666 | $name = "markdown"; |
667 | break; |
668 | default: |
669 | $name = "unknown"; |
670 | } |
671 | return $name; |
672 | } |
673 | |
6e557c08 |
674 | /** |
c81415c7 |
675 | * Convert internal single question code into |
676 | * human readable form |
6e557c08 |
677 | * @param int id single question code |
678 | * @return string single question string |
c81415c7 |
679 | */ |
84769fd8 |
680 | function get_single( $id ) { |
84769fd8 |
681 | switch( $id ) { |
682 | case 0: |
683 | $name = "false"; |
684 | break; |
685 | case 1: |
686 | $name = "true"; |
687 | break; |
688 | default: |
689 | $name = "unknown"; |
690 | } |
691 | return $name; |
692 | } |
693 | |
6e557c08 |
694 | /** |
c81415c7 |
695 | * generates <text></text> tags, processing raw text therein |
6e557c08 |
696 | * @param int ilev the current indent level |
697 | * @param boolean short stick it on one line |
698 | * @return string formatted text |
c81415c7 |
699 | */ |
84769fd8 |
700 | function writetext( $raw, $ilev=0, $short=true) { |
84769fd8 |
701 | $indent = str_repeat( " ",$ilev ); |
702 | |
3695cfa9 |
703 | // if required add CDATA tags |
704 | if (!empty($raw) and (htmlspecialchars($raw)!=$raw)) { |
705 | $raw = "<![CDATA[$raw]]>"; |
706 | } |
84769fd8 |
707 | |
708 | if ($short) { |
709 | $xml = "$indent<text>$raw</text>\n"; |
710 | } |
711 | else { |
712 | $xml = "$indent<text>\n$raw\n$indent</text>\n"; |
713 | } |
714 | |
715 | return $xml; |
716 | } |
81a7a02b |
717 | |
718 | function xmltidy( $content ) { |
719 | // can only do this if tidy is installed |
720 | if (extension_loaded('tidy')) { |
721 | $config = array( 'input-xml'=>true, 'output-xml'=>true, 'indent'=>true, 'wrap'=>0 ); |
722 | $tidy = new tidy; |
723 | $tidy->parseString($content, $config, 'utf8'); |
724 | $tidy->cleanRepair(); |
725 | return $tidy->value; |
726 | } |
727 | else { |
728 | return $content; |
729 | } |
730 | } |
731 | |
84769fd8 |
732 | |
733 | function presave_process( $content ) { |
734 | // override method to allow us to add xml headers and footers |
735 | |
81a7a02b |
736 | // add the xml headers and footers |
46013523 |
737 | $content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" . |
84769fd8 |
738 | "<quiz>\n" . |
739 | $content . "\n" . |
740 | "</quiz>"; |
741 | |
81a7a02b |
742 | // make the xml look nice |
743 | $content = $this->xmltidy( $content ); |
744 | |
84769fd8 |
745 | return $content; |
746 | } |
747 | |
6e557c08 |
748 | /** |
c81415c7 |
749 | * Include an image encoded in base 64 |
6e557c08 |
750 | * @param string imagepath The location of the image file |
751 | * @return string xml code segment |
c81415c7 |
752 | */ |
d08e16b2 |
753 | function writeimage( $imagepath ) { |
d08e16b2 |
754 | global $CFG; |
755 | |
756 | if (empty($imagepath)) { |
757 | return ''; |
758 | } |
759 | |
760 | $courseid = $this->course->id; |
761 | if (!$binary = file_get_contents( "{$CFG->dataroot}/$courseid/$imagepath" )) { |
762 | return ''; |
763 | } |
764 | |
294ce987 |
765 | $content = " <image_base64>\n".base64_encode( $binary )."\n". |
d08e16b2 |
766 | "\n </image_base64>\n"; |
767 | return $content; |
768 | } |
769 | |
6e557c08 |
770 | /** |
c81415c7 |
771 | * Turns question into an xml segment |
6e557c08 |
772 | * @param array question question array |
773 | * @return string xml segment |
c81415c7 |
774 | */ |
84769fd8 |
775 | function writequestion( $question ) { |
fef8f84e |
776 | global $CFG,$QTYPES, $OUTPUT; |
84769fd8 |
777 | // initial string; |
778 | $expout = ""; |
779 | |
780 | // add comment |
781 | $expout .= "\n\n<!-- question: $question->id -->\n"; |
782 | |
a41e3287 |
783 | // check question type |
784 | if (!$question_type = $this->get_qtype( $question->qtype )) { |
785 | // must be a plugin then, so just accept the name supplied |
786 | $question_type = $question->qtype; |
46013523 |
787 | } |
788 | |
84769fd8 |
789 | // add opening tag |
f1abd39f |
790 | // generates specific header for Cloze and category type question |
791 | if ($question->qtype == 'category') { |
6f8481ec |
792 | $categorypath = $this->writetext( $question->category ); |
f1abd39f |
793 | $expout .= " <question type=\"category\">\n"; |
794 | $expout .= " <category>\n"; |
6f8481ec |
795 | $expout .= " $categorypath\n"; |
f1abd39f |
796 | $expout .= " </category>\n"; |
797 | $expout .= " </question>\n"; |
798 | return $expout; |
799 | } |
800 | elseif ($question->qtype != MULTIANSWER) { |
7b8bc256 |
801 | // for all question types except Close |
7b8bc256 |
802 | $name_text = $this->writetext( $question->name ); |
803 | $qtformat = $this->get_format($question->questiontextformat); |
804 | $question_text = $this->writetext( $question->questiontext ); |
a4514d91 |
805 | $generalfeedback = $this->writetext( $question->generalfeedback ); |
7b8bc256 |
806 | $expout .= " <question type=\"$question_type\">\n"; |
807 | $expout .= " <name>$name_text</name>\n"; |
808 | $expout .= " <questiontext format=\"$qtformat\">\n"; |
809 | $expout .= $question_text; |
810 | $expout .= " </questiontext>\n"; |
811 | $expout .= " <image>{$question->image}</image>\n"; |
812 | $expout .= $this->writeimage($question->image); |
a4514d91 |
813 | $expout .= " <generalfeedback>\n"; |
814 | $expout .= $generalfeedback; |
815 | $expout .= " </generalfeedback>\n"; |
c81415c7 |
816 | $expout .= " <defaultgrade>{$question->defaultgrade}</defaultgrade>\n"; |
7b8bc256 |
817 | $expout .= " <penalty>{$question->penalty}</penalty>\n"; |
818 | $expout .= " <hidden>{$question->hidden}</hidden>\n"; |
819 | } |
820 | else { |
821 | // for Cloze type only |
7b8bc256 |
822 | $name_text = $this->writetext( $question->name ); |
823 | $question_text = $this->writetext( $question->questiontext ); |
4ab4dcb2 |
824 | $generalfeedback = $this->writetext( $question->generalfeedback ); |
7b8bc256 |
825 | $expout .= " <question type=\"$question_type\">\n"; |
826 | $expout .= " <name>$name_text</name>\n"; |
827 | $expout .= " <questiontext>\n"; |
828 | $expout .= $question_text; |
829 | $expout .= " </questiontext>\n"; |
4ab4dcb2 |
830 | $expout .= " <generalfeedback>\n"; |
831 | $expout .= $generalfeedback; |
832 | $expout .= " </generalfeedback>\n"; |
7b8bc256 |
833 | } |
834 | |
d08e16b2 |
835 | if (!empty($question->options->shuffleanswers)) { |
836 | $expout .= " <shuffleanswers>{$question->options->shuffleanswers}</shuffleanswers>\n"; |
837 | } |
838 | else { |
839 | $expout .= " <shuffleanswers>0</shuffleanswers>\n"; |
840 | } |
84769fd8 |
841 | |
842 | // output depends on question type |
843 | switch($question->qtype) { |
f1abd39f |
844 | case 'category': |
845 | // not a qtype really - dummy used for category switching |
846 | break; |
84769fd8 |
847 | case TRUEFALSE: |
36e2232e |
848 | foreach ($question->options->answers as $answer) { |
849 | $fraction_pc = round( $answer->fraction * 100 ); |
3246ed33 |
850 | if ($answer->id == $question->options->trueanswer) { |
851 | $answertext = 'true'; |
852 | } else { |
853 | $answertext = 'false'; |
854 | } |
36e2232e |
855 | $expout .= " <answer fraction=\"$fraction_pc\">\n"; |
3246ed33 |
856 | $expout .= $this->writetext($answertext, 3) . "\n"; |
36e2232e |
857 | $expout .= " <feedback>\n"; |
858 | $expout .= $this->writetext( $answer->feedback,4,false ); |
859 | $expout .= " </feedback>\n"; |
860 | $expout .= " </answer>\n"; |
861 | } |
84769fd8 |
862 | break; |
863 | case MULTICHOICE: |
864 | $expout .= " <single>".$this->get_single($question->options->single)."</single>\n"; |
307f045f |
865 | $expout .= " <shuffleanswers>".$this->get_single($question->options->shuffleanswers)."</shuffleanswers>\n"; |
866 | $expout .= " <correctfeedback>".$this->writetext($question->options->correctfeedback, 3)."</correctfeedback>\n"; |
867 | $expout .= " <partiallycorrectfeedback>".$this->writetext($question->options->partiallycorrectfeedback, 3)."</partiallycorrectfeedback>\n"; |
868 | $expout .= " <incorrectfeedback>".$this->writetext($question->options->incorrectfeedback, 3)."</incorrectfeedback>\n"; |
2377f8dc |
869 | $expout .= " <answernumbering>{$question->options->answernumbering}</answernumbering>\n"; |
84769fd8 |
870 | foreach($question->options->answers as $answer) { |
871 | $percent = $answer->fraction * 100; |
872 | $expout .= " <answer fraction=\"$percent\">\n"; |
873 | $expout .= $this->writetext( $answer->answer,4,false ); |
874 | $expout .= " <feedback>\n"; |
6e557c08 |
875 | $expout .= $this->writetext( $answer->feedback,5,false ); |
84769fd8 |
876 | $expout .= " </feedback>\n"; |
877 | $expout .= " </answer>\n"; |
878 | } |
879 | break; |
880 | case SHORTANSWER: |
881 | $expout .= " <usecase>{$question->options->usecase}</usecase>\n "; |
882 | foreach($question->options->answers as $answer) { |
883 | $percent = 100 * $answer->fraction; |
884 | $expout .= " <answer fraction=\"$percent\">\n"; |
885 | $expout .= $this->writetext( $answer->answer,3,false ); |
886 | $expout .= " <feedback>\n"; |
887 | $expout .= $this->writetext( $answer->feedback,4,false ); |
888 | $expout .= " </feedback>\n"; |
889 | $expout .= " </answer>\n"; |
890 | } |
891 | break; |
892 | case NUMERICAL: |
893 | foreach ($question->options->answers as $answer) { |
894 | $tolerance = $answer->tolerance; |
55c54868 |
895 | $percent = 100 * $answer->fraction; |
896 | $expout .= "<answer fraction=\"$percent\">\n"; |
897 | // <text> tags are an added feature, old filed won't have them |
898 | $expout .= " <text>{$answer->answer}</text>\n"; |
84769fd8 |
899 | $expout .= " <tolerance>$tolerance</tolerance>\n"; |
900 | $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n"; |
55c54868 |
901 | // fraction tag is deprecated |
902 | // $expout .= " <fraction>{$answer->fraction}</fraction>\n"; |
84769fd8 |
903 | $expout .= "</answer>\n"; |
904 | } |
905 | |
906 | $units = $question->options->units; |
907 | if (count($units)) { |
908 | $expout .= "<units>\n"; |
909 | foreach ($units as $unit) { |
910 | $expout .= " <unit>\n"; |
911 | $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n"; |
912 | $expout .= " <unit_name>{$unit->unit}</unit_name>\n"; |
913 | $expout .= " </unit>\n"; |
914 | } |
915 | $expout .= "</units>\n"; |
916 | } |
917 | break; |
918 | case MATCH: |
919 | foreach($question->options->subquestions as $subquestion) { |
920 | $expout .= "<subquestion>\n"; |
921 | $expout .= $this->writetext( $subquestion->questiontext ); |
922 | $expout .= "<answer>".$this->writetext( $subquestion->answertext )."</answer>\n"; |
923 | $expout .= "</subquestion>\n"; |
924 | } |
925 | break; |
926 | case DESCRIPTION: |
c81415c7 |
927 | // nothing more to do for this type |
84769fd8 |
928 | break; |
929 | case MULTIANSWER: |
7b8bc256 |
930 | $a_count=1; |
931 | foreach($question->options->questions as $question) { |
294ce987 |
932 | $thispattern = addslashes("{#".$a_count."}"); // TODO: fix this addslashes |
7b8bc256 |
933 | $thisreplace = $question->questiontext; |
6dbcacee |
934 | $expout=preg_replace("~$thispattern~", $thisreplace, $expout ); |
7b8bc256 |
935 | $a_count++; |
936 | } |
937 | break; |
c81415c7 |
938 | case ESSAY: |
33ed59a2 |
939 | if (!empty($question->options->answers)) { |
940 | foreach ($question->options->answers as $answer) { |
941 | $percent = 100 * $answer->fraction; |
942 | $expout .= "<answer fraction=\"$percent\">\n"; |
943 | $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n"; |
944 | // fraction tag is deprecated |
945 | // $expout .= " <fraction>{$answer->fraction}</fraction>\n"; |
946 | $expout .= "</answer>\n"; |
947 | } |
c81415c7 |
948 | } |
725ba2a0 |
949 | break; |
950 | case CALCULATED: |
505f7342 |
951 | if (!empty($question->options->synchronize)) { |
952 | $expout .= " <synchronize>{$question->options->synchronize}</synchronize>\n"; |
953 | } |
954 | else { |
955 | $expout .= " <synchronize>0</synchronize>\n"; |
956 | } |
9c1c6c7f |
957 | case CALCULATEDSIMPLE: |
725ba2a0 |
958 | foreach ($question->options->answers as $answer) { |
959 | $tolerance = $answer->tolerance; |
960 | $tolerancetype = $answer->tolerancetype; |
961 | $correctanswerlength= $answer->correctanswerlength ; |
962 | $correctanswerformat= $answer->correctanswerformat; |
963 | $percent = 100 * $answer->fraction; |
964 | $expout .= "<answer fraction=\"$percent\">\n"; |
965 | // "<text/>" tags are an added feature, old files won't have them |
966 | $expout .= " <text>{$answer->answer}</text>\n"; |
967 | $expout .= " <tolerance>$tolerance</tolerance>\n"; |
968 | $expout .= " <tolerancetype>$tolerancetype</tolerancetype>\n"; |
969 | $expout .= " <correctanswerformat>$correctanswerformat</correctanswerformat>\n"; |
a800639b |
970 | $expout .= " <correctanswerlength>$correctanswerlength</correctanswerlength>\n"; |
725ba2a0 |
971 | $expout .= " <feedback>".$this->writetext( $answer->feedback )."</feedback>\n"; |
972 | $expout .= "</answer>\n"; |
973 | } |
974 | $units = $question->options->units; |
975 | if (count($units)) { |
976 | $expout .= "<units>\n"; |
977 | foreach ($units as $unit) { |
978 | $expout .= " <unit>\n"; |
979 | $expout .= " <multiplier>{$unit->multiplier}</multiplier>\n"; |
980 | $expout .= " <unit_name>{$unit->unit}</unit_name>\n"; |
981 | $expout .= " </unit>\n"; |
982 | } |
983 | $expout .= "</units>\n"; |
984 | } |
9c1c6c7f |
985 | //The tag $question->export_process has been set so we get all the data items in the database |
986 | // from the function $QTYPES['calculated']->get_question_options(&$question); |
987 | // calculatedsimple defaults to calculated |
c9e4ba36 |
988 | if( isset($question->options->datasets)&&count($question->options->datasets)){// there should be |
725ba2a0 |
989 | $expout .= "<dataset_definitions>\n"; |
c9e4ba36 |
990 | foreach ($question->options->datasets as $def) { |
725ba2a0 |
991 | $expout .= "<dataset_definition>\n"; |
992 | $expout .= " <status>".$this->writetext($def->status)."</status>\n"; |
993 | $expout .= " <name>".$this->writetext($def->name)."</name>\n"; |
9c1c6c7f |
994 | if ( $question->qtype == CALCULATED){ |
995 | $expout .= " <type>calculated</type>\n"; |
996 | }else { |
997 | $expout .= " <type>calculatedsimple</type>\n"; |
998 | } |
725ba2a0 |
999 | $expout .= " <distribution>".$this->writetext($def->distribution)."</distribution>\n"; |
1000 | $expout .= " <minimum>".$this->writetext($def->minimum)."</minimum>\n"; |
1001 | $expout .= " <maximum>".$this->writetext($def->maximum)."</maximum>\n"; |
1002 | $expout .= " <decimals>".$this->writetext($def->decimals)."</decimals>\n"; |
1003 | $expout .= " <itemcount>$def->itemcount</itemcount>\n"; |
1004 | if ($def->itemcount > 0 ) { |
1005 | $expout .= " <dataset_items>\n"; |
1006 | foreach ($def->items as $item ){ |
1007 | $expout .= " <dataset_item>\n"; |
1008 | $expout .= " <number>".$item->itemnumber."</number>\n"; |
1009 | $expout .= " <value>".$item->value."</value>\n"; |
1010 | $expout .= " </dataset_item>\n"; |
1011 | } |
1012 | $expout .= " </dataset_items>\n"; |
1013 | $expout .= " <number_of_items>".$def-> number_of_items."</number_of_items>\n"; |
1014 | } |
1015 | $expout .= "</dataset_definition>\n"; |
1016 | } |
1017 | $expout .= "</dataset_definitions>\n"; |
1018 | } |
c81415c7 |
1019 | break; |
84769fd8 |
1020 | default: |
a41e3287 |
1021 | // try support by optional plugin |
392238d6 |
1022 | if (!$data = $this->try_exporting_using_qtypes( $question->qtype, $question )) { |
fef8f84e |
1023 | echo $OUTPUT->notification( get_string( 'unsupportedexport','qformat_xml',$QTYPES[$question->qtype]->local_name() ) ); |
a41e3287 |
1024 | } |
392238d6 |
1025 | $expout .= $data; |
84769fd8 |
1026 | } |
1027 | |
1028 | // close the question tag |
1029 | $expout .= "</question>\n"; |
1030 | |
84769fd8 |
1031 | return $expout; |
1032 | } |
1033 | } |
1034 | |
1035 | ?> |