diff --git a/main/exercice/export/scorm/scorm_classes.php b/main/exercice/export/scorm/scorm_classes.php index 6d8b7ed99c..ff4b2f85bb 100755 --- a/main/exercice/export/scorm/scorm_classes.php +++ b/main/exercice/export/scorm/scorm_classes.php @@ -9,16 +9,16 @@ if ( count( get_included_files() ) == 1 ) die( '---' ); * @author Claro Team * @author Yannick Warnier */ -require_once('../../exercise.class.php'); -require_once('../../question.class.php'); -require_once('../../answer.class.php'); -require_once('../../unique_answer.class.php'); -require_once('../../multiple_answer.class.php'); -require_once('../../fill_blanks.class.php'); -require_once('../../freeanswer.class.php'); -require_once('../../hotspot.class.php'); -require_once('../../matching.class.php'); -require_once('../../hotspot.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/exercise.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/question.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/answer.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/unique_answer.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/multiple_answer.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/fill_blanks.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/freeanswer.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/hotspot.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/matching.class.php'); +require_once(api_get_path(SYS_CODE_PATH).'exercice/hotspot.class.php'); // answer types define('UNIQUE_ANSWER', 1); @@ -32,6 +32,15 @@ define('MATCHING', 4); define('FREE_ANSWER', 5); define('HOTSPOT', 6); +/** + * The ScormQuestion class is a gateway to getting the answers exported + * (the question is just an HTML text, while the answers are the most important). + * It is important to note that the SCORM export process is done in two parts. + * First, the HTML part (which is the presentation), and second the JavaScript + * part (the process). + * The two bits are separate to allow for a one-big-javascript and a one-big-html + * files to be built. Each export function thus returns an array of HTML+JS + */ class ScormQuestion extends Question { /** @@ -56,6 +65,12 @@ class ScormQuestion extends Question case MATCHING : $this->answer = new ScormAnswerMatching($this->id); break; + case FREE_ANSWER : + $this->answer = new ScormAnswerFree($this->id); + break; + case HOTSPOT: + $this->answer = new ScormAnswerHotspot($this->id); + break; default : $this->answer = null; break; @@ -66,89 +81,149 @@ class ScormQuestion extends Question function export() { - $out = $this->getQuestionHtml(); + //echo "
".print_r($this,1)."
"; + $html = $this->getQuestionHTML(); + $js = $this->getQuestionJS(); if( is_object($this->answer) ) { - $out .= $this->answer->export(); + list($js2,$html2) = $this->answer->export(); + $js .= $js2; + $html .= $html2; } - return $out; + return array($js,$html); } -} + function createAnswersForm($form) + { + return true; + } + function processAnswersCreation($form) + { + return true; + } + /** + * Returns an HTML-formatted question + */ + function getQuestionHtml() + { + $title = $this->selectTitle(); + $description = $this->selectDescription(); + $type = $this->selectType(); -class ScormAnswerMultipleChoice extends answerMultipleChoice + $cols = 0; + switch($type) + { + case MCUA: + case MCMA: + case TF: + case FIB: + case FREE_ANSWER: + case HOTSPOT: + $cols = 2; + break; + case MATCHING: + $cols = 3; + break; + } + $s='' . + '' . "\n" . + api_parse_tex($title). + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + ' '.api_parse_tex($description).'' . "\n" . + '' . "\n" . + '' . "\n"; + return $s; + } + /** + * Return the JavaScript code bound to the question + */ + function getQuestionJS() + { + //$id = $this->id; + $s = 'questions.push('.$this->id.');'."\n"; + return $s; + } +} + +/** + * This class handles the export to SCORM of a multiple choice question + * (be it single answer or multiple answers) + */ +class ScormAnswerMultipleChoice extends Answer { /** - * Return the XML flow for the possible answers. - * That's one , containing several - * - * @author Amand Tihon + * Return HTML code for possible answers */ - function export() - { - $out = - '' . "\n\n"; - - - if( $this->multipleAnswer ) + function export() + { + $html = ''; + $js = ''; + $html = '
' . "\n"; + $type = $this->getQuestionType(); + if ($type == MCMA) { - $questionTypeLang = get_lang('Multiple choice (Multiple answers)'); - - - foreach( $this->answerList as $answer ) + //$questionTypeLang = get_lang('MultipleChoiceMultipleAnswers'); + $id = 1; + $jstmp = ''; + $jstmpc = ''; + foreach( $this->answer as $i => $answer ) { - $identifier = 'multiple_'.$this->questionId.'_'.$answer['id']; - $scormIdentifier = 'scorm_'.getIdCounter(); - - $out .= + $identifier = 'question_'.$this->questionId.'_multiple_'.$i; + $html .= '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n\n"; + $jstmp .= $i.','; + if($this->correct[$i]) + { + $jstmpc .= $i.','; + } + $id++; } - + $js .= 'questions_answers['.$this->questionId.'] = new Array('.substr($jstmp,0,-1).');'."\n"; + $js .= 'questions_answers_correct['.$this->questionId.'] = new Array('.substr($jstmpc,0,-1).');'."\n"; + $js .= 'questions_types['.$this->questionId.'] = \'mcma\';'."\n"; } else { - $questionTypeLang = get_lang('Multiple choice (Unique answer)'); - $identifier = 'unique_'.$this->questionId.'_x'; - - foreach( $this->answerList as $answer ) + //$questionTypeLang = get_lang('MultipleChoiceUniqueAnswer'); + $id = 1; + foreach( $this->answer as $i => $answer ) { - $scormIdentifier = 'scorm_'.getIdCounter(); - - $out .= + $identifier = 'question_'.$this->questionId.'_unique_'.$i; + $html .= '' . "\n" . '' . "\n" . '' . "\n" . '' . "\n\n"; + $id++; } - } - - $out .= - '
' . "\n" - . 'response == 'TRUE'? 'checked="checked"':'') - . '/>' . "\n" + . '' . "\n" . '' . "\n" - . '' . "\n" + . '' . "\n" . '
' . "\n" - . 'response == 'TRUE'? 'checked="checked"':'') + . 'correct[$i] == 1? 'checked="checked"':'') . '/>' . "\n" . '' . "\n" - . '' . "\n" + . '' . "\n" . '
' . "\n" - . '

' . $questionTypeLang . '

' . "\n"; - - return $out; + $html .= '
' . "\n"; + return array($js,$html); } } -class ScormAnswerTrueFalse extends answerTrueFalse +/** + * This class handles the SCORM export of true/false questions + */ +class ScormAnswerTrueFalse extends Answer { /** * Return the XML flow for the possible answers. @@ -158,14 +233,16 @@ class ScormAnswerTrueFalse extends answerTrueFalse */ function export() { + $js = ''; + $html = ''; $identifier = 'unique_'.$this->questionId.'_x'; - $out = + $html .= '' . "\n\n"; $scormIdentifier = 'scorm_'.getIdCounter(); - $out .= + $html .= '' . "\n" . '
' . "\n" . '' . "\n" . '' . "\n" . '

' . get_lang('True/False') . '

' . "\n"; - return $out; + return array($js,$html); } } -class ScormAnswerFillInBlanks extends answerFillInBlanks +/** + * This class handles the SCORM export of fill-in-the-blanks questions + */ +class ScormAnswerFillInBlanks extends Answer { /** * Export the text with missing words. @@ -210,6 +290,8 @@ class ScormAnswerFillInBlanks extends answerFillInBlanks */ function export() { + $js = ''; + $html = ''; // get all enclosed answers foreach( $this->answerList as $answer ) { @@ -260,18 +342,18 @@ class ScormAnswerFillInBlanks extends answerFillInBlanks $displayedAnswer = str_replace( $blankList, $replacementList, claro_parse_user_text($this->answerText) ); // some javascript must be added for that kind of questions - $out = - '' . "\n" - . '' . "\n\n" + $js .= ''; + //'' . "\n" + $html .= '
' . "\n\n" . '' . "\n" . '
' . "\n" @@ -284,13 +366,16 @@ class ScormAnswerFillInBlanks extends answerFillInBlanks . '
' . "\n" . '

' . get_lang('Fill in blanks') . '

' . "\n"; - return $out; + return array($js,$html); } } -class ScormAnswerMatching extends answerMatching +/** + * This class handles the SCORM export of matching questions + */ +class ScormAnswerMatching extends Answer { /** * Export the question part as a matrix-choice, with only one possible answer per line. @@ -298,6 +383,8 @@ class ScormAnswerMatching extends answerMatching */ function export() { + $js = ''; + $html = ''; // prepare list of right proposition to allow // - easiest display // - easiest randomisation if needed one day @@ -307,7 +394,7 @@ class ScormAnswerMatching extends answerMatching // get max length of displayed array $arrayLength = max( count($this->leftList), count($this->rightList) ); - $out = '' . "\n\n"; + $html .= '
' . "\n\n"; $leftCpt = 1; $rightCpt = 'A'; @@ -347,7 +434,7 @@ class ScormAnswerMatching extends answerMatching $rightHtml = ' '; } - $out .= + $html .= '' . "\n" . '' . "\n" . '' . "\n" @@ -359,11 +446,211 @@ class ScormAnswerMatching extends answerMatching } - $out .= + $html .= '
' . "\n" . $leftHtml . "\n" . '' . "\n" . $centerHtml . "\n" . '
' . "\n" . '

' . get_lang('Matching') . '

' . "\n"; - return $out; + return array($js,$html); + } +} + +/** + * This class handles the SCORM export of free-answer questions + */ +class ScormAnswerFree extends Answer +{ + /** + * Export the text with missing words. + * + * As a side effect, it stores two lists in the class : + * the missing words and their respective weightings. + * + */ + function export() + { + $js = ''; + $html = ''; + // some javascript must be added for that kind of questions + $js .= ''; + //'' . "\n" + $html .= '' . "\n\n" + + . '' . "\n" + . '' . "\n" + . '' . "\n\n" + + . '
' . "\n" + + . $displayedAnswer . "\n" + + . '
' . "\n" + . '

' . get_lang('FreeAnswer') . '

' . "\n"; + return array($js,$html); + } -} + +} + +/** + * This class handles the SCORM export of hotpot questions + */ +class ScormAnswerHotspot extends Answer +{ + /** + * Returns the javascript code that goes with HotSpot exercises + * @return string The JavaScript code + */ + function get_js_header() + { + $header = " + + + + + "; + return $header; + } + /** + * Export the text with missing words. + * + * As a side effect, it stores two lists in the class : + * the missing words and their respective weightings. + * + */ + function export() + { + $js = ''; + $html = ''; + // some javascript must be added for that kind of questions + $js .= ''; + //'' . "\n" + $html .= '' . "\n\n" + + . '' . "\n" + . '' . "\n" + . '' . "\n\n" + + . '
' . "\n" + + . $displayedAnswer . "\n" + + . '
' . "\n" + . '

' . get_lang('HotSpot') . '

' . "\n"; + return array($js,$html); + + } + +} ?> \ No newline at end of file diff --git a/main/exercice/export/scorm/scorm_export.php b/main/exercice/export/scorm/scorm_export.php new file mode 100755 index 0000000000..369ce8a7db --- /dev/null +++ b/main/exercice/export/scorm/scorm_export.php @@ -0,0 +1,542 @@ + + * @author Yannick Warnier + */ + +require dirname(__FILE__) . '/scorm_classes.php'; + +/*-------------------------------------------------------- + Classes + --------------------------------------------------------*/ +// answer types +define(UNIQUE_ANSWER, 1); +define(MULTIPLE_ANSWER, 2); +define(FILL_IN_BLANKS, 3); +define(MATCHING, 4); +define(FREE_ANSWER, 5); +define(HOT_SPOT, 6); +define(HOT_SPOT_ORDER, 7); +/** + * A SCORM item. It corresponds to a single question. + * This class allows export from Dokeos SCORM 1.2 format of a single question. + * It is not usable as-is, but must be subclassed, to support different kinds of questions. + * + * Every start_*() and corresponding end_*(), as well as export_*() methods return a string. + * + * @warning Attached files are NOT exported. + */ +class ScormAssessmentItem +{ + var $question; + var $question_ident; + var $answer; + + /** + * Constructor. + * + * @param $question The Question object we want to export. + */ + function ScormAssessmentItem($question,$standalone=false) + { + $this->question = $question; + //$this->answer = new Answer($question->id); + $this->answer = $this->question->setAnswer(); + $this->questionIdent = "QST_" . $question->id ; + $this->standalone = $standalone; + //echo "
".print_r($this,1)."
"; + } + + /** + * Start the XML flow. + * + * This opens the block, with correct attributes. + * + */ + function start_page() + { + global $charset; + $head = $foot = ""; + + if( $this->standalone) + { + /* + $head = '' . "\n"; + */ + } + return $head.''. "\n"; + } + + /** + * End the XML flow, closing the tag. + * + */ + function end_page() + { + return ''; + } + /** + * Start document header + */ + function start_header() + { + return ''. "\n"; + } + + /** + * End document header + */ + function end_header() + { + return ''. "\n"; + } + /** + * Start the itemBody + * + */ + function start_js() + { + return ''. "\n"; + } + /** + * Start the itemBody + * + */ + function start_body() + { + return ''. "\n".'
'."\n"; + } + + /** + * End the itemBody part. + * + */ + function end_body() + { + return '
'."\n".''. "\n"; + } + + /** + * Export the question as a SCORM Item. + * + * This is a default behaviour, some classes may want to override this. + * + * @param $standalone: Boolean stating if it should be exported as a stand-alone question + * @return A string, the XML flow for an Item. + */ + function export($standalone = false) + { + list($js,$html) = $this->question->export(); + //list($js,$html) = $this->question->answer->export(); + $res = $this->start_page($standalone) + . $this->start_header() + . $this->start_js() + . $this->common_js() + . $js + . $this->end_js() + . $this->end_header() + . $this->start_body() + // .$this->answer->imsExportResponsesDeclaration($this->questionIdent) + // . $this->start_item_body() + // . $this->answer->scormExportResponses($this->questionIdent, $this->question->question, $this->question->description, $this->question->picture) + // .$question + .$html + . $this->end_body() + . $this->end_page(); + + return $res; + } +} + +/** + * This class represents an entire exercise to be exported in SCORM. + * It will be represented by a single
containing several . + * + * Some properties cannot be exported, as SCORM does not support them : + * - type (one page or multiple pages) + * - start_date and end_date + * - max_attempts + * - show_answer + * - anonymous_attempts + */ +class ScormSection +{ + var $exercise; + + /** + * Constructor. + * @param $exe The Exercise instance to export + * @author Amand Tihon + */ + function ScormSection($exe) + { + $this->exercise = $exe; + } + + function start_section() + { + $out = '
' . "\n"; + return $out; + } + + function end_section() + { + return "
\n"; + } + + function export_duration() + { + if ($max_time = $this->exercise->selectTimeLimit()) + { + // return exercise duration in ISO8601 format. + $minutes = floor($max_time / 60); + $seconds = $max_time % 60; + return 'PT' . $minutes . 'M' . $seconds . "S\n"; + } + else + { + return ''; + } + } + + /** + * Export the presentation (Exercise's description) + * @author Amand Tihon + */ + function export_presentation() + { + $out = "\n" + . " exercise->selectDescription() . "]]>\n" + . "\n"; + return $out; + } + + /** + * Export the ordering information. + * Either sequential, through all questions, or random, with a selected number of questions. + * @author Amand Tihon + */ + function export_ordering() + { + $out = ''; + if ($n = $this->exercise->getShuffle()) { + $out.= "" + . " \n" + . " " . $n . "\n" + . " \n" + . ' ' + . "\n\n"; + } + else + { + $out.= '' . "\n" + . " \n" + . "\n"; + } + + return $out; + } + + /** + * Export the questions, as a succession of + * @author Amand Tihon + */ + function export_questions() + { + $out = ""; + foreach ($this->exercise->selectQuestionList() as $q) + { + $out .= export_question($q, false); + } + return $out; + } + + /** + * Export the exercise in SCORM. + * + * @param bool $standalone Wether it should include XML tag and DTD line. + * @return a string containing the XML flow + * @author Amand Tihon + */ + function export($standalone) + { + global $charset; + + $head = $foot = ""; + if ($standalone) { + $head = '' . "\n" + . '' . "\n" + . "\n"; + $foot = "\n"; + } + $out = $head + . $this->start_section() + . $this->export_duration() + . $this->export_presentation() + . $this->export_ordering() + . $this->export_questions() + . $this->end_section() + . $foot; + + return $out; + } +} + +/* + Some quick notes on identifiers generation. + The IMS format requires some blocks, like items, responses, feedbacks, to be uniquely + identified. + The unicity is mandatory in a single XML, of course, but it's prefered that the identifier stays + coherent for an entire site. + + Here's the method used to generate those identifiers. + Question identifier :: "QST_" + + "_" + + Response identifier :: + "_A_" + + Condition identifier :: + "_C_" + + Feedback identifier :: + "_F_" + +*/ +/** + * A SCORM item. It corresponds to a single question. + * This class allows export from Dokeos to SCORM 1.2 format. + * It is not usable as-is, but must be subclassed, to support different kinds of questions. + * + * Every start_*() and corresponding end_*(), as well as export_*() methods return a string. + * + * @warning Attached files are NOT exported. + */ +class ScormItem +{ + var $question; + var $question_ident; + var $answer; + + /** + * Constructor. + * + * @param $question The Question object we want to export. + * @author Anamd Tihon + */ + function ScormItem($question) + { + $this->question = $question; + $this->answer = $question->answer; + $this->questionIdent = "QST_" . $question->selectId() ; + } + + /** + * Start the XML flow. + * + * This opens the block, with correct attributes. + * + * @author Amand Tihon + */ + function start_item() + { + return '' . "\n"; + } + + /** + * End the XML flow, closing the tag. + * + * @author Amand Tihon + */ + function end_item() + { + return "\n"; + } + + /** + * Create the opening, with the question itself. + * + * This means it opens the but doesn't close it, as this is the role of end_presentation(). + * Inbetween, the export_responses from the subclass should have been called. + * + * @author Amand Tihon + */ + function start_presentation() + { + return '' . "\n" + . 'question->selectDescription() . "]]>\n"; + } + + /** + * End the part, opened by export_header. + * + * @author Amand Tihon + */ + function end_presentation() + { + return "\n"; + } + + /** + * Start the response processing, and declare the default variable, SCORE, at 0 in the outcomes. + * + * @author Amand Tihon + */ + function start_processing() + { + return '' . "\n"; + } + + /** + * End the response processing part. + * + * @author Amand Tihon + */ + function end_processing() + { + return "\n"; + } + + + /** + * Export the question as a SCORM Item. + * + * This is a default behaviour, some classes may want to override this. + * + * @param $standalone: Boolean stating if it should be exported as a stand-alone question + * @return A string, the XML flow for an Item. + * @author Amand Tihon + */ + function export($standalone = False) + { + global $charset; + $head = $foot = ""; + + if( $standalone ) + { + $head = '' . "\n" + . '' . "\n" + . "\n"; + $foot = "\n"; + } + + return $head + . $this->start_item() + . $this->start_presentation() + . $this->answer->imsExportResponses($this->questionIdent) + . $this->end_presentation() + . $this->start_processing() + . $this->answer->imsExportProcessing($this->questionIdent) + . $this->end_processing() + . $this->answer->imsExportFeedback($this->questionIdent) + . $this->end_item() + . $foot; + } +} + + +/*-------------------------------------------------------- + Functions + --------------------------------------------------------*/ + +/** + * Send a complete exercise in SCORM format, from its ID + * + * @param int $exerciseId The exercise to exporte + * @param boolean $standalone Wether it should include XML tag and DTD line. + * @return The XML as a string, or an empty string if there's no exercise with given ID. + */ +function export_exercise($exerciseId, $standalone=true) +{ + $exercise = new Exercise(); + if (! $exercise->read($exerciseId)) + { + return ''; + } + $ims = new ScormSection($exercise); + $xml = $ims->export($standalone); + return $xml; +} + +/** + * Returns the HTML + JS flow corresponding to one question + * + * @param int The question ID + * @param bool standalone (ie including XML tag, DTD declaration, etc) + */ +function export_question($questionId, $standalone=true) +{ + $question = new ScormQuestion(); + $qst = $question->read($questionId); + if( !$qst ) + { + return ''; + } + $question->id = $qst->id; + $question->type = $qst->type; + $question->question = $qst->question; + $question->description = $qst->description; + $question->weighting=$qst->weighting; + $question->position=$qst->position; + $question->picture=$qst->picture; + $assessmentItem = new ScormAssessmentItem($question); + //echo "
".print_r($scorm,1)."
";exit; + return $assessmentItem->export($standalone); +} +?> \ No newline at end of file