Browse Source

Fix Chamilo quiz to scorm export see BT#14644

Requires composer update to install new serializer.
Julio Montoya 6 years ago
parent
commit
9ce5eb789a

+ 1 - 0
composer.json

@@ -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",

+ 1 - 1
main/exercise/answer.class.php

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

+ 100 - 0
main/exercise/export/scorm/ScormAnswerMatching.php

@@ -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];
+    }
+}

+ 119 - 0
main/exercise/export/scorm/ScormAnswerMultipleChoice.php

@@ -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];
+    }
+}

+ 56 - 0
main/exercise/export/scorm/ScormAnswerTrueFalse.php

@@ -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];
+    }
+}

+ 34 - 89
main/exercise/export/scorm/ScormAssessmentItem.php

@@ -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];
     }
 }

+ 49 - 88
main/exercise/export/scorm/ScormSection.php → main/exercise/export/scorm/ScormExercise.php

@@ -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;

+ 10 - 4
main/exercise/export/scorm/ScormQuestion.php

@@ -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:

+ 25 - 0
main/exercise/export/scorm/common.js

@@ -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);

+ 4 - 270
main/exercise/export/scorm/scorm_classes.php

@@ -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>

+ 1 - 1
main/lp/aicc_api.php

@@ -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');

+ 5 - 8
main/lp/learnpath.class.php

@@ -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.

+ 279 - 214
main/lp/packaging/assets/api_wrapper.js

@@ -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;
+    }
+}

+ 15 - 32
main/lp/scorm.class.php

@@ -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'];

+ 1 - 1
src/Chamilo/CourseBundle/Component/CourseCopy/CourseBuilder.php

@@ -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)) {

+ 9 - 1
src/Chamilo/CourseBundle/Component/CourseCopy/CourseRestorer.php

@@ -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']),