This commit is contained in:
Xes
2025-08-14 22:37:50 +02:00
parent fb6d5d5926
commit 3641e93527
9156 changed files with 1813532 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class Annotation
* Allow instanciate an object of type HotSpot extending the class question.
*
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*/
class Annotation extends Question
{
public $typePicture = 'annotation.png';
public $explanationLangVar = 'Annotation';
/**
* Annotation constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = ANNOTATION;
$this->isContent = $this->getIsContent();
}
public function display()
{
}
/**
* {@inheritdoc}
*/
public function createForm(&$form, $exercise)
{
parent::createForm($form, $exercise);
$form->addElement(
'number',
'weighting',
get_lang('Weighting'),
['step' => '0.1']
);
if (!empty($this->iid)) {
$form->setDefaults(['weighting' => float_format($this->weighting, 1)]);
} else {
if ($this->isContent == 1) {
$form->setDefaults(['weighting' => '10']);
}
}
global $text;
if (isset($_GET['editQuestion'])) {
$form->addButtonUpdate($text, 'submitQuestion');
return;
}
$form->addElement(
'file',
'imageUpload',
[
Display::img(
Display::return_icon(
'annotation.png',
null,
null,
ICON_SIZE_BIG,
false,
true
)
),
get_lang('UploadJpgPicture'),
]
);
$form->addButtonSave(get_lang('GoToQuestion'), 'submitQuestion');
$form->addRule(
'imageUpload',
get_lang('OnlyImagesAllowed'),
'filetype',
['jpg', 'jpeg', 'png', 'gif']
);
$form->addRule('imageUpload', get_lang('NoImage'), 'uploadedfile');
}
/**
* {@inheritdoc}
*/
public function processCreation($form, $exercise)
{
$fileInfo = $form->getSubmitValue('imageUpload');
parent::processCreation($form, $exercise);
if (!empty($fileInfo['tmp_name'])) {
$result = $this->uploadPicture($fileInfo['tmp_name']);
if ($result) {
$this->weighting = $form->getSubmitValue('weighting');
$this->save($exercise);
return true;
}
return false;
}
return false;
}
/**
* @param FormValidator $form
*/
public function createAnswersForm($form)
{
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$this->weighting = $form->getSubmitValue('weighting');
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$score['revised'] = $this->isQuestionWaitingReview($score);
return parent::return_header($exercise, $counter, $score); // TODO: Change the autogenerated stub
}
}

View File

@@ -0,0 +1,216 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class AnswerInOfficeDoc
* Allows a question type where the answer is written in an Office document.
*
* @author Cristian
*/
class AnswerInOfficeDoc extends Question
{
public $typePicture = 'options_evaluation.png';
public $explanationLangVar = 'AnswerInOfficeDoc';
public $sessionId;
public $userId;
public $exerciseId;
public $exeId;
private $storePath;
private $fileName;
private $filePath;
/**
* Constructor.
*/
public function __construct()
{
if ('true' !== OnlyofficePlugin::create()->get('enable_onlyoffice_plugin')) {
throw new Exception(get_lang('OnlyOfficePluginRequired'));
}
parent::__construct();
$this->type = ANSWER_IN_OFFICE_DOC;
$this->isContent = $this->getIsContent();
}
/**
* Initialize the file path structure.
*/
public function initFile(int $sessionId, int $userId, int $exerciseId, int $exeId): void
{
$this->sessionId = $sessionId ?: 0;
$this->userId = $userId;
$this->exerciseId = $exerciseId ?: 0;
$this->exeId = $exeId;
$this->storePath = $this->generateDirectory();
$this->fileName = $this->generateFileName();
$this->filePath = $this->storePath.$this->fileName;
}
/**
* Create form for uploading an Office document.
*/
public function createAnswersForm($form): void
{
if (!empty($this->exerciseList)) {
$this->exerciseId = reset($this->exerciseList);
}
$allowedFormats = [
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // .xlsx
'application/msword', // .doc
'application/vnd.ms-excel', // .xls
];
$form->addElement('file', 'office_file', get_lang('UploadOfficeDoc'));
$form->addRule('office_file', get_lang('ThisFieldIsRequired'), 'required');
$form->addRule('office_file', get_lang('InvalidFileFormat'), 'mimetype', $allowedFormats);
$allowedExtensions = implode(', ', ['.docx', '.xlsx', '.doc', '.xls']);
$form->addElement('static', 'file_hint', get_lang('AllowedFormats'), "<p>{$allowedExtensions}</p>");
if (!empty($this->extra)) {
$fileUrl = api_get_path(WEB_COURSE_PATH).$this->getStoredFilePath();
$form->addElement('static', 'current_office_file', get_lang('CurrentOfficeDoc'), "<a href='{$fileUrl}' target='_blank'>{$this->extra}</a>");
}
$form->addText('weighting', get_lang('Weighting'), ['class' => 'span1']);
global $text;
$form->addButtonSave($text, 'submitQuestion');
if (!empty($this->iid)) {
$form->setDefaults(['weighting' => float_format($this->weighting, 1)]);
} else {
if ($this->isContent == 1) {
$form->setDefaults(['weighting' => '10']);
}
}
}
/**
* Process the uploaded document and save it.
*/
public function processAnswersCreation($form, $exercise): void
{
if (!empty($_FILES['office_file']['name'])) {
$extension = pathinfo($_FILES['office_file']['name'], PATHINFO_EXTENSION);
$tempFilename = "office_".uniqid().".".$extension;
$tempPath = sys_get_temp_dir().'/'.$tempFilename;
if (!move_uploaded_file($_FILES['office_file']['tmp_name'], $tempPath)) {
return;
}
$this->weighting = $form->getSubmitValue('weighting');
$this->extra = "";
$this->save($exercise);
$this->exerciseId = $exercise->iid;
$uploadDir = $this->generateDirectory();
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0775, true);
}
$filename = "office_".$this->iid.".".$extension;
$filePath = $uploadDir.$filename;
if (!rename($tempPath, $filePath)) {
return;
}
$this->extra = $filename;
$this->save($exercise);
}
}
/**
* Get the stored file path dynamically.
*/
public function getStoredFilePath(): ?string
{
if (empty($this->extra)) {
return null;
}
return "{$this->course['path']}/exercises/onlyoffice/{$this->exerciseId}/{$this->iid}/{$this->extra}";
}
/**
* Get the absolute file path. Returns null if the file doesn't exist.
*/
public function getFileUrl(bool $loadFromDatabase = false): ?string
{
if ($loadFromDatabase) {
$em = Database::getManager();
$result = $em->getRepository('ChamiloCoreBundle:TrackEAttempt')->findOneBy([
'exeId' => $this->exeId,
'userId' => $this->userId,
'questionId' => $this->iid,
'sessionId' => $this->sessionId,
'cId' => $this->course['real_id'],
]);
if (!$result || empty($result->getFilename())) {
return null;
}
$this->fileName = $result->getFilename();
} else {
if (empty($this->extra)) {
return null;
}
$this->fileName = $this->extra;
}
$filePath = $this->getStoredFilePath();
if (is_file(api_get_path(SYS_COURSE_PATH).$filePath)) {
return $filePath;
}
return null;
}
/**
* Show the question in an exercise.
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$score['revised'] = $this->isQuestionWaitingReview($score);
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'">
<tr>
<th>'.get_lang("Answer").'</th>
</tr>';
return $header;
}
/**
* Generate the necessary directory for OnlyOffice documents.
*/
private function generateDirectory(): string
{
$exercisePath = api_get_path(SYS_COURSE_PATH).$this->course['path']."/exercises/onlyoffice/{$this->exerciseId}/{$this->iid}/";
if (!is_dir($exercisePath)) {
mkdir($exercisePath, 0775, true);
}
return rtrim($exercisePath, '/').'/';
}
/**
* Generate the file name for the OnlyOffice document.
*/
private function generateFileName(): string
{
return 'office_'.uniqid();
}
}

234
main/exercise/Draggable.php Normal file
View File

@@ -0,0 +1,234 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class Draggable.
*
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*/
class Draggable extends Question
{
public $typePicture = 'ordering.png';
public $explanationLangVar = 'Draggable';
/**
* Class constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = DRAGGABLE;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$defaults = [];
$nb_matches = $nb_options = 2;
$matches = [];
$answer = null;
if ($form->isSubmitted()) {
$nb_matches = $form->getSubmitValue('nb_matches');
$nb_options = $form->getSubmitValue('nb_options');
if (isset($_POST['lessMatches'])) {
$nb_matches--;
}
if (isset($_POST['moreMatches'])) {
$nb_matches++;
}
if (isset($_POST['lessOptions'])) {
$nb_options--;
}
if (isset($_POST['moreOptions'])) {
$nb_options++;
}
} elseif (!empty($this->iid)) {
$defaults['orientation'] = in_array($this->extra, ['h', 'v']) ? $this->extra : 'v';
$answer = new Answer($this->iid);
$answer->read();
if ($answer->nbrAnswers > 0) {
$nb_matches = $nb_options = 0;
for ($i = 1; $i <= $answer->nbrAnswers; $i++) {
if ($answer->isCorrect($i)) {
$nb_matches++;
$defaults['answer['.$nb_matches.']'] = $answer->selectAnswer($i);
$defaults['weighting['.$nb_matches.']'] = float_format($answer->selectWeighting($i), 1);
$answerInfo = $answer->getAnswerByAutoId($answer->correct[$i]);
$defaults['matches['.$nb_matches.']'] = isset($answerInfo['answer']) ? $answerInfo['answer'] : '';
} else {
$nb_options++;
$defaults['option['.$nb_options.']'] = $answer->selectAnswer($i);
}
}
}
} else {
$defaults['answer[1]'] = get_lang('DefaultMakeCorrespond1');
$defaults['answer[2]'] = get_lang('DefaultMakeCorrespond2');
$defaults['matches[2]'] = '2';
$defaults['option[1]'] = get_lang('DefaultMatchingOptA');
$defaults['option[2]'] = get_lang('DefaultMatchingOptB');
$defaults['orientation'] = 'v';
}
for ($i = 1; $i <= $nb_matches; $i++) {
$matches[$i] = $i;
}
$form->addElement('hidden', 'nb_matches', $nb_matches);
$form->addElement('hidden', 'nb_options', $nb_options);
$form->addRadio(
'orientation',
get_lang('ChooseOrientation'),
['v' => get_lang('Vertical'), 'h' => get_lang('Horizontal')]
);
// DISPLAY MATCHES
$html = '<table class="table table-striped table-hover">
<thead>
<tr>
<th width="85%">'.get_lang('Answer').'</th>
<th width="15%">'.get_lang('MatchesTo').'</th>
<th width="10">'.get_lang('Weighting').'</th>
</tr>
</thead>
<tbody>';
$form->addHeader(get_lang('MakeCorrespond'));
$form->addHtml($html);
if ($nb_matches < 1) {
$nb_matches = 1;
echo Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'), 'normal');
}
for ($i = 1; $i <= $nb_matches; $i++) {
$renderer = &$form->defaultRenderer();
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"answer[$i]"
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"matches[$i]"
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"weighting[$i]"
);
$form->addHtml('<tr>');
$form->addText("answer[$i]", null);
$form->addSelect("matches[$i]", null, $matches);
$form->addText("weighting[$i]", null, true, ['value' => 10, 'style' => 'width: 60px;']);
$form->addHtml('</tr>');
}
$form->addHtml('</tbody></table>');
$renderer->setElementTemplate(
'<div class="form-group"><div class="col-sm-offset-2">{element}',
'lessMatches'
);
$renderer->setElementTemplate('{element}</div></div>', 'moreMatches');
global $text;
$group = [
$form->addButtonDelete(get_lang('DelElem'), 'lessMatches', true),
$form->addButtonCreate(get_lang('AddElem'), 'moreMatches', true),
$form->addButtonSave($text, 'submitQuestion', true),
];
$form->addGroup($group);
if (!empty($this->iid)) {
$form->setDefaults($defaults);
} else {
$form->setDefaults(['orientation' => 'v']);
if (1 == $this->isContent) {
$form->setDefaults($defaults);
}
}
$form->setConstants(
[
'nb_matches' => $nb_matches,
'nb_options' => $nb_options,
]
);
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$this->extra = $form->exportValue('orientation');
$nb_matches = $form->getSubmitValue('nb_matches');
$this->weighting = 0;
$position = 0;
$objAnswer = new Answer($this->iid);
// Insert the options
for ($i = 1; $i <= $nb_matches; $i++) {
$position++;
$objAnswer->createAnswer($position, 0, '', 0, $position);
}
// Insert the answers
for ($i = 1; $i <= $nb_matches; $i++) {
$position++;
$answer = $form->getSubmitValue('answer['.$i.']');
$matches = $form->getSubmitValue('matches['.$i.']');
$weighting = $form->getSubmitValue('weighting['.$i.']');
$this->weighting += $weighting;
$objAnswer->createAnswer(
$answer,
$matches,
'',
$weighting,
$position
);
}
$objAnswer->save();
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'"><tr>';
if ($exercise->showExpectedChoice()) {
$header .= '<th>'.get_lang('YourChoice').'</th>';
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
} else {
$header .= '<th>'.get_lang('ElementList').'</th>';
$header .= '<th>'.get_lang('YourChoice').'</th>';
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
$header .= '<th>'.get_lang('Status').'</th>';
$header .= '</tr>';
return $header;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/* For licensing terms, see /license.txt */
/**
* FillBlanksCombination.
*/
class FillBlanksCombination extends FillBlanks
{
public $typePicture = 'fill_in_blanks_co.png';
public $explanationLangVar = 'FillBlanksCombination';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = FILL_IN_BLANKS_COMBINATION;
$this->isContent = $this->getIsContent();
}
}

View File

@@ -0,0 +1,20 @@
<?php
/* For licensing terms, see /license.txt */
/**
* HotSpotCombination.
*/
class HotSpotCombination extends HotSpot
{
public $typePicture = 'hotspot_co.png';
public $explanationLangVar = 'HotSpotCombination';
/**
* HotSpot constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = HOT_SPOT_COMBINATION;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class HotSpotDelineation.
*/
class HotSpotDelineation extends HotSpot
{
public $typePicture = 'hotspot-delineation.png';
public $explanationLangVar = 'HotspotDelineation';
/**
* HotSpotDelineation constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = HOT_SPOT_DELINEATION;
}
}

View File

@@ -0,0 +1,149 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This script shows the list of exercises for administrators and students.
*
* @author Istvan Mandak
*
* @version $Id: Hpdownload.php 22201 2009-07-17 19:57:03Z cfasanando $
*/
session_cache_limiter('public');
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
$this_section = SECTION_COURSES;
$tbl_document = Database::get_course_table(TABLE_DOCUMENT);
$doc_url = str_replace(['../', '\\..', '\\0', '..\\'], ['', '', '', ''], urldecode($_GET['doc_url']));
$filename = basename($doc_url);
// launch event
//Event::event_download($doc_url);
if (isset($_course['path'])) {
$course_path = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
$full_file_name = $course_path.Security::remove_XSS($doc_url);
} else {
$course_path = api_get_path(SYS_COURSE_PATH).$cid.'/document';
$full_file_name = $course_path.Security::remove_XSS($doc_url);
}
if (!is_file($full_file_name)) {
exit;
}
if (!Security::check_abs_path($full_file_name, $course_path.'/')) {
exit;
}
$extension = explode('.', $filename);
$extension = strtolower($extension[count($extension) - 1]);
switch ($extension) {
case 'gz':
$content_type = 'application/x-gzip';
break;
case 'zip':
$content_type = 'application/zip';
break;
case 'pdf':
$content_type = 'application/pdf';
break;
case 'png':
$content_type = 'image/png';
break;
case 'gif':
$content_type = 'image/gif';
break;
case 'jpg':
$content_type = 'image/jpeg';
break;
case 'txt':
$content_type = 'text/plain';
break;
case 'htm':
$content_type = 'text/html';
break;
case 'html':
$content_type = 'text/html';
break;
default:
$content_type = 'application/octet-stream';
break;
}
header('Content-disposition: filename='.$filename);
header('Content-Type: '.$content_type);
header('Expires: '.gmdate('D, d M Y H:i:s', time() + 10).' GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s', time() + 10).' GMT');
/*
Dynamic parsing section
is activated whenever a user views an html file
work in progress
- question: we could also parse per line,
perhaps this would be faster.
($file_content = file($full_file_name) returns file in array)
*/
if ($content_type == 'text/html') {
$directory_name = dirname($full_file_name);
$coursePath = api_get_path(SYS_COURSE_PATH);
$dir = str_replace(['\\', $coursePath.$_course['path'].'/document'], ['/', ''], $directory_name);
if ($dir[strlen($dir) - 1] != '/') {
$dir .= '/';
}
//Parse whole file at one
$fp = fopen($full_file_name, "r");
$file_content = fread($fp, filesize($full_file_name));
fclose($fp);
$exercisePath = api_get_self();
$exfile = explode('/', $exercisePath);
$exfile = $exfile[sizeof($exfile) - 1];
$exercisePath = substr($exercisePath, 0, strpos($exercisePath, $exfile));
$content = $file_content;
$mit = "function Finish(){";
$js_content = "var SaveScoreVariable = 0; // This variable included by Chamilo\n".
"function mySaveScore() // This function included by Chamilo\n".
"{\n".
" if (SaveScoreVariable==0)\n".
" {\n".
" SaveScoreVariable = 1;\n".
" if (C.ie)\n".
" {\n".
" document.location.href = \"".$exercisePath."savescores.php?origin=$origin&time=$time&test=".$doc_url."&uid=".$_user['user_id']."&cid=".$cid."&score=\"+Score;\n".
" //window.alert(Score);\n".
" }\n".
" else\n".
" {\n".
" }\n".
" }\n".
"}\n".
"// Must be included \n".
"function Finish(){\n".
" mySaveScore();";
$newcontent = str_replace($mit, $js_content, $content);
$prehref = "javascript:void(0);";
$posthref = api_get_path(WEB_CODE_PATH)."main/exercise/Hpdownload.php?doc_url=".$doc_url."&cid=".$cid."&uid=".$uid;
$newcontent = str_replace($prehref, $posthref, $newcontent);
$prehref = "class=\"GridNum\" onclick=";
$posthref = "class=\"GridNum\" onMouseover=";
$newcontent = str_replace($prehref, $posthref, $newcontent);
header('Content-length: '.strlen($newcontent));
// Dipsp.
echo $newcontent;
exit();
}
//normal case, all non-html files
//header('Content-length: ' . filesize($full_file_name));
$fp = fopen($full_file_name, 'rb');
fpassthru($fp);
fclose($fp);

View File

@@ -0,0 +1,21 @@
<?php
/* For licensing terms, see /license.txt */
/**
* MatchingCombination.
*/
class MatchingCombination extends Matching
{
public $typePicture = 'matching_co.png';
public $explanationLangVar = 'MatchingCombination';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = MATCHING_COMBINATION;
$this->isContent = $this->getIsContent();
}
}

View File

@@ -0,0 +1,312 @@
<?php
/* For licensing terms, see /license.txt */
/**
* MatchingDraggable.
*
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*/
class MatchingDraggable extends Question
{
public $typePicture = 'matchingdrag.png';
public $explanationLangVar = 'MatchingDraggable';
/**
* Class constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = MATCHING_DRAGGABLE;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$defaults = [];
$nb_matches = $nb_options = 2;
$matches = [];
$answer = null;
$counter = 1;
if (!empty($this->iid)) {
$answer = new Answer($this->iid);
$answer->read();
if ($answer->nbrAnswers > 0) {
for ($i = 1; $i <= $answer->nbrAnswers; $i++) {
$correct = $answer->isCorrect($i);
if (empty($correct)) {
$matches[$answer->selectAutoId($i)] = chr(64 + $counter);
$counter++;
}
}
}
}
if ($form->isSubmitted()) {
$nb_matches = $form->getSubmitValue('nb_matches');
$nb_options = $form->getSubmitValue('nb_options');
if (isset($_POST['lessOptions'])) {
$nb_matches--;
$nb_options--;
}
if (isset($_POST['moreOptions'])) {
$nb_matches++;
$nb_options++;
}
} elseif (!empty($this->iid)) {
if ($answer->nbrAnswers > 0) {
$nb_matches = $nb_options = 0;
for ($i = 1; $i <= $answer->nbrAnswers; $i++) {
if ($answer->isCorrect($i)) {
$nb_matches++;
$defaults['answer['.$nb_matches.']'] = $answer->selectAnswer($i);
$defaults['weighting['.$nb_matches.']'] = float_format($answer->selectWeighting($i), 1);
$defaults['matches['.$nb_matches.']'] = $answer->correct[$i];
} else {
$nb_options++;
$defaults['option['.$nb_options.']'] = $answer->selectAnswer($i);
}
}
}
} else {
$defaults['answer[1]'] = get_lang('DefaultMakeCorrespond1');
$defaults['answer[2]'] = get_lang('DefaultMakeCorrespond2');
$defaults['matches[2]'] = '2';
$defaults['option[1]'] = get_lang('DefaultMatchingOptA');
$defaults['option[2]'] = get_lang('DefaultMatchingOptB');
}
if (empty($matches)) {
for ($i = 1; $i <= $nb_options; $i++) {
// fill the array with A, B, C.....
$matches[$i] = chr(64 + $i);
}
} else {
for ($i = $counter; $i <= $nb_options; $i++) {
// fill the array with A, B, C.....
$matches[$i] = chr(64 + $i);
}
}
$form->addElement('hidden', 'nb_matches', $nb_matches);
$form->addElement('hidden', 'nb_options', $nb_options);
$thWeighting = '';
if (MATCHING_DRAGGABLE === $this->type) {
$thWeighting = '<th width="10">'.get_lang('Weighting').'</th>';
}
// DISPLAY MATCHES
$html = '<table class="table table-striped table-hover">
<thead>
<tr>
<th width="10">'.get_lang('Number').'</th>
<th width="85%">'.get_lang('Answer').'</th>
<th width="15%">'.get_lang('MatchesTo').'</th>
'.$thWeighting.'
</tr>
</thead>
<tbody>';
$form->addHeader(get_lang('MakeCorrespond'));
$form->addHtml($html);
if ($nb_matches < 1) {
$nb_matches = 1;
echo Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'), 'normal');
}
$editorConfig = [
'ToolbarSet' => 'TestMatching',
'Width' => '100%',
'Height' => '125',
];
for ($i = 1; $i <= $nb_matches; $i++) {
$renderer = &$form->defaultRenderer();
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"answer[$i]"
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"matches[$i]"
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"weighting[$i]"
);
$form->addHtml('<tr>');
$form->addHtml("<td>$i</td>");
//$form->addText("answer[$i]", null);
$form->addHtmlEditor(
"answer[$i]",
null,
null,
false,
$editorConfig
);
$form->addSelect("matches[$i]", null, $matches);
if (MATCHING_DRAGGABLE === $this->type) {
$form->addText("weighting[$i]", null, true, ['style' => 'width: 60px;', 'value' => 10]);
} else {
$form->addHidden("weighting[$i]", "0");
}
$form->addHtml('</tr>');
}
$form->addHtml('</tbody></table>');
// DISPLAY OPTIONS
$html = '<table class="table table-striped table-hover">
<thead>
<tr>
<th width="15%">'.get_lang('Number').'</th>
<th width="85%">'.get_lang('Answer').'</th>
</tr>
</thead>
<tbody>';
$form->addHtml($html);
if ($nb_options < 1) {
$nb_options = 1;
echo Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'), 'normal');
}
for ($i = 1; $i <= $nb_options; $i++) {
$renderer = &$form->defaultRenderer();
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"option[$i]"
);
$form->addHtml('<tr>');
$form->addHtml('<td>'.chr(64 + $i).'</td>');
$form->addHtmlEditor(
"option[$i]",
null,
null,
false,
$editorConfig
);
$form->addHtml('</tr>');
}
$form->addHtml('</table>');
if (MATCHING_DRAGGABLE_COMBINATION === $this->type) {
//only 1 answer the all deal ...
$form->addText('questionWeighting', get_lang('Score'), true, ['value' => 10]);
if (!empty($this->iid)) {
$defaults['questionWeighting'] = $this->weighting;
}
}
global $text;
$group = [];
// setting the save button here and not in the question class.php
$group[] = $form->addButtonDelete(get_lang('DelElem'), 'lessOptions', true);
$group[] = $form->addButtonCreate(get_lang('AddElem'), 'moreOptions', true);
$group[] = $form->addButtonSave($text, 'submitQuestion', true);
$form->addGroup($group);
if (!empty($this->iid)) {
$form->setDefaults($defaults);
} else {
if ($this->isContent == 1) {
$form->setDefaults($defaults);
}
}
$form->setConstants(
[
'nb_matches' => $nb_matches,
'nb_options' => $nb_options,
]
);
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$nb_matches = $form->getSubmitValue('nb_matches');
$nb_options = $form->getSubmitValue('nb_options');
$this->weighting = 0;
$position = 0;
$objAnswer = new Answer($this->iid);
// Insert the options
for ($i = 1; $i <= $nb_options; $i++) {
$position++;
$option = $form->getSubmitValue("option[$i]");
$objAnswer->createAnswer($option, 0, '', 0, $position);
}
// Insert the answers
for ($i = 1; $i <= $nb_matches; $i++) {
$position++;
$answer = $form->getSubmitValue("answer[$i]");
$matches = $form->getSubmitValue("matches[$i]");
$weighting = $form->getSubmitValue("weighting[$i]");
$this->weighting += $weighting;
$objAnswer->createAnswer(
$answer,
$matches,
'',
$weighting,
$position
);
}
if (MATCHING_DRAGGABLE_COMBINATION == $this->type) {
$this->weighting = $form->getSubmitValue('questionWeighting');
}
$objAnswer->save();
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="matching '.$this->question_table_class.'"><tr>';
$header .= '<th>'.get_lang('ElementList').'</th>';
if (!in_array($exercise->results_disabled, [
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
//RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
])
) {
$header .= '<th>'.get_lang('YourChoice').'</th>';
}
if ($exercise->showExpectedChoice()) {
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
$header .= '<th>'.get_lang('Status').'</th>';
} else {
$header .= '<th>'.get_lang('CorrespondsTo').'</th>';
}
$header .= '</tr>';
return $header;
}
}

View File

@@ -0,0 +1,21 @@
<?php
/* For licensing terms, see /license.txt */
/**
* MatchingDraggableCombination.
*/
class MatchingDraggableCombination extends MatchingDraggable
{
public $typePicture = 'matchingdrag_co.png';
public $explanationLangVar = 'MatchingDraggableCombination';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = MATCHING_DRAGGABLE_COMBINATION;
$this->isContent = $this->getIsContent();
}
}

View File

@@ -0,0 +1,138 @@
<?php
/* For licensing terms, see /license.txt */
class MultipleAnswerDropdown extends Question
{
public $typePicture = 'mcma_dropdown.png';
public $explanationLangVar = 'MultipleAnswerDropdown';
public function __construct()
{
parent::__construct();
$this->type = MULTIPLE_ANSWER_DROPDOWN;
}
public function createForm(&$form, $exercise)
{
global $text;
parent::createForm($form, $exercise);
$objExe = ChamiloSession::read('objExercise');
$form->addTextarea(
'list_text',
[get_lang('AnswerList'), get_lang('EnterListOfAnswersOneAnswerByLine')],
['rows' => 8]
);
$form->addFile(
'list_file',
['', get_lang('OrSelectCsvFileWithListOfAnswers')],
['accept' => 'text/csv']
);
$buttonGroup = [];
if ($objExe->edit_exercise_in_lp == true ||
(empty($this->exerciseList) && empty($objExe->iid))
) {
$buttonGroup[] = $form->addButton(
'submitQuestion',
$text,
'check',
'primary',
'default',
null,
['id' => 'submit-question'],
true
);
}
$form->addGroup($buttonGroup);
if (!empty($this->iid)) {
$objAnswer = new Answer($this->iid, 0, $exercise, false);
$optionData = array_column(
$objAnswer->getAnswers(),
'answer'
);
$form->setDefaults(
['list_text' => implode(PHP_EOL, $optionData)]
);
}
}
public function createAnswersForm($form)
{
}
public function processCreation($form, $exercise)
{
$listFile = $form->getSubmitValue('list_file');
$listText = $form->getSubmitValue('list_text');
parent::processCreation($form, $exercise);
$lines = [];
if (UPLOAD_ERR_OK === (int) $listFile['error']) {
$lines = Import::csvColumnToArray($listFile['tmp_name']);
} elseif (!empty($listText)) {
$lines = explode("\n", $listText);
}
$lines = array_map('trim', $lines);
$lines = array_filter($lines);
$objAnswer = new Answer($this->iid);
$i = 1;
foreach ($lines as $line) {
$isCorrect = 0;
if (isset($objAnswer->correct[$i])) {
$isCorrect = (int) $objAnswer->correct[$i];
}
$objAnswer->createAnswer($line, $isCorrect, '', $objAnswer->weighting[$i] ?? 0, $i++);
}
$objAnswer->save();
}
/**
* @param FormValidator $form
* @param Exercise $exercise
*
* @return void
*/
public function processAnswersCreation($form, $exercise)
{
}
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'"><thead><tr>';
$header .= '<th class="text-center">'.get_lang('Choice').'</th>';
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th class="text-center">'.get_lang('ExpectedChoice').'</th>';
}
$header .= '<th>'.get_lang('Answer').'</th>';
if ($exercise->showExpectedChoice()) {
$header .= '<th class="text-center">'.get_lang('Status').'</th>';
}
$header .= '</tr></thead>';
return $header;
}
}

View File

@@ -0,0 +1,18 @@
<?php
/* For licensing terms, see /license.txt */
/**
* MultipleAnswerDropdownCombination.
*/
class MultipleAnswerDropdownCombination extends MultipleAnswerDropdown
{
public $typePicture = 'mcma_dropdown_co.png';
public $explanationLangVar = 'MultipleAnswerDropdownCombination';
public function __construct()
{
parent::__construct();
$this->type = MULTIPLE_ANSWER_DROPDOWN_COMBINATION;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,251 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class ReadingComprehension.
*
* This class allows to instantiate an object of type READING_COMPREHENSION
* extending the class question
*/
class ReadingComprehension extends UniqueAnswer
{
public $typePicture = 'reading-comprehension.png';
public $explanationLangVar = 'ReadingComprehension';
/**
* Defines the different speeds of scrolling for the reading window,
* in words per minute. If 300 words text in 50w/m, then the moving
* window will progress from top to bottom in 6 minutes.
*
* @var array
*/
public static $speeds = [
1 => 50,
2 => 100,
3 => 175,
4 => 300,
5 => 600,
];
/**
* The number of words in the question description (which serves as the
* text to read).
*
* @var int
*/
public $wordsCount = 0;
/**
* Number of words expected to show per refresh.
*
* @var int
*/
public $expectedWordsPerRefresh = 0;
/**
* Refresh delay in seconds.
*
* @var int
*/
public $refreshTime = 3;
/**
* All speeds (static $speeds + extra speeds defined in configuration.php as 'exercise_question_reading_comprehension_extra_speeds'.
*
* @var array
*/
public $allSpeeds = [];
/**
* Indicates how show the question list.
* 1 = all in one page; 2 = one per page (default).
*
* @var int
*/
private $exerciseType = 2;
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = READING_COMPREHENSION;
$this->isContent = $this->getIsContent();
$extraSpeeds = api_get_configuration_value('exercise_question_reading_comprehension_extra_speeds');
$customSpeeds = $extraSpeeds['speeds'] ?? [];
$this->allSpeeds = self::$speeds;
if (!empty($customSpeeds) && is_array($customSpeeds)) {
foreach ($customSpeeds as $speed) {
$this->allSpeeds[] = $speed;
}
asort($this->allSpeeds);
}
}
public function processText($text)
{
// Refresh is set to 5s, but speed is in words per minute
$wordsPerSecond = $this->allSpeeds[$this->level] / 60;
$this->expectedWordsPerRefresh = intval($wordsPerSecond * $this->refreshTime);
if (empty($text)) {
// We have an issue here... how do we treat this case?
// For now, let's define a default case
$text = get_lang('NoExercise');
}
$words = str_word_count($text, 2, '0..9');
$indexes = array_keys($words);
$tagEnd = '</span>';
$tagStart = $tagEnd.'<span class="text-highlight">';
$this->wordsCount = count($words);
$turns = ceil(
$this->wordsCount / $this->expectedWordsPerRefresh
);
$firstIndex = $indexes[0];
for ($i = 1; $i <= $turns; $i++) {
$text = substr_replace($text, $tagStart, $firstIndex, 0);
if ($i * $this->expectedWordsPerRefresh <= count($words)) {
$newIndex = $i * $this->expectedWordsPerRefresh;
if (isset($indexes[$newIndex])) {
$nextFirstIndex = $indexes[$newIndex];
$firstIndex = $nextFirstIndex + (strlen($tagStart) * $i);
}
}
}
$pos = strpos($text, $tagEnd);
$text = substr_replace($text, '', $pos, strlen($tagEnd));
$text .= $tagEnd;
$this->displayReading($this->wordsCount, $turns, $text);
}
/**
* Returns total count of words of the text to read.
*
* @return int
*/
public function getWordsCount()
{
$words = str_word_count($this->selectDescription(), 2, '0..9');
$this->wordsCount = count($words);
return $this->wordsCount;
}
/**
* {@inheritdoc}
*/
public function createForm(&$form, $exercise)
{
// Categories
$tabCat = TestCategory::getCategoriesIdAndName();
$form->addSelect('questionCategory', get_lang('Category'), $tabCat);
// Advanced parameters
$levels = $this->getReadingSpeeds();
$form->addSelect('questionLevel', get_lang('Difficulty'), $levels);
$form->addElement('hidden', 'answerType', READING_COMPREHENSION);
$form->addTextarea('questionDescription', get_lang('Text'), ['rows' => 20]);
// question name
if (api_get_configuration_value('save_titles_as_html')) {
$editorConfig = ['ToolbarSet' => 'TitleAsHtml'];
$form->addHtmlEditor(
'questionName',
get_lang('Question'),
false,
false,
$editorConfig,
true
);
} else {
$form->addText('questionName', get_lang('Question'), false);
}
// hidden values
$form->addRule('questionName', get_lang('GiveQuestion'), 'required');
$isContent = isset($_REQUEST['isContent']) ? (int) $_REQUEST['isContent'] : null;
// default values
$defaults = [];
$defaults['questionName'] = $this->question;
$defaults['questionDescription'] = $this->description;
$defaults['questionLevel'] = $this->level;
$defaults['questionCategory'] = $this->category;
// Came from he question pool
if (isset($_GET['fromExercise'])) {
$form->setDefaults($defaults);
}
if (!isset($_GET['newQuestion']) || $isContent) {
$form->setDefaults($defaults);
}
}
/**
* {@inheritdoc}
*/
public static function get_default_levels()
{
return [
1 => sprintf(get_lang('ReadingComprehensionLevelX'), self::$speeds[1]),
2 => sprintf(get_lang('ReadingComprehensionLevelX'), self::$speeds[2]),
3 => sprintf(get_lang('ReadingComprehensionLevelX'), self::$speeds[3]),
4 => sprintf(get_lang('ReadingComprehensionLevelX'), self::$speeds[4]),
5 => sprintf(get_lang('ReadingComprehensionLevelX'), self::$speeds[5]),
];
}
/**
* Return the augmented speeds (using, if defined, the 'exercise_question_reading_comprehension_extra_speeds' setting.
*/
public function getReadingSpeeds(): array
{
$defaultLevels = [];
foreach ($this->allSpeeds as $i => $v) {
$defaultLevels[$i] = sprintf(get_lang('ReadingComprehensionLevelX'), $this->allSpeeds[$i]);
}
return $defaultLevels;
}
/**
* @param int $type
*
* @return ReadingComprehension
*/
public function setExerciseType($type)
{
$this->exerciseType = (int) $type;
return $this;
}
/**
* @param $wordsCount
* @param $turns
* @param $text
*/
private function displayReading($wordsCount, $turns, $text)
{
$view = new Template('', false, false, false, true, false, false);
$template = $view->get_template('exercise/reading_comprehension.tpl');
$view->assign('id', $this->iid);
$view->assign('text', nl2br($text));
$view->assign('words_count', $wordsCount);
$view->assign('turns', $turns);
$view->assign('refresh_time', $this->refreshTime);
$view->assign('exercise_type', $this->exerciseType);
$view->display($template);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,428 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* UniqueAnswerImage.
*
* @author Angel Fernando Quiroz Campos <angel.quiroz@beeznest.com>
*/
class UniqueAnswerImage extends UniqueAnswer
{
public $typePicture = 'uaimg.png';
public $explanationLangVar = 'UniqueAnswerImage';
/**
* UniqueAnswerImage constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = UNIQUE_ANSWER_IMAGE;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*
* @throws Exception
*/
public function createAnswersForm($form)
{
$objExercise = Session::read('objExercise');
$editorConfig = [
'ToolbarSet' => 'TestProposedAnswer',
'Width' => '100%',
'Height' => '125',
];
//this line defines how many questions by default appear when creating a choice question
// The previous default value was 2. See task #1759.
$numberAnswers = isset($_POST['nb_answers']) ? (int) $_POST['nb_answers'] : 4;
$numberAnswers += (isset($_POST['lessAnswers']) ? -1 : (isset($_POST['moreAnswers']) ? 1 : 0));
$feedbackTitle = '';
switch ($objExercise->getFeedbackType()) {
case EXERCISE_FEEDBACK_TYPE_DIRECT:
// Scenario
$commentTitle = '<th width="20%">'.get_lang('Comment').'</th>';
$feedbackTitle = '<th width="20%">'.get_lang('Scenario').'</th>';
break;
case EXERCISE_FEEDBACK_TYPE_POPUP:
$commentTitle = '<th width="20%">'.get_lang('Comment').'</th>';
break;
default:
$commentTitle = '<th width="40%">'.get_lang('Comment').'</th>';
break;
}
$html = '<div class="alert alert-success" role="alert">'.
get_lang('UniqueAnswerImagePreferredSize200x150').'</div>';
$zoomOptions = api_get_configuration_value('quiz_image_zoom');
if (isset($zoomOptions['options'])) {
$finderFolder = api_get_path(WEB_PATH).'vendor/studio-42/elfinder/';
$html .= '<!-- elFinder CSS (REQUIRED) -->';
$html .= '<link rel="stylesheet" type="text/css" media="screen"
href="'.$finderFolder.'css/elfinder.full.css">';
$html .= '<link rel="stylesheet" type="text/css" media="screen" href="'.$finderFolder.'css/theme.css">';
$html .= '<!-- elFinder JS (REQUIRED) -->';
$html .= '<script type="text/javascript" src="'.$finderFolder.'js/elfinder.full.js"></script>';
$html .= '<!-- elFinder translation (OPTIONAL) -->';
$language = 'en';
$platformLanguage = api_get_interface_language();
$iso = api_get_language_isocode($platformLanguage);
$filePart = "vendor/studio-42/elfinder/js/i18n/elfinder.$iso.js";
$file = api_get_path(SYS_PATH).$filePart;
$includeFile = '';
if (file_exists($file)) {
$includeFile = '<script type="text/javascript" src="'.api_get_path(WEB_PATH).$filePart.'"></script>';
$language = $iso;
}
$html .= $includeFile;
$html .= '<script type="text/javascript" charset="utf-8">
$(function() {
$(".add_img_link").on("click", function(e){
e.preventDefault();
e.stopPropagation();
var name = $(this).prop("name");
var id = parseInt(name.match(/[0-9]+/));
$([document.documentElement, document.body]).animate({
scrollTop: $("#elfinder").offset().top
}, 1000);
var elf = $("#elfinder").elfinder({
url : "'.api_get_path(WEB_LIBRARY_PATH).'elfinder/connectorAction.php?'.api_get_cidreq().'",
getFileCallback: function(file) {
var filePath = file; //file contains the relative url.
var imageZoom = filePath.url;
var iname = "answer["+id+"]";
CKEDITOR.instances[iname].insertHtml(\'
<img
id="zoom_picture"
class="zoom_picture"
src="\'+imageZoom+\'"
data-zoom-image="\'+imageZoom+\'"
width="200px"
height="150px"
/>\');
$("#elfinder").elfinder("destroy"); //close the window after image is selected
},
startPathHash: "l2_Lw", // Sets the course driver as default
resizable: false,
lang: "'.$language.'"
}).elfinder("instance"+id);
});
});
</script>';
$html .= '<div id="elfinder"></div>';
}
$html .= '<table class="table table-striped table-hover">
<thead>
<tr style="text-align: center;">
<th width="10">'.get_lang('Number').'</th>
<th>'.get_lang('True').'</th>
<th>'.get_lang('Answer').'</th>
'.$commentTitle.'
'.$feedbackTitle.'
<th width="15">'.get_lang('Weighting').'</th>
</tr>
</thead>
<tbody>';
$form->addHeader(get_lang('Answers'));
$form->addHtml($html);
$defaults = [];
$correct = 0;
if (!empty($this->iid)) {
$answer = new Answer($this->iid);
$answer->read();
if ($answer->nbrAnswers > 0 && !$form->isSubmitted()) {
$numberAnswers = $answer->nbrAnswers;
}
}
$form->addElement('hidden', 'nb_answers');
//Feedback SELECT
$questionList = $objExercise->selectQuestionList();
$selectQuestion = [];
$selectQuestion[0] = get_lang('SelectTargetQuestion');
if (is_array($questionList)) {
foreach ($questionList as $key => $questionid) {
//To avoid warning messages
if (!is_numeric($questionid)) {
continue;
}
$question = Question::read($questionid);
$questionTitle = strip_tags($question->selectTitle());
$selectQuestion[$questionid] = "Q$key: $questionTitle";
}
}
$selectQuestion[-1] = get_lang('ExitTest');
$list = new LearnpathList(api_get_user_id());
$flatList = $list->get_flat_list();
$selectLpId = [];
$selectLpId[0] = get_lang('SelectTargetLP');
foreach ($flatList as $id => $details) {
$selectLpId[$id] = cut($details['lp_name'], 20);
}
$tempScenario = [];
if ($numberAnswers < 1) {
$numberAnswers = 1;
echo Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'));
}
for ($i = 1; $i <= $numberAnswers; $i++) {
$form->addHtml('<tr>');
if (isset($answer) && is_object($answer)) {
if (isset($answer->correct[$i]) && $answer->correct[$i]) {
$correct = $i;
}
$defaults['answer['.$i.']'] = $answer->answer[$i] ?? '';
$defaults['comment['.$i.']'] = $answer->comment[$i] ?? '';
$defaults['weighting['.$i.']'] = isset($answer->weighting[$i]) ? float_format($answer->weighting[$i], 1) : 0;
$itemList = [];
if (isset($answer->destination[$i])) {
$itemList = explode('@@', $answer->destination[$i]);
}
$try = $itemList[0] ?? '';
$lp = $itemList[1] ?? '';
$listDestination = $itemList[2] ?? '';
$url = $itemList[3] ?? '';
$tryResult = 0;
if (0 != $try) {
$tryResult = 1;
}
$urlResult = '';
if (0 != $url) {
$urlResult = $url;
}
$tempScenario['url'.$i] = $urlResult;
$tempScenario['try'.$i] = $tryResult;
$tempScenario['lp'.$i] = $lp;
$tempScenario['destination'.$i] = $listDestination;
} else {
$defaults['answer[1]'] = get_lang('DefaultUniqueAnswer1');
$defaults['weighting[1]'] = 10;
$defaults['answer[2]'] = get_lang('DefaultUniqueAnswer2');
$defaults['weighting[2]'] = 0;
$tempScenario['destination'.$i] = ['0'];
$tempScenario['lp'.$i] = ['0'];
}
$defaults['scenario'] = $tempScenario;
$renderer = $form->defaultRenderer();
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'correct'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'counter['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}'.
(isset($zoomOptions['options']) ?
'<br><div class="form-group ">
<label for="question_admin_form_btn_add_img['.$i.']" class="col-sm-2 control-label"></label>
<div class="col-sm-8">
<button class="add_img_link btn btn-info btn-sm"
name="btn_add_img['.$i.']"
type="submit"
id="question_admin_form_btn_add_img['.$i.']">
<em class="fa fa-plus"></em> '.get_lang('AddImageWithZoom').'
</button>
</div>
<div class="col-sm-2"></div>
</div>' : '').'</td>',
'answer['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'comment['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'weighting['.$i.']'
);
$answerNumber = $form->addElement('text', 'counter['.$i.']', null, ' value = "'.$i.'"');
$answerNumber->freeze();
$form->addElement('radio', 'correct', null, null, $i, 'class="checkbox"');
$form->addHtmlEditor('answer['.$i.']', null, null, false, $editorConfig);
$form->addRule('answer['.$i.']', get_lang('ThisFieldIsRequired'), 'required');
switch ($objExercise->getFeedbackType()) {
case EXERCISE_FEEDBACK_TYPE_DIRECT:
$this->setDirectOptions($i, $form, $renderer, $selectLpId, $selectQuestion);
break;
case EXERCISE_FEEDBACK_TYPE_POPUP:
default:
$form->addHtmlEditor('comment['.$i.']', null, null, false, $editorConfig);
break;
}
$form->addText('weighting['.$i.']', null, null, ['class' => 'col-md-1', 'value' => '0']);
$form->addHtml('</tr>');
}
$form->addHtml('</tbody>');
$form->addHtml('</table>');
global $text;
$buttonGroup = [];
if ($objExercise->edit_exercise_in_lp == true ||
(empty($this->exerciseList) && empty($objExercise->iid))
) {
//setting the save button here and not in the question class.php
$buttonGroup[] = $form->addButtonDelete(get_lang('LessAnswer'), 'lessAnswers', true);
$buttonGroup[] = $form->addButtonCreate(get_lang('PlusAnswer'), 'moreAnswers', true);
$buttonGroup[] = $form->addButtonSave($text, 'submitQuestion', true);
$form->addGroup($buttonGroup);
}
// We check the first radio button to be sure a radio button will be check
if (0 == $correct) {
$correct = 1;
}
$defaults['correct'] = $correct;
if (!empty($this->iid)) {
$form->setDefaults($defaults);
} else {
if (1 == $this->isContent) {
// Default sample content.
$form->setDefaults($defaults);
} else {
$form->setDefaults(['correct' => 1]);
}
}
$form->setConstants(['nb_answers' => $numberAnswers]);
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$questionWeighting = $nbrGoodAnswers = 0;
$correct = $form->getSubmitValue('correct');
$objAnswer = new Answer($this->iid);
$numberAnswers = $form->getSubmitValue('nb_answers');
for ($i = 1; $i <= $numberAnswers; $i++) {
$answer = trim(str_replace(['<p>', '</p>'], '', $form->getSubmitValue('answer['.$i.']')));
$comment = trim(str_replace(['<p>', '</p>'], '', $form->getSubmitValue('comment['.$i.']')));
$weighting = trim($form->getSubmitValue('weighting['.$i.']'));
$scenario = $form->getSubmitValue('scenario');
$try = null;
$lp = null;
$destination = null;
$url = null;
//$listDestination = $form -> getSubmitValue('destination'.$i);
//$destinationStr = $form -> getSubmitValue('destination'.$i);
if (!empty($scenario)) {
$try = $scenario['try'.$i];
$lp = $scenario['lp'.$i];
$destination = $scenario['destination'.$i];
$url = trim($scenario['url'.$i]);
}
/*
How we are going to parse the destination value
here we parse the destination value which is a string
1@@3@@2;4;4;@@http://www.chamilo.org
where: try_again@@lp_id@@selected_questions@@url
try_again = is 1 || 0
lp_id = id of a learning path (0 if dont select)
selected_questions= ids of questions
url= an url
$destinationStr='';
foreach ($listDestination as $destination_id)
{
$destinationStr.=$destination_id.';';
} */
$goodAnswer = $correct == $i ? true : false;
if ($goodAnswer) {
$nbrGoodAnswers++;
$weighting = abs($weighting);
if ($weighting > 0) {
$questionWeighting += $weighting;
}
}
if (empty($try)) {
$try = 0;
}
if (empty($lp)) {
$lp = 0;
}
if (empty($destination)) {
$destination = 0;
}
if ($url == '') {
$url = 0;
}
//1@@1;2;@@2;4;4;@@http://www.chamilo.org
$dest = $try.'@@'.$lp.'@@'.$destination.'@@'.$url;
$objAnswer->createAnswer(
$answer,
$goodAnswer,
$comment,
$weighting,
$i,
null,
null,
$dest
);
}
// saves the answers into the data base
$objAnswer->save();
// sets the total weighting of the question
$this->updateWeighting($questionWeighting);
$this->save($exercise);
}
}

View File

@@ -0,0 +1,64 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Question with file upload, where the file is the answer.
* Acts as an open question: requires teacher's review for a score.
*/
class UploadAnswer extends Question
{
public $typePicture = 'file_upload_question.png';
public $explanationLangVar = 'UploadAnswer';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = UPLOAD_ANSWER;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$form->addElement('text', 'weighting', get_lang('Weighting'));
global $text;
// setting the save button here and not in the question class.php
$form->addButtonSave($text, 'submitQuestion');
if (!empty($this->iid)) {
$form->setDefaults(['weighting' => float_format($this->weighting, 1)]);
} else {
if ($this->isContent == 1) {
$form->setDefaults(['weighting' => '10']);
}
}
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$this->weighting = $form->getSubmitValue('weighting');
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$score['revised'] = $this->isQuestionWaitingReview($score);
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'" >
<tr>
<th>'.get_lang('Answer').'</th>
</tr>';
return $header;
}
}

506
main/exercise/admin.php Normal file
View File

@@ -0,0 +1,506 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* Exercise administration
* This script allows to manage (create, modify) an exercise and its questions.
*
* The following scripts are included :
*
* - exercise.class.php : for the creation of an Exercise object
* - question.class.php : for the creation of a Question object
* - answer.class.php : for the creation of an Answer object
* - exercise.lib.php : functions used in the exercise tool
* - exercise_admin.inc.php : management of the exercise
* - question_admin.inc.php : management of a question (statement & answers)
* - statement_admin.inc.php : management of a statement
* - question_list_admin.inc.php : management of the question list
*
* Main variables used in this script :
*
* - $is_allowedToEdit : set to 1 if the user is allowed to manage the exercise
* - $objExercise : exercise object
* - $objQuestion : question object
* - $objAnswer : answer object
* - $exerciseId : the exercise ID
* - $picturePath : the path of question pictures
* - $newQuestion : ask to create a new question
* - $modifyQuestion : ID of the question to modify
* - $editQuestion : ID of the question to edit
* - $submitQuestion : ask to save question modifications
* - $cancelQuestion : ask to cancel question modifications
* - $deleteQuestion : ID of the question to delete
* - $moveUp : ID of the question to move up
* - $moveDown : ID of the question to move down
* - $modifyExercise : ID of the exercise to modify
* - $submitExercise : ask to save exercise modifications
* - $cancelExercise : ask to cancel exercise modifications
* - $modifyAnswers : ID of the question which we want to modify answers for
* - $cancelAnswers : ask to cancel answer modifications
* - $buttonBack : ask to go back to the previous page in answers of type "Fill in blanks"
*
* @author Olivier Brouckaert
* Modified by Hubert Borderiou 21-10-2011 Question by category
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
$this_section = SECTION_COURSES;
if (isset($_GET['r']) && 1 == $_GET['r']) {
Exercise::cleanSessionVariables();
}
// Access control
api_protect_course_script(true);
$is_allowedToEdit = api_is_allowed_to_edit(null, true, false, false);
$sessionId = api_get_session_id();
$studentViewActive = api_is_student_view_active();
$showPagination = api_get_configuration_value('show_question_pagination');
if (!$is_allowedToEdit) {
api_not_allowed(true);
}
// Variables sanitizing
$exerciseId = isset($_GET['exerciseId']) ? (int) $_GET['exerciseId'] : 0;
$newQuestion = isset($_GET['newQuestion']) ? $_GET['newQuestion'] : 0;
$modifyAnswers = isset($_GET['modifyAnswers']) ? $_GET['modifyAnswers'] : 0;
$editQuestion = isset($_GET['editQuestion']) ? $_GET['editQuestion'] : 0;
$page = isset($_GET['page']) && !empty($_GET['page']) ? (int) $_GET['page'] : 1;
$modifyQuestion = isset($_GET['modifyQuestion']) ? $_GET['modifyQuestion'] : 0;
$deleteQuestion = isset($_GET['deleteQuestion']) ? $_GET['deleteQuestion'] : 0;
$clone_question = isset($_REQUEST['clone_question']) ? $_REQUEST['clone_question'] : 0;
if (empty($questionId)) {
$questionId = Session::read('questionId');
}
if (empty($modifyExercise)) {
$modifyExercise = isset($_GET['modifyExercise']) ? $_GET['modifyExercise'] : null;
}
$fromExercise = isset($fromExercise) ? $fromExercise : null;
$cancelExercise = isset($cancelExercise) ? $cancelExercise : null;
$cancelAnswers = isset($cancelAnswers) ? $cancelAnswers : null;
$modifyIn = isset($modifyIn) ? $modifyIn : null;
$cancelQuestion = isset($cancelQuestion) ? $cancelQuestion : null;
/* Cleaning all incomplete attempts of the admin/teacher to avoid weird problems
when changing the exercise settings, number of questions, etc */
Event::delete_all_incomplete_attempts(
api_get_user_id(),
$exerciseId,
api_get_course_int_id(),
api_get_session_id()
);
// get from session
$objExercise = Session::read('objExercise');
$objQuestion = Session::read('objQuestion');
if (isset($_REQUEST['convertAnswer']) || isset($_REQUEST['convertAnswerAlt'])) {
$objQuestion = $objQuestion->swapSimpleAnswerTypes(
isset($_REQUEST['convertAnswerAlt']) ? 1 : 0
);
Session::write('objQuestion', $objQuestion);
}
$objAnswer = Session::read('objAnswer');
$_course = api_get_course_info();
// document path
$documentPath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
// picture path
$picturePath = $documentPath.'/images';
// audio path
$audioPath = $documentPath.'/audio';
if (!empty($_GET['action']) && $_GET['action'] == 'exportqti2' && !empty($_GET['questionId'])) {
require_once 'export/qti2/qti2_export.php';
$export = export_question_qti($_GET['questionId'], true);
$qid = (int) $_GET['questionId'];
$archive_path = api_get_path(SYS_ARCHIVE_PATH);
$temp_dir_short = uniqid();
$temp_zip_dir = $archive_path."/".$temp_dir_short;
if (!is_dir($temp_zip_dir)) {
mkdir($temp_zip_dir, api_get_permissions_for_new_directories());
}
$temp_zip_file = $temp_zip_dir."/".api_get_unique_id().".zip";
$temp_xml_file = $temp_zip_dir."/qti2export_".$qid.'.xml';
file_put_contents($temp_xml_file, $export);
$zip_folder = new PclZip($temp_zip_file);
$zip_folder->add($temp_xml_file, PCLZIP_OPT_REMOVE_ALL_PATH);
$name = 'qti2_export_'.$qid.'.zip';
DocumentManager::file_send_for_download($temp_zip_file, true, $name);
unlink($temp_zip_file);
unlink($temp_xml_file);
rmdir($temp_zip_dir);
exit; //otherwise following clicks may become buggy
}
// Exercise object creation.
if (!($objExercise instanceof Exercise)) {
// creation of a new exercise if wrong or not specified exercise ID
if ($exerciseId) {
$objExercise = new Exercise();
$parseQuestionList = $showPagination > 0 ? false : true;
if ($editQuestion) {
$parseQuestionList = false;
$showPagination = true;
}
$objExercise->read($exerciseId, $parseQuestionList);
Session::write('objExercise', $objExercise);
}
}
if (empty($objExercise)) {
Session::erase('objExercise');
header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
exit;
}
// Exercise can be edited in their course.
if ($objExercise->sessionId != $sessionId) {
api_not_allowed(true);
}
// Do not select the exercise ID if we come from the question pool
if (!$fromExercise) {
// gets the right exercise ID, and if 0 creates a new exercise
if (!$exerciseId = $objExercise->selectId()) {
$modifyExercise = 'yes';
}
}
$nbrQuestions = $objExercise->getQuestionCount();
// Question object creation.
if ($editQuestion || $newQuestion || $modifyQuestion || $modifyAnswers) {
if ($editQuestion || $newQuestion) {
// reads question data
if ($editQuestion) {
// question not found
if (!$objQuestion = Question::read($editQuestion)) {
api_not_allowed(true);
}
// saves the object into the session
Session::write('objQuestion', $objQuestion);
}
}
// checks if the object exists
if (is_object($objQuestion)) {
// gets the question ID
$questionId = $objQuestion->selectId();
}
}
// if cancelling an exercise
if ($cancelExercise) {
// existing exercise
if ($exerciseId) {
unset($modifyExercise);
} else {
// new exercise
// goes back to the exercise list
header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
exit();
}
}
// if cancelling question creation/modification
if ($cancelQuestion) {
// if we are creating a new question from the question pool
if (!$exerciseId && !$questionId) {
// goes back to the question pool
header('Location: question_pool.php?'.api_get_cidreq());
exit();
} else {
// goes back to the question viewing
$editQuestion = $modifyQuestion;
unset($newQuestion, $modifyQuestion);
}
}
// Cloning/copying a question
if (!empty($clone_question) && !empty($objExercise->iid)) {
$old_question_obj = Question::read($clone_question);
$old_question_obj->question = $old_question_obj->question.' - '.get_lang('Copy');
$new_id = $old_question_obj->duplicate(api_get_course_info());
$new_question_obj = Question::read($new_id);
$new_question_obj->addToList($exerciseId);
// Save category to the destination course
if (!empty($old_question_obj->category)) {
$new_question_obj->saveCategory($old_question_obj->category, api_get_course_int_id());
}
// This should be moved to the duplicate function
$new_answer_obj = new Answer($clone_question);
$new_answer_obj->read();
$new_answer_obj->duplicate($new_question_obj);
// Reloading tne $objExercise obj
$objExercise->read($objExercise->iid, false);
Display::addFlash(Display::return_message(get_lang('ItemCopied')));
header('Location: admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid.'&page='.$page);
exit;
}
// if cancelling answer creation/modification
if ($cancelAnswers) {
// goes back to the question viewing
$editQuestion = $modifyAnswers;
unset($modifyAnswers);
}
$nameTools = '';
// modifies the query string that is used in the link of tool name
if ($editQuestion || $modifyQuestion || $newQuestion || $modifyAnswers) {
$nameTools = get_lang('QuestionManagement');
}
if (api_is_in_gradebook()) {
$interbreadcrumb[] = [
'url' => Category::getUrl(),
'name' => get_lang('ToolGradebook'),
];
}
$interbreadcrumb[] = ['url' => 'exercise.php?'.api_get_cidreq(), 'name' => get_lang('Exercises')];
if (isset($_GET['newQuestion']) || isset($_GET['editQuestion'])) {
$interbreadcrumb[] = [
'url' => 'admin.php?exerciseId='.$objExercise->iid.'&'.api_get_cidreq(),
'name' => Security::remove_XSS($objExercise->selectTitle(true)),
];
} else {
$interbreadcrumb[] = [
'url' => '#',
'name' => Security::remove_XSS($objExercise->selectTitle(true)),
];
}
// shows a link to go back to the question pool
if (!$exerciseId && $nameTools != get_lang('ExerciseManagement')) {
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH)."exercise/question_pool.php?fromExercise=$fromExercise&".api_get_cidreq(),
'name' => get_lang('QuestionPool'),
];
}
// if the question is duplicated, disable the link of tool name
if ($modifyIn === 'thisExercise') {
if (!empty($buttonBack)) {
$modifyIn = 'allExercises';
}
}
$htmlHeadXtra[] = api_get_js('jqueryui-touch-punch/jquery.ui.touch-punch.min.js');
$htmlHeadXtra[] = api_get_js('jquery.jsPlumb.all.js');
$template = new Template();
$templateName = $template->get_template('exercise/submit.js.tpl');
$htmlHeadXtra[] = $template->fetch($templateName);
$htmlHeadXtra[] = api_get_js('d3/jquery.xcolor.js');
$htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/css/hotspot.css">';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>';
if (isset($_GET['message'])) {
if (in_array($_GET['message'], ['ExerciseStored', 'ItemUpdated', 'ItemAdded'])) {
Display::addFlash(Display::return_message(get_lang($_GET['message']), 'confirmation'));
}
}
Display::display_header($nameTools, 'Exercise');
// If we are in a test
$inATest = isset($exerciseId) && $exerciseId > 0;
if ($inATest) {
echo '<div class="actions">';
if (isset($_GET['hotspotadmin']) || isset($_GET['mad_admin']) || isset($_GET['newQuestion'])) {
echo '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), '', ICON_SIZE_MEDIUM).'</a>';
}
if (!isset($_GET['hotspotadmin']) && !isset($_GET['mad_admin']) && !isset($_GET['newQuestion']) && !isset($_GET['editQuestion'])) {
echo '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq().'">'.
Display::return_icon('back.png', get_lang('BackToExercisesList'), '', ICON_SIZE_MEDIUM).'</a>';
}
echo '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid.'&preview=1">'.
Display::return_icon('preview_view.png', get_lang('Preview'), '', ICON_SIZE_MEDIUM).'</a>';
echo Display::url(
Display::return_icon('test_results.png', get_lang('Results'), '', ICON_SIZE_MEDIUM),
api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid
);
echo '<a
href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise_admin.php?'.api_get_cidreq().'&modifyExercise=yes&exerciseId='.$objExercise->iid.'">'.
Display::return_icon('settings.png', get_lang('ModifyExercise'), '', ICON_SIZE_MEDIUM).'</a>';
// Get the cumulative max score of all questions to show it in the upper bar to the editor
$maxScoreAllQuestions = 0;
if ($showPagination === false) {
$questionList = $objExercise->selectQuestionList(true, $objExercise->random > 0 ? false : true);
if (!empty($questionList)) {
foreach ($questionList as $questionItemId) {
$question = Question::read($questionItemId);
if ($question) {
$maxScoreAllQuestions += $question->selectWeighting();
}
}
}
}
echo '</div>';
if ($objExercise->added_in_lp()) {
echo Display::return_message(get_lang('AddedToLPCannotBeAccessed'), 'warning');
}
// If the question is used in another test, show a warning to the editor
if ($editQuestion && $objQuestion->existsInAnotherExercise()) {
echo Display::return_message(
Display::returnFontAwesomeIcon('exclamation-triangle"')
.get_lang('ThisQuestionExistsInAnotherExercisesWarning'),
'warning',
false
);
}
$alert = '';
if ($showPagination === false) {
$originalSelectionType = $objExercise->questionSelectionType;
$objExercise->questionSelectionType = EX_Q_SELECTION_ORDERED;
$outMaxScore = 0;
$outMaxScore = array_reduce(
$objExercise->selectQuestionList(true, true),
function ($acc, $questionId) {
$objQuestionTmp = Question::read($questionId);
return $acc + $objQuestionTmp->selectWeighting();
},
0
);
$objExercise->questionSelectionType = $originalSelectionType;
$alert .= sprintf(
get_lang('XQuestionsWithTotalScoreY'),
$nbrQuestions,
$outMaxScore
);
}
if ($objExercise->random > 0) {
$alert .= '<br />'.sprintf(get_lang('OnlyXQuestionsPickedRandomly'), $objExercise->random);
$alert .= sprintf(
'<br>'.get_lang('XQuestionsSelectedWithTotalScoreY'),
$objExercise->random,
$maxScoreAllQuestions
);
}
if ($showPagination === false) {
if ($objExercise->questionSelectionType >= EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED) {
$alert .= sprintf(
'<br>'.get_lang('XQuestionsSelectedWithTotalScoreY'),
count($questionList),
$maxScoreAllQuestions
);
}
}
echo Display::return_message($alert, 'normal', false);
} elseif (isset($_GET['newQuestion'])) {
// we are in create a new question from question pool not in a test
echo '<div class="actions">';
echo '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), '', ICON_SIZE_MEDIUM).'</a>';
echo '</div>';
} else {
// If we are in question_pool but not in an test, go back to question create in pool
echo '<div class="actions">';
echo '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/question_pool.php?'.api_get_cidreq().'">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), '', ICON_SIZE_MEDIUM).
'</a>';
echo '</div>';
}
if ($newQuestion || $editQuestion) {
// Question management
$type = isset($_REQUEST['answerType']) ? Security::remove_XSS($_REQUEST['answerType']) : null;
echo '<input type="hidden" name="Type" value="'.$type.'" />';
if ($newQuestion === 'yes') {
$objExercise->edit_exercise_in_lp = true;
require 'question_admin.inc.php';
}
if ($editQuestion) {
// Question preview if teacher clicked the "switch to student"
if ($studentViewActive && $is_allowedToEdit) {
echo '<div class="main-question">';
echo Display::div(Security::remove_XSS($objQuestion->selectTitle()), ['class' => 'question_title']);
ExerciseLib::showQuestion(
$objExercise,
$editQuestion,
false,
null,
null,
false,
true,
false,
true,
true
);
echo '</div>';
} else {
require 'question_admin.inc.php';
ExerciseLib::showTestsWhereQuestionIsUsed($objQuestion->iid, $objExercise->selectId());
}
}
}
if (isset($_GET['hotspotadmin'])) {
if (!is_object($objQuestion)) {
$objQuestion = Question::read($_GET['hotspotadmin']);
}
if (!$objQuestion) {
api_not_allowed();
}
require 'hotspot_admin.inc.php';
}
if (isset($_GET['mad_admin'])) {
if (!is_object($objQuestion)) {
$objQuestion = Question::read($_GET['mad_admin']);
}
if (!$objQuestion) {
api_not_allowed();
}
require 'multiple_answer_dropdown_admin.php';
}
if (!$newQuestion && !$modifyQuestion && !$editQuestion && !isset($_GET['hotspotadmin']) && !isset($_GET['mad_admin'])) {
// question list management
require 'question_list_admin.inc.php';
}
// if we are in question authoring, display warning to user is feedback not shown at the end of the test -ref #6619
// this test to display only message in the question authoring page and not in the question list page too
if ($objExercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_EXAM) {
echo Display::return_message(get_lang('TestFeedbackNotShown'), 'normal');
}
Session::write('objQuestion', $objQuestion);
Session::write('objAnswer', $objAnswer);
Display::display_footer();

93
main/exercise/adminhp.php Normal file
View File

@@ -0,0 +1,93 @@
<?php
/* For licensing terms, see /license.txt */
/**
* HotPotatoes administration.
*
* @package chamilo.exercise
*
* @author Istvan Mandak
*/
require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_COURSES;
api_protect_course_script(true);
$_course = api_get_course_info();
if (isset($_REQUEST["cancel"])) {
if ($_REQUEST["cancel"] == get_lang('Cancel')) {
header("Location: exercise.php");
exit;
}
}
$newName = !empty($_REQUEST['newName']) ? $_REQUEST['newName'] : '';
$hotpotatoesName = !empty($_REQUEST['hotpotatoesName']) ? Security::remove_XSS($_REQUEST['hotpotatoesName']) : '';
$is_allowedToEdit = api_is_allowed_to_edit(null, true);
// document path
$documentPath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
// picture path
$picturePath = $documentPath.'/images';
// audio path
$audioPath = $documentPath.'/audio';
// Database table definitions
if (!$is_allowedToEdit) {
api_not_allowed(true);
}
if (api_is_in_gradebook()) {
$interbreadcrumb[] = [
'url' => Category::getUrl(),
'name' => get_lang('ToolGradebook'),
];
}
$interbreadcrumb[] = [
"url" => "exercise.php",
"name" => get_lang('Exercises'),
];
$nameTools = get_lang('adminHP');
Display::display_header($nameTools, "Exercise");
/** @todo probably wrong !!!! */
require_once api_get_path(SYS_CODE_PATH).'/exercise/hotpotatoes.lib.php';
?>
<h4>
<?php echo $nameTools; ?>
</h4>
<?php
if (isset($newName)) {
if ($newName != "") {
//alter database record for that test
SetComment($hotpotatoesName, $newName);
echo "<script> window.location='exercise.php'; </script>";
}
}
echo "<form action=\"".api_get_self()."\" method='post' name='form1'>";
echo "<input type=\"hidden\" name=\"hotpotatoesName\" value=\"$hotpotatoesName\">";
echo "<input type=\"text\" name=\"newName\" value=\"";
$lstrComment = '';
$lstrComment = GetComment($hotpotatoesName);
if ($lstrComment == '') {
$lstrComment = GetQuizName($hotpotatoesName, $documentPath);
}
if ($lstrComment == '') {
$lstrComment = basename($hotpotatoesName, $documentPath);
}
echo $lstrComment;
echo "\" size=40>&nbsp;";
echo "<button type=\"submit\" class=\"save\" name=\"submit\" value=\"".get_lang('Ok')."\">".get_lang('Ok')."</button>";
echo "<button type=\"button\" class=\"cancel\" name=\"cancel\" value=\"".get_lang('Cancel')."\" onclick=\"javascript:document.form1.newName.value='';\">".get_lang('Cancel')."</button>";
echo "</form>";
Display::display_footer();

55
main/exercise/aiken.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Code for Aiken import integration.
*
* @author Ronny Velasquez <ronny.velasquez@beeznest.com>
* @author César Perales <cesar.perales@gmail.com> Updated function names and import files for Aiken format support
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
$lib_path = api_get_path(LIBRARY_PATH);
$main_path = api_get_path(SYS_CODE_PATH);
require_once $main_path.'exercise/export/aiken/aiken_import.inc.php';
require_once $main_path.'exercise/export/aiken/aiken_classes.php';
// section (for the tabs)
$this_section = SECTION_COURSES;
// access restriction: only teachers are allowed here
if (!api_is_allowed_to_edit(null, true)) {
api_not_allowed();
}
$interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$is_allowedToEdit = api_is_allowed_to_edit(null, true);
if ((api_is_allowed_to_edit(null, true))) {
if (isset($_POST['submit'])) {
$id = aiken_import_file($_FILES['userFile']);
if (is_numeric($id) && !empty($id)) {
header('Location: admin.php?'.api_get_cidreq().'&exerciseId='.$id);
exit;
}
}
if (isset($_REQUEST['submit_aiken_generated'])) {
$id = aikenImportExercise(null, $_REQUEST);
if (is_numeric($id) && !empty($id)) {
header('Location: admin.php?'.api_get_cidreq().'&exerciseId='.$id);
exit;
}
}
}
Display::display_header(get_lang('ImportAikenQuiz'), 'Exercise');
aiken_display_form();
generateAikenForm();
Display::display_footer();

View File

@@ -0,0 +1,72 @@
<?php
/* For licensing terms, see /license.txt */
session_cache_limiter('none');
require_once __DIR__.'/../inc/global.inc.php';
$questionId = isset($_GET['question_id']) ? (int) $_GET['question_id'] : 0;
$exerciseId = isset($_GET['exe_id']) ? (int) $_GET['exe_id'] : 0;
$courseId = isset($_GET['course_id']) ? (int) $_GET['course_id'] : 0;
$courseInfo = api_get_course_info_by_id($courseId);
if (empty($courseInfo)) {
return '';
}
$objQuestion = Question::read($questionId, $courseInfo);
$documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
$picturePath = $documentPath.'/images';
$pictureSize = getimagesize($picturePath.'/'.$objQuestion->getPictureFilename());
$pictureWidth = $pictureSize[0];
$pictureHeight = $pictureSize[1];
$data = [
'use' => 'user',
'image' => [
'path' => $objQuestion->selectPicturePath(),
'width' => $pictureSize[0],
'height' => $pictureSize[1],
],
'answers' => [
'paths' => [],
'texts' => [],
],
];
$questionAttempt = Event::getQuestionAttemptByExeIdAndQuestion($exerciseId, $questionId);
if (!empty($questionAttempt['answer'])) {
$answers = explode('|', $questionAttempt['answer']);
foreach ($answers as $answer) {
$parts = explode(')(', $answer);
$typeProperties = array_shift($parts);
$properties = explode(';', $typeProperties);
switch ($properties[0]) {
case 'P':
$points = [];
foreach ($parts as $partPoint) {
$points[] = Geometry::decodePoint($partPoint);
}
$data['answers']['paths'][] = [
'color' => $properties[1] ?? '#FF0000',
'points' => $points,
];
break;
case 'T':
$text = [
'text' => array_shift($parts),
'color' => $properties[1] ?? '#FF0000',
'fontSize' => $properties[2] ?? '20',
];
$data['answers']['texts'][] = $text + Geometry::decodePoint($parts[0]);
break;
}
}
}
header('Content-Type: application/json');
echo json_encode($data);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,287 @@
<?php
/* For licensing terms, see /license.txt */
use Webit\Util\EvalMath\EvalMath;
/**
* Class CalculatedAnswer
* This class contains calculated answer form and answer processing functions.
*
* @author Imanol Losada
*/
class CalculatedAnswer extends Question
{
public $typePicture = 'calculated_answer.png';
public $explanationLangVar = 'CalculatedAnswer';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = CALCULATED_ANSWER;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$defaults = [];
$defaults['answer'] = get_lang('DefaultTextInBlanks');
if (!empty($this->iid)) {
$objAnswer = new Answer($this->iid);
$preArray = explode('@@', $objAnswer->selectAnswer(1));
$defaults['formula'] = array_pop($preArray);
$defaults['answer'] = array_shift($preArray);
$defaults['answer'] = preg_replace("/\[.*\]/", '', $defaults['answer']);
$defaults['weighting'] = $this->weighting;
}
$lowestValue = '1.00';
$highestValue = '20.00';
// javascript //
echo '<script>
function parseTextNumber(textNumber, floatValue) {
if (textNumber.indexOf(".") > -1) {
textNumber = parseFloat(textNumber);
floatValue.exists = "true";
} else {
textNumber = parseInt(textNumber);
}
return textNumber;
}
function updateRandomValue(element) {
// "floatValue" helps to distinguish between an integer (10) and a float with all 0 decimals (10.00)
var floatValue = { exists: "false" };
var index = (element.name).match(/\[[^\]]*\]/g);
var lowestValue = parseTextNumber(document.getElementById("lowestValue"+index).value, floatValue);
var highestValue = parseTextNumber(document.getElementById("highestValue"+index).value, floatValue);
var result = Math.random() * (highestValue - lowestValue) + lowestValue;
if (floatValue.exists == "true") {
result = parseFloat(result).toFixed(2);
} else {
result = parseInt(result);
}
document.getElementById("randomValue"+index).innerHTML = "'.get_lang("ExampleValue").': " + result;
}
CKEDITOR.on("instanceCreated", function(e) {
if (e.editor.name === "answer") {
e.editor.on("change", updateBlanks);
}
});
var firstTime = true;
function updateBlanks(e) {
if (firstTime) {
field = document.getElementById("answer");
var answer = field.value;
} else {
var answer = e.editor.getData();
}
var blanks = answer.match(/\[[^\]]*\]/g);
var fields = "<div class=\"form-group\"><label class=\"col-sm-2\">'.get_lang('VariableRanges').'</label><div class=\"col-sm-8\"><table>";
if (blanks!=null) {
if (typeof updateBlanks.randomValues === "undefined") {
updateBlanks.randomValues = [];
}
for (i=0 ; i<blanks.length ; i++){
if (document.getElementById("lowestValue["+i+"]") && document.getElementById("highestValue["+i+"]")) {
lowestValue = document.getElementById("lowestValue["+i+"]").value;
highestValue = document.getElementById("highestValue["+i+"]").value;
} else {
lowestValue = '.$lowestValue.'.toFixed(2);
highestValue = '.$highestValue.'.toFixed(2);
for (j=0; j<blanks.length; j++) {
updateBlanks.randomValues[j] = parseFloat(Math.random() * (highestValue - lowestValue) + lowestValue).toFixed(2);
}
}
fields += "<tr><td><label>"+blanks[i]+"</label></td><td><input class=\"span1\" style=\"margin-left: 0em;\" size=\"5\" value=\""+lowestValue+"\" type=\"text\" id=\"lowestValue["+i+"]\" name=\"lowestValue["+i+"]\" onblur=\"updateRandomValue(this)\"/></td><td><input class=\"span1\" style=\"margin-left: 0em; width:80px;\" size=\"5\" value=\""+highestValue+"\" type=\"text\" id=\"highestValue["+i+"]\" name=\"highestValue["+i+"]\" onblur=\"updateRandomValue(this)\"/></td><td><label class=\"span3\" id=\"randomValue["+i+"]\"/>'.get_lang('ExampleValue').': "+updateBlanks.randomValues[i]+"</label></td></tr>";
}
}
document.getElementById("blanks_weighting").innerHTML = fields + "</table></div></div>";
if (firstTime) {
firstTime = false;
}
}
window.onload = updateBlanks;
</script>';
// answer
$form->addElement('label', null, '<br /><br />'.get_lang('TypeTextBelow').', '.get_lang('And').' '.get_lang('UseTagForBlank'));
$form->addElement(
'html_editor',
'answer',
Display::return_icon('fill_field.png'),
[
'id' => 'answer',
'onkeyup' => 'javascript: updateBlanks(this);',
],
[
'ToolbarSet' => 'TestQuestionDescription',
'Width' => '100%',
'Height' => '350',
]
);
$form->addRule('answer', get_lang('GiveText'), 'required');
$form->addRule('answer', get_lang('DefineBlanks'), 'regex', '/\[.*\]/');
$form->applyFilter('answer', 'attr_on_filter');
$form->addElement('label', null, get_lang('IfYouWantOnlyIntegerValuesWriteBothLimitsWithoutDecimals'));
$form->addElement('html', '<div id="blanks_weighting"></div>');
$notationListButton = Display::url(
get_lang('NotationList'),
api_get_path(WEB_CODE_PATH).'exercise/evalmathnotation.php',
[
'class' => 'btn btn-info ajax',
'data-title' => get_lang('NotationList'),
'_target' => '_blank',
]
);
$form->addElement(
'label',
null,
$notationListButton
);
$form->addElement('text', 'formula', [get_lang('Formula'), get_lang('FormulaExample')], ['id' => 'formula']);
$form->addRule('formula', get_lang('GiveFormula'), 'required');
$form->addElement('text', 'weighting', get_lang('Weighting'), ['id' => 'weighting']);
$form->setDefaults(['weighting' => '10']);
$form->addElement('text', 'answerVariations', get_lang('AnswerVariations'));
$form->addRule(
'answerVariations',
get_lang('GiveAnswerVariations'),
'required'
);
$form->setDefaults(['answerVariations' => '1']);
global $text;
// setting the save button here and not in the question class.php
$form->addButtonSave($text, 'submitQuestion');
if (!empty($this->iid)) {
$form->setDefaults($defaults);
} else {
if ($this->isContent == 1) {
$form->setDefaults($defaults);
}
}
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
if (!self::isAnswered()) {
$table = Database::get_course_table(TABLE_QUIZ_ANSWER);
Database::delete(
$table,
[
'question_id = ?' => [
$this->iid,
],
]
);
$answer = $form->getSubmitValue('answer');
$formula = $form->getSubmitValue('formula');
$lowestValues = $form->getSubmitValue('lowestValue');
$highestValues = $form->getSubmitValue('highestValue');
$answerVariations = $form->getSubmitValue('answerVariations');
$this->weighting = $form->getSubmitValue('weighting');
// Create as many answers as $answerVariations
for ($j = 0; $j < $answerVariations; $j++) {
$auxAnswer = $answer;
$auxFormula = $formula;
$nb = preg_match_all('/\[[^\]]*\]/', $auxAnswer, $blanks);
if ($nb > 0) {
for ($i = 0; $i < $nb; $i++) {
$blankItem = $blanks[0][$i];
// take random float values when one or both edge values have a decimal point
$randomValue =
(strpos($lowestValues[$i], '.') !== false ||
strpos($highestValues[$i], '.') !== false) ?
mt_rand($lowestValues[$i] * 100, $highestValues[$i] * 100) / 100 : mt_rand($lowestValues[$i], $highestValues[$i]);
$auxAnswer = str_replace($blankItem, $randomValue, $auxAnswer);
$auxFormula = str_replace($blankItem, $randomValue, $auxFormula);
}
$math = new EvalMath();
$result = $math->evaluate($auxFormula);
$result = number_format($result, 2, '.', '');
// Remove decimal trailing zeros
$result = rtrim($result, '0');
// If it is an integer (ends in .00) remove the decimal point
if (mb_substr($result, -1) === '.') {
$result = str_replace('.', '', $result);
}
// Attach formula
$auxAnswer .= " [".$result."]@@".$formula;
}
$this->save($exercise);
$objAnswer = new Answer($this->iid);
$objAnswer->createAnswer($auxAnswer, 1, '', $this->weighting, '');
$objAnswer->position = [];
$objAnswer->save();
}
}
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'"><tr>';
$header .= '<th>'.get_lang('Answer').'</th>';
if ($exercise->showExpectedChoice()) {
$header .= '<th>'.get_lang('YourChoice').'</th>';
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
$header .= '<th>'.get_lang('Status').'</th>';
}
$header .= '</tr>';
return $header;
}
/**
* Returns true if the current question has been attempted to be answered.
*
* @return bool
*/
public function isAnswered()
{
$table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
$result = Database::select(
'question_id',
$table,
[
'where' => [
'question_id = ? AND c_id = ?' => [
$this->iid,
$this->course['real_id'],
],
],
]
);
return empty($result) ? false : true;
}
}

147
main/exercise/category.php Normal file
View File

@@ -0,0 +1,147 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
if (api_get_configuration_value('allow_exercise_categories') === false) {
api_not_allowed();
}
api_protect_course_script();
if (!api_is_allowed_to_edit()) {
api_not_allowed(true);
}
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$courseId = api_get_course_int_id();
$url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_exercise_categories&c_id='.$courseId.'&'.api_get_cidreq();
$action = isset($_GET['action']) ? Security::remove_XSS($_GET['action']) : '';
$obj = new ExerciseCategoryManager();
$check = Security::check_token('request');
$token = Security::get_token();
//Add the JS needed to use the jqgrid
$htmlHeadXtra[] = api_get_jqgrid_js();
//The order is important you need to check the the $column variable in the model.ajax.php file
$columns = [
get_lang('Name'),
get_lang('Actions'),
];
// Column config
$column_model = [
[
'name' => 'name',
'index' => 'name',
'width' => '140',
'align' => 'left',
],
[
'name' => 'actions',
'index' => 'actions',
'width' => '40',
'align' => 'left',
'formatter' => 'action_formatter',
'sortable' => 'false',
],
];
// Autowidth
$extra_params['autowidth'] = 'true';
// height auto
$extra_params['height'] = 'auto';
$action_links = $obj->getJqgridActionLinks($token);
$htmlHeadXtra[] = '<script>
$(function() {
// grid definition see the $obj->display() function
'.Display::grid_js(
'categories',
$url,
$columns,
$column_model,
$extra_params,
[],
$action_links,
true
).'
});
</script>';
$url = api_get_self().'?'.api_get_cidreq();
switch ($action) {
case 'add':
$interbreadcrumb[] = ['url' => $url, 'name' => get_lang('Categories')];
$interbreadcrumb[] = ['url' => '#', 'name' => get_lang('Add')];
$form = $obj->return_form($url.'&action=add', 'add');
// The validation or display
if ($form->validate()) {
$values = $form->exportValues();
unset($values['id']);
$res = $obj->save($values);
if ($res) {
Display::addFlash(Display::return_message(get_lang('ItemAdded'), 'confirmation'));
}
header('Location: '.$url);
exit;
} else {
$content = '<div class="actions">';
$content .= '<a href="'.$url.'">'.
Display::return_icon('back.png', get_lang('Back'), '', ICON_SIZE_MEDIUM).'</a>';
$content .= '</div>';
$form->addElement('hidden', 'sec_token');
$form->setConstants(['sec_token' => $token]);
$content .= $form->returnForm();
}
break;
case 'edit':
$interbreadcrumb[] = ['url' => $url, 'name' => get_lang('Categories')];
$interbreadcrumb[] = ['url' => '#', 'name' => get_lang('Edit')];
$form = $obj->return_form($url.'&action=edit&id='.intval($_GET['id']), 'edit');
// The validation or display
if ($form->validate()) {
$values = $form->exportValues();
$res = $obj->update($values);
if ($res) {
Display::addFlash(Display::return_message(get_lang('ItemUpdated'), 'confirmation'));
}
header('Location: '.$url);
exit;
} else {
$content = '<div class="actions">';
$content .= '<a href="'.$url.'">'.
Display::return_icon('back.png', get_lang('Back'), '', ICON_SIZE_MEDIUM).'</a>';
$content .= '</div>';
$form->addElement('hidden', 'sec_token');
$form->setConstants(['sec_token' => $token]);
$content .= $form->returnForm();
}
break;
case 'delete':
$res = $obj->delete($_GET['id']);
if ($res) {
Display::addFlash(Display::return_message(get_lang('ItemDeleted'), 'confirmation'));
}
header('Location: '.$url);
exit;
break;
default:
$content = $obj->display();
break;
}
Display::display_header();
echo $content;

View File

@@ -0,0 +1,103 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_COURSES;
api_protect_course_script(true);
$isAllowedToEdit = api_is_allowed_to_edit(null, true);
if (!$isAllowedToEdit) {
api_not_allowed(true);
}
$exerciseId = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
$exportXls = isset($_GET['export_xls']) && !empty($_GET['export_xls']) ? (int) $_GET['export_xls'] : 0;
if (empty($exerciseId)) {
api_not_allowed(true);
}
$sessionId = api_get_session_id();
$exercise = new Exercise();
$result = $exercise->read($exerciseId);
if (empty($result)) {
api_not_allowed(true);
}
$nameTools = get_lang('ExerciseManagement');
$interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$interbreadcrumb[] = [
'url' => 'admin.php?exerciseId='.$exercise->iid.'&'.api_get_cidreq(),
'name' => $exercise->selectTitle(true),
];
$interbreadcrumb[] = [
'url' => 'exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exercise->iid,
'name' => get_lang('StudentScore'),
];
$courseId = api_get_course_int_id();
$courseInfo = api_get_course_info();
$table = new HTML_Table(['class' => 'table table-hover table-striped']);
$row = 0;
$column = 0;
$headers = [
get_lang('Group'),
get_lang('AverageScore'),
];
foreach ($headers as $header) {
$table->setHeaderContents($row, $column, $header);
$column++;
}
if ($exportXls) {
$tableXls[] = $headers;
}
$row++;
$scoreDisplay = new ScoreDisplay();
$groups = GroupManager::get_group_list(null, $courseInfo);
if (!empty($groups)) {
foreach ($groups as $group) {
$average = ExerciseLib::get_average_score($exerciseId, $courseId, $sessionId, $group['iid']);
$table->setCellContents($row, 0, $group['name']);
$averageToDisplay = $scoreDisplay->display_score([$average, 1], SCORE_AVERAGE);
$table->setCellContents($row, 1, $averageToDisplay);
if ($exportXls) {
$tableXls[] = [$group['name'], $averageToDisplay];
}
$row++;
}
}
if ($exportXls) {
$fileName = get_lang('ComparativeGroupReport').'_'.api_get_course_id().'_'.$exerciseId.'_'.api_get_local_time();
Export::arrayToXls($tableXls, $fileName);
exit;
}
Display::display_header($nameTools, 'Exercise');
$actions = '<a href="exercise_report.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
Display::return_icon(
'back.png',
get_lang('GoBackToQuestionList'),
'',
ICON_SIZE_MEDIUM
)
.'</a>';
$actions .= Display::url(
Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
'comparative_group_report.php?id='.$exerciseId.'&export_xls=1&'.api_get_cidreq()
);
$actions = Display::div($actions, ['class' => 'actions']);
echo $actions;
echo $table->toHtml();
Display::display_footer();

View File

@@ -0,0 +1,51 @@
<?php
/* For licensing terms, see /license.txt */
require_once "../inc/global.inc.php";
echo "<pre>".
get_lang('SummationPlus')."\n".
get_lang('SubstractionMinus')."\n".
get_lang('MultiplicationStar')."\n".
get_lang('DivisionSlash')."\n".
get_lang('ExponentiationCircumflex')."\n".
get_lang('ModuloPercentage')."\n".
"\n".
get_lang('SquareRootSqrt')."\n".
get_lang('AbsoluteValueAbs')."\n".
get_lang('NaturalLogarithmLn')."\n".
get_lang('LogarithmLog')."\n".
get_lang('ENumberE')."\n".
get_lang('PiNumberPi')."\n".
"\n".
get_lang('SineSin')."\n".
get_lang('HyperbolicSineSinh')."\n".
get_lang('ArcsineArcsin')."\n".
get_lang('HyperbolicArcsineArcsinh')."\n".
"\n".
get_lang('CosineCos')."\n".
get_lang('HyperbolicCosineCosh')."\n".
get_lang('ArccosineArccos')."\n".
get_lang('HyperbolicArccosineArccosh')."\n".
"\n".
get_lang('TangentTan')."\n".
get_lang('HyperbolicTangentTanh')."\n".
get_lang('ArctangentArctan')."\n".
get_lang('HyperbolicArctangentArctanh')."\n".
"\n".
get_lang('CotangentCot')."\n".
get_lang('HyperbolicCotangentCoth')."\n".
get_lang('ArccotangentArccot')."\n".
get_lang('HyperbolicArccotangentArccoth')."\n".
"\n".
get_lang('SecantSec')."\n".
get_lang('HyperbolicSecantSech')."\n".
get_lang('ArcsecantArcsec')."\n".
get_lang('HyperbolicArcsecantArcsech')."\n".
"\n".
get_lang('CosecantCsc')."\n".
get_lang('HyperbolicCosecantCsch')."\n".
get_lang('ArccosecantArccsc')."\n".
get_lang('HyperbolicArccosecantArccsch')."\n".
"\n".
"</pre>";

View File

@@ -0,0 +1,10 @@
<?php
// This file serves the only purpose of maintaining backwards compatibility
// with previous content of Chamilo that might have pointed directly to
// exercise.php as it was called before.
// The *previous* exercise.php was renamed to exercise.php, which is the file
// included here. All new links to the main exercises page should point
// directly to exercise.php. This redirection is enabled for 1.10.x (2015-04-21)
// The final goal of this file is to be removed in a few years time, if
// considered realistically not harmful
require __DIR__.'/exercise.php';

File diff suppressed because it is too large Load Diff

794
main/exercise/exercise.php Normal file
View File

@@ -0,0 +1,794 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Exercise list: This script shows the list of exercises for administrators and students.
*
* @author Olivier Brouckaert, original author
* @author Denes Nagy, HotPotatoes integration
* @author Wolfgang Schneider, code/html cleanup
* @author Julio Montoya <gugli100@gmail.com>, lots of cleanup + several improvements
* Modified by hubert.borderiou (question category)
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
// Setting the tabs
$this_section = SECTION_COURSES;
$htmlHeadXtra[] = api_get_asset('qtip2/jquery.qtip.min.js');
$htmlHeadXtra[] = api_get_css_asset('qtip2/jquery.qtip.min.css');
$htmlHeadXtra[] = '<script>
function sendNotificationToUsers() {
var sendTo = $("#toUsers").val().join(",");
var url = $("#urlTo").val() + sendTo;
$("#toUsers").find("option").remove().end().selectpicker("refresh");
$.ajax({
url: url,
dataType: "json"
}).done(function(response) {
$("#cm-tools").html(response.message);
}).always(function() {
$("#toUsers").find("option").remove().end().selectpicker("refresh");
$("#urlTo").val("");
});
}
function showUserToSendNotificacion(element) {
var url = $(element).data("link");
$("#toUsers").find("option").remove().end().selectpicker("refresh");
$("#urlTo").val("");
$.ajax({
url: url,
dataType: "json",
}).done(function(response) {
$("#toUsers").find("option").remove().end().selectpicker("refresh");
$.each(response,function(a,b){
$("#toUsers").append($("<option>", {
value: b.user_id,
text: b.user_name
}));
});
$("#urlTo").val($(element).data("link").replace("send_reminder","send_reminder_to") + "&users=")
$("#toUsers").selectpicker("refresh");
$("#NotificarUsuarios").modal()
});
}
</script>';
api_protect_course_script(true);
$limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
$allowDelete = Exercise::allowAction('delete');
$allowClean = Exercise::allowAction('clean_results');
$check = Security::get_existing_token('get');
$currentUrl = api_get_self().'?'.api_get_cidreq();
require_once 'hotpotatoes.lib.php';
/* Constants and variables */
$is_allowedToEdit = api_is_allowed_to_edit(null, true);
$is_tutor = api_is_allowed_to_edit(true);
$is_tutor_course = api_is_course_tutor();
$courseInfo = api_get_course_info();
$courseId = $courseInfo['real_id'];
$userInfo = api_get_user_info();
$userId = $userInfo['id'];
$sessionId = api_get_session_id();
$isDrhOfCourse = CourseManager::isUserSubscribedInCourseAsDrh(
$userId,
$courseInfo
);
$TBL_DOCUMENT = Database::get_course_table(TABLE_DOCUMENT);
$TBL_ITEM_PROPERTY = Database::get_course_table(TABLE_ITEM_PROPERTY);
$TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
$TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
$TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
// document path
$documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document';
// picture path
$picturePath = $documentPath.'/images';
// audio path
$audioPath = $documentPath.'/audio';
// hot potatoes
$uploadPath = DIR_HOTPOTATOES; //defined in main_api
$exercisePath = api_get_self();
$exfile = explode('/', $exercisePath);
$exfile = strtolower($exfile[count($exfile) - 1]);
$exercisePath = substr($exercisePath, 0, strpos($exercisePath, $exfile));
$exercisePath = $exercisePath.'exercise.php';
// Clear the exercise session
Exercise::cleanSessionVariables();
//General POST/GET/SESSION/COOKIES parameters recovery
$origin = api_get_origin();
$choice = isset($_REQUEST['choice']) ? Security::remove_XSS($_REQUEST['choice']) : null;
$hpchoice = isset($_REQUEST['hpchoice']) ? Security::remove_XSS($_REQUEST['hpchoice']) : null;
$exerciseId = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : null;
$file = isset($_REQUEST['file']) ? Database::escape_string($_REQUEST['file']) : null;
$learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
$learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
$categoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
$keyword = isset($_REQUEST['keyword']) ? Security::remove_XSS($_REQUEST['keyword']) : '';
if (api_is_in_gradebook()) {
$interbreadcrumb[] = [
'url' => Category::getUrl(),
'name' => get_lang('ToolGradebook'),
];
}
$nameTools = get_lang('Exercises');
// Simple actions
if ($is_allowedToEdit) {
switch ($action) {
case 'export_all_exercises_results':
$sessionId = api_get_session_id();
$courseId = api_get_course_int_id();
ExerciseLib::exportAllExercisesResultsZip($sessionId, $courseId);
break;
case 'clean_all_test':
if ($check) {
if (false === $allowClean) {
api_not_allowed(true);
}
// list of exercises in a course/session
// we got variable $courseId $courseInfo session api_get_session_id()
$exerciseList = ExerciseLib::get_all_exercises_for_course_id(
$courseInfo,
$sessionId,
$courseId,
false
);
$quantity_results_deleted = 0;
foreach ($exerciseList as $exeItem) {
// delete result for test, if not in a gradebook
$exercise_action_locked = api_resource_is_locked_by_gradebook($exeItem['iid'], LINK_EXERCISE);
if ($exercise_action_locked == false) {
$objExerciseTmp = new Exercise();
if ($objExerciseTmp->read($exeItem['iid'])) {
$quantity_results_deleted += $objExerciseTmp->cleanResults(true);
}
}
}
Display::addFlash(Display::return_message(
sprintf(
get_lang('XResultsCleaned'),
$quantity_results_deleted
),
'confirm'
));
header('Location: '.$currentUrl);
exit;
}
break;
case 'exportqti2':
if ($limitTeacherAccess && !api_is_platform_admin()) {
api_not_allowed(true);
}
require_once api_get_path(SYS_CODE_PATH).'exercise/export/qti2/qti2_export.php';
$export = export_exercise_to_qti($exerciseId, true);
$archive_path = api_get_path(SYS_ARCHIVE_PATH);
$temp_dir_short = api_get_unique_id();
$temp_zip_dir = $archive_path.$temp_dir_short;
if (!is_dir($temp_zip_dir)) {
mkdir($temp_zip_dir, api_get_permissions_for_new_directories());
}
$temp_zip_file = $temp_zip_dir.'/'.api_get_unique_id().'.zip';
$temp_xml_file = $temp_zip_dir.'/qti2export_'.$exerciseId.'.xml';
file_put_contents($temp_xml_file, $export);
$xmlReader = new XMLReader();
$xmlReader->open($temp_xml_file);
$xmlReader->setParserProperty(XMLReader::VALIDATE, true);
$isValid = $xmlReader->isValid();
if ($isValid) {
$zip_folder = new PclZip($temp_zip_file);
$zip_folder->add($temp_xml_file, PCLZIP_OPT_REMOVE_ALL_PATH);
$name = 'qti2_export_'.$exerciseId.'.zip';
DocumentManager::file_send_for_download($temp_zip_file, true, $name);
unlink($temp_zip_file);
unlink($temp_xml_file);
rmdir($temp_zip_dir);
exit; // otherwise following clicks may become buggy
} else {
Display::addFlash(Display::return_message(get_lang('ErrorWritingXMLFile'), 'error'));
header('Location: '.$currentUrl);
exit;
}
break;
case 'up_category':
case 'down_category':
$categoryIdFromGet = isset($_REQUEST['category_id_edit']) ? $_REQUEST['category_id_edit'] : 0;
$em = Database::getManager();
$repo = $em->getRepository('ChamiloCourseBundle:CExerciseCategory');
$category = $repo->find($categoryIdFromGet);
$currentPosition = $category->getPosition();
if ($action === 'up_category') {
$currentPosition--;
} else {
$currentPosition++;
}
$category->setPosition($currentPosition);
$em->persist($category);
$em->flush();
Display::addFlash(Display::return_message(get_lang('Updated')));
header('Location: '.$currentUrl);
exit;
break;
}
}
// Mass actions
if (!empty($action) && $is_allowedToEdit) {
$exerciseListToEdit = isset($_REQUEST['id']) ? $_REQUEST['id'] : 0;
if (!empty($exerciseListToEdit)) {
foreach ($exerciseListToEdit as $exerciseIdToEdit) {
$objExerciseTmp = new Exercise();
$result = $objExerciseTmp->read($exerciseIdToEdit);
if (empty($result)) {
continue;
}
switch ($action) {
case 'delete':
if ($allowDelete) {
if ($objExerciseTmp->sessionId == $sessionId) {
$objExerciseTmp->delete();
} else {
Display::addFlash(Display::return_message(sprintf(get_lang('ExerciseXNotDeleted'), $objExerciseTmp->name), 'error'));
}
}
break;
case 'visible':
if ($limitTeacherAccess && !api_is_platform_admin()) {
// Teacher change exercise
break;
}
// enables an exercise
if (empty($sessionId)) {
$objExerciseTmp->enable();
$objExerciseTmp->save();
} else {
if (!empty($objExerciseTmp->sessionId)) {
$objExerciseTmp->enable();
$objExerciseTmp->save();
}
}
api_item_property_update(
$courseInfo,
TOOL_QUIZ,
$objExerciseTmp->iid,
'visible',
$userId
);
break;
case 'invisible':
if ($limitTeacherAccess && !api_is_platform_admin()) {
// Teacher change exercise
break;
}
// enables an exercise
if (empty($sessionId)) {
$objExerciseTmp->disable();
$objExerciseTmp->save();
} else {
if (!empty($objExerciseTmp->sessionId)) {
$objExerciseTmp->disable();
$objExerciseTmp->save();
}
}
api_item_property_update(
$courseInfo,
TOOL_QUIZ,
$objExerciseTmp->iid,
'visible',
$userId
);
break;
}
}
Display::addFlash(Display::return_message(get_lang('Updated')));
header('Location: '.$currentUrl);
exit;
}
}
Event::event_access_tool(TOOL_QUIZ);
$logInfo = [
'tool' => TOOL_QUIZ,
'tool_id' => (int) $exerciseId,
'action' => isset($_REQUEST['learnpath_id']) ? 'learnpath_id' : '',
'action_details' => isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : '',
];
Event::registerLog($logInfo);
HotPotGCt($documentPath, 1, $userId);
// Only for administrator
if ($is_allowedToEdit) {
if (!empty($choice)) {
// single exercise choice
// construction of Exercise
$objExerciseTmp = new Exercise();
$exercise_action_locked = api_resource_is_locked_by_gradebook(
$exerciseId,
LINK_EXERCISE
);
if ($objExerciseTmp->read($exerciseId)) {
if ($check) {
switch ($choice) {
case 'enable_launch':
$objExerciseTmp->cleanCourseLaunchSettings();
$objExerciseTmp->enableAutoLaunch();
Display::addFlash(Display::return_message(get_lang('Updated')));
break;
case 'disable_launch':
$objExerciseTmp->cleanCourseLaunchSettings();
break;
case 'delete':
// deletes an exercise
if ($allowDelete) {
$deleteQuestions = api_get_configuration_value('quiz_question_delete_automatically_when_deleting_exercise') ? true : false;
$result = $objExerciseTmp->delete(false, $deleteQuestions);
if ($result) {
Display::addFlash(Display::return_message(get_lang('ExerciseDeleted'), 'confirmation'));
}
}
break;
case 'enable':
if ($limitTeacherAccess && !api_is_platform_admin()) {
// Teacher change exercise
break;
}
// Enables an exercise
if (empty($sessionId)) {
$objExerciseTmp->enable();
$objExerciseTmp->save();
} else {
if (!empty($objExerciseTmp->sessionId)) {
$objExerciseTmp->enable();
$objExerciseTmp->save();
}
}
api_item_property_update(
$courseInfo,
TOOL_QUIZ,
$objExerciseTmp->iid,
'visible',
$userId
);
Display::addFlash(Display::return_message(get_lang('VisibilityChanged'), 'confirmation'));
break;
case 'disable':
if ($limitTeacherAccess && !api_is_platform_admin()) {
// Teacher change exercise
break;
}
// disables an exercise
if (empty($sessionId)) {
$objExerciseTmp->disable();
$objExerciseTmp->save();
} else {
// Only change active if it belongs to a session
if (!empty($objExerciseTmp->sessionId)) {
$objExerciseTmp->disable();
$objExerciseTmp->save();
}
}
api_item_property_update(
$courseInfo,
TOOL_QUIZ,
$objExerciseTmp->iid,
'invisible',
$userId
);
Display::addFlash(Display::return_message(get_lang('VisibilityChanged'), 'confirmation'));
break;
case 'disable_results':
//disable the results for the learners
$objExerciseTmp->disable_results();
$objExerciseTmp->save();
Display::addFlash(Display::return_message(get_lang('ResultsDisabled'), 'confirmation'));
break;
case 'enable_results':
//disable the results for the learners
$objExerciseTmp->enable_results();
$objExerciseTmp->save();
Display::addFlash(Display::return_message(get_lang('ResultsEnabled'), 'confirmation'));
break;
case 'clean_results':
if (false === $allowClean) {
// Teacher change exercise
break;
}
// Clean student results
if (false == $exercise_action_locked) {
$quantity_results_deleted = $objExerciseTmp->cleanResults(true);
$title = $objExerciseTmp->selectTitle();
Display::addFlash(
Display::return_message(
$title.': '.sprintf(
get_lang('XResultsCleaned'),
$quantity_results_deleted
),
'confirmation'
)
);
}
break;
case 'copy_exercise': //copy an exercise
api_set_more_memory_and_time_limits();
$objExerciseTmp->copyExercise();
Display::addFlash(Display::return_message(
get_lang('ExerciseCopied'),
'confirmation'
));
break;
case 'send_reminder_to':
$toUsers = $_GET['users'] ?? null;
if (!empty($toUsers) && !empty($exerciseId)) {
$sessionId = isset($_GET['id_session']) ? (int) $_GET['id_session'] : 0;
$courseCode = $_GET['cidReq'] ?? null;
$courseId = api_get_course_int_id($courseCode);
$temo = [];
if (is_int(strpos($toUsers, 'X'))) {
// to all users
$temo = Exercise::getUsersInExercise(
$exerciseId,
$courseId,
$sessionId,
false,
[],
false
);
$toUsers = array_column($temo, 'user_id');
} else {
$toUsers = explode(',', $toUsers);
}
api_set_more_memory_and_time_limits();
Exercise::notifyUsersOfTheExercise(
$exerciseId,
$courseId,
$sessionId,
$toUsers
);
echo json_encode(
[
'message' => Display::return_message(
get_lang('AnnounceSentByEmail'), 'confirmation'
),
]
);
}
exit();
case 'send_reminder':
$users = Exercise::getUsersInExercise(
$objExerciseTmp->iid,
$courseId,
$sessionId
);
echo json_encode($users);
exit();
}
header('Location: '.$currentUrl);
exit;
}
}
// destruction of Exercise
unset($objExerciseTmp);
Security::clear_token();
}
if (!empty($hpchoice)) {
switch ($hpchoice) {
case 'delete':
if ($limitTeacherAccess && !api_is_platform_admin()) {
// Teacher change exercise
break;
}
// deletes an exercise
$imgparams = [];
$imgcount = 0;
GetImgParams($file, $documentPath, $imgparams, $imgcount);
$fld = GetFolderName($file);
for ($i = 0; $i < $imgcount; $i++) {
my_delete($documentPath.$uploadPath."/".$fld."/".$imgparams[$i]);
DocumentManager::updateDbInfo("delete", $uploadPath."/".$fld."/".$imgparams[$i]);
}
if (!is_dir($documentPath.$uploadPath."/".$fld."/")) {
my_delete($documentPath.$file);
DocumentManager::updateDbInfo("delete", $file);
} else {
if (my_delete($documentPath.$file)) {
DocumentManager::updateDbInfo("delete", $file);
}
}
/* hotpotatoes folder may contains several tests so
don't delete folder if not empty :
http://support.chamilo.org/issues/2165
*/
if (!(strstr($uploadPath, DIR_HOTPOTATOES) &&
!folder_is_empty($documentPath.$uploadPath."/".$fld."/"))
) {
my_delete($documentPath.$uploadPath."/".$fld."/");
}
break;
case 'enable': // enables an exercise
if ($limitTeacherAccess && !api_is_platform_admin()) {
// Teacher change exercise
break;
}
$newVisibilityStatus = '1'; //"visible"
$query = "SELECT iid FROM $TBL_DOCUMENT
WHERE c_id = $courseId AND path='".Database::escape_string($file)."'";
$res = Database::query($query);
$row = Database::fetch_array($res, 'ASSOC');
api_item_property_update(
$courseInfo,
TOOL_DOCUMENT,
$row['iid'],
'visible',
$userId
);
Display::addFlash(Display::return_message(get_lang('Updated')));
break;
case 'disable': // disables an exercise
if ($limitTeacherAccess && !api_is_platform_admin()) {
// Teacher change exercise
break;
}
$newVisibilityStatus = '0'; //"invisible"
$query = "SELECT iid FROM $TBL_DOCUMENT
WHERE c_id = $courseId AND path='".Database::escape_string($file)."'";
$res = Database::query($query);
$row = Database::fetch_array($res, 'ASSOC');
api_item_property_update(
$courseInfo,
TOOL_DOCUMENT,
$row['iid'],
'invisible',
$userId
);
break;
default:
break;
}
header('Location: '.$currentUrl);
exit;
}
}
if (!in_array($origin, ['learnpath', 'mobileapp'])) {
//so we are not in learnpath tool
Display::display_header($nameTools, 'Exercise');
if (isset($_GET['message']) && in_array($_GET['message'], ['ExerciseEdited'])) {
echo Display::return_message(get_lang('ExerciseEdited'), 'confirmation');
}
} else {
Display::display_reduced_header();
}
Display::display_introduction_section(TOOL_QUIZ);
// Selects $limit exercises at the same time
// maximum number of exercises on a same page
$limit = Exercise::PAGINATION_ITEMS_PER_PAGE;
HotPotGCt($documentPath, 1, $userId);
$token = Security::get_token();
if ($is_allowedToEdit && $origin !== 'learnpath') {
$actionsLeft = '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise_admin.php?'.api_get_cidreq().'">'.
Display::return_icon('new_exercice.png', get_lang('NewEx'), '', ICON_SIZE_MEDIUM).'</a>';
$actionsLeft .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/question_create.php?'.api_get_cidreq().'">'.
Display::return_icon('new_question.png', get_lang('AddQ'), '', ICON_SIZE_MEDIUM).'</a>';
if (api_get_configuration_value('allow_exercise_categories')) {
$actionsLeft .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/category.php?'.api_get_cidreq().'">';
$actionsLeft .= Display::return_icon('folder.png', get_lang('Category'), '', ICON_SIZE_MEDIUM);
$actionsLeft .= '</a>';
}
// Question category
$actionsLeft .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/tests_category.php?'.api_get_cidreq().'">';
$actionsLeft .= Display::return_icon('green_open.png', get_lang('QuestionCategory'), '', ICON_SIZE_MEDIUM);
$actionsLeft .= '</a>';
$actionsLeft .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/question_pool.php?'.api_get_cidreq().'">';
$actionsLeft .= Display::return_icon('database.png', get_lang('QuestionPool'), '', ICON_SIZE_MEDIUM);
$actionsLeft .= '</a>';
// end question category
$actionsLeft .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/hotpotatoes.php?'.api_get_cidreq().'">'.
Display::return_icon('import_hotpotatoes.png', get_lang('ImportHotPotatoesQuiz'), '', ICON_SIZE_MEDIUM).'</a>';
// link to import qti2 ...
$actionsLeft .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/qti2.php?'.api_get_cidreq().'">'.
Display::return_icon('import_qti2.png', get_lang('ImportQtiQuiz'), '', ICON_SIZE_MEDIUM).'</a>';
$actionsLeft .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/aiken.php?'.api_get_cidreq().'">'.
Display::return_icon('import_aiken.png', get_lang('ImportAikenQuiz'), '', ICON_SIZE_MEDIUM).'</a>';
$actionsLeft .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/upload_exercise.php?'.api_get_cidreq().'">'.
Display::return_icon('import_excel.png', get_lang('ImportExcelQuiz'), '', ICON_SIZE_MEDIUM).'</a>';
$cleanAll = null;
if ($allowClean) {
$cleanAll = Display::url(
Display::return_icon(
'clean_all.png',
get_lang('CleanAllStudentsResultsForAllTests'),
'',
ICON_SIZE_MEDIUM
),
'#',
[
'data-item-question' => addslashes(get_lang('AreYouSureToEmptyAllTestResults')),
'data-href' => api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq().'&action=clean_all_test&sec_token='.$token,
'data-toggle' => 'modal',
'data-target' => '#confirm-delete',
]
);
}
$actionsLeft .= Display::url(
Display::return_icon('export_pdf.png', get_lang('ExportAllExercisesAllResults'), [], ICON_SIZE_MEDIUM),
api_get_path(WEB_CODE_PATH).'exercise/export/export_exercise_results.php?'.api_get_cidreq()
);
if ($limitTeacherAccess) {
if (api_is_platform_admin()) {
$actionsLeft .= $cleanAll;
}
} else {
$actionsLeft .= $cleanAll;
}
// Create a search-box
$form = new FormValidator('search_simple', 'get', $currentUrl, null, null, FormValidator::LAYOUT_INLINE);
$form->addCourseHiddenParams();
if (api_get_configuration_value('allow_exercise_categories')) {
$manager = new ExerciseCategoryManager();
$options = $manager->getCategoriesForSelect(api_get_course_int_id());
if (!empty($options)) {
$form->addSelect(
'category_id',
get_lang('Category'),
$options,
['placeholder' => get_lang('SelectAnOption'), 'disable_js' => true]
);
}
}
$form->addText(
'keyword',
get_lang('Search'),
false,
[
'aria-label' => get_lang('Search'),
]
);
$form->addButtonSearch(get_lang('Search'));
$actionsRight = $form->returnForm();
}
if ($is_allowedToEdit) {
echo Display::toolbarAction(
'toolbarUser',
[$actionsLeft, '', $actionsRight],
[6, 1, 5]
);
}
if (api_get_configuration_value('allow_exercise_categories') === false) {
echo Exercise::exerciseGrid(0, $keyword);
} else {
if (empty($categoryId)) {
echo Exercise::exerciseGrid(0, $keyword);
$counter = 0;
$manager = new ExerciseCategoryManager();
$categories = $manager->getCategories($courseId);
$modifyUrl = api_get_self().'?'.api_get_cidreq();
$total = count($categories);
$upIcon = Display::return_icon('up.png', get_lang('MoveUp'));
$downIcon = Display::return_icon('down.png', get_lang('MoveDown'));
/** @var \Chamilo\CourseBundle\Entity\CExerciseCategory $category */
foreach ($categories as $category) {
$categoryIdItem = $category->getId();
$up = '';
$down = '';
if ($is_allowedToEdit) {
$up = Display::url($upIcon, $modifyUrl.'&action=up_category&category_id_edit='.$categoryIdItem);
if (0 === $counter) {
$up = Display::url(Display::return_icon('up_na.png'), '#');
}
$down = Display::url($downIcon, $modifyUrl.'&action=down_category&category_id_edit='.$categoryIdItem);
$counter++;
if ($total === $counter) {
$down = Display::url(Display::return_icon('down_na.png'), '#');
}
}
echo Display::page_subheader($category->getName().$up.$down);
echo Exercise::exerciseGrid($category->getId(), $keyword);
}
} else {
$manager = new ExerciseCategoryManager();
$category = $manager->get($categoryId);
echo Display::page_subheader($category['name']);
echo Exercise::exerciseGrid($category['iid'], $keyword);
}
}
echo '<div class="modal fade" id="NotificarUsuarios" tabindex="-1" role="dialog" aria-labelledby="myModalLabel">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="'.get_lang('Close').'">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form action="#" class="form-horizontal">
<div class="row">
<div class="col-md-6" id="myModalLabel">'.get_lang('EmailNotifySubscription').'</div>
<div class="col-md-6">
<select class="selectpicker form-control" multiple="multiple" id="toUsers" name="toUsers">
<option value="">-</option>
</select>
</div>
</div>
<input class="hidden" id="urlTo" type="hidden">
</form>
<div class="clearfix clear-fix"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" onclick="sendNotificationToUsers()" data-dismiss="modal">'
.get_lang('SendMailToUsers').'
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">'.get_lang('Close').'</button>
</div>
</div>
</div>
</div>';
if ('learnpath' !== $origin) {
// We are not in learnpath tool
Display::display_footer();
}

View File

@@ -0,0 +1,224 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* Exercise administration
* This script allows to manage an exercise. It is included from
* the script admin.php.
*
* @author Olivier Brouckaert, Julio Montoya
*/
require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_COURSES;
api_protect_course_script(true);
if (!api_is_allowed_to_edit(null, true)) {
api_not_allowed(true);
}
$htmlHeadXtra[] = '<script>
function activate_start_date() {
if(document.getElementById(\'start_date_div\').style.display == \'none\') {
document.getElementById(\'start_date_div\').style.display = \'block\';
} else {
document.getElementById(\'start_date_div\').style.display = \'none\';
}
}
function activate_end_date() {
if(document.getElementById(\'end_date_div\').style.display == \'none\') {
document.getElementById(\'end_date_div\').style.display = \'block\';
} else {
document.getElementById(\'end_date_div\').style.display = \'none\';
}
}
function feedbackselection() {
var index = document.exercise_admin.exerciseFeedbackType.selectedIndex;
if (index == \'1\') {
document.exercise_admin.exerciseType[1].checked=true;
document.exercise_admin.exerciseType[0].disabled=true;
} else {
document.exercise_admin.exerciseType[0].disabled=false;
}
}
function option_time_expired() {
if(document.getElementById(\'timercontrol\').style.display == \'none\') {
document.getElementById(\'timercontrol\').style.display = \'block\';
} else {
document.getElementById(\'timercontrol\').style.display = \'none\';
}
}
function check_per_page_one() {
//document.getElementById(\'exerciseType_0\').checked=true;
}
function check_per_page_all() {
if (document.getElementById(\'exerciseType_1\') && document.getElementById(\'exerciseType_1\').checked) {
document.getElementById(\'exerciseType_0\').checked = true;
}
}
function check_feedback() {
if (document.getElementById(\'result_disabled_1\').checked == true) {
document.getElementById(\'result_disabled_0\').checked = true;
}
if (document.getElementById(\'exerciseType_0\').checked == true) {
document.getElementById(\'result_disabled_0\').checked = true;
}
}
function check_direct_feedback() {
document.getElementById(\'option_page_one\').checked = true;
document.getElementById(\'result_disabled_0\').checked = true;
}
function check_results_disabled() {
document.getElementById(\'exerciseType_2\').checked = true;
}
function disabledHideRandom() {
$("#hidden_random option:eq(0)").prop("selected", true);
$("#hidden_random").hide();
}
function checkQuestionSelection() {
var selection = $("#questionSelection option:selected").val();
switch (selection) {
case "'.EX_Q_SELECTION_ORDERED.'":
disabledHideRandom();
$("#hidden_matrix").hide();
break;
case "'.EX_Q_SELECTION_RANDOM.'":
$("#hidden_random").show();
$("#hidden_matrix").hide();
break;
case "'.EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED.'":
disabledHideRandom();
$("#hidden_matrix").show();
break;
case "per_categories":
$("#questionSelection option:eq('.EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_ORDERED.')").prop("selected", true);
disabledHideRandom();
$("#hidden_matrix").show();
break;
default:
disabledHideRandom();
$("#hidden_matrix").show();
break;
}
}
function setFocus(){
$("#exercise_title").focus();
}
// to correct #4029 Random and number of attempt menu empty added window.onload=advanced_parameters;
$(function() {
setFocus();
});
</script>';
$objExercise = new Exercise();
$course_id = api_get_course_int_id();
if (isset($_GET['exerciseId'])) {
$form = new FormValidator(
'exercise_admin',
'post',
api_get_self().'?'.api_get_cidreq().'&exerciseId='.intval($_GET['exerciseId'])
);
$objExercise->read($_GET['exerciseId'], false);
$form->addElement('hidden', 'edit', 'true');
} else {
$form = new FormValidator(
'exercise_admin',
'post',
api_get_self().'?'.api_get_cidreq()
);
$form->addElement('hidden', 'edit', 'false');
}
$objExercise->createForm($form);
if ($form->validate()) {
$objExercise->processCreation($form);
if ($form->getSubmitValue('edit') === 'true') {
Display::addFlash(
Display::return_message(get_lang('ExerciseEdited'), 'success')
);
} else {
Display::addFlash(
Display::return_message(get_lang('ExerciseAdded'), 'success')
);
}
$exercise_id = $objExercise->iid;
Session::erase('objExercise');
header('Location:admin.php?exerciseId='.$exercise_id.'&'.api_get_cidreq());
exit;
} else {
if (api_is_in_gradebook()) {
$interbreadcrumb[] = [
'url' => Category::getUrl(),
'name' => get_lang('ToolGradebook'),
];
}
$nameTools = get_lang('ExerciseManagement');
$interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$interbreadcrumb[] = [
'url' => 'admin.php?exerciseId='.$objExercise->iid.'&'.api_get_cidreq(),
'name' => $objExercise->selectTitle(true),
];
Display::display_header($nameTools, 'Exercise');
echo '<div class="actions">';
if ($objExercise->iid != 0) {
echo '<a href="admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid.'">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), '', ICON_SIZE_MEDIUM).'</a>';
} else {
if (!empty($_GET['lp_id']) || !empty($_POST['lp_id'])) {
if (!empty($_POST['lp_id'])) {
$lp_id = $_POST['lp_id'];
//TODO:this remains to be implemented after press the first post
} else {
$lp_id = $_GET['lp_id'];
}
$lp_id = (int) $lp_id;
echo "<a
href=\"../lp/lp_controller.php?".api_get_cidreq()."&gradebook=&action=add_item&type=step&lp_id=".$lp_id."#resource_tab-2\">".
Display::return_icon('back.png', get_lang("BackTo").' '.get_lang('LearningPaths'), '', ICON_SIZE_MEDIUM)."</a>";
} else {
echo '<a href="exercise.php?'.api_get_cidreq().'">'.
Display::return_icon('back.png', get_lang('BackToExercisesList'), '', ICON_SIZE_MEDIUM).
'</a>';
}
}
echo '</div>';
if (in_array($objExercise->getFeedbackType(), [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
echo Display::return_message(get_lang('DirectFeedbackCantModifyTypeQuestion'));
}
if (api_get_setting('search_enabled') === 'true' &&
!extension_loaded('xapian')
) {
echo Display::return_message(get_lang('SearchXapianModuleNotInstalled'), 'error');
}
// to hide the exercise description
echo '<style> .media { display:none;}</style>';
$form->display();
}
Display::display_footer();

View File

@@ -0,0 +1,137 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
api_protect_admin_script();
$TBL_EXERCISE_QUESTION = Database::get_course_table(TABLE_QUIZ_TEST_QUESTION);
$TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
$TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
$TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
$courseId = api_get_course_int_id();
$courseCode = api_get_course_id();
$data = [];
$students = CourseManager::get_student_list_from_course_code($courseCode);
$categories = TestCategory::getCategoryListInfo('', $courseId);
$table = Database::get_course_table(TABLE_QUIZ_TEST);
$sql = "SELECT iid, title FROM $table
WHERE c_id = $courseId AND active <> -1
ORDER by iid";
$result = Database::query($sql);
$exercises = Database::store_result($result);
$list = [];
$header = [];
$header[] = get_lang('Username');
$header[] = get_lang('FirstName');
$header[] = get_lang('LastName');
$header[] = get_lang('Email');
$header[] = get_lang('OfficialCode');
/** @var TestCategory $categoryInfo */
foreach ($categories as $categoryInfo) {
$header[] = 'Aciertos: '.$categoryInfo->name;
$header[] = 'Errores: '.$categoryInfo->name;
$header[] = 'Omisiones: '.$categoryInfo->name;
$header[] = 'Puntos: '.$categoryInfo->name;
}
foreach ($exercises as $exerciseInfo) {
$header[] = $exerciseInfo['title'];
}
$list[] = $header;
$objExercise = new Exercise();
foreach ($students as $studentInfo) {
$studentId = $studentInfo['user_id'];
$data = [];
$data[] = $studentInfo['username'];
$data[] = $studentInfo['lastname'];
$data[] = $studentInfo['firstname'];
$data[] = $studentInfo['email'];
$data[] = $studentInfo['official_code'];
$userExerciseData = [];
$categoryData = [];
foreach ($exercises as $exerciseInfo) {
$exerciseId = $exerciseInfo['iid'];
$objExercise->read($exerciseId);
$sql = "SELECT exe_id, data_tracking
FROM $TBL_TRACK_EXERCISES
WHERE
c_id = $courseId AND
exe_user_id = $studentId AND
exe_exo_id = $exerciseId AND
status = ''
LIMIT 1";
$result = Database::query($sql);
$attempt = Database::fetch_array($result, 'ASSOC');
if (empty($attempt)) {
$userExerciseData[$exerciseId] = null;
continue;
}
$exeId = $attempt['exe_id'];
ob_start();
$stats = ExerciseLib::displayQuestionListByAttempt(
$objExercise,
$exeId,
false,
'',
false,
true,
true
);
ob_end_clean();
foreach ($categories as $categoryInfo) {
if (!($categoryInfo instanceof TestCategory)) {
continue;
}
if (isset($stats['category_list'][$categoryInfo->iid])) {
$categoryItem = $stats['category_list'][$categoryInfo->iid];
if (!isset($categoryData[$categoryInfo->iid])) {
$categoryData[$categoryInfo->iid]['passed'] = 0;
$categoryData[$categoryInfo->iid]['wrong'] = 0;
$categoryData[$categoryInfo->iid]['no_answer'] = 0;
$categoryData[$categoryInfo->iid]['score'] = 0;
}
$categoryData[$categoryInfo->iid]['passed'] += $categoryItem['passed'];
$categoryData[$categoryInfo->iid]['wrong'] += $categoryItem['wrong'];
$categoryData[$categoryInfo->iid]['no_answer'] += $categoryItem['no_answer'];
$categoryData[$categoryInfo->iid]['score'] += $categoryItem['score'];
}
}
$userExerciseData[$exerciseId] = $stats['total_score'];
}
foreach ($categories as $categoryInfo) {
if (isset($categoryData[$categoryInfo->iid])) {
$data[] = $categoryData[$categoryInfo->iid]['passed'];
$data[] = $categoryData[$categoryInfo->iid]['wrong'];
$data[] = $categoryData[$categoryInfo->iid]['no_answer'];
$data[] = $categoryData[$categoryInfo->iid]['score'];
} else {
$data[] = null;
$data[] = null;
$data[] = null;
$data[] = null;
}
}
foreach ($exercises as $exerciseInfo) {
$exerciseId = $exerciseInfo['iid'];
if (isset($userExerciseData[$exerciseId])) {
$data[] = $userExerciseData[$exerciseId];
} else {
$data[] = null;
}
}
$list[] = $data;
}
$filePath = Export::arrayToCsv($list, get_lang('Report'), true);
DocumentManager::file_send_for_download($filePath, true, get_lang('Report').'.csv');

View File

@@ -0,0 +1,91 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Exercise list: This script shows the list of exercises for administrators and students.
*
* @author Olivier Brouckaert, original author
* @author Denes Nagy, HotPotatoes integration
* @author Wolfgang Schneider, code/html cleanup
*/
require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_COURSES;
api_protect_course_script(true);
$show = isset($_GET['show']) && $_GET['show'] === 'result' ? 'result' : 'test';
$is_allowedToEdit = api_is_allowed_to_edit(null, true);
$is_tutor = api_is_allowed_to_edit(true);
if (!$is_allowedToEdit) {
header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq());
exit;
}
$interbreadcrumb[] = [
'url' => 'exercise_report.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$interbreadcrumb[] = [
'url' => 'exercise_report.php?filter=2&'.api_get_cidreq(),
'name' => get_lang('StudentScore'),
];
$interbreadcrumb[] = [
'url' => 'exercise_history.php?exe_id='.intval($_GET['exe_id']).'&'.api_get_cidreq(),
'name' => get_lang('Details'),
];
$TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
$TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
$TBL_EXERCISES_QUESTION = Database::get_course_table(TABLE_QUIZ_QUESTION);
$TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
Display::display_header($nameTools, 'Exercise');
if (isset($_GET['message'])) {
if (in_array($_GET['message'], ['ExerciseEdited'])) {
$my_message_history = Security::remove_XSS($_GET['message']);
echo Display::return_message(get_lang($my_message_history), 'confirm');
}
}
echo '<div class="actions">';
echo '<a href="exercise_report.php?'.api_get_cidreq().'&filter=2">'.
Display::return_icon('back.png', get_lang('BackToResultList'), '', ICON_SIZE_MEDIUM).'</a>';
echo '</div>';
?>
<table class="table table-hover table-striped data_table">
<tr>
<th><?php echo get_lang('Question'); ?></th>
<th width="50px"><?php echo get_lang('Value'); ?></th>
<th><?php echo get_lang('Feedback'); ?></th>
<th><?php echo get_lang('Author'); ?></th>
<th width="160px"><?php echo get_lang('Date'); ?></th>
</tr>
<?php
$sql = "SELECT *, quiz_question.question, firstname, lastname
FROM $TBL_TRACK_ATTEMPT_RECORDING t, $TBL_USER,
$TBL_EXERCISES_QUESTION quiz_question
WHERE
quiz_question.iid = question_id AND
user_id = author AND
exe_id = '".(int) $_GET['exe_id']."'
ORDER BY position";
$query = Database::query($sql);
while ($row = Database::fetch_array($query)) {
echo '<tr>';
echo '<td>'.$row['question'].'</td>';
echo '<td>'.$row['marks'].'</td>';
if (!empty($row['teacher_comment'])) {
echo '<td>'.$row['teacher_comment'].'</td>';
} else {
echo '<td>'.get_lang('WithoutComment').'</td>';
}
echo '<td>'.(empty($row['firstname']) && empty($row['lastname']) ? '<i>'.get_lang('OriginalValue').'</i>' : api_get_person_name($row['firstname'], $row['lastname'])).'</td>';
echo '<td>'.api_convert_and_format_date($row['insert_date'], DATE_TIME_FORMAT_LONG).'</td>';
echo '</tr>';
}
echo '</table>';
Display::display_footer();

View File

@@ -0,0 +1,223 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
if (false === api_get_configuration_value('block_category_questions')) {
api_not_allowed(true);
}
$this_section = SECTION_COURSES;
api_protect_course_script(true);
$origin = api_get_origin();
$learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
$learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
$learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
$exerciseId = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : 0;
$currentQuestion = isset($_REQUEST['num']) ? (int) $_REQUEST['num'] : 1;
$exeId = isset($_REQUEST['exe_id']) ? (int) $_REQUEST['exe_id'] : 0;
$questionCategoryId = isset($_REQUEST['category_id']) ? (int) $_REQUEST['category_id'] : 0;
$validateCategory = isset($_REQUEST['validate']) && 1 === (int) $_REQUEST['validate'];
/** @var Exercise $objExercise */
$objExercise = null;
$exerciseInSession = Session::read('objExercise');
if (!empty($exerciseInSession)) {
$objExercise = $exerciseInSession;
}
$category = new TestCategory();
$categoryObj = $category->getCategory($questionCategoryId);
if (empty($objExercise) || empty($questionCategoryId) || empty($exeId) || empty($categoryObj)) {
api_not_allowed(true);
}
$categoryId = (int) $categoryObj->iid;
$params = "exe_id=$exeId&exerciseId=$exerciseId&learnpath_id=$learnpath_id&learnpath_item_id=$learnpath_item_id&learnpath_item_view_id=$learnpath_item_view_id&".api_get_cidreq();
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'.$params;
$validateUrl = api_get_path(WEB_CODE_PATH).'exercise/exercise_question_reminder.php?'.
$params.'&category_id='.$categoryId.'&validate=1';
$time_control = false;
$clock_expired_time = ExerciseLib::get_session_time_control_key(
$objExercise->iid,
$learnpath_id,
$learnpath_item_id
);
if ($objExercise->expired_time != 0 && !empty($clock_expired_time)) {
$time_control = true;
}
if ($time_control) {
// Get time left for expiring time
$time_left = api_strtotime($clock_expired_time, 'UTC') - time();
$htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/stylesheet/jquery.epiclock.css');
$htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/renderers/minute/epiclock.minute.css');
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.dateformat.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.epiclock.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/renderers/minute/epiclock.minute.js');
$htmlHeadXtra[] = $objExercise->showTimeControlJS($time_left);
}
$htmlHeadXtra[] = api_get_css_asset('pretty-checkbox/dist/pretty-checkbox.min.css');
$trackInfo = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
if (empty($trackInfo)) {
api_not_allowed();
}
$blockedCategories = [];
if (isset($trackInfo['blocked_categories']) && !empty($trackInfo['blocked_categories'])) {
$blockedCategories = explode(',', $trackInfo['blocked_categories']);
}
if ($validateCategory) {
$blockedCategories[] = $categoryId;
$blockedCategories = array_unique($blockedCategories);
$value = implode(',', $blockedCategories);
$value = Database::escape_string($value);
$table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
$sql = "UPDATE $table
SET blocked_categories = '$value'
WHERE exe_id = $exeId";
Database::query($sql);
// Cleaning old remind list.
$objExercise->removeAllQuestionToRemind($exeId);
api_location($url.'&num='.$currentQuestion);
}
$nameTools = get_lang('Exercises');
$interbreadcrumb[] = ['url' => 'exercise.php?'.api_get_cidreq(), 'name' => get_lang('Exercises')];
$hideHeaderAndFooter = in_array($origin, ['learnpath', 'embeddable', 'iframe']);
if (!$hideHeaderAndFooter) {
Display::display_header($nameTools, 'Exercise');
} else {
Display::display_reduced_header();
}
// I'm in a preview mode as course admin. Display the action menu.
if (!$hideHeaderAndFooter && api_is_course_admin()) {
echo '<div class="actions">';
echo '<a href="admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid.'">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), [], 32).'</a>';
echo '<a href="exercise_admin.php?'.api_get_cidreq().'&modifyExercise=yes&exerciseId='.$objExercise->iid.'">'.
Display::return_icon('edit.png', get_lang('ModifyExercise'), [], 32).'</a>';
echo '</div>';
}
echo Display::page_header($categoryObj->name);
echo '<p>'.Security::remove_XSS($categoryObj->description).'</p>';
echo '<p>'.get_lang('BlockCategoryExplanation').'</p>';
$categoryList = Session::read('categoryList');
$disableAllQuestions = '';
$questionList = [];
if (isset($categoryList[$categoryId])) {
$questionList = $categoryList[$categoryId];
}
if ($objExercise->review_answers) {
$disableAllQuestions = 'changeOptionStatus(0);';
echo $objExercise->getReminderTable($questionList, $trackInfo);
}
if ($time_control) {
echo $objExercise->returnTimeLeftDiv();
}
echo Display::div('', ['id' => 'message']);
$previousQuestion = $currentQuestion - 1;
$nextQuestion = $currentQuestion + 1;
if (!empty($questionList)) {
$firstQuestionOfCategory = end($questionList);
$dataTracking = explode(',', $trackInfo['data_tracking']);
$index = 0;
foreach ($dataTracking as $index => $question) {
if ($firstQuestionOfCategory == $question) {
break;
}
}
$nextQuestion = $index + 1;
}
echo '<script>
function goBack() {
window.location = "'.$url.'&num='.$previousQuestion.'";
}
function continueExercise() {
'.$disableAllQuestions.'
window.location = "'.$validateUrl.'&num='.$nextQuestion.'";
}
function final_submit() {
window.location = "'.api_get_path(WEB_CODE_PATH).'exercise/exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&" + lp_data;
}
</script>';
$exercise_result = $objExercise->getUserAnswersSavedInExercise($exeId);
echo '<div class="clear"></div><br />';
$table = '';
$counter = 0;
echo Display::div($table, ['class' => 'question-check-test']);
$exerciseActions = '';
if (!in_array($categoryId, $blockedCategories)) {
$exerciseActions = '&nbsp;'.Display::url(
get_lang('GoBack'),
'javascript://',
['onclick' => 'goBack();', 'class' => 'btn btn-default']
);
}
if ($objExercise->review_answers) {
$exerciseActions .= Display::url(
get_lang('ReviewQuestions'),
'javascript://',
['onclick' => 'reviewQuestions();', 'class' => 'btn btn-primary']
);
$exerciseActions .= '&nbsp;'.Display::url(
get_lang('SelectAll'),
'javascript://',
['onclick' => 'selectAll();', 'class' => 'btn btn-default']
);
$exerciseActions .= '&nbsp;'.Display::url(
get_lang('UnSelectAll'),
'javascript://',
['onclick' => 'changeOptionStatus(0);', 'class' => 'btn btn-default']
);
}
end($categoryList);
// This is the last category
if (key($categoryList) === $categoryId) {
$exerciseActions .= '&nbsp;'.Display::url(
get_lang('EndTest'),
'javascript://',
['onclick' => 'final_submit();', 'class' => 'btn btn-warning']
);
} else {
$exerciseActions .= '&nbsp;'.Display::url(
get_lang('ContinueTest'),
'javascript://',
['onclick' => 'continueExercise();', 'class' => 'btn btn-primary']
);
}
echo Display::div('', ['class' => 'clear']);
echo Display::div($exerciseActions, ['class' => 'form-actions']);
if (!$hideHeaderAndFooter) {
// We are not in learnpath tool or embeddable quiz
Display::display_footer();
} else {
Display::display_reduced_footer();
}

View File

@@ -0,0 +1,153 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* Exercise reminder overview
* Then it shows the results on the screen.
*
* @author Julio Montoya switchable fill in blank option added
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
$this_section = SECTION_COURSES;
api_protect_course_script(true);
$origin = api_get_origin();
$learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
$learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
$learnpath_item_view_id = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : 0;
$exerciseId = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : 0;
/** @var Exercise $objExercise */
$objExercise = null;
$exerciseInSession = Session::read('objExercise');
if (!empty($exerciseInSession)) {
$objExercise = $exerciseInSession;
}
if (!$objExercise) {
// Redirect to the exercise overview
// Check if the exe_id exists
header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/overview.php?exerciseId='.$exerciseId.'&'.api_get_cidreq());
exit;
}
$time_control = false;
$clock_expired_time = ExerciseLib::get_session_time_control_key(
$objExercise->iid,
$learnpath_id,
$learnpath_item_id
);
if ($objExercise->expired_time != 0 && !empty($clock_expired_time)) {
$time_control = true;
}
if ($time_control) {
// Get time left for expiring time
$time_left = api_strtotime($clock_expired_time, 'UTC') - time();
$htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/stylesheet/jquery.epiclock.css');
$htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/renderers/minute/epiclock.minute.css');
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.dateformat.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.epiclock.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/renderers/minute/epiclock.minute.js');
$htmlHeadXtra[] = $objExercise->showTimeControlJS($time_left);
}
$htmlHeadXtra[] = api_get_css_asset('pretty-checkbox/dist/pretty-checkbox.min.css');
$exe_id = 0;
if (isset($_GET['exe_id'])) {
$exe_id = (int) $_GET['exe_id'];
Session::write('exe_id', $exe_id);
}
$exe_id = (int) Session::read('exe_id');
$exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exe_id);
$question_list = [];
if (!empty($exercise_stat_info['data_tracking'])) {
$question_list = explode(',', $exercise_stat_info['data_tracking']);
}
if (empty($exercise_stat_info) || empty($question_list) || $exercise_stat_info['exe_user_id'] != api_get_user_id()) {
api_not_allowed();
}
$nameTools = get_lang('Exercises');
$interbreadcrumb[] = ['url' => 'exercise.php?'.api_get_cidreq(), 'name' => get_lang('Exercises')];
$hideHeaderAndFooter = in_array($origin, ['learnpath', 'embeddable', 'iframe']);
if (!$hideHeaderAndFooter) {
Display::display_header($nameTools, 'Exercise');
} else {
Display::display_reduced_header();
}
// I'm in a preview mode as course admin. Display the action menu.
if (!$hideHeaderAndFooter && api_is_course_admin()) {
echo '<div class="actions">';
echo '<a href="admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid.'">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), [], 32).'</a>';
echo '<a href="exercise_admin.php?'.api_get_cidreq().'&modifyExercise=yes&exerciseId='.$objExercise->iid.'">'.
Display::return_icon('edit.png', get_lang('ModifyExercise'), [], 32).'</a>';
echo '</div>';
}
echo Display::page_header(get_lang('QuestionsToReview'));
if ($time_control) {
echo $objExercise->returnTimeLeftDiv();
}
$selectionType = $objExercise->getQuestionSelectionType();
if (api_get_configuration_value('block_category_questions') &&
ONE_PER_PAGE == $objExercise->type &&
EX_Q_SELECTION_CATEGORIES_ORDERED_QUESTIONS_RANDOM == $selectionType
) {
$extraFieldValue = new ExtraFieldValue('exercise');
$extraFieldData = $extraFieldValue->get_values_by_handler_and_field_variable($objExercise->iid, 'block_category');
if ($extraFieldData && isset($extraFieldData['value']) && 1 === (int) $extraFieldData['value']) {
// get last category question list
$categoryList = Session::read('categoryList');
$question_list = end($categoryList);
}
}
echo $objExercise->getReminderTable($question_list, $exercise_stat_info);
$exerciseActions = Display::url(
get_lang('ReviewQuestions'),
'javascript://',
['onclick' => 'reviewQuestions();', 'class' => 'btn btn-primary']
);
$exerciseActions .= '&nbsp;'.Display::url(
get_lang('SelectAll'),
'javascript://',
['onclick' => 'changeOptionStatus(1);', 'class' => 'btn btn-default']
);
$exerciseActions .= '&nbsp;'.Display::url(
get_lang('UnSelectAll'),
'javascript://',
['onclick' => 'changeOptionStatus(0);', 'class' => 'btn btn-default']
);
$exerciseActions .= '&nbsp;'.Display::url(
get_lang('EndTest'),
'javascript://',
['onclick' => 'final_submit();', 'class' => 'btn btn-warning']
);
echo Display::div('', ['class' => 'clear']);
echo Display::div($exerciseActions, ['class' => 'form-actions']);
if (!$hideHeaderAndFooter) {
// We are not in learnpath tool or embeddable quiz
Display::display_footer();
} else {
Display::display_reduced_footer();
}

View File

@@ -0,0 +1,998 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Exercise list: This script shows the list of exercises for administrators and students.
*
* @author Julio Montoya <gugli100@gmail.com> jqgrid integration
* Modified by hubert.borderiou (question category)
*
* @todo fix excel export
*/
require_once __DIR__.'/../inc/global.inc.php';
// Setting the tabs
$this_section = SECTION_COURSES;
$htmlHeadXtra[] = api_get_jqgrid_js();
$filter_user = isset($_REQUEST['filter_by_user']) ? (int) $_REQUEST['filter_by_user'] : null;
$isBossOfStudent = false;
if (!empty($filter_user) && api_is_student_boss()) {
// Check if boss has access to user info.
if (UserManager::userIsBossOfStudent(api_get_user_id(), $filter_user)) {
$isBossOfStudent = true;
} else {
api_not_allowed(true);
}
} else {
api_protect_course_script(true, false, true);
}
$limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
$allowClean = Exercise::allowAction('clean_results');
if ($limitTeacherAccess && !api_is_platform_admin()) {
api_not_allowed(true);
}
require_once 'hotpotatoes.lib.php';
$_course = api_get_course_info();
// document path
$documentPath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
$origin = api_get_origin();
$is_allowedToEdit = api_is_allowed_to_edit(null, true) ||
api_is_drh() ||
api_is_student_boss() ||
api_is_session_admin();
$is_tutor = api_is_allowed_to_edit(true);
$TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
$TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
$TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
$TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
$allowCoachFeedbackExercises = api_get_setting('allow_coach_feedback_exercises') === 'true';
$course_id = api_get_course_int_id();
$exercise_id = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : 0;
$locked = api_resource_is_locked_by_gradebook($exercise_id, LINK_EXERCISE);
$sessionId = api_get_session_id();
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : null;
if ('export_all_exercises_results' !== $action && !(isset($_GET['delete']) && $_GET['delete'] === 'delete')) {
if (empty($exercise_id)) {
api_not_allowed(true);
}
}
$blockPage = true;
if (empty($sessionId)) {
if ($is_allowedToEdit) {
$blockPage = false;
}
} else {
if ($allowCoachFeedbackExercises && api_is_coach($sessionId, $course_id)) {
$blockPage = false;
} else {
if ($is_allowedToEdit) {
$blockPage = false;
}
}
}
if ($blockPage) {
api_not_allowed(true);
}
if (!empty($exercise_id)) {
$parameters['exerciseId'] = $exercise_id;
}
if (!empty($_GET['path'])) {
$parameters['path'] = Security::remove_XSS($_GET['path']);
}
if (!empty($_REQUEST['export_report']) && $_REQUEST['export_report'] == '1') {
if (api_is_platform_admin() || api_is_course_admin() ||
api_is_course_tutor() || api_is_session_general_coach()
) {
$loadExtraData = false;
if (isset($_REQUEST['extra_data']) && $_REQUEST['extra_data'] == 1) {
$loadExtraData = true;
}
$includeAllUsers = false;
if (isset($_REQUEST['include_all_users']) &&
$_REQUEST['include_all_users'] == 1
) {
$includeAllUsers = true;
}
$onlyBestAttempts = false;
if (isset($_REQUEST['only_best_attempts']) &&
$_REQUEST['only_best_attempts'] == 1
) {
$onlyBestAttempts = true;
}
require_once 'exercise_result.class.php';
$export = new ExerciseResult();
$export->setIncludeAllUsers($includeAllUsers);
$export->setOnlyBestAttempts($onlyBestAttempts);
switch ($_GET['export_format']) {
case 'xls':
$export->exportCompleteReportXLS(
$documentPath,
null,
$loadExtraData,
null,
$exercise_id
);
exit;
break;
case 'csv':
default:
$export->exportCompleteReportCSV(
$documentPath,
null,
$loadExtraData,
null,
$exercise_id
);
exit;
break;
}
} else {
api_not_allowed(true);
}
}
$objExerciseTmp = new Exercise();
$exerciseExists = $objExerciseTmp->read($exercise_id);
switch ($action) {
case 'export_all_results':
$sessionId = api_get_session_id();
$courseId = api_get_course_int_id();
ExerciseLib::exportExerciseAllResultsZip($sessionId, $courseId, $exercise_id);
break;
}
//Send student email @todo move this code in a class, library
if (isset($_REQUEST['comments']) &&
$_REQUEST['comments'] === 'update' &&
($is_allowedToEdit || $is_tutor || $allowCoachFeedbackExercises)
) {
// Filtered by post-condition
$id = (int) $_GET['exeid'];
$track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($id);
if (empty($track_exercise_info)) {
api_not_allowed();
}
$student_id = (int) $track_exercise_info['exe_user_id'];
$session_id = $track_exercise_info['session_id'];
$lp_id = $track_exercise_info['orig_lp_id'];
$lpItemId = $track_exercise_info['orig_lp_item_id'];
$lp_item_view_id = (int) $track_exercise_info['orig_lp_item_view_id'];
$exerciseId = $track_exercise_info['exe_exo_id'];
$exeWeighting = $track_exercise_info['exe_weighting'];
$attemptData = Event::get_exercise_results_by_attempt($id);
$questionListData = [];
if ($attemptData && $attemptData[$id] && $attemptData[$id]['question_list']) {
$questionListData = $attemptData[$id]['question_list'];
}
$post_content_id = [];
$comments_exist = false;
$questionListInPost = [];
foreach ($_POST as $key_index => $key_value) {
$my_post_info = explode('_', $key_index);
$post_content_id[] = isset($my_post_info[1]) ? $my_post_info[1] : null;
if ($my_post_info[0] === 'comments') {
$comments_exist = true;
$questionListInPost[] = $my_post_info[1];
}
}
foreach ($questionListInPost as $questionId) {
$marks = $_POST['marks_'.$questionId] ?? 0;
$my_comments = $_POST['comments_'.$questionId] ?? '';
$params = [
'teacher_comment' => $my_comments,
];
$question = Question::read($questionId);
if (false === $question) {
continue;
}
// From the database.
$marksFromDatabase = $questionListData[$questionId]['marks'];
if (in_array($question->type, [FREE_ANSWER, ORAL_EXPRESSION, ANNOTATION, UPLOAD_ANSWER, ANSWER_IN_OFFICE_DOC])) {
// From the form.
$params['marks'] = $marks;
if ($marksFromDatabase != $marks) {
Event::addEvent(
LOG_QUESTION_SCORE_UPDATE,
LOG_EXERCISE_ATTEMPT_QUESTION_ID,
[
'exe_id' => $id,
'question_id' => $questionId,
'old_marks' => $marksFromDatabase,
'new_marks' => $marks,
]
);
}
} else {
$marks = $marksFromDatabase;
}
Database::update(
$TBL_TRACK_ATTEMPT,
$params,
['question_id = ? AND exe_id = ?' => [$questionId, $id]]
);
$params = [
'exe_id' => $id,
'question_id' => $questionId,
'marks' => $marks,
'insert_date' => api_get_utc_datetime(),
'author' => api_get_user_id(),
'teacher_comment' => $my_comments,
];
Database::insert($TBL_TRACK_ATTEMPT_RECORDING, $params);
}
$useEvaluationPlugin = false;
$pluginEvaluation = QuestionOptionsEvaluationPlugin::create();
if ('true' === $pluginEvaluation->get(QuestionOptionsEvaluationPlugin::SETTING_ENABLE)) {
$formula = $pluginEvaluation->getFormulaForExercise($exerciseId);
if (!empty($formula)) {
$useEvaluationPlugin = true;
}
}
if (!$useEvaluationPlugin) {
$qry = 'SELECT DISTINCT question_id, marks
FROM '.$TBL_TRACK_ATTEMPT.' WHERE exe_id = '.$id.'
GROUP BY question_id';
$res = Database::query($qry);
$tot = 0;
while ($row = Database::fetch_array($res, 'ASSOC')) {
$marks = $row['marks'];
if (!$objExerciseTmp->propagate_neg && $marks < 0) {
continue;
}
$tot += $marks;
}
} else {
$tot = $pluginEvaluation->getResultWithFormula($id, $formula);
}
$totalScore = (float) $tot;
$sql = "UPDATE $TBL_TRACK_EXERCISES
SET exe_result = '".$totalScore."'
WHERE exe_id = ".$id;
Database::query($sql);
// See BT#18165
$remedialMessage = RemedialCoursePlugin::create()->getRemedialCourseList(
$objExerciseTmp,
$student_id,
api_get_session_id(),
true,
$lp_id ?: 0,
$lpItemId ?: 0
);
if (null != $remedialMessage) {
Display::addFlash(
Display::return_message($remedialMessage, 'warning', false)
);
}
$advancedMessage = RemedialCoursePlugin::create()->getAdvancedCourseList(
$objExerciseTmp,
$student_id,
api_get_session_id(),
$lp_id ?: 0,
$lpItemId ?: 0
);
if (!empty($advancedMessage)) {
$message = Display::return_message(
$advancedMessage,
'info',
false
);
}
if (isset($_POST['send_notification'])) {
//@todo move this somewhere else
$subject = get_lang('ExamSheetVCC');
$message = $_POST['notification_content'] ?? '';
$feedbackComments = ExerciseLib::getFeedbackComments($id);
$message .= "<div style='padding:10px; border:1px solid #ddd; border-radius:5px;'>";
$message .= $feedbackComments;
$message .= "</div>";
MessageManager::send_message_simple(
$student_id,
$subject,
$message,
api_get_user_id()
);
if ($allowCoachFeedbackExercises) {
Display::addFlash(
Display::return_message(get_lang('MessageSent'))
);
}
}
$notifications = api_get_configuration_value('exercise_finished_notification_settings');
if ($notifications) {
$oldResultDisabled = $objExerciseTmp->results_disabled;
$objExerciseTmp->results_disabled = RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS;
ob_start();
$stats = ExerciseLib::displayQuestionListByAttempt(
$objExerciseTmp,
$track_exercise_info['exe_id'],
false,
false,
false,
api_get_configuration_value('quiz_results_answers_report'),
false
);
$objExerciseTmp->results_disabled = $oldResultDisabled;
ob_end_clean();
// Show all for teachers.
$oldResultDisabled = $objExerciseTmp->results_disabled;
$objExerciseTmp->results_disabled = RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS;
$objExerciseTmp->forceShowExpectedChoiceColumn = true;
ob_start();
$statsTeacher = ExerciseLib::displayQuestionListByAttempt(
$objExerciseTmp,
$track_exercise_info['exe_id'],
false,
false,
false,
api_get_configuration_value('quiz_results_answers_report'),
false
);
ob_end_clean();
$objExerciseTmp->forceShowExpectedChoiceColumn = false;
$objExerciseTmp->results_disabled = $oldResultDisabled;
$attemptCount = Event::getAttemptPosition(
$track_exercise_info['exe_id'],
$student_id,
$objExerciseTmp->iid,
$lp_id,
$lpItemId,
$lp_item_view_id
);
ExerciseLib::sendNotification(
$student_id,
$objExerciseTmp,
$track_exercise_info,
api_get_course_info(),
$attemptCount,
$stats,
$statsTeacher
);
}
// Updating LP score here
if (!empty($lp_id) && !empty($lpItemId)) {
$statusCondition = '';
$item = new learnpathItem($lpItemId, api_get_user_id(), api_get_course_int_id());
if ($item) {
$prereqId = $item->get_prereq_string();
$minScore = $item->getPrerequisiteMinScore();
$maxScore = $item->getPrerequisiteMaxScore();
$passed = false;
$lp = new learnpath(api_get_course_id(), $lp_id, $student_id);
$prereqCheck = $lp->prerequisites_match($lpItemId);
if ($prereqCheck) {
$passed = true;
}
if (false === $passed) {
if (!empty($objExerciseTmp->pass_percentage)) {
$passed = ExerciseLib::isSuccessExerciseResult(
$tot,
$exeWeighting,
$objExerciseTmp->pass_percentage
);
} else {
$passed = false;
}
}
if ($passed) {
$statusCondition = ', status = "completed" ';
} else {
$statusCondition = ', status = "failed" ';
}
Display::addFlash(Display::return_message(get_lang('LearnpathUpdated')));
}
$sql = "UPDATE $TBL_LP_ITEM_VIEW
SET score = '".(float) $tot."'
$statusCondition
WHERE iid = $lp_item_view_id";
Database::query($sql);
header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/exercise_show.php?id='.$id.'&student='.$student_id.'&'.api_get_cidreq());
exit;
}
}
$actions = null;
$hideIp = api_get_configuration_value('exercise_hide_ip');
if ($is_allowedToEdit && $origin !== 'learnpath') {
// the form
if (api_is_platform_admin() || api_is_course_admin() ||
api_is_course_tutor() || api_is_session_general_coach()
) {
$actions .= '<a href="exercise.php?'.api_get_cidreq().'">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), '', ICON_SIZE_MEDIUM).'</a>';
$actions .= '<a href="live_stats.php?'.api_get_cidreq().'&exerciseId='.$exercise_id.'">'.
Display::return_icon('activity_monitor.png', get_lang('LiveResults'), '', ICON_SIZE_MEDIUM).'</a>';
$actions .= '<a href="stats.php?'.api_get_cidreq().'&exerciseId='.$exercise_id.'">'.
Display::return_icon('statistics.png', get_lang('ReportByQuestion'), '', ICON_SIZE_MEDIUM).'</a>';
$actions .= '<a href="stats_attempts.php?'.api_get_cidreq().'&exerciseId='.$exercise_id.'">'.
Display::return_icon('survey_reporting_complete.png', get_lang('ReportByAttempts'), '', ICON_SIZE_MEDIUM).'</a>';
$actions .= '<a id="export_opener" href="'.api_get_self().'?export_report=1&exerciseId='.$exercise_id.'" >'.
Display::return_icon('save.png', get_lang('Export'), '', ICON_SIZE_MEDIUM).'</a>';
$actions .= Display::url(
Display::return_icon('reload.png', get_lang('RecalculateResults'), [], ICON_SIZE_MEDIUM),
api_get_path(WEB_CODE_PATH).'exercise/recalculate_all.php?'.api_get_cidreq()."&exercise=$exercise_id"
);
$actions .= Display::url(
Display::return_icon('export_pdf.png', get_lang('ExportExerciseAllResults'), [], ICON_SIZE_MEDIUM),
api_get_self().'?'.api_get_cidreq().'&action=export_all_results&exerciseId='.$exercise_id
);
// clean result before a selected date icon
if ($allowClean) {
$actions .= Display::url(
Display::return_icon(
'clean_before_date.png',
get_lang('CleanStudentsResultsBeforeDate'),
'',
ICON_SIZE_MEDIUM
),
'#',
['onclick' => 'javascript:display_date_picker()']
);
// clean result before a selected date datepicker popup
$actions .= Display::span(
Display::input(
'input',
'datepicker_start',
get_lang('SelectADateOnTheCalendar'),
[
'onmouseover' => 'datepicker_input_mouseover()',
'id' => 'datepicker_start',
'onchange' => 'datepicker_input_changed()',
'readonly' => 'readonly',
]
).
Display::button(
'delete',
get_lang('Delete'),
['onclick' => 'submit_datepicker()']
),
['style' => 'display:none', 'id' => 'datepicker_span']
);
}
$actions .= Display::url(
get_lang('QuestionStats'),
'question_stats.php?'.api_get_cidreq().'&id='.$exercise_id,
['class' => 'btn btn-default']
);
$actions .= Display::url(
get_lang('ComparativeGroupReport'),
'comparative_group_report.php?'.api_get_cidreq().'&id='.$exercise_id,
['class' => 'btn btn-default']
);
$actions .= ExerciseFocusedPlugin::create()->getLinkReporting($exercise_id);
}
} else {
$actions .= '<a href="exercise.php">'.
Display::return_icon(
'back.png',
get_lang('GoBackToQuestionList'),
'',
ICON_SIZE_MEDIUM
).
'</a>';
}
// Deleting an attempt
if (($is_allowedToEdit || $is_tutor || api_is_coach()) &&
isset($_GET['delete']) && $_GET['delete'] === 'delete' &&
!empty($_GET['did']) && $locked == false
) {
$exe_id = (int) $_GET['did'];
if (!empty($exe_id)) {
ExerciseLib::deleteExerciseAttempt($exe_id);
if (empty($exercise_id)) {
header('Location: pending.php');
exit;
}
header('Location: exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exercise_id);
exit;
}
}
if ($is_allowedToEdit || $is_tutor) {
$interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$nameTools = get_lang('StudentScore');
if ($exerciseExists) {
$interbreadcrumb[] = [
'url' => '#',
'name' => $objExerciseTmp->selectTitle(true),
];
}
} else {
$interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
if ($exerciseExists) {
$nameTools = get_lang('Results').': '.$objExerciseTmp->selectTitle(true);
}
}
if (($is_allowedToEdit || $is_tutor || api_is_coach()) &&
isset($_GET['a']) && $_GET['a'] === 'close' &&
!empty($_GET['id']) && $locked == false
) {
// Close the user attempt otherwise left pending
$exe_id = (int) $_GET['id'];
$sql = "UPDATE $TBL_TRACK_EXERCISES SET status = ''
WHERE exe_id = $exe_id AND status = 'incomplete'";
Database::query($sql);
}
Display::display_header($nameTools);
// Clean all results for this test before the selected date
if (($is_allowedToEdit || $is_tutor || api_is_coach()) &&
isset($_GET['delete_before_date']) && $locked == false
) {
// ask for the date
$check = Security::check_token('get');
if ($check && $allowClean) {
$objExerciseTmp = new Exercise();
if ($objExerciseTmp->read($exercise_id)) {
$count = $objExerciseTmp->cleanResults(
true,
$_GET['delete_before_date'].' 23:59:59'
);
echo Display::return_message(
sprintf(get_lang('XResultsCleaned'), $count),
'confirm'
);
}
}
}
// Security token to protect deletion
$token = Security::get_token();
$actions = Display::div($actions, ['class' => 'actions']);
$extra = '<script>
$(function() {
$( "#dialog:ui-dialog" ).dialog( "destroy" );
$( "#dialog-confirm" ).dialog({
autoOpen: false,
show: "blind",
resizable: false,
height:300,
modal: true
});
$("#export_opener").click(function() {
var targetUrl = $(this).attr("href");
$( "#dialog-confirm" ).dialog({
width:400,
height:300,
buttons: {
"'.addslashes(get_lang('Download')).'": function() {
var export_format = $("input[name=export_format]:checked").val();
var extra_data = $("input[name=load_extra_data]:checked").val();
var includeAllUsers = $("input[name=include_all_users]:checked").val();
var attempts = $("input[name=only_best_attempts]:checked").val();
location.href = targetUrl+"&export_format="+export_format+"&extra_data="+extra_data+"&include_all_users="+includeAllUsers+"&only_best_attempts="+attempts;
$( this ).dialog( "close" );
}
}
});
$( "#dialog-confirm" ).dialog("open");
return false;
});
});
</script>';
$extra .= '<div id="dialog-confirm" title="'.get_lang('ConfirmYourChoice').'">';
$form = new FormValidator(
'report',
'post',
null,
null,
['class' => 'form-vertical']
);
$form->addElement(
'radio',
'export_format',
null,
get_lang('ExportAsCSV'),
'csv',
['id' => 'export_format_csv_label']
);
$form->addElement(
'radio',
'export_format',
null,
get_lang('ExportAsXLS'),
'xls',
['id' => 'export_format_xls_label']
);
$form->addElement(
'checkbox',
'load_extra_data',
null,
get_lang('LoadExtraData'),
'0',
['id' => 'export_format_xls_label']
);
$form->addElement(
'checkbox',
'include_all_users',
null,
get_lang('IncludeAllUsers'),
'0'
);
$form->addElement(
'checkbox',
'only_best_attempts',
null,
get_lang('OnlyBestAttempts'),
'0'
);
$form->setDefaults(['export_format' => 'csv']);
$extra .= $form->returnForm();
$extra .= '</div>';
if ($is_allowedToEdit) {
echo $extra;
}
echo $actions;
$url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_exercise_results&exerciseId='.$exercise_id.'&filter_by_user='.$filter_user.'&'.api_get_cidreq();
$action_links = '';
// Generating group list
$group_list = GroupManager::get_group_list();
$group_parameters = [
'group_all:'.get_lang('All'),
'group_none:'.get_lang('None'),
];
foreach ($group_list as $group) {
$group_parameters[] = $group['id'].':'.$group['name'];
}
if (!empty($group_parameters)) {
$group_parameters = implode(';', $group_parameters);
}
$officialCodeInList = api_get_setting('show_official_code_exercise_result_list');
if ($is_allowedToEdit || $is_tutor) {
// The order is important you need to check the the $column variable in the model.ajax.php file
$columns = [
get_lang('FirstName'),
get_lang('LastName'),
get_lang('LoginName'),
get_lang('Group'),
get_lang('Duration').' ('.get_lang('MinMinute').')',
get_lang('StartDate'),
get_lang('EndDate'),
get_lang('Score'),
get_lang('IP'),
get_lang('Status'),
get_lang('ToolLearnpath'),
get_lang('Actions'),
];
$indexIp = 8;
if ($officialCodeInList === 'true') {
$indexIp = 9;
$columns = array_merge([get_lang('OfficialCode')], $columns);
}
// Column config
$column_model = [
['name' => 'firstname', 'index' => 'firstname', 'width' => '50', 'align' => 'left', 'search' => 'true'],
[
'name' => 'lastname',
'index' => 'lastname',
'width' => '50',
'align' => 'left',
'formatter' => 'action_formatter',
'search' => 'true',
],
[
'name' => 'login',
'index' => 'username',
'width' => '40',
'align' => 'left',
'search' => 'true',
'hidden' => api_get_configuration_value('exercise_attempts_report_show_username') ? 'false' : 'true',
],
[
'name' => 'group_name',
'index' => 'group_id',
'width' => '40',
'align' => 'left',
'search' => 'true',
'stype' => 'select',
//for the bottom bar
'searchoptions' => [
'defaultValue' => 'group_all',
'value' => $group_parameters,
],
//for the top bar
'editoptions' => ['value' => $group_parameters],
],
['name' => 'duration', 'index' => 'exe_duration', 'width' => '30', 'align' => 'left', 'search' => 'true'],
['name' => 'start_date', 'index' => 'start_date', 'width' => '60', 'align' => 'left', 'search' => 'true'],
['name' => 'exe_date', 'index' => 'exe_date', 'width' => '60', 'align' => 'left', 'search' => 'true'],
['name' => 'score', 'index' => 'exe_result', 'width' => '50', 'align' => 'center', 'search' => 'true'],
['name' => 'ip', 'index' => 'user_ip', 'width' => '40', 'align' => 'center', 'search' => 'true'],
[
'name' => 'status',
'index' => 'revised',
'width' => '40',
'align' => 'left',
'search' => 'true',
'stype' => 'select',
//for the bottom bar
'searchoptions' => [
'defaultValue' => '',
'value' => ':'.get_lang('All').';1:'.get_lang('Validated').';0:'.get_lang('NotValidated'),
],
//for the top bar
'editoptions' => [
'value' => ':'.get_lang('All').';1:'.get_lang('Validated').';0:'.get_lang(
'NotValidated'
),
],
],
['name' => 'lp', 'index' => 'orig_lp_id', 'width' => '60', 'align' => 'left', 'search' => 'false'],
['name' => 'actions', 'index' => 'actions', 'width' => '60', 'align' => 'left', 'search' => 'false', 'sortable' => 'false'],
];
if ('true' === $officialCodeInList) {
$officialCodeRow = ['name' => 'official_code', 'index' => 'official_code', 'width' => '50', 'align' => 'left', 'search' => 'true'];
$column_model = array_merge([$officialCodeRow], $column_model);
}
if ($hideIp) {
// It removes the 9th column related to IP
unset($columns[$indexIp]);
unset($column_model[$indexIp]);
$columns = array_values($columns);
$column_model = array_values($column_model);
}
$action_links = '
// add username as title in lastname filed - ref 4226
function action_formatter(cellvalue, options, rowObject) {
// rowObject is firstname,lastname,login,... get the third word
var loginx = "'.api_htmlentities(sprintf(get_lang('LoginX'), ':::'), ENT_QUOTES).'";
var tabLoginx = loginx.split(/:::/);
// tabLoginx[0] is before and tabLoginx[1] is after :::
// may be empty string but is defined
return "<span title=\""+tabLoginx[0]+rowObject[2]+tabLoginx[1]+"\">"+cellvalue+"</span>";
}';
}
$extra_params['autowidth'] = 'true';
$extra_params['height'] = 'auto';
$extra_params['gridComplete'] = "
defaultGroupId = Cookies.get('default_group_".$exercise_id."');
if (typeof defaultGroupId !== 'undefined') {
$('#gs_group_name').val(defaultGroupId);
}
";
$extra_params['beforeRequest'] = "
var defaultGroupId = $('#gs_group_name').val();
// Load from group menu
if (typeof defaultGroupId !== 'undefined') {
Cookies.set('default_group_".$exercise_id."', defaultGroupId);
} else {
// get from cookies
defaultGroupId = Cookies.get('default_group_".$exercise_id."');
$('#gs_group_name').val(defaultGroupId);
}
if (typeof defaultGroupId !== 'undefined') {
var posted_data = $(\"#results\").jqGrid('getGridParam', 'postData');
var extraFilter = ',{\"field\":\"group_id\",\"op\":\"eq\",\"data\":\"'+ defaultGroupId +'\"}]}';
var filters = posted_data.filters;
var stringObj = new String(filters);
stringObj.replace(']}', extraFilter);
posted_data['group_id_in_toolbar'] = defaultGroupId;
$(this).jqGrid('setGridParam', 'postData', posted_data);
}
";
$gridJs = Display::grid_js(
'results',
$url,
$columns,
$column_model,
$extra_params,
[],
$action_links,
true
);
?>
<script>
function exportExcel()
{
var mya = $("#results").getDataIDs(); // Get All IDs
var data = $("#results").getRowData(mya[0]); // Get First row to get the labels
var colNames = new Array();
var ii = 0;
for (var i in data) {
colNames[ii++] = i;
}
var html = "";
for (i = 0; i < mya.length; i++) {
data = $("#results").getRowData(mya[i]); // get each row
for (j = 0; j < colNames.length; j++) {
html = html + data[colNames[j]] + ","; // output each column as tab delimited
}
html = html + "\n"; // output each row with end of line
}
html = html + "\n"; // end of line at the end
var form = $("#export_report_form");
$("#csvBuffer").attr('value', html);
form.target='_blank';
form.submit();
}
$(function() {
$("#datepicker_start").datepicker({
defaultDate: "",
changeMonth: false,
numberOfMonths: 1
});
<?php
echo $gridJs;
if ($is_allowedToEdit || $is_tutor) {
?>
$("#results").jqGrid(
'navGrid',
'#results_pager', {
view:true, edit:false, add:false, del:false, excel:false
},
{height:280, reloadAfterSubmit:false}, // view options
{height:280, reloadAfterSubmit:false}, // edit options
{height:280, reloadAfterSubmit:false}, // add options
{reloadAfterSubmit: false}, // del options
{width:500}, // search options
);
var sgrid = $("#results")[0];
// Update group
var defaultGroupId = Cookies.get('default_group_<?php echo $exercise_id; ?>');
$('#gs_group_name').val(defaultGroupId);
// Adding search options
var options = {
'stringResult': true,
'autosearch' : true,
'searchOnEnter': false,
afterSearch: function () {
$('#gs_group_name').on('change', function() {
var defaultGroupId = $('#gs_group_name').val();
// Save default group id
Cookies.set('default_group_<?php echo $exercise_id; ?>', defaultGroupId);
});
}
}
jQuery("#results").jqGrid('filterToolbar', options);
sgrid.triggerToolbar();
$('#results').on('click', 'a.exercise-recalculate', function (e) {
e.preventDefault();
if (!$(this).data('user') || !$(this).data('exercise') || !$(this).data('id')) {
return;
}
var url = '<?php echo api_get_path(WEB_CODE_PATH); ?>exercise/recalculate.php?<?php echo api_get_cidreq(); ?>';
var recalculateXhr = $.post(url, $(this).data());
$.when(recalculateXhr).done(function (response) {
$('#results').trigger('reloadGrid');
});
});
<?php
}
?>
});
// datepicker functions
var datapickerInputModified = false;
/**
* return true if the datepicker input has been modified
*/
function datepicker_input_changed() {
datapickerInputModified = true;
}
/**
* disply the datepicker calendar on mouse over the input
*/
function datepicker_input_mouseover() {
$('#datepicker_start').datepicker( "show" );
}
/**
* display or hide the datepicker input, calendar and button
*/
function display_date_picker() {
if (!$('#datepicker_span').is(":visible")) {
$('#datepicker_span').show();
$('#datepicker_start').datepicker( "show" );
} else {
$('#datepicker_start').datepicker( "hide" );
$('#datepicker_span').hide();
}
}
/**
* confirm deletion
*/
function submit_datepicker() {
if (datapickerInputModified) {
var dateTypeVar = $('#datepicker_start').datepicker('getDate');
var dateForBDD = $.datepicker.formatDate('yy-mm-dd', dateTypeVar);
// Format the date for confirm box
var dateFormat = $( "#datepicker_start" ).datepicker( "option", "dateFormat" );
var selectedDate = $.datepicker.formatDate(dateFormat, dateTypeVar);
if (confirm("<?php echo convert_double_quote_to_single(get_lang('AreYouSureDeleteTestResultBeforeDateD')).' '; ?>" + selectedDate)) {
self.location.href = "exercise_report.php?<?php echo api_get_cidreq(); ?>&exerciseId=<?php echo $exercise_id; ?>&delete_before_date="+dateForBDD+"&sec_token=<?php echo $token; ?>";
}
}
}
</script>
<form id="export_report_form" method="post" action="exercise_report.php?<?php echo api_get_cidreq(); ?>">
<input type="hidden" name="csvBuffer" id="csvBuffer" value="" />
<input type="hidden" name="export_report" id="export_report" value="1" />
<input type="hidden" name="exerciseId" id="exerciseId" value="<?php echo $exercise_id; ?>" />
</form>
<?php
echo Display::grid_html('results');
Display::display_footer();

View File

@@ -0,0 +1,747 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class ExerciseResult
* which allows you to export exercises results in multiple presentation forms.
*
* @author Yannick Warnier
*/
class ExerciseResult
{
public $includeAllUsers = false;
public $onlyBestAttempts = false;
private $results = [];
/**
* @param bool $includeAllUsers
*/
public function setIncludeAllUsers($includeAllUsers)
{
$this->includeAllUsers = $includeAllUsers;
}
/**
* @param bool $value
*/
public function setOnlyBestAttempts($value)
{
$this->onlyBestAttempts = $value;
}
/**
* Gets the results of all students (or just one student if access is limited).
*
* @param string $document_path The document path (for HotPotatoes retrieval)
* @param int $user_id User ID. Optional. If no user ID is provided, we take all the results. Defauts to null
* @param int $filter
* @param int $exercise_id
*
* @return bool
*/
public function getExercisesReporting(
$document_path,
$user_id = null,
$filter = 0,
$exercise_id = 0
) {
$return = [];
$TBL_EXERCISES = Database::get_course_table(TABLE_QUIZ_TEST);
$TBL_TABLE_LP_MAIN = Database::get_course_table(TABLE_LP_MAIN);
$TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
$TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
$TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
$cid = api_get_course_id();
$course_id = api_get_course_int_id();
$user_id = (int) $user_id;
$sessionId = api_get_session_id();
$session_id_and = ' AND te.session_id = '.$sessionId.' ';
$exercise_id = (int) $exercise_id;
if (empty($sessionId) &&
api_get_configuration_value('show_exercise_session_attempts_in_base_course')
) {
$session_id_and = '';
}
if (!empty($exercise_id)) {
$session_id_and .= " AND exe_exo_id = $exercise_id ";
}
if (empty($user_id)) {
$user_id_and = null;
$sql = "SELECT
firstname,
lastname,
official_code,
ce.title as extitle,
ce.pass_percentage as expasspercentage,
te.exe_result as exresult ,
te.exe_weighting as exweight,
te.exe_date as exdate,
te.exe_id as exid,
email as exemail,
te.start_date as exstart,
steps_counter as exstep,
exe_user_id as excruid,
te.exe_duration as duration,
te.orig_lp_id as orig_lp_id,
tlm.name as lp_name,
user.username,
te.status as exstatus
FROM $TBL_EXERCISES AS ce
INNER JOIN $TBL_TRACK_EXERCISES AS te
ON (te.exe_exo_id = ce.iid)
INNER JOIN $TBL_USER AS user
ON (user.user_id = exe_user_id)
LEFT JOIN $TBL_TABLE_LP_MAIN AS tlm
ON (tlm.id = te.orig_lp_id AND tlm.c_id = ce.c_id)
WHERE
ce.c_id = $course_id AND
te.c_id = ce.c_id $user_id_and $session_id_and AND
ce.active <> -1";
} else {
$user_id_and = ' AND te.exe_user_id = '.api_get_user_id().' ';
$orderBy = 'lastname';
if (api_is_western_name_order()) {
$orderBy = 'firstname';
}
// get only this user's results
$sql = "SELECT
firstname,
lastname,
official_code,
ce.title as extitle,
ce.pass_percentage as expasspercentage,
te.exe_result as exresult,
te.exe_weighting as exweight,
te.exe_date as exdate,
te.exe_id as exid,
email as exemail,
te.start_date as exstart,
steps_counter as exstep,
exe_user_id as excruid,
te.exe_duration as duration,
ce.results_disabled as exdisabled,
te.orig_lp_id as orig_lp_id,
tlm.name as lp_name,
user.username,
te.status as exstatus
FROM $TBL_EXERCISES AS ce
INNER JOIN $TBL_TRACK_EXERCISES AS te
ON (te.exe_exo_id = ce.iid)
INNER JOIN $TBL_USER AS user
ON (user.user_id = exe_user_id)
LEFT JOIN $TBL_TABLE_LP_MAIN AS tlm
ON (tlm.id = te.orig_lp_id AND tlm.c_id = ce.c_id)
WHERE
ce.c_id = $course_id AND
te.c_id = ce.c_id $user_id_and $session_id_and AND
ce.active <>-1 AND
ORDER BY $orderBy, te.c_id ASC, ce.title ASC, te.exe_date DESC";
}
$results = [];
$resx = Database::query($sql);
$bestAttemptPerUser = [];
while ($rowx = Database::fetch_array($resx, 'ASSOC')) {
if ($this->onlyBestAttempts) {
if (!isset($bestAttemptPerUser[$rowx['excruid']])) {
$bestAttemptPerUser[$rowx['excruid']] = $rowx;
} else {
if ($rowx['exresult'] > $bestAttemptPerUser[$rowx['excruid']]['exresult']) {
$bestAttemptPerUser[$rowx['excruid']] = $rowx;
}
}
} else {
$results[] = $rowx;
}
}
if ($this->onlyBestAttempts) {
// Remove userId indexes to avoid issues in output
foreach ($bestAttemptPerUser as $attempt) {
$results[] = $attempt;
}
}
$filter_by_not_revised = false;
$filter_by_revised = false;
if ($filter) {
switch ($filter) {
case 1:
$filter_by_not_revised = true;
break;
case 2:
$filter_by_revised = true;
break;
default:
null;
}
}
if (empty($sessionId)) {
$students = CourseManager::get_user_list_from_course_code($cid);
} else {
$students = CourseManager::get_user_list_from_course_code($cid, $sessionId);
}
$studentsUserIdList = array_keys($students);
// Print the results of tests
$userWithResults = [];
if (is_array($results)) {
$i = 0;
foreach ($results as $result) {
$revised = 0;
if ($result['exstatus'] === 'incomplete') {
$revised = -1;
} else {
//revised or not
$sql_exe = "SELECT exe_id
FROM $TBL_TRACK_ATTEMPT_RECORDING
WHERE
author != '' AND
exe_id = ".intval($result['exid'])."
LIMIT 1";
$query = Database::query($sql_exe);
if (Database::num_rows($query) > 0) {
$revised = 1;
}
}
if ($filter_by_not_revised && $revised === 1) {
continue;
}
if ($filter_by_revised && $revised < 1) {
continue;
}
$return[$i] = [];
if (empty($user_id)) {
$return[$i]['official_code'] = $result['official_code'];
$return[$i]['firstname'] = $results[$i]['firstname'];
$return[$i]['lastname'] = $results[$i]['lastname'];
$return[$i]['user_id'] = $results[$i]['excruid'];
$return[$i]['email'] = $results[$i]['exemail'];
$return[$i]['username'] = $results[$i]['username'];
}
$return[$i]['title'] = $result['extitle'];
$return[$i]['minimun'] = $result['expasspercentage']
? float_format($result['expasspercentage'] / 100 * $result['exweight'])
: 0;
$return[$i]['start_date'] = api_get_local_time($result['exstart']);
$return[$i]['end_date'] = api_get_local_time($result['exdate']);
$return[$i]['duration'] = $result['duration'];
$return[$i]['result'] = $result['exresult'];
$return[$i]['max'] = $result['exweight'];
// Revised: 1 = revised, 0 = not revised, -1 = not even finished by user
$return[$i]['status'] = $revised === 1 ? get_lang('Validated') : ($revised === 0 ? get_lang('NotValidated') : get_lang('Unclosed'));
$return[$i]['lp_id'] = $result['orig_lp_id'];
$return[$i]['lp_name'] = $result['lp_name'];
if (in_array($result['excruid'], $studentsUserIdList)) {
$return[$i]['is_user_subscribed'] = get_lang('Yes');
} else {
$return[$i]['is_user_subscribed'] = get_lang('No');
}
$userWithResults[$result['excruid']] = 1;
$i++;
}
}
if ($this->includeAllUsers) {
$latestId = count($return);
$userWithResults = array_keys($userWithResults);
if (!empty($students)) {
foreach ($students as $student) {
if (!in_array($student['user_id'], $userWithResults)) {
$i = $latestId;
$isWestern = api_is_western_name_order();
if (empty($user_id)) {
$return[$i]['official_code'] = $student['official_code'];
$return[$i]['firstname'] = $student['firstname'];
$return[$i]['lastname'] = $student['lastname'];
$return[$i]['user_id'] = $student['user_id'];
$return[$i]['email'] = $student['email'];
$return[$i]['username'] = $student['username'];
}
$return[$i]['title'] = null;
$return[$i]['minimun'] = null;
$return[$i]['start_date'] = null;
$return[$i]['end_date'] = null;
$return[$i]['duration'] = null;
$return[$i]['result'] = null;
$return[$i]['max'] = null;
$return[$i]['status'] = get_lang('NotAttempted');
$return[$i]['lp_id'] = null;
$return[$i]['lp_name'] = null;
$return[$i]['is_user_subscribed'] = get_lang('Yes');
$latestId++;
}
}
}
}
$this->results = $return;
return true;
}
/**
* Exports the complete report as a CSV file.
*
* @param string $document_path Document path inside the document tool
* @param int $user_id Optional user ID
* @param bool $export_user_fields Whether to include user fields or not
* @param int $export_filter
* @param int $exercise_id
*
* @return bool False on error
*/
public function exportCompleteReportCSV(
$document_path = '',
$user_id = null,
$export_user_fields = false,
$export_filter = 0,
$exercise_id = 0
) {
global $charset;
$this->getExercisesReporting(
$document_path,
$user_id,
$export_filter,
$exercise_id
);
$now = api_get_local_time();
$filename = 'exercise_results_'.$now.'.csv';
if (!empty($user_id)) {
$filename = 'exercise_results_user_'.$user_id.'_'.$now.'.csv';
}
$filename = api_replace_dangerous_char($filename);
$data = '';
if (api_is_western_name_order()) {
if (!empty($this->results[0]['firstname'])) {
$data .= get_lang('FirstName').';';
}
if (!empty($this->results[0]['lastname'])) {
$data .= get_lang('LastName').';';
}
} else {
if (!empty($this->results[0]['lastname'])) {
$data .= get_lang('LastName').';';
}
if (!empty($this->results[0]['firstname'])) {
$data .= get_lang('FirstName').';';
}
}
$officialCodeInList = api_get_setting('show_official_code_exercise_result_list');
if ($officialCodeInList === 'true') {
$data .= get_lang('OfficialCode').';';
}
$data .= get_lang('LoginName').';';
$data .= get_lang('Email').';';
$data .= get_lang('Groups').';';
if ($export_user_fields) {
//show user fields section with a big th colspan that spans over all fields
$extra_user_fields = UserManager::get_extra_fields(
0,
1000,
5,
'ASC',
false,
1
);
if (!empty($extra_user_fields)) {
foreach ($extra_user_fields as $field) {
$data .= '"'.str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($field[3]), ENT_QUOTES, $charset)).'";';
}
}
}
$data .= get_lang('Title').';';
$data .= get_lang('StartDate').';';
$data .= get_lang('EndDate').';';
$data .= get_lang('Duration').' ('.get_lang('MinMinutes').') ;';
$data .= get_lang('WeightNecessary').';';
$data .= get_lang('Score').';';
$data .= get_lang('Total').';';
$data .= get_lang('Status').';';
$data .= get_lang('ToolLearnpath').';';
$data .= get_lang('UserIsCurrentlySubscribed').';';
$data .= get_lang('CourseCode').';';
$data .= "\n";
//results
foreach ($this->results as $row) {
if (api_is_western_name_order()) {
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['firstname']), ENT_QUOTES, $charset)).';';
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['lastname']), ENT_QUOTES, $charset)).';';
} else {
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['lastname']), ENT_QUOTES, $charset)).';';
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['firstname']), ENT_QUOTES, $charset)).';';
}
// Official code
if ($officialCodeInList === 'true') {
$data .= $row['official_code'].';';
}
// Email
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['username']), ENT_QUOTES, $charset)).';';
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['email']), ENT_QUOTES, $charset)).';';
$data .= str_replace("\r\n", ' ', implode(", ", GroupManager::get_user_group_name($row['user_id']))).';';
if ($export_user_fields) {
//show user fields data, if any, for this user
$user_fields_values = UserManager::get_extra_user_data(
$row['user_id'],
false,
false,
false,
true
);
if (!empty($user_fields_values)) {
foreach ($user_fields_values as $value) {
$data .= '"'.str_replace('"', '""', api_html_entity_decode(strip_tags($value), ENT_QUOTES, $charset)).'";';
}
}
}
$duration = !empty($row['duration']) ? round($row['duration'] / 60) : 0;
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['title']), ENT_QUOTES, $charset)).';';
$data .= str_replace("\r\n", ' ', $row['start_date']).';';
$data .= str_replace("\r\n", ' ', $row['end_date']).';';
$data .= str_replace("\r\n", ' ', $duration).';';
$data .= str_replace("\r\n", ' ', $row['minimun']).';';
$data .= str_replace("\r\n", ' ', $row['result']).';';
$data .= str_replace("\r\n", ' ', $row['max']).';';
$data .= str_replace("\r\n", ' ', $row['status']).';';
$data .= str_replace("\r\n", ' ', $row['lp_name']).';';
$data .= str_replace("\r\n", ' ', $row['is_user_subscribed']).';';
$data .= str_replace("\r\n", ' ', api_get_course_id()).';';
$data .= "\n";
}
// output the results
$len = strlen($data);
header('Content-type: application/octet-stream');
header('Content-Type: application/force-download');
header('Content-length: '.$len);
if (preg_match("/MSIE 5.5/", $_SERVER['HTTP_USER_AGENT'])) {
header('Content-Disposition: filename= '.$filename);
} else {
header('Content-Disposition: attachment; filename= '.$filename);
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
header('Pragma: ');
header('Cache-Control: ');
header('Cache-Control: public'); // IE cannot download from sessions without a cache
}
header('Content-Description: '.$filename);
header('Content-transfer-encoding: binary');
echo $data;
return true;
}
/**
* Exports the complete report as an XLS file.
*
* @param string $document_path
* @param null $user_id
* @param bool $export_user_fields
* @param int $export_filter
* @param int $exercise_id
* @param null $hotpotato_name
*
* @return bool
*/
public function exportCompleteReportXLS(
$document_path = '',
$user_id = null,
$export_user_fields = false,
$export_filter = 0,
$exercise_id = 0,
$hotpotato_name = null
) {
global $charset;
$this->getExercisesReporting(
$document_path,
$user_id,
$export_filter,
$exercise_id,
$hotpotato_name
);
$now = api_get_local_time();
$filename = 'exercise_results_'.$now.'.xlsx';
if (!empty($user_id)) {
$filename = 'exercise_results_user_'.$user_id.'_'.$now.'.xlsx';
}
$spreadsheet = new PHPExcel();
$spreadsheet->setActiveSheetIndex(0);
$worksheet = $spreadsheet->getActiveSheet();
$line = 1; // Skip first line
$column = 0; //skip the first column (row titles)
// check if exists column 'user'
$with_column_user = false;
foreach ($this->results as $result) {
if (!empty($result['lastname']) && !empty($result['firstname'])) {
$with_column_user = true;
break;
}
}
$officialCodeInList = api_get_setting('show_official_code_exercise_result_list');
if ($with_column_user) {
if (api_is_western_name_order()) {
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('FirstName'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('LastName'));
$column++;
} else {
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('LastName'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('FirstName'));
$column++;
}
if ($officialCodeInList === 'true') {
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('OfficialCode'));
$column++;
}
$worksheet->setCellValueByColumnAndRow($column++, $line, get_lang('LoginName'));
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('Email'));
$column++;
}
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('Groups'));
$column++;
if ($export_user_fields) {
//show user fields section with a big th colspan that spans over all fields
$extra_user_fields = UserManager::get_extra_fields(
0,
1000,
5,
'ASC',
false,
1
);
//show the fields names for user fields
foreach ($extra_user_fields as $field) {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($field[3]),
ENT_QUOTES,
$charset
)
);
$column++;
}
}
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('Title'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('StartDate'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('EndDate'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('Duration').' ('.get_lang('MinMinutes').')');
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('WeightNecessary'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('Score'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('Total'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('Status'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('ToolLearnpath'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('UserIsCurrentlySubscribed'));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, get_lang('CourseCode'));
$line++;
foreach ($this->results as $row) {
$column = 0;
if ($with_column_user) {
if (api_is_western_name_order()) {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['firstname']),
ENT_QUOTES,
$charset
)
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['lastname']),
ENT_QUOTES,
$charset
)
);
$column++;
} else {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['lastname']),
ENT_QUOTES,
$charset
)
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['firstname']),
ENT_QUOTES,
$charset
)
);
$column++;
}
if ($officialCodeInList === 'true') {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['official_code']),
ENT_QUOTES,
$charset
)
);
$column++;
}
$worksheet->setCellValueByColumnAndRow(
$column++,
$line,
api_html_entity_decode(
strip_tags($row['username']),
ENT_QUOTES,
$charset
)
);
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['email']),
ENT_QUOTES,
$charset
)
);
$column++;
}
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags(
implode(
", ",
GroupManager::get_user_group_name($row['user_id'])
)
),
ENT_QUOTES,
$charset
)
);
$column++;
if ($export_user_fields) {
//show user fields data, if any, for this user
$user_fields_values = UserManager::get_extra_user_data(
$row['user_id'],
false,
false,
false,
true
);
foreach ($user_fields_values as $value) {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($value),
ENT_QUOTES,
$charset
)
);
$column++;
}
}
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['title']),
ENT_QUOTES,
$charset
)
);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['start_date']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['end_date']);
$column++;
$duration = !empty($row['duration']) ? round($row['duration'] / 60) : 0;
$worksheet->setCellValueByColumnAndRow($column, $line, $duration);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['minimun']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['result']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['max']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['status']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['lp_name']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['is_user_subscribed']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, api_get_course_id());
$line++;
}
$file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename);
$writer = new PHPExcel_Writer_Excel2007($spreadsheet);
$writer->save($file);
DocumentManager::file_send_for_download($file, true, $filename);
return true;
}
}

View File

@@ -0,0 +1,396 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* Exercise result
* This script gets information from the script "exercise_submit.php",
* through the session, and calculates the score of the student for
* that exercise.
* Then it shows the results on the screen.
*
* @author Olivier Brouckaert, main author
* @author Roan Embrechts, some refactoring
* @author Julio Montoya switchable fill in blank option added
*
* @todo split more code up in functions, move functions to library?
*/
$debug = false;
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
$this_section = SECTION_COURSES;
api_protect_course_script(true);
$origin = api_get_origin();
/** @var Exercise $objExercise */
if (empty($objExercise)) {
$objExercise = Session::read('objExercise');
}
$exeId = isset($_REQUEST['exe_id']) ? (int) $_REQUEST['exe_id'] : 0;
if (empty($objExercise)) {
// Redirect to the exercise overview
// Check if the exe_id exists
$objExercise = new Exercise();
$exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
if (!empty($exercise_stat_info) && isset($exercise_stat_info['exe_exo_id'])) {
header('Location: overview.php?exerciseId='.$exercise_stat_info['exe_exo_id'].'&'.api_get_cidreq());
exit;
}
api_not_allowed(true);
}
$js = '<script>'.api_get_language_translate_html().'</script>';
$htmlHeadXtra[] = $js;
if (api_is_in_gradebook()) {
$interbreadcrumb[] = [
'url' => Category::getUrl(),
'name' => get_lang('ToolGradebook'),
];
}
$nameTools = get_lang('Exercises');
$currentUserId = api_get_user_id();
$interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
if (RESULT_DISABLE_RADAR === (int) $objExercise->results_disabled) {
$htmlHeadXtra[] = api_get_js('chartjs/Chart.min.js');
}
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>';
$htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/css/hotspot.css">';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'annotation/js/annotation.js"></script>';
if (api_get_configuration_value('quiz_prevent_copy_paste')) {
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'jquery.nocopypaste.js"></script>';
}
if (!empty($objExercise->getResultAccess())) {
$htmlHeadXtra[] = api_get_css(
api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/renderers/minute/epiclock.minute.css'
);
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.dateformat.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.epiclock.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/renderers/minute/epiclock.minute.js');
}
$showHeader = false;
$showFooter = false;
$showLearnPath = true;
$pageActions = '';
$pageTop = '';
$pageBottom = '';
$pageContent = '';
$courseInfo = api_get_course_info();
if (!in_array($origin, ['learnpath', 'embeddable', 'mobileapp', 'iframe'])) {
// So we are not in learnpath tool
$showHeader = true;
$showLearnPath = false;
}
// I'm in a preview mode as course admin. Display the action menu.
if (api_is_course_admin() && !in_array($origin, ['learnpath', 'embeddable', 'iframe'])) {
$pageActions = Display::toolbarAction(
'exercise_result_actions',
[
Display::url(
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), [], 32),
'admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid
)
.Display::url(
Display::return_icon('settings.png', get_lang('ModifyExercise'), [], 32),
'exercise_admin.php?'.api_get_cidreq().'&modifyExercise=yes&exerciseId='.$objExercise->iid
),
]
);
}
$exercise_stat_info = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
$learnpath_id = isset($exercise_stat_info['orig_lp_id']) ? $exercise_stat_info['orig_lp_id'] : 0;
$learnpath_item_id = isset($exercise_stat_info['orig_lp_item_id']) ? $exercise_stat_info['orig_lp_item_id'] : 0;
$learnpath_item_view_id = isset($exercise_stat_info['orig_lp_item_view_id'])
? $exercise_stat_info['orig_lp_item_view_id'] : 0;
$exerciseId = isset($exercise_stat_info['exe_exo_id']) ? $exercise_stat_info['exe_exo_id'] : 0;
$logInfo = [
'tool' => TOOL_QUIZ,
'tool_id' => $objExercise->iid,
'action' => $learnpath_id,
'action_details' => $learnpath_id,
];
Event::registerLog($logInfo);
$allowSignature = ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise);
if ($allowSignature) {
$htmlHeadXtra[] = api_get_asset('signature_pad/signature_pad.umd.js');
}
if ($origin === 'learnpath') {
$pageTop .= '
<form method="GET" action="exercise.php?'.api_get_cidreq().'">
<input type="hidden" name="origin" value='.$origin.'/>
<input type="hidden" name="learnpath_id" value="'.$learnpath_id.'"/>
<input type="hidden" name="learnpath_item_id" value="'.$learnpath_item_id.'"/>
<input type="hidden" name="learnpath_item_view_id" value="'.$learnpath_item_view_id.'"/>
';
}
$i = $total_score = $max_score = 0;
$remainingMessage = '';
$attemptButton = '';
if (!in_array($origin, ['iframe', 'embeddable'])) {
$attemptButton = Display::toolbarButton(
get_lang('AnotherAttempt'),
api_get_path(WEB_CODE_PATH).'exercise/overview.php?'.api_get_cidreq().'&'.http_build_query(
[
'exerciseId' => $objExercise->iid,
'learnpath_id' => $learnpath_id,
'learnpath_item_id' => $learnpath_item_id,
'learnpath_item_view_id' => $learnpath_item_view_id,
]
),
'pencil-square-o',
'info'
);
}
// We check if the user attempts before sending to the exercise_result.php
$attempt_count = Event::get_attempt_count(
$currentUserId,
$objExercise->iid,
$learnpath_id,
$learnpath_item_id,
$learnpath_item_view_id
);
if ($objExercise->selectAttempts() > 0) {
if ($attempt_count >= $objExercise->selectAttempts()) {
Display::addFlash(
Display::return_message(
sprintf(get_lang('ReachedMaxAttempts'), $objExercise->selectTitle(), $objExercise->selectAttempts()),
'warning',
false
)
);
if (!in_array($origin, ['learnpath', 'embeddable', 'iframe'])) {
$showFooter = true;
}
$template = new Template($nameTools, $showHeader, $showFooter);
$template->assign('actions', $pageActions);
$template->display_one_col_template();
exit;
} else {
$attempt_count++;
$remainingAttempts = $objExercise->selectAttempts() - $attempt_count;
if ($remainingAttempts) {
$attemptMessage = sprintf(get_lang('RemainingXAttempts'), $remainingAttempts);
$remainingMessage = sprintf('<p>%s</p> %s', $attemptMessage, $attemptButton);
}
}
} else {
$remainingMessage = $attemptButton ? "<p>$attemptButton</p>" : '';
}
$total_score = 0;
if (!empty($exercise_stat_info)) {
$total_score = $exercise_stat_info['exe_result'];
}
$max_score = $objExercise->get_max_score();
if ('embeddable' === $origin) {
$pageTop .= showEmbeddableFinishButton();
} else {
Display::addFlash(
Display::return_message(get_lang('Saved'), 'normal', false)
);
}
$saveResults = true;
$feedbackType = $objExercise->getFeedbackType();
ob_start();
$stats = ExerciseLib::displayQuestionListByAttempt(
$objExercise,
$exeId,
$saveResults,
$remainingMessage,
$allowSignature,
api_get_configuration_value('quiz_results_answers_report'),
false
);
$pageContent .= ob_get_contents();
ob_end_clean();
// Change settings for teacher access.
$oldResultDisabled = $objExercise->results_disabled;
$objExercise->results_disabled = RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS;
$objExercise->forceShowExpectedChoiceColumn = true;
$objExercise->disableHideCorrectAnsweredQuestions = true;
ob_start();
$statsTeacher = ExerciseLib::displayQuestionListByAttempt(
$objExercise,
$exeId,
false,
$remainingMessage,
$allowSignature,
api_get_configuration_value('quiz_results_answers_report'),
false
);
ob_end_clean();
// Restore settings.
$objExercise->results_disabled = $oldResultDisabled;
$objExercise->forceShowExpectedChoiceColumn = false;
$objExercise->disableHideCorrectAnsweredQuestions = false;
// Save here LP status
if (!empty($learnpath_id) && $saveResults) {
// Save attempt in lp
Exercise::saveExerciseInLp($learnpath_item_id, $exeId);
}
ExerciseLib::sendNotification(
api_get_user_id(),
$objExercise,
$exercise_stat_info,
$courseInfo,
$attempt_count++,
$stats,
$statsTeacher
);
$hookQuizEnd = HookQuizEnd::create();
$hookQuizEnd->setEventData(['exe_id' => $exeId]);
$hookQuizEnd->notifyQuizEnd();
$advancedCourseMessage = RemedialCoursePlugin::create()->getAdvancedCourseList(
$objExercise,
api_get_user_id(),
api_get_session_id(),
$learnpath_id ?: 0,
$learnpath_item_id ?: 0
);
if (null != $advancedCourseMessage) {
Display::addFlash(
Display::return_message($advancedCourseMessage, 'info', false)
);
}
$remedialMessage = RemedialCoursePlugin::create()->getRemedialCourseList(
$objExercise,
api_get_user_id(),
api_get_session_id(),
false,
$learnpath_id ?: 0,
$learnpath_item_id ?: 0
);
if (null != $remedialMessage) {
Display::addFlash(
Display::return_message($remedialMessage, 'warning', false)
);
}
// Unset session for clock time
ExerciseLib::exercise_time_control_delete(
$objExercise->iid,
$learnpath_id,
$learnpath_item_id
);
ExerciseLib::delete_chat_exercise_session($exeId);
if (!in_array($origin, ['learnpath', 'embeddable', 'mobileapp', 'iframe'])) {
$pageBottom .= '<div class="question-return">';
$pageBottom .= Display::url(
get_lang('ReturnToCourseHomepage'),
api_get_course_url(),
['class' => 'btn btn-primary']
);
$pageBottom .= '</div>';
if (api_is_allowed_to_session_edit()) {
Exercise::cleanSessionVariables();
}
$showFooter = true;
} elseif (in_array($origin, ['embeddable', 'mobileapp', 'iframe'])) {
if (api_is_allowed_to_session_edit()) {
Exercise::cleanSessionVariables();
}
Session::write('attempt_remaining', $remainingMessage);
$showFooter = false;
} else {
$lp_mode = Session::read('lp_mode');
$url = '../lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$learnpath_id
.'&lp_item_id='.$learnpath_item_id.'&exeId='.$exeId
.'&fb_type='.$objExercise->getFeedbackType().'#atoc_'.$learnpath_item_id;
$href = 'fullscreen' === $lp_mode ? ' window.opener.location.href="'.$url.'" ' : ' top.location.href="'.$url.'"';
if (api_is_allowed_to_session_edit()) {
Exercise::cleanSessionVariables();
}
Session::write('attempt_remaining', $remainingMessage);
// Record the results in the learning path, using the SCORM interface (API)
$pageBottom .= "<script>window.parent.API.void_save_asset('$total_score', '$max_score', 0, 'completed');</script>";
if (empty($_SESSION['oLP']->lti_launch_id)) {
$pageBottom .= '<script type="text/javascript">'.$href.'</script>';
}
$showFooter = false;
}
$template = new Template($nameTools, $showHeader, $showFooter, $showLearnPath);
$template->assign('page_top', $pageTop);
$template->assign('page_content', $pageContent);
$template->assign('page_bottom', $pageBottom);
$template->assign('allow_signature', $allowSignature);
$template->assign('exe_id', $exeId);
$template->assign('actions', $pageActions);
$template->assign('content', $template->fetch($template->get_template('exercise/result.tpl')));
$template->display_one_col_template();
function showEmbeddableFinishButton()
{
$js = '<script>
$(function () {
$(\'.btn-close-quiz\').on(\'click\', function () {
var playerId = window.frameElement.parentElement.parentElement.parentElement.id;
window.parent.mejs.players[playerId].play();
});
});
</script>';
$html = Display::tag(
'p',
Display::toolbarButton(
get_lang('GoBackToVideo'),
'#',
'undo',
'warning',
['role' => 'button', 'class' => 'btn-close-quiz']
),
['class' => 'text-center']
);
// We check if a tool provider
if (isset($_REQUEST['lti_launch_id'])) {
$ltiLaunchId = Security::remove_XSS($_REQUEST['lti_launch_id']);
global $exeId;
$js .= '<script>
$(function () {
var url = "'.api_get_path(WEB_PLUGIN_PATH).'lti_provider/tool/api/score.php?'.api_get_cidreq().'&lti_tool=quiz&launch_id='.$ltiLaunchId.'&lti_result_id='.$exeId.'";
$.get(url);
});
</script>';
}
return $js.PHP_EOL.$html;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,451 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CourseBundle\Entity\CQuizAnswer;
use ChamiloSession as Session;
/**
* @author Julio Montoya <gugli100@gmail.com>
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
api_protect_course_script();
require_once api_get_path(LIBRARY_PATH).'geometry.lib.php';
/** @var Exercise $objExercise */
$objExercise = Session::read('objExercise');
$exerciseResult = Session::read('exerciseResult');
if (empty($objExercise)) {
api_not_allowed();
}
$feedbackType = $objExercise->getFeedbackType();
$exerciseType = $objExercise->type;
if (!in_array($feedbackType, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
api_not_allowed();
}
$learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : 0;
$learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : 0;
$questionList = Session::read('questionList');
$exerciseId = (int) $_GET['exerciseId'];
$questionNum = (int) $_GET['num'];
$questionId = $questionList[$questionNum];
$choiceValue = $_GET['choice'] ?? '';
$hotSpot = $_GET['hotspot'] ?? '';
$tryAgain = isset($_GET['tryagain']) && 1 === (int) $_GET['tryagain'];
$allowTryAgain = false;
if ($tryAgain) {
// Check if try again exists in this question, otherwise only allow one attempt BT#15827.
$objQuestionTmp = Question::read($questionId);
$answerType = $objQuestionTmp->selectType();
$showResult = false;
$objAnswerTmp = new Answer($questionId, api_get_course_int_id());
$answers = $objAnswerTmp->getAnswers();
if (!empty($answers)) {
foreach ($answers as $answerData) {
if (isset($answerData['destination'])) {
$itemList = explode('@@', $answerData['destination']);
if (isset($itemList[0]) && !empty($itemList[0])) {
$allowTryAgain = true;
break;
}
}
}
}
}
$loaded = isset($_GET['loaded']);
if ($allowTryAgain || $feedbackType == EXERCISE_FEEDBACK_TYPE_DIRECT) {
unset($exerciseResult[$questionId]);
}
if (empty($choiceValue) && isset($exerciseResult[$questionId])) {
$choiceValue = $exerciseResult[$questionId];
}
if (!empty($hotSpot)) {
if (isset($hotSpot[$questionId])) {
$hotSpot = $hotSpot[$questionId];
}
}
if (!empty($choiceValue)) {
if (isset($choiceValue[$questionId])) {
$choiceValue = $choiceValue[$questionId];
}
}
$header = '';
$exeId = 0;
if ($objExercise->getFeedbackType() === EXERCISE_FEEDBACK_TYPE_POPUP) {
$exeId = Session::read('exe_id');
$header = '
<div class="modal-header">
<h4 class="modal-title" id="global-modal-title">'.get_lang('Incorrect').'</h4>
</div>';
}
echo '<script>
function tryAgain() {
$(function () {
$("#global-modal").modal("hide");
});
}
function SendEx(num) {
if (num == -1) {
window.location.href = "exercise_result.php?'.api_get_cidreq().'&exe_id='.$exeId.'&take_session=1&exerciseId='.$exerciseId.'&num="+num+"&learnpath_item_id='.$learnpath_item_id.'&learnpath_id='.$learnpath_id.'";
} else {
num -= 1;
window.location.href = "exercise_submit.php?'.api_get_cidreq().'&tryagain=1&exerciseId='.$exerciseId.'&num="+num+"&learnpath_item_id='.$learnpath_item_id.'&learnpath_id='.$learnpath_id.'";
}
return false;
}
</script>';
echo '<div id="delineation-container">';
// Getting the options by js
if (empty($choiceValue) && empty($hotSpot) && $loaded) {
$nextQuestion = $questionNum + 1;
$destinationId = $questionList[$nextQuestion] ?? -1;
$icon = Display::return_icon(
'reload.png',
'',
['style' => 'width:22px; height:22px; padding-left:0px;padding-right:5px;']
);
$links = '<a onclick="tryAgain();" href="#">'.get_lang('TryAgain').'</a>&nbsp;'.$icon.'&nbsp;';
// the link to finish the test
if (-1 == $destinationId) {
$links .= Display::return_icon(
'finish.gif',
'',
['style' => 'width:22px; height:22px; padding-left:0px;padding-right:5px;']
).'<a onclick="SendEx(-1);" href="#">'.get_lang('EndActivity').'</a><br /><br />';
} else {
// the link to other question
if (in_array($destinationId, $questionList)) {
$num_value_array = array_keys($questionList, $destinationId);
$icon = Display::return_icon(
'quiz.png',
'',
['style' => 'padding-left:0px;padding-right:5px;']
);
$links .= '<a onclick="SendEx('.$num_value_array[0].');" href="#">'.
get_lang('Question').' '.$num_value_array[0].'</a>&nbsp;';
$links .= $icon;
}
}
echo $header;
echo '<div class="row"><div class="col-md-5 col-md-offset-7"><h5 class="pull-right">'.$links.'</h5></div></div>';
exit;
}
if (empty($choiceValue) && empty($hotSpot)) {
echo "<script>
// this works for only radio buttons
var f = window.document.frm_exercise;
var choice_js = {answers: []};
var hotspot = new Array();
var hotspotcoord = new Array();
var counter = 0;
for (var i = 0; i < f.elements.length; i++) {
if (f.elements[i].type == 'radio' && f.elements[i].checked) {
choice_js.answers.push(f.elements[i].value);
counter ++;
}
if (f.elements[i].type == 'checkbox' && f.elements[i].checked) {
choice_js.answers.push(f.elements[i].value);
counter ++;
}
if (f.elements[i].type == 'hidden') {
var name = f.elements[i].name;
if (name.substr(0,7) == 'hotspot') {
hotspot.push(f.elements[i].value);
}
if (name.substr(0,20) == 'hotspot_coordinates') {
hotspotcoord.push(f.elements[i].value);
}
}
}
var my_choice = $('*[name*=\"choice[".$questionId."]\"]').serialize();
var hotspot = $('*[name*=\"hotspot[".$questionId."]\"]').serialize();
";
// IMPORTANT
// This is the real redirect function
$extraUrl = '&loaded=1&exerciseId='.$exerciseId.'&num='.$questionNum.'&learnpath_id='.$learnpath_id.'&learnpath_item_id='.$learnpath_item_id;
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit_modal.php?'.api_get_cidreq().$extraUrl;
echo ' url = "'.addslashes($url).'&hotspotcoord="+ hotspotcoord + "&"+ hotspot + "&"+ my_choice;';
echo "$('#global-modal .modal-body').load(url);";
echo '</script>';
exit;
}
$choice = [];
$choice[$questionId] = $choiceValue ?? null;
if (!is_array($exerciseResult)) {
$exerciseResult = [];
}
$saveResults = (int) $objExercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_POPUP;
// if the user has answered at least one question
if (is_array($choice)) {
if (in_array($exerciseType, [EXERCISE_FEEDBACK_TYPE_DIRECT, EXERCISE_FEEDBACK_TYPE_POPUP])) {
// $exerciseResult receives the content of the form.
// Each choice of the student is stored into the array $choice
$exerciseResult = $choice;
} else {
// gets the question ID from $choice. It is the key of the array
[$key] = array_keys($choice);
// if the user didn't already answer this question
if (!isset($exerciseResult[$key])) {
// stores the user answer into the array
$exerciseResult[$key] = $choice[$key];
}
}
}
// the script "exercise_result.php" will take the variable $exerciseResult from the session
Session::write('exerciseResult', $exerciseResult);
$objQuestionTmp = Question::read($questionId);
$answerType = $objQuestionTmp->selectType();
$showResult = false;
$objAnswerTmp = new Answer($questionId, api_get_course_int_id());
if (EXERCISE_FEEDBACK_TYPE_DIRECT === $objExercise->getFeedbackType()) {
$showResult = true;
}
switch ($answerType) {
case MULTIPLE_ANSWER:
if (is_array($choiceValue)) {
$choiceValue = array_combine(array_values($choiceValue), array_values($choiceValue));
}
break;
case UNIQUE_ANSWER:
if (is_array($choiceValue) && isset($choiceValue[0])) {
$choiceValue = $choiceValue[0];
}
break;
case DRAGGABLE:
break;
case HOT_SPOT_DELINEATION:
$showResult = true;
if (is_array($hotSpot)) {
$choiceValue = $hotSpot[1] ?? '';
$_SESSION['exerciseResultCoordinates'][$questionId] = $choiceValue; //needed for exercise_result.php
$delineation_cord = $objAnswerTmp->selectHotspotCoordinates(1);
$answer_delineation_destination = $objAnswerTmp->selectDestination(1);
$_SESSION['hotspot_coord'][$questionId][1] = $delineation_cord;
$_SESSION['hotspot_dest'][$questionId][1] = $answer_delineation_destination;
}
break;
case CALCULATED_ANSWER:
break;
}
ob_start();
$result = $objExercise->manage_answer(
$exeId,
$questionId,
$choiceValue,
'exercise_result',
[],
$saveResults,
false,
$showResult,
null,
[],
true,
false,
true
);
$manageAnswerHtmlContent = ob_get_clean();
$contents = '';
$answerCorrect = false;
$partialCorrect = false;
if (!empty($result)) {
switch ($answerType) {
case MULTIPLE_ANSWER:
case UNIQUE_ANSWER:
case DRAGGABLE:
case HOT_SPOT_DELINEATION:
case CALCULATED_ANSWER:
if ($result['score'] == $result['weight']) {
$answerCorrect = true;
}
// Check partial correct
if (false === $answerCorrect) {
if (!empty($result['score'])) {
$partialCorrect = true;
}
}
break;
}
}
$header = '';
if ($objExercise->getFeedbackType() === EXERCISE_FEEDBACK_TYPE_DIRECT) {
if (isset($result['correct_answer_id'])) {
foreach ($result['correct_answer_id'] as $answerId) {
/** @var Answer $answer */
$contents .= $objAnswerTmp->selectComment($answerId);
}
}
} else {
$message = get_lang('Incorrect');
if ($answerCorrect) {
$message = get_lang('Correct');
} else {
if ($partialCorrect) {
$message = get_lang('PartialCorrect');
}
}
$comments = '';
if ($answerType != HOT_SPOT_DELINEATION) {
if (isset($result['correct_answer_id'])) {
$table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
$row = 0;
$table->setCellContents($row, 0, get_lang('YourAnswer'));
if ($answerType != DRAGGABLE) {
$table->setCellContents($row, 1, get_lang('Comment'));
}
$data = [];
foreach ($result['correct_answer_id'] as $answerId) {
$answer = $objAnswerTmp->getAnswerByAutoId($answerId);
if (!empty($answer) && isset($answer['comment'])) {
$data[] = [$answer['answer'], $answer['comment']];
} else {
$answer = $objAnswerTmp->selectAnswer($answerId);
$comment = $objAnswerTmp->selectComment($answerId);
$data[] = [$answer, $comment];
}
}
if (!empty($data)) {
$row = 1;
foreach ($data as $dataItem) {
$table->setCellContents($row, 0, $dataItem[0]);
$table->setCellContents($row, 1, $dataItem[1]);
$row++;
}
$comments = $table->toHtml();
}
}
}
$contents .= $comments;
$header = '
<div class="modal-header">
<h4 class="modal-title" id="global-modal-title">'.$message.'</h4>
</div>';
}
if ($answerType === HOT_SPOT_DELINEATION) {
$contents = $manageAnswerHtmlContent;
}
$links = '';
if (EXERCISE_FEEDBACK_TYPE_DIRECT === $objExercise->getFeedbackType()) {
if (isset($choiceValue) && -1 == $choiceValue) {
if (HOT_SPOT_DELINEATION != $answerType) {
$links .= '<a href="#" onclick="tb_remove();">'.get_lang('ChooseAnAnswer').'</a><br />';
}
}
}
$destinationId = null;
if (isset($result['answer_destination']) && CQuizAnswer::DEFAULT_DESTINATION !== $result['answer_destination']) {
$itemList = explode('@@', $result['answer_destination']);
$try = $itemList[0];
$lp = $itemList[1];
$destinationId = $itemList[2];
$url = $itemList[3];
}
// the link to retry the question
if (isset($try) && 1 == $try) {
$num_value_array = array_keys($questionList, $questionId);
$links .= Display::return_icon(
'reload.gif',
'',
['style' => 'padding-left:0px;padding-right:5px;']
).'<a onclick="SendEx('.$num_value_array[0].');" href="#">'.get_lang('TryAgain').'</a><br /><br />';
}
// the link to theory (a learning path)
if (!empty($lp)) {
$lp_url = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?'.api_get_cidreq().'&action=view&lp_id='.$lp;
$links .= Display::return_icon(
'theory.gif',
'',
['style' => 'padding-left:0px;padding-right:5px;']
).'<a target="_blank" href="'.$lp_url.'">'.get_lang('SeeTheory').'</a><br />';
}
$links .= '<br />';
// the link to an external website or link
if (!empty($url) && $url != -1) {
$links .= Display::return_icon(
'link.gif',
'',
['style' => 'padding-left:0px;padding-right:5px;']
).'<a target="_blank" href="'.$url.'">'.get_lang('VisitUrl').'</a><br /><br />';
}
if (null === $destinationId) {
$nextQuestion = $questionNum + 1;
$destinationId = $questionList[$nextQuestion] ?? -1;
}
// the link to finish the test
if (-1 == $destinationId) {
$links .= Display::return_icon(
'finish.gif',
'',
['style' => 'width:22px; height:22px; padding-left:0px;padding-right:5px;']
).'<a onclick="SendEx(-1);" href="#">'.get_lang('EndActivity').'</a><br /><br />';
} else {
// the link to other question
if (in_array($destinationId, $questionList)) {
$num_value_array = array_keys($questionList, $destinationId);
$icon = Display::return_icon(
'quiz.png',
'',
['style' => 'padding-left:0px;padding-right:5px;']
);
$links .= '<a onclick="SendEx('.$num_value_array[0].');" href="#">'.
get_lang('Question').' '.$num_value_array[0].'</a>&nbsp;';
$links .= $icon;
}
}
if (!empty($links)) {
echo $header;
echo '<div>'.$contents.'</div>';
echo '<div style="padding-left: 450px"><h5>'.$links.'</h5></div>';
echo '</div>';
} else {
$questionNum++;
echo '<script>
window.location.href = "exercise_submit.php?exerciseId='.$exerciseId.'&num='.$questionNum.'&'.api_get_cidreq().'";
</script>';
}
echo '</div>';

View File

@@ -0,0 +1,50 @@
<?php
/* For licensing terms, see /license.txt */
/**
* @author Claro Team <cvs@claroline.net>
* @author Yannick Warnier <yannick.warnier@beeznest.com> - updated ImsAnswerHotspot to match QTI norms
* @author César Perales <cesar.perales@gmail.com> Updated function names and import files for Aiken format support
*/
/**
* Aiken2Question transformation class.
*/
class Aiken2Question extends Question
{
/**
* Include the correct answer class and create answer.
*/
public function setAnswer()
{
switch ($this->type) {
case MCUA:
$answer = new AikenAnswerMultipleChoice($this->iid);
return $answer;
default:
$answer = null;
break;
}
return $answer;
}
public function createAnswersForm($form)
{
return true;
}
public function processAnswersCreation($form, $exercise)
{
return true;
}
}
/**
* Class.
*/
class AikenAnswerMultipleChoice extends Answer
{
}

View File

@@ -0,0 +1,611 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Library for the import of Aiken format.
*
* @author claro team <cvs@claroline.net>
* @author Guillaume Lederer <guillaume@claroline.net>
* @author César Perales <cesar.perales@gmail.com> Parse function for Aiken format
*/
/**
* This function displays the form for import of the zip file with qti2.
*
* @param string Report message to show in case of error
*/
function aiken_display_form()
{
$name_tools = get_lang('ImportAikenQuiz');
$form = '<div class="actions">';
$form .= '<a href="exercise.php?show=test&'.api_get_cidreq().'">'.
Display::return_icon(
'back.png',
get_lang('BackToExercisesList'),
'',
ICON_SIZE_MEDIUM
).'</a>';
$form .= '</div>';
$form_validator = new FormValidator(
'aiken_upload',
'post',
api_get_self()."?".api_get_cidreq(),
null,
['enctype' => 'multipart/form-data']
);
$form_validator->addElement('header', $name_tools);
$form_validator->addElement('text', 'total_weight', get_lang('TotalWeight'));
$form_validator->addElement('file', 'userFile', get_lang('File'));
$form_validator->addButtonUpload(get_lang('Upload'), 'submit');
$form .= $form_validator->returnForm();
$form .= '<blockquote>'.get_lang('ImportAikenQuizExplanation').'<br /><pre>'.get_lang('ImportAikenQuizExplanationExample').'</pre></blockquote>';
echo $form;
}
/**
* Generates Aiken format using AI APIs (supports multiple providers).
* Requires plugin ai_helper to connect to the API.
*/
function generateAikenForm()
{
if (!('true' === api_get_plugin_setting('ai_helper', 'tool_enable') && 'true' === api_get_plugin_setting('ai_helper', 'tool_quiz_enable'))) {
return false;
}
$plugin = AiHelperPlugin::create();
$availableApis = $plugin->getApiList();
$configuredApi = $plugin->get('api_name');
$hasSingleApi = count($availableApis) === 1 || isset($availableApis[$configuredApi]);
$form = new FormValidator(
'aiken_generate',
'post',
api_get_self()."?".api_get_cidreq(),
null
);
$form->addElement('header', get_lang('AIQuestionsGenerator'));
if ($hasSingleApi) {
$apiName = $availableApis[$configuredApi] ?? $configuredApi;
$form->addHtml('<div style="margin-bottom: 10px; font-size: 14px; color: #555;">'
.sprintf(get_lang('UsingAIProviderX'), '<strong>'.htmlspecialchars($apiName).'</strong>').'</div>');
}
$form->addElement('text', 'quiz_name', get_lang('QuestionsTopic'));
$form->addRule('quiz_name', get_lang('ThisFieldIsRequired'), 'required');
$form->addElement('number', 'nro_questions', get_lang('NumberOfQuestions'));
$form->addRule('nro_questions', get_lang('ThisFieldIsRequired'), 'required');
$options = [
'multiple_choice' => get_lang('MultipleAnswer'),
];
$form->addElement('select', 'question_type', get_lang('QuestionType'), $options);
if (!$hasSingleApi) {
$form->addElement(
'select',
'ai_provider',
get_lang('AIProvider'),
array_combine(array_keys($availableApis), array_keys($availableApis))
);
}
$generateUrl = api_get_path(WEB_PLUGIN_PATH).'ai_helper/tool/answers.php';
$language = api_get_interface_language();
$form->addHtml('<script>
$(function () {
$("#aiken-area").hide();
$("#generate-aiken").on("click", function (e) {
e.preventDefault();
e.stopPropagation();
var btnGenerate = $(this);
var quizName = $("[name=\'quiz_name\']").val();
var nroQ = parseInt($("[name=\'nro_questions\']").val());
var qType = $("[name=\'question_type\']").val();'
.(!$hasSingleApi ? 'var provider = $("[name=\'ai_provider\']").val();' : 'var provider = "'.$configuredApi.'";').
'var valid = (quizName != \'\' && nroQ > 0);
if (valid) {
btnGenerate.attr("disabled", true);
btnGenerate.text("'.get_lang('PleaseWaitThisCouldTakeAWhile').'");
$("#textarea-aiken").text("");
$("#aiken-area").hide();
$.getJSON("'.$generateUrl.'", {
"quiz_name": quizName,
"nro_questions": nroQ,
"question_type": qType,
"language": "'.$language.'",
"ai_provider": provider
}).done(function (data) {
btnGenerate.attr("disabled", false);
btnGenerate.text("'.get_lang('Generate').'");
if (data.success && data.success == true) {
$("#aiken-area").show();
$("#textarea-aiken").text(data.text);
$("input[name=\'ai_total_weight\']").val(nroQ * qWeight);
$("#textarea-aiken").focus();
} else {
var errorMessage = "'.get_lang('NoSearchResults').'. '.get_lang('PleaseTryAgain').'";
if (data.text) {
errorMessage = data.text;
}
alert(errorMessage);
}
});
}
});
});
</script>');
$form->addButton(
'generate_aiken_button',
get_lang('Generate'),
'',
'default',
'default',
null,
['id' => 'generate-aiken']
);
$form->addHtml('<div id="aiken-area">');
$form->addElement(
'textarea',
'aiken_format',
get_lang('Answers'),
[
'id' => 'textarea-aiken',
'style' => 'width: 100%; height: 250px;',
]
);
$form->addElement('number', 'ai_total_weight', get_lang('TotalWeight'));
$form->addButtonImport(get_lang('Import'), 'submit_aiken_generated');
$form->addHtml('</div>');
echo $form->returnForm();
}
/**
* Gets the uploaded file (from $_FILES) and unzip it to the given directory.
*
* @param string The directory where to do the work
* @param string The path of the temporary directory where the exercise was uploaded and unzipped
* @param string $baseWorkDir
* @param string $uploadPath
*
* @return bool True on success, false on failure
*/
function get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)
{
$_course = api_get_course_info();
$_user = api_get_user_info();
// Check if the file is valid (not to big and exists)
if (!isset($_FILES['userFile']) || !is_uploaded_file($_FILES['userFile']['tmp_name'])) {
// upload failed
return false;
}
if (preg_match('/.zip$/i', $_FILES['userFile']['name']) &&
handle_uploaded_document(
$_course,
$_FILES['userFile'],
$baseWorkDir,
$uploadPath,
$_user['user_id'],
0,
null,
1,
'overwrite',
false,
true
)
) {
if (!function_exists('gzopen')) {
return false;
}
// upload successful
return true;
} elseif (preg_match('/.txt/i', $_FILES['userFile']['name']) &&
handle_uploaded_document(
$_course,
$_FILES['userFile'],
$baseWorkDir,
$uploadPath,
$_user['user_id'],
0,
null,
0,
'overwrite',
false
)
) {
return true;
}
return false;
}
/**
* Main function to import the Aiken exercise.
*
* @param string $file
* @param array $request
*
* @return mixed True on success, error message on failure
*/
function aikenImportExercise($file = null, $request = [])
{
$exerciseInfo = [];
$fileIsSet = false;
if (isset($file)) {
$fileIsSet = true;
// The import is from aiken file format.
$archivePath = api_get_path(SYS_ARCHIVE_PATH).'aiken/';
$baseWorkDir = $archivePath;
$uploadPath = 'aiken_'.api_get_unique_id();
if (!is_dir($baseWorkDir.$uploadPath)) {
mkdir($baseWorkDir.$uploadPath, api_get_permissions_for_new_directories(), true);
}
// set some default values for the new exercise
$exerciseInfo['name'] = preg_replace('/.(zip|txt)$/i', '', $file);
$exerciseInfo['total_weight'] = !empty($_POST['total_weight']) ? (int) ($_POST['total_weight']) : 20;
$exerciseInfo['question'] = [];
// if file is not a .zip, then we cancel all
if (!preg_match('/.(zip|txt)$/i', $file)) {
return 'YouMustUploadAZipOrTxtFile';
}
// unzip the uploaded file in a tmp directory
if (preg_match('/.(zip|txt)$/i', $file)) {
if (!get_and_unzip_uploaded_exercise($baseWorkDir.$uploadPath, '/')) {
return 'ThereWasAProblemWithYourFile';
}
}
// find the different manifests for each question and parse them
$exerciseHandle = opendir($baseWorkDir.$uploadPath);
$fileFound = false;
$operation = false;
$result = false;
// Parse every subdirectory to search txt question files
while (false !== ($file = readdir($exerciseHandle))) {
if (is_dir($baseWorkDir.'/'.$uploadPath.$file) && $file != "." && $file != "..") {
//find each manifest for each question repository found
$questionHandle = opendir($baseWorkDir.'/'.$uploadPath.$file);
while (false !== ($questionFile = readdir($questionHandle))) {
if (preg_match('/.txt$/i', $questionFile)) {
$result = aiken_parse_file(
$exerciseInfo,
$baseWorkDir,
$file,
$questionFile
);
$fileFound = true;
}
}
} elseif (preg_match('/.txt$/i', $file)) {
$result = aiken_parse_file($exerciseInfo, $baseWorkDir.$uploadPath, '', $file);
$fileFound = true;
}
}
if (!$fileFound) {
$result = 'NoTxtFileFoundInTheZip';
}
if (true !== $result) {
return $result;
}
} elseif (!empty($request)) {
// The import is from aiken generated in textarea.
$exerciseInfo['name'] = $request['quiz_name'];
$exerciseInfo['total_weight'] = !empty($_POST['ai_total_weight']) ? (int) ($_POST['ai_total_weight']) : (int) $request['nro_questions'];
$exerciseInfo['question'] = [];
$exerciseInfo['course_id'] = isset($request['course_id']) ? (int) $request['course_id'] : 0;
setExerciseInfoFromAikenText($request['aiken_format'], $exerciseInfo);
}
// 1. Create exercise.
if (!empty($exerciseInfo)) {
$exercise = new Exercise($exerciseInfo['course_id']);
$exercise->exercise = $exerciseInfo['name'];
$exercise->disable(); // Invisible by default
$exercise->updateResultsDisabled(0); // Auto-evaluation mode: show score and expected answers
$exercise->save();
$lastExerciseId = $exercise->selectId();
$tableQuestion = Database::get_course_table(TABLE_QUIZ_QUESTION);
$tableAnswer = Database::get_course_table(TABLE_QUIZ_ANSWER);
if (!empty($lastExerciseId)) {
$courseId = !empty($exerciseInfo['course_id']) ? (int) $exerciseInfo['course_id'] : api_get_course_int_id();
foreach ($exerciseInfo['question'] as $key => $questionArray) {
if (!isset($questionArray['title'])) {
continue;
}
// 2. Create question.
$question = new Aiken2Question();
$question->type = $questionArray['type'];
$question->setAnswer();
$question->updateTitle($questionArray['title']);
if (isset($questionArray['description'])) {
$question->updateDescription($questionArray['description']);
}
$type = $question->selectType();
$question->course = api_get_course_info_by_id($courseId);
$question->type = constant($type);
$question->save($exercise);
$last_question_id = $question->selectId();
// 3. Create answer
$answer = new Answer($last_question_id, $courseId, $exercise, false);
$answer->new_nbrAnswers = isset($questionArray['answer']) ? count($questionArray['answer']) : 0;
$max_score = 0;
$scoreFromFile = 0;
if (isset($questionArray['score']) && !empty($questionArray['score'])) {
$scoreFromFile = $questionArray['score'];
}
foreach ($questionArray['answer'] as $key => $answers) {
$key++;
$answer->new_answer[$key] = $answers['value'];
$answer->new_position[$key] = $key;
$answer->new_comment[$key] = '';
// Correct answers ...
if (isset($questionArray['correct_answers']) &&
in_array($key, $questionArray['correct_answers'])
) {
$answer->new_correct[$key] = 1;
if (isset($questionArray['feedback'])) {
$answer->new_comment[$key] = $questionArray['feedback'];
}
} else {
$answer->new_correct[$key] = 0;
}
if (isset($questionArray['weighting'][$key - 1])) {
$answer->new_weighting[$key] = $questionArray['weighting'][$key - 1];
$max_score += $questionArray['weighting'][$key - 1];
}
if (!empty($scoreFromFile) && $answer->new_correct[$key]) {
$answer->new_weighting[$key] = $scoreFromFile;
}
$params = [
'c_id' => $courseId,
'question_id' => $last_question_id,
'answer' => $answer->new_answer[$key],
'correct' => $answer->new_correct[$key],
'comment' => $answer->new_comment[$key],
'ponderation' => isset($answer->new_weighting[$key]) ? $answer->new_weighting[$key] : '',
'position' => $answer->new_position[$key],
'hotspot_coordinates' => '',
'hotspot_type' => '',
];
$answerId = Database::insert($tableAnswer, $params);
if ($answerId) {
$params = [
'id_auto' => $answerId,
'iid' => $answerId,
];
Database::update($tableAnswer, $params, ['iid = ?' => [$answerId]]);
}
}
if (!empty($scoreFromFile)) {
$max_score = $scoreFromFile;
}
$params = ['ponderation' => $max_score];
Database::update(
$tableQuestion,
$params,
['iid = ?' => [$last_question_id]]
);
}
// Delete the temp dir where the exercise was unzipped
if ($fileIsSet) {
my_delete($baseWorkDir.$uploadPath);
}
// Invisible by default
api_item_property_update(
api_get_course_info(),
TOOL_QUIZ,
$lastExerciseId,
'invisible',
api_get_user_id()
);
return $lastExerciseId;
}
}
return false;
}
/**
* Set the exercise information from an aiken text formatted.
*/
function setExerciseInfoFromAikenText($aikenText, &$exerciseInfo)
{
$detect = mb_detect_encoding($aikenText, 'ASCII', true);
if ('ASCII' === $detect) {
$data = explode("\n", $aikenText);
} else {
if (false !== stripos($aikenText, "\x0D") || false !== stripos($aikenText, "\r\n")) {
$text = str_ireplace(["\x0D", "\r\n"], "\n", $aikenText); // Removes ^M char from win files.
$data = explode("\n\n", $text);
} else {
$data = explode("\n", $aikenText);
}
}
$questionIndex = 0;
$answersArray = [];
foreach ($data as $line => $info) {
$info = trim($info);
if (empty($info)) {
continue;
}
//make sure it is transformed from iso-8859-1 to utf-8 if in that form
if (!mb_check_encoding($info, 'utf-8') && mb_check_encoding($info, 'iso-8859-1')) {
$info = utf8_encode($info);
}
$exerciseInfo['question'][$questionIndex]['type'] = 'MCUA';
if (preg_match('/^([A-Za-z])(\)|\.)\s(.*)/', $info, $matches)) {
//adding one of the possible answers
$exerciseInfo['question'][$questionIndex]['answer'][]['value'] = $matches[3];
$answersArray[] = $matches[1];
} elseif (preg_match('/^ANSWER:\s?([A-Z])\s?/', $info, $matches)) {
//the correct answers
$correctAnswerIndex = array_search($matches[1], $answersArray);
$exerciseInfo['question'][$questionIndex]['correct_answers'][] = $correctAnswerIndex + 1;
//weight for correct answer
$exerciseInfo['question'][$questionIndex]['weighting'][$correctAnswerIndex] = 1;
$next = $line + 1;
if (isset($data[$next]) && false !== strpos($data[$next], 'ANSWER_EXPLANATION:')) {
continue;
}
if (isset($data[$next]) && false !== strpos($data[$next], 'DESCRIPTION:')) {
continue;
}
// Check if next has score, otherwise loop too next question.
if (isset($data[$next]) && false === strpos($data[$next], 'SCORE:')) {
$answersArray = [];
$questionIndex++;
continue;
}
} elseif (preg_match('/^SCORE:\s?(.*)/', $info, $matches)) {
$exerciseInfo['question'][$questionIndex]['score'] = (float) $matches[1];
$answersArray = [];
$questionIndex++;
continue;
} elseif (preg_match('/^DESCRIPTION:\s?(.*)/', $info, $matches)) {
$exerciseInfo['question'][$questionIndex]['description'] = $matches[1];
$next = $line + 1;
if (isset($data[$next]) && false !== strpos($data[$next], 'ANSWER_EXPLANATION:')) {
continue;
}
// Check if next has score, otherwise loop too next question.
if (isset($data[$next]) && false === strpos($data[$next], 'SCORE:')) {
$answersArray = [];
$questionIndex++;
continue;
}
} elseif (preg_match('/^ANSWER_EXPLANATION:\s?(.*)/', $info, $matches)) {
// Comment of correct answer
$correctAnswerIndex = array_search($matches[1], $answersArray);
$exerciseInfo['question'][$questionIndex]['feedback'] = $matches[1];
$next = $line + 1;
// Check if next has score, otherwise loop too next question.
if (isset($data[$next]) && false === strpos($data[$next], 'SCORE:')) {
$answersArray = [];
$questionIndex++;
continue;
}
} elseif (preg_match('/^TEXTO_CORRECTA:\s?(.*)/', $info, $matches)) {
//Comment of correct answer (Spanish e-ducativa format)
$correctAnswerIndex = array_search($matches[1], $answersArray);
$exerciseInfo['question'][$questionIndex]['feedback'] = $matches[1];
} elseif (preg_match('/^T:\s?(.*)/', $info, $matches)) {
//Question Title
$correctAnswerIndex = array_search($matches[1], $answersArray);
$exerciseInfo['question'][$questionIndex]['title'] = $matches[1];
} elseif (preg_match('/^TAGS:\s?([A-Z])\s?/', $info, $matches)) {
//TAGS for chamilo >= 1.10
$exerciseInfo['question'][$questionIndex]['answer_tags'] = explode(',', $matches[1]);
} elseif (preg_match('/^ETIQUETAS:\s?([A-Z])\s?/', $info, $matches)) {
//TAGS for chamilo >= 1.10 (Spanish e-ducativa format)
$exerciseInfo['question'][$questionIndex]['answer_tags'] = explode(',', $matches[1]);
} else {
if (empty($exerciseInfo['question'][$questionIndex]['title'])) {
$exerciseInfo['question'][$questionIndex]['title'] = $info;
}
}
}
$totalQuestions = count($exerciseInfo['question']);
$totalWeight = (int) $exerciseInfo['total_weight'];
foreach ($exerciseInfo['question'] as $key => $question) {
if (!isset($exerciseInfo['question'][$key]['weighting'])) {
continue;
}
$exerciseInfo['question'][$key]['weighting'][current(array_keys($exerciseInfo['question'][$key]['weighting']))] = $totalWeight / $totalQuestions;
}
}
/**
* Parses an Aiken file and builds an array of exercise + questions to be
* imported by the import_exercise() function.
*
* @param array The reference to the array in which to store the questions
* @param string Path to the directory with the file to be parsed (without final /)
* @param string Name of the last directory part for the file (without /)
* @param string Name of the file to be parsed (including extension)
* @param string $exercisePath
* @param string $file
* @param string $questionFile
*
* @return string|bool True on success, error message on error
* @assert ('','','') === false
*/
function aiken_parse_file(&$exercise_info, $exercisePath, $file, $questionFile)
{
$questionTempDir = $exercisePath.'/'.$file.'/';
$questionFilePath = $questionTempDir.$questionFile;
if (!is_file($questionFilePath)) {
return 'FileNotFound';
}
$text = file_get_contents($questionFilePath);
setExerciseInfoFromAikenText($text, $exercise_info);
return true;
}
/**
* Imports the zip file.
*
* @param array $array_file ($_FILES)
*
* @return bool
*/
function aiken_import_file($array_file)
{
$unzip = 0;
$process = process_uploaded_file($array_file, false);
if (preg_match('/\.(zip|txt)$/i', $array_file['name'])) {
// if it's a zip, allow zip upload
$unzip = 1;
}
if ($process && $unzip == 1) {
$imported = aikenImportExercise($array_file['name']);
if (is_numeric($imported) && !empty($imported)) {
Display::addFlash(Display::return_message(get_lang('Uploaded')));
return $imported;
} else {
Display::addFlash(Display::return_message(get_lang($imported), 'error'));
return false;
}
}
}

View File

@@ -0,0 +1,737 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
use Symfony\Component\DomCrawler\Crawler;
/**
* @copyright (c) 2001-2006 Universite catholique de Louvain (UCL)
* @author claro team <cvs@claroline.net>
* @author Guillaume Lederer <guillaume@claroline.net>
* @author Yannick Warnier <yannick.warnier@beeznest.com>
*/
/**
* Unzip the exercise in the temp folder.
*
* @param string $baseWorkDir The path of the temporary directory where the exercise was uploaded and unzipped
* @param string $uploadPath
*
* @return bool
*/
function get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)
{
$_course = api_get_course_info();
$_user = api_get_user_info();
//Check if the file is valid (not to big and exists)
if (!isset($_FILES['userFile']) || !is_uploaded_file($_FILES['userFile']['tmp_name'])) {
// upload failed
return false;
}
if (preg_match('/.zip$/i', $_FILES['userFile']['name'])) {
return handle_uploaded_document(
$_course,
$_FILES['userFile'],
$baseWorkDir,
$uploadPath,
$_user['user_id'],
0,
null,
1,
null,
null,
true,
null,
null,
false
);
}
return false;
}
/**
* Imports an exercise in QTI format if the XML structure can be found in it.
*
* @param array $file
*
* @return string|array as a backlog of what was really imported, and error or debug messages to display
*/
function import_exercise($file)
{
global $exerciseInfo;
global $resourcesLinks;
$baseWorkDir = api_get_path(SYS_ARCHIVE_PATH).'qti2/';
if (!is_dir($baseWorkDir)) {
mkdir($baseWorkDir, api_get_permissions_for_new_directories(), true);
}
$uploadPath = api_get_unique_id().'/';
if (!is_dir($baseWorkDir.$uploadPath)) {
mkdir($baseWorkDir.$uploadPath, api_get_permissions_for_new_directories(), true);
}
// set some default values for the new exercise
$exerciseInfo = [];
$exerciseInfo['name'] = preg_replace('/.zip$/i', '', $file);
$exerciseInfo['question'] = [];
// if file is not a .zip, then we cancel all
if (!preg_match('/.zip$/i', $file)) {
return 'UplZipCorrupt';
}
// unzip the uploaded file in a tmp directory
if (!get_and_unzip_uploaded_exercise($baseWorkDir, $uploadPath)) {
return 'UplZipCorrupt';
}
$baseWorkDir = $baseWorkDir.$uploadPath;
// find the different manifests for each question and parse them.
$exerciseHandle = opendir($baseWorkDir);
$fileFound = false;
$result = false;
$filePath = null;
$resourcesLinks = [];
// parse every subdirectory to search xml question files and other assets to be imported
// The assets-related code is a bit fragile as it has to deal with files renamed by Chamilo and it only works if
// the imsmanifest.xml file is read.
while (false !== ($file = readdir($exerciseHandle))) {
if (is_dir($baseWorkDir.'/'.$file) && $file != "." && $file != "..") {
// Find each manifest for each question repository found
$questionHandle = opendir($baseWorkDir.'/'.$file);
// Only analyse one level of subdirectory - no recursivity here
while (false !== ($questionFile = readdir($questionHandle))) {
if (preg_match('/.xml$/i', $questionFile)) {
$isQti = isQtiQuestionBank($baseWorkDir.'/'.$file.'/'.$questionFile);
if ($isQti) {
$result = qti_parse_file($baseWorkDir, $file, $questionFile);
$filePath = $baseWorkDir.$file;
$fileFound = true;
} else {
$isManifest = isQtiManifest($baseWorkDir.'/'.$file.'/'.$questionFile);
if ($isManifest) {
$resourcesLinks = qtiProcessManifest($baseWorkDir.'/'.$file.'/'.$questionFile);
}
}
}
}
} elseif (preg_match('/.xml$/i', $file)) {
$isQti = isQtiQuestionBank($baseWorkDir.'/'.$file);
if ($isQti) {
$result = qti_parse_file($baseWorkDir, '', $file);
$filePath = $baseWorkDir.'/'.$file;
$fileFound = true;
} else {
$isManifest = isQtiManifest($baseWorkDir.'/'.$file);
if ($isManifest) {
$resourcesLinks = qtiProcessManifest($baseWorkDir.'/'.$file);
}
}
}
}
if (!$fileFound) {
return 'NoXMLFileFoundInTheZip';
}
if ($result == false) {
return false;
}
// 1. Create exercise.
$exercise = new Exercise();
$exercise->exercise = $exerciseInfo['name'];
// Random QTI support
if (isset($exerciseInfo['order_type'])) {
if ($exerciseInfo['order_type'] == 'Random') {
$exercise->setQuestionSelectionType(2);
$exercise->random = -1;
}
}
if (!empty($exerciseInfo['description'])) {
$exercise->updateDescription(formatText(strip_tags($exerciseInfo['description'])));
}
$exercise->save();
$last_exercise_id = $exercise->selectId();
$courseId = api_get_course_int_id();
if (!empty($last_exercise_id)) {
// For each question found...
foreach ($exerciseInfo['question'] as $question_array) {
if (!in_array($question_array['type'], [UNIQUE_ANSWER, MULTIPLE_ANSWER, FREE_ANSWER])) {
continue;
}
//2. Create question
$question = new Ims2Question();
$question->type = $question_array['type'];
if (empty($question->type)) {
// If the type was not provided, assume this is a multiple choice, unique answer type (the most basic)
$question->type = MCUA;
}
$question->setAnswer();
$description = '';
$question->updateTitle(formatText(strip_tags($question_array['title'])));
if (isset($question_array['category'])) {
$category = formatText(strip_tags($question_array['category']));
if (!empty($category)) {
$categoryId = TestCategory::get_category_id_for_title(
$category,
$courseId
);
if (empty($categoryId)) {
$cat = new TestCategory();
$cat->name = $category;
$cat->description = '';
$categoryId = $cat->save($courseId);
if ($categoryId) {
$question->category = $categoryId;
}
} else {
$question->category = $categoryId;
}
}
}
if (!empty($question_array['description'])) {
$description .= $question_array['description'];
}
$question->updateDescription($description);
$question->save($exercise);
$last_question_id = $question->selectId();
//3. Create answer
$answer = new Answer($last_question_id);
$answerList = $question_array['answer'];
$answer->new_nbrAnswers = count($answerList);
$totalCorrectWeight = 0;
$j = 1;
$matchAnswerIds = [];
if (!empty($answerList)) {
foreach ($answerList as $key => $answers) {
if (preg_match('/_/', $key)) {
$split = explode('_', $key);
$i = $split[1];
} else {
$i = $j;
$j++;
$matchAnswerIds[$key] = $j;
}
// Answer
$answer->new_answer[$i] = isset($answers['value']) ? formatText($answers['value']) : '';
// Comment
$answer->new_comment[$i] = isset($answers['feedback']) ? formatText($answers['feedback']) : null;
// Position
$answer->new_position[$i] = $i;
// Correct answers
if (in_array($key, $question_array['correct_answers'])) {
$answer->new_correct[$i] = 1;
} else {
$answer->new_correct[$i] = 0;
}
$answer->new_weighting[$i] = 0;
if (isset($question_array['weighting'][$key])) {
$answer->new_weighting[$i] = $question_array['weighting'][$key];
}
if ($answer->new_correct[$i]) {
$totalCorrectWeight += $answer->new_weighting[$i];
}
}
}
if ($question->type == FREE_ANSWER) {
$totalCorrectWeight = $question_array['weighting'][0];
}
if (!empty($question_array['default_weighting'])) {
$totalCorrectWeight = (float) $question_array['default_weighting'];
}
$question->updateWeighting($totalCorrectWeight);
$question->save($exercise);
$answer->save();
}
// delete the temp dir where the exercise was unzipped
my_delete($baseWorkDir.$uploadPath);
return $last_exercise_id;
}
return false;
}
/**
* We assume the file charset is UTF8.
*/
function formatText($text)
{
return api_html_entity_decode($text);
}
/**
* Parses a given XML file and fills global arrays with the elements.
*
* @param string $exercisePath
* @param string $file
* @param string $questionFile
*
* @return bool
*/
function qti_parse_file($exercisePath, $file, $questionFile)
{
global $record_item_body;
global $questionTempDir;
$questionTempDir = $exercisePath.'/'.$file.'/';
$questionFilePath = $questionTempDir.$questionFile;
if (!($fp = fopen($questionFilePath, 'r'))) {
Display::addFlash(Display::return_message(get_lang('Error opening question\'s XML file'), 'error'));
return false;
}
$data = fread($fp, filesize($questionFilePath));
//close file
fclose($fp);
//parse XML question file
//$data = str_replace(array('<p>', '</p>', '<front>', '</front>'), '', $data);
$data = ChamiloApi::stripGivenTags($data, ['p', 'front']);
$qtiVersion = [];
$match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data, $qtiVersion);
$qtiMainVersion = 2; //by default, assume QTI version 2
if ($match) {
$qtiMainVersion = $qtiVersion[1];
}
//used global variable start values declaration:
$record_item_body = false;
if ($qtiMainVersion != 2) {
Display::addFlash(
Display::return_message(
get_lang('UnsupportedQtiVersion'),
'error'
)
);
return false;
}
parseQti2($data);
return true;
}
/**
* Function used to parser a QTI2 xml file.
*
* @param string $xmlData
*/
function parseQti2($xmlData)
{
global $exerciseInfo;
global $questionTempDir;
global $resourcesLinks;
$crawler = new Crawler($xmlData);
$nodes = $crawler->filter('*');
$currentQuestionIdent = '';
$currentAnswerId = '';
$currentQuestionItemBody = '';
$cardinality = '';
$nonHTMLTagToAvoid = [
'prompt',
'simpleChoice',
'choiceInteraction',
'inlineChoiceInteraction',
'inlineChoice',
'soMPLEMATCHSET',
'simpleAssociableChoice',
'textEntryInteraction',
'feedbackInline',
'matchInteraction',
'extendedTextInteraction',
'itemBody',
'br',
'img',
];
$currentMatchSet = null;
/** @var DOMElement $node */
foreach ($nodes as $node) {
if ('#text' === $node->nodeName) {
continue;
}
switch ($node->nodeName) {
case 'assessmentItem':
$currentQuestionIdent = $node->getAttribute('identifier');
$exerciseInfo['question'][$currentQuestionIdent] = [
'answer' => [],
'correct_answers' => [],
'title' => $node->getAttribute('title'),
'category' => $node->getAttribute('category'),
'type' => '',
'tempdir' => $questionTempDir,
'description' => null,
];
break;
case 'section':
$title = $node->getAttribute('title');
if (!empty($title)) {
$exerciseInfo['name'] = $title;
}
break;
case 'responseDeclaration':
if ('multiple' === $node->getAttribute('cardinality')) {
$exerciseInfo['question'][$currentQuestionIdent]['type'] = MCMA;
$cardinality = 'multiple';
}
if ('single' === $node->getAttribute('cardinality')) {
$exerciseInfo['question'][$currentQuestionIdent]['type'] = MCUA;
$cardinality = 'single';
}
$currentAnswerId = $node->getAttribute('identifier');
break;
case 'inlineChoiceInteraction':
$exerciseInfo['question'][$currentQuestionIdent]['type'] = FIB;
$exerciseInfo['question'][$currentQuestionIdent]['subtype'] = 'LISTBOX_FILL';
$currentAnswerId = $node->getAttribute('responseIdentifier');
break;
case 'inlineChoice':
$answerIdentifier = $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$currentAnswerId];
if ($node->getAttribute('identifier') == $answerIdentifier) {
$currentQuestionItemBody = str_replace(
"**claroline_start**".$currentAnswerId."**claroline_end**",
"[".$node->nodeValue."]",
$currentQuestionItemBody
);
} else {
if (!isset($exerciseInfo['question'][$currentQuestionIdent]['wrong_answers'])) {
$exerciseInfo['question'][$currentQuestionIdent]['wrong_answers'] = [];
}
$exerciseInfo['question'][$currentQuestionIdent]['wrong_answers'][] = $node->nodeValue;
}
break;
case 'textEntryInteraction':
$exerciseInfo['question'][$currentQuestionIdent]['type'] = FIB;
$exerciseInfo['question'][$currentQuestionIdent]['subtype'] = 'TEXTFIELD_FILL';
$exerciseInfo['question'][$currentQuestionIdent]['response_text'] = $currentQuestionItemBody;
break;
case 'matchInteraction':
$exerciseInfo['question'][$currentQuestionIdent]['type'] = MATCHING;
break;
case 'extendedTextInteraction':
$exerciseInfo['question'][$currentQuestionIdent]['type'] = FREE_ANSWER;
$exerciseInfo['question'][$currentQuestionIdent]['description'] = $node->nodeValue;
break;
case 'simpleMatchSet':
if (!isset($currentMatchSet)) {
$currentMatchSet = 1;
} else {
$currentMatchSet++;
}
$exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentMatchSet] = [];
break;
case 'simpleAssociableChoice':
$currentAssociableChoice = $node->getAttribute('identifier');
$exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentMatchSet][$currentAssociableChoice] = trim($node->nodeValue);
break;
case 'simpleChoice':
$currentAnswerId = $node->getAttribute('identifier');
if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId])) {
$exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId] = [];
}
//$simpleChoiceValue = $node->nodeValue;
$simpleChoiceValue = '';
/** @var DOMElement $childNode */
foreach ($node->childNodes as $childNode) {
if ('feedbackInline' === $childNode->nodeName) {
continue;
}
$simpleChoiceValue .= $childNode->nodeValue;
}
$simpleChoiceValue = trim($simpleChoiceValue);
if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'])) {
$exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'] = $simpleChoiceValue;
} else {
$exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['value'] .= $simpleChoiceValue;
}
break;
case 'mapEntry':
if (in_array($node->parentNode->nodeName, ['mapping', 'mapEntry'])) {
$answer_id = $node->getAttribute('mapKey');
if (!isset($exerciseInfo['question'][$currentQuestionIdent]['weighting'])) {
$exerciseInfo['question'][$currentQuestionIdent]['weighting'] = [];
}
$exerciseInfo['question'][$currentQuestionIdent]['weighting'][$answer_id] = $node->getAttribute(
'mappedValue'
);
}
break;
case 'mapping':
$defaultValue = $node->getAttribute('defaultValue');
if (!empty($defaultValue)) {
$exerciseInfo['question'][$currentQuestionIdent]['default_weighting'] = $defaultValue;
}
// no break ?
case 'itemBody':
$nodeValue = $node->nodeValue;
$currentQuestionItemBody = '';
/** @var DOMElement $childNode */
foreach ($node->childNodes as $childNode) {
if ('#text' === $childNode->nodeName) {
continue;
}
if (!in_array($childNode->nodeName, $nonHTMLTagToAvoid)) {
$currentQuestionItemBody .= '<'.$childNode->nodeName;
if ($childNode->attributes) {
foreach ($childNode->attributes as $attribute) {
$currentQuestionItemBody .= ' '.$attribute->nodeName.'="'.$attribute->nodeValue.'"';
}
}
$currentQuestionItemBody .= '>'.$childNode->nodeValue.'</'.$node->nodeName.'>';
continue;
}
if ('inlineChoiceInteraction' === $childNode->nodeName) {
$currentQuestionItemBody .= "**claroline_start**".$childNode->attr('responseIdentifier')
."**claroline_end**";
continue;
}
if ('textEntryInteraction' === $childNode->nodeName) {
$correct_answer_value = $exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$currentAnswerId];
$currentQuestionItemBody .= "[".$correct_answer_value."]";
continue;
}
if ('br' === $childNode->nodeName) {
$currentQuestionItemBody .= '<br>';
}
}
// Replace relative links by links to the documents in the course
// $resourcesLinks is only defined by qtiProcessManifest()
if (isset($resourcesLinks) && isset($resourcesLinks['manifest']) && isset($resourcesLinks['web'])) {
foreach ($resourcesLinks['manifest'] as $key => $value) {
$nodeValue = preg_replace('|'.$value.'|', $resourcesLinks['web'][$key], $nodeValue);
}
}
$currentQuestionItemBody .= $node->firstChild->nodeValue;
if ($exerciseInfo['question'][$currentQuestionIdent]['type'] == FIB) {
$exerciseInfo['question'][$currentQuestionIdent]['response_text'] = $currentQuestionItemBody;
} else {
if ($exerciseInfo['question'][$currentQuestionIdent]['type'] == FREE_ANSWER) {
$currentQuestionItemBody = trim($currentQuestionItemBody);
if (!empty($currentQuestionItemBody)) {
$exerciseInfo['question'][$currentQuestionIdent]['description'] = $currentQuestionItemBody;
}
} else {
$exerciseInfo['question'][$currentQuestionIdent]['statement'] = $currentQuestionItemBody;
}
}
break;
case 'img':
$exerciseInfo['question'][$currentQuestionIdent]['attached_file_url'] = $node->getAttribute('src');
break;
case 'order':
$orderType = $node->getAttribute('order_type');
if (!empty($orderType)) {
$exerciseInfo['order_type'] = $orderType;
}
break;
case 'feedbackInline':
if (!isset($exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'])) {
$exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'] = trim(
$node->nodeValue
);
} else {
$exerciseInfo['question'][$currentQuestionIdent]['answer'][$currentAnswerId]['feedback'] .= trim(
$node->nodeValue
);
}
break;
case 'value':
if ('correctResponse' === $node->parentNode->nodeName) {
$nodeValue = trim($node->nodeValue);
if ('single' === $cardinality) {
$exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][$nodeValue] = $nodeValue;
} else {
$exerciseInfo['question'][$currentQuestionIdent]['correct_answers'][] = $nodeValue;
}
}
if ('outcomeDeclaration' === $node->parentNode->parentNode->nodeName) {
$nodeValue = trim($node->nodeValue);
if (!empty($nodeValue)) {
$exerciseInfo['question'][$currentQuestionIdent]['weighting'][0] = $nodeValue;
}
}
break;
case 'mattext':
if ('flow_mat' === $node->parentNode->parentNode->nodeName &&
('presentation_material' === $node->parentNode->parentNode->parentNode->nodeName ||
'section' === $node->parentNode->parentNode->parentNode->nodeName
)
) {
$nodeValue = trim($node->nodeValue);
if (!empty($nodeValue)) {
$exerciseInfo['description'] = $node->nodeValue;
}
}
break;
case 'prompt':
$description = trim($node->nodeValue);
$description = htmlspecialchars_decode($description);
$description = Security::remove_XSS($description);
if (!empty($description)) {
$exerciseInfo['question'][$currentQuestionIdent]['description'] = $description;
}
break;
}
}
}
/**
* Check if a given file is an IMS/QTI question bank file.
*
* @param string $filePath The absolute filepath
*
* @return bool Whether it is an IMS/QTI question bank or not
*/
function isQtiQuestionBank($filePath)
{
$data = file_get_contents($filePath);
if (!empty($data)) {
$match = preg_match('/ims_qtiasiv(\d)p(\d)/', $data);
// @todo allow other types
//$match2 = preg_match('/imsqti_v(\d)p(\d)/', $data);
if ($match) {
return true;
}
}
return false;
}
/**
* Check if a given file is an IMS/QTI manifest file (listing of extra files).
*
* @param string $filePath The absolute filepath
*
* @return bool Whether it is an IMS/QTI manifest file or not
*/
function isQtiManifest($filePath)
{
$data = file_get_contents($filePath);
if (!empty($data)) {
$match = preg_match('/imsccv(\d)p(\d)/', $data);
if ($match) {
return true;
}
}
return false;
}
/**
* Processes an IMS/QTI manifest file: store links to new files
* to be able to transform them into the questions text.
*
* @param string $filePath The absolute filepath
*
* @return bool
*/
function qtiProcessManifest($filePath)
{
$xml = simplexml_load_file($filePath);
$course = api_get_course_info();
$sessionId = api_get_session_id();
$courseDir = $course['path'];
$sysPath = api_get_path(SYS_COURSE_PATH);
$exercisesSysPath = $sysPath.$courseDir.'/document/';
$webPath = api_get_path(WEB_CODE_PATH);
$exercisesWebPath = $webPath.'document/document.php?'.api_get_cidreq().'&action=download&id=';
$links = [
'manifest' => [],
'system' => [],
'web' => [],
];
$tableDocuments = Database::get_course_table(TABLE_DOCUMENT);
$countResources = count($xml->resources->resource->file);
for ($i = 0; $i < $countResources; $i++) {
$file = $xml->resources->resource->file[$i];
$href = '';
foreach ($file->attributes() as $key => $value) {
if ('href' == $key) {
if ('xml' != substr($value, -3, 3)) {
$href = $value;
}
}
}
if (!empty($href)) {
$links['manifest'][] = (string) $href;
$links['system'][] = $exercisesSysPath.strtolower($href);
$specialHref = Database::escape_string(preg_replace('/_/', '-', strtolower($href)));
$specialHref = preg_replace('/(-){2,8}/', '-', $specialHref);
$sql = "SELECT iid FROM $tableDocuments
WHERE
c_id = ".$course['real_id']." AND
session_id = $sessionId AND
path = '/".$specialHref."'";
$result = Database::query($sql);
$documentId = 0;
while ($row = Database::fetch_assoc($result)) {
$documentId = $row['iid'];
}
$links['web'][] = $exercisesWebPath.$documentId;
}
}
return $links;
}

View File

@@ -0,0 +1,117 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This script exports the PDF reports from a test for several students at once.
* This script is the teacher view of a similar script (for admins) at main/admin/export_exercise_results.php.
*/
require_once __DIR__.'/../../inc/global.inc.php';
// Setting the section (for the tabs).
$this_section = SECTION_COURSES;
api_protect_course_script(true);
if (!api_is_allowed_to_edit()) {
api_not_allowed(true);
}
$sessionId = api_get_session_id();
$courseId = api_get_course_int_id();
$exerciseId = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : null;
$exerciseIdChanged = isset($_GET['exercise_id_changed']) ? (int) $_GET['exercise_id_changed'] : null;
$courseInfo = [];
if (empty($courseId)) {
$exerciseId = 0;
} else {
$courseInfo = api_get_course_info_by_id($courseId);
}
$interbreadcrumb[] = ['url' => '../exercise.php?'.api_get_cidreq(), 'name' => get_lang('Exercises')];
$confirmYourChoice = addslashes(get_lang('ConfirmYourChoice'));
$htmlHeadXtra[] = "
<script>
function submit_form(obj) {
document.export_all_results_form.submit();
}
function mark_exercise_id_changed() {
$('#exercise_id_changed').val('0');
}
function confirm_your_choice() {
return confirm('$confirmYourChoice');
}
</script>";
// Get exercise list for this course
$exerciseList = ExerciseLib::get_all_exercises_for_course_id(
$courseInfo,
$sessionId,
$courseId,
false
);
$exerciseSelectList = [];
$exerciseSelectList = [0 => get_lang('All')];
if (is_array($exerciseList)) {
foreach ($exerciseList as $row) {
$exerciseTitle = $row['title'];
$exerciseSelectList[$row['iid']] = $exerciseTitle;
}
}
$url = api_get_self().'?'.api_get_cidreq().'&'.http_build_query(
[
'session_id' => $sessionId,
'exerciseId' => $exerciseId,
'exercise_id_changed' => $exerciseIdChanged,
]
);
// Form
$form = new FormValidator('export_all_results_form', 'GET', $url);
$form->addHeader(get_lang('ExportExerciseAllResults'));
$form
->addSelect(
'exerciseId',
get_lang('Exercise'),
$exerciseSelectList
)
->setSelected($exerciseId);
$form->addDateTimePicker('start_date', get_lang('StartDate'));
$form->addDateTimePicker('end_date', get_lang('EndDate'));
$form->addRule('start_date', get_lang('InvalidDate'), 'datetime');
$form->addRule('end_date', get_lang('InvalidDate'), 'datetime');
$form->addRule(
['start_date', 'end_date'],
get_lang('StartDateShouldBeBeforeEndDate'),
'date_compare',
'lte'
);
$form->addHidden('exercise_id_changed', '0');
$form->addButtonExport(get_lang('Export'), 'name');
if ($form->validate()) {
$values = $form->getSubmitValues();
$exerciseId = (int) $values['exerciseId'];
$filterDates = [
'start_date' => (!empty($values['start_date']) ? $values['start_date'] : ''),
'end_date' => (!empty($values['end_date']) ? $values['end_date'] : ''),
];
if ($exerciseId === 0) {
ExerciseLib::exportAllExercisesResultsZip($sessionId, $courseId, $filterDates);
} else {
ExerciseLib::exportExerciseAllResultsZip($sessionId, $courseId, $exerciseId, $filterDates);
}
}
Display::display_header(get_lang('ExportExerciseAllResults'));
echo $form->display();
Display::display_footer();

View File

@@ -0,0 +1,11 @@
<?php
/* For licensing terms, see /license.txt */
/**
* @author Claro Team <cvs@claroline.net>
*/
/**
* Redirection.
*/
header('Location: ../../../');
exit();

View File

@@ -0,0 +1,471 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Interface ImsAnswerInterface.
*/
interface ImsAnswerInterface
{
/**
* @param string $questionIdent
* @param string $questionStatment
* @param string $questionDesc
* @param string $questionMedia
*
* @return string
*/
public function imsExportResponses($questionIdent, $questionStatment, $questionDesc = '', $questionMedia = '');
/**
* @param $questionIdent
*
* @return mixed
*/
public function imsExportResponsesDeclaration($questionIdent, Question $question = null);
}
/**
* @author Claro Team <cvs@claroline.net>
* @author Yannick Warnier <yannick.warnier@beeznest.com> -
* updated ImsAnswerHotspot to match QTI norms
*/
class Ims2Question extends Question
{
/**
* Include the correct answer class and create answer.
*
* @return Answer
*/
public function setAnswer()
{
switch ($this->type) {
case MCUA:
$answer = new ImsAnswerMultipleChoice($this->iid);
return $answer;
case MCMA:
case MULTIPLE_ANSWER_DROPDOWN:
case MULTIPLE_ANSWER_DROPDOWN_COMBINATION:
$answer = new ImsAnswerMultipleChoice($this->iid);
return $answer;
case TF:
$answer = new ImsAnswerMultipleChoice($this->iid);
return $answer;
case FIB:
$answer = new ImsAnswerFillInBlanks($this->iid);
return $answer;
case MATCHING:
case MATCHING_DRAGGABLE:
$answer = new ImsAnswerMatching($this->iid);
return $answer;
case FREE_ANSWER:
$answer = new ImsAnswerFree($this->iid);
return $answer;
case HOT_SPOT:
case HOT_SPOT_COMBINATION:
$answer = new ImsAnswerHotspot($this->iid);
return $answer;
default:
$answer = null;
break;
}
return $answer;
}
public function createAnswersForm($form)
{
return true;
}
public function processAnswersCreation($form, $exercise)
{
return true;
}
}
/**
* Class.
*/
class ImsAnswerMultipleChoice extends Answer implements ImsAnswerInterface
{
/**
* Return the XML flow for the possible answers.
*
* @param string $questionIdent
* @param string $questionStatment
* @param string $questionDesc
* @param string $questionMedia
*
* @return string
*/
public function imsExportResponses($questionIdent, $questionStatment, $questionDesc = '', $questionMedia = '')
{
// @todo getAnswersList() converts the answers using api_html_entity_decode()
$this->answerList = $this->getAnswersList(true);
$out = ' <choiceInteraction responseIdentifier="'.$questionIdent.'" >'."\n";
$out .= ' <prompt><![CDATA['.formatExerciseQtiText($questionDesc).']]></prompt>'."\n";
if (is_array($this->answerList)) {
foreach ($this->answerList as $current_answer) {
$out .= '<simpleChoice identifier="answer_'.$current_answer['iid'].'" fixed="false">
<![CDATA['.formatExerciseQtiText($current_answer['answer']).']]>';
if (isset($current_answer['comment']) && $current_answer['comment'] != '') {
$out .= '<feedbackInline identifier="answer_'.$current_answer['iid'].'">
<![CDATA['.formatExerciseQtiText($current_answer['comment']).']]>
</feedbackInline>';
}
$out .= '</simpleChoice>'."\n";
}
}
$out .= ' </choiceInteraction>'."\n";
return $out;
}
/**
* Return the XML flow of answer ResponsesDeclaration.
*/
public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
{
$this->answerList = $this->getAnswersList(true);
$type = $this->getQuestionType();
if (in_array($type, [MCMA, MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION])) {
$cardinality = 'multiple';
} else {
$cardinality = 'single';
}
$out = ' <responseDeclaration identifier="'.$questionIdent.'" cardinality="'.$cardinality.'" baseType="identifier">'."\n";
// Match the correct answers.
if (is_array($this->answerList)) {
$out .= ' <correctResponse>'."\n";
foreach ($this->answerList as $current_answer) {
if ($current_answer['correct']) {
$out .= ' <value>answer_'.$current_answer['iid'].'</value>'."\n";
}
}
$out .= ' </correctResponse>'."\n";
}
// Add the grading
if (is_array($this->answerList)) {
$out .= ' <mapping';
if (MULTIPLE_ANSWER_DROPDOWN_COMBINATION == $this->getQuestionType()) {
$out .= ' defaultValue="'.$question->selectWeighting().'"';
}
$out .= '>'."\n";
foreach ($this->answerList as $current_answer) {
if (isset($current_answer['grade'])) {
$out .= ' <mapEntry mapKey="answer_'.$current_answer['iid'].'" mappedValue="'.$current_answer['grade'].'" />'."\n";
}
}
$out .= ' </mapping>'."\n";
}
$out .= ' </responseDeclaration>'."\n";
return $out;
}
}
/**
* Class.
*
* @package chamilo.exercise
*/
class ImsAnswerFillInBlanks extends Answer implements ImsAnswerInterface
{
private $answerList = [];
private $gradeList = [];
/**
* Export the text with missing words.
*
* @param string $questionIdent
* @param string $questionStatment
* @param string $questionDesc
* @param string $questionMedia
*
* @return string
*/
public function imsExportResponses($questionIdent, $questionStatment, $questionDesc = '', $questionMedia = '')
{
$this->answerList = $this->getAnswersList(true);
$text = isset($this->answerText) ? $this->answerText : '';
if (is_array($this->answerList)) {
foreach ($this->answerList as $key => $answer) {
$key = $answer['iid'];
$answer = $answer['answer'];
$len = api_strlen($answer);
$text = str_replace('['.$answer.']', '<textEntryInteraction responseIdentifier="fill_'.$key.'" expectedLength="'.api_strlen($answer).'"/>', $text);
}
}
return $text;
}
public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
{
$this->answerList = $this->getAnswersList(true);
$this->gradeList = $this->getGradesList();
$out = '';
if (is_array($this->answerList)) {
foreach ($this->answerList as $answer) {
$answerKey = $answer['iid'];
$answer = $answer['answer'];
$out .= ' <responseDeclaration identifier="fill_'.$answerKey.'" cardinality="single" baseType="identifier">'."\n";
$out .= ' <correctResponse>'."\n";
$out .= ' <value><![CDATA['.formatExerciseQtiText($answer).']]></value>'."\n";
$out .= ' </correctResponse>'."\n";
if (isset($this->gradeList[$answerKey])) {
$out .= ' <mapping>'."\n";
$out .= ' <mapEntry mapKey="'.$answer.'" mappedValue="'.$this->gradeList[$answerKey].'"/>'."\n";
$out .= ' </mapping>'."\n";
}
$out .= ' </responseDeclaration>'."\n";
}
}
return $out;
}
}
/**
* Class.
*
* @package chamilo.exercise
*/
class ImsAnswerMatching extends Answer implements ImsAnswerInterface
{
public $leftList = [];
public $rightList = [];
private $answerList = [];
/**
* Export the question part as a matrix-choice, with only one possible answer per line.
*
* @param string $questionIdent
* @param string $questionStatment
* @param string $questionDesc
* @param string $questionMedia
*
* @return string
*/
public function imsExportResponses($questionIdent, $questionStatment, $questionDesc = '', $questionMedia = '')
{
$this->answerList = $this->getAnswersList(true);
$maxAssociation = max(count($this->leftList), count($this->rightList));
$out = '<matchInteraction responseIdentifier="'.$questionIdent.'" maxAssociations="'.$maxAssociation.'">'."\n";
$out .= $questionStatment;
//add left column
$out .= ' <simpleMatchSet>'."\n";
if (is_array($this->leftList)) {
foreach ($this->leftList as $leftKey => $leftElement) {
$out .= '
<simpleAssociableChoice identifier="left_'.$leftKey.'" >
<![CDATA['.formatExerciseQtiText($leftElement['answer']).']]>
</simpleAssociableChoice>'."\n";
}
}
$out .= ' </simpleMatchSet>'."\n";
//add right column
$out .= ' <simpleMatchSet>'."\n";
$i = 0;
if (is_array($this->rightList)) {
foreach ($this->rightList as $rightKey => $rightElement) {
$out .= '<simpleAssociableChoice identifier="right_'.$i.'" >
<![CDATA['.formatExerciseQtiText($rightElement['answer']).']]>
</simpleAssociableChoice>'."\n";
$i++;
}
}
$out .= ' </simpleMatchSet>'."\n";
$out .= '</matchInteraction>'."\n";
return $out;
}
public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
{
$this->answerList = $this->getAnswersList(true);
$out = ' <responseDeclaration identifier="'.$questionIdent.'" cardinality="single" baseType="identifier">'."\n";
$out .= ' <correctResponse>'."\n";
$gradeArray = [];
if (isset($this->leftList) && is_array($this->leftList)) {
foreach ($this->leftList as $leftKey => $leftElement) {
$i = 0;
foreach ($this->rightList as $rightKey => $rightElement) {
if (($leftElement['match'] == $rightElement['code'])) {
$out .= ' <value>left_'.$leftKey.' right_'.$i.'</value>'."\n";
$gradeArray['left_'.$leftKey.' right_'.$i] = $leftElement['grade'];
}
$i++;
}
}
}
$out .= ' </correctResponse>'."\n";
if (is_array($gradeArray)) {
$out .= ' <mapping>'."\n";
foreach ($gradeArray as $gradeKey => $grade) {
$out .= ' <mapEntry mapKey="'.$gradeKey.'" mappedValue="'.$grade.'"/>'."\n";
}
$out .= ' </mapping>'."\n";
}
$out .= ' </responseDeclaration>'."\n";
return $out;
}
}
/**
* Class.
*
* @package chamilo.exercise
*/
class ImsAnswerHotspot extends Answer implements ImsAnswerInterface
{
private $answerList = [];
private $gradeList = [];
/**
* @todo update this to match hot spots instead of copying matching
* Export the question part as a matrix-choice, with only one possible answer per line.
*
* @param string $questionIdent
* @param string $questionStatment
* @param string $questionDesc
* @param string $questionMedia
*
* @return string
*/
public function imsExportResponses($questionIdent, $questionStatment, $questionDesc = '', $questionMedia = '')
{
$this->answerList = $this->getAnswersList(true);
$mediaFilePath = api_get_course_path().'/document/images/'.$questionMedia;
$sysQuestionMediaPath = api_get_path(SYS_COURSE_PATH).$mediaFilePath;
$questionMedia = api_get_path(WEB_COURSE_PATH).$mediaFilePath;
$mimetype = mime_content_type($sysQuestionMediaPath);
if (empty($mimetype)) {
$mimetype = 'image/jpeg';
}
$text = ' <p>'.$questionStatment.'</p>'."\n";
$text .= ' <graphicOrderInteraction responseIdentifier="hotspot_'.$questionIdent.'">'."\n";
$text .= ' <prompt>'.$questionDesc.'</prompt>'."\n";
$text .= ' <object type="'.$mimetype.'" width="250" height="230" data="'.$questionMedia.'">-</object>'."\n";
if (is_array($this->answerList)) {
foreach ($this->answerList as $key => $answer) {
$key = $answer['iid'];
$answerTxt = $answer['answer'];
$len = api_strlen($answerTxt);
//coords are transformed according to QTIv2 rules here: http://www.imsproject.org/question/qtiv2p1pd/imsqti_infov2p1pd.html#element10663
$coords = '';
$type = 'default';
switch ($answer['hotspot_type']) {
case 'square':
$type = 'rect';
$res = [];
$coords = preg_match('/^\s*(\d+);(\d+)\|(\d+)\|(\d+)\s*$/', $answer['hotspot_coord'], $res);
$coords = $res[1].','.$res[2].','.((int) $res[1] + (int) $res[3]).",".((int) $res[2] + (int) $res[4]);
break;
case 'circle':
$type = 'circle';
$res = [];
$coords = preg_match('/^\s*(\d+);(\d+)\|(\d+)\|(\d+)\s*$/', $answer['hotspot_coord'], $res);
$coords = $res[1].','.$res[2].','.sqrt(pow(($res[1] - $res[3]), 2) + pow(($res[2] - $res[4])));
break;
case 'poly':
$type = 'poly';
$coords = str_replace([';', '|'], [',', ','], $answer['hotspot_coord']);
break;
case 'delineation':
$type = 'delineation';
$coords = str_replace([';', '|'], [',', ','], $answer['hotspot_coord']);
break;
}
$text .= ' <hotspotChoice shape="'.$type.'" coords="'.$coords.'" identifier="'.$key.'"/>'."\n";
}
}
$text .= ' </graphicOrderInteraction>'."\n";
return $text;
}
public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
{
$this->answerList = $this->getAnswersList(true);
$this->gradeList = $this->getGradesList();
$out = ' <responseDeclaration identifier="hotspot_'.$questionIdent.'" cardinality="ordered" baseType="identifier">'."\n";
if (is_array($this->answerList)) {
$out .= ' <correctResponse>'."\n";
foreach ($this->answerList as $answerKey => $answer) {
$answerKey = $answer['iid'];
$answer = $answer['answer'];
$out .= '<value><![CDATA['.formatExerciseQtiText($answerKey).']]></value>';
}
$out .= ' </correctResponse>'."\n";
}
$out .= ' </responseDeclaration>'."\n";
return $out;
}
}
/**
* Class.
*
* @package chamilo.exercise
*/
class ImsAnswerFree extends Answer implements ImsAnswerInterface
{
/**
* @todo implement
* Export the question part as a matrix-choice, with only one possible answer per line.
*
* @param string $questionIdent
* @param string $questionStatment
* @param string $questionDesc
* @param string $questionMedia
*
* @return string
*/
public function imsExportResponses($questionIdent, $questionStatment, $questionDesc = '', $questionMedia = '')
{
$questionDesc = formatExerciseQtiText($questionDesc);
return '<extendedTextInteraction responseIdentifier="'.$questionIdent.'" >
<prompt>
'.$questionDesc.'
</prompt>
</extendedTextInteraction>';
}
public function imsExportResponsesDeclaration($questionIdent, Question $question = null)
{
$out = ' <responseDeclaration identifier="'.$questionIdent.'" cardinality="single" baseType="string">';
$out .= '<outcomeDeclaration identifier="SCORE" cardinality="single" baseType="float">
<defaultValue><value>'.$question->weighting.'</value></defaultValue></outcomeDeclaration>';
$out .= ' </responseDeclaration>'."\n";
return $out;
}
}

View File

@@ -0,0 +1,515 @@
<?php
/* For licensing terms, see /license.txt */
/**
* @author Claro Team <cvs@claroline.net>
* @author Yannick Warnier <yannick.warnier@beeznest.com>
*/
require __DIR__.'/qti2_classes.php';
/**
* An IMS/QTI item. It corresponds to a single question.
* This class allows export from Claroline to IMS/QTI2.0 XML 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.
*
* note: Attached files are NOT exported.
*/
class ImsAssessmentItem
{
/**
* @var Ims2Question
*/
public $question;
/**
* @var string
*/
public $questionIdent;
/**
* @var ImsAnswerInterface
*/
public $answer;
/**
* Constructor.
*
* @param Ims2Question $question ims2Question object we want to export
*/
public function __construct($question)
{
$this->question = $question;
$this->answer = $this->question->setAnswer();
$this->questionIdent = 'QST_'.$question->iid;
}
/**
* Start the XML flow.
*
* This opens the <item> block, with correct attributes.
*/
public function start_item()
{
$categoryTitle = '';
if (!empty($this->question->category)) {
$category = new TestCategory();
$category = $category->getCategory($this->question->category);
if ($category) {
$categoryTitle = htmlspecialchars(formatExerciseQtiText($category->name));
}
}
return '<assessmentItem xmlns="http://www.imsglobal.org/xsd/imsqti_v2p1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqti_v2p1 imsqti_v2p1.xsd"
identifier="'.$this->questionIdent.'"
title = "'.htmlspecialchars(formatExerciseQtiText($this->question->selectTitle())).'"
category = "'.$categoryTitle.'"
>'."\n";
}
/**
* End the XML flow, closing the </item> tag.
*/
public function end_item()
{
return "</assessmentItem>\n";
}
/**
* Start the itemBody.
*/
public function start_item_body()
{
return ' <itemBody>'."\n";
}
/**
* End the itemBody part.
*/
public function end_item_body()
{
return " </itemBody>\n";
}
/**
* add the response processing template used.
*/
public function add_response_processing()
{
return ' <responseProcessing template="http://www.imsglobal.org/question/qti_v2p1/rptemplates/map_correct"/>'."\n";
}
/**
* Export the question as an IMS/QTI Item.
*
* This is a default behaviour, some classes may want to override this.
*
* @return string string, the XML flow for an Item
*/
public function export($standalone = false)
{
$head = $foot = '';
if ($standalone) {
$head = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'."\n";
}
//TODO understand why answer might be a non-object sometimes
if (!is_object($this->answer)) {
return $head;
}
return $head
.$this->start_item()
.$this->answer->imsExportResponsesDeclaration($this->questionIdent, $this->question)
.$this->start_item_body()
.$this->answer->imsExportResponses(
$this->questionIdent,
$this->question->question,
$this->question->description,
$this->question->getPictureFilename()
)
.$this->end_item_body()
.$this->add_response_processing()
.$this->end_item()
.$foot;
}
}
/**
* This class represents an entire exercise to be exported in IMS/QTI.
* It will be represented by a single <section> containing several <item>.
*
* Some properties cannot be exported, as IMS does not support them :
* - type (one page or multiple pages)
* - start_date and end_date
* - max_attempts
* - show_answer
* - anonymous_attempts
*
* @author Amand Tihon <amand@alrj.org>
*/
class ImsSection
{
public $exercise;
/**
* Constructor.
*
* @param Exercise $exe The Exercise instance to export
*
* @author Amand Tihon <amand@alrj.org>
*/
public function __construct($exe)
{
$this->exercise = $exe;
}
public function start_section()
{
return '<section
ident = "EXO_'.$this->exercise->selectId().'"
title = "'.cleanAttribute(formatExerciseQtiDescription($this->exercise->selectTitle())).'"
>'."\n";
}
public function end_section()
{
return "</section>\n";
}
public function export_duration()
{
if ($max_time = $this->exercise->selectTimeLimit()) {
// return exercise duration in ISO8601 format.
$minutes = floor($max_time / 60);
$seconds = $max_time % 60;
return '<duration>PT'.$minutes.'M'.$seconds."S</duration>\n";
} else {
return '';
}
}
/**
* Export the presentation (Exercise's description).
*
* @author Amand Tihon <amand@alrj.org>
*/
public function export_presentation()
{
return "<presentation_material><flow_mat><material>\n"
.' <mattext><![CDATA['.formatExerciseQtiDescription($this->exercise->selectDescription())."]]></mattext>\n"
."</material></flow_mat></presentation_material>\n";
}
/**
* Export the ordering information.
* Either sequential, through all questions, or random, with a selected number of questions.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function export_ordering()
{
$out = '';
if ($n = $this->exercise->getShuffle()) {
$out .= "<selection_ordering>"
." <selection>\n"
." <selection_number>".$n."</selection_number>\n"
." </selection>\n"
.' <order order_type="Random" />'
."\n</selection_ordering>\n";
} else {
$out .= '<selection_ordering sequence_type="Normal">'."\n"
." <selection />\n"
."</selection_ordering>\n";
}
return $out;
}
/**
* Export the questions, as a succession of <items>.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function exportQuestions()
{
$out = '';
foreach ($this->exercise->selectQuestionList() as $q) {
$out .= export_question_qti($q, false);
}
return $out;
}
/**
* Export the exercise in IMS/QTI.
*
* @param bool $standalone wether it should include XML tag and DTD line
*
* @return string string containing the XML flow
*
* @author Amand Tihon <amand@alrj.org>
*/
public function export($standalone)
{
$head = $foot = '';
if ($standalone) {
$head = '<?xml version = "1.0" encoding = "UTF-8" standalone = "no"?>'."\n"
.'<!DOCTYPE questestinterop SYSTEM "ims_qtiasiv2p1.dtd">'."\n"
."<questestinterop>\n";
$foot = "</questestinterop>\n";
}
return $head
.$this->start_section()
.$this->export_duration()
.$this->export_presentation()
.$this->export_ordering()
.$this->exportQuestions()
.$this->end_section()
.$foot;
}
}
/*
Some quick notes on identifiers generation.
The IMS format requires some blocks, like items, responses, feedbacks, to be uniquely
identified.
The unicity is mandatory in a single XML, of course, but it's prefered that the identifier stays
coherent for an entire site.
Here's the method used to generate those identifiers.
Question identifier :: "QST_" + <Question Id from the DB> + "_" + <Question numeric type>
Response identifier :: <Question identifier> + "_A_" + <Response Id from the DB>
Condition identifier :: <Question identifier> + "_C_" + <Response Id from the DB>
Feedback identifier :: <Question identifier> + "_F_" + <Response Id from the DB>
*/
/**
* Class ImsItem.
*
* An IMS/QTI item. It corresponds to a single question.
* This class allows export from Claroline to IMS/QTI XML format.
* It is not usable as-is, but must be subclassed, to support different kinds of questions.
*
* Every start_*() and corresponding end_*(), as well as export_*() methods return a string.
*
* warning: Attached files are NOT exported.
*
* @author Amand Tihon <amand@alrj.org>
*/
class ImsItem
{
public $question;
public $questionIdent;
public $answer;
/**
* Constructor.
*
* @param Question $question the Question object we want to export
*
* @author Anamd Tihon
*/
public function __construct($question)
{
$this->question = $question;
$this->answer = $question->answer;
$this->questionIdent = 'QST_'.$question->selectId();
}
/**
* Start the XML flow.
*
* This opens the <item> block, with correct attributes.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function start_item()
{
return '<item title="'.cleanAttribute(formatExerciseQtiDescription($this->question->selectTitle())).'" ident="'.$this->questionIdent.'">'."\n";
}
/**
* End the XML flow, closing the </item> tag.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function end_item()
{
return "</item>\n";
}
/**
* Create the opening, with the question itself.
*
* This means it opens the <presentation> but doesn't close it, as this is the role of end_presentation().
* In between, the export_responses from the subclass should have been called.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function start_presentation()
{
return '<presentation label="'.$this->questionIdent.'"><flow>'."\n"
.'<material><mattext>'.formatExerciseQtiDescription($this->question->selectDescription())."</mattext></material>\n";
}
/**
* End the </presentation> part, opened by export_header.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function end_presentation()
{
return "</flow></presentation>\n";
}
/**
* Start the response processing, and declare the default variable, SCORE, at 0 in the outcomes.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function start_processing()
{
return '<resprocessing><outcomes><decvar vartype="Integer" defaultval="0" /></outcomes>'."\n";
}
/**
* End the response processing part.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function end_processing()
{
return "</resprocessing>\n";
}
/**
* Export the question as an IMS/QTI Item.
*
* This is a default behaviour, some classes may want to override this.
*
* @param bool $standalone Boolean stating if it should be exported as a stand-alone question
*
* @return string string, the XML flow for an Item
*
* @author Amand Tihon <amand@alrj.org>
*/
public function export($standalone = false)
{
global $charset;
$head = $foot = '';
if ($standalone) {
$head = '<?xml version = "1.0" encoding = "'.$charset.'" standalone = "no"?>'."\n"
.'<!DOCTYPE questestinterop SYSTEM "ims_qtiasiv2p1.dtd">'."\n"
."<questestinterop>\n";
$foot = "</questestinterop>\n";
}
return $head
.$this->start_item()
.$this->start_presentation()
.$this->answer->imsExportResponses($this->questionIdent)
.$this->end_presentation()
.$this->start_processing()
.$this->answer->imsExportProcessing($this->questionIdent)
.$this->end_processing()
.$this->answer->imsExportFeedback($this->questionIdent)
.$this->end_item()
.$foot;
}
}
/**
* Send a complete exercise in IMS/QTI format, from its ID.
*
* @param int $exerciseId 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
*/
function export_exercise_to_qti($exerciseId, $standalone = true)
{
$exercise = new Exercise();
if (!$exercise->read($exerciseId)) {
return '';
}
$ims = new ImsSection($exercise);
return $ims->export($standalone);
}
/**
* Returns the XML flow corresponding to one question.
*
* @param int $questionId
* @param bool $standalone (ie including XML tag, DTD declaration, etc)
*
* @return string
*/
function export_question_qti($questionId, $standalone = true)
{
$question = new Ims2Question();
$qst = $question->read($questionId);
if (!$qst) {
return '';
}
$isValid = $qst instanceof UniqueAnswer
|| $qst instanceof MultipleAnswer
|| $qst instanceof FreeAnswer
|| $qst instanceof MultipleAnswerDropdown
|| $qst instanceof MultipleAnswerDropdownCombination
;
if (!$isValid) {
return '';
}
$question->iid = $qst->iid;
$question->type = $qst->type;
$question->question = $qst->question;
$question->description = $qst->description;
$question->weighting = $qst->weighting;
$question->position = $qst->position;
$question->picture = $qst->picture;
$question->category = $qst->category;
$ims = new ImsAssessmentItem($question);
return $ims->export($standalone);
}
/**
* Clean text like a description.
*/
function formatExerciseQtiDescription($text)
{
$entities = api_html_entity_decode($text);
return htmlspecialchars($entities);
}
/**
* Clean titles.
*
* @param $text
*
* @return string
*/
function formatExerciseQtiText($text)
{
return htmlspecialchars($text);
}
/**
* @param string $text
*
* @return string
*/
function cleanAttribute($text)
{
return $text;
}

View File

@@ -0,0 +1,79 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This class handles the SCORM export of fill-in-the-blanks questions.
*/
class ScormAnswerFillInBlanks extends Answer
{
/**
* Export the text with missing words.
*
* As a side effect, it stores two lists in the class :
* the missing words and their respective weightings.
*/
public function export()
{
$js = '';
// get all enclosed answers
$blankList = [];
foreach ($this->answer as $i => $answer) {
$blankList[] = '['.$answer.']';
}
// splits text and weightings that are joined with the character '::'
$listAnswerInfo = FillBlanks::getAnswerInfo($answer);
//$switchableAnswerSet = $listAnswerInfo['switchable'];
// display empty [input] with the right width for student to fill it
$answer = '';
$answerList = [];
for ($i = 0; $i < count($listAnswerInfo['common_words']) - 1; $i++) {
// display the common words
$answer .= $listAnswerInfo['common_words'][$i];
// display the blank word
$attributes['style'] = 'width:'.$listAnswerInfo['input_size'][$i].'px';
$answer .= FillBlanks::getFillTheBlankHtml(
$this->questionJSId,
$this->questionJSId + 1,
'',
$attributes,
$answer,
$listAnswerInfo,
true,
$i,
'question_'.$this->questionJSId.'_fib_'.($i + 1)
);
$answerList[] = $i + 1;
}
// display the last common word
$answer .= $listAnswerInfo['common_words'][$i];
// because [] is parsed here we follow this procedure:
// 1. find everything between the [ and ] tags
$jstmpw = 'questions_answers_ponderation['.$this->questionJSId.'] = new Array();'."\n";
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.'][0] = 0;'."\n";
foreach ($listAnswerInfo['weighting'] as $key => $weight) {
$jstmpw .= 'questions_answers_ponderation['.$this->questionJSId.']['.($key + 1).'] = '.$weight.";\n";
}
$wordList = "'".implode("', '", $listAnswerInfo['words'])."'";
$answerList = "'".implode("', '", $answerList)."'";
$html = '<tr><td colspan="2"><table width="100%">';
$html .= '<tr>
<td>
'.$answer.'
</td>
</tr></table></td></tr>';
$js .= 'questions_answers['.$this->questionJSId.'] = new Array('.$answerList.');'."\n";
$js .= 'questions_answers_correct['.$this->questionJSId.'] = new Array('.$wordList.');'."\n";
$js .= 'questions_types['.$this->questionJSId.'] = \'fib\';'."\n";
$js .= $jstmpw;
return [$js, $html];
}
}

View File

@@ -0,0 +1,98 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This class handles the SCORM export of matching questions.
*/
class ScormAnswerMatching extends Answer
{
/**
* Export the question part as a matrix-choice, with only one possible answer per line.
*
* @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">&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 (2 == count($myWeight)) {
$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];
}
}

View File

@@ -0,0 +1,118 @@
<?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).
*/
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 (MCMA == $type) {
$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 (MULTIPLE_ANSWER_COMBINATION == $type) {
$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];
}
}

View File

@@ -0,0 +1,55 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This class handles the SCORM export of true/false questions.
*/
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 ('TRUE' === $this->response) {
$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];
}
}

View File

@@ -0,0 +1,159 @@
<?php
/* For licensing terms, see /license.txt */
/**
* A SCORM item. It corresponds to 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.
*
* Attached files are NOT exported.
*/
class ScormAssessmentItem
{
public $question;
public $question_ident;
public $answer;
/**
* Constructor.
*
* @param ScormQuestion $question the Question object we want to export
*/
public function __construct($question)
{
$this->question = $question;
$this->question->setAnswer();
$this->questionIdent = 'QST_'.$question->iid;
}
/**
* Start the XML flow.
*
* This opens the <item> block, with correct attributes.
*/
public function start_page()
{
return '';
}
/**
* End the XML flow, closing the </item> tag.
*/
public function end_page()
{
/*if ($this->standalone) {
return '</html>';
}*/
return '';
}
/**
* Start document header.
*/
public function start_header()
{
/*if ($this->standalone) {
return '<head>';
}*/
return '';
}
/**
* Print CSS inclusion.
*/
public function css()
{
return '';
}
/**
* End document header.
*/
public function end_header()
{
// if ($this->standalone) {
// return '</head>';
// }
return '';
}
/**
* Start the itemBody.
*/
public function start_js()
{
return '<script type="text/javascript" src="assets/api_wrapper.js"></script>';
}
/**
* End the itemBody part.
*/
public function end_js()
{
/*if ($this->standalone) {
return '</script>';
}*/
return '';
}
/**
* Start the itemBody.
*/
public function start_body()
{
/*if ($this->standalone) {
return '<body><form id="dokeos_scorm_form" method="post" action="">';
}*/
return '';
}
/**
* End the itemBody part.
*/
public function end_body()
{
/*if ($this->standalone) {
return '<br /><input class="btn" type="button" id="dokeos_scorm_submit" name="dokeos_scorm_submit" value="OK" /></form></body>';
}*/
return '';
}
/**
* Export the question as a SCORM Item.
* This is a default behaviour, some classes may want to override this.
*
* @return string|array a string, the XML flow for an Item
*/
public function export()
{
list($js, $html) = $this->question->export();
/*if ($this->standalone) {
$res = $this->start_page()
.$this->start_header()
.$this->css()
.$this->start_js()
.$this->common_js()
.$js
.$this->end_js()
.$this->end_header()
.$this->start_body()
.$html
.$this->end_body()
.$this->end_page();
return $res;
} else {
return [$js, $html];
}*/
return [$js, $html];
}
}

View File

@@ -0,0 +1,216 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CourseBundle\Entity\CQuiz;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
use Symfony\Component\Serializer\Serializer;
/**
* This class represents an entire exercise to be exported in SCORM.
* It will be represented by a single <section> containing several <item>.
*
* Some properties cannot be exported, as SCORM does not support them :
* - type (one page or multiple pages)
* - start_date and end_date
* - max_attempts
* - show_answer
* - anonymous_attempts
*
* @author Julio Montoya
* @author Amand Tihon <amand@alrj.org>
*/
class ScormExercise
{
public $exercise;
public $standalone;
/**
* ScormExercise constructor.
*
* @param Exercise $exe
* @param bool $standalone
*/
public function __construct($exe, $standalone)
{
$this->exercise = $exe;
$this->standalone = $standalone;
}
/**
* Start the XML flow.
*
* This opens the <item> block, with correct attributes.
*/
public function startPage()
{
$charset = 'UTF-8';
return '<?xml version="1.0" encoding="'.$charset.'" standalone="no"?><html>';
}
/**
* End the XML flow, closing the </item> tag.
*/
public function end_page()
{
return '</html>';
}
/**
* Start document header.
*/
public function start_header()
{
return '<head>';
}
/**
* Common JS functions.
*/
public function common_js()
{
$js = file_get_contents(api_get_path(SYS_CODE_PATH).'exercise/export/scorm/common.js');
return $js."\n";
}
/**
* End the itemBody part.
*/
public function end_js()
{
return '</script>';
}
/**
* Start the itemBody.
*/
public function start_body()
{
return '<body>'.
'<h1>'.$this->exercise->selectTitle().'</h1><p>'.$this->exercise->selectDescription().'</p>'.
'<form id="chamilo_scorm_form" method="post" action="">'.
'<table width="100%">';
}
/**
* End the itemBody part.
*/
public function end_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>';
}
/**
* Export the question as a SCORM Item.
*
* This is a default behaviour, some classes may want to override this.
*
* @return string string, the XML flow for an Item
*/
public function export()
{
global $charset;
/*$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->exportQuestions();
return $this->startPage()
.$this->start_header()
.$this->css()
.$this->globalAssets()
.$this->start_js()
.$this->common_js()
.$js
.$this->end_js()
.$this->end_header()
.$this->start_body()
.$html
.$this->end_body()
.$this->end_page();
}
/**
* Export the questions, as a succession of <items>.
*
* @author Amand Tihon <amand@alrj.org>
*/
public function exportQuestions()
{
$js = $html = '';
$encoders = [new JsonEncoder()];
$normalizers = [new ObjectNormalizer()];
$em = Database::getManager();
// Export cquiz data
/** @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;
$scormQuestion = new ScormQuestion();
foreach ($this->exercise->selectQuestionList() as $q) {
list($jstmp, $htmltmp) = $scormQuestion->exportQuestionToScorm($q, $counter);
$js .= $jstmp."\n";
$html .= $htmltmp."\n";
$counter++;
}
return [$js, $html];
}
/**
* Print CSS inclusion.
*/
private function css()
{
return '';
}
/**
* End document header.
*/
private function end_header()
{
return '</head>';
}
/**
* Start the itemBody.
*/
private function start_js()
{
return '<script>';
}
/**
* @return string
*/
private function globalAssets()
{
$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;
}
}

View File

@@ -0,0 +1,221 @@
<?php
/* For licensing terms, see /license.txt */
/**
* The ScormQuestion class is a gateway to getting the answers exported
* (the question is just an HTML text, while the answers are the most important).
* It is important to note that the SCORM export process is done in two parts.
* First, the HTML part (which is the presentation), and second the JavaScript
* part (the process).
* The two bits are separate to allow for a one-big-javascript and a one-big-html
* files to be built. Each export function thus returns an array of HTML+JS.
*
* @author Claro Team <cvs@claroline.net>
* @author Yannick Warnier <yannick.warnier@beeznest.com>
*/
class ScormQuestion extends Question
{
public $js_id;
public $answer;
/**
* ScormQuestion constructor.
*/
public function __construct()
{
parent::__construct();
}
/**
* Returns the HTML + JS flow corresponding to one question.
*
* @param int $questionId The question ID
* @param int $jsId The JavaScript ID for this question.
* Due to the nature of interactions, we must have a natural sequence for
* questions in the generated JavaScript.
*
* @return string|array
*/
public function exportQuestionToScorm(
$questionId,
$jsId
) {
$question = self::read($questionId);
if (!$question) {
return '';
}
$this->iid = $question->iid;
$this->js_id = $jsId;
$this->type = $question->type;
$this->question = $question->question;
$this->description = $question->description;
$this->weighting = $question->weighting;
$this->position = $question->position;
$this->picture = $question->picture;
$assessmentItem = new ScormAssessmentItem($this);
return $assessmentItem->export();
}
/**
* Include the correct answer class and create answer.
*/
public function setAnswer()
{
switch ($this->type) {
case MCUA:
$this->answer = new ScormAnswerMultipleChoice($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
case MCMA:
case GLOBAL_MULTIPLE_ANSWER:
$this->answer = new ScormAnswerMultipleChoice($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
case TF:
$this->answer = new ScormAnswerTrueFalse($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
case FIB:
$this->answer = new ScormAnswerFillInBlanks($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
case MATCHING:
case MATCHING_DRAGGABLE:
case DRAGGABLE:
$this->answer = new ScormAnswerMatching($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
case ORAL_EXPRESSION:
case FREE_ANSWER:
$this->answer = new ScormAnswerFree($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
case HOT_SPOT:
case HOT_SPOT_COMBINATION:
$this->answer = new ScormAnswerHotspot($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
case MULTIPLE_ANSWER_COMBINATION:
$this->answer = new ScormAnswerMultipleChoice($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
case HOT_SPOT_ORDER:
$this->answer = new ScormAnswerHotspot($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
case HOT_SPOT_DELINEATION:
$this->answer = new ScormAnswerHotspot($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
// not supported
case UNIQUE_ANSWER_NO_OPTION:
case MULTIPLE_ANSWER_TRUE_FALSE:
case MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE:
case UNIQUE_ANSWER_IMAGE:
case CALCULATED_ANSWER:
$this->answer = new ScormAnswerMultipleChoice($this->iid);
$this->answer->questionJSId = $this->js_id;
break;
default:
$this->answer = new stdClass();
$this->answer->questionJSId = $this->js_id;
break;
}
return true;
}
/**
* @throws Exception
*
* @return array
*/
public function export()
{
$html = $this->getQuestionHTML();
$js = $this->getQuestionJS();
if (is_object($this->answer) && $this->answer instanceof Answer) {
list($js2, $html2) = $this->answer->export();
$js .= $js2;
$html .= $html2;
} else {
throw new \Exception('Question not supported. Exercise: '.$this->selectTitle());
}
return [$js, $html];
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
return true;
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
return true;
}
/**
* Returns an HTML-formatted question.
*/
public function getQuestionHTML()
{
$title = $this->selectTitle();
$description = $this->selectDescription();
$cols = 2;
return '<tr>
<td colspan="'.$cols.'" id="question_'.$this->iid.'_title" valign="middle" style="background-color:#d6d6d6;">
'.$title.'
</td>
</tr>
<tr>
<td valign="top" colspan="'.$cols.'">
<i>'.$description.'</i>
</td>
</tr>';
}
/**
* Return the JavaScript code bound to the question.
*/
public function getQuestionJS()
{
$weight = $this->selectWeighting();
$js = '
questions.push('.$this->js_id.');
$(function() {
if (exerciseInfo.randomAnswers == true) {
$("#question_'.$this->js_id.'").shuffleRows();
}
});';
$js .= "\n";
switch ($this->type) {
case ORAL_EXPRESSION:
/*$script = file_get_contents(api_get_path(LIBRARY_PATH) . 'javascript/rtc/RecordRTC.js');
$script .= file_get_contents(api_get_path(LIBRARY_PATH) . 'wami-recorder/recorder.js');
$script .= file_get_contents(api_get_path(LIBRARY_PATH) . 'wami-recorder/gui.js');
$js .= $script;*/
break;
case HOT_SPOT:
case HOT_SPOT_COMBINATION:
//put the max score to 0 to avoid discounting the points of
//non-exported quiz types in the SCORM
$weight = 0;
break;
}
$js .= 'questions_score_max['.$this->js_id.'] = '.$weight.';';
return $js;
}
}

View File

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

View File

@@ -0,0 +1,144 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This class handles the SCORM export of free-answer questions.
*/
class ScormAnswerFree extends Answer
{
/**
* Export the text with missing words.
*
* As a side effect, it stores two lists in the class :
* the missing words and their respective weightings.
*/
public function export()
{
$js = '';
$identifier = 'question_'.$this->questionJSId.'_free';
// currently the free answers cannot be displayed, so ignore the textarea
$html = '<tr><td colspan="2">';
$type = $this->getQuestionType();
if (ORAL_EXPRESSION == $type) {
/*
$template = new Template('');
$template->assign('directory', '/tmp/');
$template->assign('user_id', api_get_user_id());
$layout = $template->get_template('document/record_audio.tpl');
$html .= $template->fetch($layout);*/
$html = '<tr><td colspan="2">'.get_lang('ThisItemIsNotExportable').'</td></tr>';
return [$js, $html];
}
$html .= '<textarea minlength="20" name="'.$identifier.'" id="'.$identifier.'" ></textarea>';
$html .= '</td></tr>';
$js .= 'questions_answers['.$this->questionJSId.'] = new Array();';
$js .= 'questions_answers_correct['.$this->questionJSId.'] = "";';
$js .= 'questions_types['.$this->questionJSId.'] = \'free\';';
$jstmpw = 'questions_answers_ponderation['.$this->questionJSId.'] = "0";';
$js .= $jstmpw;
return [$js, $html];
}
}
/**
* This class handles the SCORM export of hotpot questions.
*/
class ScormAnswerHotspot extends Answer
{
/**
* Returns the javascript code that goes with HotSpot exercises.
*
* @return string The JavaScript code
*/
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) {
//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";
$header .= 'questions_answers_correct['.$this->questionJSId.'] = new Array();'."\n";
$header .= 'questions_types['.$this->questionJSId.'] = \'hotspot\';'."\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] = 0;'.";\n";
$header .= $jstmpw;
} else {
$header = '';
$header .= 'questions_answers['.$this->questionJSId.'] = new Array();'."\n";
$header .= 'questions_answers_correct['.$this->questionJSId.'] = new Array();'."\n";
$header .= 'questions_types['.$this->questionJSId.'] = \'hotspot\';'."\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] = 0;'."\n";
$header .= $jstmpw;
}
return $header;
}
/**
* Export the text with missing words.
*
* As a side effect, it stores two lists in the class :
* the missing words and their respective weightings.
*/
public function export()
{
$js = $this->get_js_header();
$html = '<tr><td colspan="2"><table width="100%">';
// some javascript must be added for that kind of questions
$html .= '';
// Get the answers, make a list
$nbrAnswers = $this->selectNbrAnswers();
$answerList = '<div
style="padding: 10px;
margin-left: -8px;
border: 1px solid #4271b5;
height: 448px;
width: 200px;"><b>'.get_lang('HotspotZones').'</b><ol>';
for ($answerId = 1; $answerId <= $nbrAnswers; $answerId++) {
$answerList .= '<li>'.$this->selectAnswer($answerId).'</li>';
}
$answerList .= '</ol></div>';
$relPath = api_get_path(REL_PATH);
$html .= <<<HTML
<tr>
<td>
<div id="hotspot-{$this->questionJSId}"></div>
<script>
document.addEventListener('DOMContentListener', function () {
new HotspotQuestion({
questionId: {$this->questionJSId},
selector: '#hotspot-{$this->questionJSId}',
for: 'user',
relPath: '$relPath'
});
});
</script>
</td>
<td>
$answerList
</td>
<tr>
HTML;
$html .= '</table></td></tr>';
// currently the free answers cannot be displayed, so ignore the textarea
$html = '<tr><td colspan="2">'.get_lang('ThisItemIsNotExportable').'</td></tr>';
return [$js, $html];
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,68 @@
<?php
/* For licensing terms, see /license.txt */
/**
* File containing the FreeAnswer class.
* This class allows to instantiate an object of type FREE_ANSWER,
* extending the class question.
*
* @author Eric Marguin
*/
class FreeAnswer extends Question
{
public $typePicture = 'open_answer.png';
public $explanationLangVar = 'FreeAnswer';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = FREE_ANSWER;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$form->addElement('text', 'weighting', get_lang('Weighting'));
global $text;
// setting the save button here and not in the question class.php
$form->addButtonSave($text, 'submitQuestion');
if (!empty($this->iid)) {
$form->setDefaults(['weighting' => float_format($this->weighting, 1)]);
} else {
if ($this->isContent == 1) {
$form->setDefaults(['weighting' => '10']);
}
}
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$this->weighting = $form->getSubmitValue('weighting');
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$score['revised'] = $this->isQuestionWaitingReview($score);
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'" >
<tr>
<th>'.get_lang('Answer').'</th>
</tr>';
return $header;
}
}

View File

@@ -0,0 +1,278 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* Class GlobalMultipleAnswer.
*/
class GlobalMultipleAnswer extends Question
{
public $typePicture = 'mcmagl.png';
public $explanationLangVar = 'GlobalMultipleAnswer';
/**
* GlobalMultipleAnswer constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = GLOBAL_MULTIPLE_ANSWER;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$nb_answers = isset($_POST['nb_answers']) ? $_POST['nb_answers'] : 4;
$nb_answers += isset($_POST['lessAnswers']) ? -1 : (isset($_POST['moreAnswers']) ? 1 : 0);
$obj_ex = Session::read('objExercise');
$form->addHeader(get_lang('Answers'));
/* Mise en variable de Affichage "Reponses" et son icone, "N<>", "Vrai", "Reponse" */
$html = '<table class="table table-striped table-hover">
<tr>
<th width="10px">'.get_lang('Number').'</th>
<th width="10px">'.get_lang('True').'</th>
<th width="50%">'.get_lang('Answer').'</th>
<th width="50%">'.get_lang('Comment').'</th>
</tr>
';
$form->addHtml($html);
$defaults = [];
$correct = 0;
$answer = false;
if (!empty($this->iid)) {
$answer = new Answer($this->iid);
$answer->read();
if ($answer->nbrAnswers > 0 && !$form->isSubmitted()) {
$nb_answers = $answer->nbrAnswers;
}
}
// le nombre de r<>ponses est bien enregistr<74> sous la forme int(nb)
/* Ajout mise en forme nb reponse */
$form->addElement('hidden', 'nb_answers');
$boxes_names = [];
if ($nb_answers < 1) {
$nb_answers = 1;
echo Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'), 'normal');
}
//D<>but affichage score global dans la modification d'une question
$scoreA = 0; //par reponse
$scoreG = 0; //Global
/* boucle pour sauvegarder les donn<6E>es dans le tableau defaults */
for ($i = 1; $i <= $nb_answers; $i++) {
/* si la reponse est de type objet */
if (is_object($answer)) {
$defaults['answer['.$i.']'] = $answer->answer[$i];
$defaults['comment['.$i.']'] = $answer->comment[$i];
$defaults['correct['.$i.']'] = $answer->correct[$i];
// start
$scoreA = $answer->weighting[$i];
}
if ($scoreA > 0) {
$scoreG = $scoreG + $scoreA;
}
//------------- Fin
//------------- Debut si un des scores par reponse est egal <20> 0 : la coche vaut 1 (coch<63>)
if ($scoreA == 0) {
$defaults['pts'] = 1;
}
$renderer = &$form->defaultRenderer();
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'correct['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'counter['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'answer['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'comment['.$i.']'
);
$answer_number = $form->addElement(
'text',
'counter['.$i.']',
null,
'value="'.$i.'"'
);
$answer_number->freeze();
$form->addElement('checkbox', 'correct['.$i.']', null, null, 'class="checkbox"');
$boxes_names[] = 'correct['.$i.']';
$form->addHtmlEditor(
"answer[$i]",
'',
false,
false,
[
'ToolbarSet' => 'TestProposedAnswer',
'Width' => '100%',
'Height' => '100',
]
);
$form->addHtmlEditor(
"comment[$i]",
'',
false,
false,
[
'ToolbarSet' => 'TestProposedAnswer',
'Width' => '100%',
'Height' => '100',
]
);
$form->addElement('html', '</tr>');
}
//--------- Mise en variable du score global lors d'une modification de la question/r<>ponse
$defaults['weighting[1]'] = (round($scoreG));
$form->addElement('html', '</div></div></table>');
$form->add_multiple_required_rule(
$boxes_names,
get_lang('ChooseAtLeastOneCheckbox'),
'multiple_required'
);
//only 1 answer the all deal ...
$form->addElement('text', 'weighting[1]', get_lang('Score'));
//--------- Creation coche pour ne pas prendre en compte les n<>gatifs
$form->addElement('checkbox', 'pts', '', get_lang('NoNegativeScore'));
$form->addElement('html', '<br />');
// Affiche un message si le score n'est pas renseign<67>
$form->addRule('weighting[1]', get_lang('ThisFieldIsRequired'), 'required');
global $text;
if ($obj_ex->edit_exercise_in_lp ||
(empty($this->exerciseList) && empty($obj_ex->iid))
) {
// setting the save button here and not in the question class.php
$form->addButtonDelete(get_lang('LessAnswer'), 'lessAnswers');
$form->addButtonCreate(get_lang('PlusAnswer'), 'moreAnswers');
$form->addButtonSave($text, 'submitQuestion');
}
$renderer->setElementTemplate('{element}&nbsp;', 'lessAnswers');
$renderer->setElementTemplate('{element}&nbsp;', 'submitQuestion');
$renderer->setElementTemplate('{element}', 'moreAnswers');
$form->addElement('html', '</div></div>');
$defaults['correct'] = $correct;
if (!empty($this->iid)) {
$form->setDefaults($defaults);
} else {
if ($this->isContent == 1) {
$form->setDefaults($defaults);
}
}
$form->setConstants(['nb_answers' => $nb_answers]);
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$objAnswer = new Answer($this->iid);
$nb_answers = $form->getSubmitValue('nb_answers');
// Score total
$answer_score = trim($form->getSubmitValue('weighting[1]'));
// Reponses correctes
$nbr_corrects = 0;
for ($i = 1; $i <= $nb_answers; $i++) {
$goodAnswer = trim($form->getSubmitValue('correct['.$i.']'));
if ($goodAnswer) {
$nbr_corrects++;
}
}
// Set question weighting (score total)
$questionWeighting = $answer_score;
// Set score per answer
$nbr_corrects = $nbr_corrects == 0 ? 1 : $nbr_corrects;
$answer_score = $nbr_corrects == 0 ? 0 : $answer_score;
$answer_score = $answer_score / $nbr_corrects;
//$answer_score <20>quivaut <20> la valeur d'une bonne r<>ponse
// cr<63>ation variable pour r<>cuperer la valeur de la coche pour la prise en compte des n<>gatifs
$test = $form->getSubmitValue('pts');
for ($i = 1; $i <= $nb_answers; $i++) {
$answer = trim($form->getSubmitValue('answer['.$i.']'));
$comment = trim($form->getSubmitValue('comment['.$i.']'));
$goodAnswer = trim($form->getSubmitValue('correct['.$i.']'));
if ($goodAnswer) {
$weighting = abs($answer_score);
} else {
if ($test == 1) {
$weighting = 0;
} else {
$weighting = -abs($answer_score);
}
}
$objAnswer->createAnswer($answer, $goodAnswer, $comment, $weighting, $i);
}
// saves the answers into the data base
$objAnswer->save();
// sets the total weighting of the question --> sert <20> donner le score total pendant l'examen
$this->updateWeighting($questionWeighting);
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'"><tr>';
if (!in_array($exercise->results_disabled, [RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER])) {
$header .= '<th>'.get_lang('Choice').'</th>';
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
}
$header .= '<th>'.get_lang('Answer').'</th>';
if ($exercise->showExpectedChoice()) {
$header .= '<th>'.get_lang('Status').'</th>';
}
if (false === $exercise->hideComment) {
$header .= '<th>'.get_lang('Comment').'</th>';
}
$header .= '</tr>';
return $header;
}
}

View File

@@ -0,0 +1,526 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Code library for HotPotatoes integration.
*
* @package chamilo.exercise
*
* @author Istvan Mandak (original author)
*/
/* TODO: This is a global variable with too simple name, conflicts are possible.
Better eliminate it. Correct the test unit too. */
$dbTable = Database::get_course_table(TABLE_DOCUMENT);
/**
* Creates a hotpotato directory.
*
* If a directory of that name already exists, don't create any.
* If a file of that name exists, remove it and create a directory.
*
* @param string $base_work_dir Wanted path
*
* @return bool Always true so far
*/
function hotpotatoes_init($base_work_dir)
{
//global $_course, $_user;
$document_path = $base_work_dir.'/';
if (!is_dir($document_path)) {
if (is_file($document_path)) {
@unlink($document_path);
}
@mkdir($document_path, api_get_permissions_for_new_directories());
return true;
} else {
return false;
}
//why create a .htaccess here?
//if (!is_file($document_path.".htacces"))
//{
// if (!($fp = fopen($document_path.".htaccess", "w"))) {
// }
// $str = "order deny,allow\nallow from all";
// if (!fwrite($fp,$str)) { }
//}
}
/**
* Gets the title of the quiz file given as parameter.
*
* @param string $fname File name
* @param string $fpath File path
*
* @return string The exercise title
*/
function GetQuizName($fname, $fpath)
{
$title = GetComment($fname);
if (trim($title) == '') {
if (file_exists($fpath.$fname)) {
if (!($fp = @fopen($fpath.$fname, 'r'))) {
//die('Could not open Quiz input.');
return basename($fname);
}
$contents = @fread($fp, filesize($fpath.$fname));
@fclose($fp);
$title = api_get_title_html($contents);
}
}
if ($title == '') {
$title = basename($fname);
}
return (string) $title;
}
/**
* Gets the comment about a file from the corresponding database record.
*
* @param string $path File path
* @param string $courseCode (if not given, the course will be taken from the context)
*
* @return string comment from the database record
* Added conditional to the table if is empty
*/
function GetComment($path, $courseCode = '')
{
$dbTable = Database::get_course_table(TABLE_DOCUMENT);
$course_info = api_get_course_info($courseCode);
$path = Database::escape_string($path);
if (!empty($course_info) && !empty($path)) {
$query = "SELECT comment FROM $dbTable WHERE c_id = {$course_info['real_id']}";
$result = Database::query($query);
while ($row = Database::fetch_array($result)) {
return $row[0];
}
}
return null;
}
/**
* Sets the comment in the database for a particular path.
*
* @param string $path File path
* @param string $comment Comment to set
*
* @return Doctrine\DBAL\Driver\Statement|null
* Result of the database operation
* (Database::query will output some message directly on error anyway)
*/
function SetComment($path, $comment)
{
$dbTable = Database::get_course_table(TABLE_DOCUMENT);
$path = Database::escape_string($path);
$comment = Database::escape_string($comment);
$course_id = api_get_course_int_id();
$query = "UPDATE $dbTable SET comment = '$comment'
WHERE $course_id AND path = '$path'";
$result = Database::query($query);
return $result;
}
/**
* Reads the file contents into a string.
*
* @param string $full_file_path Urlencoded path
*
* @return string The file contents or false on security error
*/
function ReadFileCont($full_file_path)
{
if (empty($full_file_path)) {
return false;
}
if (Security::check_abs_path(dirname($full_file_path).'/', api_get_path(SYS_COURSE_PATH))) {
if (is_file($full_file_path)) {
if (!($fp = fopen(urldecode($full_file_path), 'r'))) {
return '';
}
$contents = fread($fp, filesize($full_file_path));
fclose($fp);
return $contents;
}
}
return false;
}
/**
* Writes the file contents into the given file path.
*
* @param string $full_file_path Urlencoded path
* @param string $content The file contents
*
* @return bool True on success, false on security error
*/
function WriteFileCont($full_file_path, $content)
{
// Check if this is not an attack, trying to get into other directories or something like that.
$_course = api_get_course_info();
if (Security::check_abs_path(
dirname($full_file_path).'/',
api_get_path(SYS_COURSE_PATH).$_course['path'].'/'
)) {
// Check if this is not an attack, trying to upload a php file or something like that.
if (basename($full_file_path) != Security::filter_filename(basename($full_file_path))) {
return false;
}
//if (!($fp = fopen(urldecode($full_file_path), 'w'))) {
//die('Could not open Quiz input.');
//}
$fp = fopen(urldecode($full_file_path), 'w');
if ($fp !== false) {
fwrite($fp, $content);
fclose($fp);
return true;
}
}
return false;
}
/**
* Gets the name of an img whose path is given (without directories or extensions).
*
* @param string $imageTag An image tag (<img src="...." ...>)
*
* @return string The image file name or an empty string
*/
function GetImgName($imageTag)
{
// Select src tag from img tag.
$match = [];
//preg_match('/(src=(["\'])1.*(["\'])1)/i', $imageTag, $match); //src
preg_match('/src(\s)*=(\s)*[\'"]([^\'"]*)[\'"]/i', $imageTag, $match); //get the img src as contained between " or '
//list($key, $srctag) = each($match);
$src = $match[3];
//$src = substr($srctag, 5, (strlen($srctag) - 7));
if (stristr($src, 'http') === false) {
// Valid or invalid image name.
if ($src == '') {
return '';
} else {
$tmp_src = basename($src);
if ($tmp_src == '') {
return $src;
} else {
return $tmp_src;
}
}
} else {
// The img tag contained "http", which means it is probably external. Ignore it.
return '';
}
}
/**
* Gets the source path of an image tag.
*
* @param string $imageTag An image tag
*
* @return string The image source or ""
*/
function GetSrcName($imageTag)
{
// Select src tag from img tag.
$match = [];
preg_match("|(src=\".*\" )|U", $imageTag, $match); //src
$srctag = reset($match);
$src = substr($srctag, 5, (strlen($srctag) - 7));
if (stristr($src, 'http') === false) {
// valid or invalid image name
return $src;
} else {
return '';
}
}
/**
* Gets the image parameters from an image path.
*
* @param string $fname File name
* @param string $fpath File path
* @param array $imgparams Reference to a list of image parameters (emptied, then used to return results)
* @param int $imgcount Reference to a counter of images (emptied, then used to return results)
*/
function GetImgParams($fname, $fpath, &$imgparams, &$imgcount)
{
// Select img tags from context.
$imgparams = [];
//phpinfo();
$contents = ReadFileCont("$fpath"."$fname");
$matches = [];
preg_match_all('(<img .*>)', $contents, $matches);
$imgcount = 0;
foreach ($matches as $match) {
// Each match consists of a key and a value.
foreach ($match as $imageTag) {
$imgname = GetImgName($imageTag);
if ($imgname != '' && !in_array($imgname, $imgparams)) {
array_push($imgparams, $imgname); // name (+ type) of the images in the html test
$imgcount = $imgcount + 1; // number of images in the html test
}
}
}
}
/**
* Generates a list of hidden fields with the image params given as parameter to this function.
*
* @param array $imgparams List of image parameters
*
* @return string String containing the hidden parameters built from the list given
*/
function GenerateHiddenList($imgparams)
{
$list = '';
if (is_array($imgparams)) {
foreach ($imgparams as $string) {
$list .= "<input type=\"hidden\" name=\"imgparams[]\" value=\"$string\" />\n";
}
}
return $list;
}
/**
* Searches for a node in the given array.
*
* @param array $array Reference to the array to search
* @param string $node Node we are looking for in the array
*
* @return mixed Node name or false if not found
*/
function myarraysearch(&$array, $node)
{
$match = false;
$tmp_array = [];
for ($i = 0; $i < count($array); $i++) {
if (!strcmp($array[$i], $node)) {
$match = $node;
} else {
array_push($tmp_array, $array[$i]);
}
}
$array = $tmp_array;
return $match;
}
/**
* Searches an image name into an array.
*
* @param array $imgparams Reference to an array to search
* @param string $string String to look for
*
* @return mixed String given if found, false otherwise
*
* @uses \myarraysearch This function is just an additional layer on the myarraysearch() function
*/
function CheckImageName(&$imgparams, $string)
{
$checked = myarraysearch($imgparams, $string);
return $checked;
}
/**
* Replaces an image tag by ???
*
* @param string $content The content to replace
*
* @return string The modified content
*/
function ReplaceImgTag($content)
{
$newcontent = $content;
$matches = [];
preg_match_all('(<img .*>)', $content, $matches);
foreach ($matches as $match) {
foreach ($match as $imageTag) {
$imgname = GetSrcName($imageTag);
if ($imgname != '') {
$prehref = $imgname;
$posthref = basename($imgname);
$newcontent = str_replace($prehref, $posthref, $newcontent);
}
}
}
return $newcontent;
}
/**
* Fills the folder name up to a certain length with "0".
*
* @param string $name Original folder name
* @param int $nsize Length to reach
*
* @return string Modified folder name
*/
function FillFolderName($name, $nsize)
{
$str = '';
for ($i = 0; $i < $nsize - strlen($name); $i++) {
$str .= '0';
}
$str .= $name;
return $str;
}
/**
* Generates the HotPotato folder tree.
*
* @param string $folder Folder path
*
* @return string Folder name (modified)
*/
function GenerateHpFolder($folder)
{
$filelist = [];
if ($dir = @opendir($folder)) {
while (($file = readdir($dir)) !== false) {
if ($file != '.') {
if ($file != '..') {
$full_name = $folder.'/'.$file;
if (is_dir($full_name)) {
$filelist[] = $file;
}
}
}
}
}
$name = '';
$w = true;
while ($w == true) {
$name = FillFolderName(mt_rand(1, 99999), 6);
$checked = myarraysearch($filelist, $name);
// As long as we find the name in the array, continue looping. As soon as we have a new element, quit.
if (!$checked) {
$w = false;
}
}
return $name;
}
/**
* Gets the folder name (strips down path).
*
* @param string $fname Path
*
* @return string Folder name stripped down
*/
function GetFolderName($fname)
{
$name = explode('/', $fname);
$name = $name[count($name) - 2];
return $name;
}
/**
* Gets the folder path (with out the name of the folder itself) ?
*
* @param string $fname Path
*
* @return string Path stripped down
*/
function GetFolderPath($fname)
{
$str = '';
$name = explode('/', $fname);
for ($i = 0; $i < sizeof($name) - 1; $i++) {
$str = $str.$name[$i].'/';
}
return $str;
}
/**
* Checks if there are subfolders.
*
* @param string $path Path
*
* @return int 1 if a subfolder was found, 0 otherwise
*/
function CheckSubFolder($path)
{
$folder = GetFolderPath($path);
$dflag = 0;
if ($dir = @opendir($folder)) {
while (($file = readdir($dir)) !== false) {
if ($file != '.') {
if ($file != '..') {
$full_name = $folder.'/'.$file;
if (is_dir($full_name)) {
$dflag = 1; // first directory
}
}
}
}
}
return $dflag;
}
/**
* Hotpotato Garbage Collector.
*
* @param string $folder Path
* @param int $flag Flag
* @param int $user_id User id
*/
function HotPotGCt($folder, $flag, $user_id)
{
// Garbage Collector
$filelist = [];
if ($dir = @opendir($folder)) {
while (($file = readdir($dir)) !== false) {
if ($file != '.') {
if ($file != '..') {
$full_name = $folder.'/'.$file;
if (is_dir($full_name)) {
HotPotGCt($folder.'/'.$file, $flag, $user_id);
} else {
$filelist[] = $file;
}
}
}
}
closedir($dir);
}
foreach ($filelist as $val) {
if (stristr($val, $user_id.'.t.html')) {
if ($flag == 1) {
my_delete($folder.'/'.$val);
} else {
echo $folder.'/'.$val.'<br />';
}
}
}
}
/**
* Deletes an attempt from TABLE_STATISTIC_TRACK_E_HOTPOTATOES.
*
* @param int $id
*/
function deleteAttempt($id)
{
$table = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
$id = intval($id);
$sql = "DELETE FROM $table WHERE id = $id";
Database::query($sql);
}

View File

@@ -0,0 +1,262 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Code for HotPotatoes integration.
*
* @package chamilo.exercise
*
* @author Istvan Mandak (original author)
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
require_once 'hotpotatoes.lib.php';
// Section (for the tabs).
$this_section = SECTION_COURSES;
$_course = api_get_course_info();
// Access restriction: only teachers are allowed here.
if (!api_is_allowed_to_edit(null, true)) {
api_not_allowed();
}
if (api_is_in_gradebook()) {
$interbreadcrumb[] = [
'url' => Category::getUrl(),
'name' => get_lang('ToolGradebook'),
];
}
// The breadcrumbs.
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$is_allowedToEdit = api_is_allowed_to_edit(null, true);
// Database table definitions.
$dbTable = Database::get_course_table(TABLE_DOCUMENT);
$course_id = $_course['real_id'];
$sessionId = api_get_session_id();
// Setting some variables.
$document_sys_path = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
$uploadPath = '/HotPotatoes_files';
$finish = (!empty($_POST['finish']) ? $_POST['finish'] : 0);
$imgcount = (!empty($_POST['imgcount']) ? $_POST['imgcount'] : null);
$fld = (!empty($_POST['fld']) ? $_POST['fld'] : null);
$imgparams = [];
$dialogBox = '';
if ($finish == 2 && isset($_POST['imgparams'])) {
$imgparams = $_POST['imgparams'];
}
// If user is allowed to edit...
if (api_is_allowed_to_edit(null, true)) {
if (hotpotatoes_init($document_sys_path.$uploadPath)) {
// If the directory doesn't exist, create the "HotPotatoes" directory.
$doc_id = add_document(
$_course,
'/HotPotatoes_files',
'folder',
0,
get_lang('HotPotatoesFiles')
);
// Update properties in dbase (in any case).
api_item_property_update(
$_course,
TOOL_DOCUMENT,
$doc_id,
'FolderCreated',
api_get_user_id()
);
// Make invisible (in any case) - why?
api_item_property_update(
$_course,
TOOL_DOCUMENT,
$doc_id,
'invisible',
api_get_user_id()
);
}
}
/** Display */
$nameTools = get_lang('HotPotatoesTests');
$form = new FormValidator(
'hotpotatoes',
'post',
api_get_self()."?".api_get_cidreq(),
null,
['enctype' => 'multipart/form-data']
);
$form->addElement('header', $nameTools);
$form->addElement('hidden', 'uploadPath');
$form->addElement('hidden', 'fld', $fld);
$form->addElement('hidden', 'imgcount', $imgcount);
$form->addElement('hidden', 'finish', $finish);
$form->addElement('html', GenerateHiddenList($imgparams));
$form->addElement('label', '', Display::return_icon('hotpotatoes.jpg', 'HotPotatoes'));
$label = get_lang('DownloadImg').' : ';
if ($finish == 0) {
$label = get_lang('DownloadFile').' : ';
}
$form->addElement('file', 'userFile', $label);
$form->addButtonSend(get_lang('SendFile'));
// If finish is set; it's because the user came from this script in the first place (displaying hidden "finish" field).
if ((api_is_allowed_to_edit(null, true)) && (($finish == 0) || ($finish == 2))) {
// Moved this down here as the upload handling functions give output.
if ($form->validate()) {
// Initialise $finish
if (!isset($finish)) {
$finish = 0;
}
//if the size is not defined, it's probably because there has been an error or no file was submitted
if (!$_FILES['userFile']['size']) {
$dialogBox .= get_lang('SendFileError').'<br />'.get_lang('Notice').' : '.get_lang('MaxFileSize').' '.ini_get('upload_max_filesize');
} else {
$unzip = 0;
if (preg_match('/\.zip$/i', $_FILES['userFile']['name'])) {
//if it's a zip, allow zip upload
$unzip = 1;
}
$filename = api_replace_dangerous_char(trim($_FILES['userFile']['name']));
if ($finish == 0) {
// Generate new test folder if on first step of file upload.
$fld = GenerateHpFolder($document_sys_path.$uploadPath.'/');
@mkdir($document_sys_path.$uploadPath.'/'.$fld, api_get_permissions_for_new_directories());
$doc_id = add_document($_course, '/HotPotatoes_files/'.$fld, 'folder', 0, $fld);
api_item_property_update(
$_course,
TOOL_DOCUMENT,
$doc_id,
'FolderCreated',
api_get_user_id()
);
}
$allow_output_on_success = false;
if (handle_uploaded_document(
$_course,
$_FILES['userFile'],
$document_sys_path,
$uploadPath.'/'.$fld,
api_get_user_id(),
null,
null,
$unzip,
'',
$allow_output_on_success
)) {
if ($finish == 2) {
$imgparams = $_POST['imgparams'];
$checked = CheckImageName($imgparams, $filename);
if ($checked) {
$imgcount = $imgcount - 1;
} else {
$dialogBox .= $filename.' '.get_lang('NameNotEqual');
my_delete($document_sys_path.$uploadPath.'/'.$fld.'/'.$filename);
DocumentManager::updateDbInfo('delete', $uploadPath.'/'.$fld.'/'.$filename);
}
if ($imgcount == 0) { // all image uploaded
$finish = 1;
}
} else {
// If we are (still) on the first step of the upload process.
if ($finish == 0) {
$finish = 2;
// Get number and name of images from the files contents.
GetImgParams('/'.$filename, $document_sys_path.$uploadPath.'/'.$fld, $imgparams, $imgcount);
if ($imgcount == 0) {
// There is no img link, so finish the upload process.
$finish = 1;
} else {
// There is still one or more img missing.
$dialogBox .= get_lang('DownloadEnd');
}
}
}
// Set the HotPotatoes title in the document record's "comment" field
$title = @htmlspecialchars(
GetQuizName(
$filename,
$document_sys_path.$uploadPath.'/'.$fld.'/'
),
ENT_COMPAT,
api_get_system_encoding()
);
$sessionAnd = ' AND session_id IS NULL ';
if ($sessionId) {
$sessionAnd = " AND session_id = $sessionId ";
}
$path = $uploadPath.'/'.$fld.'/'.$filename;
// Find the proper record
$select = "SELECT iid FROM $dbTable
WHERE c_id = $course_id
$sessionAnd
AND path = '".Database::escape_string($path)."'";
$query = Database::query($select);
if (Database::num_rows($query)) {
$row = Database::fetch_array($query);
// Update the record with the 'comment' (HP title)
Database::update(
$dbTable,
['comment' => $title],
['iid = ?' => $row['iid']]
);
// Mark the addition of the HP quiz in the item_property table
api_item_property_update(
$_course,
TOOL_QUIZ,
$row['iid'],
'QuizAdded',
api_get_user_id()
);
}
} else {
$finish = 0;
}
}
}
if ($finish == 1) {
/** ok -> send to main exercises page */
header('Location: exercise.php?'.api_get_cidreq());
exit;
}
Display::display_header($nameTools, 'Exercise');
echo '<div class="actions">';
echo '<a href="exercise.php?show=test">'.
Display::return_icon('back.png', get_lang('BackToExercisesList'), '', ICON_SIZE_MEDIUM).
'</a>';
echo '</div>';
if ($finish == 2) {
// If we are in the img upload process.
$dialogBox .= get_lang('ImgNote_st').$imgcount.get_lang('ImgNote_en').'<br />';
foreach ($imgparams as $key => $string) {
$dialogBox .= $string.'; ';
}
}
if ($dialogBox) {
echo Display::return_message($dialogBox, 'normal', false);
}
$form->display();
}
// Display the footer.
Display::display_footer();

View File

@@ -0,0 +1,328 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Exercise list: This script shows the list of exercises for administrators and students.
*
* @package chamilo.exercise
*
* @author hubert.borderiou
*/
require_once __DIR__.'/../inc/global.inc.php';
// Setting the tabs
$this_section = SECTION_COURSES;
$htmlHeadXtra[] = api_get_jqgrid_js();
$_course = api_get_course_info();
// Access control
api_protect_course_script(true, false, true);
// including additional libraries
require_once 'hotpotatoes.lib.php';
// document path
$documentPath = api_get_path(SYS_COURSE_PATH).$_course['path']."/document";
/* Constants and variables */
$is_allowedToEdit = api_is_allowed_to_edit(null, true) || api_is_drh();
$is_tutor = api_is_allowed_to_edit(true);
$TBL_QUESTIONS = Database::get_course_table(TABLE_QUIZ_QUESTION);
$TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
$TBL_TRACK_HOTPOTATOES_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
$TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
$course_id = api_get_course_int_id();
$hotpotatoes_path = isset($_REQUEST['path']) ? Security::remove_XSS($_REQUEST['path']) : null;
$filter_user = isset($_REQUEST['filter_by_user']) ? intval($_REQUEST['filter_by_user']) : null;
if (empty($hotpotatoes_path)) {
api_not_allowed();
}
if (!$is_allowedToEdit) {
// api_not_allowed();
}
if (!empty($_REQUEST['path'])) {
$parameters['path'] = Security::remove_XSS($_REQUEST['path']);
}
$origin = isset($origin) ? $origin : null;
if (!empty($_REQUEST['export_report']) && $_REQUEST['export_report'] == '1') {
if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor() || api_is_session_general_coach()) {
$load_extra_data = false;
if (isset($_REQUEST['extra_data']) && $_REQUEST['extra_data'] == 1) {
$load_extra_data = true;
}
require_once 'hotpotatoes_exercise_result.class.php';
$export = new HotpotatoesExerciseResult();
$export->exportCompleteReportCSV($documentPath, $hotpotatoes_path);
exit;
} else {
api_not_allowed(true);
}
}
$actions = null;
if ($is_allowedToEdit && $origin != 'learnpath') {
// the form
if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor() || api_is_session_general_coach()) {
$actions .= '<a id="export_opener" href="'.api_get_self().'?export_report=1&path='.$hotpotatoes_path.' ">'.
Display::return_icon('save.png', get_lang('Export'), '', ICON_SIZE_MEDIUM).'</a>';
}
} else {
$actions .= '<a href="exercise.php">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), '', ICON_SIZE_MEDIUM).'</a>';
}
if ($is_allowedToEdit) {
$action = isset($_GET['action']) ? $_GET['action'] : null;
switch ($action) {
case 'delete':
$fileToDelete = isset($_GET['id']) ? $_GET['id'] : null;
deleteAttempt($fileToDelete);
Display::addFlash(Display::return_message(get_lang('ItemDeleted')));
$url = api_get_self().'?'.api_get_cidreq().'&path='.$hotpotatoes_path;
header("Location: $url");
exit;
break;
}
}
$nameTools = get_lang('Results');
if ($is_allowedToEdit || $is_tutor) {
$nameTools = get_lang('StudentScore');
$interbreadcrumb[] = ["url" => "exercise.php?".api_get_cidreq(), "name" => get_lang('Exercises')];
$objExerciseTmp = new Exercise();
} else {
$interbreadcrumb[] = ["url" => "exercise.php?".api_get_cidreq(), "name" => get_lang('Exercises')];
$objExerciseTmp = new Exercise();
}
Display::display_header($nameTools);
$actions = Display::div($actions, ['class' => 'actions']);
$extra = '<script>
$(function() {
$( "#dialog:ui-dialog" ).dialog( "destroy" );
$( "#dialog-confirm" ).dialog({
autoOpen: false,
show: "blind",
resizable: false,
height:300,
modal: true
});
$("#export_opener").click(function() {
var targetUrl = $(this).attr("href");
$( "#dialog-confirm" ).dialog({
width:400,
height:300,
buttons: {
"'.addslashes(get_lang('Download')).'": function() {
var export_format = $("input[name=export_format]:checked").val();
var extra_data = $("input[name=load_extra_data]:checked").val();
location.href = targetUrl+"&export_format="+export_format+"&extra_data="+extra_data;
$( this ).dialog( "close" );
}
}
});
$( "#dialog-confirm" ).dialog("open");
return false;
});
});
</script>';
$extra .= '<div id="dialog-confirm" title="'.get_lang("ConfirmYourChoice").'">';
$form = new FormValidator('report', 'post', null, null, ['class' => 'form-vertical']);
$form->addElement('radio', 'export_format', null, get_lang('ExportAsCSV'), 'csv', ['id' => 'export_format_csv_label']);
//$form->addElement('radio', 'export_format', null, get_lang('ExportAsXLS'), 'xls', array('id' => 'export_format_xls_label'));
//$form->addElement('checkbox', 'load_extra_data', null, get_lang('LoadExtraData'), '0', array('id' => 'export_format_xls_label'));
$form->setDefaults(['export_format' => 'csv']);
$extra .= $form->returnForm();
$extra .= '</div>';
if ($is_allowedToEdit) {
echo $extra;
}
echo $actions;
$url = api_get_path(WEB_AJAX_PATH).'model.ajax.php?a=get_hotpotatoes_exercise_results&path='.$hotpotatoes_path.'&filter_by_user='.$filter_user;
$action_links = '';
// Generating group list
$group_list = GroupManager::get_group_list();
$group_parameters = ['group_all:'.get_lang('All'), 'group_none:'.get_lang('None')];
foreach ($group_list as $group) {
$group_parameters[] = $group['id'].':'.$group['name'];
}
if (!empty($group_parameters)) {
$group_parameters = implode(';', $group_parameters);
}
if ($is_allowedToEdit || $is_tutor) {
// The order is important you need to check the the $column variable in the model.ajax.php file
$columns = [
get_lang('FirstName'),
get_lang('LastName'),
get_lang('LoginName'),
get_lang('Group'),
get_lang('StartDate'),
get_lang('Score'),
get_lang('Actions'),
];
// Column config
// @todo fix search firstname/lastname that doesn't work. rmove search for the moment
$column_model = [
['name' => 'firstname', 'index' => 'firstname', 'width' => '50', 'align' => 'left', 'search' => 'false'],
[
'name' => 'lastname',
'index' => 'lastname',
'width' => '50',
'align' => 'left',
'formatter' => 'action_formatter',
'search' => 'false',
],
[
'name' => 'login',
'hidden' => 'true',
'index' => 'username',
'width' => '40',
'align' => 'left',
'search' => 'false',
],
['name' => 'group_name', 'index' => 'group_id', 'width' => '40', 'align' => 'left', 'search' => 'false'],
['name' => 'exe_date', 'index' => 'exe_date', 'width' => '60', 'align' => 'left', 'search' => 'false'],
['name' => 'score', 'index' => 'exe_result', 'width' => '50', 'align' => 'left', 'search' => 'false'],
['name' => 'actions', 'index' => 'actions', 'width' => '60', 'align' => 'left', 'search' => 'false'],
];
$action_links = '
// add username as title in lastname filed - ref 4226
function action_formatter(cellvalue, options, rowObject) {
// rowObject is firstname,lastname,login,... get the third word
var loginx = "'.api_htmlentities(sprintf(get_lang("LoginX"), ":::"), ENT_QUOTES).'";
var tabLoginx = loginx.split(/:::/);
// tabLoginx[0] is before and tabLoginx[1] is after :::
// may be empty string but is defined
return "<span title=\""+tabLoginx[0]+rowObject[2]+tabLoginx[1]+"\">"+cellvalue+"</span>";
}';
} else {
//The order is important you need to check the the $column variable in the model.ajax.php file
$columns = [
get_lang('StartDate'),
get_lang('Score'),
get_lang('Actions'),
];
//Column config
// @todo fix search firstname/lastname that doesn't work. rmove search for the moment
$column_model = [
['name' => 'exe_date', 'index' => 'exe_date', 'width' => '60', 'align' => 'left', 'search' => 'false'],
['name' => 'score', 'index' => 'exe_result', 'width' => '50', 'align' => 'left', 'search' => 'false'],
['name' => 'actions', 'index' => 'actions', 'width' => '60', 'align' => 'left', 'search' => 'false'],
];
}
//Autowidth
$extra_params['autowidth'] = 'true';
//height auto
$extra_params['height'] = 'auto';
?>
<script>
function setSearchSelect(columnName) {
$("#results").jqGrid('setColProp', columnName,
{
searchoptions:{
dataInit:function(el){
$("option[value='1']",el).attr("selected", "selected");
setTimeout(function(){
$(el).trigger('change');
},1000);
}
}
});
}
function exportExcel() {
var mya=new Array();
mya=$("#results").getDataIDs(); // Get All IDs
var data=$("#results").getRowData(mya[0]); // Get First row to get the labels
var colNames=new Array();
var ii=0;
for (var i in data){colNames[ii++]=i;} // capture col names
var html="";
for(i=0;i<mya.length;i++) {
data=$("#results").getRowData(mya[i]); // get each row
for(j=0;j<colNames.length;j++) {
html=html+data[colNames[j]]+","; // output each column as tab delimited
}
html=html+"\n"; // output each row with end of line
}
html = html+"\n"; // end of line at the end
var form = $("#export_report_form");
$("#csvBuffer").attr('value', html);
form.target='_blank';
form.submit();
}
$(function() {
<?php
echo Display::grid_js(
'results',
$url,
$columns,
$column_model,
$extra_params,
[],
$action_links,
true
);
if ($is_allowedToEdit || $is_tutor) {
?>
//setSearchSelect("status");
//
//view:true, del:false, add:false, edit:false, excel:true}
$("#results").jqGrid('navGrid','#results_pager', {view:true, edit:false, add:false, del:false, excel:false},
{height:280, reloadAfterSubmit:false}, // view options
{height:280, reloadAfterSubmit:false}, // edit options
{height:280, reloadAfterSubmit:false}, // add options
{reloadAfterSubmit: false}, // del options
{width:500} // search options
);
//Adding search options
var options = {
'stringResult': true,
'autosearch' : true,
'searchOnEnter':false
}
jQuery("#results").jqGrid('filterToolbar',options);
var sgrid = $("#results")[0];
sgrid.triggerToolbar();
<?php
} ?>
});
</script>
<form id="export_report_form" method="post" action="hotpotatoes_exercise_report.php?<?php echo api_get_cidreq(); ?>">
<input type="hidden" name="csvBuffer" id="csvBuffer" value="" />
<input type="hidden" name="export_report" id="export_report" value="1" />
<input type="hidden" name="path" id="path" value="<?php echo $hotpotatoes_path; ?>" />
</form>
<?php
echo Display::grid_html('results');
Display::display_footer();

View File

@@ -0,0 +1,435 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class HotpotatoesExerciseResult
* Allows you to export exercises results in multiple presentation forms.
*
* @package chamilo.exercise
*/
class HotpotatoesExerciseResult
{
//stores the list of exercises
private $exercises_list = [];
//stores the results
private $results = [];
/**
* Gets the results of all students (or just one student if access is limited).
*
* @param string $document_path The document path (for HotPotatoes retrieval)
* @param int User ID. Optional. If no user ID is provided, we take all the results. Defauts to null
*
* @return bool
*/
public function getExercisesReporting($document_path, $hotpotato_name)
{
$return = [];
$TBL_USER = Database::get_main_table(TABLE_MAIN_USER);
$TBL_TRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
$course_id = api_get_course_int_id();
$user_id = null;
$session_id_and = ' AND te.session_id = '.api_get_session_id().' ';
$hotpotato_name = Database::escape_string($hotpotato_name);
if (!empty($exercise_id)) {
$session_id_and .= " AND exe_exo_id = $exercise_id ";
}
if (empty($user_id)) {
$sql = "SELECT firstname as userpart1, lastname as userpart2 ,
email,
tth.exe_name,
tth.exe_result,
tth.exe_weighting,
tth.exe_date
FROM $TBL_TRACK_HOTPOTATOES tth, $TBL_USER tu
WHERE tu.user_id=tth.exe_user_id AND
tth.c_id = $course_id AND
tth.exe_name = '$hotpotato_name'
ORDER BY tth.c_id ASC, tth.exe_date ASC";
} else {
// get only this user's results
$sql = "SELECT '', exe_name, exe_result , exe_weighting, exe_date
FROM $TBL_TRACK_HOTPOTATOES
WHERE
exe_user_id = $user_id AND
c_id = $course_id AND
tth.exe_name = '$hotpotato_name'
ORDER BY c_id ASC, exe_date ASC";
}
$results = [];
$resx = Database::query($sql);
while ($rowx = Database::fetch_array($resx, 'ASSOC')) {
$results[] = $rowx;
}
$hpresults = [];
$resx = Database::query($sql);
while ($rowx = Database::fetch_array($resx, 'ASSOC')) {
$hpresults[] = $rowx;
}
// Print the Result of Hotpotatoes Tests
if (is_array($hpresults)) {
for ($i = 0; $i < sizeof($hpresults); $i++) {
$return[$i] = [];
$title = GetQuizName($hpresults[$i]['exe_name'], $document_path);
if ($title == '') {
$title = basename($hpresults[$i]['exe_name']);
}
if (empty($user_id)) {
$return[$i]['email'] = $hpresults[$i]['email'];
$return[$i]['first_name'] = $hpresults[$i]['userpart1'];
$return[$i]['last_name'] = $hpresults[$i]['userpart2'];
}
$return[$i]['title'] = $title;
$return[$i]['exe_date'] = $hpresults[$i]['exe_date'];
$return[$i]['result'] = $hpresults[$i]['exe_result'];
$return[$i]['max'] = $hpresults[$i]['exe_weighting'];
}
}
$this->results = $return;
return true;
}
/**
* Exports the complete report as a CSV file.
*
* @param string $document_path Document path inside the document tool
* @param string $hotpotato_name
*
* @return bool False on error
*/
public function exportCompleteReportCSV($document_path = '', $hotpotato_name = '')
{
global $charset;
$this->getExercisesReporting($document_path, $hotpotato_name);
$filename = 'exercise_results_'.date('YmdGis').'.csv';
if (!empty($user_id)) {
$filename = 'exercise_results_user_'.$user_id.'_'.date('YmdGis').'.csv';
}
$data = '';
if (api_is_western_name_order()) {
if (!empty($this->results[0]['first_name'])) {
$data .= get_lang('FirstName').';';
}
if (!empty($this->results[0]['last_name'])) {
$data .= get_lang('LastName').';';
}
} else {
if (!empty($this->results[0]['last_name'])) {
$data .= get_lang('LastName').';';
}
if (!empty($this->results[0]['first_name'])) {
$data .= get_lang('FirstName').';';
}
}
$data .= get_lang('Email').';';
$data .= get_lang('Title').';';
$data .= get_lang('StartDate').';';
$data .= get_lang('Score').';';
$data .= get_lang('Total').';';
$data .= "\n";
// Results
foreach ($this->results as $row) {
if (api_is_western_name_order()) {
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['first_name']), ENT_QUOTES, $charset)).';';
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['last_name']), ENT_QUOTES, $charset)).';';
} else {
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['last_name']), ENT_QUOTES, $charset)).';';
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['first_name']), ENT_QUOTES, $charset)).';';
}
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['email']), ENT_QUOTES, $charset)).';';
$data .= str_replace("\r\n", ' ', api_html_entity_decode(strip_tags($row['title']), ENT_QUOTES, $charset)).';';
$data .= str_replace("\r\n", ' ', $row['exe_date']).';';
$data .= str_replace("\r\n", ' ', $row['result']).';';
$data .= str_replace("\r\n", ' ', $row['max']).';';
$data .= "\n";
}
//output the results
$len = strlen($data);
header('Content-type: application/octet-stream');
header('Content-Type: application/force-download');
header('Content-length: '.$len);
if (preg_match("/MSIE 5.5/", $_SERVER['HTTP_USER_AGENT'])) {
header('Content-Disposition: filename= '.$filename);
} else {
header('Content-Disposition: attachment; filename= '.$filename);
}
if (strpos($_SERVER['HTTP_USER_AGENT'], 'MSIE')) {
header('Pragma: ');
header('Cache-Control: ');
header('Cache-Control: public'); // IE cannot download from sessions without a cache
}
header('Content-Description: '.$filename);
header('Content-transfer-encoding: binary');
// @todo add this utf-8 header for all csv files
echo "\xEF\xBB\xBF"; // force utf-8 header of csv file
echo $data;
return true;
}
/**
* Exports the complete report as an XLS file.
*
* @param string $document_path
* @param null $user_id
* @param bool $export_user_fields
* @param int $export_filter
* @param int $exercise_id
* @param null $hotpotato_name
*
* @return bool
*/
public function exportCompleteReportXLS(
$document_path = '',
$user_id = null,
$export_user_fields = false,
$export_filter = 0,
$exercise_id = 0,
$hotpotato_name = null
) {
global $charset;
$this->getExercisesReporting(
$document_path,
$user_id,
$export_filter,
$exercise_id,
$hotpotato_name
);
$filename = 'exercise_results_'.api_get_local_time().'.xls';
if (!empty($user_id)) {
$filename = 'exercise_results_user_'.$user_id.'_'.api_get_local_time().'.xls';
}
$spreadsheet = new PHPExcel();
$spreadsheet->setActiveSheetIndex(0);
$worksheet = $spreadsheet->getActiveSheet();
$line = 0;
$column = 0; //skip the first column (row titles)
// check if exists column 'user'
$with_column_user = false;
foreach ($this->results as $result) {
if (!empty($result['last_name']) && !empty($result['first_name'])) {
$with_column_user = true;
break;
}
}
if ($with_column_user) {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('Email')
);
$column++;
if (api_is_western_name_order()) {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('FirstName')
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('LastName')
);
$column++;
} else {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('LastName')
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('FirstName')
);
$column++;
}
}
if ($export_user_fields) {
//show user fields section with a big th colspan that spans over all fields
$extra_user_fields = UserManager::get_extra_fields(
0,
1000,
5,
'ASC',
false,
1
);
//show the fields names for user fields
foreach ($extra_user_fields as $field) {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($field[3]),
ENT_QUOTES,
$charset
)
);
$column++;
}
}
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('Title')
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('StartDate')
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('EndDate')
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('Duration').' ('.get_lang('MinMinutes').')'
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('Score')
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('Total')
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
get_lang('Status')
);
$line++;
foreach ($this->results as $row) {
$column = 0;
if ($with_column_user) {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['email']),
ENT_QUOTES,
$charset
)
);
$column++;
if (api_is_western_name_order()) {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['first_name']),
ENT_QUOTES,
$charset
)
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['last_name']),
ENT_QUOTES,
$charset
)
);
$column++;
} else {
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['last_name']),
ENT_QUOTES,
$charset
)
);
$column++;
$worksheet->setCellValueByColumnAndRow(
$column,
$line,
api_html_entity_decode(
strip_tags($row['first_name']),
ENT_QUOTES,
$charset
)
);
$column++;
}
}
if ($export_user_fields) {
//show user fields data, if any, for this user
$user_fields_values = UserManager::get_extra_user_data(
$row['user_id'],
false,
false,
false,
true
);
foreach ($user_fields_values as $value) {
$worksheet->setCellValueByColumnAndRow($column, $line, api_html_entity_decode(strip_tags($value), ENT_QUOTES, $charset));
$column++;
}
}
$worksheet->setCellValueByColumnAndRow($column, $line, api_html_entity_decode(strip_tags($row['title']), ENT_QUOTES, $charset));
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['start_date']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['end_date']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['duration']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['result']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['max']);
$column++;
$worksheet->setCellValueByColumnAndRow($column, $line, $row['status']);
$line++;
}
$file = api_get_path(SYS_ARCHIVE_PATH).api_replace_dangerous_char($filename);
$writer = new PHPExcel_Writer_Excel2007($spreadsheet);
$writer->save($file);
DocumentManager::file_send_for_download($file, true, $filename);
return true;
}
}

View File

@@ -0,0 +1,115 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class HotSpot.
*
* This class allows to instantiate an object of
* type HotSpot (MULTIPLE CHOICE, UNIQUE ANSWER)
* extending the class question
*
* @author Eric Marguin
*/
class HotSpot extends Question
{
public $typePicture = 'hotspot.png';
public $explanationLangVar = 'HotSpot';
/**
* HotSpot constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = HOT_SPOT;
}
public function display()
{
}
/**
* {@inheritdoc}
*/
public function createForm(&$form, $exercise)
{
parent::createForm($form, $exercise);
if (!isset($_GET['editQuestion'])) {
$icon = Display::return_icon(
'hotspot.png',
null,
null,
ICON_SIZE_BIG,
false,
true
);
$form->addElement(
'file',
'imageUpload',
[
'<img src="'.$icon.'" />',
get_lang('UploadJpgPicture'),
]
);
// setting the save button here and not in the question class.php
// Saving a question
$form->addButtonSave(get_lang('GoToQuestion'), 'submitQuestion');
$form->addRule(
'imageUpload',
get_lang('OnlyImagesAllowed'),
'filetype',
['jpg', 'jpeg', 'png', 'gif']
);
$form->addRule('imageUpload', get_lang('NoImage'), 'uploadedfile');
} else {
// setting the save button here and not in the question class.php
// Editing a question
$form->addButtonUpdate(get_lang('ModifyQuestion'), 'submitQuestion');
}
}
/**
* {@inheritdoc}
*/
public function processCreation($form, $exercise)
{
$fileInfo = $form->getSubmitValue('imageUpload');
parent::processCreation($form, $exercise);
if (!empty($fileInfo['tmp_name'])) {
$result = $this->uploadPicture($fileInfo['tmp_name']);
if ($result) {
$this->save($exercise);
return true;
}
}
return false;
}
public function createAnswersForm($form)
{
// nothing
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
// nothing
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
return parent::return_header($exercise, $counter, $score)
.'<table><tr><td><table class="table">';
}
}

View File

@@ -0,0 +1,28 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Hotspot languae conversion.
*
* @package chamilo.exercise
*/
/**
* Code.
*/
session_cache_limiter('none');
$language_file = 'hotspot';
require_once __DIR__.'/../inc/global.inc.php';
header('Content-Type: text/html; charset=UTF-8');
$file = file(api_get_path(SYS_LANG_PATH).'english/hotspot.inc.php');
foreach ($file as &$value) {
$variable = explode('=', $value, 2);
if (count($variable) > 1) {
$variable = substr(trim($variable[0]), 1);
$variable = '&'.$variable.'='.api_utf8_encode(get_lang($variable)).' ';
echo $variable;
}
}

View File

@@ -0,0 +1,181 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* This file generates the ActionScript variables code used by the HotSpot .swf.
*
* @package chamilo.exercise
*
* @author Toon Keppens
*
* @version $Id: admin.php 10680 2007-01-11 21:26:23Z pcool $
*/
session_cache_limiter('none');
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
$_course = api_get_course_info();
require api_get_path(LIBRARY_PATH).'geometry.lib.php';
// set vars
$questionId = intval($_GET['modifyAnswers']);
$exerciseId = isset($_GET['exe_id']) ? intval($_GET['exe_id']) : 0;
$objQuestion = Question::read($questionId);
$answer_type = $objQuestion->selectType(); //very important
$TBL_ANSWERS = Database::get_course_table(TABLE_QUIZ_ANSWER);
$documentPath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
$picturePath = $documentPath.'/images';
$pictureName = $objQuestion->getPictureFilename();
$pictureSize = getimagesize($picturePath.'/'.$pictureName);
$pictureWidth = $pictureSize[0];
$pictureHeight = $pictureSize[1];
$course_id = api_get_course_int_id();
// Query db for answers
if ($answer_type == HOT_SPOT_DELINEATION) {
$sql = "SELECT iid, answer, hotspot_coordinates, hotspot_type, ponderation
FROM $TBL_ANSWERS
WHERE
c_id = $course_id AND
question_id = $questionId AND
hotspot_type = 'delineation'
ORDER BY iid";
} else {
$sql = "SELECT iid, answer, hotspot_coordinates, hotspot_type, ponderation
FROM $TBL_ANSWERS
WHERE c_id = $course_id AND question_id = $questionId
ORDER BY position";
}
$result = Database::query($sql);
$data = [];
$data['type'] = 'user';
$data['lang'] = [
'Square' => get_lang('Square'),
'Ellipse' => get_lang('Ellipse'),
'Polygon' => get_lang('Polygon'),
'HotspotStatus1' => get_lang('HotspotStatus1'),
'HotspotStatus2Polygon' => get_lang('HotspotStatus2Polygon'),
'HotspotStatus2Other' => get_lang('HotspotStatus2Other'),
'HotspotStatus3' => get_lang('HotspotStatus3'),
'HotspotShowUserPoints' => get_lang('HotspotShowUserPoints'),
'ShowHotspots' => get_lang('ShowHotspots'),
'Triesleft' => get_lang('Triesleft'),
'HotspotExerciseFinished' => get_lang('HotspotExerciseFinished'),
'NextAnswer' => get_lang('NextAnswer'),
'Delineation' => get_lang('Delineation'),
'CloseDelineation' => get_lang('CloseDelineation'),
'Oar' => get_lang('Oar'),
'ClosePolygon' => get_lang('ClosePolygon'),
'DelineationStatus1' => get_lang('DelineationStatus1'),
];
$data['image'] = $objQuestion->selectPicturePath();
$data['image_width'] = $pictureWidth;
$data['image_height'] = $pictureHeight;
$data['courseCode'] = $_course['path'];
$data['hotspots'] = [];
$data['answers'] = [];
$nmbrTries = 0;
while ($hotspot = Database::fetch_assoc($result)) {
$hotSpot = [];
$hotSpot['iid'] = $hotspot['iid'];
$hotSpot['answer'] = $hotspot['answer'];
// Square or rectancle
if ($hotspot['hotspot_type'] == 'square') {
$hotSpot['type'] = 'square';
}
// Circle or ovale
if ($hotspot['hotspot_type'] == 'circle') {
$hotSpot['type'] = 'circle';
}
// Polygon
if ($hotspot['hotspot_type'] == 'poly') {
$hotSpot['type'] = 'poly';
}
// Delineation
if ($hotspot['hotspot_type'] == 'delineation') {
$hotSpot['type'] = 'delineation';
}
// No error
if ($hotspot['hotspot_type'] == 'noerror') {
$hotSpot['type'] = 'noerror';
}
// This is a good answer, count + 1 for nmbr of clicks
if ($hotspot['hotspot_type'] > 0) {
$nmbrTries++;
}
$hotSpot['coord'] = $hotspot['hotspot_coordinates'];
$data['hotspots'][] = $hotSpot;
}
$attemptInfo = Database::select(
'exe_id',
Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES),
[
'where' => [
'exe_exo_id = ? AND c_id = ? AND exe_user_id = ? AND status = ?' => [
(int) $exerciseId,
$course_id,
api_get_user_id(),
'incomplete',
],
],
'order' => 'exe_id DESC',
'limit' => 1,
],
'first'
);
if (empty($attemptInfo)) {
exit(0);
}
$attemptList = Event::getAllExerciseEventByExeId($attemptInfo['exe_id']);
if (!empty($attemptList)) {
if (isset($attemptList[$questionId])) {
$questionAttempt = $attemptList[$questionId][0];
if (!empty($questionAttempt['answer'])) {
$coordinates = explode('|', $questionAttempt['answer']);
foreach ($coordinates as $coordinate) {
$data['answers'][] = Geometry::decodePoint($coordinate);
}
}
}
}
$data['nmbrTries'] = $nmbrTries;
$data['done'] = 'done';
if (Session::has("hotspot_ordered$questionId")) {
$tempHotspots = [];
$hotspotOrdered = Session::read("hotspot_ordered$questionId");
foreach ($hotspotOrdered as $hotspotOrder) {
foreach ($data['hotspots'] as $hotspot) {
if ($hotspot['iid'] != $hotspotOrder) {
continue;
}
$tempHotspots[] = $hotspot;
}
}
$data['hotspots'] = $tempHotspots;
Session::erase("hotspot_ordered$questionId");
}
header('Content-Type: application/json');
echo json_encode($data);

View File

@@ -0,0 +1,116 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* This file generates the ActionScript variables code used by the HotSpot .swf.
*
* @package chamilo.exercise
*
* @author Toon Keppens
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(false);
$isAllowedToEdit = api_is_allowed_to_edit(null, true);
if (!$isAllowedToEdit) {
api_not_allowed(true);
exit;
}
$_course = api_get_course_info();
$questionId = isset($_GET['modifyAnswers']) ? (int) $_GET['modifyAnswers'] : 0;
$objQuestion = Question::read($questionId);
$documentPath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
$picturePath = $documentPath.'/images';
$pictureName = $objQuestion->getPictureFilename();
$pictureSize = getimagesize($picturePath.'/'.$pictureName);
$pictureWidth = $pictureSize[0];
$pictureHeight = $pictureSize[1];
$data = [];
$data['type'] = 'admin';
$data['lang'] = [
'Square' => get_lang('Square'),
'Ellipse' => get_lang('Ellipse'),
'Polygon' => get_lang('Polygon'),
'HotspotStatus1' => get_lang('HotspotStatus1'),
'HotspotStatus2Polygon' => get_lang('HotspotStatus2Polygon'),
'HotspotStatus2Other' => get_lang('HotspotStatus2Other'),
'HotspotStatus3' => get_lang('HotspotStatus3'),
'HotspotShowUserPoints' => get_lang('HotspotShowUserPoints'),
'ShowHotspots' => get_lang('ShowHotspots'),
'Triesleft' => get_lang('Triesleft'),
'HotspotExerciseFinished' => get_lang('HotspotExerciseFinished'),
'NextAnswer' => get_lang('NextAnswer'),
'Delineation' => get_lang('Delineation'),
'CloseDelineation' => get_lang('CloseDelineation'),
'Oar' => get_lang('Oar'),
'ClosePolygon' => get_lang('ClosePolygon'),
'DelineationStatus1' => get_lang('DelineationStatus1'),
];
$data['image'] = $objQuestion->selectPicturePath();
$data['image_width'] = $pictureWidth;
$data['image_height'] = $pictureHeight;
$data['courseCode'] = $_course['path'];
$data['hotspots'] = [];
$i = 0;
$nmbrTries = 0;
$answer_type = $objQuestion->type;
$answers = Session::read('tmp_answers');
$nbrAnswers = count($answers['answer']);
for ($i = 1; $i <= $nbrAnswers; $i++) {
$hotSpot = [];
$hotSpot['iid'] = null;
$hotSpot['answer'] = $answers['answer'][$i];
if ($answer_type == HOT_SPOT_DELINEATION) {
if ($i == 1) {
$hotSpot['type'] = 'delineation';
} else {
$hotSpot['type'] = 'oar';
}
} else {
// Square or rectancle
if ($answers['hotspot_type'][$i] == 'square') {
$hotSpot['type'] = 'square';
}
// Circle or ovale
if ($answers['hotspot_type'][$i] == 'circle') {
$hotSpot['type'] = 'circle';
}
// Polygon
if ($answers['hotspot_type'][$i] == 'poly') {
$hotSpot['type'] = 'poly';
}
/*// Delineation
if ($answers['hotspot_type'][$i] == 'delineation')
{
$output .= "&hotspot_".$i."_type=delineation";
}*/
}
// This is a good answer, count + 1 for nmbr of clicks
if ($answers['weighting'][$i] > 0) {
$nmbrTries++;
}
$hotSpot['coord'] = $answers['hotspot_coordinates'][$i];
$data['hotspots'][] = $hotSpot;
}
// Output
$data['nmbrTries'] = $nmbrTries;
$data['done'] = 'done';
header('Content-Type: application/json');
echo json_encode($data);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,238 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEHotspot;
use Chamilo\CourseBundle\Entity\CQuizAnswer;
/**
* This file generates a json answer to the question preview.
*
* @author Toon Keppens, Julio Montoya adding hotspot "medical" support
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script();
$_course = api_get_course_info();
$questionId = isset($_GET['modifyAnswers']) ? (int) $_GET['modifyAnswers'] : 0;
$exerciseId = isset($_GET['exerciseId']) ? (int) $_GET['exerciseId'] : 0;
$exeId = isset($_GET['exeId']) ? (int) $_GET['exeId'] : 0;
$userId = api_get_user_id();
$courseId = api_get_course_int_id();
$objExercise = new Exercise($courseId);
$debug = false;
if ($debug) {
error_log("Call to hotspot_answers.as.php");
}
$trackExerciseInfo = $objExercise->get_stat_track_exercise_info_by_exe_id($exeId);
// Check if student has access to the hotspot answers
if (!api_is_allowed_to_edit(null, true)) {
if (empty($exeId)) {
api_not_allowed();
}
if (empty($trackExerciseInfo)) {
api_not_allowed();
}
// Different exercise
if ($exerciseId != $trackExerciseInfo['exe_exo_id']) {
api_not_allowed();
}
// Different user
if ($trackExerciseInfo['exe_user_id'] != $userId) {
api_not_allowed();
}
}
$objQuestion = Question::read($questionId, $objExercise->course);
$objExercise->read($exerciseId);
if (empty($objQuestion) || empty($objExercise)) {
exit;
}
$em = Database::getManager();
$documentPath = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
$picturePath = $documentPath.'/images';
$pictureName = $objQuestion->getPictureFilename();
$pictureSize = getimagesize($picturePath.'/'.$pictureName);
$pictureWidth = $pictureSize[0];
$pictureHeight = $pictureSize[1];
$data = [];
$data['type'] = 'solution';
$data['lang'] = [
'Square' => get_lang('Square'),
'Ellipse' => get_lang('Ellipse'),
'Polygon' => get_lang('Polygon'),
'HotspotStatus1' => get_lang('HotspotStatus1'),
'HotspotStatus2Polygon' => get_lang('HotspotStatus2Polygon'),
'HotspotStatus2Other' => get_lang('HotspotStatus2Other'),
'HotspotStatus3' => get_lang('HotspotStatus3'),
'HotspotShowUserPoints' => get_lang('HotspotShowUserPoints'),
'ShowHotspots' => get_lang('ShowHotspots'),
'Triesleft' => get_lang('Triesleft'),
'HotspotExerciseFinished' => get_lang('HotspotExerciseFinished'),
'NextAnswer' => get_lang('NextAnswer'),
'Delineation' => get_lang('Delineation'),
'CloseDelineation' => get_lang('CloseDelineation'),
'Oar' => get_lang('Oar'),
'ClosePolygon' => get_lang('ClosePolygon'),
'DelineationStatus1' => get_lang('DelineationStatus1'),
];
$data['image'] = $objQuestion->selectPicturePath();
$data['image_width'] = $pictureWidth;
$data['image_height'] = $pictureHeight;
$data['courseCode'] = $_course['path'];
$data['hotspots'] = [];
$resultDisable = $objExercise->selectResultsDisabled();
$showTotalScoreAndUserChoicesInLastAttempt = true;
if (in_array(
$resultDisable, [
RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
]
)
) {
$showOnlyScore = true;
$showResults = true;
$lpId = isset($trackExerciseInfo['orig_lp_id']) ? $trackExerciseInfo['orig_lp_id'] : 0;
$lpItemId = isset($trackExerciseInfo['orig_lp_item_id']) ? $trackExerciseInfo['orig_lp_item_id'] : 0;
if ($objExercise->attempts > 0) {
$attempts = Event::getExerciseResultsByUser(
api_get_user_id(),
$objExercise->iid,
$courseId,
api_get_session_id(),
$lpId,
$lpItemId,
'desc'
);
$numberAttempts = count($attempts);
$showTotalScoreAndUserChoicesInLastAttempt = false;
if ($numberAttempts >= $objExercise->attempts) {
$showResults = true;
$showOnlyScore = false;
$showTotalScoreAndUserChoicesInLastAttempt = true;
}
}
}
$hideExpectedAnswer = false;
if ($objExercise->getFeedbackType() == 0 &&
$resultDisable == RESULT_DISABLE_SHOW_SCORE_ONLY
) {
$hideExpectedAnswer = true;
}
if (in_array(
$resultDisable, [
RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
]
)
) {
$hideExpectedAnswer = $showTotalScoreAndUserChoicesInLastAttempt ? false : true;
}
if (in_array(
$resultDisable, [
RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
]
)
) {
$hideExpectedAnswer = false;
}
$hotSpotWithAnswer = [];
$data['answers'] = [];
$rs = $em
->getRepository('ChamiloCoreBundle:TrackEHotspot')
->findBy(
[
'hotspotQuestionId' => $questionId,
'cId' => $courseId,
'hotspotExeId' => $exeId,
],
['hotspotAnswerId' => 'ASC']
);
/** @var TrackEHotspot $row */
foreach ($rs as $row) {
$data['answers'][] = $row->getHotspotCoordinate();
if ($row->getHotspotCorrect()) {
$hotSpotWithAnswer[] = $row->getHotspotAnswerId();
}
}
if (!$hideExpectedAnswer) {
$qb = $em->createQueryBuilder();
$qb
->select('a')
->from('ChamiloCourseBundle:CQuizAnswer', 'a');
if ($objQuestion->selectType() == HOT_SPOT_DELINEATION) {
$qb
->where($qb->expr()->eq('a.questionId', $questionId))
->andWhere("a.hotspotType != 'noerror'")
->orderBy('a.iid', 'ASC');
} else {
$qb
->where($qb->expr()->eq('a.questionId', $questionId))
->orderBy('a.position', 'ASC');
}
$result = $qb->getQuery()->getResult();
/** @var CQuizAnswer $hotSpotAnswer */
foreach ($result as $hotSpotAnswer) {
if (RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK == $resultDisable) {
if (false === $showTotalScoreAndUserChoicesInLastAttempt) {
if (!in_array($hotSpotAnswer->getId(), $hotSpotWithAnswer)) {
continue;
}
}
}
$hotSpot = [];
$hotSpot['id'] = $hotSpotAnswer->getId();
$hotSpot['answer'] = $hotSpotAnswer->getAnswer();
switch ($hotSpotAnswer->getHotspotType()) {
case 'square':
$hotSpot['type'] = 'square';
break;
case 'circle':
$hotSpot['type'] = 'circle';
break;
case 'poly':
$hotSpot['type'] = 'poly';
break;
case 'delineation':
$hotSpot['type'] = 'delineation';
break;
case 'oar':
$hotSpot['type'] = 'delineation';
break;
}
$hotSpot['coord'] = $hotSpotAnswer->getHotspotCoordinates();
$data['hotspots'][] = $hotSpot;
}
}
$data['done'] = 'done';
header('Content-Type: application/json');
echo json_encode($data);
if ($debug) {
error_log("---------- End call to hotspot_answers.as.php------------");
}

View File

@@ -0,0 +1,55 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Hotspot language conversion.
*
* @package chamilo.exercise
*
* @author
*
* @deprecated ?
*
* @version $Id: admin.php 10680 2007-01-11 21:26:23Z pcool $
*/
require_once __DIR__.'/../inc/global.inc.php';
$hotspot_lang_file = api_get_path(SYS_LANG_PATH);
if (isset($_GET['lang'])) {
//$search = array('../','\\0','\\');
$lang = urldecode($_GET['lang']);
if (preg_match('/^[a-zA-Z0-9\._-]+$/', $lang)) {
//$lang = str_replace($search,$replace,urldecode($_GET['lang']));
if (file_exists($hotspot_lang_file.$lang.'/hotspot.inc.php')) {
$hotspot_lang_file .= $lang.'/hotspot.inc.php';
} else {
$hotspot_lang_file .= 'english/hotspot.inc.php';
}
} else {
$hotspot_lang_file .= 'english/hotspot.inc.php';
}
} else {
$hotspot_lang_file .= 'english/hotspot.inc.php';
}
$file = file($hotspot_lang_file);
$temp = [];
foreach ($file as $value) {
$explode = explode('=', $value);
if (count($explode) > 1) {
$explode[0] = trim($explode[0]);
$explode[0] = '&'.substr($explode[0], 1, strlen($explode[0]));
$explode[1] = trim($explode[1]);
$explode[1] = substr($explode[1], 0, strlen($explode[1]) - 1);
$explode[1] = str_replace('"', '', $explode[1]);
$temp[] = $explode[0].'='.$explode[1];
}
}
foreach ($temp as $value) {
echo $value.' ';
}

View File

@@ -0,0 +1,46 @@
<?php
/* For licensing terms, see /license.txt */
/**
* @package chamilo.exercise
*
* @author Toon Keppens
*
* @version $Id: admin.php 10680 2007-01-11 21:26:23Z pcool $
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
$TBL_ANSWER = Database::get_course_table(TABLE_QUIZ_ANSWER);
$questionId = intval($_GET['questionId']);
$answerId = intval($_GET['answerId']);
if ($_GET['type'] == "square" || $_GET['type'] == "circle") {
$hotspot_type = $_GET['type'];
$hotspot_coordinates = $_GET['x'].";".$_GET['y']."|".$_GET['width']."|".$_GET['height'];
}
if ($_GET['type'] == "poly" || $_GET['type'] == "delineation" || $_GET['type'] == "oar") {
$hotspot_type = $_GET['type'];
$tmp_coord = explode(",", $_GET['co']);
$i = 0;
$hotspot_coordinates = "";
foreach ($tmp_coord as $coord) {
if ($i % 2 == 0) {
$delimiter = ";";
} else {
$delimiter = "|";
}
$hotspot_coordinates .= $coord.$delimiter;
$i++;
}
$hotspot_coordinates = api_substr($hotspot_coordinates, 0, -2);
}
$sql = "UPDATE $TBL_ANSWER SET
hotspot_coordinates = '".Database::escape_string($hotspot_coordinates)."',
hotspot_type = '".Database::escape_string($hotspot_type)."'
WHERE
iid = ".intval($answerId)."
LIMIT 1 ";
$result = Database::query($sql);
echo "done=done";

View File

@@ -0,0 +1,60 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* This file saves every click in the hotspot tool into track_e_hotspots.
*
* @package chamilo.exercise
*
* @author Toon Keppens
*
* @version $Id: admin.php 10680 2007-01-11 21:26:23Z pcool $
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
$courseCode = $_GET['coursecode'];
$questionId = $_GET['questionId'];
$coordinates = $_GET['coord'];
$objExercise = Session::read('objExercise');
$exerciseId = $objExercise->selectId();
// Save clicking order
$answerOrderId = count($_SESSION['exerciseResult'][$questionId]['ids']) + 1;
if ($_GET['answerId'] == "0") {
// click is NOT on a hotspot
$hit = 0;
$answerId = null;
} else {
// user clicked ON a hotspot
$hit = 1;
$answerId = api_substr($_GET['answerId'], 22, 2);
// Save into session
$_SESSION['exerciseResult'][$questionId][$answerId] = $hit;
}
//round-up the coordinates
$coords = explode('/', $coordinates);
$coordinates = '';
foreach ($coords as $coord) {
list($x, $y) = explode(';', $coord);
$coordinates .= round($x).';'.round($y).'/';
}
$coordinates = substr($coordinates, 0, -1);
$TBL_TRACK_E_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
// Save into db
$params = [
'user_id' => api_get_user_id(),
'course_id' => $courseCode,
'quiz_id' => $exerciseId,
'question_id' => $questionId,
'answer_id' => $answerId,
'correct' => $hit,
'coordinate' => $coordinates,
];
// Save insert id into session if users changes answer.
$insert_id = Database::insert($TBL_TRACK_E_HOTSPOT, $params);
$_SESSION['exerciseResult'][$questionId]['ids'][$answerOrderId] = $insert_id;

View File

@@ -0,0 +1,56 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* This file saves every click in the hotspot tool into track_e_hotspots.
*
* @package chamilo.exercise
*
* @author Toon Keppens
*
* @version $Id: admin.php 10680 2007-01-11 21:26:23Z pcool $
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
$courseCode = $_GET['coursecode'];
$questionId = $_GET['questionId'];
$coordinates = $_GET['coord'];
$objExercise = Session::read('objExercise');
$hotspotId = $_GET['hotspotId'];
$exerciseId = $objExercise->selectId();
if ($_GET['answerId'] == "0") { // click is NOT on a hotspot
$hit = 0;
$answerId = $hotspotId;
// remove from session
unset($_SESSION['exerciseResult'][$questionId][$answerId]);
} else { // user clicked ON a hotspot
$hit = 1;
$answerId = $hotspotId;
// Save into session
$_SESSION['exerciseResult'][$questionId][$answerId] = $hit;
}
//round-up the coordinates
$coords = explode('/', $coordinates);
$coordinates = '';
foreach ($coords as $coord) {
list($x, $y) = explode(';', $coord);
$coordinates .= round($x).';'.round($y).'/';
}
$coordinates = substr($coordinates, 0, -1);
$TBL_TRACK_E_HOTSPOT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTSPOT);
// update db
$update_id = $_SESSION['exerciseResult'][$questionId]['ids'][$answerId];
$sql = "UPDATE $TBL_TRACK_E_HOTSPOT
SET coordinate = '".Database::escape_string($coordinates)."'
WHERE id = ".intval($update_id)."
LIMIT 1";
$result = Database::query($sql);

7
main/exercise/index.html Normal file
View File

@@ -0,0 +1,7 @@
<html>
<head>
<meta http-equiv="refresh" content="0; url=exercise.php">
</head>
<body>
</body>
</html>

View File

@@ -0,0 +1,119 @@
<?php
/* See license terms in /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_COURSES;
$exercise_id = isset($_GET['exerciseId']) && !empty($_GET['exerciseId']) ? (int) ($_GET['exerciseId']) : 0;
api_protect_course_script(true);
if (!api_is_allowed_to_edit()) {
api_not_allowed();
}
$objExercise = new Exercise();
$result = $objExercise->read($exercise_id);
if (!$result) {
api_not_allowed(true);
}
$interbreadcrumb[] = [
"url" => "exercise.php?".api_get_cidreq(),
"name" => get_lang('Exercises'),
];
$interbreadcrumb[] = [
'url' => "admin.php?exerciseId=$exercise_id&".api_get_cidreq(),
'name' => $objExercise->selectTitle(true),
];
//Add the JS needed to use the jqgrid
$htmlHeadXtra[] = api_get_jqgrid_js();
// The header.
Display::display_header(get_lang('StudentsWhoAreTakingTheExerciseRightNow'));
//jqgrid will use this URL to do the selects
$minutes = 60;
$url = api_get_path(WEB_AJAX_PATH).
'exercise.ajax.php?'.api_get_cidreq().'&a=get_live_stats&exercise_id='.$objExercise->iid.'&minutes='.$minutes;
//The order is important you need to check the the $column variable in the model.ajax.php file
$columns = [
get_lang('FirstName'),
get_lang('LastName'),
get_lang('Time'),
get_lang('QuestionsAlreadyAnswered'),
get_lang('Score'),
];
//Column config
$column_model = [
[
'name' => 'firstname',
'index' => 'firstname',
'width' => '100',
'align' => 'left',
],
[
'name' => 'lastname',
'index' => 'lastname',
'width' => '100',
'align' => 'left',
],
[
'name' => 'start_date',
'index' => 'start_date',
'width' => '100',
'align' => 'left',
],
[
'name' => 'question',
'index' => 'count_questions',
'width' => '60',
'align' => 'left',
'sortable' => 'false',
],
[
'name' => 'score',
'index' => 'score',
'width' => '50',
'align' => 'left',
'sortable' => 'false',
],
];
$extra_params['autowidth'] = 'true';
$extra_params['height'] = 'auto';
?>
<script>
function refreshGrid() {
var grid = $("#live_stats");
grid.trigger("reloadGrid");
t = setTimeout("refreshGrid()", 10000);
}
$(function() {
<?php
echo Display::grid_js(
'live_stats',
$url,
$columns,
$column_model,
$extra_params,
[],
null,
true
);
?>
refreshGrid();
});
</script>
<?php
$actions = '<a href="exercise_report.php?exerciseId='.$exercise_id.'&'.api_get_cidreq().'">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), '', ICON_SIZE_MEDIUM).'</a>';
echo $actions = Display::div($actions, ['class' => 'actions']);
echo Display::grid_html('live_stats');
Display::display_footer();

View File

@@ -0,0 +1,358 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class Matching
* Matching questions type class.
*
* This class allows to instantiate an object of
* type MULTIPLE_ANSWER (MULTIPLE CHOICE, MULTIPLE ANSWER)
* extending the class question
*
* @author Eric Marguin
*/
class Matching extends Question
{
public $typePicture = 'matching.png';
public $explanationLangVar = 'Matching';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = MATCHING;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$defaults = [];
$nb_matches = $nb_options = 2;
$matches = [];
$answer = null;
$counter = 1;
if (!empty($this->iid)) {
$answer = new Answer($this->iid);
$answer->read();
if ($answer->nbrAnswers > 0) {
for ($i = 1; $i <= $answer->nbrAnswers; $i++) {
$correct = $answer->isCorrect($i);
if (empty($correct)) {
$matches[$answer->selectAutoId($i)] = chr(64 + $counter);
$counter++;
}
}
}
}
if ($form->isSubmitted()) {
$nb_matches = $form->getSubmitValue('nb_matches');
$nb_options = $form->getSubmitValue('nb_options');
if (isset($_POST['lessOptions'])) {
$nb_matches--;
$nb_options--;
}
if (isset($_POST['moreOptions'])) {
$nb_matches++;
$nb_options++;
}
} elseif (!empty($this->iid)) {
if ($answer->nbrAnswers > 0) {
$nb_matches = $nb_options = 0;
for ($i = 1; $i <= $answer->nbrAnswers; $i++) {
if ($answer->isCorrect($i)) {
$nb_matches++;
$defaults['answer['.$nb_matches.']'] = $answer->selectAnswer($i);
$defaults['weighting['.$nb_matches.']'] = float_format($answer->selectWeighting($i));
$defaults['matches['.$nb_matches.']'] = $answer->correct[$i];
} else {
$nb_options++;
$defaults['option['.$nb_options.']'] = $answer->selectAnswer($i);
}
}
}
} else {
$defaults['answer[1]'] = get_lang('DefaultMakeCorrespond1');
$defaults['answer[2]'] = get_lang('DefaultMakeCorrespond2');
$defaults['matches[2]'] = '2';
$defaults['option[1]'] = get_lang('DefaultMatchingOptA');
$defaults['option[2]'] = get_lang('DefaultMatchingOptB');
}
if (empty($matches)) {
for ($i = 1; $i <= $nb_options; $i++) {
// fill the array with A, B, C.....
$matches[$i] = chr(64 + $i);
}
} else {
for ($i = $counter; $i <= $nb_options; $i++) {
// fill the array with A, B, C.....
$matches[$i] = chr(64 + $i);
}
}
$form->addElement('hidden', 'nb_matches', $nb_matches);
$form->addElement('hidden', 'nb_options', $nb_options);
$thWeighting = '';
if (MATCHING === $this->type) {
$thWeighting = '<th width="10%">'.get_lang('Weighting').'</th>';
}
// DISPLAY MATCHES
$html = '<table class="table table-striped table-hover">
<thead>
<tr>
<th width="5%">'.get_lang('Number').'</th>
<th width="70%">'.get_lang('Answer').'</th>
<th width="15%">'.get_lang('MatchesTo').'</th>
'.$thWeighting.'
</tr>
</thead>
<tbody>';
$form->addHeader(get_lang('MakeCorrespond'));
$form->addHtml($html);
if ($nb_matches < 1) {
$nb_matches = 1;
Display::addFlash(
Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'))
);
}
$editorConfig = [
'ToolbarSet' => 'TestMatching',
'Width' => '100%',
'Height' => '125',
];
for ($i = 1; $i <= $nb_matches; $i++) {
$renderer = &$form->defaultRenderer();
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"answer[$i]"
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"matches[$i]"
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"weighting[$i]"
);
$form->addHtml('<tr>');
$form->addHtml("<td>$i</td>");
$form->addHtmlEditor(
"answer[$i]",
null,
null,
false,
$editorConfig
);
$form->addSelect(
"matches[$i]",
null,
$matches,
['id' => 'matches_'.$i]
);
if (MATCHING === $this->type) {
$form->addText(
"weighting[$i]",
null,
true,
['id' => 'weighting_'.$i, 'value' => 10]
);
} else {
$form->addHidden("weighting[$i]", "0");
}
$form->addHtml('</tr>');
}
$form->addHtml('</tbody></table>');
// DISPLAY OPTIONS
$html = '<table class="table table-striped table-hover">
<thead>
<tr>
<th width="15%">'.get_lang('Number').'</th>
<th width="85%">'.get_lang('Answer').'</th>
</tr>
</thead>
<tbody>';
$form->addHtml($html);
if ($nb_options < 1) {
$nb_options = 1;
Display::addFlash(
Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'))
);
}
for ($i = 1; $i <= $nb_options; $i++) {
$renderer = &$form->defaultRenderer();
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->{element}</td>',
"option[$i]"
);
$form->addHtml('<tr>');
$form->addHtml('<td>'.chr(64 + $i).'</td>');
$form->addHtmlEditor(
"option[$i]",
null,
null,
false,
$editorConfig
);
$form->addHtml('</tr>');
}
$form->addHtml('</table>');
if (MATCHING_COMBINATION === $this->type) {
//only 1 answer the all deal ...
$form->addText('questionWeighting', get_lang('Score'), true, ['value' => 10]);
if (!empty($this->iid)) {
$defaults['questionWeighting'] = $this->weighting;
}
}
global $text;
$group = [];
// setting the save button here and not in the question class.php
$group[] = $form->addButtonDelete(get_lang('DelElem'), 'lessOptions', true);
$group[] = $form->addButtonCreate(get_lang('AddElem'), 'moreOptions', true);
$group[] = $form->addButtonSave($text, 'submitQuestion', true);
$form->addGroup($group);
if (!empty($this->iid)) {
$form->setDefaults($defaults);
} else {
if ($this->isContent == 1) {
$form->setDefaults($defaults);
}
}
$form->setConstants(
[
'nb_matches' => $nb_matches,
'nb_options' => $nb_options,
]
);
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$nb_matches = $form->getSubmitValue('nb_matches');
$nb_options = $form->getSubmitValue('nb_options');
$this->weighting = 0;
$position = 0;
$objAnswer = new Answer($this->iid);
// Insert the options
for ($i = 1; $i <= $nb_options; $i++) {
$position++;
$option = $form->getSubmitValue('option['.$i.']');
$objAnswer->createAnswer($option, 0, '', 0, $position);
}
// Insert the answers
for ($i = 1; $i <= $nb_matches; $i++) {
$position++;
$answer = $form->getSubmitValue('answer['.$i.']');
$matches = $form->getSubmitValue('matches['.$i.']');
$weighting = $form->getSubmitValue('weighting['.$i.']');
$this->weighting += $weighting;
$objAnswer->createAnswer(
$answer,
$matches,
'',
$weighting,
$position
);
}
if (MATCHING_COMBINATION == $this->type) {
$this->weighting = $form->getSubmitValue('questionWeighting');
}
$objAnswer->save();
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'">';
$header .= '<tr>';
$header .= '<th>'.get_lang('ElementList').'</th>';
if (!in_array($exercise->results_disabled, [
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
])
) {
$header .= '<th>'.get_lang('Choice').'</th>';
}
if ($exercise->showExpectedChoice()) {
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
$header .= '<th>'.get_lang('Status').'</th>';
} else {
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('CorrespondsTo').'</th>';
}
}
$header .= '</tr>';
return $header;
}
/**
* Check if a answer is correct.
*
* @param int $position
* @param int $answer
* @param int $questionId
*
* @return bool
*/
public static function isCorrect($position, $answer, $questionId)
{
$em = Database::getManager();
$count = $em
->createQuery('
SELECT COUNT(a) From ChamiloCourseBundle:CQuizAnswer a
WHERE a.iid = :position AND a.correct = :answer AND a.questionId = :question
')
->setParameters([
'position' => $position,
'answer' => $answer,
'question' => $questionId,
])
->getSingleScalarResult();
return $count ? true : false;
}
}

View File

@@ -0,0 +1,303 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* Class MultipleAnswer.
*
* This class allows to instantiate an object of type MULTIPLE_ANSWER (MULTIPLE CHOICE, MULTIPLE ANSWER),
* extending the class question
*
* @author Eric Marguin
*/
class MultipleAnswer extends Question
{
public $typePicture = 'mcma.png';
public $explanationLangVar = 'MultipleSelect';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = MULTIPLE_ANSWER;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$editorConfig = [
'ToolbarSet' => 'TestProposedAnswer',
'Width' => '100%',
'Height' => '125',
];
// The previous default value was 2. See task #1759.
$nb_answers = isset($_POST['nb_answers']) ? $_POST['nb_answers'] : 4;
$nb_answers += (isset($_POST['lessAnswers']) ? -1 : (isset($_POST['moreAnswers']) ? 1 : 0));
$obj_ex = Session::read('objExercise');
$form->addHeader(get_lang('Answers'));
$html = '<table class="table table-striped table-hover">
<thead>
<tr>
<th width="10">'.get_lang('Number').'</th>
<th width="10">'.get_lang('True').'</th>
<th width="50%">'.get_lang('Answer').'</th>
<th width="50%">'.get_lang('Comment').'</th>
<th width="10">'.get_lang('Weighting').'</th>
</tr>
</thead>
<tbody>';
$form->addHtml($html);
$defaults = [];
$correct = 0;
$answer = false;
if (!empty($this->iid)) {
$answer = new Answer($this->iid);
$answer->read();
if ($answer->nbrAnswers > 0 && !$form->isSubmitted()) {
$nb_answers = $answer->nbrAnswers;
}
}
$form->addElement('hidden', 'nb_answers');
$boxes_names = [];
if ($nb_answers < 1) {
$nb_answers = 1;
echo Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'));
}
for ($i = 1; $i <= $nb_answers; $i++) {
$form->addHtml('<tr>');
if (is_object($answer)) {
$defaults['answer['.$i.']'] = $answer->answer[$i];
$defaults['comment['.$i.']'] = $answer->comment[$i];
$defaults['weighting['.$i.']'] = float_format($answer->weighting[$i], 1);
$defaults['correct['.$i.']'] = $answer->correct[$i];
} else {
$defaults['answer[1]'] = get_lang('DefaultMultipleAnswer2');
$defaults['comment[1]'] = get_lang('DefaultMultipleComment2');
$defaults['correct[1]'] = true;
$defaults['weighting[1]'] = 10;
$defaults['answer[2]'] = get_lang('DefaultMultipleAnswer1');
$defaults['comment[2]'] = get_lang('DefaultMultipleComment1');
$defaults['correct[2]'] = false;
$defaults['weighting[2]'] = -5;
}
$renderer = &$form->defaultRenderer();
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'correct['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'counter['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'answer['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'comment['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'weighting['.$i.']'
);
$answer_number = $form->addElement('text', 'counter['.$i.']', null, 'value="'.$i.'"');
$answer_number->freeze();
$form->addElement(
'checkbox',
'correct['.$i.']',
null,
null,
'class="checkbox" style="margin-left: 0em;"'
);
$boxes_names[] = 'correct['.$i.']';
$form->addHtmlEditor("answer[$i]", null, null, false, $editorConfig);
$form->addRule('answer['.$i.']', get_lang('ThisFieldIsRequired'), 'required');
$form->addHtmlEditor("comment[$i]", null, null, false, $editorConfig);
$form->addElement('text', 'weighting['.$i.']', null, ['style' => "width: 60px;", 'value' => '0']);
$form->addHtml('</tr>');
}
$form->addHtml('</tbody>');
$form->addHtml('</table>');
$form->add_multiple_required_rule(
$boxes_names,
get_lang('ChooseAtLeastOneCheckbox'),
'multiple_required'
);
$buttonGroup = [];
global $text;
if ($obj_ex->edit_exercise_in_lp == true ||
(empty($this->exerciseList) && empty($obj_ex->iid))
) {
// setting the save button here and not in the question class.php
$buttonGroup[] = $form->addButtonDelete(get_lang('LessAnswer'), 'lessAnswers', true);
$buttonGroup[] = $form->addButtonCreate(get_lang('PlusAnswer'), 'moreAnswers', true);
$buttonGroup[] = $form->addButton(
'submitQuestion',
$text,
'check',
'primary',
'default',
null,
['id' => 'submit-question'],
true
);
}
$form->addGroup($buttonGroup);
$defaults['correct'] = $correct;
if (!empty($this->iid)) {
$form->setDefaults($defaults);
} else {
if ($this->isContent == 1) {
$form->setDefaults($defaults);
}
}
$form->setConstants(['nb_answers' => $nb_answers]);
}
/**
* Validate question answers before saving.
*
* @param object $form The form object.
*
* @return array|bool True if valid, or an array with errors if invalid.
*/
public function validateAnswers($form)
{
$nb_answers = $form->getSubmitValue('nb_answers');
$hasCorrectAnswer = false;
$hasValidWeighting = false;
$errors = [];
$error_fields = [];
for ($i = 1; $i <= $nb_answers; $i++) {
$isCorrect = $form->getSubmitValue("correct[$i]");
$weighting = trim($form->getSubmitValue("weighting[$i]") ?? '');
if ($isCorrect) {
$hasCorrectAnswer = true;
if (is_numeric($weighting) && floatval($weighting) > 0) {
$hasValidWeighting = true;
}
}
}
if (!$hasCorrectAnswer) {
$errors[] = get_lang('AtLeastOneCorrectAnswerRequired');
$error_fields[] = "correct";
}
if ($hasCorrectAnswer && !$hasValidWeighting) {
// Only show if at least one correct answer exists but its weighting is not valid
$errors[] = get_lang('AtLeastOneCorrectAnswerMustHaveAPositiveScore');
}
return empty($errors) ? true : ['errors' => $errors, 'error_fields' => $error_fields];
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$validationResult = $this->validateAnswers($form);
if ($validationResult !== true) {
Display::addFlash(Display::return_message(implode("<br>", $validationResult['errors']), 'error'));
return;
}
$questionWeighting = 0;
$objAnswer = new Answer($this->iid);
$nb_answers = $form->getSubmitValue('nb_answers');
for ($i = 1; $i <= $nb_answers; $i++) {
$answer = trim(str_replace(['<p>', '</p>'], '', $form->getSubmitValue('answer['.$i.']')));
$comment = trim(str_replace(['<p>', '</p>'], '', $form->getSubmitValue('comment['.$i.']')));
$weighting = trim($form->getSubmitValue('weighting['.$i.']'));
$goodAnswer = trim($form->getSubmitValue('correct['.$i.']'));
if ($goodAnswer) {
$weighting = abs($weighting);
} else {
$weighting = abs($weighting);
$weighting = -$weighting;
}
if ($weighting > 0) {
$questionWeighting += $weighting;
}
$objAnswer->createAnswer(
$answer,
$goodAnswer,
$comment,
$weighting,
$i
);
}
// saves the answers into the data base
$objAnswer->save();
// sets the total weighting of the question
$this->updateWeighting($questionWeighting);
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'"><tr>';
$header .= '<th>'.get_lang('Choice').'</th>';
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
$header .= '<th>'.get_lang('Answer').'</th>';
if ($exercise->showExpectedChoice()) {
$header .= '<th>'.get_lang('Status').'</th>';
}
if (false === $exercise->hideComment) {
$header .= '<th>'.get_lang('Comment').'</th>';
}
$header .= '</tr>';
return $header;
}
}

View File

@@ -0,0 +1,256 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* Class MultipleAnswerCombination.
*
* This class allows to instantiate an object of type
* MULTIPLE_ANSWER (MULTIPLE CHOICE, MULTIPLE ANSWER),
* extending the class question
*
* @author Eric Marguin
*/
class MultipleAnswerCombination extends Question
{
public $typePicture = 'mcmac.png';
public $explanationLangVar = 'MultipleSelectCombination';
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = MULTIPLE_ANSWER_COMBINATION;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$nb_answers = isset($_POST['nb_answers']) ? $_POST['nb_answers'] : 2;
$nb_answers += (isset($_POST['lessAnswers']) ? -1 : (isset($_POST['moreAnswers']) ? 1 : 0));
$obj_ex = Session::read('objExercise');
$html = '<table class="table table-striped table-hover">';
$html .= '<thead>';
$html .= '<tr>';
$html .= '<th width="10">'.get_lang('Number').'</th>';
$html .= '<th width="10">'.get_lang('True').'</th>';
$html .= '<th width="50%">'.get_lang('Answer').'</th>';
$html .= '<th width="50%">'.get_lang('Comment').'</th>';
$html .= '</tr>';
$html .= '</thead>';
$html .= '<tbody>';
$form->addHeader(get_lang('Answers'));
$form->addHtml($html);
$defaults = [];
$correct = 0;
$answer = false;
if (!empty($this->iid)) {
$answer = new Answer($this->iid);
$answer->read();
if ($answer->nbrAnswers > 0 && !$form->isSubmitted()) {
$nb_answers = $answer->nbrAnswers;
}
}
$form->addElement('hidden', 'nb_answers');
$boxes_names = [];
if ($nb_answers < 1) {
$nb_answers = 1;
echo Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'));
}
for ($i = 1; $i <= $nb_answers; $i++) {
$form->addHtml('<tr>');
if (is_object($answer)) {
$defaults['answer['.$i.']'] = $answer->answer[$i];
$defaults['comment['.$i.']'] = $answer->comment[$i];
$defaults['weighting['.$i.']'] = float_format($answer->weighting[$i], 1);
$defaults['correct['.$i.']'] = $answer->correct[$i];
} else {
$defaults['answer[1]'] = get_lang('DefaultMultipleAnswer2');
$defaults['comment[1]'] = get_lang('DefaultMultipleComment2');
$defaults['correct[1]'] = true;
$defaults['weighting[1]'] = 10;
$defaults['answer[2]'] = get_lang('DefaultMultipleAnswer1');
$defaults['comment[2]'] = get_lang('DefaultMultipleComment1');
$defaults['correct[2]'] = false;
}
$renderer = &$form->defaultRenderer();
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'correct['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'counter['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'answer['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'comment['.$i.']'
);
$answer_number = $form->addElement('text', 'counter['.$i.']', null, 'value="'.$i.'"');
$answer_number->freeze();
$form->addElement(
'checkbox',
'correct['.$i.']',
null,
null,
'class="checkbox" style="margin-left: 0em;"'
);
$boxes_names[] = 'correct['.$i.']';
$form->addHtmlEditor(
"answer[$i]",
'',
true,
false,
['ToolbarSet' => 'TestProposedAnswer', 'Width' => '100%', 'Height' => '100']
);
$form->addHtmlEditor(
"comment[$i]",
'',
false,
false,
['ToolbarSet' => 'TestProposedAnswer', 'Width' => '100%', 'Height' => '100']
);
$form->addHtml('</tr>');
}
$form->addElement('html', '</tbody></table>');
$form->add_multiple_required_rule(
$boxes_names,
get_lang('ChooseAtLeastOneCheckbox'),
'multiple_required'
);
//only 1 answer the all deal ...
$form->addText('weighting[1]', get_lang('Score'), false, ['value' => 10]);
global $text;
if ($obj_ex->edit_exercise_in_lp == true ||
(empty($this->exerciseList) && empty($obj_ex->iid))
) {
// setting the save button here and not in the question class.php
$buttonGroup = [
$form->addButtonDelete(get_lang('LessAnswer'), 'lessAnswers', true),
$form->addButtonCreate(get_lang('PlusAnswer'), 'moreAnswers', true),
$form->addButtonSave($text, 'submitQuestion', true),
];
$form->addGroup($buttonGroup);
}
$defaults['correct'] = $correct;
if (!empty($this->iid)) {
$form->setDefaults($defaults);
} else {
if ($this->isContent == 1) {
$form->setDefaults($defaults);
}
}
$form->setConstants(['nb_answers' => $nb_answers]);
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$questionWeighting = 0;
$objAnswer = new Answer($this->iid);
$nb_answers = $form->getSubmitValue('nb_answers');
for ($i = 1; $i <= $nb_answers; $i++) {
$answer = trim($form->getSubmitValue('answer['.$i.']'));
$comment = trim($form->getSubmitValue('comment['.$i.']'));
if ($i == 1) {
$weighting = trim($form->getSubmitValue('weighting['.$i.']'));
} else {
$weighting = 0;
}
$goodAnswer = trim($form->getSubmitValue('correct['.$i.']'));
if ($goodAnswer) {
$weighting = abs($weighting);
} else {
// $weighting = -$weighting;
$weighting = abs($weighting);
}
if ($weighting > 0) {
$questionWeighting += $weighting;
}
$objAnswer->createAnswer(
$answer,
$goodAnswer,
$comment,
$weighting,
$i
);
}
// saves the answers into the data base
$objAnswer->save();
// sets the total weighting of the question
$this->updateWeighting($questionWeighting);
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'"><tr>';
if (!in_array($exercise->results_disabled, [
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
])
) {
$header .= '<th>'.get_lang('Choice').'</th>';
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
}
$header .= '<th>'.get_lang('Answer').'</th>';
if ($exercise->showExpectedChoice()) {
$header .= '<th>'.get_lang('Status').'</th>';
}
if (false === $exercise->hideComment) {
$header .= '<th>'.get_lang('Comment').'</th>';
}
$header .= '</tr>';
return $header;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class MultipleAnswerCombinationTrueFalse.
*
* This class allows to instantiate an object of type MULTIPLE_ANSWER (MULTIPLE CHOICE, MULTIPLE ANSWER),
* extending the class question
*
* @author Eric Marguin
*/
class MultipleAnswerCombinationTrueFalse extends MultipleAnswerCombination
{
public $typePicture = 'mcmaco.png';
public $explanationLangVar = 'MultipleAnswerCombinationTrueFalse';
public $options;
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = MULTIPLE_ANSWER_COMBINATION_TRUE_FALSE;
$this->isContent = $this->getIsContent();
$this->options = [
'1' => get_lang('True'),
'0' => get_lang('False'),
'2' => get_lang('DontKnow'),
];
}
}

View File

@@ -0,0 +1,219 @@
<?php
/* For licensing terms, see /license.txt */
use Symfony\Component\HttpFoundation\Request as HttpRequest;
$questionId = (int) $_GET['mad_admin'];
$exerciseId = $exerciseId ?? 0;
/** @var Exercise $objExercise */
$objExercise = $objExercise ?? null;
/** @var Question $objQuestion */
$objQuestion = $objQuestion ?? null;
if (!is_object($objQuestion)) {
$objQuestion = Question::read($questionId);
}
$isGlobal = MULTIPLE_ANSWER_DROPDOWN_COMBINATION === (int) $objQuestion->type;
$objAnswer = new Answer($objQuestion->iid, 0, $objExercise);
$options = [];
for ($i = 1; $i <= $objAnswer->nbrAnswers; $i++) {
$options[$objAnswer->iid[$i]] = $objAnswer->answer[$i];
}
$webCodePath = api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&';
$adminUrl = $webCodePath.http_build_query(['exerciseId' => $exerciseId]);
$httpRequest = HttpRequest::createFromGlobals();
$submitAnswers = $httpRequest->request->has('submitAnswers');
if ($submitAnswers) {
$questionAnswers = array_map(
'intval',
(array) $httpRequest->request->get('answer', [])
);
$tblQuizAnswer = Database::get_course_table(TABLE_QUIZ_ANSWER);
Database::query(
"UPDATE $tblQuizAnswer SET correct = 0, ponderation = 0 WHERE question_id = ".$objQuestion->iid
);
Database::query(
"UPDATE $tblQuizAnswer SET correct = 1
WHERE question_id = {$objQuestion->iid} AND iid IN (".implode(', ', $questionAnswers).")"
);
if ($isGlobal) {
$questionWeighting = (float) $httpRequest->request->get('weighting', 0);
} else {
$questionWeighting = 0;
$choiceWeighting = array_map(
'intval',
(array) $httpRequest->request->get('c_weighting', [])
);
foreach ($questionAnswers as $key => $questionAnswer) {
if (empty($choiceWeighting[$key])) {
continue;
}
$questionWeighting += $choiceWeighting[$key];
Database::query(
"UPDATE $tblQuizAnswer SET ponderation = {$choiceWeighting[$key]}
WHERE question_id = {$objQuestion->iid} AND iid = $questionAnswer"
);
}
}
$objQuestion->updateWeighting($questionWeighting);
$objQuestion->save($objExercise);
echo '<script type="text/javascript">window.location.href="'.$adminUrl.'&message=ItemUpdated"</script>';
exit;
}
if ($questionId) {
$answers = [];
for ($i = 1; $i <= $objAnswer->nbrAnswers; $i++) {
if (false === (bool) $objAnswer->correct[$i]) {
continue;
}
$answers[] = [
$objAnswer->iid[$i],
$objAnswer->answer[$i],
$objAnswer->weighting[$i],
];
}
$selfUrl = $webCodePath.http_build_query(['mad_admin' => $questionId, 'exerciseId' => $exerciseId]);
echo Display::page_header(
get_lang('Question').': '.$objQuestion->selectTitle()
); ?>
<form action="<?php echo $selfUrl; ?>" class="form-horizontal" method="post">
<div class="form-group">
<label for="option" class="col-sm-2 control-label"><?php echo get_lang('Answer'); ?></label>
<div class="col-sm-8">
<?php echo Display::select(
'option',
$options,
-1,
['data-live-search' => 'true', 'class' => 'form-control selectpicker']
); ?>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="button" class="btn btn-default" name="add_answers">
<em class="fa fa-plus fa-fw" aria-hidden="true"></em>
<?php echo get_lang('Add'); ?>
</button>
</div>
</div>
<fieldset>
<legend><?php echo get_lang('Answers'); ?></legend>
<table class="table table-striped table-hover table-condensed">
<thead>
<tr>
<th class="text-right"><?php echo get_lang('Number'); ?></th>
<th style="width: 85%;"><?php echo get_lang('Answer'); ?></th>
<?php if (!$isGlobal) { ?>
<th class="text-right"><?php echo get_lang('Weighting'); ?></th>
<?php } ?>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</fieldset>
<?php if ($isGlobal) { ?>
<div class="form-group">
<label for="weighting" class="control-label col-sm-2"><?php echo get_lang('Weighting'); ?></label>
<div class="col-sm-8">
<input type="number" required min="0" class="form-control" step="any" id="weighting" name="weighting"
value="<?php echo $objQuestion->weighting; ?>">
</div>
</div>
<?php } ?>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" name="submitAnswers" value="submitAnswers">
<em class="fa fa-save fa-fw" aria-hidden="true"></em>
<?php echo get_lang('AddQuestionToExercise'); ?>
</button>
</div>
</div>
</form>
<script>
$(function () {
var lines = <?php echo json_encode($options); ?>;
var answers = <?php echo json_encode($answers); ?>;
var $txtOption = $('#option');
var $tBody = $('table tbody');
$('[name="add_answers"]').on('click', function (e) {
e.preventDefault();
var selected = $txtOption.val();
if ($txtOption.val() < 0) {
return;
}
answers.push([selected, lines[selected], 0]);
$txtOption.val(-1).selectpicker('refresh');
renderList();
});
$tBody.on('click', '.btn-remove', function (e) {
e.preventDefault();
var index = $(this).data('index');
answers.splice(index, 1);
renderList();
});
function renderList () {
var html = '';
$.each(answers, function (key, line) {
var counter = key + 1;
html += '<tr><td class="text-right">'
+ counter + "\n"
+ '<input type="hidden" name="counter[]" value="' + counter + '">'
+ '</td><td>'
+ line[1] + "\n"
+ '<input type="hidden" name="answer[]" value="' + line[0] + '">'
+ '</td><td class="text-right">'
<?php if (!$isGlobal) { ?>
+ '<input type="number" required min="0" class="form-control" step="any" name="c_weighting[]" value="' + line[2] + '">'
+ '</td><td class="text-right">'
<?php } ?>
+ '<button type="button" class="btn btn-default btn-remove" data-index="' + key + '" aria-label="<?php echo get_lang('Remove'); ?>">'
+ '<?php echo Display::returnFontAwesomeIcon('minus', '', true); ?>'
+ '</button>'
+ '</td></tr>';
});
$tBody.html(html);
}
renderList();
})
</script>
<?php
}

View File

@@ -0,0 +1,356 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* Class MultipleAnswerTrueFalse
* This class allows to instantiate an object of type MULTIPLE_ANSWER
* (MULTIPLE CHOICE, MULTIPLE ANSWER), extending the class question.
*
* @author Julio Montoya
*/
class MultipleAnswerTrueFalse extends Question
{
public $typePicture = 'mcmao.png';
public $explanationLangVar = 'MultipleAnswerTrueFalse';
public $options;
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = MULTIPLE_ANSWER_TRUE_FALSE;
$this->isContent = $this->getIsContent();
$this->options = [1 => 'True', 2 => 'False', 3 => 'DoubtScore'];
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$nb_answers = isset($_POST['nb_answers']) ? $_POST['nb_answers'] : 4;
// The previous default value was 2. See task #1759.
$nb_answers += (isset($_POST['lessAnswers']) ? -1 : (isset($_POST['moreAnswers']) ? 1 : 0));
$course_id = api_get_course_int_id();
$obj_ex = Session::read('objExercise');
$renderer = &$form->defaultRenderer();
$defaults = [];
$html = '<table class="table table-striped table-hover">';
$html .= '<thead>';
$html .= '<tr>';
$html .= '<th width="10px">'.get_lang('Number').'</th>';
$html .= '<th width="10px">'.get_lang('True').'</th>';
$html .= '<th width="10px">'.get_lang('False').'</th>';
$html .= '<th width="50%">'.get_lang('Answer').'</th>';
// show column comment when feedback is enable
if ($obj_ex->getFeedbackType() != EXERCISE_FEEDBACK_TYPE_EXAM) {
$html .= '<th width="50%">'.get_lang('Comment').'</th>';
}
$html .= '</tr>';
$html .= '</thead>';
$html .= '<tbody>';
$form->addHeader(get_lang('Answers'));
$form->addHtml($html);
$answer = null;
if (!empty($this->iid)) {
$answer = new Answer($this->iid);
$answer->read();
if ($answer->nbrAnswers > 0 && !$form->isSubmitted()) {
$nb_answers = $answer->nbrAnswers;
}
}
$form->addElement('hidden', 'nb_answers');
if ($nb_answers < 1) {
$nb_answers = 1;
echo Display::return_message(get_lang('YouHaveToCreateAtLeastOneAnswer'));
}
// Can be more options
$optionData = Question::readQuestionOption($this->iid, $course_id);
for ($i = 1; $i <= $nb_answers; $i++) {
$form->addHtml('<tr>');
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'correct['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'counter['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'answer['.$i.']'
);
$renderer->setElementTemplate(
'<td><!-- BEGIN error --><span class="form_error">{error}</span><!-- END error --><br/>{element}</td>',
'comment['.$i.']'
);
$answer_number = $form->addElement(
'text',
'counter['.$i.']',
null,
'value="'.$i.'"'
);
$answer_number->freeze();
if (is_object($answer)) {
$defaults['answer['.$i.']'] = $answer->answer[$i];
$defaults['comment['.$i.']'] = $answer->comment[$i];
$correct = $answer->correct[$i];
$defaults['correct['.$i.']'] = $correct;
$j = 1;
if (!empty($optionData)) {
foreach ($optionData as $id => $data) {
$rdoCorrect = $form->addElement('radio', 'correct['.$i.']', null, null, $id);
if (isset($_POST['correct']) && isset($_POST['correct'][$i]) && $id == $_POST['correct'][$i]) {
$rdoCorrect->setValue(Security::remove_XSS($_POST['correct'][$i]));
}
$j++;
if ($j == 3) {
break;
}
}
}
} else {
$form->addElement('radio', 'correct['.$i.']', null, null, 1);
$form->addElement('radio', 'correct['.$i.']', null, null, 2);
}
$form->addHtmlEditor(
"answer[$i]",
get_lang('ThisFieldIsRequired'),
true,
false,
['ToolbarSet' => 'TestProposedAnswer', 'Width' => '100%', 'Height' => '100']
);
if (isset($_POST['answer']) && isset($_POST['answer'][$i])) {
$form->getElement("answer[$i]")->setValue(Security::remove_XSS($_POST['answer'][$i]));
}
// show comment when feedback is enable
if ($obj_ex->getFeedbackType() != EXERCISE_FEEDBACK_TYPE_EXAM) {
$txtComment = $form->addElement(
'html_editor',
'comment['.$i.']',
null,
[],
[
'ToolbarSet' => 'TestProposedAnswer',
'Width' => '100%',
'Height' => '100',
]
);
$form->applyFilter("comment[$i]", 'attr_on_filter');
if (isset($_POST['comment']) && isset($_POST['comment'][$i])) {
$txtComment->setValue(Security::remove_XSS($_POST['comment'][$i]));
}
}
$form->addHtml('</tr>');
}
$form->addHtml('</tbody></table>');
$correctInputTemplate = '<div class="form-group">';
$correctInputTemplate .= '<label class="col-sm-2 control-label">';
$correctInputTemplate .= '<span class="form_required">*</span>'.get_lang('Score');
$correctInputTemplate .= '</label>';
$correctInputTemplate .= '<div class="col-sm-8">';
$correctInputTemplate .= '<table>';
$correctInputTemplate .= '<tr>';
$correctInputTemplate .= '<td>';
$correctInputTemplate .= get_lang('Correct').'{element}';
$correctInputTemplate .= '<!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->';
$correctInputTemplate .= '</td>';
$wrongInputTemplate = '<td>';
$wrongInputTemplate .= get_lang('Wrong').'{element}';
$wrongInputTemplate .= '<!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->';
$wrongInputTemplate .= '</td>';
$doubtScoreInputTemplate = '<td>'.get_lang('DoubtScore').'<br>{element}';
$doubtScoreInputTemplate .= '<!-- BEGIN error --><span class="form_error">{error}</span><!-- END error -->';
$doubtScoreInputTemplate .= '</td>';
$doubtScoreInputTemplate .= '</tr>';
$doubtScoreInputTemplate .= '</table>';
$doubtScoreInputTemplate .= '</div>';
$doubtScoreInputTemplate .= '</div>';
$renderer->setElementTemplate($correctInputTemplate, 'option[1]');
$renderer->setElementTemplate($wrongInputTemplate, 'option[2]');
$renderer->setElementTemplate($doubtScoreInputTemplate, 'option[3]');
// 3 scores
$txtOption1 = $form->addElement('text', 'option[1]', get_lang('Correct'), ['class' => 'span1', 'value' => '1']);
$txtOption2 = $form->addElement('text', 'option[2]', get_lang('Wrong'), ['class' => 'span1', 'value' => '-0.5']);
$txtOption3 = $form->addElement('text', 'option[3]', get_lang('DoubtScore'), ['class' => 'span1', 'value' => '0']);
$form->addRule('option[1]', get_lang('ThisFieldIsRequired'), 'required');
$form->addRule('option[2]', get_lang('ThisFieldIsRequired'), 'required');
$form->addRule('option[3]', get_lang('ThisFieldIsRequired'), 'required');
$form->addElement('hidden', 'options_count', 3);
// Extra values True, false, Dont known
if (!empty($this->extra)) {
$scores = explode(':', $this->extra);
if (!empty($scores)) {
$txtOption1->setValue($scores[0]);
$txtOption2->setValue($scores[1]);
$txtOption3->setValue($scores[2]);
}
}
global $text;
if ($obj_ex->edit_exercise_in_lp == true ||
(empty($this->exerciseList) && empty($obj_ex->iid))
) {
// setting the save button here and not in the question class.php
$buttonGroup[] = $form->addButtonDelete(get_lang('LessAnswer'), 'lessAnswers', true);
$buttonGroup[] = $form->addButtonCreate(get_lang('PlusAnswer'), 'moreAnswers', true);
$buttonGroup[] = $form->addButtonSave($text, 'submitQuestion', true);
$form->addGroup($buttonGroup);
}
if (!empty($this->iid) && !$form->isSubmitted()) {
$form->setDefaults($defaults);
}
$form->setConstants(['nb_answers' => $nb_answers]);
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$questionWeighting = 0;
$objAnswer = new Answer($this->iid);
$nb_answers = $form->getSubmitValue('nb_answers');
$course_id = api_get_course_int_id();
$correct = [];
$options = Question::readQuestionOption($this->iid, $course_id);
if (!empty($options)) {
foreach ($options as $optionData) {
$id = $optionData['iid'];
unset($optionData['iid']);
Question::updateQuestionOption($id, $optionData, $course_id);
}
} else {
for ($i = 1; $i <= 3; $i++) {
$last_id = Question::saveQuestionOption(
$this->iid,
$this->options[$i],
$course_id,
$i
);
$correct[$i] = $last_id;
}
}
/* Getting quiz_question_options (true, false, doubt) because
it's possible that there are more options in the future */
$new_options = Question::readQuestionOption($this->iid, $course_id);
$sortedByPosition = [];
foreach ($new_options as $item) {
$sortedByPosition[$item['position']] = $item;
}
/* Saving quiz_question.extra values that has the correct scores of
the true, false, doubt options registered in this format
XX:YY:ZZZ where XX is a float score value.*/
$extra_values = [];
for ($i = 1; $i <= 3; $i++) {
$score = trim($form->getSubmitValue('option['.$i.']'));
$extra_values[] = $score;
}
$this->setExtra(implode(':', $extra_values));
for ($i = 1; $i <= $nb_answers; $i++) {
$answer = trim($form->getSubmitValue('answer['.$i.']'));
$comment = trim($form->getSubmitValue('comment['.$i.']'));
$goodAnswer = trim($form->getSubmitValue('correct['.$i.']'));
if (empty($options)) {
//If this is the first time that the question is created when
// change the default values from the form 1 and 2 by the correct "option id" registered
$goodAnswer = isset($sortedByPosition[$goodAnswer]) ? $sortedByPosition[$goodAnswer]['iid'] : '';
}
$questionWeighting += $extra_values[0]; //By default 0 has the correct answers
$objAnswer->createAnswer($answer, $goodAnswer, $comment, '', $i);
}
// saves the answers into the database
$objAnswer->save();
// sets the total weighting of the question
$this->updateWeighting($questionWeighting);
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'"><tr>';
if (!in_array($exercise->results_disabled, [
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
])
) {
$header .= '<th>'.get_lang('Choice').'</th>';
if ($exercise->showExpectedChoiceColumn()) {
$header .= '<th>'.get_lang('ExpectedChoice').'</th>';
}
}
$header .= '<th>'.get_lang('Answer').'</th>';
if ($exercise->showExpectedChoice()) {
$header .= '<th>'.get_lang('Status').'</th>';
}
if ($exercise->getFeedbackType() != EXERCISE_FEEDBACK_TYPE_EXAM ||
in_array(
$exercise->results_disabled,
[
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
]
)
) {
if (false === $exercise->hideComment) {
$header .= '<th>'.get_lang('Comment').'</th>';
}
}
$header .= '</tr>';
return $header;
}
}

View File

@@ -0,0 +1,324 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Class OralExpression
* This class allows to instantiate an object of type FREE_ANSWER,
* extending the class question.
*
* @author Eric Marguin
*/
class OralExpression extends Question
{
public $typePicture = 'audio_question.png';
public $explanationLangVar = 'OralExpression';
public $available_extensions = ['wav', 'ogg'];
private $sessionId;
private $userId;
private $exerciseId;
private $exeId;
private $storePath;
private $fileName;
private $filePath;
/**
* Constructor.
*/
public function __construct()
{
parent::__construct();
$this->type = ORAL_EXPRESSION;
$this->isContent = $this->getIsContent();
}
/**
* {@inheritdoc}
*/
public function createAnswersForm($form)
{
$form->addText(
'weighting',
get_lang('Weighting'),
['class' => 'span1']
);
global $text;
// setting the save button here and not in the question class.php
$form->addButtonSave($text, 'submitQuestion');
if (!empty($this->iid)) {
$form->setDefaults(['weighting' => float_format($this->weighting, 1)]);
} else {
if ($this->isContent == 1) {
$form->setDefaults(['weighting' => '10']);
}
}
}
/**
* {@inheritdoc}
*/
public function processAnswersCreation($form, $exercise)
{
$this->weighting = $form->getSubmitValue('weighting');
$this->save($exercise);
}
/**
* {@inheritdoc}
*/
public function return_header(Exercise $exercise, $counter = null, $score = [])
{
$score['revised'] = $this->isQuestionWaitingReview($score);
$header = parent::return_header($exercise, $counter, $score);
$header .= '<table class="'.$this->question_table_class.'">
<tr>
<th>'.get_lang("Answer").'</th>
</tr>';
return $header;
}
/**
* Initialize the attributes to generate the file path.
*/
public function initFile(
int $sessionId,
int $userId,
int $exerciseId,
int $exeId,
int $courseId = 0
): void {
if (!empty($courseId)) {
$this->course = api_get_course_info_by_id($courseId);
}
$this->sessionId = $sessionId;
$this->userId = $userId;
$this->exerciseId = 0;
if (!empty($exerciseId)) {
$this->exerciseId = $exerciseId;
}
$this->exeId = $exeId;
$this->storePath = $this->generateDirectory();
$this->fileName = $this->generateFileName();
$this->filePath = $this->storePath.$this->fileName;
}
/**
* Return the HTML code to show the RecordRTC/Wami recorder.
*
* @return string
*/
public function returnRecorder()
{
$directory = '/..'.$this->generateRelativeDirectory();
$recordAudioView = new Template(
'',
false,
false,
false,
false,
false,
false
);
$recordAudioView->assign('directory', $directory);
$recordAudioView->assign('user_id', $this->userId);
$recordAudioView->assign('file_name', $this->fileName);
$recordAudioView->assign('question_id', $this->iid);
$template = $recordAudioView->get_template('exercise/oral_expression.tpl');
return $recordAudioView->fetch($template);
}
/**
* Get the absolute file path. Return null if the file doesn't exists.
*
* @param bool $loadFromDatabase
*
* @return string
*/
public function getAbsoluteFilePath($loadFromDatabase = false)
{
$fileName = $this->fileName;
if ($loadFromDatabase) {
$em = Database::getManager();
//Load the real filename just if exists
if (isset($this->exeId, $this->userId, $this->iid, $this->sessionId, $this->course['real_id'])) {
$result = $em
->getRepository('ChamiloCoreBundle:TrackEAttempt')
->findOneBy([
'exeId' => $this->exeId,
'userId' => $this->userId,
'questionId' => $this->iid,
'sessionId' => $this->sessionId,
'cId' => $this->course['real_id'],
]);
if (!$result) {
return '';
}
$fileName = $result->getFilename();
if (empty($fileName)) {
return '';
}
return $this->storePath.$result->getFilename();
}
}
foreach ($this->available_extensions as $extension) {
$audioFile = $this->storePath.$fileName;
$file = "$audioFile.$extension";
if (is_file($file)) {
return $file;
}
// Function handle_uploaded_document() adds the session and group id by default.
$file = "$audioFile".'__'.$this->sessionId."__0.$extension";
if (is_file($file)) {
return $file;
}
continue;
}
return '';
}
/**
* Get the URL for the audio file. Return null if the file doesn't exists.
*
* @param bool $loadFromDatabase
*
* @return string
*/
public function getFileUrl($loadFromDatabase = false)
{
$filePath = $this->getAbsoluteFilePath($loadFromDatabase);
if (empty($filePath)) {
return null;
}
return str_replace(
api_get_path(SYS_COURSE_PATH),
api_get_path(WEB_COURSE_PATH),
$filePath
);
}
/**
* Tricky stuff to deal with the feedback = 0 in exercises (all question per page).
*
* @param int $exe_id
*/
public function replaceWithRealExe($exe_id)
{
$filename = null;
//ugly fix
foreach ($this->available_extensions as $extension) {
$items = explode('-', $this->fileName);
$items[5] = 'temp_exe';
$filename = implode('-', $items);
if (is_file($this->storePath.$filename.'.'.$extension)) {
$old_name = $this->storePath.$filename.'.'.$extension;
$items = explode('-', $this->fileName);
$items[5] = $exe_id;
$filename = $filename = implode('-', $items);
$new_name = $this->storePath.$filename.'.'.$extension;
rename($old_name, $new_name);
break;
}
}
}
/**
* Generate the necessary directory for audios. If them not exists, are created.
*
* @return string
*/
private function generateDirectory()
{
$this->storePath = api_get_path(SYS_COURSE_PATH).$this->course['path'].'/exercises/';
if (!is_dir($this->storePath)) {
mkdir($this->storePath);
}
if (!is_dir($this->storePath.$this->sessionId)) {
mkdir($this->storePath.$this->sessionId);
}
if (!empty($this->exerciseId) && !is_dir($this->storePath.$this->sessionId.'/'.$this->exerciseId)) {
mkdir($this->storePath.$this->sessionId.'/'.$this->exerciseId);
}
if (!empty($this->iid) && !is_dir($this->storePath.$this->sessionId.'/'.$this->exerciseId.'/'.$this->iid)) {
mkdir($this->storePath.$this->sessionId.'/'.$this->exerciseId.'/'.$this->iid);
}
if (!empty($this->userId) &&
!is_dir($this->storePath.$this->sessionId.'/'.$this->exerciseId.'/'.$this->iid.'/'.$this->userId)
) {
mkdir($this->storePath.$this->sessionId.'/'.$this->exerciseId.'/'.$this->iid.'/'.$this->userId);
}
$params = [
$this->sessionId,
$this->exerciseId,
$this->iid,
$this->userId,
];
$this->storePath .= implode('/', $params).'/';
return $this->storePath;
}
/**
* Generate the file name.
*
* @return string
*/
private function generateFileName()
{
return implode(
'-',
[
$this->course['real_id'],
$this->sessionId,
$this->userId,
$this->exerciseId,
$this->iid,
$this->exeId,
]
);
}
/**
* Generate a relative directory path.
*
* @return string
*/
private function generateRelativeDirectory()
{
$params = [
$this->sessionId,
$this->exerciseId,
$this->iid,
$this->userId,
];
$path = implode('/', $params);
$directory = '/exercises/'.$path.'/';
return $directory;
}
}

611
main/exercise/overview.php Normal file
View File

@@ -0,0 +1,611 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Exercise preview.
*
* @author Julio Montoya <gugli100@gmail.com>
*/
$use_anonymous = true;
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
Exercise::cleanSessionVariables();
$this_section = SECTION_COURSES;
$js = '<script>'.api_get_language_translate_html().'</script>';
$htmlHeadXtra[] = $js;
// Notice for unauthorized people.
api_protect_course_script(true);
$sessionId = api_get_session_id();
$courseCode = api_get_course_id();
$exercise_id = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : 0;
$objExercise = new Exercise();
$result = $objExercise->read($exercise_id, true);
if (!$result) {
api_not_allowed(true);
}
if ('true' === api_get_plugin_setting('positioning', 'tool_enable')) {
$plugin = Positioning::create();
if ($plugin->blockFinalExercise(api_get_user_id(), $exercise_id, api_get_course_int_id(), $sessionId)) {
api_not_allowed(true);
}
}
$learnpath_id = isset($_REQUEST['learnpath_id']) ? (int) $_REQUEST['learnpath_id'] : null;
$learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? (int) $_REQUEST['learnpath_item_id'] : null;
$learnpathItemViewId = isset($_REQUEST['learnpath_item_view_id']) ? (int) $_REQUEST['learnpath_item_view_id'] : null;
$origin = api_get_origin();
$logInfo = [
'tool' => TOOL_QUIZ,
'tool_id' => $exercise_id,
'action' => $learnpath_id ? 'learnpath_id' : '',
'action_details' => $learnpath_id ?: '',
];
Event::registerLog($logInfo);
$interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$interbreadcrumb[] = ['url' => '#', 'name' => Security::remove_XSS($objExercise->selectTitle(true))];
$time_control = false;
$clock_expired_time = ExerciseLib::get_session_time_control_key($objExercise->iid, $learnpath_id, $learnpath_item_id);
if ($objExercise->expired_time != 0 && !empty($clock_expired_time)) {
$time_control = true;
}
$exerciseUrlParams = [
'exerciseId' => $objExercise->iid,
'learnpath_id' => $learnpath_id,
'learnpath_item_id' => $learnpath_item_id,
'learnpath_item_view_id' => $learnpathItemViewId,
];
if (isset($_GET['preview'])) {
$exerciseUrlParams['preview'] = 1;
}
// It is a lti provider
if (isset($_GET['lti_launch_id'])) {
$exerciseUrlParams['lti_launch_id'] = Security::remove_XSS($_GET['lti_launch_id']);
}
$exercise_url = api_get_path(WEB_CODE_PATH).'exercise/exercise_submit.php?'.
api_get_cidreq().'&'.http_build_query($exerciseUrlParams);
if ($time_control) {
// Get time left for expiring time
$time_left = api_strtotime($clock_expired_time, 'UTC') - time();
$htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/stylesheet/jquery.epiclock.css');
$htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/renderers/minute/epiclock.minute.css');
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.dateformat.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.epiclock.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/renderers/minute/epiclock.minute.js');
$htmlHeadXtra[] = $objExercise->showTimeControlJS($time_left, $exercise_url);
}
if (!in_array($origin, ['learnpath', 'embeddable', 'mobileapp', 'iframe'])) {
SessionManager::addFlashSessionReadOnly();
Display::display_header();
} else {
$htmlHeadXtra[] = "
<style>
body { background: none;}
</style>
";
Display::display_reduced_header();
}
if ($origin === 'mobileapp') {
echo '<div class="actions">';
echo '<a href="javascript:window.history.go(-1);">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), [], 32).'</a>';
echo '</div>';
}
$html = '';
$message = '';
$html .= '<div class="exercise-overview">';
$is_allowed_to_edit = api_is_allowed_to_edit(null, true);
$hideIp = api_get_configuration_value('exercise_hide_ip');
$editLink = '';
if ($is_allowed_to_edit) {
if ($objExercise->sessionId == $sessionId) {
$editLink = Display::url(
Display::return_icon('edit.png', get_lang('Edit'), [], ICON_SIZE_SMALL),
api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid
);
}
$editLink .= Display::url(
Display::return_icon('test_results.png', get_lang('Results'), [], ICON_SIZE_SMALL),
api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'.api_get_cidreq().'&exerciseId='.$objExercise->iid,
['title' => get_lang('Results')]
);
}
$iconExercise = Display::return_icon('test-quiz.png', null, [], ICON_SIZE_MEDIUM);
// Exercise name.
if (api_get_configuration_value('save_titles_as_html')) {
$html .= Display::div(
Security::remove_XSS($objExercise->get_formated_title()).PHP_EOL.$editLink
);
} else {
$html .= Display::page_header(
$iconExercise.PHP_EOL.Security::remove_XSS($objExercise->selectTitle()).PHP_EOL.$editLink
);
}
// Exercise description.
if (!empty($objExercise->description)) {
$html .= Display::div(Security::remove_XSS($objExercise->description, COURSEMANAGERLOWSECURITY), ['class' => 'exercise_description']);
}
$exercise_stat_info = $objExercise->get_stat_track_exercise_info(
$learnpath_id,
$learnpath_item_id,
0
);
//1. Check if this is a new attempt or a previous
$label = get_lang('StartTest');
if ($time_control && !empty($clock_expired_time) || isset($exercise_stat_info['exe_id'])) {
$label = get_lang('ContinueTest');
}
if (isset($exercise_stat_info['exe_id'])) {
$message = Display::return_message(get_lang('YouTriedToResolveThisExerciseEarlier'));
}
// 2. Exercise button
// Notice we not add there the lp_item_view_id because is not already generated
$exercise_url_button = Display::url(
$label,
$exercise_url,
['class' => 'btn btn-success btn-large']
);
$btnCheck = '';
$quizCheckButtonEnabled = api_get_configuration_value('quiz_check_button_enable');
if ($quizCheckButtonEnabled) {
$btnCheck = Display::button(
'quiz_check_request_button',
Display::returnFontAwesomeIcon('spinner', '', true, 'fa-spin hidden').' '.get_lang('TestYourBrowser'),
[
'type' => 'button',
'role' => 'button',
'id' => 'quiz-check-request-button',
'class' => 'btn btn-default',
'data-loading-text' => get_lang('Loading'),
'autocomplete' => 'off',
]
).PHP_EOL.'<strong id="quiz-check-request-text"></strong>';
}
// 3. Checking visibility of the exercise (overwrites the exercise button).
$visible_return = $objExercise->is_visible(
$learnpath_id,
$learnpath_item_id,
null,
true,
$sessionId
);
// Exercise is not visible remove the button
if ($visible_return['value'] == false) {
if ($is_allowed_to_edit) {
$message = Display::return_message(
get_lang('ThisItemIsInvisibleForStudentsButYouHaveAccessAsTeacher'),
'warning'
);
} else {
$message = $visible_return['message'];
$exercise_url_button = null;
}
}
$advancedMessage = RemedialCoursePlugin::create()->getAdvancedCourseList(
$objExercise,
api_get_user_id(),
api_get_session_id(),
$learnpath_id ?: 0,
$learnpath_item_id ?: 0
);
if (!empty($advancedMessage)) {
$message .= Display::return_message(
$advancedMessage,
'info',
false
);
}
$remedialMessage = RemedialCoursePlugin::create()->getRemedialCourseList(
$objExercise,
api_get_user_id(),
api_get_session_id(),
false,
$learnpath_id ?: 0,
$learnpath_item_id ?: 0
);
if (null != $remedialMessage) {
$message .= Display::return_message($remedialMessage, 'warning', false);
}
if (!api_is_allowed_to_session_edit()) {
$exercise_url_button = null;
}
$attempts = Event::getExerciseResultsByUser(
api_get_user_id(),
$objExercise->iid,
api_get_course_int_id(),
api_get_session_id(),
$learnpath_id,
$learnpath_item_id,
'desc'
);
$counter = count($attempts);
$my_attempt_array = [];
$table_content = '';
/* Make a special case for IE, which doesn't seem to be able to handle the
* results popup -> send it to the full results page */
$browser = new Browser();
$current_browser = $browser->getBrowser();
$url_suffix = '';
$btn_class = ' ';
if ($current_browser == 'Internet Explorer') {
$url_suffix = '&show_headers=1';
$btn_class = '';
}
$blockShowAnswers = false;
if (in_array(
$objExercise->results_disabled,
[
RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
//RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
])
) {
if (count($attempts) < $objExercise->attempts) {
$blockShowAnswers = true;
}
}
$certificateBlock = '';
if (!empty($attempts)) {
$i = $counter;
foreach ($attempts as $attempt_result) {
if (empty($certificateBlock)) {
$certificateBlock = ExerciseLib::generateAndShowCertificateBlock(
$attempt_result['exe_result'],
$attempt_result['exe_weighting'],
$objExercise,
$attempt_result['exe_user_id'],
$courseCode,
$sessionId
);
}
$score = ExerciseLib::show_score($attempt_result['exe_result'], $attempt_result['exe_weighting']);
$attempt_url = api_get_path(WEB_CODE_PATH).'exercise/result.php?';
$attempt_url .= api_get_cidreq().'&show_headers=1&';
$attempt_url .= http_build_query(['id' => $attempt_result['exe_id']]);
$attempt_url .= $url_suffix;
$attempt_link = Display::url(
get_lang('Show'),
$attempt_url,
[
'class' => $btn_class.'btn btn-default',
'data-title' => get_lang('Show'),
'data-size' => 'lg',
]
);
$teacher_revised = Display::label(get_lang('Validated'), 'success');
if ($attempt_result['attempt_revised'] == 0) {
$teacher_revised = Display::label(get_lang('NotValidated'), 'info');
}
$row = [
'count' => $i,
'date' => api_convert_and_format_date($attempt_result['start_date'], DATE_TIME_FORMAT_LONG),
'userIp' => $attempt_result['user_ip'],
];
if (api_is_anonymous() || $hideIp) {
unset($row['userIp']);
}
$attempt_link .= PHP_EOL.$teacher_revised;
if (in_array(
$objExercise->results_disabled,
[
RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
RESULT_DISABLE_SHOW_SCORE_ONLY,
RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
RESULT_DISABLE_RANKING,
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
]
)) {
$row['result'] = $score;
}
if (in_array(
$objExercise->results_disabled,
[
RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS,
RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING,
RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES,
RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT,
RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK,
RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK,
RESULT_DISABLE_RANKING,
RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER,
]
) || (
$objExercise->results_disabled == RESULT_DISABLE_SHOW_SCORE_ONLY &&
$objExercise->getFeedbackType() == EXERCISE_FEEDBACK_TYPE_END
)
) {
if ($blockShowAnswers &&
$objExercise->results_disabled != RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK
) {
$attempt_link = '';
}
if ($blockShowAnswers == true &&
$objExercise->results_disabled == RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK
) {
if (isset($row['result'])) {
unset($row['result']);
}
}
if (!empty($objExercise->getResultAccess())) {
if (!$objExercise->hasResultsAccess($attempt_result)) {
$attempt_link = '';
}
}
$row['attempt_link'] = $attempt_link;
}
$my_attempt_array[] = $row;
$i--;
}
$header_names = [];
$table = new HTML_Table(['class' => 'table table-striped table-hover']);
// Hiding score and answer.
switch ($objExercise->results_disabled) {
case RESULT_DISABLE_DONT_SHOW_SCORE_ONLY_IF_USER_FINISHES_ATTEMPTS_SHOW_ALWAYS_FEEDBACK:
if ($blockShowAnswers) {
$header_names = [get_lang('Attempt'), get_lang('StartDate'), get_lang('IP'), get_lang('Details')];
} else {
$header_names = [
get_lang('Attempt'),
get_lang('StartDate'),
get_lang('IP'),
get_lang('Score'),
get_lang('Details'),
];
}
break;
case RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT_NO_FEEDBACK:
case RESULT_DISABLE_SHOW_SCORE_ATTEMPT_SHOW_ANSWERS_LAST_ATTEMPT:
if ($blockShowAnswers) {
$header_names = [get_lang('Attempt'), get_lang('StartDate'), get_lang('IP'), get_lang('Score')];
} else {
$header_names = [
get_lang('Attempt'),
get_lang('StartDate'),
get_lang('IP'),
get_lang('Score'),
get_lang('Details'),
];
}
break;
case RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS:
case RESULT_DISABLE_SHOW_SCORE_AND_EXPECTED_ANSWERS_AND_RANKING:
case RESULT_DISABLE_SHOW_FINAL_SCORE_ONLY_WITH_CATEGORIES:
case RESULT_DISABLE_RANKING:
case RESULT_DISABLE_SHOW_ONLY_IN_CORRECT_ANSWER:
$header_names = [
get_lang('Attempt'),
get_lang('StartDate'),
get_lang('IP'),
get_lang('Score'),
get_lang('Details'),
];
break;
case RESULT_DISABLE_NO_SCORE_AND_EXPECTED_ANSWERS:
$header_names = [get_lang('Attempt'), get_lang('StartDate'), get_lang('IP')];
break;
case RESULT_DISABLE_SHOW_SCORE_ONLY:
if ($objExercise->getFeedbackType() != EXERCISE_FEEDBACK_TYPE_END) {
$header_names = [get_lang('Attempt'), get_lang('StartDate'), get_lang('IP'), get_lang('Score')];
} else {
$header_names = [
get_lang('Attempt'),
get_lang('StartDate'),
get_lang('IP'),
get_lang('Score'),
get_lang('Details'),
];
}
break;
}
if (api_is_anonymous() || $hideIp) {
unset($header_names[2]); // It removes the 3rd column related to IP
}
$column = 0;
foreach ($header_names as $item) {
$table->setHeaderContents(0, $column, $item);
$column++;
}
$row = 1;
if (!empty($my_attempt_array)) {
foreach ($my_attempt_array as $data) {
$column = 0;
$table->setCellContents($row, $column, $data);
$table->setRowAttributes($row, null, true);
$column++;
$row++;
}
}
$table_content = $table->toHtml();
}
$selectAttempts = $objExercise->selectAttempts();
if ($selectAttempts) {
$attempt_message = get_lang('Attempts').' '.$counter.' / '.$selectAttempts;
if ($counter == $selectAttempts) {
$attempt_message = Display::return_message($attempt_message, 'error');
} else {
$attempt_message = Display::return_message($attempt_message, 'info');
}
if ($visible_return['value'] == true) {
$message .= $attempt_message;
}
}
if ($time_control) {
$html .= $objExercise->returnTimeLeftDiv();
}
$html .= $message;
$disable = api_get_configuration_value('exercises_disable_new_attempts');
if ($disable && empty($exercise_stat_info)) {
$exercise_url_button = Display::return_message(get_lang('NewExerciseAttemptDisabled'));
}
$isLimitReached = ExerciseLib::isQuestionsLimitPerDayReached(
api_get_user_id(),
count($objExercise->get_validated_question_list()),
api_get_course_int_id(),
api_get_session_id()
);
if (!empty($exercise_url_button) && !$isLimitReached) {
if ($quizCheckButtonEnabled) {
$html .= Display::div(
$btnCheck,
['class' => 'exercise_overview_options']
);
$html .= '<br>';
}
$html .= Display::div(
Display::div(
$exercise_url_button,
['class' => 'exercise_overview_options']
),
['class' => 'options']
);
}
if ($isLimitReached) {
$maxQuestionsAnswered = (int) api_get_course_setting('quiz_question_limit_per_day');
$html .= Display::return_message(
sprintf(get_lang('QuizQuestionsLimitPerDayXReached'), $maxQuestionsAnswered),
'warning',
false
);
}
if (0 == $objExercise->getHideAttemptsTableOnStartPage()) {
$html .= Display::tag(
'div',
$table_content,
['class' => 'table-responsive']
);
}
$html .= '</div>';
if ($certificateBlock) {
$html .= PHP_EOL.$certificateBlock;
}
if ($quizCheckButtonEnabled) {
$quizCheckRequestUrl = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&a=browser_test';
$params = http_build_query(
[
'exe_id' => 1,
'exerciseId' => $exercise_id,
'learnpath_id' => $learnpath_id,
'learnpath_item_id' => $learnpath_item_id,
'learnpath_item_view_id' => $learnpathItemViewId,
'reminder' => '0',
'type' => 'simple',
'question_id' => 23,
'choice[23]' => 45,
]
).'&'.api_get_cidreq();
$html .= "<script>
$(function () {
var btnTest = $('#quiz-check-request-button'),
iconBtnTest = btnTest.children('.fa.fa-spin');
btnTest.on('click', function (e) {
e.preventDefault();
btnTest.prop('disabled', true).removeClass('btn-success btn-danger').addClass('btn-default');
iconBtnTest.removeClass('hidden');
var txtResult = $('#quiz-check-request-text').removeClass('text-success text-danger').hide();
$
.when(
$.ajax({
url: '$quizCheckRequestUrl',
type: 'post',
data: '$params'
}),
$.ajax({
url: '$quizCheckRequestUrl',
type: 'post',
data: '$params&sleep=1'
})
)
.then(
function (xhr1, xhr2) {
var xhr1IsOk = !!xhr1 && xhr1[1] === 'success' && !!xhr1[0] && 'ok' === xhr1[0];
var xhr2IsOk = !!xhr2 && xhr2[1] === 'success' && !!xhr2[0] && 'ok' === xhr2[0];
if (xhr1IsOk && xhr2IsOk) {
btnTest.removeClass('btn-default btn-danger').addClass('btn-success');
txtResult.text(\"".get_lang('QuizBrowserCheckOK')."\").addClass('text-success').show();
} else {
btnTest.removeClass('btn-default btn-success').addClass('btn-danger');
txtResult.text(\"".get_lang('QuizBrowserCheckKO')."\").addClass('text-danger').show();
}
},
function () {
txtResult.text(\"".get_lang('QuizBrowserCheckKO')."\").addClass('text-danger').show();
btnTest.removeClass('btn-default btn-success').addClass('btn-danger');
}
)
.always(function () {
btnTest.prop('disabled', false);
iconBtnTest.addClass('hidden');
});
});
});
</script>";
}
echo $html;
Display::display_footer();

580
main/exercise/pending.php Normal file
View File

@@ -0,0 +1,580 @@
<?php
/* For licensing terms, see /license.txt */
$cidReset = true;
require_once __DIR__.'/../inc/global.inc.php';
// Setting the tabs
$this_section = SECTION_COURSES;
$htmlHeadXtra[] = api_get_jqgrid_js();
$filter_user = isset($_REQUEST['filter_by_user']) ? (int) $_REQUEST['filter_by_user'] : null;
$courseId = isset($_REQUEST['course_id']) ? (int) $_REQUEST['course_id'] : 0;
$exerciseId = isset($_REQUEST['exercise_id']) ? (int) $_REQUEST['exercise_id'] : 0;
$statusId = isset($_REQUEST['status']) ? (int) $_REQUEST['status'] : 0;
$questionTypeId = isset($_REQUEST['questionTypeId']) ? (int) $_REQUEST['questionTypeId'] : 0;
$exportXls = isset($_REQUEST['export_xls']) && !empty($_REQUEST['export_xls']) ? (int) $_REQUEST['export_xls'] : 0;
$action = $_REQUEST['a'] ?? null;
api_block_anonymous_users();
// Only teachers.
if (false === api_is_teacher() && false === api_is_session_admin()) {
api_not_allowed(true);
}
switch ($action) {
case 'get_exercise_by_course':
$data = [];
$results = ExerciseLib::get_all_exercises_for_course_id(
null,
0,
$courseId,
false
);
if (!empty($results)) {
foreach ($results as $exercise) {
$data[] = ['id' => $exercise['iid'], 'text' => html_entity_decode($exercise['title'])];
}
}
echo json_encode($data);
exit;
break;
}
$userId = api_get_user_id();
$origin = api_get_origin();
$TBL_TRACK_EXERCISES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES);
$TBL_TRACK_ATTEMPT = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT);
$TBL_TRACK_ATTEMPT_RECORDING = Database::get_main_table(TABLE_STATISTIC_TRACK_E_ATTEMPT_RECORDING);
$TBL_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
$allowCoachFeedbackExercises = api_get_setting('allow_coach_feedback_exercises') === 'true';
$documentPath = null;
if (!empty($_GET['path'])) {
$parameters['path'] = Security::remove_XSS($_GET['path']);
}
if (!empty($_REQUEST['export_report']) && $_REQUEST['export_report'] == '1') {
if (api_is_platform_admin() || api_is_course_admin() ||
api_is_course_tutor() || api_is_session_general_coach() ||
api_is_session_admin()
) {
$loadExtraData = false;
if (isset($_REQUEST['extra_data']) && $_REQUEST['extra_data'] == 1) {
$loadExtraData = true;
}
$includeAllUsers = false;
if (isset($_REQUEST['include_all_users']) &&
$_REQUEST['include_all_users'] == 1
) {
$includeAllUsers = true;
}
$onlyBestAttempts = false;
if (isset($_REQUEST['only_best_attempts']) &&
$_REQUEST['only_best_attempts'] == 1
) {
$onlyBestAttempts = true;
}
require_once 'exercise_result.class.php';
$export = new ExerciseResult();
$export->setIncludeAllUsers($includeAllUsers);
$export->setOnlyBestAttempts($onlyBestAttempts);
switch ($_GET['export_format']) {
case 'xls':
$export->exportCompleteReportXLS(
$documentPath,
null,
$loadExtraData,
null,
$exerciseId
);
exit;
break;
case 'csv':
default:
$export->exportCompleteReportCSV(
$documentPath,
null,
$loadExtraData,
null,
$exerciseId
);
exit;
break;
}
} else {
api_not_allowed(true);
}
}
$htmlHeadXtra[] = '<script>
$(function() {
$("#export-xls").bind("click", function(e) {
e.preventDefault();
var input = $("<input>", {
type: "hidden",
name: "export_xls",
value: "1"
});
$("#pending").append(input);
$("#pending").submit();
});
$("#pending_pendingSubmit").bind("click", function(e) {
e.preventDefault();
if ($("input[name=\"export_xls\"]").length > 0) {
$("input[name=\"export_xls\"]").remove();
}
$("#pending").submit();
});
$("select#course_id").on("change", function () {
var courseId = parseInt(this.value, 10);
updateExerciseList(courseId);
});
});
function updateExerciseList(courseId) {
if (courseId == 0) {
return;
}
var $selectExercise = $("select#exercise_id");
$selectExercise.empty();
$.get("'.api_get_self().'", {
a: "get_exercise_by_course",
course_id: courseId,
}, function (exerciseList) {
$("<option>", {
value: 0,
text: "'.get_lang('All').'"
}).appendTo($selectExercise);
if (exerciseList.length > 0) {
$.each(exerciseList, function (index, exercise) {
$("<option>", {
value: exercise.id,
text: exercise.text
}).appendTo($selectExercise);
});
$selectExercise.find("option[value=\''.$exerciseId.'\']").attr("selected",true);
}
$selectExercise.selectpicker("refresh");
}, "json");
}
</script>';
if ($exportXls) {
ExerciseLib::exportPendingAttemptsToExcel($_REQUEST);
}
Display::display_header(get_lang('PendingAttempts'));
$actions = '';
$actions .= Display::url(
Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
'#',
['id' => 'export-xls']
);
echo Display::div($actions, ['class' => 'actions']);
$token = Security::get_token();
$extra = '<script>
$(function() {
$( "#dialog:ui-dialog" ).dialog( "destroy" );
$( "#dialog-confirm" ).dialog({
autoOpen: false,
show: "blind",
resizable: false,
height:300,
modal: true
});
$("#export_opener").click(function() {
var targetUrl = $(this).attr("href");
$( "#dialog-confirm" ).dialog({
width:400,
height:300,
buttons: {
"'.addslashes(get_lang('Download')).'": function() {
var export_format = $("input[name=export_format]:checked").val();
var extra_data = $("input[name=load_extra_data]:checked").val();
var includeAllUsers = $("input[name=include_all_users]:checked").val();
var attempts = $("input[name=only_best_attempts]:checked").val();
location.href = targetUrl+"&export_format="+export_format+"&extra_data="+extra_data+"&include_all_users="+includeAllUsers+"&only_best_attempts="+attempts;
$( this ).dialog( "close" );
}
}
});
$( "#dialog-confirm" ).dialog("open");
return false;
});
});
</script>';
$extra .= '<div id="dialog-confirm" title="'.get_lang('ConfirmYourChoice').'">';
$form = new FormValidator(
'report',
'post',
null,
null,
['class' => 'form-vertical']
);
$form->addElement(
'radio',
'export_format',
null,
get_lang('ExportAsCSV'),
'csv',
['id' => 'export_format_csv_label']
);
$form->addElement(
'radio',
'export_format',
null,
get_lang('ExportAsXLS'),
'xls',
['id' => 'export_format_xls_label']
);
$form->addElement(
'checkbox',
'load_extra_data',
null,
get_lang('LoadExtraData'),
'0',
['id' => 'export_format_xls_label']
);
$form->addElement(
'checkbox',
'include_all_users',
null,
get_lang('IncludeAllUsers'),
'0'
);
$form->addElement(
'checkbox',
'only_best_attempts',
null,
get_lang('OnlyBestAttempts'),
'0'
);
$form->setDefaults(['export_format' => 'csv']);
$extra .= $form->returnForm();
$extra .= '</div>';
echo $extra;
$showAttemptsInSessions = api_get_configuration_value('show_exercise_attempts_in_all_user_sessions');
$courses = CourseManager::get_courses_list_by_user_id($userId, $showAttemptsInSessions, false, false);
$form = new FormValidator('pending', 'GET');
$courses = array_column($courses, 'title', 'real_id');
$form->addSelect('course_id', get_lang('Course'), $courses, ['placeholder' => get_lang('All'), 'id' => 'course_id']);
$form->addSelect(
'exercise_id',
get_lang('Exercise'),
[],
[
'placeholder' => get_lang('All'),
'id' => 'exercise_id',
]
);
$status = [
1 => get_lang('All'),
2 => get_lang('Validated'),
3 => get_lang('NotValidated'),
4 => get_lang('Unclosed'),
5 => get_lang('Ongoing'),
];
$form->addSelect('status', get_lang('Status'), $status);
$questionType = [
0 => get_lang('All'),
1 => get_lang('QuestionsWithNoAutomaticCorrection'),
];
$form->addSelect('questionTypeId', get_lang('QuestionType'), $questionType);
$form->addButtonSearch(get_lang('Search'), 'pendingSubmit');
$content = $form->returnForm();
echo $content;
if (empty($statusId)) {
Display::display_footer();
exit;
}
$url = api_get_path(WEB_AJAX_PATH).
'model.ajax.php?a=get_exercise_pending_results&filter_by_user='.$filter_user.
'&course_id='.$courseId.'&exercise_id='.$exerciseId.'&status='.$statusId.'&questionType='.$questionTypeId.'&showAttemptsInSessions='.$showAttemptsInSessions;
$action_links = '';
$officialCodeInList = api_get_setting('show_official_code_exercise_result_list');
// The order is important you need to check the the $column variable in the model.ajax.php file
$columns = [
get_lang('Course'),
get_lang('Exercise'),
get_lang('FirstName'),
get_lang('LastName'),
get_lang('LoginName'),
get_lang('Duration').' ('.get_lang('MinMinute').')',
get_lang('StartDate'),
get_lang('EndDate'),
get_lang('Score'),
get_lang('IP'),
get_lang('Status'),
get_lang('Corrector'),
get_lang('CorrectionDate'),
get_lang('Actions'),
];
if ($officialCodeInList === 'true') {
$columns = array_merge([get_lang('OfficialCode')], $columns);
}
// Column config
$column_model = [
['name' => 'course', 'index' => 'course', 'width' => '50', 'align' => 'left', 'search' => 'false', 'sortable' => 'false'],
['name' => 'exercise', 'index' => 'exercise', 'width' => '50', 'align' => 'left', 'search' => 'false', 'sortable' => 'false'],
['name' => 'firstname', 'index' => 'firstname', 'width' => '50', 'align' => 'left', 'search' => 'true'],
[
'name' => 'lastname',
'index' => 'lastname',
'width' => '50',
'align' => 'left',
'formatter' => 'action_formatter',
'search' => 'true',
],
[
'name' => 'login',
'index' => 'username',
'width' => '40',
'align' => 'left',
'search' => 'true',
'hidden' => api_get_configuration_value('exercise_attempts_report_show_username') ? 'false' : 'true',
],
['name' => 'duration', 'index' => 'exe_duration', 'width' => '30', 'align' => 'left', 'search' => 'true'],
['name' => 'start_date', 'index' => 'start_date', 'width' => '60', 'align' => 'left', 'search' => 'true'],
['name' => 'exe_date', 'index' => 'exe_date', 'width' => '60', 'align' => 'left', 'search' => 'true'],
['name' => 'score', 'index' => 'exe_result', 'width' => '50', 'align' => 'center', 'search' => 'true'],
['name' => 'ip', 'index' => 'user_ip', 'width' => '40', 'align' => 'center', 'search' => 'true'],
[
'name' => 'status',
'index' => 'revised',
'width' => '40',
'align' => 'left',
'search' => 'false',
'sortable' => 'false',
//'stype' => 'select',
//for the bottom bar
/*'searchoptions' => [
'defaultValue' => '',
'value' => ':'.get_lang('All').';1:'.get_lang('Validated').';0:'.get_lang('NotValidated'),
],*/
//for the top bar
/*'editoptions' => [
'value' => ':'.get_lang('All').';1:'.get_lang('Validated').';0:'.get_lang('NotValidated'),
],*/
],
[
'name' => 'qualificator_fullname',
'index' => 'qualificator_fullname',
'width' => '20',
'align' => 'left',
'search' => 'true',
],
[
'name' => 'date_of_qualification',
'index' => 'date_of_qualification',
'width' => '20',
'align' => 'left',
'search' => 'true',
],
[
'name' => 'actions',
'index' => 'actions',
'width' => '60',
'align' => 'left',
'search' => 'false',
'sortable' => 'false',
],
];
if ('true' === $officialCodeInList) {
$officialCodeRow = [
'name' => 'official_code',
'index' => 'official_code',
'width' => '50',
'align' => 'left',
'search' => 'true',
];
$column_model = array_merge([$officialCodeRow], $column_model);
}
$action_links = '
// add username as title in lastname filed - ref 4226
function action_formatter(cellvalue, options, rowObject) {
// rowObject is firstname,lastname,login,... get the third word
var loginx = "'.api_htmlentities(sprintf(get_lang('LoginX'), ':::'), ENT_QUOTES).'";
var tabLoginx = loginx.split(/:::/);
// tabLoginx[0] is before and tabLoginx[1] is after :::
// may be empty string but is defined
return "<span title=\""+tabLoginx[0]+rowObject[2]+tabLoginx[1]+"\">"+cellvalue+"</span>";
}';
$extra_params['autowidth'] = 'true';
$extra_params['height'] = 'auto';
$gridJs = Display::grid_js(
'results',
$url,
$columns,
$column_model,
$extra_params,
[],
$action_links,
true
);
?>
<script>
function exportExcel()
{
var mya = $("#results").getDataIDs(); // Get All IDs
var data = $("#results").getRowData(mya[0]); // Get First row to get the labels
var colNames = new Array();
var ii = 0;
for (var i in data) {
colNames[ii++] = i;
}
var html = "";
for (i = 0; i < mya.length; i++) {
data = $("#results").getRowData(mya[i]); // get each row
for (j = 0; j < colNames.length; j++) {
html = html + data[colNames[j]] + ","; // output each column as tab delimited
}
html = html + "\n"; // output each row with end of line
}
html = html + "\n"; // end of line at the end
var form = $("#export_report_form");
$("#csvBuffer").attr('value', html);
form.target='_blank';
form.submit();
}
$(function() {
$("#datepicker_start").datepicker({
defaultDate: "",
changeMonth: false,
numberOfMonths: 1
});
var $selectCourse = $("select#course_id");
$selectCourse.on("change", function () {
var courseId = parseInt(this.value, 10);
updateExerciseList(courseId);
});
var courseId = $selectCourse.val() ? $selectCourse.val() : 0;
updateExerciseList(courseId);
<?php
echo $gridJs;
?>
$("#results").jqGrid(
'navGrid',
'#results_pager', {
view:true, edit:false, add:false, del:false, excel:false
},
{height:280, reloadAfterSubmit:false}, // view options
{height:280, reloadAfterSubmit:false}, // edit options
{height:280, reloadAfterSubmit:false}, // add options
{reloadAfterSubmit: false}, // del options
{width:500}, // search options
);
var sgrid = $("#results")[0];
// Adding search options
var options = {
'stringResult': true,
'autosearch' : true,
'searchOnEnter': false,
}
jQuery("#results").jqGrid('filterToolbar', options);
sgrid.triggerToolbar();
$('#results').on('click', 'a.exercise-recalculate', function (e) {
e.preventDefault();
if (!$(this).data('user') || !$(this).data('exercise') || !$(this).data('id')) {
return;
}
var url = '<?php echo api_get_path(WEB_CODE_PATH); ?>exercise/recalculate.php?<?php echo api_get_cidreq(); ?>';
var recalculateXhr = $.post(url, $(this).data());
$.when(recalculateXhr).done(function (response) {
$('#results').trigger('reloadGrid');
});
});
});
// datepicker functions
var datapickerInputModified = false;
/**
* return true if the datepicker input has been modified
*/
function datepicker_input_changed() {
datapickerInputModified = true;
}
/**
* disply the datepicker calendar on mouse over the input
*/
function datepicker_input_mouseover() {
$('#datepicker_start').datepicker( "show" );
}
/**
* display or hide the datepicker input, calendar and button
*/
function display_date_picker() {
if (!$('#datepicker_span').is(":visible")) {
$('#datepicker_span').show();
$('#datepicker_start').datepicker( "show" );
} else {
$('#datepicker_start').datepicker( "hide" );
$('#datepicker_span').hide();
}
}
/**
* confirm deletion
*/
function submit_datepicker() {
if (datapickerInputModified) {
var dateTypeVar = $('#datepicker_start').datepicker('getDate');
var dateForBDD = $.datepicker.formatDate('yy-mm-dd', dateTypeVar);
// Format the date for confirm box
var dateFormat = $( "#datepicker_start" ).datepicker( "option", "dateFormat" );
var selectedDate = $.datepicker.formatDate(dateFormat, dateTypeVar);
if (confirm("<?php echo convert_double_quote_to_single(get_lang('AreYouSureDeleteTestResultBeforeDateD')).' '; ?>" + selectedDate)) {
self.location.href = "exercise_report.php?<?php echo api_get_cidreq(); ?>&exerciseId=<?php echo $exerciseId; ?>&delete_before_date="+dateForBDD+"&sec_token=<?php echo $token; ?>";
}
}
}
</script>
<form id="export_report_form" method="post" action="exercise_report.php?<?php echo api_get_cidreq(); ?>">
<input type="hidden" name="csvBuffer" id="csvBuffer" value="" />
<input type="hidden" name="export_report" id="export_report" value="1" />
<input type="hidden" name="exerciseId" id="exerciseId" value="<?php echo $exerciseId; ?>" />
</form>
<?php
echo Display::grid_html('results');
Display::display_footer();

105
main/exercise/qti2.php Normal file
View File

@@ -0,0 +1,105 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Code for Qti2 import integration.
*
* @author Ronny Velasquez
*
* @version $Id: qti2.php 2010-03-12 12:14:25Z $
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
// section (for the tabs)
$this_section = SECTION_COURSES;
// access restriction: only teachers are allowed here
if (!api_is_allowed_to_edit(null, true)) {
api_not_allowed(true);
}
// the breadcrumbs
$interbreadcrumb[] = [
'url' => api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$is_allowedToEdit = api_is_allowed_to_edit(null, true);
/**
* This function displays the form to import the zip file with qti2.
*/
function displayForm()
{
$form = '<div class="actions">';
$form .= '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?show=test&'.api_get_cidreq().'">'.
Display::return_icon('back.png', get_lang('BackToExercisesList'), '', ICON_SIZE_MEDIUM).'</a>';
$form .= '</div>';
$formValidator = new FormValidator(
'qti_upload',
'post',
api_get_self().'?'.api_get_cidreq(),
null,
['enctype' => 'multipart/form-data']
);
$formValidator->addHeader(get_lang('ImportQtiQuiz'));
$formValidator->addElement('file', 'userFile', get_lang('DownloadFile'));
$formValidator->addButtonImport(get_lang('Upload'));
$form .= $formValidator->returnForm();
echo $form;
}
/**
* This function will import the zip file with the respective qti2.
*
* @param array $array_file ($_FILES)
*
* @return string|array
*/
function importFile($array_file)
{
$unzip = 0;
$process = process_uploaded_file($array_file, false);
if (preg_match('/\.zip$/i', $array_file['name'])) {
// if it's a zip, allow zip upload
$unzip = 1;
}
if ($process && $unzip == 1) {
$main_path = api_get_path(SYS_CODE_PATH);
require_once $main_path.'exercise/export/exercise_import.inc.php';
require_once $main_path.'exercise/export/qti2/qti2_classes.php';
return import_exercise($array_file['name']);
}
return 'FileError';
}
$message = null;
// import file
if (api_is_allowed_to_edit(null, true)) {
if (isset($_POST['submit'])) {
$imported = importFile($_FILES['userFile']);
if (is_numeric($imported) && !empty($imported)) {
header('Location: '.api_get_path(WEB_CODE_PATH).'exercise/admin.php?'.api_get_cidreq().'&exerciseId='.$imported);
exit;
} else {
$message = Display::return_message(get_lang($imported));
}
}
}
Display::display_header(get_lang('ImportQtiQuiz'), 'Exercise');
echo $message;
// display qti form
displayForm();
Display::display_footer();

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,103 @@
<?php
/* For licensing terms, see /license.txt */
/**
* This script allows to manage the statements of questions.
* It is included from the script admin.php.
*
* @author Olivier Brouckaert
*/
if (isset($_GET['editQuestion'])) {
$objQuestion = Question::read($_GET['editQuestion']);
$action = api_get_self().'?'.api_get_cidreq().'&modifyQuestion='.$modifyQuestion.'&editQuestion='.$objQuestion->iid.'&page='.$page;
} else {
$objQuestion = Question::getInstance($_REQUEST['answerType']);
$action = api_get_self().'?'.api_get_cidreq().'&modifyQuestion='.$modifyQuestion.'&newQuestion='.$newQuestion;
}
if (is_object($objQuestion)) {
// FORM CREATION
$form = new FormValidator('question_admin_form', 'post', $action);
if (isset($_GET['editQuestion'])) {
$class = 'btn btn-default';
$text = get_lang('ModifyQuestion');
$type = isset($_GET['type']) ? Security::remove_XSS($_GET['type']) : null;
} else {
$class = 'btn btn-default';
$text = get_lang('AddQuestionToExercise');
$type = $_REQUEST['answerType'];
}
$typesInformation = Question::getQuestionTypeList();
$form_title_extra = isset($typesInformation[$type][1]) ? get_lang($typesInformation[$type][1]) : null;
$code = '';
if (isset($objQuestion->code) && !empty($objQuestion->code)) {
$code = ' ('.$objQuestion->code.')';
}
// form title
$form->addHeader($text.': '.$form_title_extra.$code);
// question form elements
$objQuestion->createForm($form, $objExercise);
// answer form elements
$objQuestion->createAnswersForm($form);
// this variable $show_quiz_edition comes from admin.php blocks the exercise/quiz modifications
if (!empty($objExercise->iid) && $objExercise->edit_exercise_in_lp == false) {
$form->freeze();
}
// FORM VALIDATION
if (isset($_POST['submitQuestion'])) {
$validationResult = true;
if (method_exists($objQuestion, 'validateAnswers')) {
$validationResult = $objQuestion->validateAnswers($form);
}
if (is_array($validationResult) && !empty($validationResult['errors'])) {
echo Display::return_message(implode("<br>", $validationResult['errors']), 'error', false);
} elseif ($form->validate()) {
$objQuestion->processCreation($form, $objExercise);
$objQuestion->processAnswersCreation($form, $objExercise);
if (in_array($objQuestion->type, [HOT_SPOT, HOT_SPOT_COMBINATION, HOT_SPOT_DELINEATION])) {
echo '<script type="text/javascript">window.location.href="admin.php?exerciseId='.$exerciseId.'&page='.$page.'&hotspotadmin='.$objQuestion->iid.'&'.api_get_cidreq().'";</script>';
} elseif (in_array($objQuestion->type, [MULTIPLE_ANSWER_DROPDOWN, MULTIPLE_ANSWER_DROPDOWN_COMBINATION])) {
$url = 'admin.php?'.api_get_cidreq().'&'.http_build_query(['exerciseId' => $exerciseId, 'page' => $page, 'mad_admin' => $objQuestion->iid]);
echo '<script type="text/javascript">window.location.href="'.$url.'";</script>';
} else {
if (isset($_GET['editQuestion'])) {
if (empty($exerciseId)) {
Display::addFlash(Display::return_message(get_lang('ItemUpdated')));
$url = 'admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'&editQuestion='.$objQuestion->iid;
echo '<script type="text/javascript">window.location.href="'.$url.'";</script>';
exit;
}
echo '<script type="text/javascript">window.location.href="admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'&page='.$page.'&message=ItemUpdated";</script>';
} else {
// New question
$page = 1;
$length = api_get_configuration_value('question_pagination_length');
if (!empty($length)) {
$page = round($objExercise->getQuestionCount() / $length);
}
echo '<script type="text/javascript">window.location.href="admin.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'&page='.$page.'&message=ItemAdded";</script>';
}
}
exit();
}
}
if (isset($questionName)) {
echo '<h3>'.$questionName.'</h3>';
}
if (!empty($pictureName)) {
echo '<img src="../document/download.php?doc_url=%2Fimages%2F'.$pictureName.'" border="0">';
}
if (!empty($msgErr)) {
echo Display::return_message($msgErr);
}
$form->display();
}

View File

@@ -0,0 +1,125 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
// the section (tabs)
$this_section = SECTION_COURSES;
// notice for unauthorized people.
api_protect_course_script(true);
$allow = api_is_allowed_to_edit();
if (!$allow) {
api_not_allowed(true);
}
// breadcrumbs
$interbreadcrumb[] = ["url" => "exercise.php", "name" => get_lang('Exercises')];
// Tool name
$nameTools = get_lang('AddQuestionToExercise');
// The form
$form = new FormValidator('add_question', 'post', api_get_self().'?'.api_get_cidreq());
// form title
$form->addElement('header', '', get_lang('AddQuestionToExercise'));
$question_list = Question::getQuestionTypeList();
$question_list_options = [];
foreach ($question_list as $key => $value) {
$question_list_options[$key] = addslashes(get_lang($value[1]));
}
$form->addElement(
'select',
'question_type_hidden',
get_lang('QuestionType'),
$question_list_options,
['id' => 'question_type_hidden']
);
//session id
$session_id = api_get_session_id();
// the exercises
$tbl_exercises = Database::get_course_table(TABLE_QUIZ_TEST);
$course_id = api_get_course_int_id();
$sql = "SELECT iid, title, type, description, results_disabled
FROM $tbl_exercises
WHERE c_id = $course_id AND active<>'-1' AND session_id = ".$session_id."
ORDER BY title ASC";
$result = Database::query($sql);
$exercises['-'] = '-'.get_lang('SelectExercise').'-';
while ($row = Database::fetch_array($result)) {
$exercises[$row['iid']] = cut($row['title'], EXERCISE_MAX_NAME_SIZE);
}
$form->addElement('select', 'exercise', get_lang('Exercise'), $exercises);
// generate default content
$form->addElement(
'checkbox',
'is_content',
null,
get_lang('GenerateDefaultContent'),
['checked' => true]
);
// the submit button
$form->addButtonCreate(get_lang('CreateQuestion'), 'SubmitCreateQuestion');
// setting the rules
$form->addRule('exercise', get_lang('ThisFieldIsRequired'), 'required');
$form->addRule('exercise', get_lang('YouHaveToSelectATest'), 'numeric');
$form->registerRule('validquestiontype', 'callback', 'check_question_type');
$form->addRule('question_type_hidden', get_lang('InvalidQuestionType'), 'validquestiontype');
if ($form->validate()) {
$values = $form->exportValues();
$answer_type = $values['question_type_hidden'];
// check feedback_type from current exercise for type of question delineation
$exercise_id = (int) $values['exercise'];
$sql = "SELECT feedback_type FROM $tbl_exercises WHERE iid = $exercise_id";
$rs_feedback_type = Database::query($sql);
$row_feedback_type = Database::fetch_row($rs_feedback_type);
$feedback_type = $row_feedback_type[0];
// if question type does not belong to self-evaluation (immediate feedback) it'll send an error
if (($answer_type == HOT_SPOT_DELINEATION && $feedback_type != 1) ||
($feedback_type == 1 && ($answer_type != HOT_SPOT_DELINEATION && $answer_type != UNIQUE_ANSWER))) {
header('Location: question_create.php?'.api_get_cidreq().'&error=true');
exit;
}
header('Location: admin.php?exerciseId='.$values['exercise'].'&newQuestion=yes&isContent='.$values['is_content'].'&answerType='.$answer_type);
exit;
} else {
// header
Display::display_header($nameTools);
echo '<div class="actions">';
echo '<a href="exercise.php?show=test">'.Display::return_icon('back.png', get_lang('BackToExercisesList'), '', ICON_SIZE_MEDIUM).'</a>';
echo '</div>';
// displaying the form
$form->display();
// footer
Display::display_footer();
}
function check_question_type($parameter)
{
$question_list = Question::getQuestionTypeList();
foreach ($question_list as $key => $value) {
$valid_question_types[] = $key;
}
if (in_array($parameter, $valid_question_types)) {
return true;
} else {
return false;
}
}

View File

@@ -0,0 +1,442 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* @author Olivier Brouckaert & Julio Montoya & Hubert Borderiou 21-10-2011 (Question by category)
* QUESTION LIST ADMINISTRATION
*
* This script allows to manage the question list
* It is included from the script admin.php
*/
$limitTeacherAccess = api_get_configuration_value('limit_exercise_teacher_access');
$allowInterCourseLinking = api_get_configuration_value('quiz_question_allow_inter_course_linking');
// deletes a question from the exercise (not from the data base)
if ($deleteQuestion) {
if ($limitTeacherAccess && !api_is_platform_admin()) {
exit;
}
// if the question exists
if ($objQuestionTmp = Question::read($deleteQuestion)) {
if ($allowInterCourseLinking === true) {
$masterExerciseId = Question::getMasterQuizForQuestion($objQuestionTmp->iid);
if ($masterExerciseId != $exerciseId) {
$objQuestionTmp->delete($exerciseId);
} else {
$objQuestionTmp->delete();
}
} else {
$objQuestionTmp->delete($exerciseId);
}
// if the question has been removed from the exercise
if ($objExercise->removeFromList($deleteQuestion)) {
$nbrQuestions--;
}
}
// destruction of the Question object
unset($objQuestionTmp);
}
$ajax_url = api_get_path(WEB_AJAX_PATH).'exercise.ajax.php?'.api_get_cidreq().'&exercise_id='.intval($exerciseId);
$addImageUrl = api_get_path(WEB_CODE_PATH).'inc/lib/elfinder/filemanager.php?add_image=1&'.api_get_cidreq();
?>
<div id="dialog-confirm"
title="<?php echo get_lang('ConfirmYourChoice'); ?>"
style="display:none;">
<p>
<?php echo get_lang('AreYouSureToDelete'); ?>
</p>
</div>
<script>
function addImageToQuestion(image, questionId) {
let url = '<?php echo $ajax_url; ?>' + '&a=save_question_description&question_id=' + questionId;
let params = {
image: image
}
$.ajax({
url: url,
data: params,
success: function (data) {
window.alert('<?php echo addslashes(get_lang('Updated')); ?>');
}
});
}
function loadEditor(button, questionId) {
event.preventDefault();
let url = '<?php echo $addImageUrl; ?>' + '&question_id=' + questionId;
let w = 850;
let h = 500;
let left = (screen.width / 2) - (w / 2);
let top = (screen.height / 2) - (h / 2);
let params = 'height=' + h + ',width=' + w + ',resizable=0,top=' + top + ',left=' + left;
setTimeout(() => window.open(url, 'editor', params), 1000);
return;
}
$(function () {
$("#dialog:ui-dialog").dialog("destroy");
$("#dialog-confirm").dialog({
autoOpen: false,
show: "blind",
resizable: false,
height: 150,
modal: false
});
$(".opener").click(function () {
var targetUrl = $(this).attr("href");
var otherQuizs = $(this).data("otherquizs");
if (otherQuizs == 1) {
$("#dialog-confirm p").text("<?php echo get_lang('QuestionInOtherExercises'); ?>")
}
$("#dialog-confirm").dialog({
modal: true,
buttons: {
"<?php echo get_lang('Yes'); ?>": function () {
location.href = targetUrl;
$(this).dialog("close");
},
"<?php echo get_lang('No'); ?>": function () {
$(this).dialog("close");
}
}
});
$("#dialog-confirm").dialog("open");
return false;
});
var stop = false;
$("#question_list h3").click(function (event) {
if (stop) {
event.stopImmediatePropagation();
event.preventDefault();
stop = false;
}
});
/* We can add links in the accordion header */
$(".btn-actions .edition a.btn").click(function () {
//Avoid the redirecto when selecting the delete button
if (this.id.indexOf('delete') == -1) {
newWind = window.open(this.href, "_self");
newWind.focus();
return false;
}
});
$("#question_list").accordion({
icons: null,
heightStyle: "content",
active: false, // all items closed by default
collapsible: true,
header: ".header_operations",
beforeActivate: function (e, ui) {
var data = ui.newHeader.data();
if (typeof data === 'undefined') {
return;
}
var exerciseId = data.exercise || 0,
questionId = data.question || 0;
if (!questionId || !exerciseId) {
return;
}
var $pnlQuestion = $('#pnl-question-' + questionId);
if ($pnlQuestion.html().trim().length) {
return;
}
$pnlQuestion.html('<span class="fa fa-spinner fa-spin fa-3x fa-fw" aria-hidden="true"></span>');
$.get('<?php echo api_get_path(WEB_AJAX_PATH); ?>exercise.ajax.php?<?php echo api_get_cidreq(); ?>', {
a: 'show_question',
exercise: exerciseId,
question: questionId
}, function (response) {
$pnlQuestion.html(response)
});
}
})
.sortable({
cursor: "move", // works?
update: function (event, ui) {
var order = $(this).sortable("serialize") + "&a=update_question_order&exercise_id=<?php echo $exerciseId; ?>";
$.post("<?php echo $ajax_url; ?>", order, function (result) {
$("#message").html(result);
});
},
axis: "y",
placeholder: "ui-state-highlight", //defines the yellow highlight
handle: ".moved", //only the class "moved"
stop: function () {
stop = true;
}
});
});
</script>
<?php
// Filter the type of questions we can add
Question::displayTypeMenu($objExercise);
echo '<div id="message"></div>';
$token = Security::get_token();
//deletes a session when using don't know question type (ugly fix)
Session::erase('less_answer');
// If we are in a test
$inATest = isset($exerciseId) && $exerciseId > 0;
if (!$inATest) {
echo Display::return_message(get_lang('ChoiceQuestionType'), 'warning');
} else {
if ($nbrQuestions) {
// In the building exercise mode show question list ordered as is.
$objExercise->setCategoriesGrouping(false);
$originalQuestionSelectType = $objExercise->questionSelectionType;
// In building mode show all questions not render by teacher order.
$objExercise->questionSelectionType = EX_Q_SELECTION_ORDERED;
$allowQuestionOrdering = true;
$showPagination = api_get_configuration_value('show_question_pagination');
if (!empty($showPagination) && $nbrQuestions > $showPagination) {
$length = api_get_configuration_value('question_pagination_length');
$url = api_get_self().'?'.api_get_cidreq();
// Use pagination for exercise with more than 200 questions.
$allowQuestionOrdering = false;
$start = ($page - 1) * $length;
$questionList = $objExercise->getQuestionForTeacher($start, $length);
$paginator = new Knp\Component\Pager\Paginator();
$pagination = $paginator->paginate([]);
$pagination->setTotalItemCount($nbrQuestions);
$pagination->setItemNumberPerPage($length);
$pagination->setCurrentPageNumber($page);
$pagination->renderer = function ($data) use ($url) {
$render = '<ul class="pagination">';
for ($i = 1; $i <= $data['pageCount']; $i++) {
$pageContent = '<li><a href="'.$url.'&page='.$i.'">'.$i.'</a></li>';
if ($data['current'] == $i) {
$pageContent = '<li class="active"><a href="#" >'.$i.'</a></li>';
}
$render .= $pageContent;
}
$render .= '</ul>';
return $render;
};
echo $pagination;
} else {
// Teacher view, see all questions.
//$questionList = $objExercise->selectQuestionList(true, true);
$questionList = $objExercise->getQuestionOrderedList(true);
}
// Restore original value
$objExercise->questionSelectionType = $originalQuestionSelectType;
echo '
<div class="row hidden-xs">
<div class="col-sm-5"><strong>'.get_lang('Questions').'</strong></div>
<div class="col-sm-1 text-center"><strong>'.get_lang('Type').'</strong></div>
<div class="col-sm-2"><strong>'.get_lang('Category').'</strong></div>
<div class="col-sm-1 text-right"><strong>'.get_lang('Difficulty').'</strong></div>
<div class="col-sm-1 text-right"><strong>'.get_lang('MaximumScore').'</strong></div>
<div class="col-sm-2 text-right"><strong>'.get_lang('Actions').'</strong></div>
</div>
<div id="question_list">
';
$category_list = TestCategory::getListOfCategoriesNameForTest($objExercise->iid, false);
if (is_array($questionList)) {
foreach ($questionList as $id) {
// To avoid warning messages.
if (!is_numeric($id)) {
continue;
}
/** @var Question $objQuestionTmp */
$objQuestionTmp = Question::read($id);
if (empty($objQuestionTmp)) {
continue;
}
$addImageLink = '';
if (api_get_configuration_value('allow_quick_question_description_popup')) {
$addImageLink = Display::url(
Display::return_icon(
'image.png',
get_lang('AddImage'),
[],
ICON_SIZE_TINY
),
'javascript:void(0);',
['class' => 'btn btn-default btn-sm ajax', 'onclick' => 'loadEditor(this, '.$id.')']
);
}
$clone_link = null;
if ($objExercise->edit_exercise_in_lp == true) {
$clone_link = Display::url(
Display::return_icon(
'cd.png',
get_lang('Copy'),
[],
ICON_SIZE_TINY
),
api_get_self().'?'.api_get_cidreq().'&clone_question='.$id.'&page='.$page,
['class' => 'btn btn-default btn-sm']
);
}
$edit_link = $objQuestionTmp->selectType() == CALCULATED_ANSWER && $objQuestionTmp->isAnswered()
? Display::span(
Display::return_icon(
'edit_na.png',
get_lang(
'QuestionEditionNotAvailableBecauseItIsAlreadyAnsweredHoweverYouCanCopyItAndModifyTheCopy'
),
[],
ICON_SIZE_TINY
),
['class' => 'btn btn-default btn-sm']
)
: Display::url(
Display::return_icon(
'edit.png',
get_lang('Modify'),
[],
ICON_SIZE_TINY
),
api_get_self().'?'.api_get_cidreq().'&'
.http_build_query([
'type' => $objQuestionTmp->selectType(),
'editQuestion' => $id,
'page' => $page,
]),
['class' => 'btn btn-default btn-sm']
);
$delete_link = null;
if (!$limitTeacherAccess && api_is_allowed_to_edit() && $objExercise->edit_exercise_in_lp) {
$questionInOtherQuizs = 0;
$results = Question::countQuizzesUsingQuestion($id);
if ($results > 1) {
$masterExerciseId = Question::getMasterQuizForQuestion($id);
if ($masterExerciseId !== $exerciseId) {
$questionInOtherQuizs = 1;
}
}
$delete_link = Display::url(
Display::return_icon(
'delete.png',
get_lang('RemoveFromTest'),
[],
ICON_SIZE_TINY
),
api_get_self().'?'.api_get_cidreq().'&'
.http_build_query([
'exerciseId' => $exerciseId,
'deleteQuestion' => $id,
'page' => $page,
]),
[
'id' => "delete_$id",
'class' => 'opener btn btn-default btn-sm',
'data-otherquizs' => $questionInOtherQuizs,
]
);
}
$btnActions = implode(
PHP_EOL,
[$edit_link, $clone_link, $addImageLink, $delete_link]
);
$title = Security::remove_XSS(strip_tags($objQuestionTmp->selectTitle()));
$move = '&nbsp;';
if ($allowQuestionOrdering) {
$move = Display::returnFontAwesomeIcon('arrows moved', 1, true);
}
// Question name
$questionName =
'<a href="#" title = "'.$title.'">
'.$move.' '.cut($title, 42).'
</a>';
// Question type
$typeImg = $objQuestionTmp->getTypePicture();
$typeExpl = $objQuestionTmp->getExplanation();
$questionType = Display::return_icon($typeImg, $typeExpl);
// Question category
$txtQuestionCat = Security::remove_XSS(
TestCategory::getCategoryNameForQuestion($objQuestionTmp->iid)
);
if (empty($txtQuestionCat)) {
$txtQuestionCat = '-';
}
// Question level
$txtQuestionLevel = $objQuestionTmp->getLevel();
if (empty($objQuestionTmp->level)) {
$txtQuestionLevel = '-';
}
$questionLevel = $txtQuestionLevel;
// Question score
$questionScore = $objQuestionTmp->selectWeighting();
echo '<div id="question_id_list_'.$id.'">
<div class="header_operations" data-exercise="'.$objExercise->selectId().'"
data-question="'.$id.'">
<div class="row">
<div class="question col-sm-5 col-xs-12">'
.$questionName.'
</div>
<div class="type text-center col-sm-1 col-xs-12">
<span class="visible-xs-inline">'.get_lang('Type').' </span>'
.$questionType.'
</div>
<div class="category col-sm-2 col-xs-12" title="'.$txtQuestionCat.'">
<span class="visible-xs-inline">'.get_lang('Category').' </span>'
.cut($txtQuestionCat, 42).'
</div>
<div class="level text-right col-sm-1 col-xs-6">
<span class="visible-xs-inline">'.get_lang('Difficulty').' </span>'
.$questionLevel.'
</div>
<div class="score text-right col-sm-1 col-xs-6">
<span class="visible-xs-inline">'.get_lang('Score').' </span>'
.$questionScore.'
</div>
<div class="btn-actions text-right col-sm-2 col-xs-6">
<div class="edition">'.$btnActions.'</div>
</div>
</div>
</div>
<div class="question-list-description-block" id="pnl-question-'.$id.'">
</div>
</div>
';
unset($objQuestionTmp);
}
}
echo '</div>'; //question list div
} else {
echo Display::return_message(get_lang('NoQuestion'), 'warning');
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,209 @@
<?php
/* For licensing terms, see /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_COURSES;
api_protect_course_script(true);
$isAllowedToEdit = api_is_allowed_to_edit(null, true);
if (!$isAllowedToEdit) {
api_not_allowed(true);
}
$exerciseId = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0;
$exportXls = isset($_REQUEST['export_xls']) && !empty($_REQUEST['export_xls']) ? (int) $_REQUEST['export_xls'] : 0;
$groups = $_REQUEST['groups'] ?? [];
$users = $_REQUEST['users'] ?? [];
if (empty($exerciseId)) {
api_not_allowed(true);
}
$sessionId = api_get_session_id();
$exercise = new Exercise();
$result = $exercise->read($exerciseId);
if (empty($result)) {
api_not_allowed(true);
}
$nameTools = get_lang('ExerciseManagement');
$interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$interbreadcrumb[] = [
'url' => 'admin.php?exerciseId='.$exercise->iid.'&'.api_get_cidreq(),
'name' => $exercise->selectTitle(true),
];
$interbreadcrumb[] = [
'url' => 'exercise_report.php?'.api_get_cidreq().'&exerciseId='.$exercise->iid,
'name' => get_lang('StudentScore'),
];
$courseId = api_get_course_int_id();
$courseInfo = api_get_course_info();
$form = new FormValidator('search_form', 'GET', api_get_self().'?id='.$exerciseId.'&'.api_get_cidreq());
$form->addCourseHiddenParams();
$form->addHidden('id', $exerciseId);
$courseGroups = GroupManager::get_group_list(null, $courseInfo);
if (!empty($courseGroups)) {
$courseGroups = array_column($courseGroups, 'name', 'iid');
$form->addSelect(
'groups',
get_lang('Groups'),
$courseGroups,
[
'multiple' => true,
]
);
}
$courseUsers = CourseManager::get_user_list_from_course_code($courseInfo['code']);
if (!empty($courseUsers)) {
array_walk(
$courseUsers,
function (&$data, $key) {
$data = api_get_person_name($data['firstname'], $data['lastname']);
}
);
}
$form->addSelect(
'users',
get_lang('Users'),
$courseUsers,
[
'multiple' => true,
]
);
$form->addButtonSearch(get_lang('Search'), 'searchSubmit');
$formToString = $form->toHtml();
$table = new HTML_Table(['class' => 'table table-hover table-striped']);
$row = 0;
$column = 0;
$headers = [
get_lang('Question'),
get_lang('WrongAnswer').' / '.get_lang('Total'),
'%',
];
foreach ($headers as $header) {
$table->setHeaderContents($row, $column, $header);
$column++;
}
if ($exportXls) {
$tableXls[] = $headers;
}
$row++;
$scoreDisplay = new ScoreDisplay();
$orderedData = [];
if ($form->validate()) {
$questions = ExerciseLib::getWrongQuestionResults($courseId, $exerciseId, $sessionId, $groups, $users);
foreach ($questions as $data) {
$questionId = (int) $data['question_id'];
$total = ExerciseLib::getTotalQuestionAnswered(
$courseId,
$exerciseId,
$questionId,
$sessionId,
$groups,
$users
);
$ordered = [
$data['question'],
$data['count'].' / '.$total,
$scoreDisplay->display_score([$data['count'], $total], SCORE_AVERAGE),
];
$orderedData[] = $ordered;
if ($exportXls) {
$tableXls[] = $ordered;
}
}
} else {
$questions = ExerciseLib::getWrongQuestionResults($courseId, $exerciseId, $sessionId);
foreach ($questions as $data) {
$questionId = (int) $data['question_id'];
$total = ExerciseLib::getTotalQuestionAnswered($courseId, $exerciseId, $questionId, $sessionId);
$ordered = [
$data['question'],
$data['count'].' / '.$total,
$scoreDisplay->display_score([$data['count'], $total], SCORE_AVERAGE),
];
$orderedData[] = $ordered;
}
}
$table = new SortableTableFromArray(
$orderedData,
0,
100,
'question_tracking'
);
$table->hideNavigation = true;
$table->column = 2;
$column = 0;
foreach ($headers as $header) {
$table->set_header($column, $header, false);
$column++;
}
if ($exportXls) {
$fileName = get_lang('QuestionStats').'_'.api_get_course_id().'_'.$exerciseId.'_'.api_get_local_time();
Export::arrayToXls($tableXls, $fileName);
exit;
}
$htmlHeadXtra[] = '<script>
$(function() {
$("#export-xls").bind("click", function(e) {
e.preventDefault();
var input = $("<input>", {
type: "hidden",
name: "export_xls",
value: "1"
});
$("#search_form").append(input);
$("#search_form").submit();
});
$("#search_form_searchSubmit").bind("click", function(e) {
e.preventDefault();
if ($("input[name=\"export_xls\"]").length > 0) {
$("input[name=\"export_xls\"]").remove();
}
$("#search_form").submit();
});
});
</script>';
Display::display_header($nameTools, get_lang('Exercise'));
$actions = '<a href="exercise_report.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
Display::return_icon(
'back.png',
get_lang('GoBackToQuestionList'),
'',
ICON_SIZE_MEDIUM
)
.'</a>';
$actions .= Display::url(
Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
'#',
['id' => 'export-xls']
);
$actions = Display::div($actions, ['class' => 'actions']);
echo $actions;
echo $formToString;
echo $table->return_table();
Display::display_footer();

Binary file not shown.

View File

@@ -0,0 +1,35 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEExercises;
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
if (!isset($_REQUEST['user'], $_REQUEST['exercise'], $_REQUEST['id'])) {
exit;
}
$isAllowedToEdit = api_is_allowed_to_edit(true, true);
if (!$isAllowedToEdit) {
exit;
}
$studentId = (int) $_REQUEST['user'];
$exerciseId = (int) $_REQUEST['exercise'];
$exeId = (int) $_REQUEST['id'];
/** @var TrackEExercises $trackedExercise */
$trackedExercise = ExerciseLib::recalculateResult(
$_REQUEST['id'],
$_REQUEST['user'],
$_REQUEST['exercise']
);
$totalScore = $trackedExercise->getExeResult();
$totalWeight = $trackedExercise->getExeWeighting();
echo $totalScore.'/'.$totalWeight;

View File

@@ -0,0 +1,61 @@
<?php
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\TrackEExercises;
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
if (!isset($_REQUEST['exercise'])) {
api_not_allowed(true);
}
$exerciseId = (int) $_REQUEST['exercise'];
$is_allowedToEdit = api_is_allowed_to_edit(null, true) ||
api_is_drh() ||
api_is_student_boss() ||
api_is_session_admin();
if (!$is_allowedToEdit) {
api_not_allowed(true);
}
$result = ExerciseLib::get_exam_results_data(
0,
0,
null,
'asc',
$exerciseId,
'',
false,
null,
false,
false,
[],
false,
false,
true
);
foreach ($result as $track) {
/** @var TrackEExercises $trackedExercise */
$trackedExercise = ExerciseLib::recalculateResult(
$track['id'],
$track['user_id'],
$exerciseId
);
if (!$trackedExercise) {
Display::addFlash(
Display::return_message(get_lang('BadFormData').'<br>ID: '.$track['id'], 'warning', false)
);
}
}
$url = api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'
.api_get_cidreq()
."&exerciseId=$exerciseId";
header("Location: $url");

184
main/exercise/result.php Normal file
View File

@@ -0,0 +1,184 @@
<?php
/* For licensing terms, see /license.txt */
use ChamiloSession as Session;
/**
* Shows the exercise results.
*
* @author Julio Montoya - Simple exercise result page
*/
require_once __DIR__.'/../inc/global.inc.php';
$current_course_tool = TOOL_QUIZ;
$id = isset($_REQUEST['id']) ? (int) $_GET['id'] : 0; // exe id
$show_headers = isset($_REQUEST['show_headers']) ? (int) $_REQUEST['show_headers'] : null;
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
$origin = api_get_origin();
if (in_array($origin, ['learnpath', 'embeddable', 'mobileapp', 'iframe'])) {
$show_headers = false;
}
api_protect_course_script($show_headers);
if (empty($id)) {
api_not_allowed($show_headers);
}
$is_allowedToEdit = api_is_allowed_to_edit(null, true) || api_is_course_tutor();
// Getting results from the exe_id. This variable also contain all the information about the exercise
$track_exercise_info = ExerciseLib::get_exercise_track_exercise_info($id);
// No track info
if (empty($track_exercise_info)) {
api_not_allowed($show_headers);
}
$exercise_id = $track_exercise_info['exe_exo_id'];
$student_id = (int) $track_exercise_info['exe_user_id'];
$current_user_id = api_get_user_id();
$objExercise = new Exercise();
if (!empty($exercise_id)) {
$objExercise->read($exercise_id);
}
if (empty($objExercise)) {
api_not_allowed($show_headers);
}
// Only users can see their own results
if (!$is_allowedToEdit) {
if ($student_id != $current_user_id) {
api_not_allowed($show_headers);
}
}
$allowSignature = false;
if ($student_id === $current_user_id && ExerciseSignaturePlugin::exerciseHasSignatureActivated($objExercise)) {
// Check if signature exists.
$signature = ExerciseSignaturePlugin::getSignature($current_user_id, $track_exercise_info);
if (false === $signature) {
$allowSignature = true;
}
}
$htmlHeadXtra[] = '<link rel="stylesheet" href="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/css/hotspot.css">';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'hotspot/js/hotspot.js"></script>';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'annotation/js/annotation.js"></script>';
if ($allowSignature) {
$htmlHeadXtra[] = api_get_asset('signature_pad/signature_pad.umd.js');
}
if (!empty($objExercise->getResultAccess())) {
$htmlHeadXtra[] = api_get_css(api_get_path(WEB_LIBRARY_PATH).'javascript/epiclock/renderers/minute/epiclock.minute.css');
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.dateformat.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/javascript/jquery.epiclock.min.js');
$htmlHeadXtra[] = api_get_js('epiclock/renderers/minute/epiclock.minute.js');
}
if ($show_headers) {
$interbreadcrumb[] = [
'url' => 'exercise.php?'.api_get_cidreq(),
'name' => get_lang('Exercises'),
];
$interbreadcrumb[] = ['url' => '#', 'name' => get_lang('Result')];
$this_section = SECTION_COURSES;
} else {
$htmlHeadXtra[] = '<style>
body { background: none;}
</style>';
if ($origin === 'mobileapp') {
echo '<div class="actions">';
echo '<a href="javascript:window.history.go(-1);">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), [], 32).'</a>';
echo '</div>';
}
}
if (api_get_configuration_value('allow_skill_rel_items') == true) {
$htmlContentExtraClass[] = 'feature-item-user-skill-on';
}
$message = Session::read('attempt_remaining');
Session::erase('attempt_remaining');
$allowExportPdf = api_get_configuration_value('quiz_results_answers_report');
ob_start();
$stats = ExerciseLib::displayQuestionListByAttempt(
$objExercise,
$id,
false,
$message,
$allowSignature,
$allowExportPdf,
'export' === $action
);
$pageContent = ob_get_contents();
ob_end_clean();
switch ($action) {
case 'export':
if ($allowExportPdf) {
$allAnswers = $stats['all_answers_html'];
@$pdf = new PDF();
$cssFile = api_get_path(SYS_CSS_PATH).'themes/chamilo/default.css';
$title = get_lang('ResponseReport');
$exerciseTitle = $objExercise->get_formated_title();
$studentInfo = api_get_user_info($student_id);
$userHeader = $objExercise->showExerciseResultHeader(
$studentInfo,
$track_exercise_info,
false,
false,
false
);
$filename = get_lang('Exercise').'_'.$exerciseTitle;
$pdf->content_to_pdf("
<html><body>
<h2 style='text-align: center'>$title</h2>
$userHeader
$allAnswers
</body></html>",
file_get_contents($cssFile),
$filename,
api_get_course_id(),
'D',
false,
null,
false,
true
);
} else {
api_not_allowed(true);
}
exit;
break;
}
$lpId = (int) $track_exercise_info['orig_lp_id'];
$lpItemId = (int) $track_exercise_info['orig_lp_item_id'];
$lpViewId = (int) $track_exercise_info['orig_lp_item_view_id'];
$pageBottom = '<div class="question-return">';
$pageBottom .= Display::url(
get_lang('BackToAttemptList'),
api_get_path(WEB_CODE_PATH).'exercise/overview.php?exerciseId='.$exercise_id.'&'.api_get_cidreq().
"&learnpath_id=$lpId&learnpath_item_id=$lpItemId&learnpath_item_view_id=$lpViewId",
['class' => 'btn btn-primary']
);
$pageBottom .= '</div>';
$pageContent .= $pageBottom;
$template = new Template('', $show_headers, $show_headers);
$template->assign('page_content', $pageContent);
$template->assign('allow_signature', $allowSignature);
$template->assign('exe_id', $id);
$layout = $template->fetch($template->get_template('exercise/result.tpl'));
$template->assign('content', $layout);
$template->display_one_col_template();

View File

@@ -0,0 +1,113 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Saving the scores.
*
* @package chamilo.exercise
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
$courseInfo = api_get_course_info();
$_user = api_get_user_info();
$this_section = SECTION_COURSES;
$documentPath = api_get_path(SYS_COURSE_PATH).$courseInfo['path']."/document";
$test = $_REQUEST['test'];
$full_file_path = $documentPath.$test;
my_delete($full_file_path.$_user['user_id'].".t.html");
$TABLETRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
$TABLE_LP_ITEM_VIEW = Database::get_course_table(TABLE_LP_ITEM_VIEW);
$score = $_REQUEST['score'];
$origin = api_get_origin();
$learnpath_item_id = intval($_REQUEST['learnpath_item_id']);
$lpViewId = isset($_REQUEST['lp_view_id']) ? intval($_REQUEST['lp_view_id']) : null;
$course_id = $courseInfo['real_id'];
$jscript2run = '';
/**
* Save the score for a HP quiz. Can be used by the learnpath tool as well
* for HotPotatoes quizzes. When coming from the learning path, we
* use the session variables telling us which item of the learning path has to
* be updated (score-wise).
*
* @param string File is the exercise name (the file name for a HP)
* @param int Score to save inside the tracking tables (HP and learnpath)
*/
function save_scores($file, $score)
{
global $origin;
$TABLETRACK_HOTPOTATOES = Database::get_main_table(TABLE_STATISTIC_TRACK_E_HOTPOTATOES);
$_user = api_get_user_info();
// if tracking is disabled record nothing
$weighting = 100; // 100%
$date = api_get_utc_datetime();
$c_id = api_get_course_int_id();
if ($_user['user_id']) {
$user_id = $_user['user_id'];
} else {
// anonymous
$user_id = "NULL";
}
$params = [
'exe_name' => $file,
'exe_user_id' => $user_id,
'exe_date' => $date,
'c_id' => $c_id,
'exe_result' => $score,
'exe_weighting' => $weighting,
];
Database::insert($TABLETRACK_HOTPOTATOES, $params);
if ($origin == 'learnpath') {
//if we are in a learning path, save the score in the corresponding
//table to get tracking in there as well
global $jscript2run;
//record the results in the learning path, using the SCORM interface (API)
$jscript2run .= "<script>
$(function() {
//API_obj = window.frames.window.content.API;
//API_obj = $('content_id').context.defaultView.content.API; //works only in FF
//API_obj = window.parent.frames.window.top.API;
API_obj = window.top.API;
API_obj.void_save_asset('$score', '$weighting', 0, 'completed');
});
</script>";
}
}
// Save the Scores
save_scores($test, $score);
// Back
if ($origin != 'learnpath') {
$url = "exercise.php"; // back to exercises
$jscript2run .= '<script>'."window.open('$url', '_top', '')".'</script>';
echo $jscript2run;
} else {
$htmlHeadXtra[] = $jscript2run;
Display::display_reduced_header();
if (!empty($course_id) && !empty($learnpath_item_id) && !empty($lpViewId)) {
$sql = "UPDATE $TABLE_LP_ITEM_VIEW SET
status = 'completed'
WHERE
c_id = $course_id AND
lp_item_id= $learnpath_item_id AND
lp_view_id = $lpViewId
";
Database::query($sql);
echo Display::return_message(get_lang('HotPotatoesFinished'), 'confirm');
} else {
echo Display::return_message(get_lang('Error'), 'error');
}
Display::display_footer();
}

View File

@@ -0,0 +1,124 @@
<?php
/* For licensing terms, see /license.txt */
/**
* Code library for HotPotatoes integration.
*
* @package chamilo.exercise
*
* @author Istvan Mandak
*/
require_once __DIR__.'/../inc/global.inc.php';
api_protect_course_script(true);
require_once api_get_path(SYS_CODE_PATH).'exercise/hotpotatoes.lib.php';
$_course = api_get_course_info();
$time = time();
$doc_url = str_replace(['../', '\\', '\\0', '..'], ['', '', '', ''], urldecode($_GET['file']));
$cid = api_get_course_id();
$document_path = api_get_path(SYS_COURSE_PATH).$_course['path'].'/document';
$document_web_path = api_get_path(WEB_COURSE_PATH).$_course['path'].'/document';
$origin = api_get_origin();
$learnpath_id = isset($_REQUEST['learnpath_id']) ? $_REQUEST['learnpath_id'] : null;
$learnpath_item_id = isset($_REQUEST['learnpath_item_id']) ? $_REQUEST['learnpath_item_id'] : null;
$time = isset($_REQUEST['time']) ? $_REQUEST['time'] : null;
$lpViewId = isset($_REQUEST['lp_view_id']) ? $_REQUEST['lp_view_id'] : null;
$user_id = api_get_user_id();
$full_file_path = $document_path.$doc_url;
my_delete($full_file_path.$user_id.'.t.html');
$content = ReadFileCont($full_file_path.$user_id.'.t.html');
if ($content == '') {
$content = ReadFileCont($full_file_path);
// Do not move this like:
$mit = "function Finish(){";
$js_content = "
// Code added - start
var SaveScoreVariable = 0;
function mySaveScore() {
if (SaveScoreVariable==0) {
SaveScoreVariable = 1;
if (C.ie) {
document.location.href = '".api_get_path(WEB_CODE_PATH)."exercise/savescores.php?lp_view_id=$lpViewId&origin=$origin&learnpath_id=$learnpath_id&learnpath_item_id=$learnpath_item_id&time=".Security::remove_XSS($time)."&test=".$doc_url."&uid=".$user_id."&cid=".$cid."&score='+Score;
} else {
window.location.href = '".api_get_path(WEB_CODE_PATH)."exercise/savescores.php?lp_view_id=$lpViewId&origin=$origin&learnpath_id=$learnpath_id&learnpath_item_id=$learnpath_item_id&time=".Security::remove_XSS($time)."&test=".$doc_url."&uid=".$user_id."&cid=".$cid."&score='+Score;
}
}
}
function Finish() {
mySaveScore();
// Code added - end
";
$newcontent = str_replace($mit, $js_content, $content);
$prehref = "<!-- BeginTopNavButtons -->";
$posthref = "<!-- BeginTopNavButtons -->";
$newcontent = str_replace($prehref, $posthref, $newcontent);
if (CheckSubFolder($full_file_path.$user_id.'.t.html') == 0) {
$newcontent = ReplaceImgTag($newcontent);
}
} else {
$newcontent = $content;
}
WriteFileCont($full_file_path.$user_id.'.t.html', $newcontent);
$doc_url = GetFolderPath($doc_url).urlencode(basename($doc_url));
$documentPath = api_get_path(SYS_COURSE_PATH).$_course['path']."/document";
$my_file = Security::remove_XSS($_GET['file']);
$my_file = str_replace(['../', '\\..', '\\0', '..\\'], ['', '', '', ''], urldecode($my_file));
$title = GetQuizName($my_file, $documentPath);
if ($title == '') {
$title = basename($my_file);
}
$nameTools = $title;
$htmlHeadXtra[] = <<<HTML
<script>
function setHeight()
{
var iframe = document.getElementById('hotpotatoe');
iframe.height = 800;
var maxheight = $(iframe.contentDocument.body).outerHeight(true);
iframe.height = maxheight
$(iframe.contentDocument.body).children().each(function() {
// If this elements height is bigger than the biggestHeight
var tempheight = $(this)["0"].offsetHeight + $(this)["0"].offsetTop;
if (tempheight > maxheight) {
// Set the maxheight to this Height
maxheight = tempheight;
}
});
iframe.height = maxheight;
}
$(function() {
var iframe = document.getElementById('hotpotatoe');
iframe.onload = function () {
// this.height = $(this.contentDocument.body).outerHeight(true)
setTimeout(function(){
setHeight();
}, 1750);
};
});
</script>
HTML;
$interbreadcrumb[] = ["url" => './exercise.php?'.api_get_cidreq(), 'name' => get_lang('Exercises')];
if ($origin == 'learnpath') {
Display::display_reduced_header($nameTools, "Exercise");
} else {
Display::display_header($nameTools, "Exercise");
}
$url = $document_web_path.$doc_url.$user_id.'.t.html?time='.intval($time);
echo '<iframe id="hotpotatoe" name="hotpotatoe" width="100%" height="100%" frameborder="0" src="'.$url.'"></iframe>';
if ($origin == 'learnpath') {
Display::display_reduced_footer();
} else {
Display::display_footer();
}

390
main/exercise/stats.php Normal file
View File

@@ -0,0 +1,390 @@
<?php
/* See license terms in /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_COURSES;
api_protect_course_script(true, false, true);
$showPage = false;
if (api_is_platform_admin() || api_is_course_admin() ||
api_is_course_tutor() || api_is_session_general_coach() || api_is_allowed_to_edit(null, true)
) {
$showPage = true;
}
if (!$showPage) {
api_not_allowed(true);
}
$exportXls = isset($_GET['export_xls']) && !empty($_GET['export_xls']) ? (int) $_GET['export_xls'] : 0;
$exerciseId = isset($_GET['exerciseId']) && !empty($_GET['exerciseId']) ? (int) $_GET['exerciseId'] : 0;
$objExercise = new Exercise();
$result = $objExercise->read($exerciseId);
if (!$result) {
api_not_allowed(true);
}
$sessionId = api_get_session_id();
$courseCode = api_get_course_id();
$courseId = api_get_course_int_id();
if (empty($sessionId)) {
$students = CourseManager::get_student_list_from_course_code($courseCode, false);
} else {
$students = CourseManager::get_student_list_from_course_code($courseCode, true, $sessionId);
}
$count_students = count($students);
$questionList = $objExercise->getQuestionForTeacher(0, $objExercise->getQuestionCount());
$data = [];
// Question title # of students who tool it Lowest score Average Highest score Maximum score
$headers = [
get_lang('Question'),
get_lang('QuestionType'),
get_lang('NumberStudentWhoSelectedIt'),
get_lang('LowestScore'),
get_lang('AverageScore'),
get_lang('HighestScore'),
get_lang('Weighting'),
];
if (!empty($questionList)) {
foreach ($questionList as $question_id) {
$questionObj = Question::read($question_id);
$exerciseStats = ExerciseLib::get_student_stats_by_question(
$question_id,
$exerciseId,
$courseId,
$sessionId,
true
);
$count_users = ExerciseLib::get_number_students_question_with_answer_count(
$question_id,
$exerciseId,
$courseCode,
$sessionId,
$questionObj->type
);
$data[$question_id]['name'] = cut($questionObj->question, 100);
$data[$question_id]['type'] = $questionObj->get_question_type_name();
$percentage = 0;
if ($count_students) {
$percentage = $count_users / $count_students * 100;
}
if ($exportXls) {
$data[$question_id]['students_who_try_exercise'] = $count_users.' / '.$count_students.' ('.$percentage.'%)';
} else {
$data[$question_id]['students_who_try_exercise'] = Display::bar_progress(
$percentage,
false,
$count_users.' / '.$count_students
);
}
$data[$question_id]['lowest_score'] = round($exerciseStats['min'], 2);
$data[$question_id]['average_score'] = round($exerciseStats['average'], 2);
$data[$question_id]['highest_score'] = round($exerciseStats['max'], 2);
$data[$question_id]['max_score'] = round($questionObj->weighting, 2);
}
}
$table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
$row = 0;
$column = 0;
foreach ($headers as $header) {
$table->setHeaderContents($row, $column, $header);
$column++;
}
if ($exportXls) {
$tableXls1[] = $headers;
}
$row++;
foreach ($data as $row_table) {
$column = 0;
foreach ($row_table as $key => $cell) {
$table->setCellContents($row, $column, $cell);
$table->updateCellAttributes($row, $column, 'align="center"');
if ($exportXls) {
$row_table[$key] = strip_tags($cell);
}
$column++;
}
$table->updateRowAttributes($row, $row % 2 ? 'class="row_even"' : 'class="row_odd"', true);
if ($exportXls) {
$tableXls1[] = $row_table;
}
$row++;
}
$content = $table->toHtml();
// Format B
$headers = [
get_lang('Question'),
get_lang('Answer'),
get_lang('Correct'),
get_lang('NumberStudentWhoSelectedIt'),
];
$data = [];
if (!empty($questionList)) {
$id = 0;
foreach ($questionList as $question_id) {
$questionObj = Question::read($question_id);
$exerciseStats = ExerciseLib::get_student_stats_by_question(
$question_id,
$exerciseId,
$courseId,
$sessionId,
true
);
$answer = new Answer($question_id);
$answer_count = $answer->selectNbrAnswers();
for ($answer_id = 1; $answer_id <= $answer_count; $answer_id++) {
$answer_info = $answer->selectAnswer($answer_id);
$is_correct = $answer->isCorrect($answer_id);
$correct_answer = $is_correct == 1 ? get_lang('Yes') : get_lang('No');
$real_answer_id = $answer->selectAutoId($answer_id);
// Overwriting values depending of the question
switch ($questionObj->type) {
case FILL_IN_BLANKS:
case FILL_IN_BLANKS_COMBINATION:
$answer_info_db = $answer_info;
$answer_info = substr($answer_info, 0, strpos($answer_info, '::'));
$correct_answer = $is_correct;
$answers = $objExercise->fill_in_blank_answer_to_array($answer_info);
$counter = 0;
foreach ($answers as $answer_item) {
if (0 == $counter) {
$data[$id]['name'] = cut($questionObj->question, 100);
} else {
$data[$id]['name'] = '-';
}
$data[$id]['answer'] = $answer_item;
$answer_item = api_substr($answer_item, 1);
$answer_item = api_substr($answer_item, 0, api_strlen($answer_item) - 1);
$data[$id]['answer'] = $answer_item;
$data[$id]['correct'] = '-';
$count = ExerciseLib::getNumberStudentsFillBlanksAnswerCount($question_id, $exerciseId);
$count = isset($count[$counter]) ? $count[$counter] : 0;
$percentage = 0;
if (!empty($count_students)) {
$percentage = $count / $count_students * 100;
}
if ($exportXls) {
$data[$id]['attempts'] = $count.' / '.$count_students.' ('.$percentage.'%)';
} else {
$data[$id]['attempts'] = Display::bar_progress(
$percentage,
false,
$count.' / '.$count_students
);
}
$id++;
$counter++;
}
break;
case MATCHING:
case MATCHING_DRAGGABLE:
if ($is_correct == 0) {
if ($answer_id == 1) {
$data[$id]['name'] = cut($questionObj->question, 100);
} else {
$data[$id]['name'] = '-';
}
$correct = '';
for ($i = 1; $i <= $answer_count; $i++) {
$is_correct_i = $answer->isCorrect($i);
if ($is_correct_i != 0 && $is_correct_i == $answer_id) {
$correct = $answer->selectAnswer($i);
break;
}
}
$data[$id]['answer'] = $correct;
$data[$id]['correct'] = $answer_info;
$count = ExerciseLib::get_number_students_answer_count(
$answer_id,
$question_id,
$exerciseId,
$courseId,
$sessionId,
MATCHING
);
$percentage = 0;
if (!empty($count_students)) {
$percentage = $count / $count_students * 100;
}
if ($exportXls) {
$data[$id]['attempts'] = $count.' / '.$count_students.' ('.$percentage.'%)';
} else {
$data[$id]['attempts'] = Display::bar_progress(
$percentage,
false,
$count.' / '.$count_students
);
}
}
break;
case HOT_SPOT:
case HOT_SPOT_COMBINATION:
if ($answer_id == 1) {
$data[$id]['name'] = cut($questionObj->question, 100);
} else {
$data[$id]['name'] = '-';
}
$data[$id]['answer'] = $answer_info;
$data[$id]['correct'] = '-';
$count = ExerciseLib::get_number_students_answer_hotspot_count(
$answer_id,
$question_id,
$exerciseId,
$courseCode,
$sessionId
);
$percentage = 0;
if (!empty($count_students)) {
$percentage = $count / $count_students * 100;
}
if ($exportXls) {
$data[$id]['attempts'] = $count.' / '.$count_students.' ('.$percentage.'%)';
} else {
$data[$id]['attempts'] = Display::bar_progress(
$percentage,
false,
$count.' / '.$count_students
);
}
break;
default:
if ($answer_id == 1) {
$data[$id]['name'] = cut($questionObj->question, 100);
} else {
$data[$id]['name'] = '-';
}
$data[$id]['answer'] = $answer_info;
$data[$id]['correct'] = $correct_answer;
$count = ExerciseLib::get_number_students_answer_count(
$real_answer_id,
$question_id,
$exerciseId,
$courseId,
$sessionId
);
$percentage = 0;
if (!empty($count_students)) {
$percentage = $count / $count_students * 100;
}
if ($exportXls) {
$data[$id]['attempts'] = $count.' / '.$count_students.' ('.$percentage.'%)';
} else {
$data[$id]['attempts'] = Display::bar_progress(
$percentage,
false,
$count.' / '.$count_students
);
}
}
$id++;
}
}
}
// Format A table
$table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
$row = 0;
$column = 0;
foreach ($headers as $header) {
$table->setHeaderContents($row, $column, $header);
$column++;
}
if ($exportXls) {
$tableXls1[] = []; // it adds an empty line after the first table
$tableXls2[] = $headers;
}
$row++;
foreach ($data as $row_table) {
$column = 0;
foreach ($row_table as $key => $cell) {
$table->setCellContents($row, $column, $cell);
$table->updateCellAttributes($row, $column, 'align="center"');
if ($exportXls) {
$row_table[$key] = strip_tags($cell);
}
$column++;
}
$table->updateRowAttributes($row, $row % 2 ? 'class="row_even"' : 'class="row_odd"', true);
if ($exportXls) {
$tableXls2[] = $row_table;
}
$row++;
}
$content .= $table->toHtml();
$exportPdf = isset($_GET['export_pdf']) && !empty($_GET['export_pdf']) ? (int) $_GET['export_pdf'] : 0;
if ($exportPdf) {
$fileName = get_lang('Report').'_'.api_get_course_id().'_'.api_get_local_time();
$params = [
'filename' => $fileName,
'pdf_title' => $objExercise->selectTitle(true).'<br>'.get_lang('ReportByQuestion'),
'pdf_description' => get_lang('Report'),
'format' => 'A4',
'orientation' => 'P',
];
Export::export_html_to_pdf($content, $params);
exit;
}
if ($exportXls) {
$fileName = get_lang('Report').'_'.api_get_course_id().'_'.api_get_local_time();
$tableXls = array_merge($tableXls1, $tableXls2);
Export::arrayToXls($tableXls, $fileName);
exit;
}
$interbreadcrumb[] = [
"url" => "exercise.php?".api_get_cidreq(),
"name" => get_lang('Exercises'),
];
$interbreadcrumb[] = [
'url' => "admin.php?exerciseId=$exerciseId&".api_get_cidreq(),
'name' => $objExercise->selectTitle(true),
];
$tpl = new Template(get_lang('ReportByQuestion'));
$actions = '<a href="exercise_report.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
Display::return_icon(
'back.png',
get_lang('GoBackToQuestionList'),
'',
ICON_SIZE_MEDIUM
)
.'</a>';
$actions .= Display::url(
Display::return_icon('pdf.png', get_lang('ExportToPDF'), [], ICON_SIZE_MEDIUM),
'stats.php?exerciseId='.$exerciseId.'&export_pdf=1&'.api_get_cidreq()
);
$actions .= Display::url(
Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
'stats.php?exerciseId='.$exerciseId.'&export_xls=1&'.api_get_cidreq()
);
$actions = Display::div($actions, ['class' => 'actions']);
$content = $actions.$content;
$tpl->assign('content', $content);
$tpl->display_one_col_template();

View File

@@ -0,0 +1,100 @@
<?php
/* See license terms in /license.txt */
require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_COURSES;
api_protect_course_script(true, false, true);
$showPage = false;
if (api_is_platform_admin() || api_is_course_admin() ||
api_is_course_tutor() || api_is_session_general_coach() || api_is_allowed_to_edit(null, true)
) {
$showPage = true;
}
if (!$showPage) {
api_not_allowed(true);
}
$exportXls = isset($_GET['export_xls']) && !empty($_GET['export_xls']) ? (int) $_GET['export_xls'] : 0;
$exerciseId = isset($_GET['exerciseId']) && !empty($_GET['exerciseId']) ? (int) $_GET['exerciseId'] : 0;
$objExercise = new Exercise();
$result = $objExercise->read($exerciseId);
if (!$result) {
api_not_allowed(true);
}
$stats = ExerciseLib::getTrackExerciseAttemptsTable($objExercise);
$table = new HTML_Table(['class' => 'table table-hover table-striped data_table']);
$row = 0;
$column = 0;
foreach ($stats['headers'] as $header) {
$table->setHeaderContents($row, $column, $header);
$column++;
}
if ($exportXls) {
$tableXls[] = $stats['headers_xls'];
}
$row++;
foreach ($stats['rows'] as $rowTable) {
$column = 0;
foreach ($rowTable as $key => $cell) {
$table->setCellContents($row, $column, $cell);
$table->updateCellAttributes($row, $column, 'align="center"');
if ($exportXls) {
$rowTable[$key] = strip_tags($cell);
}
$column++;
}
$table->updateRowAttributes($row, $row % 2 ? 'class="row_even"' : 'class="row_odd"', true);
if ($exportXls) {
$tableXls[] = $rowTable;
}
$row++;
}
$nameTools = get_lang('Results').': '.$objExercise->selectTitle(true);
$content = '<h2 class="page-header">'.$nameTools.'</h2>';
$content .= '<div style="width: 100%;overflow: auto;">';
$content .= $table->toHtml();
$content .= '</div>';
if ($exportXls) {
$fileName = get_lang('Report').'_'.api_get_course_id().'_'.api_get_local_time();
$tableXls = array_merge($tableXls);
Export::arrayToXls($tableXls, $fileName);
exit;
}
$interbreadcrumb[] = [
"url" => "exercise.php?".api_get_cidreq(),
"name" => get_lang('Exercises'),
];
$interbreadcrumb[] = [
'url' => "admin.php?exerciseId=$exerciseId&".api_get_cidreq(),
'name' => $objExercise->selectTitle(true),
];
$tpl = new Template(get_lang('ReportByAttempts'));
$actions = '<a href="exercise_report.php?exerciseId='.$exerciseId.'&'.api_get_cidreq().'">'.
Display::return_icon(
'back.png',
get_lang('GoBackToQuestionList'),
'',
ICON_SIZE_MEDIUM
)
.'</a>';
$actions .= Display::url(
Display::return_icon('excel.png', get_lang('ExportAsXLS'), [], ICON_SIZE_MEDIUM),
'stats_attempts.php?exerciseId='.$exerciseId.'&export_xls=1&'.api_get_cidreq()
);
$actions = Display::div($actions, ['class' => 'actions']);
$content = $actions.$content;
$tpl->assign('content', $content);
$tpl->display_one_col_template();

View File

@@ -0,0 +1,272 @@
<?php
/* For licensing terms, see /license.txt */
/**
hubert.borderiou
Manage tests category page
*/
$htmlHeadXtra[] = '
<script>
function confirmDelete(in_txt, in_id) {
var oldbgcolor = document.getElementById(in_id).style.backgroundColor;
document.getElementById(in_id).style.backgroundColor="#AAFFB0";
if (confirm(in_txt)) {
return true;
} else {
document.getElementById(in_id).style.backgroundColor = oldbgcolor;
return false;
}
}
</script>';
$nameTools = '';
require_once __DIR__.'/../inc/global.inc.php';
$this_section = SECTION_COURSES;
api_protect_course_script(true);
if (!api_is_allowed_to_edit()) {
api_not_allowed(true);
}
$category = new TestCategory();
$courseId = api_get_course_int_id();
$sessionId = api_get_session_id();
// breadcrumbs
$interbreadcrumb[] = [
"url" => "exercise.php?".api_get_cidreq(),
"name" => get_lang('Exercises'),
];
$action = isset($_GET['action']) ? $_GET['action'] : '';
$content = '';
switch ($action) {
case 'addcategory':
$content = add_category_form('addcategory');
break;
case 'editcategory':
$content = edit_category_form('editcategory');
break;
case 'deletecategory':
delete_category_form();
break;
case 'export_category':
$archiveFile = 'export_exercise_categories_'.api_get_course_id().'_'.api_get_local_time();
$categories = $category->getCategories($courseId, $sessionId);
$export = [];
$export[] = ['title', 'description'];
if (!empty($categories)) {
foreach ($categories as $category) {
$export[] = [$category['title'], $category['description']];
}
}
Export::arrayToCsv($export, $archiveFile);
exit;
break;
case 'import_category':
$form = importCategoryForm();
if ($form->validate()) {
$categories = Import::csv_reader($_FILES['file']['tmp_name']);
if (!empty($categories)) {
foreach ($categories as $item) {
$cat = new TestCategory();
$cat->name = $item['title'];
$cat->description = $item['description'];
$cat->save();
}
Display::addFlash(Display::return_message(get_lang('Imported')));
}
}
$content = $form->returnForm();
break;
}
Display::display_header(get_lang('Category'));
displayActionBar();
echo $content;
echo $category->displayCategories($courseId, $sessionId);
Display::display_footer();
/**
* @return FormValidator
*/
function importCategoryForm()
{
$form = new FormValidator('import', 'post', api_get_self().'?action=import_category&'.api_get_cidreq());
$form->addElement('file', 'file', get_lang('ImportCSVFileLocation'));
$form->addRule('file', get_lang('ThisFieldIsRequired'), 'required');
$form->addButtonImport(get_lang('Import'));
return $form;
}
/**
* Form to edit a category.
*
* @todo move to TestCategory.class.php
*
* @param string $action
*/
function edit_category_form($action)
{
$action = Security::remove_XSS($action);
if (isset($_GET['category_id']) && is_numeric($_GET['category_id'])) {
$category_id = intval($_GET['category_id']);
$objcat = new TestCategory();
$objcat = $objcat->getCategory($category_id);
$form = new FormValidator(
'note',
'post',
api_get_self().'?action='.$action.'&category_id='.$category_id.'&'.api_get_cidreq()
);
// Setting the form elements
$form->addElement('header', get_lang('EditCategory'));
$form->addElement('hidden', 'category_id');
$form->addElement('text', 'category_name', get_lang('CategoryName'), ['size' => '95']);
$form->addHtmlEditor(
'category_description',
get_lang('CategoryDescription'),
false,
false,
['ToolbarSet' => 'TestQuestionDescription', 'Height' => '200']
);
$form->addButtonSave(get_lang('ModifyCategory'), 'SubmitNote');
// setting the defaults
$defaults = [];
$defaults['category_id'] = $objcat->iid;
$defaults['category_name'] = $objcat->name;
$defaults['category_description'] = $objcat->description;
$form->setDefaults($defaults);
// setting the rules
$form->addRule('category_name', get_lang('ThisFieldIsRequired'), 'required');
// The validation or display
if ($form->validate()) {
$check = Security::check_token('post');
if ($check) {
$values = $form->exportValues();
$category = new TestCategory();
$category = $category->getCategory($values['category_id']);
if ($category) {
$category->name = $values['category_name'];
$category->description = $values['category_description'];
$category->modifyCategory();
Display::addFlash(Display::return_message(get_lang('Updated')));
} else {
Display::addFlash(Display::return_message(get_lang('ModifyCategoryError'), 'error'));
}
}
Security::clear_token();
} else {
$token = Security::get_token();
$form->addElement('hidden', 'sec_token');
$form->setConstants(['sec_token' => $token]);
return $form->returnForm();
}
} else {
Display::addFlash(
Display::return_message(get_lang('CannotEditCategory'), 'error')
);
}
}
// process to delete a category
function delete_category_form()
{
if (isset($_GET['category_id']) && is_numeric($_GET['category_id'])) {
$category = new TestCategory();
if ($category->removeCategory($_GET['category_id'])) {
Display::addFlash(Display::return_message(get_lang('DeleteCategoryDone')));
} else {
Display::addFlash(Display::return_message(get_lang('CannotDeleteCategoryError'), 'error'));
}
} else {
Display::addFlash(Display::return_message(get_lang('CannotDeleteCategoryError'), 'error'));
}
}
/**
* form to add a category.
*
* @todo move to TestCategory.class.php
*
* @param string $action
*/
function add_category_form($action)
{
$action = Security::remove_XSS($action);
// initiate the object
$form = new FormValidator('note', 'post', api_get_self().'?action='.$action.'&'.api_get_cidreq());
// Setting the form elements
$form->addElement('header', get_lang('AddACategory'));
$form->addElement('text', 'category_name', get_lang('CategoryName'), ['size' => '95']);
$form->addHtmlEditor(
'category_description',
get_lang('CategoryDescription'),
false,
false,
['ToolbarSet' => 'TestQuestionDescription', 'Height' => '200']
);
$form->addButtonCreate(get_lang('AddTestCategory'), 'SubmitNote');
// setting the rules
$form->addRule('category_name', get_lang('ThisFieldIsRequired'), 'required');
// The validation or display
if ($form->validate()) {
$check = Security::check_token('post');
if ($check) {
$values = $form->exportValues();
$category = new TestCategory();
$category->name = $values['category_name'];
$category->description = $values['category_description'];
if ($category->save()) {
Display::addFlash(Display::return_message(get_lang('AddCategoryDone')));
} else {
Display::addFlash(Display::return_message(get_lang('AddCategoryNameAlreadyExists'), 'warning'));
}
}
Security::clear_token();
} else {
$token = Security::get_token();
$form->addElement('hidden', 'sec_token');
$form->setConstants(['sec_token' => $token]);
return $form->returnForm();
}
}
// Display add category button
function displayActionBar()
{
echo '<div class="actions">';
echo '<a href="'.api_get_path(WEB_CODE_PATH).'exercise/exercise.php?'.api_get_cidreq().'">'.
Display::return_icon('back.png', get_lang('GoBackToQuestionList'), '', ICON_SIZE_MEDIUM).'</a>';
echo '<a href="'.api_get_self().'?action=addcategory&'.api_get_cidreq().'">'.
Display::return_icon('new_folder.png', get_lang('AddACategory'), null, ICON_SIZE_MEDIUM).'</a>';
echo Display::url(
Display::return_icon('export_csv.png', get_lang('ExportAsCSV'), [], ICON_SIZE_MEDIUM),
api_get_self().'?action=export_category&'.api_get_cidreq()
);
echo Display::url(
Display::return_icon('import_csv.png', get_lang('ImportAsCSV'), [], ICON_SIZE_MEDIUM),
api_get_self().'?action=import_category&'.api_get_cidreq()
);
echo '</div>';
echo "<br/>";
echo "<fieldset><legend>".get_lang('QuestionCategory')."</legend></fieldset>";
}

Some files were not shown because too many files have changed in this diff Show More