Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

MDL25-492 Blackboard V6+ question import is broken

  • Loading branch information...
commit 1d23052a6082a707044d96d0f36f67c73c6fb751 1 parent 4ff0f02
Jean-Michel Vedrine jmvedrine authored
68 question/format.php
@@ -408,20 +408,44 @@ public function importprocess($category) {
408 408 $question->timecreated = time();
409 409 $question->modifiedby = $USER->id;
410 410 $question->timemodified = time();
  411 + $fileoptions = array(
  412 + 'subdirs' => false,
  413 + 'maxfiles' => -1,
  414 + 'maxbytes' => 0,
  415 + );
  416 + if (is_array($question->questiontext)) {
  417 + // Importing images from draftfile.
  418 + $questiontext = $question->questiontext;
  419 + $question->questiontext = $questiontext['text'];
  420 + }
  421 + if (is_array($question->generalfeedback)) {
  422 + $generalfeedback = $question->generalfeedback;
  423 + $question->generalfeedback = $generalfeedback['text'];
  424 + }
411 425
412 426 $question->id = $DB->insert_record('question', $question);
413   - if (isset($question->questiontextfiles)) {
  427 +
  428 + if (!empty($questiontext['itemid'])) {
  429 + $question->questiontext = file_save_draft_area_files($questiontext['itemid'],
  430 + $this->importcontext->id, 'question', 'questiontext', $question->id,
  431 + $fileoptions, $question->questiontext);
  432 + } else if (isset($question->questiontextfiles)) {
414 433 foreach ($question->questiontextfiles as $file) {
415 434 question_bank::get_qtype($question->qtype)->import_file(
416 435 $this->importcontext, 'question', 'questiontext', $question->id, $file);
417 436 }
418 437 }
419   - if (isset($question->generalfeedbackfiles)) {
  438 + if (!empty($generalfeedback['itemid'])) {
  439 + $question->generalfeedback = file_save_draft_area_files($generalfeedback['itemid'],
  440 + $this->importcontext->id, 'question', 'generalfeedback', $question->id,
  441 + $fileoptions, $question->generalfeedback);
  442 + } else if (isset($question->generalfeedbackfiles)) {
420 443 foreach ($question->generalfeedbackfiles as $file) {
421 444 question_bank::get_qtype($question->qtype)->import_file(
422 445 $this->importcontext, 'question', 'generalfeedback', $question->id, $file);
423 446 }
424 447 }
  448 + $DB->update_record('question', $question);
425 449
426 450 $this->questionids[] = $question->id;
427 451
@@ -637,6 +661,24 @@ protected function defaultquestion() {
637 661 }
638 662
639 663 /**
  664 + * Add a blank combined feedback to a question object.
  665 + * @param object question
  666 + * @return object question
  667 + */
  668 + protected function add_blank_combined_feedback($question) {
  669 + $question->correctfeedback['text'] = '';
  670 + $question->correctfeedback['format'] = $question->questiontextformat;
  671 + $question->correctfeedback['files'] = array();
  672 + $question->partiallycorrectfeedback['text'] = '';
  673 + $question->partiallycorrectfeedback['format'] = $question->questiontextformat;
  674 + $question->partiallycorrectfeedback['files'] = array();
  675 + $question->incorrectfeedback['text'] = '';
  676 + $question->incorrectfeedback['format'] = $question->questiontextformat;
  677 + $question->incorrectfeedback['files'] = array();
  678 + return $question;
  679 + }
  680 +
  681 + /**
640 682 * Given the data known to define a question in
641 683 * this format, this function converts it into a question
642 684 * object suitable for processing and insertion into Moodle.
@@ -902,6 +944,28 @@ protected function format_question_text($question) {
902 944 class qformat_based_on_xml extends qformat_default {
903 945
904 946 /**
  947 + * A lot of imported files contain unwanted entities.
  948 + * This method tries to clean up all known problems.
  949 + * @param string str string to correct
  950 + * @return string the corrected string
  951 + */
  952 + public function cleaninput($str) {
  953 +
  954 + $html_code_list = array(
  955 + "'" => "'",
  956 + "’" => "'",
  957 + "“" => "\"",
  958 + "”" => "\"",
  959 + "–" => "-",
  960 + "—" => "-",
  961 + );
  962 + $str = strtr($str, $html_code_list);
  963 + // Use textlib entities_to_utf8 function to convert only numerical entities.
  964 + $str = textlib::entities_to_utf8($str, false);
  965 + return $str;
  966 + }
  967 +
  968 + /**
905 969 * Return the array moodle is expecting
906 970 * for an HTML text. No processing is done on $text.
907 971 * qformat classes that want to process $text
1,029 question/format/blackboard_six/format.php
@@ -15,936 +15,169 @@
15 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16 16
17 17 /**
18   - * Blackboard 6.0 question importer.
  18 + * Blackboard V5 and V6 question importer.
19 19 *
20   - * @package qformat
21   - * @subpackage blackboard_six
  20 + * @package qformat_blackboard_six
22 21 * @copyright 2005 Michael Penney
23 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 23 */
25 24
26   -
27 25 defined('MOODLE_INTERNAL') || die();
28 26
29   -require_once ($CFG->libdir . '/xmlize.php');
30   -
31   -
32   -/**
33   - * Blackboard 6.0 question importer.
34   - *
35   - * @copyright 2005 Michael Penney
36   - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37   - */
38   -class qformat_blackboard_six extends qformat_default {
39   - function provide_import() {
40   - return true;
41   - }
42   -
43   - public function can_import_file($file) {
44   - $mimetypes = array(
45   - mimeinfo('type', '.dat'),
46   - mimeinfo('type', '.zip')
47   - );
48   - return in_array($file->get_mimetype(), $mimetypes);
49   - }
50   -
51   -
52   - //Function to check and create the needed dir to unzip file to
53   - function check_and_create_import_dir($unique_code) {
54   -
55   - global $CFG;
56   -
57   - $status = $this->check_dir_exists($CFG->tempdir."",true);
58   - if ($status) {
59   - $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import",true);
60   - }
61   - if ($status) {
62   - $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import/".$unique_code,true);
63   - }
64   -
65   - return $status;
66   - }
67   -
68   - function clean_temp_dir($dir='') {
  27 +require_once($CFG->libdir . '/xmlize.php');
  28 +require_once($CFG->dirroot . '/question/format/blackboard_six/formatbase.php');
  29 +require_once($CFG->dirroot . '/question/format/blackboard_six/formatqti.php');
  30 +require_once($CFG->dirroot . '/question/format/blackboard_six/formatpool.php');
  31 +
  32 +class qformat_blackboard_six extends qformat_blackboard_six_base {
  33 + /** @var int Blackboard assessment qti files were always imported by the blackboard_six plugin. */
  34 + const FILETYPE_QTI = 1;
  35 + /** @var int Blackboard question pool files were previously handled by the blackboard plugin. */
  36 + const FILETYPE_POOL = 2;
  37 + /** @var int type of file being imported, one of the constants FILETYPE_QTI or FILETYPE_POOL. */
  38 + public $filetype;
  39 +
  40 + public function get_filecontent($path) {
  41 + $fullpath = $this->tempdir . '/' . $path;
  42 + if (is_file($fullpath) && is_readable($fullpath)) {
  43 + return file_get_contents($fullpath);
  44 + }
  45 + return false;
  46 + }
  47 +
  48 + /**
  49 + * Set the file type being imported
  50 + * @param int $type the imported file's type
  51 + */
  52 + public function set_filetype($type) {
  53 + $this->filetype = $type;
  54 + }
  55 +
  56 + /**
  57 + * Return content of all files containing questions,
  58 + * as an array one element for each file found,
  59 + * For each file, the corresponding element is an array of lines.
  60 + * @param string filename name of file
  61 + * @return mixed contents array or false on failure
  62 + */
  63 + public function readdata($filename) {
69 64 global $CFG;
70 65
71   - // for now we will just say everything happened okay note
72   - // that a mess may be piling up in $CFG->tempdir/bbquiz_import
73   - // TODO return true at top of the function renders all the following code useless
74   - return true;
75   -
76   - if ($dir == '') {
77   - $dir = $this->temp_dir;
78   - }
79   - $slash = "/";
80   -
81   - // Create arrays to store files and directories
82   - $dir_files = array();
83   - $dir_subdirs = array();
84   -
85   - // Make sure we can delete it
86   - chmod($dir, $CFG->directorypermissions);
87   -
88   - if ((($handle = opendir($dir))) == FALSE) {
89   - // The directory could not be opened
90   - return false;
91   - }
92   -
93   - // Loop through all directory entries, and construct two temporary arrays containing files and sub directories
94   - while(false !== ($entry = readdir($handle))) {
95   - if (is_dir($dir. $slash .$entry) && $entry != ".." && $entry != ".") {
96   - $dir_subdirs[] = $dir. $slash .$entry;
97   - }
98   - else if ($entry != ".." && $entry != ".") {
99   - $dir_files[] = $dir. $slash .$entry;
100   - }
101   - }
102   -
103   - // Delete all files in the curent directory return false and halt if a file cannot be removed
104   - $countdir_files = count($dir_files);
105   - for($i=0; $i<$countdir_files; $i++) {
106   - chmod($dir_files[$i], $CFG->directorypermissions);
107   - if (((unlink($dir_files[$i]))) == FALSE) {
  66 + // Find if we are importing a .dat file.
  67 + if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'dat') {
  68 + if (!is_readable($filename)) {
  69 + $this->error(get_string('filenotreadable', 'error'));
108 70 return false;
109 71 }
110   - }
111   -
112   - // Empty sub directories and then remove the directory
113   - $countdir_subdirs = count($dir_subdirs);
114   - for($i=0; $i<$countdir_subdirs; $i++) {
115   - chmod($dir_subdirs[$i], $CFG->directorypermissions);
116   - if ($this->clean_temp_dir($dir_subdirs[$i]) == FALSE) {
117   - return false;
  72 + // As we are not importing a .zip file,
  73 + // there is no imsmanifest, and it is not possible
  74 + // to parse it to find the file type.
  75 + // So we need to guess the file type by looking at the content.
  76 + // For now we will do that searching for a required tag.
  77 + // This is certainly not bullet-proof but works for all usual files.
  78 + $text = file_get_contents($filename);
  79 + if (strpos($text, '<questestinterop>')) {
  80 + $this->set_filetype(self::FILETYPE_QTI);
118 81 }
119   - else {
120   - if (rmdir($dir_subdirs[$i]) == FALSE) {
121   - return false;
122   - }
  82 + if (strpos($text, '<POOL>')) {
  83 + $this->set_filetype(self::FILETYPE_POOL);
123 84 }
124   - }
  85 + // In all other cases we are not able to handle this question file.
125 86
126   - // Close directory
127   - closedir($handle);
128   - if (rmdir($this->temp_dir) == FALSE) {
129   - return false;
  87 + // Readquestions is now expecting an array of strings.
  88 + return array($text);
130 89 }
131   - // Success, every thing is gone return true
132   - return true;
133   - }
134   -
135   - //Function to check if a directory exists and, optionally, create it
136   - function check_dir_exists($dir,$create=false) {
137   -
138   - global $CFG;
139   -
140   - $status = true;
141   - if(!is_dir($dir)) {
142   - if (!$create) {
143   - $status = false;
144   - } else {
145   - umask(0000);
146   - $status = mkdir ($dir,$CFG->directorypermissions);
  90 + // We are importing a zip file.
  91 + // Create name for temporary directory.
  92 + $unique_code = time();
  93 + $this->tempdir = make_temp_directory('bbquiz_import/' . $unique_code);
  94 + if (is_readable($filename)) {
  95 + if (!copy($filename, $this->tempdir . '/bboard.zip')) {
  96 + $this->error(get_string('cannotcopybackup', 'question'));
  97 + fulldelete($this->tempdir);
  98 + return false;
147 99 }
148   - }
149   - return $status;
150   - }
  100 + if (unzip_file($this->tempdir . '/bboard.zip', '', false)) {
  101 + $dom = new DomDocument();
151 102
152   - function importpostprocess() {
153   - /// Does any post-processing that may be desired
154   - /// Argument is a simple array of question ids that
155   - /// have just been added.
156   -
157   - // need to clean up temporary directory
158   - return $this->clean_temp_dir();
159   - }
160   -
161   - function copy_file_to_course($filename) {
162   - global $CFG, $COURSE;
163   - $filename = str_replace('\\','/',$filename);
164   - $fullpath = $this->temp_dir.'/res00001/'.$filename;
165   - $basename = basename($filename);
166   -
167   - $copy_to = $CFG->dataroot.'/'.$COURSE->id.'/bb_import';
168   -
169   - if ($this->check_dir_exists($copy_to,true)) {
170   - if(is_readable($fullpath)) {
171   - $copy_to.= '/'.$basename;
172   - if (!copy($fullpath, $copy_to)) {
  103 + if (!$dom->load($this->tempdir . '/imsmanifest.xml')) {
  104 + $this->error(get_string('errormanifest', 'qformat_blackboard_six'));
  105 + fulldelete($this->tempdir);
173 106 return false;
174 107 }
175   - else {
176   - return $copy_to;
177   - }
178   - }
179   - }
180   - else {
181   - return false;
182   - }
183   - }
184 108
185   - function readdata($filename) {
186   - /// Returns complete file with an array, one item per line
187   - global $CFG;
  109 + $xpath = new DOMXPath($dom);
188 110
189   - // if the extension is .dat we just return that,
190   - // if .zip we unzip the file and get the data
191   - $ext = substr($this->realfilename, strpos($this->realfilename,'.'), strlen($this->realfilename)-1);
192   - if ($ext=='.dat') {
193   - if (!is_readable($filename)) {
194   - print_error('filenotreadable', 'error');
195   - }
196   - return file($filename);
197   - }
  111 + // We starts from the root element.
  112 + $query = '//resources/resource';
  113 + $this->filebase = $this->tempdir;
  114 + $q_file = array();
198 115
199   - $unique_code = time();
200   - $temp_dir = $CFG->tempdir."/bbquiz_import/".$unique_code;
201   - $this->temp_dir = $temp_dir;
202   - if ($this->check_and_create_import_dir($unique_code)) {
203   - if(is_readable($filename)) {
204   - if (!copy($filename, "$temp_dir/bboard.zip")) {
205   - print_error('cannotcopybackup', 'question');
206   - }
207   - if(unzip_file("$temp_dir/bboard.zip", '', false)) {
208   - // assuming that the information is in res0001.dat
209   - // after looking at 6 examples this was always the case
210   - $q_file = "$temp_dir/res00001.dat";
211   - if (is_file($q_file)) {
212   - if (is_readable($q_file)) {
213   - $filearray = file($q_file);
214   - /// Check for Macintosh OS line returns (ie file on one line), and fix
215   - if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) {
216   - return explode("\r", $filearray[0]);
217   - } else {
218   - return $filearray;
219   - }
  116 + $examfiles = $xpath->query($query);
  117 + foreach ($examfiles as $examfile) {
  118 + if ($examfile->getAttribute('type') == 'assessment/x-bb-qti-test'
  119 + || $examfile->getAttribute('type') == 'assessment/x-bb-qti-pool') {
  120 +
  121 + if ($content = $this->get_filecontent($examfile->getAttribute('bb:file'))) {
  122 + $this->set_filetype(self::FILETYPE_QTI);
  123 + $q_file[] = $content;
220 124 }
221 125 }
222   - else {
223   - print_error('cannotfindquestionfile', 'questioni');
224   - }
225   - }
226   - else {
227   - print "filename: $filename<br />tempdir: $temp_dir <br />";
228   - print_error('cannotunzip', 'question');
229   - }
230   - }
231   - else {
232   - print_error('cannotreaduploadfile');
233   - }
234   - }
235   - else {
236   - print_error('cannotcreatetempdir');
237   - }
238   - }
239   -
240   - function save_question_options($question) {
241   - return true;
242   - }
243   -
244   -
245   -
246   - function readquestions ($lines) {
247   - /// Parses an array of lines into an array of questions,
248   - /// where each item is a question object as defined by
249   - /// readquestion().
250   -
251   - $text = implode($lines, " ");
252   - $xml = xmlize($text, 0);
253   -
254   - $raw_questions = $xml['questestinterop']['#']['assessment'][0]['#']['section'][0]['#']['item'];
255   - $questions = array();
256   -
257   - foreach($raw_questions as $quest) {
258   - $question = $this->create_raw_question($quest);
259   -
260   - switch($question->qtype) {
261   - case "Matching":
262   - $this->process_matching($question, $questions);
263   - break;
264   - case "Multiple Choice":
265   - $this->process_mc($question, $questions);
266   - break;
267   - case "Essay":
268   - $this->process_essay($question, $questions);
269   - break;
270   - case "Multiple Answer":
271   - $this->process_ma($question, $questions);
272   - break;
273   - case "True/False":
274   - $this->process_tf($question, $questions);
275   - break;
276   - case 'Fill in the Blank':
277   - $this->process_fblank($question, $questions);
278   - break;
279   - case 'Short Response':
280   - $this->process_essay($question, $questions);
281   - break;
282   - default:
283   - print "Unknown or unhandled question type: \"$question->qtype\"<br />";
284   - break;
285   - }
286   -
287   - }
288   - return $questions;
289   - }
290   -
291   -
292   -// creates a cleaner object to deal with for processing into moodle
293   -// the object created is NOT a moodle question object
294   -function create_raw_question($quest) {
295   -
296   - $question = new stdClass();
297   - $question->qtype = $quest['#']['itemmetadata'][0]['#']['bbmd_questiontype'][0]['#'];
298   - $question->id = $quest['#']['itemmetadata'][0]['#']['bbmd_asi_object_id'][0]['#'];
299   - $presentation->blocks = $quest['#']['presentation'][0]['#']['flow'][0]['#']['flow'];
300   -
301   - foreach($presentation->blocks as $pblock) {
302   -
303   - $block = NULL;
304   - $block->type = $pblock['@']['class'];
305   -
306   - switch($block->type) {
307   - case 'QUESTION_BLOCK':
308   - $sub_blocks = $pblock['#']['flow'];
309   - foreach($sub_blocks as $sblock) {
310   - //echo "Calling process_block from line 263<br>";
311   - $this->process_block($sblock, $block);
312   - }
313   - break;
314   -
315   - case 'RESPONSE_BLOCK':
316   - $choices = NULL;
317   - switch($question->qtype) {
318   - case 'Matching':
319   - $bb_subquestions = $pblock['#']['flow'];
320   - $sub_questions = array();
321   - foreach($bb_subquestions as $bb_subquestion) {
322   - $sub_question = NULL;
323   - $sub_question->ident = $bb_subquestion['#']['response_lid'][0]['@']['ident'];
324   - $this->process_block($bb_subquestion['#']['flow'][0], $sub_question);
325   - $bb_choices = $bb_subquestion['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label'];
326   - $choices = array();
327   - $this->process_choices($bb_choices, $choices);
328   - $sub_question->choices = $choices;
329   - if (!isset($block->subquestions)) {
330   - $block->subquestions = array();
331   - }
332   - $block->subquestions[] = $sub_question;
  126 + if ($examfile->getAttribute('type') == 'assessment/x-bb-pool') {
  127 + if ($examfile->getAttribute('baseurl')) {
  128 + $this->filebase = $this->tempdir. '/' . $examfile->getAttribute('baseurl');
333 129 }
334   - break;
335   - case 'Multiple Answer':
336   - $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'];
337   - $choices = array();
338   - $this->process_choices($bb_choices, $choices);
339   - $block->choices = $choices;
340   - break;
341   - case 'Essay':
342   - // Doesn't apply since the user responds with text input
343   - break;
344   - case 'Multiple Choice':
345   - $mc_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'];
346   - foreach($mc_choices as $mc_choice) {
347   - $choices = NULL;
348   - $choices = $this->process_block($mc_choice, $choices);
349   - $block->choices[] = $choices;
  130 + if ($content = $this->get_filecontent($examfile->getAttribute('file'))) {
  131 + $this->set_filetype(self::FILETYPE_POOL);
  132 + $q_file[] = $content;
350 133 }
351   - break;
352   - case 'Short Response':
353   - // do nothing?
354   - break;
355   - case 'Fill in the Blank':
356   - // do nothing?
357   - break;
358   - default:
359   - $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label'];
360   - $choices = array();
361   - $this->process_choices($bb_choices, $choices);
362   - $block->choices = $choices;
363   - }
364   - break;
365   - case 'RIGHT_MATCH_BLOCK':
366   - $matching_answerset = $pblock['#']['flow'];
367   -
368   - $answerset = array();
369   - foreach($matching_answerset as $answer) {
370   - // $answerset[] = $this->process_block($answer, $bb_answer);
371   - $bb_answer = null;
372   - $bb_answer->text = $answer['#']['flow'][0]['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'];
373   - $answerset[] = $bb_answer;
374   - }
375   - $block->matching_answerset = $answerset;
376   - break;
377   - default:
378   - print "UNHANDLED PRESENTATION BLOCK";
379   - break;
380   - }
381   - $question->{$block->type} = $block;
382   - }
383   -
384   - // determine response processing
385   - // there is a section called 'outcomes' that I don't know what to do with
386   - $resprocessing = $quest['#']['resprocessing'];
387   - $respconditions = $resprocessing[0]['#']['respcondition'];
388   - $reponses = array();
389   - if ($question->qtype == 'Matching') {
390   - $this->process_matching_responses($respconditions, $responses);
391   - }
392   - else {
393   - $this->process_responses($respconditions, $responses);
394   - }
395   - $question->responses = $responses;
396   - $feedbackset = $quest['#']['itemfeedback'];
397   - $feedbacks = array();
398   - $this->process_feedback($feedbackset, $feedbacks);
399   - $question->feedback = $feedbacks;
400   - return $question;
401   -}
402   -
403   -function process_block($cur_block, &$block) {
404   - global $COURSE, $CFG;
405   -
406   - $cur_type = $cur_block['@']['class'];
407   - switch($cur_type) {
408   - case 'FORMATTED_TEXT_BLOCK':
409   - $block->text = $this->strip_applet_tags_get_mathml($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']);
410   - break;
411   - case 'FILE_BLOCK':
412   - //revisit this to make sure it is working correctly
413   - // Commented out ['matapplication']..., etc. because I
414   - // noticed that when I imported a new Blackboard 6 file
415   - // and printed out the block, the tree did not extend past ['material'][0]['#'] - CT 8/3/06
416   - $block->file = $cur_block['#']['material'][0]['#'];//['matapplication'][0]['@']['uri'];
417   - if ($block->file != '') {
418   - // if we have a file copy it to the course dir and adjust its name to be visible over the web.
419   - $block->file = $this->copy_file_to_course($block->file);
420   - $block->file = $CFG->wwwroot.'/file.php/'.$COURSE->id.'/bb_import/'.basename($block->file);
421   - }
422   - break;
423   - case 'Block':
424   - if (isset($cur_block['#']['material'][0]['#']['mattext'][0]['#'])) {
425   - $block->text = $cur_block['#']['material'][0]['#']['mattext'][0]['#'];
426   - }
427   - else if (isset($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'])) {
428   - $block->text = $cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'];
429   - }
430   - else if (isset($cur_block['#']['response_label'])) {
431   - // this is a response label block
432   - $sub_blocks = $cur_block['#']['response_label'][0];
433   - if(!isset($block->ident)) {
434   - if(isset($sub_blocks['@']['ident'])) {
435   - $block->ident = $sub_blocks['@']['ident'];
436 134 }
437 135 }
438   - foreach($sub_blocks['#']['flow_mat'] as $sub_block) {
439   - $this->process_block($sub_block, $block);
440   - }
441   - }
442   - else {
443   - if (isset($cur_block['#']['flow_mat']) || isset($cur_block['#']['flow'])) {
444   - if (isset($cur_block['#']['flow_mat'])) {
445   - $sub_blocks = $cur_block['#']['flow_mat'];
446   - }
447   - elseif (isset($cur_block['#']['flow'])) {
448   - $sub_blocks = $cur_block['#']['flow'];
449   - }
450   - foreach ($sub_blocks as $sblock) {
451   - // this will recursively grab the sub blocks which should be of one of the other types
452   - $this->process_block($sblock, $block);
453   - }
454   - }
455   - }
456   - break;
457   - case 'LINK_BLOCK':
458   - // not sure how this should be included
459   - if (!empty($cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri'])) {
460   - $block->link = $cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri'];
461   - }
462   - else {
463   - $block->link = '';
464   - }
465   - break;
466   - }
467   - return $block;
468   -}
469 136
470   -function process_choices($bb_choices, &$choices) {
471   - foreach($bb_choices as $choice) {
472   - if (isset($choice['@']['ident'])) {
473   - $cur_choice = $choice['@']['ident'];
474   - }
475   - else { //for multiple answer
476   - $cur_choice = $choice['#']['response_label'][0];//['@']['ident'];
477   - }
478   - if (isset($choice['#']['flow_mat'][0])) { //for multiple answer
479   - $cur_block = $choice['#']['flow_mat'][0];
480   - // Reset $cur_choice to NULL because process_block is expecting an object
481   - // for the second argument and not a string, which is what is was set as
482   - // originally - CT 8/7/06
483   - $cur_choice = null;
484   - $this->process_block($cur_block, $cur_choice);
485   - }
486   - elseif (isset($choice['#']['response_label'])) {
487   - // Reset $cur_choice to NULL because process_block is expecting an object
488   - // for the second argument and not a string, which is what is was set as
489   - // originally - CT 8/7/06
490   - $cur_choice = null;
491   - $this->process_block($choice, $cur_choice);
492   - }
493   - $choices[] = $cur_choice;
494   - }
495   -}
496   -
497   -function process_matching_responses($bb_responses, &$responses) {
498   - foreach($bb_responses as $bb_response) {
499   - $response = NULL;
500   - if (isset($bb_response['#']['conditionvar'][0]['#']['varequal'])) {
501   - $response->correct = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['#'];
502   - $response->ident = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['@']['respident'];
503   - }
504   - else {
505   - $response->correct = 'Broken Question?';
506   - $response->ident = 'Broken Question?';
507   - }
508   - $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid'];
509   - $responses[] = $response;
510   - }
511   -}
512   -
513   -function process_responses($bb_responses, &$responses) {
514   - foreach($bb_responses as $bb_response) {
515   - //Added this line to instantiate $response.
516   - // Without instantiating the $response variable, the same object
517   - // gets added to the array
518   - $response = new stdClass();
519   - if (isset($bb_response['@']['title'])) {
520   - $response->title = $bb_response['@']['title'];
521   - }
522   - else {
523   - $reponse->title = $bb_response['#']['displayfeedback'][0]['@']['linkrefid'];
524   - }
525   - $reponse->ident = array();
526   - if (isset($bb_response['#']['conditionvar'][0]['#'])){//['varequal'][0]['#'])) {
527   - $response->ident[0] = $bb_response['#']['conditionvar'][0]['#'];//['varequal'][0]['#'];
528   - }
529   - else if (isset($bb_response['#']['conditionvar'][0]['#']['other'][0]['#'])) {
530   - $response->ident[0] = $bb_response['#']['conditionvar'][0]['#']['other'][0]['#'];
531   - }
532   -
533   - if (isset($bb_response['#']['conditionvar'][0]['#']['and'])){//[0]['#'])) {
534   - $responseset = $bb_response['#']['conditionvar'][0]['#']['and'];//[0]['#']['varequal'];
535   - foreach($responseset as $rs) {
536   - $response->ident[] = $rs['#'];
537   - if(!isset($response->feedback) and isset( $rs['@'] ) ) {
538   - $response->feedback = $rs['@']['respident'];
539   - }
540   - }
541   - }
542   - else {
543   - $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid'];
544   - }
545   -
546   - // determine what point value to give response
547   - if (isset($bb_response['#']['setvar'])) {
548   - switch ($bb_response['#']['setvar'][0]['#']) {
549   - case "SCORE.max":
550   - $response->fraction = 1;
551   - break;
552   - default:
553   - // I have only seen this being 0 or unset
554   - // there are probably fractional values of SCORE.max, but I'm not sure what they look like
555   - $response->fraction = 0;
556   - break;
557   - }
558   - }
559   - else {
560   - // just going to assume this is the case this is probably not correct.
561   - $response->fraction = 0;
562   - }
563   -
564   - $responses[] = $response;
565   - }
566   -}
567   -
568   -function process_feedback($feedbackset, &$feedbacks) {
569   - foreach($feedbackset as $bb_feedback) {
570   - // Added line $feedback=null so that $feedback does not get reused in the loop
571   - // and added the the $feedbacks[] array multiple times
572   - $feedback = null;
573   - $feedback->ident = $bb_feedback['@']['ident'];
574   - if (isset($bb_feedback['#']['flow_mat'][0])) {
575   - $this->process_block($bb_feedback['#']['flow_mat'][0], $feedback);
576   - }
577   - elseif (isset($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0])) {
578   - $this->process_block($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0], $feedback);
579   - }
580   - $feedbacks[] = $feedback;
581   - }
582   -}
583   -
584   -/**
585   - * Create common parts of question
586   - */
587   -function process_common( $quest ) {
588   - $question = $this->defaultquestion();
589   - $question->questiontext = $quest->QUESTION_BLOCK->text;
590   - $question->name = shorten_text( $quest->id, 250 );
591   -
592   - return $question;
593   -}
594   -
595   -//----------------------------------------
596   -// Process True / False Questions
597   -//----------------------------------------
598   -function process_tf($quest, &$questions) {
599   - $question = $this->process_common( $quest );
600   -
601   - $question->qtype = TRUEFALSE;
602   - $question->single = 1; // Only one answer is allowed
603   - // 0th [response] is the correct answer.
604   - $responses = $quest->responses;
605   - $correctresponse = $responses[0]->ident[0]['varequal'][0]['#'];
606   - if ($correctresponse != 'false') {
607   - $correct = true;
608   - }
609   - else {
610   - $correct = false;
611   - }
612   -
613   - foreach($quest->feedback as $fb) {
614   - $fback->{$fb->ident} = $fb->text;
615   - }
616   -
617   - if ($correct) { // true is correct
618   - $question->answer = 1;
619   - $question->feedbacktrue = $fback->correct;
620   - $question->feedbackfalse = $fback->incorrect;
621   - } else { // false is correct
622   - $question->answer = 0;
623   - $question->feedbacktrue = $fback->incorrect;
624   - $question->feedbackfalse = $fback->correct;
625   - }
626   - $question->correctanswer = $question->answer;
627   - $questions[] = $question;
628   -}
629   -
630   -
631   -//----------------------------------------
632   -// Process Fill in the Blank
633   -//----------------------------------------
634   -function process_fblank($quest, &$questions) {
635   - $question = $this->process_common( $quest );
636   - $question->qtype = SHORTANSWER;
637   - $question->single = 1;
638   -
639   - $answers = array();
640   - $fractions = array();
641   - $feedbacks = array();
642   -
643   - // extract the feedback
644   - $feedback = array();
645   - foreach($quest->feedback as $fback) {
646   - if (isset($fback->ident)) {
647   - if ($fback->ident == 'correct' || $fback->ident == 'incorrect') {
648   - $feedback[$fback->ident] = $fback->text;
649   - }
650   - }
651   - }
652   -
653   - foreach($quest->responses as $response) {
654   - if(isset($response->title)) {
655   - if (isset($response->ident[0]['varequal'][0]['#'])) {
656   - //for BB Fill in the Blank, only interested in correct answers
657   - if ($response->feedback = 'correct') {
658   - $answers[] = $response->ident[0]['varequal'][0]['#'];
659   - $fractions[] = 1;
660   - if (isset($feedback['correct'])) {
661   - $feedbacks[] = $feedback['correct'];
662   - }
663   - else {
664   - $feedbacks[] = '';
665   - }
  137 + if ($q_file) {
  138 + return $q_file;
  139 + } else {
  140 + $this->error(get_string('cannotfindquestionfile', 'question'));
  141 + fulldelete($this->tempdir);
666 142 }
667   - }
668   -
669   - }
670   - }
671   -
672   - //Adding catchall to so that students can see feedback for incorrect answers when they enter something the
673   - //instructor did not enter
674   - $answers[] = '*';
675   - $fractions[] = 0;
676   - if (isset($feedback['incorrect'])) {
677   - $feedbacks[] = $feedback['incorrect'];
678   - }
679   - else {
680   - $feedbacks[] = '';
681   - }
682   -
683   - $question->answer = $answers;
684   - $question->fraction = $fractions;
685   - $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of
686   -
687   - if (!empty($question)) {
688   - $questions[] = $question;
689   - }
690   -
691   -}
692   -
693   -//----------------------------------------
694   -// Process Multiple Choice Questions
695   -//----------------------------------------
696   -function process_mc($quest, &$questions) {
697   - $question = $this->process_common( $quest );
698   - $question->qtype = MULTICHOICE;
699   - $question->single = 1;
700   -
701   - $feedback = array();
702   - foreach($quest->feedback as $fback) {
703   - $feedback[$fback->ident] = $fback->text;
704   - }
705   -
706   - foreach($quest->responses as $response) {
707   - if (isset($response->title)) {
708   - if ($response->title == 'correct') {
709   - // only one answer possible for this qtype so first index is correct answer
710   - $correct = $response->ident[0]['varequal'][0]['#'];
711   - }
712   - }
713   - else {
714   - // fallback method for when the title is not set
715   - if ($response->feedback == 'correct') {
716   - // only one answer possible for this qtype so first index is correct answer
717   - $correct = $response->ident[0]['varequal'][0]['#']; // added [0]['varequal'][0]['#'] to $response->ident - CT 8/9/06
718   - }
719   - }
720   - }
721   -
722   - $i = 0;
723   - foreach($quest->RESPONSE_BLOCK->choices as $response) {
724   - $question->answer[$i] = $response->text;
725   - if ($correct == $response->ident) {
726   - $question->fraction[$i] = 1;
727   - // this is a bit of a hack to catch the feedback... first we see if a 'correct' feedback exists
728   - // then specific feedback for this question (maybe this should be switched?, but from my example
729   - // question pools I have not seen response specific feedback, only correct or incorrect feedback
730   - if (!empty($feedback['correct'])) {
731   - $question->feedback[$i] = $feedback['correct'];
732   - }
733   - elseif (!empty($feedback[$i])) {
734   - $question->feedback[$i] = $feedback[$i];
735   - }
736   - else {
737   - // failsafe feedback (should be '' instead?)
738   - $question->feedback[$i] = "correct";
739   - }
740   - }
741   - else {
742   - $question->fraction[$i] = 0;
743   - if (!empty($feedback['incorrect'])) {
744   - $question->feedback[$i] = $feedback['incorrect'];
745   - }
746   - elseif (!empty($feedback[$i])) {
747   - $question->feedback[$i] = $feedback[$i];
748   - }
749   - else {
750   - // failsafe feedback (should be '' instead?)
751   - $question->feedback[$i] = 'incorrect';
752   - }
753   - }
754   - $i++;
755   - }
756   -
757   - if (!empty($question)) {
758   - $questions[] = $question;
759   - }
760   -}
761   -
762   -//----------------------------------------
763   -// Process Multiple Choice Questions With Multiple Answers
764   -//----------------------------------------
765   -function process_ma($quest, &$questions) {
766   - $question = $this->process_common( $quest ); // copied this from process_mc
767   - $question->qtype = MULTICHOICE;
768   - $question->single = 0; // More than one answer allowed
769   -
770   - $answers = $quest->responses;
771   - $correct_answers = array();
772   - foreach($answers as $answer) {
773   - if($answer->title == 'correct') {
774   - $answerset = $answer->ident[0]['and'][0]['#']['varequal'];
775   - foreach($answerset as $ans) {
776   - $correct_answers[] = $ans['#'];
777   - }
778   - }
779   - }
780   -
781   - foreach ($quest->feedback as $fb) {
782   - $feedback->{$fb->ident} = trim($fb->text);
783   - }
784   -
785   - $correct_answer_count = count($correct_answers);
786   - $choiceset = $quest->RESPONSE_BLOCK->choices;
787   - $i = 0;
788   - foreach($choiceset as $choice) {
789   - $question->answer[$i] = trim($choice->text);
790   - if (in_array($choice->ident, $correct_answers)) {
791   - // correct answer
792   - $question->fraction[$i] = floor(100000/$correct_answer_count)/100000; // strange behavior if we have more than 5 decimal places
793   - $question->feedback[$i] = $feedback->correct;
794   - }
795   - else {
796   - // wrong answer
797   - $question->fraction[$i] = 0;
798   - $question->feedback[$i] = $feedback->incorrect;
799   - }
800   - $i++;
801   - }
802   -
803   - $questions[] = $question;
804   -}
805   -
806   -//----------------------------------------
807   -// Process Essay Questions
808   -//----------------------------------------
809   -function process_essay($quest, &$questions) {
810   -// this should be rewritten to accomodate moodle 1.6 essay question type eventually
811   -
812   - if (defined("ESSAY")) {
813   - // treat as short answer
814   - $question = $this->process_common( $quest ); // copied this from process_mc
815   - $question->qtype = ESSAY;
816   -
817   - $question->feedback = array();
818   - // not sure where to get the correct answer from
819   - foreach($quest->feedback as $feedback) {
820   - // Added this code to put the possible solution that the
821   - // instructor gives as the Moodle answer for an essay question
822   - if ($feedback->ident == 'solution') {
823   - $question->feedback = $feedback->text;
824   - }
825   - }
826   - //Added because essay/questiontype.php:save_question_option is expecting a
827   - //fraction property - CT 8/10/06
828   - $question->fraction[] = 1;
829   - if (!empty($question)) {
830   - $questions[]=$question;
831   - }
832   - }
833   - else {
834   - print "Essay question types are not handled because the quiz question type 'Essay' does not exist in this installation of Moodle<br/>";
835   - print "&nbsp;&nbsp;&nbsp;&nbsp;Omitted Question: ".$quest->QUESTION_BLOCK->text.'<br/><br/>';
836   - }
837   -}
838   -
839   -//----------------------------------------
840   -// Process Matching Questions
841   -//----------------------------------------
842   -function process_matching($quest, &$questions) {
843   - // renderedmatch is an optional plugin, so we need to check if it is defined
844   - if (question_bank::is_qtype_installed('renderedmatch')) {
845   - $question = $this->process_common($quest);
846   - $question->valid = true;
847   - $question->qtype = 'renderedmatch';
848   -
849   - foreach($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) {
850   - foreach($quest->responses as $rid => $resp) {
851   - if ($resp->ident == $subq->ident) {
852   - $correct = $resp->correct;
853   - $feedback = $resp->feedback;
854   - }
855   - }
856   -
857   - foreach($subq->choices as $cid => $choice) {
858   - if ($choice == $correct) {
859   - $question->subquestions[] = $subq->text;
860   - $question->subanswers[] = $quest->RIGHT_MATCH_BLOCK->matching_answerset[$cid]->text;
861   - }
862   - }
863   - }
864   -
865   - // check format
866   - $status = true;
867   - if ( count($quest->RESPONSE_BLOCK->subquestions) > count($quest->RIGHT_MATCH_BLOCK->matching_answerset) || count($question->subquestions) < 2) {
868   - $status = false;
869   - }
870   - else {
871   - // need to redo to make sure that no two questions have the same answer (rudimentary now)
872   - foreach($question->subanswers as $qstn) {
873   - if(isset($previous)) {
874   - if ($qstn == $previous) {
875   - $status = false;
876   - }
877   - }
878   - $previous = $qstn;
879   - if ($qstn == '') {
880   - $status = false;
881   - }
882   - }
883   - }
884   -
885   - if ($status) {
886   - $questions[] = $question;
887   - }
888   - else {
889   - global $COURSE, $CFG;
890   - print '<table class="boxaligncenter" border="1">';
891   - print '<tr><td colspan="2" style="background-color:#FF8888;">This matching question is malformed. Please ensure there are no blank answers, no two questions have the same answer, and/or there are correct answers for each question. There must be at least as many subanswers as subquestions, and at least one subquestion.</td></tr>';
892   -
893   - print "<tr><td>Question:</td><td>".$quest->QUESTION_BLOCK->text;
894   - if (isset($quest->QUESTION_BLOCK->file)) {
895   - print '<br/><font color="red">There is a subfile contained in the zipfile that has been copied to course files: bb_import/'.basename($quest->QUESTION_BLOCK->file).'</font>';
896   - if (preg_match('/(gif|jpg|jpeg|png)$/i', $quest->QUESTION_BLOCK->file)) {
897   - print '<img src="'.$CFG->wwwroot.'/file.php/'.$COURSE->id.'/bb_import/'.basename($quest->QUESTION_BLOCK->file).'" />';
898   - }
899   - }
900   - print "</td></tr>";
901   - print "<tr><td>Subquestions:</td><td><ul>";
902   - foreach($quest->responses as $rs) {
903   - $correct_responses->{$rs->ident} = $rs->correct;
904   - }
905   - foreach($quest->RESPONSE_BLOCK->subquestions as $subq) {
906   - print '<li>'.$subq->text.'<ul>';
907   - foreach($subq->choices as $id=>$choice) {
908   - print '<li>';
909   - if ($choice == $correct_responses->{$subq->ident}) {
910   - print '<font color="green">';
911   - }
912   - else {
913   - print '<font color="red">';
914   - }
915   - print $quest->RIGHT_MATCH_BLOCK->matching_answerset[$id]->text.'</font></li>';
916   - }
917   - print '</ul>';
918   - }
919   - print '</ul></td></tr>';
920   -
921   - print '<tr><td>Feedback:</td><td><ul>';
922   - foreach($quest->feedback as $fb) {
923   - print '<li>'.$fb->ident.': '.$fb->text.'</li>';
924   - }
925   - print '</ul></td></tr></table>';
  143 + } else {
  144 + $this->error(get_string('cannotunzip', 'question'));
  145 + fulldelete($this->temp_dir);
  146 + }
  147 + } else {
  148 + $this->error(get_string('cannotreaduploadfile', 'error'));
  149 + fulldelete($this->tempdir);
  150 + }
  151 + return false;
  152 + }
  153 +
  154 + /**
  155 + * Parse the array of strings into an array of questions.
  156 + * Each string is the content of a .dat questions file.
  157 + * This *could* burn memory - but it won't happen that much
  158 + * so fingers crossed!
  159 + * @param array of strings from the input file.
  160 + * @param stdClass $context
  161 + * @return array (of objects) question objects.
  162 + */