From cc0188c21cfa30b75c4ffa4f8bd8a86481c68568 Mon Sep 17 00:00:00 2001 From: Jean-Michel Vedrine Date: Sat, 11 Aug 2012 21:00:54 +0200 Subject: [PATCH] MDL-25492 Blackboard V6+ question import is broken. --- question/format.php | 68 +- question/format/blackboard/format.php | 27 - question/format/blackboard_six/format.php | 1029 ++-------------- question/format/blackboard_six/formatbase.php | 163 +++ question/format/blackboard_six/formatpool.php | 467 ++++++++ question/format/blackboard_six/formatqti.php | 894 ++++++++++++++ .../lang/en/qformat_blackboard_six.php | 13 +- .../tests/blackboardformatpool_test.php | 330 +++++ .../tests/blackboardsixformatqti_test.php | 330 +++++ .../tests/fixtures/sample_blackboard_pool.dat | 142 +++ .../tests/fixtures/sample_blackboard_qti.dat | 1058 +++++++++++++++++ question/format/blackboard_six/version.php | 5 +- question/format/examview/format.php | 35 - question/format/gift/format.php | 13 - 14 files changed, 3593 insertions(+), 981 deletions(-) create mode 100644 question/format/blackboard_six/formatbase.php create mode 100644 question/format/blackboard_six/formatpool.php create mode 100644 question/format/blackboard_six/formatqti.php create mode 100644 question/format/blackboard_six/tests/blackboardformatpool_test.php create mode 100644 question/format/blackboard_six/tests/blackboardsixformatqti_test.php create mode 100644 question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat create mode 100644 question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat diff --git a/question/format.php b/question/format.php index a0e9df4e4379c..dd02cfc9dbc0a 100644 --- a/question/format.php +++ b/question/format.php @@ -408,20 +408,44 @@ public function importprocess($category) { $question->timecreated = time(); $question->modifiedby = $USER->id; $question->timemodified = time(); + $fileoptions = array( + 'subdirs' => false, + 'maxfiles' => -1, + 'maxbytes' => 0, + ); + if (is_array($question->questiontext)) { + // Importing images from draftfile. + $questiontext = $question->questiontext; + $question->questiontext = $questiontext['text']; + } + if (is_array($question->generalfeedback)) { + $generalfeedback = $question->generalfeedback; + $question->generalfeedback = $generalfeedback['text']; + } $question->id = $DB->insert_record('question', $question); - if (isset($question->questiontextfiles)) { + + if (!empty($questiontext['itemid'])) { + $question->questiontext = file_save_draft_area_files($questiontext['itemid'], + $this->importcontext->id, 'question', 'questiontext', $question->id, + $fileoptions, $question->questiontext); + } else if (isset($question->questiontextfiles)) { foreach ($question->questiontextfiles as $file) { question_bank::get_qtype($question->qtype)->import_file( $this->importcontext, 'question', 'questiontext', $question->id, $file); } } - if (isset($question->generalfeedbackfiles)) { + if (!empty($generalfeedback['itemid'])) { + $question->generalfeedback = file_save_draft_area_files($generalfeedback['itemid'], + $this->importcontext->id, 'question', 'generalfeedback', $question->id, + $fileoptions, $question->generalfeedback); + } else if (isset($question->generalfeedbackfiles)) { foreach ($question->generalfeedbackfiles as $file) { question_bank::get_qtype($question->qtype)->import_file( $this->importcontext, 'question', 'generalfeedback', $question->id, $file); } } + $DB->update_record('question', $question); $this->questionids[] = $question->id; @@ -636,6 +660,24 @@ protected function defaultquestion() { return $question; } + /** + * Add a blank combined feedback to a question object. + * @param object question + * @return object question + */ + protected function add_blank_combined_feedback($question) { + $question->correctfeedback['text'] = ''; + $question->correctfeedback['format'] = $question->questiontextformat; + $question->correctfeedback['files'] = array(); + $question->partiallycorrectfeedback['text'] = ''; + $question->partiallycorrectfeedback['format'] = $question->questiontextformat; + $question->partiallycorrectfeedback['files'] = array(); + $question->incorrectfeedback['text'] = ''; + $question->incorrectfeedback['format'] = $question->questiontextformat; + $question->incorrectfeedback['files'] = array(); + return $question; + } + /** * Given the data known to define a question in * this format, this function converts it into a question @@ -901,6 +943,28 @@ protected function format_question_text($question) { class qformat_based_on_xml extends qformat_default { + /** + * A lot of imported files contain unwanted entities. + * This method tries to clean up all known problems. + * @param string str string to correct + * @return string the corrected string + */ + public function cleaninput($str) { + + $html_code_list = array( + "'" => "'", + "’" => "'", + "“" => "\"", + "”" => "\"", + "–" => "-", + "—" => "-", + ); + $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; + } + /** * Return the array moodle is expecting * for an HTML text. No processing is done on $text. diff --git a/question/format/blackboard/format.php b/question/format/blackboard/format.php index 88e01306dca83..1e7fa0dd779b2 100644 --- a/question/format/blackboard/format.php +++ b/question/format/blackboard/format.php @@ -59,33 +59,6 @@ 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( - "'" => "'", - "’" => "'", - "[" => "[", - "“" => "\"", - "”" => "\"", - "]" => "]", - "'" => "'", - "–" => "-", - "—" => "-", - ); - $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; - } - /** * Parse the array of lines into an array of questions * this *could* burn memory - but it won't happen that much diff --git a/question/format/blackboard_six/format.php b/question/format/blackboard_six/format.php index 047bfa94f0e3f..32652d131ff79 100644 --- a/question/format/blackboard_six/format.php +++ b/question/format/blackboard_six/format.php @@ -15,936 +15,169 @@ // along with Moodle. If not, see . /** - * Blackboard 6.0 question importer. + * Blackboard V5 and V6 question importer. * - * @package qformat - * @subpackage blackboard_six + * @package qformat_blackboard_six * @copyright 2005 Michael Penney * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ - defined('MOODLE_INTERNAL') || die(); -require_once ($CFG->libdir . '/xmlize.php'); - - -/** - * Blackboard 6.0 question importer. - * - * @copyright 2005 Michael Penney - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ -class qformat_blackboard_six extends qformat_default { - function provide_import() { - return true; - } - - public function can_import_file($file) { - $mimetypes = array( - mimeinfo('type', '.dat'), - mimeinfo('type', '.zip') - ); - return in_array($file->get_mimetype(), $mimetypes); - } - - - //Function to check and create the needed dir to unzip file to - function check_and_create_import_dir($unique_code) { - - global $CFG; - - $status = $this->check_dir_exists($CFG->tempdir."",true); - if ($status) { - $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import",true); - } - if ($status) { - $status = $this->check_dir_exists($CFG->tempdir."/bbquiz_import/".$unique_code,true); - } - - return $status; - } - - function clean_temp_dir($dir='') { +require_once($CFG->libdir . '/xmlize.php'); +require_once($CFG->dirroot . '/question/format/blackboard_six/formatbase.php'); +require_once($CFG->dirroot . '/question/format/blackboard_six/formatqti.php'); +require_once($CFG->dirroot . '/question/format/blackboard_six/formatpool.php'); + +class qformat_blackboard_six extends qformat_blackboard_six_base { + /** @var int Blackboard assessment qti files were always imported by the blackboard_six plugin. */ + const FILETYPE_QTI = 1; + /** @var int Blackboard question pool files were previously handled by the blackboard plugin. */ + const FILETYPE_POOL = 2; + /** @var int type of file being imported, one of the constants FILETYPE_QTI or FILETYPE_POOL. */ + public $filetype; + + public function get_filecontent($path) { + $fullpath = $this->tempdir . '/' . $path; + if (is_file($fullpath) && is_readable($fullpath)) { + return file_get_contents($fullpath); + } + return false; + } + + /** + * Set the file type being imported + * @param int $type the imported file's type + */ + public function set_filetype($type) { + $this->filetype = $type; + } + + /** + * Return content of all files containing questions, + * as an array one element for each file found, + * For each file, the corresponding element is an array of lines. + * @param string filename name of file + * @return mixed contents array or false on failure + */ + public function readdata($filename) { global $CFG; - // for now we will just say everything happened okay note - // that a mess may be piling up in $CFG->tempdir/bbquiz_import - // TODO return true at top of the function renders all the following code useless - return true; - - if ($dir == '') { - $dir = $this->temp_dir; - } - $slash = "/"; - - // Create arrays to store files and directories - $dir_files = array(); - $dir_subdirs = array(); - - // Make sure we can delete it - chmod($dir, $CFG->directorypermissions); - - if ((($handle = opendir($dir))) == FALSE) { - // The directory could not be opened - return false; - } - - // Loop through all directory entries, and construct two temporary arrays containing files and sub directories - while(false !== ($entry = readdir($handle))) { - if (is_dir($dir. $slash .$entry) && $entry != ".." && $entry != ".") { - $dir_subdirs[] = $dir. $slash .$entry; - } - else if ($entry != ".." && $entry != ".") { - $dir_files[] = $dir. $slash .$entry; - } - } - - // Delete all files in the curent directory return false and halt if a file cannot be removed - $countdir_files = count($dir_files); - for($i=0; $i<$countdir_files; $i++) { - chmod($dir_files[$i], $CFG->directorypermissions); - if (((unlink($dir_files[$i]))) == FALSE) { + // Find if we are importing a .dat file. + if (strtolower(pathinfo($filename, PATHINFO_EXTENSION)) == 'dat') { + if (!is_readable($filename)) { + $this->error(get_string('filenotreadable', 'error')); return false; } - } - - // Empty sub directories and then remove the directory - $countdir_subdirs = count($dir_subdirs); - for($i=0; $i<$countdir_subdirs; $i++) { - chmod($dir_subdirs[$i], $CFG->directorypermissions); - if ($this->clean_temp_dir($dir_subdirs[$i]) == FALSE) { - return false; + // As we are not importing a .zip file, + // there is no imsmanifest, and it is not possible + // to parse it to find the file type. + // So we need to guess the file type by looking at the content. + // For now we will do that searching for a required tag. + // This is certainly not bullet-proof but works for all usual files. + $text = file_get_contents($filename); + if (strpos($text, '')) { + $this->set_filetype(self::FILETYPE_QTI); } - else { - if (rmdir($dir_subdirs[$i]) == FALSE) { - return false; - } + if (strpos($text, '')) { + $this->set_filetype(self::FILETYPE_POOL); } - } + // In all other cases we are not able to handle this question file. - // Close directory - closedir($handle); - if (rmdir($this->temp_dir) == FALSE) { - return false; + // Readquestions is now expecting an array of strings. + return array($text); } - // Success, every thing is gone return true - return true; - } - - //Function to check if a directory exists and, optionally, create it - function check_dir_exists($dir,$create=false) { - - global $CFG; - - $status = true; - if(!is_dir($dir)) { - if (!$create) { - $status = false; - } else { - umask(0000); - $status = mkdir ($dir,$CFG->directorypermissions); + // We are importing a zip file. + // Create name for temporary directory. + $unique_code = time(); + $this->tempdir = make_temp_directory('bbquiz_import/' . $unique_code); + if (is_readable($filename)) { + if (!copy($filename, $this->tempdir . '/bboard.zip')) { + $this->error(get_string('cannotcopybackup', 'question')); + fulldelete($this->tempdir); + return false; } - } - return $status; - } + if (unzip_file($this->tempdir . '/bboard.zip', '', false)) { + $dom = new DomDocument(); - function importpostprocess() { - /// Does any post-processing that may be desired - /// Argument is a simple array of question ids that - /// have just been added. - - // need to clean up temporary directory - return $this->clean_temp_dir(); - } - - function copy_file_to_course($filename) { - global $CFG, $COURSE; - $filename = str_replace('\\','/',$filename); - $fullpath = $this->temp_dir.'/res00001/'.$filename; - $basename = basename($filename); - - $copy_to = $CFG->dataroot.'/'.$COURSE->id.'/bb_import'; - - if ($this->check_dir_exists($copy_to,true)) { - if(is_readable($fullpath)) { - $copy_to.= '/'.$basename; - if (!copy($fullpath, $copy_to)) { + if (!$dom->load($this->tempdir . '/imsmanifest.xml')) { + $this->error(get_string('errormanifest', 'qformat_blackboard_six')); + fulldelete($this->tempdir); return false; } - else { - return $copy_to; - } - } - } - else { - return false; - } - } - function readdata($filename) { - /// Returns complete file with an array, one item per line - global $CFG; + $xpath = new DOMXPath($dom); - // if the extension is .dat we just return that, - // if .zip we unzip the file and get the data - $ext = substr($this->realfilename, strpos($this->realfilename,'.'), strlen($this->realfilename)-1); - if ($ext=='.dat') { - if (!is_readable($filename)) { - print_error('filenotreadable', 'error'); - } - return file($filename); - } + // We starts from the root element. + $query = '//resources/resource'; + $this->filebase = $this->tempdir; + $q_file = array(); - $unique_code = time(); - $temp_dir = $CFG->tempdir."/bbquiz_import/".$unique_code; - $this->temp_dir = $temp_dir; - if ($this->check_and_create_import_dir($unique_code)) { - if(is_readable($filename)) { - if (!copy($filename, "$temp_dir/bboard.zip")) { - print_error('cannotcopybackup', 'question'); - } - if(unzip_file("$temp_dir/bboard.zip", '', false)) { - // assuming that the information is in res0001.dat - // after looking at 6 examples this was always the case - $q_file = "$temp_dir/res00001.dat"; - if (is_file($q_file)) { - if (is_readable($q_file)) { - $filearray = file($q_file); - /// Check for Macintosh OS line returns (ie file on one line), and fix - if (preg_match("~\r~", $filearray[0]) AND !preg_match("~\n~", $filearray[0])) { - return explode("\r", $filearray[0]); - } else { - return $filearray; - } + $examfiles = $xpath->query($query); + foreach ($examfiles as $examfile) { + if ($examfile->getAttribute('type') == 'assessment/x-bb-qti-test' + || $examfile->getAttribute('type') == 'assessment/x-bb-qti-pool') { + + if ($content = $this->get_filecontent($examfile->getAttribute('bb:file'))) { + $this->set_filetype(self::FILETYPE_QTI); + $q_file[] = $content; } } - else { - print_error('cannotfindquestionfile', 'questioni'); - } - } - else { - print "filename: $filename
tempdir: $temp_dir
"; - print_error('cannotunzip', 'question'); - } - } - else { - print_error('cannotreaduploadfile'); - } - } - else { - print_error('cannotcreatetempdir'); - } - } - - function save_question_options($question) { - return true; - } - - - - protected function readquestions($lines) { - /// Parses an array of lines into an array of questions, - /// where each item is a question object as defined by - /// readquestion(). - - $text = implode($lines, " "); - $xml = xmlize($text, 0); - - $raw_questions = $xml['questestinterop']['#']['assessment'][0]['#']['section'][0]['#']['item']; - $questions = array(); - - foreach($raw_questions as $quest) { - $question = $this->create_raw_question($quest); - - switch($question->qtype) { - case "Matching": - $this->process_matching($question, $questions); - break; - case "Multiple Choice": - $this->process_mc($question, $questions); - break; - case "Essay": - $this->process_essay($question, $questions); - break; - case "Multiple Answer": - $this->process_ma($question, $questions); - break; - case "True/False": - $this->process_tf($question, $questions); - break; - case 'Fill in the Blank': - $this->process_fblank($question, $questions); - break; - case 'Short Response': - $this->process_essay($question, $questions); - break; - default: - print "Unknown or unhandled question type: \"$question->qtype\"
"; - break; - } - - } - return $questions; - } - - -// creates a cleaner object to deal with for processing into moodle -// the object created is NOT a moodle question object -function create_raw_question($quest) { - - $question = new stdClass(); - $question->qtype = $quest['#']['itemmetadata'][0]['#']['bbmd_questiontype'][0]['#']; - $question->id = $quest['#']['itemmetadata'][0]['#']['bbmd_asi_object_id'][0]['#']; - $presentation->blocks = $quest['#']['presentation'][0]['#']['flow'][0]['#']['flow']; - - foreach($presentation->blocks as $pblock) { - - $block = NULL; - $block->type = $pblock['@']['class']; - - switch($block->type) { - case 'QUESTION_BLOCK': - $sub_blocks = $pblock['#']['flow']; - foreach($sub_blocks as $sblock) { - //echo "Calling process_block from line 263
"; - $this->process_block($sblock, $block); - } - break; - - case 'RESPONSE_BLOCK': - $choices = NULL; - switch($question->qtype) { - case 'Matching': - $bb_subquestions = $pblock['#']['flow']; - $sub_questions = array(); - foreach($bb_subquestions as $bb_subquestion) { - $sub_question = NULL; - $sub_question->ident = $bb_subquestion['#']['response_lid'][0]['@']['ident']; - $this->process_block($bb_subquestion['#']['flow'][0], $sub_question); - $bb_choices = $bb_subquestion['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label']; - $choices = array(); - $this->process_choices($bb_choices, $choices); - $sub_question->choices = $choices; - if (!isset($block->subquestions)) { - $block->subquestions = array(); - } - $block->subquestions[] = $sub_question; + if ($examfile->getAttribute('type') == 'assessment/x-bb-pool') { + if ($examfile->getAttribute('baseurl')) { + $this->filebase = $this->tempdir. '/' . $examfile->getAttribute('baseurl'); } - break; - case 'Multiple Answer': - $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label']; - $choices = array(); - $this->process_choices($bb_choices, $choices); - $block->choices = $choices; - break; - case 'Essay': - // Doesn't apply since the user responds with text input - break; - case 'Multiple Choice': - $mc_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label']; - foreach($mc_choices as $mc_choice) { - $choices = NULL; - $choices = $this->process_block($mc_choice, $choices); - $block->choices[] = $choices; + if ($content = $this->get_filecontent($examfile->getAttribute('file'))) { + $this->set_filetype(self::FILETYPE_POOL); + $q_file[] = $content; } - break; - case 'Short Response': - // do nothing? - break; - case 'Fill in the Blank': - // do nothing? - break; - default: - $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label']; - $choices = array(); - $this->process_choices($bb_choices, $choices); - $block->choices = $choices; - } - break; - case 'RIGHT_MATCH_BLOCK': - $matching_answerset = $pblock['#']['flow']; - - $answerset = array(); - foreach($matching_answerset as $answer) { - // $answerset[] = $this->process_block($answer, $bb_answer); - $bb_answer = null; - $bb_answer->text = $answer['#']['flow'][0]['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']; - $answerset[] = $bb_answer; - } - $block->matching_answerset = $answerset; - break; - default: - print "UNHANDLED PRESENTATION BLOCK"; - break; - } - $question->{$block->type} = $block; - } - - // determine response processing - // there is a section called 'outcomes' that I don't know what to do with - $resprocessing = $quest['#']['resprocessing']; - $respconditions = $resprocessing[0]['#']['respcondition']; - $reponses = array(); - if ($question->qtype == 'Matching') { - $this->process_matching_responses($respconditions, $responses); - } - else { - $this->process_responses($respconditions, $responses); - } - $question->responses = $responses; - $feedbackset = $quest['#']['itemfeedback']; - $feedbacks = array(); - $this->process_feedback($feedbackset, $feedbacks); - $question->feedback = $feedbacks; - return $question; -} - -function process_block($cur_block, &$block) { - global $COURSE, $CFG; - - $cur_type = $cur_block['@']['class']; - switch($cur_type) { - case 'FORMATTED_TEXT_BLOCK': - $block->text = $this->strip_applet_tags_get_mathml($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']); - break; - case 'FILE_BLOCK': - //revisit this to make sure it is working correctly - // Commented out ['matapplication']..., etc. because I - // noticed that when I imported a new Blackboard 6 file - // and printed out the block, the tree did not extend past ['material'][0]['#'] - CT 8/3/06 - $block->file = $cur_block['#']['material'][0]['#'];//['matapplication'][0]['@']['uri']; - if ($block->file != '') { - // if we have a file copy it to the course dir and adjust its name to be visible over the web. - $block->file = $this->copy_file_to_course($block->file); - $block->file = $CFG->wwwroot.'/file.php/'.$COURSE->id.'/bb_import/'.basename($block->file); - } - break; - case 'Block': - if (isset($cur_block['#']['material'][0]['#']['mattext'][0]['#'])) { - $block->text = $cur_block['#']['material'][0]['#']['mattext'][0]['#']; - } - else if (isset($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'])) { - $block->text = $cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']; - } - else if (isset($cur_block['#']['response_label'])) { - // this is a response label block - $sub_blocks = $cur_block['#']['response_label'][0]; - if(!isset($block->ident)) { - if(isset($sub_blocks['@']['ident'])) { - $block->ident = $sub_blocks['@']['ident']; } } - foreach($sub_blocks['#']['flow_mat'] as $sub_block) { - $this->process_block($sub_block, $block); - } - } - else { - if (isset($cur_block['#']['flow_mat']) || isset($cur_block['#']['flow'])) { - if (isset($cur_block['#']['flow_mat'])) { - $sub_blocks = $cur_block['#']['flow_mat']; - } - elseif (isset($cur_block['#']['flow'])) { - $sub_blocks = $cur_block['#']['flow']; - } - foreach ($sub_blocks as $sblock) { - // this will recursively grab the sub blocks which should be of one of the other types - $this->process_block($sblock, $block); - } - } - } - break; - case 'LINK_BLOCK': - // not sure how this should be included - if (!empty($cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri'])) { - $block->link = $cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri']; - } - else { - $block->link = ''; - } - break; - } - return $block; -} -function process_choices($bb_choices, &$choices) { - foreach($bb_choices as $choice) { - if (isset($choice['@']['ident'])) { - $cur_choice = $choice['@']['ident']; - } - else { //for multiple answer - $cur_choice = $choice['#']['response_label'][0];//['@']['ident']; - } - if (isset($choice['#']['flow_mat'][0])) { //for multiple answer - $cur_block = $choice['#']['flow_mat'][0]; - // Reset $cur_choice to NULL because process_block is expecting an object - // for the second argument and not a string, which is what is was set as - // originally - CT 8/7/06 - $cur_choice = null; - $this->process_block($cur_block, $cur_choice); - } - elseif (isset($choice['#']['response_label'])) { - // Reset $cur_choice to NULL because process_block is expecting an object - // for the second argument and not a string, which is what is was set as - // originally - CT 8/7/06 - $cur_choice = null; - $this->process_block($choice, $cur_choice); - } - $choices[] = $cur_choice; - } -} - -function process_matching_responses($bb_responses, &$responses) { - foreach($bb_responses as $bb_response) { - $response = NULL; - if (isset($bb_response['#']['conditionvar'][0]['#']['varequal'])) { - $response->correct = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['#']; - $response->ident = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['@']['respident']; - } - else { - $response->correct = 'Broken Question?'; - $response->ident = 'Broken Question?'; - } - $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid']; - $responses[] = $response; - } -} - -function process_responses($bb_responses, &$responses) { - foreach($bb_responses as $bb_response) { - //Added this line to instantiate $response. - // Without instantiating the $response variable, the same object - // gets added to the array - $response = new stdClass(); - if (isset($bb_response['@']['title'])) { - $response->title = $bb_response['@']['title']; - } - else { - $reponse->title = $bb_response['#']['displayfeedback'][0]['@']['linkrefid']; - } - $reponse->ident = array(); - if (isset($bb_response['#']['conditionvar'][0]['#'])){//['varequal'][0]['#'])) { - $response->ident[0] = $bb_response['#']['conditionvar'][0]['#'];//['varequal'][0]['#']; - } - else if (isset($bb_response['#']['conditionvar'][0]['#']['other'][0]['#'])) { - $response->ident[0] = $bb_response['#']['conditionvar'][0]['#']['other'][0]['#']; - } - - if (isset($bb_response['#']['conditionvar'][0]['#']['and'])){//[0]['#'])) { - $responseset = $bb_response['#']['conditionvar'][0]['#']['and'];//[0]['#']['varequal']; - foreach($responseset as $rs) { - $response->ident[] = $rs['#']; - if(!isset($response->feedback) and isset( $rs['@'] ) ) { - $response->feedback = $rs['@']['respident']; - } - } - } - else { - $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid']; - } - - // determine what point value to give response - if (isset($bb_response['#']['setvar'])) { - switch ($bb_response['#']['setvar'][0]['#']) { - case "SCORE.max": - $response->fraction = 1; - break; - default: - // I have only seen this being 0 or unset - // there are probably fractional values of SCORE.max, but I'm not sure what they look like - $response->fraction = 0; - break; - } - } - else { - // just going to assume this is the case this is probably not correct. - $response->fraction = 0; - } - - $responses[] = $response; - } -} - -function process_feedback($feedbackset, &$feedbacks) { - foreach($feedbackset as $bb_feedback) { - // Added line $feedback=null so that $feedback does not get reused in the loop - // and added the the $feedbacks[] array multiple times - $feedback = null; - $feedback->ident = $bb_feedback['@']['ident']; - if (isset($bb_feedback['#']['flow_mat'][0])) { - $this->process_block($bb_feedback['#']['flow_mat'][0], $feedback); - } - elseif (isset($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0])) { - $this->process_block($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0], $feedback); - } - $feedbacks[] = $feedback; - } -} - -/** - * Create common parts of question - */ -function process_common( $quest ) { - $question = $this->defaultquestion(); - $question->questiontext = $quest->QUESTION_BLOCK->text; - $question->name = shorten_text( $quest->id, 250 ); - - return $question; -} - -//---------------------------------------- -// Process True / False Questions -//---------------------------------------- -function process_tf($quest, &$questions) { - $question = $this->process_common( $quest ); - - $question->qtype = TRUEFALSE; - $question->single = 1; // Only one answer is allowed - // 0th [response] is the correct answer. - $responses = $quest->responses; - $correctresponse = $responses[0]->ident[0]['varequal'][0]['#']; - if ($correctresponse != 'false') { - $correct = true; - } - else { - $correct = false; - } - - foreach($quest->feedback as $fb) { - $fback->{$fb->ident} = $fb->text; - } - - if ($correct) { // true is correct - $question->answer = 1; - $question->feedbacktrue = $fback->correct; - $question->feedbackfalse = $fback->incorrect; - } else { // false is correct - $question->answer = 0; - $question->feedbacktrue = $fback->incorrect; - $question->feedbackfalse = $fback->correct; - } - $question->correctanswer = $question->answer; - $questions[] = $question; -} - - -//---------------------------------------- -// Process Fill in the Blank -//---------------------------------------- -function process_fblank($quest, &$questions) { - $question = $this->process_common( $quest ); - $question->qtype = SHORTANSWER; - $question->single = 1; - - $answers = array(); - $fractions = array(); - $feedbacks = array(); - - // extract the feedback - $feedback = array(); - foreach($quest->feedback as $fback) { - if (isset($fback->ident)) { - if ($fback->ident == 'correct' || $fback->ident == 'incorrect') { - $feedback[$fback->ident] = $fback->text; - } - } - } - - foreach($quest->responses as $response) { - if(isset($response->title)) { - if (isset($response->ident[0]['varequal'][0]['#'])) { - //for BB Fill in the Blank, only interested in correct answers - if ($response->feedback = 'correct') { - $answers[] = $response->ident[0]['varequal'][0]['#']; - $fractions[] = 1; - if (isset($feedback['correct'])) { - $feedbacks[] = $feedback['correct']; - } - else { - $feedbacks[] = ''; - } + if ($q_file) { + return $q_file; + } else { + $this->error(get_string('cannotfindquestionfile', 'question')); + fulldelete($this->tempdir); } - } - - } - } - - //Adding catchall to so that students can see feedback for incorrect answers when they enter something the - //instructor did not enter - $answers[] = '*'; - $fractions[] = 0; - if (isset($feedback['incorrect'])) { - $feedbacks[] = $feedback['incorrect']; - } - else { - $feedbacks[] = ''; - } - - $question->answer = $answers; - $question->fraction = $fractions; - $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of - - if (!empty($question)) { - $questions[] = $question; - } - -} - -//---------------------------------------- -// Process Multiple Choice Questions -//---------------------------------------- -function process_mc($quest, &$questions) { - $question = $this->process_common( $quest ); - $question->qtype = MULTICHOICE; - $question->single = 1; - - $feedback = array(); - foreach($quest->feedback as $fback) { - $feedback[$fback->ident] = $fback->text; - } - - foreach($quest->responses as $response) { - if (isset($response->title)) { - if ($response->title == 'correct') { - // only one answer possible for this qtype so first index is correct answer - $correct = $response->ident[0]['varequal'][0]['#']; - } - } - else { - // fallback method for when the title is not set - if ($response->feedback == 'correct') { - // only one answer possible for this qtype so first index is correct answer - $correct = $response->ident[0]['varequal'][0]['#']; // added [0]['varequal'][0]['#'] to $response->ident - CT 8/9/06 - } - } - } - - $i = 0; - foreach($quest->RESPONSE_BLOCK->choices as $response) { - $question->answer[$i] = $response->text; - if ($correct == $response->ident) { - $question->fraction[$i] = 1; - // this is a bit of a hack to catch the feedback... first we see if a 'correct' feedback exists - // then specific feedback for this question (maybe this should be switched?, but from my example - // question pools I have not seen response specific feedback, only correct or incorrect feedback - if (!empty($feedback['correct'])) { - $question->feedback[$i] = $feedback['correct']; - } - elseif (!empty($feedback[$i])) { - $question->feedback[$i] = $feedback[$i]; - } - else { - // failsafe feedback (should be '' instead?) - $question->feedback[$i] = "correct"; - } - } - else { - $question->fraction[$i] = 0; - if (!empty($feedback['incorrect'])) { - $question->feedback[$i] = $feedback['incorrect']; - } - elseif (!empty($feedback[$i])) { - $question->feedback[$i] = $feedback[$i]; - } - else { - // failsafe feedback (should be '' instead?) - $question->feedback[$i] = 'incorrect'; - } - } - $i++; - } - - if (!empty($question)) { - $questions[] = $question; - } -} - -//---------------------------------------- -// Process Multiple Choice Questions With Multiple Answers -//---------------------------------------- -function process_ma($quest, &$questions) { - $question = $this->process_common( $quest ); // copied this from process_mc - $question->qtype = MULTICHOICE; - $question->single = 0; // More than one answer allowed - - $answers = $quest->responses; - $correct_answers = array(); - foreach($answers as $answer) { - if($answer->title == 'correct') { - $answerset = $answer->ident[0]['and'][0]['#']['varequal']; - foreach($answerset as $ans) { - $correct_answers[] = $ans['#']; - } - } - } - - foreach ($quest->feedback as $fb) { - $feedback->{$fb->ident} = trim($fb->text); - } - - $correct_answer_count = count($correct_answers); - $choiceset = $quest->RESPONSE_BLOCK->choices; - $i = 0; - foreach($choiceset as $choice) { - $question->answer[$i] = trim($choice->text); - if (in_array($choice->ident, $correct_answers)) { - // correct answer - $question->fraction[$i] = floor(100000/$correct_answer_count)/100000; // strange behavior if we have more than 5 decimal places - $question->feedback[$i] = $feedback->correct; - } - else { - // wrong answer - $question->fraction[$i] = 0; - $question->feedback[$i] = $feedback->incorrect; - } - $i++; - } - - $questions[] = $question; -} - -//---------------------------------------- -// Process Essay Questions -//---------------------------------------- -function process_essay($quest, &$questions) { -// this should be rewritten to accomodate moodle 1.6 essay question type eventually - - if (defined("ESSAY")) { - // treat as short answer - $question = $this->process_common( $quest ); // copied this from process_mc - $question->qtype = ESSAY; - - $question->feedback = array(); - // not sure where to get the correct answer from - foreach($quest->feedback as $feedback) { - // Added this code to put the possible solution that the - // instructor gives as the Moodle answer for an essay question - if ($feedback->ident == 'solution') { - $question->feedback = $feedback->text; - } - } - //Added because essay/questiontype.php:save_question_option is expecting a - //fraction property - CT 8/10/06 - $question->fraction[] = 1; - if (!empty($question)) { - $questions[]=$question; - } - } - else { - print "Essay question types are not handled because the quiz question type 'Essay' does not exist in this installation of Moodle
"; - print "    Omitted Question: ".$quest->QUESTION_BLOCK->text.'

'; - } -} - -//---------------------------------------- -// Process Matching Questions -//---------------------------------------- -function process_matching($quest, &$questions) { - // renderedmatch is an optional plugin, so we need to check if it is defined - if (question_bank::is_qtype_installed('renderedmatch')) { - $question = $this->process_common($quest); - $question->valid = true; - $question->qtype = 'renderedmatch'; - - foreach($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { - foreach($quest->responses as $rid => $resp) { - if ($resp->ident == $subq->ident) { - $correct = $resp->correct; - $feedback = $resp->feedback; - } - } - - foreach($subq->choices as $cid => $choice) { - if ($choice == $correct) { - $question->subquestions[] = $subq->text; - $question->subanswers[] = $quest->RIGHT_MATCH_BLOCK->matching_answerset[$cid]->text; - } - } - } - - // check format - $status = true; - if ( count($quest->RESPONSE_BLOCK->subquestions) > count($quest->RIGHT_MATCH_BLOCK->matching_answerset) || count($question->subquestions) < 2) { - $status = false; - } - else { - // need to redo to make sure that no two questions have the same answer (rudimentary now) - foreach($question->subanswers as $qstn) { - if(isset($previous)) { - if ($qstn == $previous) { - $status = false; - } - } - $previous = $qstn; - if ($qstn == '') { - $status = false; - } - } - } - - if ($status) { - $questions[] = $question; - } - else { - global $COURSE, $CFG; - print ''; - print ''; - - print ""; - print "'; - - print '
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.
Question:".$quest->QUESTION_BLOCK->text; - if (isset($quest->QUESTION_BLOCK->file)) { - print '
There is a subfile contained in the zipfile that has been copied to course files: bb_import/'.basename($quest->QUESTION_BLOCK->file).''; - if (preg_match('/(gif|jpg|jpeg|png)$/i', $quest->QUESTION_BLOCK->file)) { - print ''; - } - } - print "
Subquestions:
    "; - foreach($quest->responses as $rs) { - $correct_responses->{$rs->ident} = $rs->correct; - } - foreach($quest->RESPONSE_BLOCK->subquestions as $subq) { - print '
  • '.$subq->text.'
      '; - foreach($subq->choices as $id=>$choice) { - print '
    • '; - if ($choice == $correct_responses->{$subq->ident}) { - print ''; - } - else { - print ''; - } - print $quest->RIGHT_MATCH_BLOCK->matching_answerset[$id]->text.'
    • '; - } - print '
    '; - } - print '
Feedback:
    '; - foreach($quest->feedback as $fb) { - print '
  • '.$fb->ident.': '.$fb->text.'
  • '; - } - print '
'; + } else { + $this->error(get_string('cannotunzip', 'question')); + fulldelete($this->temp_dir); + } + } else { + $this->error(get_string('cannotreaduploadfile', 'error')); + fulldelete($this->tempdir); + } + return false; + } + + /** + * Parse the array of strings into an array of questions. + * Each string is the content of a .dat questions file. + * This *could* burn memory - but it won't happen that much + * so fingers crossed! + * @param array of strings from the input file. + * @param stdClass $context + * @return array (of objects) question objects. + */ + public function readquestions($lines) { + + // Set up array to hold all our questions. + $questions = array(); + if ($this->filetype == self::FILETYPE_QTI) { + $importer = new qformat_blackboard_six_qti(); + } else if ($this->filetype == self::FILETYPE_POOL) { + $importer = new qformat_blackboard_six_pool(); + } else { + // In all other cases we are not able to import the file. + return false; } - } - else { - print "Matching question types are not handled because the quiz question type 'Rendered Matching' does not exist in this installation of Moodle
"; - print "    Omitted Question: ".$quest->QUESTION_BLOCK->text.'

'; - } -} + $importer->set_filebase($this->filebase); - -function strip_applet_tags_get_mathml($string) { - if(stristr($string, '') === FALSE) { - return $string; - } - else { - // strip all applet tags keeping stuff before/after and inbetween (if mathml) them - while (stristr($string, '') !== FALSE) { - preg_match("/(.*)\.*\<\/math\>)\".*\<\/applet\>(.*)/i",$string, $mathmls); - $string = $mathmls[1].$mathmls[2].$mathmls[3]; + // Each element of $lines is a string containing a complete xml document. + foreach ($lines as $text) { + $questions = array_merge($questions, $importer->readquestions($text)); } - return $string; + return $questions; } } - -} // close object - diff --git a/question/format/blackboard_six/formatbase.php b/question/format/blackboard_six/formatbase.php new file mode 100644 index 0000000000000..da08e913551f1 --- /dev/null +++ b/question/format/blackboard_six/formatbase.php @@ -0,0 +1,163 @@ +. + +/** + * Blackboard V5 and V6 question importer. + * + * @package qformat_blackboard_six + * @copyright 2012 Jean-Michel Vedrine + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Base class question import format for zip files with images + * + */ + +class qformat_blackboard_six_base extends qformat_based_on_xml { + /** @var string path to path to root of image tree in unzipped archive. */ + public $filebase = ''; + /** @var string path to the temporary directory. */ + public $tempdir = ''; + /** + * This plugin provide import + * @return bool true + */ + public function provide_import() { + return true; + } + + /** + * Check if the given file is capable of being imported by this plugin. + * As {@link file_storage::mimetype()} now uses finfo PHP extension if available, + * the value returned by $file->get_mimetype for a .dat file is not the same on all servers. + * So we must made 2 checks to verify if the plugin can import the file. + * @param stored_file $file the file to check + * @return bool whether this plugin can import the file + */ + public function can_import_file($file) { + $mimetypes = array( + mimeinfo('type', '.dat'), + mimeinfo('type', '.zip') + ); + return in_array($file->get_mimetype(), $mimetypes) || in_array(mimeinfo('type', $file->get_filename()), $mimetypes); + } + + public function mime_type() { + return mimeinfo('type', '.zip'); + } + + /** + * Does any post-processing that may be desired + * Clean the temporary directory if a zip file was imported + * @return bool success + */ + public function importpostprocess() { + if ($this->tempdir != '') { + fulldelete($this->tempdir); + } + return true; + } + /** + * Set the path to the root of images tree + * @param string $path path to images root + */ + public function set_filebase($path) { + $this->filebase = $path; + } + + /** + * Store an image file in a draft filearea + * @param array $text, if itemid element don't exists it will be created + * @param string tempdir path to root of image tree + * @param string filepathinsidetempdir path to image in the tree + * @param string filename image's name + * @return string new name of the image as it was stored + */ + protected function store_file_for_text_field(&$text, $tempdir, $filepathinsidetempdir, $filename) { + global $USER; + $fs = get_file_storage(); + if (empty($text['itemid'])) { + $text['itemid'] = file_get_unused_draft_itemid(); + } + // As question file areas don't support subdirs, + // convert path to filename. + // So that images with same name can be imported. + $newfilename = clean_param(str_replace('/', '__', $filepathinsidetempdir . '__' . $filename), PARAM_FILE); + $filerecord = array( + 'contextid' => context_user::instance($USER->id)->id, + 'component' => 'user', + 'filearea' => 'draft', + 'itemid' => $text['itemid'], + 'filepath' => '/', + 'filename' => $newfilename, + ); + $fs->create_file_from_pathname($filerecord, $tempdir . '/' . $filepathinsidetempdir . '/' . $filename); + return $newfilename; + } + + /** + * Given an HTML text with references to images files, + * store all images in a draft filearea, + * and return an array with all urls in text recoded, + * format set to FORMAT_HTML, and itemid set to filearea itemid + * @param string text text to parse and recode + * @return array with keys text, format, itemid. + */ + public function text_field($text) { + $data = array(); + // Step one, find all file refs then add to array. + preg_match_all('|]+src="([^"]*)"|i', $text, $out); // Find all src refs. + + foreach ($out[1] as $path) { + $fullpath = $this->filebase . '/' . $path; + + if (is_readable($fullpath)) { + $dirpath = dirname($path); + $filename = basename($path); + $newfilename = $this->store_file_for_text_field($data, $this->filebase, $dirpath, $filename); + $text = preg_replace("|$path|", "@@PLUGINFILE@@/" . $newfilename, $text); + } + + } + $data['text'] = $text; + $data['format'] = FORMAT_HTML; + return $data; + } + + /** + * Same as text_field but text is cleaned. + * @param string text text to parse and recode + * @return array with keys text, format, itemid. + */ + public function cleaned_text_field($text) { + return $this->text_field($this->cleaninput($text)); + } + + /** + * Convert the question text to plain text. + * We need to overwrite this function because questiontext is an array. + */ + protected function format_question_text($question) { + global $DB; + $formatoptions = new stdClass(); + $formatoptions->noclean = true; + return html_to_text(format_text($question->questiontext['text'], + $question->questiontext['format'], $formatoptions), 0, false); + } +} diff --git a/question/format/blackboard_six/formatpool.php b/question/format/blackboard_six/formatpool.php new file mode 100644 index 0000000000000..80867f34b85f6 --- /dev/null +++ b/question/format/blackboard_six/formatpool.php @@ -0,0 +1,467 @@ +. + +/** + * Blackboard V5 and V6 question importer. + * + * @package qformat_blackboard_six + * @copyright 2003 Scott Elliott + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/xmlize.php'); + +/** + * Blackboard pool question importer. + * + * @copyright 2003 Scott Elliott + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +class qformat_blackboard_six_pool extends qformat_blackboard_six_base { + // Is the current question's question text escaped HTML (true for most if not all Blackboard files). + public $ishtml = true; + + /** + * Parse the xml document 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) questions objects. + */ + protected function readquestions($text) { + + // 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(); + + $this->process_tf($xml, $questions); + $this->process_mc($xml, $questions); + $this->process_ma($xml, $questions); + $this->process_fib($xml, $questions); + $this->process_matching($xml, $questions); + $this->process_essay($xml, $questions); + + return $questions; + } + + /** + * 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) { + + // 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_six')); + + $question->questiontext = $this->cleaned_text_field($text); + $question->questiontextformat = FORMAT_HTML; // Needed because add_blank_combined_feedback uses it. + + // 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['text']), 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_six' , $id); + } + + $question->generalfeedback = ''; + $question->generalfeedbackformat = FORMAT_HTML; + $question->generalfeedbackfiles = array(); + + // 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; + } + + foreach ($essayquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + + $question->qtype = 'essay'; + + $question->answer = ''; + $answer = $this->getpath($thisquestion, + array('#', 'ANSWER', 0, '#', 'TEXT', 0, '#'), '', true); + $question->graderinfo = $this->cleaned_text_field($answer); + $question->feedback = ''; + $question->responseformat = 'editor'; + $question->responsefieldlines = 15; + $question->attachments = 0; + $question->fraction = 0; + + $questions[] = $question; + } + } + + /** + * 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; + } + + foreach ($tfquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + + $question->qtype = 'truefalse'; + $question->single = 1; // Only one answer is allowed. + + $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), array(), false); + + $correctanswer = $this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER', 0, '@', 'answer_id'), + '', true); + + // 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, $correctanswer) == 0) { // True is correct. + $question->answer = 1; + $question->feedbacktrue = $this->cleaned_text_field($correctfeedback); + $question->feedbackfalse = $this->cleaned_text_field($incorrectfeedback); + } else { // False is correct. + $question->answer = 0; + $question->feedbacktrue = $this->cleaned_text_field($incorrectfeedback); + $question->feedbackfalse = $this->cleaned_text_field($correctfeedback); + } + $question->correctanswer = $question->answer; + $questions[] = $question; + } + } + + /** + * 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; + } + + 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->cleaned_text_field($correctfeedback); + $question->partiallycorrectfeedback = $this->text_field(''); + $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); + + $question->qtype = 'multichoice'; + $question->single = 1; // Only one answer is allowed. + + $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); + $correctanswerid = $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->cleaned_text_field($choicetext); + + $choiceid = $this->getpath($choice, array('@', 'id'), '', true); + // If choice is the right answer, give 100% mark, otherwise give 0%. + if (strcmp ($choiceid, $correctanswerid) == 0) { + $question->fraction[] = 1; + } else { + $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 + * @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; + } + + 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->cleaned_text_field($correctfeedback); + // As there is no partially correct feedback we use incorrect one. + $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback); + $question->incorrectfeedback = $this->cleaned_text_field($incorrectfeedback); + + $question->qtype = 'multichoice'; + $question->defaultmark = 1; + $question->single = 0; // More than one answers allowed. + + $choices = $this->getpath($thisquestion, array('#', 'ANSWER'), false, false); + $correctanswerids = array(); + foreach ($this->getpath($thisquestion, + array('#', 'GRADABLE', 0, '#', 'CORRECTANSWER'), false, false) as $correctanswer) { + if ($correctanswer) { + $correctanswerids[] = $this->getpath($correctanswer, + array('@', 'answer_id'), + '', true); + } + } + $fraction = 1/count($correctanswerids); + + foreach ($choices as $choice) { + $choicetext = $this->getpath($choice, array('#', 'TEXT', 0, '#'), '', true); + // Put this choice in the question object. + $question->answer[] = $this->cleaned_text_field($choicetext); + + $choiceid = $this->getpath($choice, array('@', 'id'), '', true); + + $iscorrect = in_array($choiceid, $correctanswerids); + + if ($iscorrect) { + $question->fraction[] = $fraction; + } else { + $question->fraction[] = 0; + } + // There is never feedback specific to each choice. + $question->feedback[] = $this->text_field(''); + } + $questions[] = $question; + } + } + + /** + * 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; + } + + 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->cleaned_text_field($correctfeedback); + } + $question->answer[] = '*'; + $question->fraction[] = 0; + $question->feedback[] = $this->cleaned_text_field($incorrectfeedback); + + $questions[] = $question; + } + } + + /** + * 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; + } + // 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. + $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch'); + + foreach ($matchquestions as $thisquestion) { + + $question = $this->process_common($thisquestion); + if ($ddmatchisinstalled) { + $question->qtype = 'ddmatch'; + } else { + $question->qtype = 'match'; + } + + $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->cleaned_text_field($correctfeedback); + // As there is no partially correct feedback we use incorrect one. + $question->partiallycorrectfeedback = $this->cleaned_text_field($incorrectfeedback); + $question->incorrectfeedback = $this->cleaned_text_field($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) { + $correctchoiceid = $this->getpath($correctanswer, + array('@', 'choice_id'), '', true); + $correctanswerid = $this->getpath($correctanswer, + array('@', 'answer_id'), + '', true); + $mappings[$correctanswerid] = $correctchoiceid; + } + } + + foreach ($choices as $choice) { + if ($ddmatchisinstalled) { + $choicetext = $this->cleaned_text_field($this->getpath($choice, + array('#', 'TEXT', 0, '#'), '', true)); + } else { + $choicetext = trim(strip_tags($this->getpath($choice, + array('#', 'TEXT', 0, '#'), '', true))); + } + + if ($choicetext != '') { // Only import non empty subanswers. + $subquestion = ''; + $choiceid = $this->getpath($choice, + array('@', 'id'), '', true); + $fiber = array_search($choiceid, $mappings); + $fiber = array_keys ($mappings, $choiceid); + foreach ($fiber as $correctanswerid) { + // We have found a correspondance for this choice so we need to take the associated answer. + foreach ($answers as $answer) { + $currentanswerid = $this->getpath($answer, + array('@', 'id'), '', true); + if (strcmp ($currentanswerid, $correctanswerid) == 0) { + $subquestion = $this->getpath($answer, + array('#', 'TEXT', 0, '#'), '', true); + break; + } + } + $question->subquestions[] = $this->cleaned_text_field($subquestion); + $question->subanswers[] = $choicetext; + } + + if ($subquestion == '') { // Then in this case, $choice is a distractor. + $question->subquestions[] = $this->text_field(''); + $question->subanswers[] = $choicetext; + } + } + } + + // 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_six', $question->questiontext['text'])); + } else { + $questions[] = $question; + } + + } + } +} diff --git a/question/format/blackboard_six/formatqti.php b/question/format/blackboard_six/formatqti.php new file mode 100644 index 0000000000000..223d9f608ca26 --- /dev/null +++ b/question/format/blackboard_six/formatqti.php @@ -0,0 +1,894 @@ +. + +/** + * Blackboard V5 and V6 question importer. + * + * @package qformat_blackboard_six + * @copyright 2005 Michael Penney + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/xmlize.php'); + +/** + * Blackboard 6.0 question importer. + * + * @copyright 2005 Michael Penney + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class qformat_blackboard_six_qti extends qformat_blackboard_six_base { + /** + * Parse the xml document 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) questions objects. + */ + protected function readquestions($text) { + + // 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(); + // First step : we are only interested in the tags. + $rawquestions = $this->getpath($xml, + array('questestinterop', '#', 'assessment', 0, '#', 'section', 0, '#', 'item'), + array(), false); + // Each tag contains data related to a single question. + foreach ($rawquestions as $quest) { + // Second step : parse each question data into the intermediate + // rawquestion structure array. + // Warning : rawquestions are not Moodle questions. + $question = $this->create_raw_question($quest); + // Third step : convert a rawquestion into a Moodle question. + switch($question->qtype) { + case "Matching": + $this->process_matching($question, $questions); + break; + case "Multiple Choice": + $this->process_mc($question, $questions); + break; + case "Essay": + $this->process_essay($question, $questions); + break; + case "Multiple Answer": + $this->process_ma($question, $questions); + break; + case "True/False": + $this->process_tf($question, $questions); + break; + case 'Fill in the Blank': + $this->process_fblank($question, $questions); + break; + case 'Short Response': + $this->process_essay($question, $questions); + break; + default: + $this->error(get_string('unknownorunhandledtype', 'qformat_blackboard_six', $question->qtype)); + break; + } + } + return $questions; + } + + /** + * Creates a cleaner object to deal with for processing into Moodle. + * The object returned is NOT a moodle question object. + * @param array $quest XML question data + * @return object rawquestion + */ + public function create_raw_question($quest) { + + $rawquestion = new stdClass(); + $rawquestion->qtype = $this->getpath($quest, + array('#', 'itemmetadata', 0, '#', 'bbmd_questiontype', 0, '#'), + '', true); + $rawquestion->id = $this->getpath($quest, + array('#', 'itemmetadata', 0, '#', 'bbmd_asi_object_id', 0, '#'), + '', true); + $presentation = new stdClass(); + $presentation->blocks = $this->getpath($quest, + array('#', 'presentation', 0, '#', 'flow', 0, '#', 'flow'), + array(), false); + + foreach ($presentation->blocks as $pblock) { + $block = new stdClass(); + $block->type = $this->getpath($pblock, + array('@', 'class'), + '', true); + + switch($block->type) { + case 'QUESTION_BLOCK': + $subblocks = $this->getpath($pblock, + array('#', 'flow'), + array(), false); + foreach ($subblocks as $sblock) { + $this->process_block($sblock, $block); + } + break; + + case 'RESPONSE_BLOCK': + $choices = null; + switch($rawquestion->qtype) { + case 'Matching': + $bbsubquestions = $this->getpath($pblock, + array('#', 'flow'), + array(), false); + $sub_questions = array(); + foreach ($bbsubquestions as $bbsubquestion) { + $sub_question = new stdClass(); + $sub_question->ident = $this->getpath($bbsubquestion, + array('#', 'response_lid', 0, '@', 'ident'), + '', true); + $this->process_block($this->getpath($bbsubquestion, + array('#', 'flow', 0), + false, false), $sub_question); + $bbchoices = $this->getpath($bbsubquestion, + array('#', 'response_lid', 0, '#', 'render_choice', 0, + '#', 'flow_label', 0, '#', 'response_label'), + array(), false); + $choices = array(); + $this->process_choices($bbchoices, $choices); + $sub_question->choices = $choices; + if (!isset($block->subquestions)) { + $block->subquestions = array(); + } + $block->subquestions[] = $sub_question; + } + break; + case 'Multiple Answer': + $bbchoices = $this->getpath($pblock, + array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'), + array(), false); + $choices = array(); + $this->process_choices($bbchoices, $choices); + $block->choices = $choices; + break; + case 'Essay': + // Doesn't apply since the user responds with text input. + break; + case 'Multiple Choice': + $mcchoices = $this->getpath($pblock, + array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', 'flow_label'), + array(), false); + foreach ($mcchoices as $mcchoice) { + $choices = new stdClass(); + $choices = $this->process_block($mcchoice, $choices); + $block->choices[] = $choices; + } + break; + case 'Short Response': + // Do nothing? + break; + case 'Fill in the Blank': + // Do nothing? + break; + default: + $bbchoices = $this->getpath($pblock, + array('#', 'response_lid', 0, '#', 'render_choice', 0, '#', + 'flow_label', 0, '#', 'response_label'), + array(), false); + $choices = array(); + $this->process_choices($bbchoices, $choices); + $block->choices = $choices; + } + break; + case 'RIGHT_MATCH_BLOCK': + $matchinganswerset = $this->getpath($pblock, + array('#', 'flow'), + false, false); + + $answerset = array(); + foreach ($matchinganswerset as $answer) { + $bbanswer = new stdClass; + $bbanswer->text = $this->getpath($answer, + array('#', 'flow', 0, '#', 'material', 0, '#', 'mat_extension', + 0, '#', 'mat_formattedtext', 0, '#'), + false, false); + $answerset[] = $bbanswer; + } + $block->matchinganswerset = $answerset; + break; + default: + $this->error(get_string('unhandledpresblock', 'qformat_blackboard_six')); + break; + } + $rawquestion->{$block->type} = $block; + } + + // Determine response processing. + // There is a section called 'outcomes' that I don't know what to do with. + $resprocessing = $this->getpath($quest, + array('#', 'resprocessing'), + array(), false); + + $respconditions = $this->getpath($resprocessing[0], + array('#', 'respcondition'), + array(), false); + $responses = array(); + if ($rawquestion->qtype == 'Matching') { + $this->process_matching_responses($respconditions, $responses); + } else { + $this->process_responses($respconditions, $responses); + } + $rawquestion->responses = $responses; + $feedbackset = $this->getpath($quest, + array('#', 'itemfeedback'), + array(), false); + + $feedbacks = array(); + $this->process_feedback($feedbackset, $feedbacks); + $rawquestion->feedback = $feedbacks; + return $rawquestion; + } + + /** + * Helper function to process an XML block into an object. + * Can call himself recursively if necessary to parse this branch of the XML tree. + * @param array $curblock XML block to parse + * @return object $block parsed + */ + public function process_block($curblock, $block) { + + $curtype = $this->getpath($curblock, + array('@', 'class'), + '', true); + + switch($curtype) { + case 'FORMATTED_TEXT_BLOCK': + $text = $this->getpath($curblock, + array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'), + '', true); + $block->text = $this->strip_applet_tags_get_mathml($text); + break; + case 'FILE_BLOCK': + $block->filename = $this->getpath($curblock, + array('#', 'material', 0, '#'), + '', true); + if ($block->filename != '') { + // TODO : determine what to do with the file's content. + $this->error(get_string('filenothandled', 'qformat_blackboard_six', $block->filename)); + } + break; + case 'Block': + if ($this->getpath($curblock, + array('#', 'material', 0, '#', 'mattext'), + false, false)) { + $block->text = $this->getpath($curblock, + array('#', 'material', 0, '#', 'mattext', 0, '#'), + '', true); + } else if ($this->getpath($curblock, + array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext'), + false, false)) { + $block->text = $this->getpath($curblock, + array('#', 'material', 0, '#', 'mat_extension', 0, '#', 'mat_formattedtext', 0, '#'), + '', true); + } else if ($this->getpath($curblock, + array('#', 'response_label'), + false, false)) { + // This is a response label block. + $subblocks = $this->getpath($curblock, + array('#', 'response_label', 0), + array(), false); + if (!isset($block->ident)) { + + if ($this->getpath($subblocks, + array('@', 'ident'), '', true)) { + $block->ident = $this->getpath($subblocks, + array('@', 'ident'), '', true); + } + } + foreach ($this->getpath($subblocks, + array('#', 'flow_mat'), array(), false) as $subblock) { + $this->process_block($subblock, $block); + } + } else { + if ($this->getpath($curblock, + array('#', 'flow_mat'), false, false) + || $this->getpath($curblock, + array('#', 'flow'), false, false)) { + if ($this->getpath($curblock, + array('#', 'flow_mat'), false, false)) { + $subblocks = $this->getpath($curblock, + array('#', 'flow_mat'), array(), false); + } else if ($this->getpath($curblock, + array('#', 'flow'), false, false)) { + $subblocks = $this->getpath($curblock, + array('#', 'flow'), array(), false); + } + foreach ($subblocks as $sblock) { + // This will recursively grab the sub blocks which should be of one of the other types. + $this->process_block($sblock, $block); + } + } + } + break; + case 'LINK_BLOCK': + // Not sure how this should be included? + $link = $this->getpath($curblock, + array('#', 'material', 0, '#', 'mattext', 0, '@', 'uri'), '', true); + if (!empty($link)) { + $block->link = $link; + } else { + $block->link = ''; + } + break; + } + return $block; + } + + /** + * Preprocess XML blocks containing data for questions' choices. + * Called by {@link create_raw_question()} + * for matching, multichoice and fill in the blank questions. + * @param array $bbchoices XML block to parse + * @param array $choices array of choices suitable for a rawquestion. + */ + protected function process_choices($bbchoices, &$choices) { + foreach ($bbchoices as $choice) { + if ($this->getpath($choice, + array('@', 'ident'), '', true)) { + $curchoice = $this->getpath($choice, + array('@', 'ident'), '', true); + } else { // For multiple answers. + $curchoice = $this->getpath($choice, + array('#', 'response_label', 0), array(), false); + } + if ($this->getpath($choice, + array('#', 'flow_mat', 0), false, false)) { // For multiple answers. + $curblock = $this->getpath($choice, + array('#', 'flow_mat', 0), false, false); + // Reset $curchoice to new stdClass because process_block is expecting an object + // for the second argument and not a string, + // which is what is was set as originally - CT 8/7/06. + $curchoice = new stdClass(); + $this->process_block($curblock, $curchoice); + } else if ($this->getpath($choice, + array('#', 'response_label'), false, false)) { + // Reset $curchoice to new stdClass because process_block is expecting an object + // for the second argument and not a string, + // which is what is was set as originally - CT 8/7/06. + $curchoice = new stdClass(); + $this->process_block($choice, $curchoice); + } + $choices[] = $curchoice; + } + } + + /** + * Preprocess XML blocks containing data for subanswers + * Called by {@link create_raw_question()} + * for matching questions only. + * @param array $bbresponses XML block to parse + * @param array $responses array of responses suitable for a matching rawquestion. + */ + protected function process_matching_responses($bbresponses, &$responses) { + foreach ($bbresponses as $bbresponse) { + $response = new stdClass; + if ($this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'varequal'), false, false)) { + $response->correct = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'varequal', 0, '#'), '', true); + $response->ident = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'varequal', 0, '@', 'respident'), '', true); + } + // Suppressed an else block because if the above if condition is false, + // the question is not necessary a broken one, most of the time it's an tag. + + $response->feedback = $this->getpath($bbresponse, + array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); + $responses[] = $response; + } + } + + /** + * Preprocess XML blocks containing data for responses processing. + * Called by {@link create_raw_question()} + * for all questions types. + * @param array $bbresponses XML block to parse + * @param array $responses array of responses suitable for a rawquestion. + */ + protected function process_responses($bbresponses, &$responses) { + foreach ($bbresponses as $bbresponse) { + $response = new stdClass(); + if ($this->getpath($bbresponse, + array('@', 'title'), '', true)) { + $response->title = $this->getpath($bbresponse, + array('@', 'title'), '', true); + } else { + $response->title = $this->getpath($bbresponse, + array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); + } + $response->ident = array(); + if ($this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#'), false, false)) { + $response->ident[0] = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#'), array(), false); + } else if ($this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'other', 0, '#'), false, false)) { + $response->ident[0] = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'other', 0, '#'), array(), false); + } + if ($this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'and'), false, false)) { + $responseset = $this->getpath($bbresponse, + array('#', 'conditionvar', 0, '#', 'and'), array(), false); + foreach ($responseset as $rs) { + $response->ident[] = $this->getpath($rs, array('#'), array(), false); + if (!isset($response->feedback) and $this->getpath($rs, array('@'), false, false)) { + $response->feedback = $this->getpath($rs, + array('@', 'respident'), '', true); + } + } + } else { + $response->feedback = $this->getpath($bbresponse, + array('#', 'displayfeedback', 0, '@', 'linkrefid'), '', true); + } + + // Determine what fraction to give response. + if ($this->getpath($bbresponse, + array('#', 'setvar'), false, false)) { + switch ($this->getpath($bbresponse, + array('#', 'setvar', 0, '#'), false, false)) { + case "SCORE.max": + $response->fraction = 1; + break; + default: + // I have only seen this being 0 or unset. + // There are probably fractional values of SCORE.max, but I'm not sure what they look like. + $response->fraction = 0; + break; + } + } else { + // Just going to assume this is the case this is probably not correct. + $response->fraction = 0; + } + + $responses[] = $response; + } + } + + /** + * Preprocess XML blocks containing data for responses feedbacks. + * Called by {@link create_raw_question()} + * for all questions types. + * @param array $feedbackset XML block to parse + * @param array $feedbacks array of feedbacks suitable for a rawquestion. + */ + public function process_feedback($feedbackset, &$feedbacks) { + foreach ($feedbackset as $bb_feedback) { + $feedback = new stdClass(); + $feedback->ident = $this->getpath($bb_feedback, + array('@', 'ident'), '', true); + $feedback->text = ''; + if ($this->getpath($bb_feedback, + array('#', 'flow_mat', 0), false, false)) { + $this->process_block($this->getpath($bb_feedback, + array('#', 'flow_mat', 0), false, false), $feedback); + } else if ($this->getpath($bb_feedback, + array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false)) { + $this->process_block($this->getpath($bb_feedback, + array('#', 'solution', 0, '#', 'solutionmaterial', 0, '#', 'flow_mat', 0), false, false), $feedback); + } + + $feedbacks[$feedback->ident] = $feedback; + } + } + + /** + * Create common parts of question + * @param object $quest rawquestion + * @return object Moodle question. + */ + public function process_common($quest) { + $question = $this->defaultquestion(); + $text = $quest->QUESTION_BLOCK->text; + + $question->questiontext = $this->cleaned_text_field($text); + $question->questiontextformat = FORMAT_HTML; // Needed because add_blank_combined_feedback uses it. + + $question->name = shorten_text(strip_tags($question->questiontext['text']), 200); + $question->name = substr($question->name, 0, 250); + if (!$question->name) { + $question->name = get_string('defaultname', 'qformat_blackboard_six' , $quest->id); + } + $question->generalfeedback = ''; + $question->generalfeedbackformat = FORMAT_HTML; + $question->generalfeedbackfiles = array(); + + return $question; + } + + /** + * Process True / False Questions + * Parse a truefalse rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + protected function process_tf($quest, &$questions) { + $question = $this->process_common($quest); + + $question->qtype = 'truefalse'; + $question->single = 1; // Only one answer is allowed. + $question->penalty = 1; // Penalty = 1 for truefalse questions. + // 0th [response] is the correct answer. + $responses = $quest->responses; + $correctresponse = $this->getpath($responses[0]->ident[0], + array('varequal', 0, '#'), '', true); + if ($correctresponse != 'false') { + $correct = true; + } else { + $correct = false; + } + $fback = new stdClass(); + + foreach ($quest->feedback as $fb) { + $fback->{$fb->ident} = $fb->text; + } + + if ($correct) { // True is correct. + $question->answer = 1; + $question->feedbacktrue = $this->cleaned_text_field($fback->correct); + $question->feedbackfalse = $this->cleaned_text_field($fback->incorrect); + } else { // False is correct. + $question->answer = 0; + $question->feedbacktrue = $this->cleaned_text_field($fback->incorrect); + $question->feedbackfalse = $this->cleaned_text_field($fback->correct); + } + $question->correctanswer = $question->answer; + $questions[] = $question; + } + + /** + * Process Fill in the Blank Questions + * Parse a fillintheblank rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + protected function process_fblank($quest, &$questions) { + $question = $this->process_common($quest); + $question->qtype = 'shortanswer'; + $question->usecase = 0; // Ignore case. + + $answers = array(); + $fractions = array(); + $feedbacks = array(); + + // Extract the feedback. + $feedback = array(); + foreach ($quest->feedback as $fback) { + if (isset($fback->ident)) { + if ($fback->ident == 'correct' || $fback->ident == 'incorrect') { + $feedback[$fback->ident] = $fback->text; + } + } + } + + foreach ($quest->responses as $response) { + if (isset($response->title)) { + if ($this->getpath($response->ident[0], + array('varequal', 0, '#'), false, false)) { + // For BB Fill in the Blank, only interested in correct answers. + if ($response->feedback = 'correct') { + $answers[] = $this->getpath($response->ident[0], + array('varequal', 0, '#'), '', true); + $fractions[] = 1; + if (isset($feedback['correct'])) { + $feedbacks[] = $this->cleaned_text_field($feedback['correct']); + } else { + $feedbacks[] = $this->text_field(''); + } + } + } + + } + } + + // Adding catchall to so that students can see feedback for incorrect answers when they enter something, + // the instructor did not enter. + $answers[] = '*'; + $fractions[] = 0; + if (isset($feedback['incorrect'])) { + $feedbacks[] = $this->cleaned_text_field($feedback['incorrect']); + } else { + $feedbacks[] = $this->text_field(''); + } + + $question->answer = $answers; + $question->fraction = $fractions; + $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of. + + if (!empty($question)) { + $questions[] = $question; + } + + } + + /** + * Process Multichoice Questions + * Parse a multichoice single answer rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + protected function process_mc($quest, &$questions) { + $question = $this->process_common($quest); + $question->qtype = 'multichoice'; + $question = $this->add_blank_combined_feedback($question); + $question->single = 1; + $feedback = array(); + foreach ($quest->feedback as $fback) { + $feedback[$fback->ident] = $fback->text; + } + + foreach ($quest->responses as $response) { + if (isset($response->title)) { + if ($response->title == 'correct') { + // Only one answer possible for this qtype so first index is correct answer. + $correct = $this->getpath($response->ident[0], + array('varequal', 0, '#'), '', true); + } + } else { + // Fallback method for when the title is not set. + if ($response->feedback == 'correct') { + // Only one answer possible for this qtype so first index is correct answer. + $correct = $this->getpath($response->ident[0], + array('varequal', 0, '#'), '', true); + } + } + } + + $i = 0; + foreach ($quest->RESPONSE_BLOCK->choices as $response) { + $question->answer[$i] = $this->cleaned_text_field($response->text); + if ($correct == $response->ident) { + $question->fraction[$i] = 1; + // This is a bit of a hack to catch the feedback... first we see if a 'specific' + // feedback for this response exists, then if a 'correct' feedback exists. + + if (!empty($feedback[$response->ident]) ) { + $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]); + } else if (!empty($feedback['correct'])) { + $question->feedback[$i] = $this->cleaned_text_field($feedback['correct']); + } else if (!empty($feedback[$i])) { + $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]); + } else { + $question->feedback[$i] = $this->cleaned_text_field(get_string('correct', 'question')); + } + } else { + $question->fraction[$i] = 0; + if (!empty($feedback[$response->ident]) ) { + $question->feedback[$i] = $this->cleaned_text_field($feedback[$response->ident]); + } else if (!empty($feedback['incorrect'])) { + $question->feedback[$i] = $this->cleaned_text_field($feedback['incorrect']); + } else if (!empty($feedback[$i])) { + $question->feedback[$i] = $this->cleaned_text_field($feedback[$i]); + } else { + $question->feedback[$i] = $this->cleaned_text_field(get_string('incorrect', 'question')); + } + } + $i++; + } + + if (!empty($question)) { + $questions[] = $question; + } + } + + /** + * Process Multiple Choice Questions With Multiple Answers. + * Parse a multichoice multianswer rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + public function process_ma($quest, &$questions) { + $question = $this->process_common($quest); + $question->qtype = 'multichoice'; + $question = $this->add_blank_combined_feedback($question); + $question->single = 0; // More than one answer allowed. + + $answers = $quest->responses; + $correctanswers = array(); + foreach ($answers as $answer) { + if ($answer->title == 'correct') { + $answerset = $this->getpath($answer->ident[0], + array('and', 0, '#', 'varequal'), array(), false); + foreach ($answerset as $ans) { + $correctanswers[] = $ans['#']; + } + } + } + $feedback = new stdClass(); + foreach ($quest->feedback as $fb) { + $feedback->{$fb->ident} = trim($fb->text); + } + + $correctanswercount = count($correctanswers); + $fraction = 1/$correctanswercount; + $choiceset = $quest->RESPONSE_BLOCK->choices; + $i = 0; + foreach ($choiceset as $choice) { + $question->answer[$i] = $this->cleaned_text_field(trim($choice->text)); + if (in_array($choice->ident, $correctanswers)) { + // Correct answer. + $question->fraction[$i] = $fraction; + $question->feedback[$i] = $this->cleaned_text_field($feedback->correct); + } else { + // Wrong answer. + $question->fraction[$i] = 0; + $question->feedback[$i] = $this->cleaned_text_field($feedback->incorrect); + } + $i++; + } + + $questions[] = $question; + } + + /** + * Process Essay Questions + * Parse an essay rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + public function process_essay($quest, &$questions) { + + $question = $this->process_common($quest); + $question->qtype = 'essay'; + + $question->feedback = array(); + // Not sure where to get the correct answer from? + foreach ($quest->feedback as $feedback) { + // Added this code to put the possible solution that the + // instructor gives as the Moodle answer for an essay question. + if ($feedback->ident == 'solution') { + $question->graderinfo = $this->cleaned_text_field($feedback->text); + } + } + // Added because essay/questiontype.php:save_question_option is expecting a + // fraction property - CT 8/10/06. + $question->fraction[] = 1; + $question->defaultmark = 1; + $question->responseformat = 'editor'; + $question->responsefieldlines = 15; + $question->attachments = 0; + + $questions[]=$question; + } + + /** + * Process Matching Questions + * Parse a matching rawquestion and add the result + * to the array of questions already parsed. + * @param object $quest rawquestion + * @param $questions array of Moodle questions already done. + */ + public function process_matching($quest, &$questions) { + + // Blackboard matching 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. + $ddmatchisinstalled = question_bank::is_qtype_installed('ddmatch'); + + $question = $this->process_common($quest); + $question = $this->add_blank_combined_feedback($question); + $question->valid = true; + if ($ddmatchisinstalled) { + $question->qtype = 'ddmatch'; + } else { + $question->qtype = 'match'; + } + // Construction of the array holding mappings between subanswers and subquestions. + foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { + foreach ($quest->responses as $rid => $resp) { + if (isset($resp->ident) && $resp->ident == $subq->ident) { + $correct = $resp->correct; + } + } + + foreach ($subq->choices as $cid => $choice) { + if ($choice == $correct) { + $mappings[$subq->ident] = $cid; + } + } + } + + foreach ($subq->choices as $choiceid => $choice) { + $subanswertext = $quest->RIGHT_MATCH_BLOCK->matchinganswerset[$choiceid]->text; + if ($ddmatchisinstalled) { + $subanswer = $this->cleaned_text_field($subanswertext); + } else { + $subanswertext = html_to_text($this->cleaninput($subanswertext), 0); + $subanswer = $subanswertext; + } + + if ($subanswertext != '') { // Only import non empty subanswers. + $subquestion = ''; + + $fiber = array_keys ($mappings, $choiceid); + foreach ($fiber as $correctanswerid) { + // We have found a correspondance for this subanswer so we need to take the associated subquestion. + foreach ($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { + $currentsubqid = $subq->ident; + if (strcmp ($currentsubqid, $correctanswerid) == 0) { + $subquestion = $subq->text; + break; + } + } + $question->subquestions[] = $this->cleaned_text_field($subquestion); + $question->subanswers[] = $subanswer; + } + + if ($subquestion == '') { // Then in this case, $choice is a distractor. + $question->subquestions[] = $this->text_field(''); + $question->subanswers[] = $subanswer; + } + } + } + + // 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_six', $question->questiontext['text'])); + } else { + $questions[] = $question; + } + } + + /** + * Strip the applet tag used by Blackboard to render mathml formulas, + * keeping the mathml tag. + * @param string $string + * @return string + */ + public function strip_applet_tags_get_mathml($string) { + if (stristr($string, '') === false) { + return $string; + } else { + // Strip all applet tags keeping stuff before/after and inbetween (if mathml) them. + while (stristr($string, '') !== false) { + preg_match("/(.*)\.*\<\/math\>)\".*\<\/applet\>(.*)/i", $string, $mathmls); + $string = $mathmls[1].$mathmls[2].$mathmls[3]; + } + return $string; + } + } + +} diff --git a/question/format/blackboard_six/lang/en/qformat_blackboard_six.php b/question/format/blackboard_six/lang/en/qformat_blackboard_six.php index 75ff1e1e19021..020c7a25e8e4c 100644 --- a/question/format/blackboard_six/lang/en/qformat_blackboard_six.php +++ b/question/format/blackboard_six/lang/en/qformat_blackboard_six.php @@ -17,11 +17,18 @@ /** * Strings for component 'qformat_blackboard_six', language 'en', branch 'MOODLE_20_STABLE' * - * @package qformat - * @subpackage blackboard_six + * @package qformat_blackboard_six * @copyright 2010 Helen Foster * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ +$string['defaultname'] = 'Imported question {$a}'; +$string['errormanifest'] = 'Error while parsing the IMS manifest document'; +$string['importnotext'] = 'Missing question text in XML file'; +$string['filenothandled'] = 'This archive contains reference to a file material {$a} wich is not currently handled by import'; +$string['imagenotfound'] = 'Image file with path {$a} was not found in the import.'; +$string['notenoughtsubans'] = 'Unable to import matching question \'{$a}\' because a matching question must comprise at least two questions and three answers.'; $string['pluginname'] = 'Blackboard V6+'; -$string['pluginname_help'] = 'Blackboard V6+ format enables questions saved in Blackboard\'s export format to be imported via zip file. It provides limited support for Blackboard Version 6 and 7.'; +$string['pluginname_help'] = 'Blackboard V6+ format enables questions saved in all Blackboard export formats to be imported via a dat or zip file. For zip files, images import is supported.'; +$string['unhandledpresblock'] = 'Unhandled presentation bloc'; +$string['unknownorunhandledtype'] = 'Unknown or unhandled question type: {$a}'; diff --git a/question/format/blackboard_six/tests/blackboardformatpool_test.php b/question/format/blackboard_six/tests/blackboardformatpool_test.php new file mode 100644 index 0000000000000..22cadb73589d9 --- /dev/null +++ b/question/format/blackboard_six/tests/blackboardformatpool_test.php @@ -0,0 +1,330 @@ +. + +/** + * Unit tests for the Moodle Blackboard V6+ format. + * + * @package qformat_blackboard_six + * @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_six/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_six_pool_test extends question_testcase { + + public function make_test_xml() { + $xml = file_get_contents(__DIR__ . '/fixtures/sample_blackboard_pool.dat'); + return array(0=>$xml); + } + + public function test_import_match() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + + $q = $questions[4]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'match'; + $expectedq->name = 'Classify the animals.'; + $expectedq->questiontext = array( + 'text' => 'Classify the animals.', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array('text' => '', + 'format' => FORMAT_HTML); + $expectedq->partiallycorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML); + $expectedq->incorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML); + $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), + array('text' => '', 'format' => FORMAT_HTML), + array('text' => 'frog', 'format' => FORMAT_HTML), + array('text' => 'newt', 'format' => FORMAT_HTML)); + $expectedq->subanswers = array('mammal', 'insect', 'amphibian', 'amphibian'); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_single() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[1]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 1; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = array( + 'text' =>'What\'s between orange and green in the spectrum?', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array('text' => 'You gave the right answer.', + 'format' => FORMAT_HTML); + $expectedq->partiallycorrectfeedback = array('text' => '', + 'format' => FORMAT_HTML); + $expectedq->incorrectfeedback = array('text' => 'Only yellow is between orange and green in the spectrum.', + 'format' => FORMAT_HTML); + $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' => 'red', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'yellow', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'blue', + 'format' => FORMAT_HTML, + ) + ); + $expectedq->fraction = array(0, 1, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_multi() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[2]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 0; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = array( + 'text' => 'What\'s between orange and green in the spectrum?', + 'format' => FORMAT_HTML, + ); + $expectedq->questiontextformat = FORMAT_HTML; + $expectedq->correctfeedback = array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + ); + $expectedq->partiallycorrectfeedback = array( + 'text' => 'Only yellow and off-beige are between orange and green in the spectrum.', + 'format' => FORMAT_HTML, + ); + $expectedq->incorrectfeedback = array( + 'text' => 'Only yellow and off-beige are between orange and green in the spectrum.', + 'format' => FORMAT_HTML, + ); + $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' => 'yellow', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'red', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'off-beige', + 'format' => FORMAT_HTML, + ), + 3 => array( + 'text' => 'blue', + 'format' => FORMAT_HTML, + ) + ); + $expectedq->fraction = array(0.5, 0, 0.5, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 3 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_truefalse() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[0]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'truefalse'; + $expectedq->name = '42 is the Absolute Answer to everything.'; + $expectedq->questiontext = array( + 'text' => '42 is the Absolute Answer to everything.', + 'format' => FORMAT_HTML, + ); + $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, + ); + $expectedq->feedbackfalse = array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + ); + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_fill_in_the_blank() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[3]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'shortanswer'; + $expectedq->name = 'Name an amphibian: __________.'; + $expectedq->questiontext = array( + 'text' => 'Name an amphibian: __________.', + 'format' => FORMAT_HTML, + ); + $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, + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_essay() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(2); + $questions = $importer->readquestions($xml); + $q = $questions[5]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'essay'; + $expectedq->name = 'How are you?'; + $expectedq->questiontext = array( + 'text' => 'How are you?', + 'format' => FORMAT_HTML, + ); + $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, + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } +} diff --git a/question/format/blackboard_six/tests/blackboardsixformatqti_test.php b/question/format/blackboard_six/tests/blackboardsixformatqti_test.php new file mode 100644 index 0000000000000..68df181bba3d9 --- /dev/null +++ b/question/format/blackboard_six/tests/blackboardsixformatqti_test.php @@ -0,0 +1,330 @@ +. + +/** + * Unit tests for the Moodle Blackboard V6+ format. + * + * @package qformat_blackboard_six + * @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_six/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_six_qti_test extends question_testcase { + + public function make_test_xml() { + $xml = file_get_contents(__DIR__ . '/fixtures/sample_blackboard_qti.dat'); + return array(0=>$xml); + } + public function test_import_match() { + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[3]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'match'; + $expectedq->name = 'Classify the animals.'; + $expectedq->questiontext = array( + 'text' => 'Classify the animals.', + 'format' => FORMAT_HTML, + ); + $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' => '', 'format' => FORMAT_HTML), + array('text' => 'cat', 'format' => FORMAT_HTML), + array('text' => 'frog', 'format' => FORMAT_HTML), + array('text' => 'newt', 'format' => FORMAT_HTML)); + $expectedq->subanswers = array('insect', 'mammal', 'amphibian', 'amphibian'); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_single() { + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[1]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 1; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = array( + 'text' => 'What\'s between orange and green in the spectrum?', + 'format' => FORMAT_HTML, + ); + $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->answer = array( + 0 => array( + 'text' => 'red', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'yellow', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'blue', + 'format' => FORMAT_HTML, + ) + ); + $expectedq->fraction = array(0, 1, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => 'Red is not between orange and green in the spectrum but yellow is.', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'Blue is not between orange and green in the spectrum but yellow is.', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_multichoice_multi() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[2]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'multichoice'; + $expectedq->single = 0; + $expectedq->name = 'What\'s between orange and green in the spectrum?'; + $expectedq->questiontext = array( + 'text' => 'What\'s between orange and green in the spectrum?', + 'format' => FORMAT_HTML, + ); + $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->answer = array( + 0 => array( + 'text' => 'yellow', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'red', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => 'off-beige', + 'format' => FORMAT_HTML, + ), + 3 => array( + 'text' => 'blue', + 'format' => FORMAT_HTML, + ) + ); + $expectedq->fraction = array(0.5, 0, 0.5, 0); + $expectedq->feedback = array( + 0 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 2 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ), + 3 => array( + 'text' => '', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_truefalse() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[0]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'truefalse'; + $expectedq->name = '42 is the Absolute Answer to everything.'; + $expectedq->questiontext = array( + 'text' => '42 is the Absolute Answer to everything.', + 'format' => FORMAT_HTML, + ); + $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, + ); + $expectedq->feedbackfalse = array( + 'text' => 'You gave the right answer.', + 'format' => FORMAT_HTML, + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_fill_in_the_blank() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[4]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'shortanswer'; + $expectedq->name = 'Name an amphibian: __________.'; + $expectedq->questiontext = array( + 'text' => 'Name an amphibian: __________.', + 'format' => FORMAT_HTML, + ); + $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' => 'A frog is an amphibian.', + 'format' => FORMAT_HTML, + ), + 1 => array( + 'text' => 'A frog is an amphibian.', + 'format' => FORMAT_HTML, + ) + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } + + public function test_import_essay() { + + $xml = $this->make_test_xml(); + + $importer = new qformat_blackboard_six(); + $importer->set_filetype(1); + $questions = $importer->readquestions($xml); + $q = $questions[5]; + + $expectedq = new stdClass(); + $expectedq->qtype = 'essay'; + $expectedq->name = 'How are you?'; + $expectedq->questiontext = array( + 'text' => 'How are you?', + 'format' => FORMAT_HTML + ); + $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, + ); + + $this->assert(new question_check_specified_fields_expectation($expectedq), $q); + } +} diff --git a/question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat b/question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat new file mode 100644 index 0000000000000..93bb583a3a447 --- /dev/null +++ b/question/format/blackboard_six/tests/fixtures/sample_blackboard_pool.dat @@ -0,0 +1,142 @@ + + + + <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='1'/> + <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> diff --git a/question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat b/question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat new file mode 100644 index 0000000000000..4705c2b856730 --- /dev/null +++ b/question/format/blackboard_six/tests/fixtures/sample_blackboard_qti.dat @@ -0,0 +1,1058 @@ +<?xml version="1.0" encoding="UTF-8"?> +<questestinterop> + <assessment title="sample_blackboard_six"> + <section> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>E03EE0034702442C9306629CBF618049</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>True/False</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">42 is the Absolute Answer to everything.</span></mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_lid ident="response" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="No"> + <flow_label class="Block"> + <response_label ident="true" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="Block"> + <material> + <mattext charset="us-ascii" texttype="text/plain" xml:space="default">true</mattext> + </material> + </flow_mat> + </response_label> + <response_label ident="false" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="Block"> + <material> + <mattext charset="us-ascii" texttype="text/plain" xml:space="default">false</mattext> + </material> + </flow_mat> + </response_label> + </flow_label> + </render_choice> + </response_lid> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="correct"> + <conditionvar> + <varequal case="No" respident="response">false</varequal> + </conditionvar> + <setvar action="Set" variablename="SCORE">SCORE.max</setvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">You gave the right answer.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">42 is the <b>Ultimate</b> Answer.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>C74698725FFD4F85A692662108608D53</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Multiple Choice</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">What's between orange and green in the spectrum?</span></mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_lid ident="response" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="7C2A0246CE8D46599FC0120BAE9FC92D" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">red</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="2CBE1E044DE54F8395BDE7877A57837A" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">yellow</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="67A8748A0883467FB45328E922C31D29" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">blue</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + </render_choice> + </response_lid> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="correct"> + <conditionvar> + <varequal case="No" respident="response">2CBE1E044DE54F8395BDE7877A57837A</varequal> + </conditionvar> + <setvar action="Set" variablename="SCORE">SCORE.max</setvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="7C2A0246CE8D46599FC0120BAE9FC92D" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">Red is not between orange and green in the spectrum but yellow is.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + <itemfeedback ident="2CBE1E044DE54F8395BDE7877A57837A" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">You gave the right answer.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + <itemfeedback ident="67A8748A0883467FB45328E922C31D29" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">Blue is not between orange and green in the spectrum but yellow is.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>C18C56154AA04D56A4AE2FE430F4F49D</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Multiple Answer</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><i>What's between orange and green in the spectrum?</i></mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_lid ident="response" rcardinality="Multiple" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="76CA08C366984445AC94B0244D1DBF4A" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">yellow</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="FEC2A9886C8B498787A573C9181C9698" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">red</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="7F66D24D2CAA472EA728773D46706DF3" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">off-beige</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + <flow_label class="Block"> + <response_label ident="547B16C1D788446396618EDD0A41D623" rarea="Ellipse" rrange="Exact" shuffle="Yes"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">blue</span></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </response_label> + </flow_label> + </render_choice> + </response_lid> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="correct"> + <conditionvar> + <and> + <varequal case="No" respident="response">76CA08C366984445AC94B0244D1DBF4A</varequal> + <not> + <varequal case="No" respident="response">FEC2A9886C8B498787A573C9181C9698</varequal> + </not> + <varequal case="No" respident="response">7F66D24D2CAA472EA728773D46706DF3</varequal> + <not> + <varequal case="No" respident="response">547B16C1D788446396618EDD0A41D623</varequal> + </not> + </and> + </conditionvar> + <setvar action="Set" variablename="SCORE">SCORE.max</setvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>9C5DBA6A142A4B5887C61D333CFFEDA9</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Matching</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,3</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>3</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">Classify the animals.</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">cat</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + <response_lid ident="6D3235200B3F43DFA8FA13E2B31BB40B" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="2F591AA030B240EF869FD56392FC41BC" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="D75FEB705DCE41D59106659A2F94D819" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="207B18A11C4B42BF87882A4BAF3CC805" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + </flow_label> + </render_choice> + </response_lid> + </flow> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">frog</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + <response_lid ident="0933892218204F5AB561E62A27701447" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="2C1FB19B5F9A4F7A85E798B9C46B8BF8" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="0EEF502254D2496FB21FFD82B0A7F2B9" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="8A9B69A93B0943AFAB890702199AB290" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + </flow_label> + </render_choice> + </response_lid> + </flow> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">newt</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + <response_lid ident="80F56B540A44490B8E94EC71C4584722" rcardinality="Single" rtiming="No"> + <render_choice maxnumber="0" minnumber="0" shuffle="Yes"> + <flow_label class="Block"> + <response_label ident="1D066C2DAEF349EB8E7845B339B0A4A9" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="F2FB88DFE0D04DBEBD42B961728CA022" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + <response_label ident="4D1F5B3DB0EB4C41A0012625750DF86C" rarea="Ellipse" rrange="Exact" shuffle="Yes"/> + </flow_label> + </render_choice> + </response_lid> + </flow> + </flow> + <flow class="RIGHT_MATCH_BLOCK"> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">insect</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">mammal</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="Block"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">amphibian</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="3.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition> + <conditionvar> + <varequal case="No" respident="6D3235200B3F43DFA8FA13E2B31BB40B">D75FEB705DCE41D59106659A2F94D819</varequal> + </conditionvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition> + <conditionvar> + <varequal case="No" respident="0933892218204F5AB561E62A27701447">8A9B69A93B0943AFAB890702199AB290</varequal> + </conditionvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition> + <conditionvar> + <varequal case="No" respident="80F56B540A44490B8E94EC71C4584722">4D1F5B3DB0EB4C41A0012625750DF86C</varequal> + </conditionvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>DD76E663D4244C598FC91CFC433F6D5B</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Fill in the Blank</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"><span style="font-size:12pt">Name an amphibian&#58; __________.</span></mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_str ident="response" rcardinality="Single" rtiming="No"> + <render_fib charset="us-ascii" columns="127" encoding="UTF_8" fibtype="String" maxchars="0" maxnumber="0" minnumber="0" prompt="Box" rows="1"/> + </response_str> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="1CE934E53BDB437B8FD315E68063DA47"> + <conditionvar> + <varequal case="No" respident="response">frog</varequal> + </conditionvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + <displayfeedback feedbacktype="Response" linkrefid="1CE934E53BDB437B8FD315E68063DA47"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">A frog is an amphibian.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">A frog is an amphibian.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="1CE934E53BDB437B8FD315E68063DA47" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">A frog is an amphibian.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + </item> + <item maxattempts="0"> + <itemmetadata> + <bbmd_asi_object_id>39970AD8D5AE425A82338E17D42B7845</bbmd_asi_object_id> + <bbmd_asitype>Item</bbmd_asitype> + <bbmd_assessmenttype>Pool</bbmd_assessmenttype> + <bbmd_sectiontype>Subsection</bbmd_sectiontype> + <bbmd_questiontype>Essay</bbmd_questiontype> + <bbmd_is_from_cartridge>false</bbmd_is_from_cartridge> + <qmd_absolutescore>0.0,1.0</qmd_absolutescore> + <qmd_absolutescore_min>0.0</qmd_absolutescore_min> + <qmd_absolutescore_max>1.0</qmd_absolutescore_max> + <qmd_assessmenttype>Proprietary</qmd_assessmenttype> + <qmd_itemtype>Logical Identifier</qmd_itemtype> + <qmd_levelofdifficulty>School</qmd_levelofdifficulty> + <qmd_maximumscore>0.0</qmd_maximumscore> + <qmd_numberofitems>0</qmd_numberofitems> + <qmd_renderingtype>Proprietary</qmd_renderingtype> + <qmd_responsetype>Single</qmd_responsetype> + <qmd_scoretype>Absolute</qmd_scoretype> + <qmd_status>Normal</qmd_status> + <qmd_timelimit>0</qmd_timelimit> + <qmd_weighting>0.0</qmd_weighting> + <qmd_typeofsolution>Complete</qmd_typeofsolution> + </itemmetadata> + <presentation> + <flow class="Block"> + <flow class="QUESTION_BLOCK"> + <flow class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">How are you?</mat_formattedtext> + </mat_extension> + </material> + </flow> + <flow class="FILE_BLOCK"> + <material/> + </flow> + <flow class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow> + </flow> + <flow class="RESPONSE_BLOCK"> + <response_str ident="response" rcardinality="Single" rtiming="No"> + <render_fib charset="us-ascii" columns="127" encoding="UTF_8" fibtype="String" maxchars="0" maxnumber="0" minnumber="0" prompt="Box" rows="8"/> + </response_str> + </flow> + </flow> + </presentation> + <resprocessing scoremodel="SumOfScores"> + <outcomes> + <decvar defaultval="0.0" maxvalue="1.0" minvalue="0.0" varname="SCORE" vartype="Decimal"/> + </outcomes> + <respcondition title="correct"> + <conditionvar/> + <setvar action="Set" variablename="SCORE">SCORE.max</setvar> + <displayfeedback feedbacktype="Response" linkrefid="correct"/> + </respcondition> + <respcondition title="incorrect"> + <conditionvar> + <other/> + </conditionvar> + <setvar action="Set" variablename="SCORE">0.0</setvar> + <displayfeedback feedbacktype="Response" linkrefid="incorrect"/> + </respcondition> + </resprocessing> + <itemfeedback ident="correct" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="incorrect" view="All"> + <flow_mat class="Block"> + <flow_mat class="FORMATTED_TEXT_BLOCK"> + <material> + <mat_extension> + <mat_formattedtext type="HTML"></mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + <flow_mat class="FILE_BLOCK"> + <material/> + </flow_mat> + <flow_mat class="LINK_BLOCK"> + <material> + <mattext charset="us-ascii" texttype="text/plain" uri="" xml:space="default"/> + </material> + </flow_mat> + </flow_mat> + </itemfeedback> + <itemfeedback ident="solution" view="All"> + <solution feedbackstyle="Complete" view="All"> + <solutionmaterial> + <flow_mat class="Block"> + <material> + <mat_extension> + <mat_formattedtext type="HTML">Blackboard answer for essay questions will be imported as informations for graders.</mat_formattedtext> + </mat_extension> + </material> + </flow_mat> + </solutionmaterial> + </solution> + </itemfeedback> + </item> + </section> + </assessment> +</questestinterop> diff --git a/question/format/blackboard_six/version.php b/question/format/blackboard_six/version.php index c037b03cf0729..c413d77ef4218 100644 --- a/question/format/blackboard_six/version.php +++ b/question/format/blackboard_six/version.php @@ -15,10 +15,9 @@ // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** - * Version information for the calculated question type. + * Version information for the blackboard_six question import format. * - * @package qformat - * @subpackage blackboard_six + * @package qformat_blackboard_six * @copyright 2011 The Open University * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ diff --git a/question/format/examview/format.php b/question/format/examview/format.php index 7cea482d0343a..9cb9bb5c55309 100644 --- a/question/format/examview/format.php +++ b/question/format/examview/format.php @@ -61,28 +61,6 @@ public function mime_type() { return 'application/xml'; } - /** - * 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) { - - $html_code_list = array( - "'" => "'", - "’" => "'", - "“" => "\"", - "”" => "\"", - "–" => "-", - "—" => "-", - ); - $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; - } - /** * unxmlise reconstructs part of the xml data structure in order * to identify the actual data therein @@ -109,19 +87,6 @@ protected function unxmlise( $xml ) { return $text; } - protected function add_blank_combined_feedback($question) { - $question->correctfeedback['text'] = ''; - $question->correctfeedback['format'] = $question->questiontextformat; - $question->correctfeedback['files'] = array(); - $question->partiallycorrectfeedback['text'] = ''; - $question->partiallycorrectfeedback['format'] = $question->questiontextformat; - $question->partiallycorrectfeedback['files'] = array(); - $question->incorrectfeedback['text'] = ''; - $question->incorrectfeedback['format'] = $question->questiontextformat; - $question->incorrectfeedback['files'] = array(); - return $question; - } - public function parse_matching_groups($matching_groups) { if (empty($matching_groups)) { return; diff --git a/question/format/gift/format.php b/question/format/gift/format.php index 06bcebc564a17..1cb0e25157bfc 100644 --- a/question/format/gift/format.php +++ b/question/format/gift/format.php @@ -537,19 +537,6 @@ public function readquestion($lines) { } } - protected function add_blank_combined_feedback($question) { - $question->correctfeedback['text'] = ''; - $question->correctfeedback['format'] = $question->questiontextformat; - $question->correctfeedback['files'] = array(); - $question->partiallycorrectfeedback['text'] = ''; - $question->partiallycorrectfeedback['format'] = $question->questiontextformat; - $question->partiallycorrectfeedback['files'] = array(); - $question->incorrectfeedback['text'] = ''; - $question->incorrectfeedback['format'] = $question->questiontextformat; - $question->incorrectfeedback['files'] = array(); - return $question; - } - protected function repchar($text, $notused = 0) { // Escapes 'reserved' characters # = ~ {) : // Removes new lines