Fix Chamilo quiz to scorm export see BT#14644

Requires composer update to install new serializer.
pull/2606/head
Julio Montoya 7 years ago
parent 2d4b5b8858
commit 9ce5eb789a
  1. 1
      composer.json
  2. 2
      main/exercise/answer.class.php
  3. 100
      main/exercise/export/scorm/ScormAnswerMatching.php
  4. 119
      main/exercise/export/scorm/ScormAnswerMultipleChoice.php
  5. 56
      main/exercise/export/scorm/ScormAnswerTrueFalse.php
  6. 123
      main/exercise/export/scorm/ScormAssessmentItem.php
  7. 137
      main/exercise/export/scorm/ScormExercise.php
  8. 14
      main/exercise/export/scorm/ScormQuestion.php
  9. 25
      main/exercise/export/scorm/common.js
  10. 274
      main/exercise/export/scorm/scorm_classes.php
  11. 2
      main/lp/aicc_api.php
  12. 13
      main/lp/learnpath.class.php
  13. 493
      main/lp/packaging/assets/api_wrapper.js
  14. 47
      main/lp/scorm.class.php
  15. 2
      src/Chamilo/CourseBundle/Component/CourseCopy/CourseBuilder.php
  16. 10
      src/Chamilo/CourseBundle/Component/CourseCopy/CourseRestorer.php

@ -51,6 +51,7 @@
"symfony/validator": "~2.8",
"symfony/filesystem": "~2.8",
"symfony/security": "~2.8",
"symfony/serializer": "~3.0",
"doctrine/data-fixtures": "~1.0@dev",
"knplabs/gaufrette": "~0.3",
"gedmo/doctrine-extensions": "~2.3",

@ -483,7 +483,7 @@ class Answer
}
$row = Database::fetch_array($res);
return $row['type'];
return (int) $row['type'];
}
/**

@ -0,0 +1,100 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This class handles the SCORM export of matching questions.
*
* @package chamilo.exercise.scorm
*/
class ScormAnswerMatching extends Answer
{
/**
* Export the question part as a matrix-choice, with only one possible answer per line.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function export()
{
$js = '';
// prepare list of right proposition to allow
// - easiest display
// - easiest randomisation if needed one day
// (here I use array_values to change array keys from $code1 $code2 ... to 0 1 ...)
// get max length of displayed array
$nbrAnswers = $this->selectNbrAnswers();
$counter = 1;
$questionId = $this->questionJSId;
$jstmpw = 'questions_answers_ponderation['.$questionId.'] = new Array();'."\n";
$jstmpw .= 'questions_answers_ponderation['.$questionId.'][0] = 0;'."\n";
// Options (A, B, C, ...) that will be put into the list-box
$options = [];
$letter = 'A';
for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
$answerCorrect = $this->isCorrect($answerId);
$answer = $this->selectAnswer($answerId);
$realAnswerId = $this->selectAutoId($answerId);
if (!$answerCorrect) {
$options[$realAnswerId]['Lettre'] = $letter;
// answers that will be shown at the right side
$options[$realAnswerId]['Reponse'] = $answer;
$letter++;
}
}
$html = [];
$jstmp = '';
$jstmpc = '';
// Answers
for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
$identifier = 'question_'.$questionId.'_matching_';
$answer = $this->selectAnswer($answerId);
$answerCorrect = $this->isCorrect($answerId);
$weight = $this->selectWeighting($answerId);
$jstmp .= $answerId.',';
if ($answerCorrect) {
$html[] = '<tr class="option_row">';
//$html[] = '<td width="40%" valign="top"><b>'.$counter.'</b>.&nbsp;'.$answer."</td>";
$html[] = '<td width="40%" valign="top">&nbsp;'.$answer."</td>";
$html[] = '<td width="20%" align="center">&nbsp;&nbsp;';
$html[] = '<select name="'.$identifier.$counter.'" id="'.$identifier.$counter.'">';
$html[] = ' <option value="0">--</option>';
// fills the list-box
foreach ($options as $key => $val) {
$html[] = '<option value="'.$key.'">'.$val['Lettre'].'</option>';
}
$html[] = '</select>&nbsp;&nbsp;</td>';
$html[] = '<td width="40%" valign="top">';
foreach ($options as $key => $val) {
$html[] = '<b>'.$val['Lettre'].'.</b> '.$val['Reponse'].'<br />';
}
$html[] = '</td></tr>';
$jstmpc .= '['.$answerCorrect.','.$counter.'],';
$myWeight = explode('@', $weight);
if (count($myWeight) == 2) {
$weight = $myWeight[0];
} else {
$weight = $myWeight[0];
}
$jstmpw .= 'questions_answers_ponderation['.$questionId.']['.$counter.'] = '.$weight.";\n";
$counter++;
}
}
$js .= 'questions_answers['.$questionId.'] = new Array('.substr($jstmp, 0, -1).');'."\n";
$js .= 'questions_answers_correct['.$questionId.'] = new Array('.substr($jstmpc, 0, -1).');'."\n";
$js .= 'questions_types['.$questionId.'] = \'matching\';'."\n";
$js .= $jstmpw;
$htmlResult = '<tr><td colspan="2"><table id="question_'.$questionId.'" width="100%">';
$htmlResult .= implode("\n", $html);
$htmlResult .= '</table></td></tr>'."\n";
return [$js, $htmlResult];
}
}

@ -0,0 +1,119 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This class handles the export to SCORM of a multiple choice question
* (be it single answer or multiple answers).
*
* @package chamilo.exercise.scorm
*/
class ScormAnswerMultipleChoice extends Answer
{
/**
* Return HTML code for possible answers.
*/
public function export()
{
$js = [];
$type = $this->getQuestionType();
$questionId = $this->questionJSId;
$jstmpw = 'questions_answers_ponderation['.$questionId.'] = new Array();';
$jstmpw .= 'questions_answers_ponderation['.$questionId.'][0] = 0;';
$jstmpw .= 'questions_answers_correct['.$questionId.'] = new Array();';
$html = [];
//not sure if we are going to export also the MULTIPLE_ANSWER_COMBINATION to SCORM
//if ($type == MCMA || $type == MULTIPLE_ANSWER_COMBINATION ) {
if ($type == MCMA) {
$id = 1;
$jstmp = '';
$jstmpc = '';
foreach ($this->answer as $i => $answer) {
$identifier = 'question_'.$questionId.'_multiple_'.$i;
$html[] =
'<tr>
<td align="center" width="5%">
<input name="'.$identifier.'" id="'.$identifier.'" value="'.$i.'" type="checkbox" />
</td>
<td width="95%">
<label for="'.$identifier.'">'.Security::remove_XSS($this->answer[$i]).'</label>
</td>
</tr>';
$jstmp .= $i.',';
if ($this->correct[$i]) {
$jstmpc .= $i.',';
}
$jstmpw .= 'questions_answers_ponderation['.$questionId.']['.$i.'] = '.$this->weighting[$i].";";
$jstmpw .= 'questions_answers_correct['.$questionId.']['.$i.'] = '.$this->correct[$i].';';
$id++;
}
$js[] = 'questions_answers['.$questionId.'] = new Array('.substr($jstmp, 0, -1).');'."\n";
$js[] = 'questions_types['.$questionId.'] = \'mcma\';'."\n";
$js[] = $jstmpw;
} elseif ($type == MULTIPLE_ANSWER_COMBINATION) {
$js = '';
$id = 1;
$jstmp = '';
$jstmpc = '';
foreach ($this->answer as $i => $answer) {
$identifier = 'question_'.$questionId.'_exact_'.$i;
$html[] =
'<tr>
<td align="center" width="5%">
<input name="'.$identifier.'" id="'.$identifier.'" value="'.$i.'" type="checkbox" />
</td>
<td width="95%">
<label for="'.$identifier.'">'.Security::remove_XSS($this->answer[$i]).'</label>
</td>
</tr>';
$jstmp .= $i.',';
if ($this->correct[$i]) {
$jstmpc .= $i.',';
}
$jstmpw .= 'questions_answers_ponderation['.$questionId.']['.$i.'] = '.$this->weighting[$i].';';
$jstmpw .= 'questions_answers_correct['.$questionId.']['.$i.'] = '.$this->correct[$i].';';
$id++;
}
$js[] = 'questions_answers['.$questionId.'] = new Array('.substr($jstmp, 0, -1).');';
$js[] = 'questions_types['.$questionId.'] = "exact";';
$js[] = $jstmpw;
} else {
$id = 1;
$jstmp = '';
$jstmpc = '';
foreach ($this->answer as $i => $answer) {
$identifier = 'question_'.$questionId.'_unique_'.$i;
$identifier_name = 'question_'.$questionId.'_unique_answer';
$html[] =
'<tr>
<td align="center" width="5%">
<input name="'.$identifier_name.'" id="'.$identifier.'" value="'.$i.'" type="checkbox"/>
</td>
<td width="95%">
<label for="'.$identifier.'">'.Security::remove_XSS($this->answer[$i]).'</label>
</td>
</tr>';
$jstmp .= $i.',';
if ($this->correct[$i]) {
$jstmpc .= $i;
}
$jstmpw .= 'questions_answers_ponderation['.$questionId.']['.$i.'] = '.$this->weighting[$i].';';
$jstmpw .= 'questions_answers_correct['.$questionId.']['.$i.'] = '.$this->correct[$i].';';
$id++;
}
$js[] = 'questions_answers['.$questionId.'] = new Array('.substr($jstmp, 0, -1).');';
$js[] = 'questions_types['.$questionId.'] = \'mcua\';';
$js[] = $jstmpw;
}
$htmlResult = '<tr><td colspan="2"><table id="question_'.$questionId.'" width="100%">';
$htmlResult .= implode("\n", $html);
$htmlResult .= '</table></td></tr>';
$js = implode("\n", $js);
return [$js, $htmlResult];
}
}

@ -0,0 +1,56 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This class handles the SCORM export of true/false questions.
*
* @package chamilo.exercise.scorm
*/
class ScormAnswerTrueFalse extends Answer
{
/**
* Return the XML flow for the possible answers.
* That's one <response_lid>, containing several <flow_label>.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function export()
{
$js = '';
$html = '<tr><td colspan="2"><table width="100%">';
$identifier = 'question_'.$this->questionJSId.'_tf';
$identifier_true = $identifier.'_true';
$identifier_false = $identifier.'_false';
$html .=
'<tr>
<td align="center" width="5%">
<input name="'.$identifier_true.'" id="'.$identifier_true.'" value="'.$this->trueGrade.'" type="radio" />
</td>
<td width="95%">
<label for="'.$identifier_true.'">'.get_lang('True').'</label>
</td>
</tr>';
$html .=
'<tr>
<td align="center" width="5%">
<input name="'.$identifier_false.'" id="'.$identifier_false.'" value="'.$this->falseGrade.'" type="radio" />
</td>
<td width="95%">
<label for="'.$identifier_false.'">'.get_lang('False').'</label>
</td>
</tr></table></td></tr>';
$js .= 'questions_answers['.$this->questionJSId.'] = new Array(\'true\',\'false\');'."\n";
$js .= 'questions_types['.$this->questionJSId.'] = \'tf\';'."\n";
if ($this->response === 'TRUE') {
$js .= 'questions_answers_correct['.$this->questionJSId.'] = new Array(\'true\');'."\n";
} else {
$js .= 'questions_answers_correct['.$this->questionJSId.'] = new Array(\'false\');'."\n";
}
$jstmpw = 'questions_answers_ponderation['.$this->questionJSId.'] = new Array();'."\n";
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.'][0] = 0;'."\n";
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.'][1] = '.$this->weighting[1].";\n";
$js .= $jstmpw;
return [$js, $html];
}
}

@ -3,7 +3,7 @@
/**
* A SCORM item. It corresponds to a single question.
* This class allows export from Dokeos SCORM 1.2 format of a single question.
* This class allows export from Chamilo 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.
@ -17,19 +17,17 @@ class ScormAssessmentItem
public $question;
public $question_ident;
public $answer;
public $standalone;
/**
* Constructor.
*
* @param ScormQuestion $question the Question object we want to export
*/
public function __construct($question, $standalone = false)
public function __construct($question)
{
$this->question = $question;
$this->question->setAnswer();
$this->questionIdent = "QST_".$question->id;
$this->standalone = $standalone;
$this->questionIdent = 'QST_'.$question->id;
}
/**
@ -40,11 +38,11 @@ class ScormAssessmentItem
public function start_page()
{
$head = '';
if ($this->standalone) {
/*if ($this->standalone) {
$charset = 'UTF-8';
$head = '<?xml version="1.0" encoding="'.$charset.'" standalone="no"?>';
$head .= '<html>';
}
}*/
return $head;
}
@ -54,9 +52,9 @@ class ScormAssessmentItem
*/
public function end_page()
{
if ($this->standalone) {
/*if ($this->standalone) {
return '</html>';
}
}*/
return '';
}
@ -66,9 +64,9 @@ class ScormAssessmentItem
*/
public function start_header()
{
if ($this->standalone) {
/*if ($this->standalone) {
return '<head>';
}
}*/
return '';
}
@ -79,16 +77,16 @@ class ScormAssessmentItem
public function css()
{
$css = '';
if ($this->standalone) {
$css = '<style type="text/css" media="screen, projection">';
$css .= '/*<![CDATA[*/'."\n";
$css .= '/*]]>*/'."\n";
$css .= '</style>'."\n";
$css .= '<style type="text/css" media="print">';
$css .= '/*<![CDATA[*/'."\n";
$css .= '/*]]>*/'."\n";
$css .= '</style>';
}
// if ($this->standalone) {
// $css = '<style type="text/css" media="screen, projection">';
// $css .= '/*<![CDATA[*/'."\n";
// $css .= '/*]]>*/'."\n";
// $css .= '</style>'."\n";
// $css .= '<style type="text/css" media="print">';
// $css .= '/*<![CDATA[*/'."\n";
// $css .= '/*]]>*/'."\n";
// $css .= '</style>';
// }
return $css;
}
@ -98,9 +96,9 @@ class ScormAssessmentItem
*/
public function end_header()
{
if ($this->standalone) {
return '</head>';
}
// if ($this->standalone) {
// return '</head>';
// }
return '';
}
@ -111,75 +109,21 @@ class ScormAssessmentItem
public function start_js()
{
$js = '<script type="text/javascript" src="assets/api_wrapper.js"></script>';
if ($this->standalone) {
return '<script>';
}
// if ($this->standalone) {
// return '<script>';
// }
return $js;
}
/**
* Common JS functions.
*/
public function common_js()
{
$js = 'var questions = new Array();';
$js .= 'var questions_answers = new Array();';
$js .= 'var questions_answers_correct = new Array();';
$js .= 'var questions_types = new Array();';
$js .= "\n".
'/**
* Assigns any event handler to any element
* @param object Element on which the event is added
* @param string Name of event
* @param string Function to trigger on event
* @param boolean Capture the event and prevent
*/
function addEvent(elm, evType, fn, useCapture)
{ //by Scott Andrew
if(elm.addEventListener){
elm.addEventListener(evType, fn, useCapture);
return true;
} else if(elm.attachEvent) {
var r = elm.attachEvent(\'on\' + evType, fn);
return r;
} else {
elm[\'on\' + evType] = fn;
}
}
/**
* Adds the event listener
*/
function addListeners(e) {
loadPage();
/*
var my_form = document.getElementById(\'dokeos_scorm_form\');
addEvent(my_form,\'submit\',checkAnswers,false);
*/
var my_button = document.getElementById(\'dokeos_scorm_submit\');
addEvent(my_button,\'click\',doQuit,false);
//addEvent(my_button,\'click\',checkAnswers,false);
//addEvent(my_button,\'change\',checkAnswers,false);
addEvent(window,\'unload\',unloadPage,false);
}'."\n\n";
$js .= '';
$js .= 'addEvent(window,\'load\',addListeners,false);'."\n";
if ($this->standalone) {
return $js."\n";
}
return '';
}
/**
* End the itemBody part.
*/
public function end_js()
{
if ($this->standalone) {
/*if ($this->standalone) {
return '</script>';
}
}*/
return '';
}
@ -189,9 +133,9 @@ class ScormAssessmentItem
*/
public function start_body()
{
if ($this->standalone) {
/*if ($this->standalone) {
return '<body><form id="dokeos_scorm_form" method="post" action="">';
}
}*/
return '';
}
@ -201,9 +145,9 @@ class ScormAssessmentItem
*/
public function end_body()
{
if ($this->standalone) {
/*if ($this->standalone) {
return '<br /><input class="btn" type="button" id="dokeos_scorm_submit" name="dokeos_scorm_submit" value="OK" /></form></body>';
}
}*/
return '';
}
@ -217,7 +161,7 @@ class ScormAssessmentItem
public function export()
{
list($js, $html) = $this->question->export();
if ($this->standalone) {
/*if ($this->standalone) {
$res = $this->start_page()
.$this->start_header()
.$this->css()
@ -234,6 +178,7 @@ class ScormAssessmentItem
return $res;
} else {
return [$js, $html];
}
}*/
return [$js, $html];
}
}

@ -1,6 +1,11 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CourseBundle\Entity\CQuiz;
use Symfony\Component\Serializer\Serializer;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
/**
* This class represents an entire exercise to be exported in SCORM.
* It will be represented by a single <section> containing several <item>.
@ -11,42 +16,26 @@
* - max_attempts
* - show_answer
* - anonymous_attempts
* @author Julio Montoya
* @author Amand Tihon <amand@alrj.org>
*
* @package chamilo.exercise.scorm
*/
class ScormSection
class ScormExercise
{
public $exercise;
public $standalone;
/**
* Constructor.
*
* @param Exercise $exe The Exercise instance to export
* ScormExercise constructor.
*
* @author Amand Tihon <amand@alrj.org>
* @param Exercise $exe
* @param bool $standalone
*/
public function __construct($exe)
public function __construct($exe, $standalone)
{
$this->exercise = $exe;
}
/**
* Send a complete exercise in SCORM format, from its ID.
*
* @param Exercise $exercise The exercise to export
* @param bool $standalone wether it should include XML tag and DTD line
*
* @return string XML as a string, or an empty string if there's no exercise with given ID
*/
public static function exportExerciseToScorm(
Exercise $exercise,
$standalone = true
) {
$ims = new ScormSection($exercise);
$xml = $ims->export($standalone);
return $xml;
$this->standalone = $standalone;
}
/**
@ -54,7 +43,7 @@ class ScormSection
*
* This opens the <item> block, with correct attributes.
*/
public function start_page()
public function startPage()
{
$charset = 'UTF-8';
$head = '<?xml version="1.0" encoding="'.$charset.'" standalone="no"?><html>';
@ -83,57 +72,7 @@ class ScormSection
*/
public function common_js()
{
$js = file_get_contents('../inc/lib/javascript/hotspot/js/hotspot.js');
$js .= 'var questions = new Array();'."\n";
$js .= 'var questions_answers = new Array();'."\n";
$js .= 'var questions_answers_correct = new Array();'."\n";
$js .= 'var questions_types = new Array();'."\n";
$js .= "\n".
'/**
* Assigns any event handler to any element
* @param object Element on which the event is added
* @param string Name of event
* @param string Function to trigger on event
* @param boolean Capture the event and prevent
*/
function addEvent(elm, evType, fn, useCapture)
{ //by Scott Andrew
if(elm.addEventListener){
elm.addEventListener(evType, fn, useCapture);
return true;
} else if(elm.attachEvent) {
var r = elm.attachEvent(\'on\' + evType, fn);
return r;
} else {
elm[\'on\' + evType] = fn;
}
}
/**
* Adds the event listener
*/
function addListeners(e) {
loadPage();
/*
var my_form = document.getElementById(\'dokeos_scorm_form\');
addEvent(my_form,\'submit\',checkAnswers,false);
*/
var my_button = document.getElementById(\'dokeos_scorm_submit\');
addEvent(my_button,\'click\',doQuit,false);
addEvent(my_button,\'click\',disableButton,false);
//addEvent(my_button,\'click\',checkAnswers,false);
//addEvent(my_button,\'change\',checkAnswers,false);
addEvent(window,\'unload\',unloadPage,false);
}
/** Disables the submit button on SCORM result submission **/
function disableButton() {
var mybtn = document.getElementById(\'dokeos_scorm_submit\');
mybtn.setAttribute(\'disabled\',\'disabled\');
}
'."\n";
$js .= '';
$js .= 'addEvent(window,\'load\',addListeners,false);'."\n";
$js = file_get_contents(api_get_path(SYS_CODE_PATH).'exercise/export/scorm/common.js');
return $js."\n";
}
@ -153,7 +92,7 @@ class ScormSection
{
return '<body>'.
'<h1>'.$this->exercise->selectTitle().'</h1><p>'.$this->exercise->selectDescription()."</p>".
'<form id="dokeos_scorm_form" method="post" action="">'.
'<form id="chamilo_scorm_form" method="post" action="">'.
'<table width="100%">';
}
@ -162,7 +101,14 @@ class ScormSection
*/
public function end_body()
{
return '</table><br /><input class="btn btn-primary" type="button" id="dokeos_scorm_submit" name="dokeos_scorm_submit" value="OK" /></form></body>';
$button = '<input
id="chamilo_scorm_submit"
class="btn btn-primary"
type="button"
name="chamilo_scorm_submit"
value="OK" />';
return '</table><br />'.$button.'</form></body>';
}
/**
@ -178,14 +124,14 @@ class ScormSection
{
global $charset;
$head = '';
/*$head = '';
if ($this->standalone) {
$head = '<?xml version = "1.0" encoding = "'.$charset.'" standalone = "no"?>'."\n"
.'<!DOCTYPE questestinterop SYSTEM "ims_qtiasiv2p1.dtd">'."\n";
}
}*/
list($js, $html) = $this->export_questions();
$res = $this->start_page()
list($js, $html) = $this->exportQuestions();
$res = $this->startPage()
.$this->start_header()
.$this->css()
.$this->globalAssets()
@ -207,15 +153,30 @@ class ScormSection
*
* @author Amand Tihon <amand@alrj.org>
*/
public function export_questions()
public function exportQuestions()
{
$js = $html = '';
$js_id = 0;
$encoders = array(new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$em = Database::getManager();
// Export cquiz
/** @var CQuiz $exercise */
$exercise = $em->find('ChamiloCourseBundle:CQuiz', $this->exercise->iId);
$exercise->setDescription('');
$exercise->setTextWhenFinished('');
$serializer = new Serializer($normalizers, $encoders);
$jsonContent = $serializer->serialize($exercise, 'json');
$js .= "var exerciseInfo = JSON.parse('".$jsonContent."');\n";
$counter = 0;
foreach ($this->exercise->selectQuestionList() as $q) {
list($jstmp, $htmltmp) = ScormQuestion::export_question($q, false, $js_id);
list($jstmp, $htmltmp) = ScormQuestion::exportQuestion($q, $counter);
$js .= $jstmp."\n";
$html .= $htmltmp."\n";
$js_id++;
$counter++;
}
return [$js, $html];
@ -250,8 +211,8 @@ class ScormSection
*/
private function globalAssets()
{
$assets = '<script type="text/javascript" src="assets/jquery/jquery.min.js"></script>';
$assets .= '<script type="text/javascript" src="assets/api_wrapper.js"></script>';
$assets = '<script type="text/javascript" src="assets/jquery/jquery.min.js"></script>'."\n";
$assets .= '<script type="text/javascript" src="assets/api_wrapper.js"></script>'."\n";
$assets .= '<link href="assets/bootstrap/bootstrap.min.css" rel="stylesheet" media="screen" type="text/css" />';
return $assets;

@ -32,9 +32,8 @@ class ScormQuestion extends Question
*
* @return string|array
*/
public static function export_question(
public static function exportQuestion(
$questionId,
$standalone = true,
$js_id
) {
$question = new ScormQuestion();
@ -50,7 +49,7 @@ class ScormQuestion extends Question
$question->weighting = $qst->weighting;
$question->position = $qst->position;
$question->picture = $qst->picture;
$assessmentItem = new ScormAssessmentItem($question, $standalone);
$assessmentItem = new ScormAssessmentItem($question);
return $assessmentItem->export();
}
@ -188,7 +187,14 @@ class ScormQuestion extends Question
public function getQuestionJS()
{
$weight = $this->selectWeighting();
$js = 'questions.push('.$this->js_id.');'."\n";
$js = '
questions.push('.$this->js_id.');
$(document).ready(function() {
if (exerciseInfo.randomAnswers == true) {
$("#question_'.$this->js_id.'").shuffleRows();
}
});';
$js .= "\n";
switch ($this->type) {
case ORAL_EXPRESSION:

@ -0,0 +1,25 @@
var questions = new Array();
var questions_answers = new Array();
var questions_answers_correct = new Array();
var questions_types = new Array();
var questions_score_max = new Array();
var questions_answers_ponderation = new Array();
/**
* Adds the event listener
*/
function addListeners(e) {
loadPage();
var myButton = document.getElementById('chamilo_scorm_submit');
addEvent(myButton, 'click', doQuit, false);
addEvent(myButton, 'click', disableButton, false);
addEvent(window, 'unload', unloadPage, false);
}
/** Disables the submit button on SCORM result submission **/
function disableButton() {
var mybtn = document.getElementById('chamilo_scorm_submit');
mybtn.setAttribute('disabled', 'disabled');
}
addEvent(window,'load', addListeners, false);

@ -1,171 +1,6 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This class handles the export to SCORM of a multiple choice question
* (be it single answer or multiple answers).
*
* @package chamilo.exercise.scorm
*/
class ScormAnswerMultipleChoice extends Answer
{
/**
* Return HTML code for possible answers.
*/
public function export()
{
$js = '';
$html = '<tr><td colspan="2"><table width="100%">';
$type = $this->getQuestionType();
$jstmpw = 'questions_answers_ponderation['.$this->questionJSId.'] = new Array();';
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.'][0] = 0;';
$jstmpw .= 'questions_answers_correct['.$this->questionJSId.'] = new Array();';
//not sure if we are going to export also the MULTIPLE_ANSWER_COMBINATION to SCORM
//if ($type == MCMA || $type == MULTIPLE_ANSWER_COMBINATION ) {
if ($type == MCMA) {
$id = 1;
$jstmp = '';
$jstmpc = '';
foreach ($this->answer as $i => $answer) {
$identifier = 'question_'.$this->questionJSId.'_multiple_'.$i;
$html .=
'<tr>
<td align="center" width="5%">
<input name="'.$identifier.'" id="'.$identifier.'" value="'.$i.'" type="checkbox" />
</td>
<td width="95%">
<label for="'.$identifier.'">'.Security::remove_XSS($this->answer[$i]).'</label>
</td>
</tr>';
$jstmp .= $i.',';
if ($this->correct[$i]) {
$jstmpc .= $i.',';
}
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.']['.$i.'] = '.$this->weighting[$i].";";
$jstmpw .= 'questions_answers_correct['.$this->questionJSId.']['.$i.'] = '.$this->correct[$i].';';
$id++;
}
$js .= 'questions_answers['.$this->questionJSId.'] = new Array('.substr($jstmp, 0, -1).');'."\n";
$js .= 'questions_types['.$this->questionJSId.'] = \'mcma\';'."\n";
$js .= $jstmpw;
} elseif ($type == MULTIPLE_ANSWER_COMBINATION) {
$js = '';
$id = 1;
$jstmp = '';
$jstmpc = '';
foreach ($this->answer as $i => $answer) {
$identifier = 'question_'.$this->questionJSId.'_exact_'.$i;
$html .=
'<tr>
<td align="center" width="5%">
<input name="'.$identifier.'" id="'.$identifier.'" value="'.$i.'" type="checkbox" />
</td>
<td width="95%">
<label for="'.$identifier.'">'.Security::remove_XSS($this->answer[$i]).'</label>
</td>
</tr>';
$jstmp .= $i.',';
if ($this->correct[$i]) {
$jstmpc .= $i.',';
}
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.']['.$i.'] = '.$this->weighting[$i].";";
$jstmpw .= 'questions_answers_correct['.$this->questionJSId.']['.$i.'] = '.$this->correct[$i].";";
$id++;
}
$js .= 'questions_answers['.$this->questionJSId.'] = new Array('.substr($jstmp, 0, -1).');';
$js .= 'questions_types['.$this->questionJSId.'] = "exact";';
$js .= $jstmpw;
} else {
$id = 1;
$jstmp = '';
$jstmpc = '';
foreach ($this->answer as $i => $answer) {
$identifier = 'question_'.$this->questionJSId.'_unique_'.$i;
$identifier_name = 'question_'.$this->questionJSId.'_unique_answer';
$html .=
'<tr>
<td align="center" width="5%">
<input name="'.$identifier_name.'" id="'.$identifier.'" value="'.$i.'" type="checkbox"/>
</td>
<td width="95%">
<label for="'.$identifier.'">'.Security::remove_XSS($this->answer[$i]).'</label>
</td>
</tr>';
$jstmp .= $i.',';
if ($this->correct[$i]) {
$jstmpc .= $i;
}
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.']['.$i.'] = '.$this->weighting[$i].";";
$jstmpw .= 'questions_answers_correct['.$this->questionJSId.']['.$i.'] = '.$this->correct[$i].';';
$id++;
}
$js .= 'questions_answers['.$this->questionJSId.'] = new Array('.substr($jstmp, 0, -1).');';
$js .= 'questions_types['.$this->questionJSId.'] = \'mcua\';';
$js .= $jstmpw;
}
$html .= '</table></td></tr>';
return [$js, $html];
}
}
/**
* This class handles the SCORM export of true/false questions.
*
* @package chamilo.exercise.scorm
*/
class ScormAnswerTrueFalse extends Answer
{
/**
* Return the XML flow for the possible answers.
* That's one <response_lid>, containing several <flow_label>.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function export()
{
$js = '';
$html = '<tr><td colspan="2"><table width="100%">';
$identifier = 'question_'.$this->questionJSId.'_tf';
$identifier_true = $identifier.'_true';
$identifier_false = $identifier.'_false';
$html .=
'<tr>
<td align="center" width="5%">
<input name="'.$identifier_true.'" id="'.$identifier_true.'" value="'.$this->trueGrade.'" type="radio" />
</td>
<td width="95%">
<label for="'.$identifier_true.'">'.get_lang('True').'</label>
</td>
</tr>';
$html .=
'<tr>
<td align="center" width="5%">
<input name="'.$identifier_false.'" id="'.$identifier_false.'" value="'.$this->falseGrade.'" type="radio" />
</td>
<td width="95%">
<label for="'.$identifier_false.'">'.get_lang('False').'</label>
</td>
</tr></table></td></tr>';
$js .= 'questions_answers['.$this->questionJSId.'] = new Array(\'true\',\'false\');'."\n";
$js .= 'questions_types['.$this->questionJSId.'] = \'tf\';'."\n";
if ($this->response === 'TRUE') {
$js .= 'questions_answers_correct['.$this->questionJSId.'] = new Array(\'true\');'."\n";
} else {
$js .= 'questions_answers_correct['.$this->questionJSId.'] = new Array(\'false\');'."\n";
}
$jstmpw = 'questions_answers_ponderation['.$this->questionJSId.'] = new Array();'."\n";
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.'][0] = 0;'."\n";
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.'][1] = '.$this->weighting[1].";\n";
$js .= $jstmpw;
return [$js, $html];
}
}
/**
* This class handles the SCORM export of fill-in-the-blanks questions.
*
@ -243,107 +78,6 @@ class ScormAnswerFillInBlanks extends Answer
}
}
/**
* This class handles the SCORM export of matching questions.
*
* @package chamilo.exercise.scorm
*/
class ScormAnswerMatching extends Answer
{
/**
* Export the question part as a matrix-choice, with only one possible answer per line.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function export()
{
$js = '';
$html = '<tr><td colspan="2"><table width="100%">';
// prepare list of right proposition to allow
// - easiest display
// - easiest randomisation if needed one day
// (here I use array_values to change array keys from $code1 $code2 ... to 0 1 ...)
// get max length of displayed array
$nbrAnswers = $this->selectNbrAnswers();
$cpt1 = 'A';
$cpt2 = 1;
$Select = [];
$qId = $this->questionJSId;
$s = '';
$jstmp = '';
$jstmpc = '';
$jstmpw = 'questions_answers_ponderation['.$this->questionJSId.'] = new Array();'."\n";
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.'][0] = 0;'."\n";
for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
$identifier = 'question_'.$qId.'_matching_';
$answer = $this->selectAnswer($answerId);
$answerCorrect = $this->isCorrect($answerId);
$weight = $this->selectWeighting($answerId);
$jstmp .= $answerId.',';
if (!$answerCorrect) {
// options (A, B, C, ...) that will be put into the list-box
$Select[$answerId]['Lettre'] = $cpt1;
// answers that will be shown at the right side
$Select[$answerId]['Reponse'] = $answer;
$cpt1++;
} else {
$s .= '<tr>';
$s .= '<td width="40%" valign="top"><b>'.$cpt2.'</b>.&nbsp;'.$answer."</td>";
$s .= '<td width="20%" align="center">&nbsp;&nbsp;<select name="'.$identifier.$cpt2.'" id="'.$identifier.$cpt2.'">';
$s .= ' <option value="0">--</option>';
// fills the list-box
foreach ($Select as $key => $val) {
$s .= '<option value="'.$key.'">'.$val['Lettre'].'</option>';
} // end foreach()
$s .= '</select>&nbsp;&nbsp;</td>';
$s .= '<td width="40%" valign="top">';
if (isset($Select[$cpt2])) {
$s .= '<b>'.$Select[$cpt2]['Lettre'].'.</b> '.$Select[$cpt2]['Reponse'];
} else {
$s .= '&nbsp;';
}
$s .= "</td></tr>";
$jstmpc .= '['.$answerCorrect.','.$cpt2.'],';
$my_weight = explode('@', $weight);
if (count($my_weight) == 2) {
$weight = $my_weight[0];
} else {
$weight = $my_weight[0];
}
$jstmpw .= 'questions_answers_ponderation['.$qId.']['.$cpt2.'] = '.$weight.";\n";
$cpt2++;
// if the left side of the "matching" has been completely shown
if ($answerId == $nbrAnswers) {
// if there remain answers to be shown on the right side
while (isset($Select[$cpt2])) {
$s .= '<tr>';
$s .= '<td width="60%" colspan="2">&nbsp;</td>';
$s .= '<td width="40%" valign="top">';
$s .= '<b>'.$Select[$cpt2]['Lettre'].'.</b> '.$Select[$cpt2]['Reponse'];
$s .= "</td></tr>";
$cpt2++;
}
// end while()
} // end if()
}
}
$js .= 'questions_answers['.$this->questionJSId.'] = new Array('.substr($jstmp, 0, -1).');'."\n";
$js .= 'questions_answers_correct['.$this->questionJSId.'] = new Array('.substr($jstmpc, 0, -1).');'."\n";
$js .= 'questions_types['.$this->questionJSId.'] = \'matching\';'."\n";
$js .= $jstmpw;
$html .= $s;
$html .= '</table></td></tr>'."\n";
return [$js, $html];
}
}
/**
* This class handles the SCORM export of free-answer questions.
@ -406,10 +140,11 @@ class ScormAnswerHotspot extends Answer
*/
public function get_js_header()
{
$header = '<script>';
$header .= file_get_contents(api_get_path(SYS_CODE_PATH).'inc/lib/javascript/hotspot/js/hotspot.js');
$header .= '</script>';
if ($this->standalone) {
$header = '<script>';
$header .= file_get_contents('../inc/lib/javascript/hotspot/js/hotspot.js');
$header .= '</script>';
//because this header closes so many times the <script> tag, we have to reopen our own
$header .= '<script>';
$header .= 'questions_answers['.$this->questionJSId.'] = new Array();'."\n";
@ -454,7 +189,6 @@ class ScormAnswerHotspot extends Answer
$answer_list .= '<li>'.$this->selectAnswer($answerId).'</li>';
}
$answer_list .= '</ol></div>';
$canClick = true;
$relPath = api_get_path(REL_PATH);
$html .= <<<HTML
<tr>

@ -385,7 +385,7 @@ function addListeners(){
}
//assign event handlers to objects
if(lms_lp_type==1 || lms_item_type=='asset'){
logit_lms('Dokeos LP or asset',2);
logit_lms('Chamilo LP or asset',2);
// If this path is a Chamilo learnpath, then start manual save
// when something is loaded in there.
var myelem = document.getElementById('content_id');

@ -1556,7 +1556,7 @@ class learnpath
return false;
}
$prerequisite_id = intval($prerequisite_id);
$prerequisite_id = (int) $prerequisite_id;
$tbl_lp_item = Database::get_course_table(TABLE_LP_ITEM);
if (!is_numeric($mastery_score) || $mastery_score < 0) {
@ -10513,7 +10513,7 @@ class learnpath
// Create the zip handler (this will remain available throughout the method).
$archivePath = api_get_path(SYS_ARCHIVE_PATH);
$sys_course_path = api_get_path(SYS_COURSE_PATH);
$temp_dir_short = uniqid('scorm_export');
$temp_dir_short = uniqid('scorm_export', true);
$temp_zip_dir = $archivePath.'/'.$temp_dir_short;
$temp_zip_file = $temp_zip_dir.'/'.md5(time()).'.zip';
$zip_folder = new PclZip($temp_zip_file);
@ -11010,7 +11010,7 @@ class learnpath
);
$my_item->appendChild($my_title);
$my_max_score = $xmldoc->createElement('max_score', $item->get_max());
//$my_item->appendChild($my_max_score);
$my_item->appendChild($my_max_score);
// Give a child element <adlcp:prerequisites> to the <item> element.
$my_prereqs = $xmldoc->createElement('adlcp:prerequisites', $item->get_prereq_string());
$my_prereqs->setAttribute('type', 'aicc_script');
@ -11037,10 +11037,8 @@ class learnpath
$my_file_path = 'quiz_'.$item->get_id().'.html';
// Write the contents of the exported exercise into a (big) html file
// to later pack it into the exported SCORM. The file will be removed afterwards.
$contents = ScormSection::exportExerciseToScorm(
$exe,
true
);
$scormExercise = new ScormExercise($exe, true);
$contents = $scormExercise->export();
$tmp_file_path = $archivePath.$temp_dir_short.'/'.$my_file_path;
$res = file_put_contents($tmp_file_path, $contents);
@ -11057,7 +11055,6 @@ class learnpath
$my_resource->setAttribute('identifier', 'RESOURCE_'.$item->get_id());
$my_resource->setAttribute('type', 'webcontent');
$my_resource->setAttribute('href', $my_xml_file_path);
// adlcp:scormtype can be either 'sco' or 'asset'.
$my_resource->setAttribute('adlcp:scormtype', 'sco');
// xml:base is the base directory to find the files declared in this resource.

@ -6,11 +6,13 @@
* @author Yannick Warnier - inspired by the ADLNet documentation on SCORM content-side API
* @package scorm.js
*/
/**
* Initialisation of the SCORM API section.
* Find the SCO functions (startTimer, computeTime, etc in the second section)
* Find the Chamilo-proper functions (checkAnswers, etc in the third section)
*/
var _debug = true;
var findAPITries = 0;
var _apiHandle = null; //private variable
@ -27,6 +29,8 @@ var _InvalidSetValue = 402;
var _ElementIsReadOnly = 403;
var _ElementIsWriteOnly = 404;
var _IncorrectDataType = 405;
var startTime;
var exitPageStatus;
/**
* Gets the API handle right into the local API object and ensure there is only one.
@ -78,11 +82,12 @@ function getAPI()
}
return MyAPI;
}
/**
* Handles error codes (prints the error if it has a description)
* @return int Error code from LMS's API
*/
function ErrorHandler()
function errorHandler()
{
if (API == null) {
alert("Unable to locate the LMS's API. Cannot determine LMS error code");
@ -96,19 +101,31 @@ function ErrorHandler()
errDescription += "\n";
errDescription += api.LMSGetDiagnostic(null);
}
console.log(errDescription);
addDebug(errDescription);
} else {
var errDescription = API.LMSGetErrorString(errCode);
if (_debug) {
errDescription += "\n";
errDescription += api.LMSGetDiagnostic(null);
}
console.log(errDescription);
addDebug(errDescription);
}
}
return errCode;
}
function addDebug(message) {
if (_debug && window.console) {
console.log(message);
}
}
function addDebugTable(message) {
if (_debug && window.console) {
console.table(message);
}
}
/**
* Calls the LMSInitialize method of the LMS's API object
* @return string The string value of the LMS returned value or false if error (should be "true" otherwise)
@ -119,9 +136,10 @@ function doLMSInitialize()
alert(errMsgLocate + "\nLMSInitialize failed");
return false;
}
var result = API.LMSInitialize("");
if (result.toString() != "true") {
var err = ErrorHandler();
var err = errorHandler();
}
return result.toString();
}
@ -138,7 +156,7 @@ function doLMSFinish()
} else {
var result = API.LMSFinish('');
if (result.toString() != "true") {
var err = ErrorHandler();
var err = errorHandler();
}
}
return result.toString();
@ -152,16 +170,18 @@ function doLMSGetValue(name)
{
if (API == null) {
alert(errMsgLocate + "\nLMSGetValue was not successful.");
return "";
return '';
} else {
var value = API.LMSGetValue(name);
var errCode = API.LMSGetLastError().toString();
if (errCode != _NoError) {
// an error was encountered so display the error description
var errDescription = API.LMSGetErrorString(errCode);
alert("LMSGetValue(" + name + ") failed. \n" + errDescription);
return "";
addDebug("LMSGetValue(" + name + ") failed. \n" + errDescription)
return '';
}
return value.toString();
}
}
@ -179,7 +199,7 @@ function doLMSSetValue(name, value)
} else {
var result = API.LMSSetValue(name, value);
if (result.toString() != "true") {
var err = ErrorHandler();
var err = errorHandler();
}
}
return;
@ -196,7 +216,7 @@ function doLMSCommit()
} else {
var result = API.LMSCommit("");
if (result != "true") {
var err = ErrorHandler();
var err = errorHandler();
}
}
return result.toString();
@ -224,7 +244,7 @@ function doLMSGetErrorString(errorCode)
alert(errMsgLocate + "\nLMSGetErrorString was not successful.");
}
return API.LMSGetErrorString(errorCode).toString();
return API.LMSGetErrorString(errorCode).toString();
}
/**
@ -236,15 +256,9 @@ function doLMSGetDiagnostic(errorCode)
alert(errMsgLocate + "\nLMSGetDiagnostic was not successful.");
}
return API.LMSGetDiagnostic(errorCode).toString();
return API.LMSGetDiagnostic(errorCode).toString();
}
/**
* Second section. The SCO functions are located here (handle time and score messaging to SCORM API)
* Initialisation
*/
var startTime;
var exitPageStatus;
/**
* Initialise page values
*/
@ -379,18 +393,10 @@ function unloadPage(status)
{
if (!exitPageStatus)
{
// doQuit( status );
// doQuit( status );
}
}
/**
* Third section - depending on Chamilo - check answers and set score
*/
var questions = new Array();
var questions_answers = new Array();
var questions_answers_correct = new Array();
var questions_types = new Array();
var questions_score_max = new Array();
var questions_answers_ponderation = new Array();
/**
* Checks the answers on the test formular page
*/
@ -400,13 +406,10 @@ function checkAnswers(interrupted)
var status = 'not attempted';
var scoreMax = 0;
if (_debug) {
console.log('questions_answers_correct:');
console.log(questions_answers_correct);
}
addDebug('Number of questions: '+ questions.length);
for (var i=0; i < questions.length; i++) {
if (questions[i] != undefined && questions[i] != null){
if (questions[i] != undefined && questions[i] != null) {
var idQuestion = questions[i];
var type = questions_types[idQuestion];
var interactionScore = 0;
@ -414,69 +417,45 @@ function checkAnswers(interrupted)
var interactionCorrectResponses = '';
var interactionType = '';
if (_debug) {
console.log('Type: ' +type);
console.log('idQuestion: ' +idQuestion);
console.log('questions_answers: ');
console.log(questions_answers[idQuestion]);
console.log('questions_answers_ponderation: ');
console.log(questions_answers_ponderation[idQuestion]);
console.log('questions_answers_correct: ');
console.log(questions_answers_correct[idQuestion]);
}
if (type == 'mcma') {
interactionType = 'choice';
var myScore = 0;
for(var j=0; j<questions_answers[idQuestion].length;j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_'+(idQuestion)+'_multiple_'+(idAnswer));
if (answer.checked) {
interactionAnswers += idAnswer+'__|';// changed by isaac flores
myScore += questions_answers_ponderation[idQuestion][idAnswer];
}
}
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
if (_debug) {
console.log("Score: "+myScore);
}
} else if (type == 'mcua') {
interactionType = 'choice';
var myScore = 0;
for (var j=0; j<questions_answers[idQuestion].length;j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_'+(idQuestion)+'_unique_'+(idAnswer));
if (answer.checked) {
interactionAnswers += idAnswer;
if (_debug) {
console.log("idAnswer: "+idAnswer);
console.log("questions_answers_correct: "+questions_answers_correct[idQuestion][idAnswer]);
}
if (questions_answers_correct[idQuestion][idAnswer] == idAnswer) {
if (questions_answers_ponderation[idQuestion][idAnswer]) {
myScore += questions_answers_ponderation[idQuestion][idAnswer];
} else {
myScore++;
}
addDebug('idQuestion: ' +idQuestion + ', Type: ' +type);
addDebug('questions_answers: ');
addDebugTable(questions_answers[idQuestion]);
addDebug('questions_answers_ponderation: ');
addDebugTable(questions_answers_ponderation[idQuestion]);
addDebug('questions_answers_correct: ');
addDebugTable(questions_answers_correct[idQuestion]);
switch (type) {
case 'mcma':
interactionType = 'choice';
var myScore = 0;
for(var j = 0; j< questions_answers[idQuestion].length;j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_'+(idQuestion)+'_multiple_'+(idAnswer));
if (answer.checked) {
interactionAnswers += idAnswer+'__|';// changed by isaac flores
myScore += questions_answers_ponderation[idQuestion][idAnswer];
}
}
}
if (_debug) {
console.log("Score: "+myScore);
}
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
} else if (type == 'tf') {
interactionType = 'true-false';
var myScore = 0;
for (var j = 0; j < questions_answers[idQuestion].length; j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_' + idQuestion + '_tf_' + (idAnswer));
if (answer.checked.value) {
interactionAnswers += idAnswer;
for (k = 0; k < questions_answers_correct[idQuestion].length; k++) {
if (questions_answers_correct[idQuestion][k] == idAnswer) {
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
addDebug("Score: "+myScore);
break;
case 'mcua':
interactionType = 'choice';
var myScore = 0;
for (var j = 0; j<questions_answers[idQuestion].length; j++) {
var idAnswer = questions_answers[idQuestion][j];
var elementId = 'question_'+(idQuestion)+'_unique_'+(idAnswer);
var answer = document.getElementById(elementId);
if (answer.checked) {
addDebug('Element id # "'+ elementId +'" was checked');
interactionAnswers += idAnswer;
addDebug("List of correct answers: "+questions_answers_correct[idQuestion]);
addDebug('Score for this answer: ' + questions_answers_ponderation[idQuestion][idAnswer]);
addDebug("idAnswer: "+idAnswer);
addDebug("Option selected: "+questions_answers_correct[idQuestion][idAnswer]);
if (questions_answers_correct[idQuestion][idAnswer] == 1) {
if (questions_answers_ponderation[idQuestion][idAnswer]) {
myScore += questions_answers_ponderation[idQuestion][idAnswer];
} else {
@ -485,132 +464,166 @@ function checkAnswers(interrupted)
}
}
}
}
if (_debug) {
console.log("Score: "+myScore);
}
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
} else if (type == 'fib') {
interactionType = 'fill-in';
var myScore = 0;
for (var j = 0; j < questions_answers[idQuestion].length; j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_'+(idQuestion)+'_fib_'+(idAnswer));
if (answer.value) {
interactionAnswers += answer.value + '__|';//changed by isaac flores
for (k = 0; k < questions_answers_correct[idQuestion].length; k++) {
if (questions_answers_correct[idQuestion][k] == answer.value) {
if (questions_answers_ponderation[idQuestion][idAnswer]) {
myScore += questions_answers_ponderation[idQuestion][idAnswer];
} else {
myScore++;
addDebug("Score: "+myScore);
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
break;
case 'tf':
interactionType = 'true-false';
var myScore = 0;
for (var j = 0; j < questions_answers[idQuestion].length; j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_' + idQuestion + '_tf_' + (idAnswer));
if (answer.checked.value) {
interactionAnswers += idAnswer;
for (k = 0; k < questions_answers_correct[idQuestion].length; k++) {
if (questions_answers_correct[idQuestion][k] == idAnswer) {
if (questions_answers_ponderation[idQuestion][idAnswer]) {
myScore += questions_answers_ponderation[idQuestion][idAnswer];
} else {
myScore++;
}
}
}
}
}
}
if (_debug) {
console.log("Score: "+myScore);
}
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
} else if (type == 'matching') {
interactionType = 'matching';
var myScore = 0;
for (var j = 0; j < questions_answers[idQuestion].length; j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_' + (idQuestion) + '_matching_' + (idAnswer));
if (answer && answer.value) {
interactionAnswers += answer.value + '__|';//changed by isaac flores
for (k = 0; k < questions_answers_correct[idQuestion].length; k++) {
var left = questions_answers_correct[idQuestion][k][0];
var right = questions_answers_correct[idQuestion][k][1];
if (left == idAnswer && right == answer.value) {
if (questions_answers_ponderation[idQuestion][idAnswer]) {
myScore += questions_answers_ponderation[idQuestion][idAnswer];
} else {
myScore++;
addDebug("Score: "+ myScore);
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
break;
case 'fib':
interactionType = 'fill-in';
var myScore = 0;
for (var j = 0; j < questions_answers[idQuestion].length; j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_'+(idQuestion)+'_fib_'+(idAnswer));
if (answer.value) {
interactionAnswers += answer.value + '__|';//changed by isaac flores
for (k = 0; k < questions_answers_correct[idQuestion].length; k++) {
if (questions_answers_correct[idQuestion][k] == answer.value) {
if (questions_answers_ponderation[idQuestion][idAnswer]) {
myScore += questions_answers_ponderation[idQuestion][idAnswer];
} else {
myScore++;
}
}
}
}
}
}
if (_debug) {
console.log("Score: "+myScore);
}
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
} else if (type == 'free') {
//ignore for now as a score cannot be given
interactionType = 'free';
var answer = document.getElementById('question_'+(idQuestion)+'_free');
if (answer && answer.value) {
interactionAnswers += answer.value
}
//interactionScore = questions_score_max[idQuestion];
interactionScore = 0;
scoreMax += questions_score_max[idQuestion];
//interactionAnswers = document.getElementById('question_'+(idQuestion)+'_free').value;
//correct responses work by pattern, see SCORM Runtime Env Doc
//interactionCorrectResponses += questions_answers_correct[idQuestion].toString();
} else if (type == 'hotspot') {
interactionType = 'sequencing';
interactionScore = 0;
//if(question_score && question_score[idQuestion]){
// interactionScore = question_score[idQuestion];
//} //else, 0
//interactionAnswers = document.getElementById('question_'+(idQuestion)+'_free').innerHTML;
//correct responses work by pattern, see SCORM Runtime Env Doc
//for(k=0;k<questions_answers_correct[idQuestion].length;k++)
//{
// interactionCorrectResponses += questions_answers_correct[idQuestion][k].toString()+',';
//}
} else if (type == 'exact') {
interactionType = 'exact';
interactionScore = 0;
var real_answers = new Array();
for (var j = 0; j < questions_answers[idQuestion].length; j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_' + (idQuestion) + '_exact_' + (idAnswer));
if (answer.checked == true) {
interactionAnswers += idAnswer+', ';
if (questions_answers_correct[idQuestion][idAnswer] != 0) {
real_answers[j] = true;
} else {
real_answers[j] = false;
addDebug("Score: "+myScore);
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
break;
case 'matching':
interactionType = 'matching';
var myScore = 0;
addDebug("List of correct answers: ");
console.log(questions_answers_correct[idQuestion]);
for (var j = 0; j < questions_answers[idQuestion].length; j++) {
var idAnswer = questions_answers[idQuestion][j];
var elementId = 'question_' + (idQuestion) + '_matching_' + (idAnswer);
addDebug("---------idAnswer #"+idAnswer+'------------------');
addDebug("Checking element #"+elementId);
var answer = document.getElementById(elementId);
if (answer && answer.value) {
interactionAnswers += answer.value + '__|';//changed by isaac flores
for (k = 0; k < questions_answers_correct[idQuestion].length; k++) {
var left = questions_answers_correct[idQuestion][k][0];
var right = questions_answers_correct[idQuestion][k][1];
addDebug('Left ' + left);
addDebug('Right ' + right);
addDebug('answer.value ' + answer.value);
if (right == idAnswer && left == answer.value) {
addDebug('Score for this answer: ' + questions_answers_ponderation[idQuestion][idAnswer]);
if (questions_answers_ponderation[idQuestion][idAnswer]) {
myScore += questions_answers_ponderation[idQuestion][idAnswer];
} else {
// myScore++;
}
}
}
}
} else {
if (questions_answers_correct[idQuestion][idAnswer] != 0) {
real_answers[j] = false;
addDebug("Partial score: "+myScore);
addDebug("--------- end --- idAnswer #"+idAnswer+'------------------');
}
addDebug("Score: "+myScore);
interactionScore = myScore;
scoreMax += questions_score_max[idQuestion];
break;
case 'free':
//ignore for now as a score cannot be given
interactionType = 'free';
var answer = document.getElementById('question_'+(idQuestion)+'_free');
if (answer && answer.value) {
interactionAnswers += answer.value
}
//interactionScore = questions_score_max[idQuestion];
interactionScore = 0;
scoreMax += questions_score_max[idQuestion];
//interactionAnswers = document.getElementById('question_'+(idQuestion)+'_free').value;
//correct responses work by pattern, see SCORM Runtime Env Doc
//interactionCorrectResponses += questions_answers_correct[idQuestion].toString();
break;
case 'hotspot':
interactionType = 'sequencing';
interactionScore = 0;
//if(question_score && question_score[idQuestion]){
// interactionScore = question_score[idQuestion];
//} //else, 0
//interactionAnswers = document.getElementById('question_'+(idQuestion)+'_free').innerHTML;
//correct responses work by pattern, see SCORM Runtime Env Doc
//for(k=0;k<questions_answers_correct[idQuestion].length;k++)
//{
// interactionCorrectResponses += questions_answers_correct[idQuestion][k].toString()+',';
//}
break;
case 'exact':
interactionType = 'exact';
interactionScore = 0;
var real_answers = new Array();
for (var j = 0; j < questions_answers[idQuestion].length; j++) {
var idAnswer = questions_answers[idQuestion][j];
var answer = document.getElementById('question_' + (idQuestion) + '_exact_' + (idAnswer));
if (answer.checked == true) {
interactionAnswers += idAnswer+', ';
if (questions_answers_correct[idQuestion][idAnswer] != 0) {
real_answers[j] = true;
} else {
real_answers[j] = false;
}
} else {
real_answers[j] = true;
if (questions_answers_correct[idQuestion][idAnswer] != 0) {
real_answers[j] = false;
} else {
real_answers[j] = true;
}
}
}
}
var final_answer = true;
for (var z = 0; z < real_answers.length; z++) {
if (real_answers[z] == false) {
final_answer = false;
var final_answer = true;
for (var z = 0; z < real_answers.length; z++) {
if (real_answers[z] == false) {
final_answer = false;
}
}
interactionScore = 0;
addDebug(real_answers);
if (final_answer) {
//getting only the first score where we save the weight of all the question
interactionScore = questions_answers_ponderation[idQuestion][1];
}
}
interactionScore = 0;
console.log(real_answers);
if (final_answer) {
//getting only the first score where we save the weight of all the question
interactionScore = questions_answers_ponderation[idQuestion][1];
}
if (_debug) {
console.log("Score: "+interactionScore);
}
scoreMax += questions_score_max[idQuestion];
addDebug("Score: "+interactionScore);
scoreMax += questions_score_max[idQuestion];
break;
}
tmpScore += interactionScore;
doLMSSetValue('cmi.interactions.'+idQuestion+'.id', 'Q'+idQuestion);
doLMSSetValue('cmi.interactions.'+idQuestion+'.type', interactionType);
doLMSSetValue('cmi.interactions.'+idQuestion+'.student_response', interactionAnswers);
@ -621,29 +634,81 @@ function checkAnswers(interrupted)
doLMSSetValue('cmi.core.score.max', scoreMax);
doLMSSetValue('cmi.core.score.raw', tmpScore);
//get status
// Get status
var mastery_score = doLMSGetValue('cmi.student_data.mastery_score');
if (mastery_score <= 0) {
mastery_score = (scoreMax * 0.80);
}
if (tmpScore > mastery_score) {
if (tmpScore >= mastery_score) {
status = 'passed';
} else {
status = 'failed';
}
if (_debug) {
console.log('student_score: ' + tmpScore);
console.log('mastery_score: ' + mastery_score);
console.log('cmi.core.score.max: ' + scoreMax);
console.log('cmi.core.lesson_status: ' + status);
}
addDebug('student_score: ' + tmpScore);
addDebug('mastery_score: ' + mastery_score);
addDebug('cmi.core.score.max: ' + scoreMax);
addDebug('cmi.core.lesson_status: ' + status);
doLMSSetValue('cmi.core.lesson_status', status);
if (interrupted && (status != 'completed') && (status != 'passed')) {
doLMSSetValue('cmi.core.exit', 'suspended');
}
return false; //do not submit the form
}
(function($){
//Shuffle all rows, while keeping the first column
//Requires: Shuffle
$.fn.shuffleRows = function(){
return this.each(function(){
var main = $(/table/i.test(this.tagName) ? this.tBodies[0] : this);
var firstElem = [], counter=0;
main.children().each(function(){
firstElem.push(this.firstChild);
});
main.shuffle();
main.children().each(function(){
this.insertBefore(firstElem[counter++], this.firstChild);
});
});
}
/* Shuffle is required */
$.fn.shuffle = function() {
return this.each(function(){
var items = $(this).children();
return (items.length)
? $(this).html($.shuffle(items))
: this;
});
}
$.shuffle = function(arr) {
for(
var j, x, i = arr.length; i;
j = parseInt(Math.random() * i),
x = arr[--i], arr[i] = arr[j], arr[j] = x
);
return arr;
}
})(jQuery);
/*
* Assigns any event handler to any element
* @param object Element on which the event is added
* @param string Name of event
* @param string Function to trigger on event
* @param boolean Capture the event and prevent
*/
function addEvent(elm, evType, fn, useCapture)
{
if (elm.addEventListener) {
elm.addEventListener(evType, fn, useCapture);
return true;
} else if(elm.attachEvent) {
var r = elm.attachEvent('on' + evType, fn);
return r;
} else {
elm['on' + evType] = fn;
}
}

@ -63,13 +63,8 @@ class scorm extends learnpath
/**
* Possible SCO status: see CAM doc 2.3.2.5.1: passed, completed, browsed, failed, not attempted, incomplete.
*/
/**
* Prerequisites: see CAM doc 2.3.2.5.1 for pseudo-code.
*/
/**
*
* Parses an imsmanifest.xml file and puts everything into the $manifest array.
*
* @param string Path to the imsmanifest.xml file on the system.
@ -340,15 +335,15 @@ class scorm extends learnpath
$courseInfo = api_get_course_info($courseCode);
$courseId = $courseInfo['real_id'];
$userId = intval($userId);
$userId = (int) $userId;
if (empty($userId)) {
$userId = api_get_user_id();
}
// Get table names.
$new_lp = Database::get_course_table(TABLE_LP_MAIN);
$new_lp_item = Database::get_course_table(TABLE_LP_ITEM);
$userMaxScore = intval($userMaxScore);
$sessionId = empty($sessionId) ? api_get_session_id() : intval($sessionId);
$userMaxScore = (int) $userMaxScore;
$sessionId = empty($sessionId) ? api_get_session_id() : (int) $sessionId;
foreach ($this->organizations as $id => $dummy) {
$oOrganization = &$this->organizations[$id];
// Prepare and execute insert queries:
@ -472,13 +467,12 @@ class scorm extends learnpath
}
$title = Database::escape_string($item['title']);
$title = api_utf8_decode($title);
$max_score = (int) $item['max_score'];
$max_score = intval($item['max_score']);
if ($max_score == 0 || is_null($max_score) || $max_score == '') {
if ($max_score === 0) {
// If max score is not set The use_max_score parameter
// is check in order to use 100 (chamilo style) or '' (strict scorm)
$max_score = "NULL";
$max_score = 'NULL';
if ($userMaxScore) {
$max_score = 100;
}
@ -547,11 +541,12 @@ class scorm extends learnpath
$courseid = api_get_course_id();
$ic_slide->addCourseId($courseid);
$ic_slide->addToolId(TOOL_LEARNPATH);
// TODO: Unify with other lp types.
$xapian_data = [
SE_COURSE_ID => $courseid,
SE_TOOL_ID => TOOL_LEARNPATH,
SE_DATA => ['lp_id' => $lp_id, 'lp_item' => $previous, 'document_id' => ''], // TODO: Unify with other lp types.
SE_USER => (int) api_get_user_id(),
SE_DATA => ['lp_id' => $lp_id, 'lp_item' => $previous, 'document_id' => ''],
SE_USER => api_get_user_id(),
];
$ic_slide->xapian_data = serialize($xapian_data);
$di->addChunk($ic_slide);
@ -607,7 +602,9 @@ class scorm extends learnpath
$lpToCheck = null
) {
if ($this->debug > 0) {
error_log('In scorm::import_package('.print_r($zip_file_info, true).',"'.$current_dir.'") method', 0);
error_log(
'In scorm::import_package('.print_r($zip_file_info, true).',"'.$current_dir.'") method'
);
}
$courseInfo = empty($courseInfo) ? api_get_course_info() : $courseInfo;
@ -646,8 +643,6 @@ class scorm extends learnpath
// Check the zip content (real size and file extension).
$zipContentArray = $zipFile->listContent();
$package_type = '';
$at_root = false;
$manifest = '';
$manifest_list = [];
// The following loop should be stopped as soon as we found the right imsmanifest.xml (how to recognize it?).
$realFileSize = 0;
@ -656,9 +651,7 @@ class scorm extends learnpath
$file = $thisContent['filename'];
$this->set_error_msg("File $file contains a PHP script");
} elseif (stristr($thisContent['filename'], 'imsmanifest.xml')) {
//error_log('Found imsmanifest at '.$thisContent['filename'], 0);
if ($thisContent['filename'] == basename($thisContent['filename'])) {
$at_root = true;
} else {
if ($this->debug > 2) {
error_log("New LP - subdir is now ".$this->subdir);
@ -666,7 +659,6 @@ class scorm extends learnpath
}
$package_type = 'scorm';
$manifest_list[] = $thisContent['filename'];
$manifest = $thisContent['filename']; //just the relative directory inside scorm/
}
$realFileSize += $thisContent['size'];
}
@ -684,13 +676,10 @@ class scorm extends learnpath
$this->subdir .= '/'.dirname($shortest_path); // Do not concatenate because already done above.
$manifest = $shortest_path;
if ($this->debug > 1) {
error_log('New LP - Package type is now '.$package_type);
if ($this->debug) {
error_log("New LP - Package type is now: '$package_type'");
}
if ($package_type == '') {
if ($this->debug > 1) {
error_log('New LP - Package type is empty');
}
Display::addFlash(
Display::return_message(get_lang('NotScormContent'))
);
@ -728,7 +717,6 @@ class scorm extends learnpath
}
/* Uncompressing phase */
/*
We need to process each individual file in the zip archive to
- add it to the database
@ -770,11 +758,6 @@ class scorm extends learnpath
}
while ($file = readdir($dir)) {
if ($file != '.' && $file != '..') {
$filetype = 'file';
if (is_dir($course_sys_dir.$new_dir.$file)) {
$filetype = 'folder';
}
// TODO: RENAMING FILES CAN BE VERY DANGEROUS SCORM-WISE, avoid that as much as possible!
//$safe_file = api_replace_dangerous_char($file, 'strict');
$find_str = ['\\', '.php', '.phtml'];

@ -1378,7 +1378,7 @@ class CourseBuilder
if ($result) {
while ($obj = Database::fetch_object($result)) {
$items = [];
$sql = "SELECT * FROM ".$table_item."
$sql = "SELECT * FROM $table_item
WHERE c_id = '$courseId' AND lp_id = ".$obj->id;
$resultItem = Database::query($sql);
while ($obj_item = Database::fetch_object($resultItem)) {

@ -2806,6 +2806,14 @@ class CourseRestorer
$item['item_type'] = $item['item_type'] == 'dokeos_chapter' ? 'dir' : $item['item_type'];
$masteryScore = $item['mastery_score'];
// If item is a chamilo quiz, then use the max score as mastery_score
if ($item['item_type'] == 'quiz') {
if (empty($masteryScore)) {
$masteryScore = $item['max_score'];
}
}
$params = [
'c_id' => $this->destination_course_id,
'lp_id' => self::DBUTF8($new_lp_id),
@ -2816,7 +2824,7 @@ class CourseRestorer
'description' => self::DBUTF8($item['description']),
'min_score' => self::DBUTF8($item['min_score']),
'max_score' => self::DBUTF8($item['max_score']),
'mastery_score' => self::DBUTF8($item['mastery_score']),
'mastery_score' => self::DBUTF8($masteryScore),
'parent_item_id' => self::DBUTF8($item['parent_item_id']),
'previous_item_id' => self::DBUTF8($item['previous_item_id']),
'next_item_id' => self::DBUTF8($item['next_item_id']),

Loading…
Cancel
Save