MDL-34738 qformat blackboard: blackboard format is broken
authorJean-Michel Vedrine <vedrine@vedrine.org>
Mon, 6 Aug 2012 16:16:00 +0000 (18:16 +0200)
committerJean-Michel Vedrine <vedrine@vedrine.org>
Sat, 18 Aug 2012 08:55:26 +0000 (10:55 +0200)
question/format.php
question/format/blackboard/format.php
question/format/blackboard/lang/en/qformat_blackboard.php
question/format/blackboard/tests/blackboardformat_test.php [new file with mode: 0644]
question/format/blackboard/tests/fixtures/sample_blackboard.dat [new file with mode: 0644]
question/format/blackboard/version.php

index 0ccdbce..a0e9df4 100644 (file)
@@ -898,3 +898,57 @@ class qformat_default {
                 $question->questiontextformat, $formatoptions), 0, false);
     }
 }
+
+class qformat_based_on_xml extends qformat_default {
+
+    /**
+     * Return the array moodle is expecting
+     * for an HTML text. No processing is done on $text.
+     * qformat classes that want to process $text
+     * for instance to import external images files
+     * and recode urls in $text must overwrite this method.
+     * @param array $text some HTML text string
+     * @return array with keys text, format and files.
+     */
+    public function text_field($text) {
+        return array(
+            'text' => trim($text),
+            'format' => FORMAT_HTML,
+            'files' => array(),
+        );
+    }
+
+    /**
+     * Return the value of a node, given a path to the node
+     * if it doesn't exist return the default value.
+     * @param array xml data to read
+     * @param array path path to node expressed as array
+     * @param mixed default
+     * @param bool istext process as text
+     * @param string error if set value must exist, return false and issue message if not
+     * @return mixed value
+     */
+    public function getpath($xml, $path, $default, $istext=false, $error='') {
+        foreach ($path as $index) {
+            if (!isset($xml[$index])) {
+                if (!empty($error)) {
+                    $this->error($error);
+                    return false;
+                } else {
+                    return $default;
+                }
+            }
+
+            $xml = $xml[$index];
+        }
+
+        if ($istext) {
+            if (!is_string($xml)) {
+                $this->error(get_string('invalidxml', 'qformat_xml'));
+            }
+            $xml = trim($xml);
+        }
+
+        return $xml;
+    }
+}
index 9ce23d0..1626489 100644 (file)
@@ -17,8 +17,7 @@
 /**
  * Blackboard question importer.
  *
- * @package    qformat
- * @subpackage blackboard
+ * @package qformat_blackboard
  * @copyright  2003 Scott Elliott
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
@@ -26,7 +25,7 @@
 
 defined('MOODLE_INTERNAL') || die();
 
-require_once ($CFG->libdir . '/xmlize.php');
+require_once($CFG->libdir . '/xmlize.php');
 
 
 /**
@@ -35,19 +34,67 @@ require_once ($CFG->libdir . '/xmlize.php');
  * @copyright  2003 Scott Elliott
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class qformat_blackboard extends qformat_default {
+class qformat_blackboard extends qformat_based_on_xml {
+    // Is the current question's question text escaped HTML (true for most if not all Blackboard files).
+    public $ishtml = true;
+
 
     public function provide_import() {
         return true;
     }
 
-    function readquestions($lines) {
-        /// Parses an array of lines into an array of questions,
-        /// where each item is a question object as defined by
-        /// readquestion().
+    public function mime_type() {
+        return mimeinfo('type', '.dat');
+    }
+
+    /**
+     * Some softwares put entities in exported files.
+     * This method try to clean up known problems.
+     * @param string str string to correct
+     * @return string the corrected string
+     */
+    public function cleaninput($str) {
+        if (!$this->ishtml) {
+            return $str;
+        }
+        $html_code_list = array(
+            "&#039;" => "'",
+            "&#8217;" => "'",
+            "&#091;" => "[",
+            "&#8220;" => "\"",
+            "&#8221;" => "\"",
+            "&#093;" => "]",
+            "&#039;" => "'",
+            "&#8211;" => "-",
+            "&#8212;" => "-",
+        );
+        $str = strtr($str, $html_code_list);
+        // Use textlib entities_to_utf8 function to convert only numerical entities.
+        $str = textlib::entities_to_utf8($str, false);
+        return $str;
+    }
 
-        $text = implode($lines, " ");
-        $xml = xmlize($text, 0);
+    /**
+     * Parse the array of lines into an array of questions
+     * this *could* burn memory - but it won't happen that much
+     * so fingers crossed!
+     * @param array of lines from the input file.
+     * @param stdClass $context
+     * @return array (of objects) question objects.
+     */
+    protected function readquestions($lines) {
+
+        $text = implode($lines, ' ');
+        unset($lines);
+
+        // This converts xml to big nasty data structure,
+        // the 0 means keep white space as it is.
+        try {
+            $xml = xmlize($text, 0, 'UTF-8', true);
+        } catch (xml_format_exception $e) {
+            $this->error($e->getMessage(), '');
+            return false;
+        }
 
         $questions = array();
 
@@ -61,341 +108,405 @@ class qformat_blackboard extends qformat_default {
         return $questions;
     }
 
-//----------------------------------------
-// Process Essay Questions
-//----------------------------------------
-    function process_essay($xml, &$questions ) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_ESSAY"])) {
-            $essayquestions = $xml["POOL"]["#"]["QUESTION_ESSAY"];
+    /**
+     * Do question import processing common to every qtype.
+     * @param array $questiondata the xml tree related to the current question
+     * @return object initialized question object.
+     */
+    public function process_common($questiondata) {
+        global $CFG;
+
+        // This routine initialises the question object.
+        $question = $this->defaultquestion();
+
+        // Determine if the question is already escaped html.
+        $this->ishtml = $this->getpath($questiondata,
+                array('#', 'BODY', 0, '#', 'FLAGS', 0, '#', 'ISHTML', 0, '@', 'value'),
+                false, false);
+
+        // Put questiontext in question object.
+        $text = $this->getpath($questiondata,
+                array('#', 'BODY', 0, '#', 'TEXT', 0, '#'),
+                '', true, get_string('importnotext', 'qformat_blackboard'));
+
+        if ($this->ishtml) {
+            $question->questiontext = $this->cleaninput($text);
+            $question->questiontextformat = FORMAT_HTML;
+            $question->questiontextfiles = array();
+
+        } else {
+            $question->questiontext = $text;
         }
-        else {
-            return;
+        // Put name in question object. We must ensure it is not empty and it is less than 250 chars.
+        $question->name = shorten_text(strip_tags($question->questiontext), 200);
+        $question->name = substr($question->name, 0, 250);
+        if (!$question->name) {
+            $id = $this->getpath($questiondata,
+                    array('@', 'id'), '',  true);
+            $question->name = get_string('defaultname', 'qformat_blackboard' , $id);
         }
 
-        foreach ($essayquestions as $essayquestion) {
+        $question->generalfeedback = '';
+        $question->generalfeedbackformat = FORMAT_HTML;
+        $question->generalfeedbackfiles = array();
 
-            $question = $this->defaultquestion();
+        // TODO : read the mark from the POOL TITLE QUESTIONLIST section.
+        $question->defaultmark = 1;
+        return $question;
+    }
+
+    /**
+     * Process Essay Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_essay($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_ESSAY'), false, false)) {
+            $essayquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_ESSAY'), false, false);
+        } else {
+            return;
+        }
 
-            $question->qtype = ESSAY;
+        foreach ($essayquestions as $thisquestion) {
 
-            // determine if the question is already escaped html
-            $ishtml = $essayquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
+            $question = $this->process_common($thisquestion);
 
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($essayquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]));
-            }
+            $question->qtype = 'essay';
 
-            // put name in question object
-            $question->name = substr($question->questiontext, 0, 254);
             $question->answer = '';
+            $answer = $this->getpath($thisquestion,
+                    array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true);
+            $question->graderinfo =  $this->text_field($this->cleaninput($answer));
             $question->feedback = '';
+            $question->responseformat = 'editor';
+            $question->responsefieldlines = 15;
+            $question->attachments = 0;
             $question->fraction = 0;
 
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process True / False Questions
-    //----------------------------------------
-    function process_tf($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_TRUEFALSE"])) {
-            $tfquestions = $xml["POOL"]["#"]["QUESTION_TRUEFALSE"];
-        }
-        else {
+    /**
+     * Process True / False Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_tf($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false)) {
+            $tfquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_TRUEFALSE'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($tfquestions); $i++) {
-
-            $question = $this->defaultquestion();
+        foreach ($tfquestions as $thisquestion) {
 
-            $question->qtype = TRUEFALSE;
-            $question->single = 1; // Only one answer is allowed
+            $question = $this->process_common($thisquestion);
 
-            $thisquestion = $tfquestions[$i];
+            $question->qtype = 'truefalse';
+            $question->single = 1; // Only one answer is allowed.
 
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false);
 
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
-            }
-            // put name in question object
-            $question->name = shorten_text($question->questiontext, 254);
+            $correct_answer = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
+                    '', true);
 
-            $choices = $thisquestion["#"]["ANSWER"];
-
-            $correct_answer = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"][0]["@"]["answer_id"];
-
-            // first choice is true, second is false.
-            $id = $choices[0]["@"]["id"];
-
-            if (strcmp($id, $correct_answer) == 0) {  // true is correct
+            // First choice is true, second is false.
+            $id = $this->getpath($choices[0], array('@', 'id'), '', true);
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            if (strcmp($id,  $correct_answer) == 0) {  // True is correct.
                 $question->answer = 1;
-                $question->feedbacktrue = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
-                $question->feedbackfalse = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
-            } else {  // false is correct
+                $question->feedbacktrue = $this->text_field($this->cleaninput($correctfeedback));
+                $question->feedbackfalse = $this->text_field($this->cleaninput($incorrectfeedback));
+            } else {  // False is correct.
                 $question->answer = 0;
-                $question->feedbacktrue = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
-                $question->feedbackfalse = trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
+                $question->feedbacktrue = $this->text_field($this->cleaninput($incorrectfeedback));
+                $question->feedbackfalse = $this->text_field($this->cleaninput($correctfeedback));
             }
             $question->correctanswer = $question->answer;
             $questions[] = $question;
-          }
+        }
     }
 
-    //----------------------------------------
-    // Process Multiple Choice Questions
-    //----------------------------------------
-    function process_mc($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_MULTIPLECHOICE"])) {
-            $mcquestions = $xml["POOL"]["#"]["QUESTION_MULTIPLECHOICE"];
-        }
-        else {
+    /**
+     * Process Multiple Choice Questions with single answer
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_mc($xml, &$questions) {
+
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false)) {
+            $mcquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MULTIPLECHOICE'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($mcquestions); $i++) {
-
-            $question = $this->defaultquestion();
-
-            $question->qtype = MULTICHOICE;
-            $question->single = 1; // Only one answer is allowed
-
-            $thisquestion = $mcquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
-            }
-
-            // put name of question in question object, careful of length
-            $question->name = shorten_text($question->questiontext, 254);
-
-            $choices = $thisquestion["#"]["ANSWER"];
-            for ($j = 0; $j < sizeof ($choices); $j++) {
-
-                $choice = trim($choices[$j]["#"]["TEXT"][0]["#"]);
-                // put this choice in the question object.
-                if ($ishtml) {
-                    $question->answer[$j] = html_entity_decode($choice,ENT_QUOTES,'UTF-8');
-                }
-                $question->answer[$j] = $question->answer[$j];
-
-                $id = $choices[$j]["@"]["id"];
-                $correct_answer_id = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"][0]["@"]["answer_id"];
-                // if choice is the answer, give 100%, otherwise give 0%
-                if (strcmp ($id, $correct_answer_id) == 0) {
-                    $question->fraction[$j] = 1;
-                    if ($ishtml) {
-                        $question->feedback[$j] = html_entity_decode(trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]),ENT_QUOTES,'UTF-8');
-                    }
-                    $question->feedback[$j] = $question->feedback[$j];
+        foreach ($mcquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback));
+            $question->partiallycorrectfeedback = $this->text_field('');
+            $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+
+            $question->qtype = 'multichoice';
+            $question->single = 1; // Only one answer is allowed.
+
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            $correct_answer_id = $this->getpath($thisquestion,
+                        array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'),
+                        '', true);
+            foreach ($choices as $choice) {
+                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
+                // Put this choice in the question object.
+                $question->answer[] =  $this->text_field($this->cleaninput($choicetext));
+
+                $choice_id = $this->getpath($choice, array('@', 'id'), '', true);
+                // If choice is the right answer, give 100% mark, otherwise give 0%.
+                if (strcmp ($choice_id, $correct_answer_id) == 0) {
+                    $question->fraction[] = 1;
                 } else {
-                    $question->fraction[$j] = 0;
-                    if ($ishtml) {
-                        $question->feedback[$j] = html_entity_decode(trim(@$thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]),ENT_QUOTES,'UTF-8');
-                    }
-                    $question->feedback[$j] = $question->feedback[$j];
+                    $question->fraction[] = 0;
                 }
+                // There is never feedback specific to each choice.
+                $question->feedback[] =  $this->text_field('');
             }
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process Multiple Choice Questions With Multiple Answers
-    //----------------------------------------
-    function process_ma($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_MULTIPLEANSWER"])) {
-            $maquestions = $xml["POOL"]["#"]["QUESTION_MULTIPLEANSWER"];
-        }
-        else {
+    /**
+     * Process Multiple Choice Questions With Multiple Answers
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_ma($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false)) {
+            $maquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MULTIPLEANSWER'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($maquestions); $i++) {
-
-            $question = $this->defaultquestion();
-
-            $question->qtype = MULTICHOICE;
+        foreach ($maquestions as $thisquestion) {
+            $question = $this->process_common($thisquestion);
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback));
+            // As there is no partially correct feedback we use incorrect one.
+            $question->partiallycorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+            $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+
+            $question->qtype = 'multichoice';
             $question->defaultmark = 1;
-            $question->single = 0; // More than one answers allowed
-            $question->image = ""; // No images with this format
-
-            $thisquestion = $maquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
+            $question->single = 0; // More than one answers allowed.
+
+            $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            $correct_answer_ids = array();
+            foreach ($this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) {
+                if ($correctanswer) {
+                    $correct_answer_ids[] = $this->getpath($correctanswer,
+                            array('@', 'answer_id'),
+                            '', true);
+                }
             }
-            // put name of question in question object
-            $question->name = shorten_text($question->questiontext, 254);
+            $fraction = 1/count($correct_answer_ids);
 
-            $choices = $thisquestion["#"]["ANSWER"];
-            $correctanswers = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"];
+            foreach ($choices as $choice) {
+                $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true);
+                // Put this choice in the question object.
+                $question->answer[] =  $this->text_field($this->cleaninput($choicetext));
 
-            for ($j = 0; $j < sizeof ($choices); $j++) {
+                $choice_id = $this->getpath($choice, array('@', 'id'), '', true);
 
-                $choice = trim($choices[$j]["#"]["TEXT"][0]["#"]);
-                // put this choice in the question object.
-                $question->answer[$j] = $choice;
+                $iscorrect = in_array($choice_id, $correct_answer_ids);
 
-                $correctanswercount = sizeof($correctanswers);
-                $id = $choices[$j]["@"]["id"];
-                $iscorrect = 0;
-                for ($k = 0; $k < $correctanswercount; $k++) {
-
-                    $correct_answer_id = trim($correctanswers[$k]["@"]["answer_id"]);
-                    if (strcmp ($id, $correct_answer_id) == 0) {
-                        $iscorrect = 1;
-                    }
-
-                }
                 if ($iscorrect) {
-                    $question->fraction[$j] = floor(100000/$correctanswercount)/100000; // strange behavior if we have more than 5 decimal places
-                    $question->feedback[$j] = trim($thisquestion["#"]["GRADABLE"][$j]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
+                    $question->fraction[] = $fraction;
                 } else {
-                    $question->fraction[$j] = 0;
-                    $question->feedback[$j] = trim($thisquestion["#"]["GRADABLE"][$j]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
+                    $question->fraction[] = 0;
                 }
+                // There is never feedback specific to each choice.
+                $question->feedback[] =  $this->text_field('');
             }
-
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process Fill in the Blank Questions
-    //----------------------------------------
-    function process_fib($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_FILLINBLANK"])) {
-            $fibquestions = $xml["POOL"]["#"]["QUESTION_FILLINBLANK"];
-        }
-        else {
+    /**
+     * Process Fill in the Blank Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_fib($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false)) {
+            $fibquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_FILLINBLANK'), false, false);
+        } else {
             return;
         }
 
-        for ($i = 0; $i < sizeof ($fibquestions); $i++) {
-            $question = $this->defaultquestion();
-
-            $question->qtype = SHORTANSWER;
-            $question->usecase = 0; // Ignore case
-
-            $thisquestion = $fibquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
-            }
-            // put name of question in question object
-            $question->name = shorten_text($question->questiontext, 254);
-
-            $answer = trim($thisquestion["#"]["ANSWER"][0]["#"]["TEXT"][0]["#"]);
-
-            $question->answer[] = $answer;
-            $question->fraction[] = 1;
-            $question->feedback = array();
-
-            if (is_array( $thisquestion['#']['GRADABLE'][0]['#'] )) {
-                $question->feedback[0] = trim($thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_CORRECT"][0]["#"]);
-            }
-            else {
-                $question->feedback[0] = '';
-            }
-            if (is_array( $thisquestion["#"]["GRADABLE"][0]["#"] )) {
-                $question->feedback[1] = trim($thisquestion["#"]["GRADABLE"][0]["#"]["FEEDBACK_WHEN_INCORRECT"][0]["#"]);
-            }
-            else {
-                $question->feedback[1] = '';
+        foreach ($fibquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+
+            $question->qtype = 'shortanswer';
+            $question->usecase = 0; // Ignore case.
+
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $answers = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false);
+            foreach ($answers as $answer) {
+                $question->answer[] = $this->getpath($answer,
+                        array('#', 'TEXT', 0, '#'), '', true);
+                $question->fraction[] = 1;
+                $question->feedback[] = $this->text_field($this->cleaninput($correctfeedback));
             }
+            $question->answer[] = '*';
+            $question->fraction[] = 0;
+            $question->feedback[] = $this->text_field($this->cleaninput($incorrectfeedback));
 
             $questions[] = $question;
         }
     }
 
-    //----------------------------------------
-    // Process Matching Questions
-    //----------------------------------------
-    function process_matching($xml, &$questions) {
-
-        if (isset($xml["POOL"]["#"]["QUESTION_MATCH"])) {
-            $matchquestions = $xml["POOL"]["#"]["QUESTION_MATCH"];
-        }
-        else {
+    /**
+     * Process Matching Questions
+     * @param array xml the xml tree
+     * @param array questions the questions already parsed
+     */
+    public function process_matching($xml, &$questions) {
+        if ($this->getpath($xml, array('POOL', '#', 'QUESTION_MATCH'), false, false)) {
+            $matchquestions = $this->getpath($xml,
+                    array('POOL', '#', 'QUESTION_MATCH'), false, false);
+        } else {
             return;
         }
-
-        for ($i = 0; $i < sizeof ($matchquestions); $i++) {
-
-            $question = $this->defaultquestion();
-
-            $question->qtype = MATCH;
-
-            $thisquestion = $matchquestions[$i];
-
-            // determine if the question is already escaped html
-            $ishtml = $thisquestion["#"]["BODY"][0]["#"]["FLAGS"][0]["#"]["ISHTML"][0]["@"]["value"];
-
-            // put questiontext in question object
-            if ($ishtml) {
-                $question->questiontext = html_entity_decode(trim($thisquestion["#"]["BODY"][0]["#"]["TEXT"][0]["#"]),ENT_QUOTES,'UTF-8');
+        // Blackboard questions can't be imported in core Moodle without a loss in data,
+        // as core match question don't allow HTML in subanswers. The contributed ddmatch
+        // question type support HTML in subanswers.
+        // The ddmatch question type is not part of core, so we need to check if it is defined.
+        $ddmatch_is_installed = question_bank::is_qtype_installed('ddmatch');
+
+        foreach ($matchquestions as $thisquestion) {
+
+            $question = $this->process_common($thisquestion);
+            if ($ddmatch_is_installed) {
+                $question->qtype = 'ddmatch';
+            } else {
+                $question->qtype = 'match';
             }
-            // put name of question in question object
-            $question->name = shorten_text($question->questiontext, 254);
-
-            $choices = $thisquestion["#"]["CHOICE"];
-            for ($j = 0; $j < sizeof ($choices); $j++) {
-
-                $subquestion = NULL;
-
-                $choice = $choices[$j]["#"]["TEXT"][0]["#"];
-                $choice_id = $choices[$j]["@"]["id"];
-
-                $question->subanswers[] = trim($choice);
-
-                $correctanswers = $thisquestion["#"]["GRADABLE"][0]["#"]["CORRECTANSWER"];
-                for ($k = 0; $k < sizeof ($correctanswers); $k++) {
-
-                    if (strcmp($choice_id, $correctanswers[$k]["@"]["choice_id"]) == 0) {
-
-                        $answer_id = $correctanswers[$k]["@"]["answer_id"];
 
-                        $answers = $thisquestion["#"]["ANSWER"];
-                        for ($m = 0; $m < sizeof ($answers); $m++) {
+            $correctfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_CORRECT', 0, '#'),
+                    '', true);
+            $incorrectfeedback = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'FEEDBACK_WHEN_INCORRECT', 0, '#'),
+                    '', true);
+            $question->correctfeedback = $this->text_field($this->cleaninput($correctfeedback));
+            // As there is no partially correct feedback we use incorrect one.
+            $question->partiallycorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+            $question->incorrectfeedback = $this->text_field($this->cleaninput($incorrectfeedback));
+
+            $choices = $this->getpath($thisquestion,
+                    array('#', 'CHOICE'), false, false); // Blackboard "choices" are Moodle subanswers.
+            $answers = $this->getpath($thisquestion,
+                    array('#', 'ANSWER'), false, false); // Blackboard "answers" are Moodle subquestions.
+            $correctanswers = $this->getpath($thisquestion,
+                    array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false); // Mapping between choices and answers.
+            $mappings = array();
+            foreach ($correctanswers as $correctanswer) {
+                if ($correctanswer) {
+                    $correct_choice_id = $this->getpath($correctanswer,
+                                array('@', 'choice_id'), '', true);
+                    $correct_answer_id = $this->getpath($correctanswer,
+                            array('@', 'answer_id'),
+                            '', true);
+                    $mappings[$correct_answer_id] = $correct_choice_id;
+                }
+            }
 
-                            $answer = $answers[$m];
-                            $current_ans_id = $answer["@"]["id"];
-                            if (strcmp ($current_ans_id, $answer_id) == 0) {
+            foreach ($choices as $choice) {
+                if ($ddmatch_is_installed) {
+                    $choicetext = $this->text_field($this->cleaninput($this->getpath($choice,
+                            array('#', 'TEXT', 0, '#'), '', true)));
+                } else {
+                    $choicetext = trim(strip_tags($this->getpath($choice,
+                            array('#', 'TEXT', 0, '#'), '', true)));
+                }
 
-                                $answer = $answer["#"]["TEXT"][0]["#"];
-                                $question->subquestions[] = trim($answer);
+                if ($choicetext != '') { // Only import non empty subanswers.
+                    $subquestion = '';
+                    $choice_id = $this->getpath($choice,
+                            array('@', 'id'), '', true);
+                    $fiber = array_search($choice_id, $mappings);
+                    $fiber = array_keys ($mappings, $choice_id);
+                    foreach ($fiber as $correct_answer_id) {
+                        // We have found a correspondance for this choice so we need to take the associated answer.
+                        foreach ($answers as $answer) {
+                            $current_ans_id = $this->getpath($answer,
+                                    array('@', 'id'), '', true);
+                            if (strcmp ($current_ans_id, $correct_answer_id) == 0) {
+                                $subquestion = $this->getpath($answer,
+                                        array('#', 'TEXT', 0, '#'), '', true);
                                 break;
                             }
                         }
-                        break;
+                        $question->subquestions[] = $this->text_field($this->cleaninput($subquestion));
+                        $question->subanswers[] = $choicetext;
+                    }
+
+                    if ($subquestion == '') { // Then in this case, $choice is a distractor.
+                        $question->subquestions[] = $this->text_field('');
+                        $question->subanswers[] = $choicetext;
                     }
                 }
             }
 
-            $questions[] = $question;
+            // Verify that this matching question has enough subquestions and subanswers.
+            $subquestioncount = 0;
+            $subanswercount = 0;
+            $subanswers = $question->subanswers;
+            foreach ($question->subquestions as $key => $subquestion) {
+                $subquestion = $subquestion['text'];
+                $subanswer = $subanswers[$key];
+                if ($subquestion != '') {
+                    $subquestioncount++;
+                }
+                $subanswercount++;
+            }
+            if ($subquestioncount < 2 || $subanswercount < 3) {
+                    $this->error(get_string('notenoughtsubans', 'qformat_blackboard', $question->questiontext));
+            } else {
+                $questions[] = $question;
+            }
 
         }
     }
index 6f68e21..f5fa957 100644 (file)
 /**
  * Strings for component 'qformat_blackboard', language 'en', branch 'MOODLE_20_STABLE'
  *
- * @package    qformat
- * @subpackage blackboard
+ * @package    qformat_blackboard
  * @copyright  2010 Helen Foster
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+$string['defaultname'] = 'Imported question {$a}';
+$string['importnotext'] = 'Missing question text in XML file';
+$string['notenoughtsubans'] = 'Unable to import matching question \'{$a}\' because a matching question must comprise at least two questions and three answers.';
 $string['pluginname'] = 'Blackboard';
 $string['pluginname_help'] = 'Blackboard format enables questions saved in the Blackboard version 5 "POOL" type export format to be imported.';
diff --git a/question/format/blackboard/tests/blackboardformat_test.php b/question/format/blackboard/tests/blackboardformat_test.php
new file mode 100644 (file)
index 0000000..222b6b3
--- /dev/null
@@ -0,0 +1,469 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Unit tests for the Moodle Blackboard format.
+ *
+ * @package    qformat_blackboard
+ * @copyright  2012 Jean-Michel Vedrine
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->libdir . '/questionlib.php');
+require_once($CFG->dirroot . '/question/format.php');
+require_once($CFG->dirroot . '/question/format/blackboard/format.php');
+require_once($CFG->dirroot . '/question/engine/tests/helpers.php');
+
+
+/**
+ * Unit tests for the blackboard question import format.
+ *
+ * @copyright  2012 Jean-Michel Vedrine
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class qformat_blackboard_test extends question_testcase {
+
+    public function make_test_xml() {
+        $xml = "<?xml version='1.0' encoding='utf-8'?>
+<POOL>
+    <TITLE value='exam 3 2008-9'/>
+    <QUESTIONLIST>
+        <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>
+        <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/>
+        <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/>
+        <QUESTION id='q39-44' class='QUESTION_MATCH' points='2'/>
+        <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/>
+        <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/>
+    </QUESTIONLIST>
+    <QUESTION_TRUEFALSE id='q1'>
+        <BODY>
+            <TEXT><![CDATA[<span style=\"font-size:12pt\">42 is the Absolute Answer to everything.</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q1_a1'>
+            <TEXT>False</TEXT>
+        </ANSWER>
+        <ANSWER id='q1_a2'>
+            <TEXT>True</TEXT>
+        </ANSWER>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q1_a2'/>
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>
+        </GRADABLE>
+    </QUESTION_TRUEFALSE>
+    <QUESTION_MULTIPLECHOICE id='q7'>
+        <BODY>
+            <TEXT><![CDATA[<span style=\"font-size:12pt\">What's between orange and green in the spectrum?</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q7_a1' position='1'>
+        <TEXT><![CDATA[<span style=\"font-size:12pt\">red</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q7_a2' position='2'>
+        <TEXT><![CDATA[<span style=\"font-size:12pt\">yellow</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q7_a3' position='3'>
+        <TEXT><![CDATA[<span style=\"font-size:12pt\">blue</span>]]></TEXT>
+        </ANSWER>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q7_a2'/>
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>
+            </GRADABLE>
+    </QUESTION_MULTIPLECHOICE>
+    <QUESTION_MULTIPLEANSWER id='q8'>
+        <BODY>
+            <TEXT><![CDATA[<span style=\"font-size:12pt\">What's between orange and green in the spectrum?</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q8_a1' position='1'>
+        <TEXT><![CDATA[<span style=\"font-size:12pt\">yellow</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q8_a2' position='2'>
+        <TEXT><![CDATA[<span style=\"font-size:12pt\">red</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q8_a3' position='3'>
+        <TEXT><![CDATA[<span style=\"font-size:12pt\">off-beige</span>]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q8_a4' position='4'>
+        <TEXT><![CDATA[<span style=\"font-size:12pt\">blue</span>]]></TEXT>
+        </ANSWER>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q8_a1'/>
+            <CORRECTANSWER answer_id='q8_a3'/>
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>
+            <FEEDBACK_WHEN_INCORRECT>
+                <![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]>
+            </FEEDBACK_WHEN_INCORRECT>
+        </GRADABLE>
+    </QUESTION_MULTIPLEANSWER>
+    <QUESTION_MATCH id='q39-44'>
+        <BODY>
+            <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q39-44_a1' position='1'>
+            <TEXT><![CDATA[frog]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q39-44_a2' position='2'>
+            <TEXT><![CDATA[cat]]></TEXT>
+        </ANSWER>
+        <ANSWER id='q39-44_a3' position='3'>
+            <TEXT><![CDATA[newt]]></TEXT>
+        </ANSWER>
+        <CHOICE id='q39-44_c1' position='1'>
+            <TEXT><![CDATA[mammal]]></TEXT>
+        </CHOICE>
+        <CHOICE id='q39-44_c2' position='2'>
+            <TEXT><![CDATA[insect]]></TEXT>
+        </CHOICE>
+        <CHOICE id='q39-44_c3' position='3'>
+            <TEXT><![CDATA[amphibian]]></TEXT>
+        </CHOICE>
+        <GRADABLE>
+            <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/>
+            <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/>
+            <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/>
+        </GRADABLE>
+    </QUESTION_MATCH>
+    <QUESTION_ESSAY id='q9'>
+        <BODY>
+            <TEXT><![CDATA[How are you?]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q9_a1'>
+            <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT>
+        </ANSWER>
+        <GRADABLE>
+        </GRADABLE>
+    </QUESTION_ESSAY>
+    <QUESTION_FILLINBLANK id='q27'>
+        <BODY>
+            <TEXT><![CDATA[<span style=\"font-size:12pt\">Name an amphibian: __________.</span>]]></TEXT>
+            <FLAGS>
+                <ISHTML value='true'/>
+                <ISNEWLINELITERAL value='false'/>
+            </FLAGS>
+        </BODY>
+        <ANSWER id='q27_a1' position='1'>
+            <TEXT>frog</TEXT>
+        </ANSWER>
+        <GRADABLE>
+        </GRADABLE>
+    </QUESTION_FILLINBLANK></POOL>";
+        return $xml;
+    }
+    public function test_import_match() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_matching($xmldata, $questions);
+        $q = $questions[0];
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'match';
+        $expectedq->name = 'Classify the animals.';
+        $expectedq->questiontext = '<i>Classify the animals.</i>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->correctfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->partiallycorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->incorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+        $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
+        $expectedq->subquestions = array(
+            array('text' => 'cat', 'format' => FORMAT_HTML, 'files' => array()),
+            array('text' => '', 'format' => FORMAT_HTML, 'files' => array()),
+            array('text' => 'frog', 'format' => FORMAT_HTML, 'files' => array()),
+            array('text' => 'newt', 'format' => FORMAT_HTML, 'files' => array()));
+        $expectedq->subanswers = array('mammal', 'insect', 'amphibian', 'amphibian');
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_multichoice_single() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_mc($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'multichoice';
+        $expectedq->single = 1;
+        $expectedq->name = 'What\'s between orange and green in the spectrum?';
+        $expectedq->questiontext = '<span style="font-size:12pt">What\'s between orange and green in the spectrum?</span>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->correctfeedback = array('text' => 'You gave the right answer.',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->partiallycorrectfeedback = array('text' => '',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->incorrectfeedback = array('text' => 'Only yellow is between orange and green in the spectrum.',
+                'format' => FORMAT_HTML, 'files' => array());
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+        $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
+        $expectedq->answer = array(
+                0 => array(
+                    'text' => '<span style="font-size:12pt">red</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '<span style="font-size:12pt">yellow</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => '<span style="font-size:12pt">blue</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+        $expectedq->fraction = array(0, 1, 0);
+        $expectedq->feedback = array(
+                0 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_multichoice_multi() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_ma($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'multichoice';
+        $expectedq->single = 0;
+        $expectedq->name = 'What\'s between orange and green in the spectrum?';
+        $expectedq->questiontext = '<span style="font-size:12pt">What\'s between orange and green in the spectrum?</span>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->correctfeedback = array(
+                'text' => 'You gave the right answer.',
+                'format' => FORMAT_HTML,
+                'files' => array());
+        $expectedq->partiallycorrectfeedback = array(
+                'text' => 'Only yellow and off-beige are between orange and green in the spectrum.',
+                'format' => FORMAT_HTML,
+                'files' => array());
+        $expectedq->incorrectfeedback = array(
+                'text' => 'Only yellow and off-beige are between orange and green in the spectrum.',
+                'format' => FORMAT_HTML,
+                'files' => array());
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->penalty = 0.3333333;
+        $expectedq->shuffleanswers = get_config('quiz', 'shuffleanswers');
+        $expectedq->answer = array(
+                0 => array(
+                    'text' => '<span style="font-size:12pt">yellow</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '<span style="font-size:12pt">red</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => '<span style="font-size:12pt">off-beige</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                3 => array(
+                    'text' => '<span style="font-size:12pt">blue</span>',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+        $expectedq->fraction = array(0.5, 0, 0.5, 0);
+        $expectedq->feedback = array(
+                0 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                2 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                3 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_truefalse() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_tf($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'truefalse';
+        $expectedq->name = '42 is the Absolute Answer to everything.';
+        $expectedq->questiontext = '<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->correctanswer = 0;
+        $expectedq->feedbacktrue = array(
+                'text' => '42 is the Ultimate Answer.',
+                'format' => FORMAT_HTML,
+                'files' => array(),
+            );
+        $expectedq->feedbackfalse = array(
+                'text' => 'You gave the right answer.',
+                'format' => FORMAT_HTML,
+                'files' => array(),
+            );
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_fill_in_the_blank() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_fib($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'shortanswer';
+        $expectedq->name = 'Name an amphibian: __________.';
+        $expectedq->questiontext = '<span style="font-size:12pt">Name an amphibian: __________.</span>';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->usecase = 0;
+        $expectedq->answer = array('frog', '*');
+        $expectedq->fraction = array(1, 0);
+        $expectedq->feedback = array(
+                0 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                ),
+                1 => array(
+                    'text' => '',
+                    'format' => FORMAT_HTML,
+                    'files' => array(),
+                )
+            );
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+
+    public function test_import_essay() {
+
+        $xmldata = xmlize($this->make_test_xml());
+        $questions = array();
+
+        $importer = new qformat_blackboard();
+        $importer->process_essay($xmldata, $questions);
+        $q = $questions[0];
+
+        $expectedq = new stdClass();
+        $expectedq->qtype = 'essay';
+        $expectedq->name = 'How are you?';
+        $expectedq->questiontext = 'How are you?';
+        $expectedq->questiontextformat = FORMAT_HTML;
+        $expectedq->generalfeedback = '';
+        $expectedq->generalfeedbackformat = FORMAT_HTML;
+        $expectedq->defaultmark = 1;
+        $expectedq->length = 1;
+        $expectedq->responseformat = 'editor';
+        $expectedq->responsefieldlines = 15;
+        $expectedq->attachments = 0;
+        $expectedq->graderinfo = array(
+                'text' => 'Blackboard answer for essay questions will be imported as informations for graders.',
+                'format' => FORMAT_HTML,
+                'files' => array());
+
+        $this->assert(new question_check_specified_fields_expectation($expectedq), $q);
+    }
+}
diff --git a/question/format/blackboard/tests/fixtures/sample_blackboard.dat b/question/format/blackboard/tests/fixtures/sample_blackboard.dat
new file mode 100644 (file)
index 0000000..88088c3
--- /dev/null
@@ -0,0 +1,142 @@
+<?xml version='1.0' encoding='utf-8'?>\r
+<POOL>\r
+    <TITLE value='exam 3 2008-9'/>\r
+    <QUESTIONLIST>\r
+        <QUESTION id='q1' class='QUESTION_TRUEFALSE' points='1'/>\r
+        <QUESTION id='q7' class='QUESTION_MULTIPLECHOICE' points='1'/>\r
+        <QUESTION id='q8' class='QUESTION_MULTIPLEANSWER' points='1'/>\r
+        <QUESTION id='q39-44' class='QUESTION_MATCH' points='1'/>\r
+        <QUESTION id='q9' class='QUESTION_ESSAY' points='1'/>\r
+        <QUESTION id='q27' class='QUESTION_FILLINBLANK' points='1'/>\r
+    </QUESTIONLIST>\r
+    <QUESTION_TRUEFALSE id='q1'>\r
+        <BODY>\r
+            <TEXT><![CDATA[<span style="font-size:12pt">42 is the Absolute Answer to everything.</span>]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q1_a1'>\r
+            <TEXT>False</TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q1_a2'>\r
+            <TEXT>True</TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+            <CORRECTANSWER answer_id='q1_a2'/>\r
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[42 is the Ultimate Answer.]]></FEEDBACK_WHEN_INCORRECT>\r
+        </GRADABLE>\r
+    </QUESTION_TRUEFALSE>\r
+    <QUESTION_MULTIPLECHOICE id='q7'>\r
+        <BODY>\r
+            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q7_a1' position='1'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q7_a2' position='2'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q7_a3' position='3'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+            <CORRECTANSWER answer_id='q7_a2'/>\r
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow is between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>\r
+        </GRADABLE>\r
+    </QUESTION_MULTIPLECHOICE>\r
+    <QUESTION_MULTIPLEANSWER id='q8'>\r
+        <BODY>\r
+            <TEXT><![CDATA[<span style="font-size:12pt">What's between orange and green in the spectrum?</span>]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q8_a1' position='1'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">yellow</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q8_a2' position='2'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">red</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q8_a3' position='3'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">off-beige</span>]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q8_a4' position='4'>\r
+        <TEXT><![CDATA[<span style="font-size:12pt">blue</span>]]></TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+            <CORRECTANSWER answer_id='q8_a1'/>\r
+            <CORRECTANSWER answer_id='q8_a3'/>\r
+            <FEEDBACK_WHEN_CORRECT><![CDATA[You gave the right answer.]]></FEEDBACK_WHEN_CORRECT>\r
+            <FEEDBACK_WHEN_INCORRECT><![CDATA[Only yellow and off-beige are between orange and green in the spectrum.]]></FEEDBACK_WHEN_INCORRECT>\r
+        </GRADABLE>\r
+    </QUESTION_MULTIPLEANSWER>\r
+    <QUESTION_MATCH id='q39-44'>\r
+        <BODY>\r
+            <TEXT><![CDATA[<i>Classify the animals.</i>]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q39-44_a1' position='1'>\r
+            <TEXT><![CDATA[frog]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q39-44_a2' position='2'>\r
+            <TEXT><![CDATA[cat]]></TEXT>\r
+        </ANSWER>\r
+        <ANSWER id='q39-44_a3' position='3'>\r
+            <TEXT><![CDATA[newt]]></TEXT>\r
+        </ANSWER>\r
+        <CHOICE id='q39-44_c1' position='1'>\r
+            <TEXT><![CDATA[mammal]]></TEXT>\r
+        </CHOICE>\r
+        <CHOICE id='q39-44_c2' position='2'>\r
+            <TEXT><![CDATA[insect]]></TEXT>\r
+        </CHOICE>\r
+        <CHOICE id='q39-44_c3' position='3'>\r
+            <TEXT><![CDATA[amphibian]]></TEXT>\r
+        </CHOICE>\r
+        <GRADABLE>\r
+            <CORRECTANSWER answer_id='q39-44_a1' choice_id='q39-44_c3'/>\r
+            <CORRECTANSWER answer_id='q39-44_a2' choice_id='q39-44_c1'/>\r
+            <CORRECTANSWER answer_id='q39-44_a3' choice_id='q39-44_c3'/>\r
+        </GRADABLE>\r
+    </QUESTION_MATCH>\r
+    <QUESTION_ESSAY id='q9'>\r
+        <BODY>\r
+            <TEXT><![CDATA[How are you?]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q9_a1'>\r
+            <TEXT><![CDATA[Blackboard answer for essay questions will be imported as informations for graders.]]></TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+        </GRADABLE>\r
+    </QUESTION_ESSAY>\r
+    <QUESTION_FILLINBLANK id='q27'>\r
+        <BODY>\r
+            <TEXT><![CDATA[Name an amphibian: __________.]]></TEXT>\r
+            <FLAGS>\r
+                <ISHTML value='true'/>\r
+                <ISNEWLINELITERAL value='false'/>\r
+            </FLAGS>\r
+        </BODY>\r
+        <ANSWER id='q27_a1' position='1'>\r
+            <TEXT>frog</TEXT>\r
+        </ANSWER>\r
+        <GRADABLE>\r
+        </GRADABLE>\r
+    </QUESTION_FILLINBLANK>\r
+</POOL>\r
index f2334f3..53948ab 100644 (file)
@@ -17,8 +17,7 @@
 /**
  * Version information for the calculated question type.
  *
- * @package    qformat
- * @subpackage blackboard
+ * @package    qformat_blackboard
  * @copyright  2011 The Open University
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */