Fix oral expression questions

pull/4014/head
Angel Fernando Quiroz Campos 4 years ago
parent 144948c2db
commit 03f1295a71
  1. 10
      public/main/exercise/exercise.class.php
  2. 8
      public/main/exercise/export/scorm/scorm_classes.php
  3. 131
      public/main/exercise/oral_expression.class.php
  4. 136
      public/main/inc/ajax/record_audio_rtc.ajax.php
  5. 17
      public/main/inc/lib/exercise.lib.php
  6. 45
      public/main/inc/lib/javascript/record_audio/record_audio.js
  7. 88
      public/main/template/default/document/record_audio.html.twig
  8. 7
      public/main/template/default/exercise/oral_expression.html.twig
  9. 66
      public/main/template/default/message/record_audio.html.twig
  10. 21
      src/CoreBundle/Entity/TrackExercise.php

@ -3594,10 +3594,6 @@ class Exercise
isset($exe_info['exe_id']) ? $exe_info['exe_id'] : $exeId
);
// Probably this attempt came in an exercise all question by page
if (0 == $feedback_type) {
$objQuestionTmp->replaceWithRealExe($exeId);
}
$generatedFile = $objQuestionTmp->getFileUrl();
}
@ -5997,7 +5993,7 @@ class Exercise
} elseif (ORAL_EXPRESSION == $answerType) {
$answer = $choice;
/** @var OralExpression $objQuestionTmp */
Event::saveQuestionAttempt(
$questionAttemptId = Event::saveQuestionAttempt(
$this,
$questionScore,
$answer,
@ -6009,6 +6005,10 @@ class Exercise
$questionDuration,
$objQuestionTmp->getAbsoluteFilePath()
);
if (false !== $questionAttemptId) {
OralExpression::saveAssetInQuestionAttempt($questionAttemptId);
}
} elseif (
in_array(
$answerType,

@ -22,14 +22,6 @@ class ScormAnswerFree extends Answer
$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('This learning object or activity is not SCORM compliant. That\'s why it is not exportable.').'</td></tr>';
return [$js, $html];

@ -2,7 +2,11 @@
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\Asset;
use Chamilo\CoreBundle\Entity\AttemptFile;
use Chamilo\CoreBundle\Entity\TrackEAttempt;
use Chamilo\CoreBundle\Framework\Container;
use Symfony\Component\Uid\Uuid;
/**
* Class OralExpression
@ -85,9 +89,6 @@ class OralExpression extends Question
if (!empty($exerciseId)) {
$this->exerciseId = (int) $exerciseId;
}
$this->storePath = $this->generateDirectory();
$this->fileName = $this->generateFileName();
$this->filePath = $this->storePath.$this->fileName;
}
/**
@ -95,9 +96,8 @@ class OralExpression extends Question
*
* @return string
*/
public function returnRecorder()
public function returnRecorder(int $trackExerciseId): string
{
$directory = '/..'.$this->generateRelativeDirectory();
$recordAudioView = new Template(
'',
false,
@ -108,12 +108,11 @@ class OralExpression extends Question
false
);
$recordAudioView->assign('directory', $directory);
$recordAudioView->assign('user_id', $this->userId);
$recordAudioView->assign('file_name', $this->fileName);
$recordAudioView->assign('type', Asset::EXERCISE_ATTEMPT);
$recordAudioView->assign('t_exercise_id', $trackExerciseId);
$recordAudioView->assign('question_id', $this->id);
$template = $recordAudioView->get_template('exercise/oral_expression.tpl');
$template = $recordAudioView->get_template('exercise/oral_expression.html.twig');
return $recordAudioView->fetch($template);
}
@ -204,114 +203,30 @@ class OralExpression extends Question
);
}
/**
* 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()
public static function saveAssetInQuestionAttempt($attemptId)
{
return null;
$em = Container::getEntityManager();
$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);
}
$attempt = $em->find(TrackEAttempt::class, $attemptId);
if (!empty($this->exerciseId) && !is_dir($this->storePath.$this->sessionId.'/'.$this->exerciseId)) {
mkdir($this->storePath.$this->sessionId.'/'.$this->exerciseId);
}
$variable = 'oral_expression_asset_'.$attempt->getQuestionId();
if (!empty($this->id) && !is_dir($this->storePath.$this->sessionId.'/'.$this->exerciseId.'/'.$this->id)) {
mkdir($this->storePath.$this->sessionId.'/'.$this->exerciseId.'/'.$this->id);
}
$assetId = ChamiloSession::read($variable);
$asset = Container::getAssetRepository()->find(Uuid::fromRfc4122($assetId));
if (!empty($this->userId) &&
!is_dir($this->storePath.$this->sessionId.'/'.$this->exerciseId.'/'.$this->id.'/'.$this->userId)
) {
mkdir($this->storePath.$this->sessionId.'/'.$this->exerciseId.'/'.$this->id.'/'.$this->userId);
if (null === $asset) {
return;
}
$params = [
$this->sessionId,
$this->exerciseId,
$this->id,
$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->id,
$this->exeId,
]
);
}
ChamiloSession::erase($variable);
/**
* Generate a relative directory path.
*
* @return string
*/
private function generateRelativeDirectory()
{
$params = [
$this->sessionId,
$this->exerciseId,
$this->id,
$this->userId,
];
$attemptFile = (new AttemptFile())
->setAsset($asset)
;
$path = implode('/', $params);
$attempt->addAttemptFile($attemptFile);
return '/exercises/'.$path.'/';
$em->persist($attemptFile);
$em->flush();
}
}

@ -2,39 +2,27 @@
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Entity\Asset;
use Chamilo\CoreBundle\Entity\AttemptFeedback;
use Chamilo\CoreBundle\Entity\TrackEAttempt;
use Chamilo\CoreBundle\Entity\TrackExercise;
use Chamilo\CoreBundle\Framework\Container;
use ChamiloSession as Session;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
require_once __DIR__.'/../global.inc.php';
api_block_anonymous_users();
$courseInfo = api_get_course_info();
/** @var string $tool document or exercise */
$tool = $_REQUEST['tool'] ?? '';
$type = $_REQUEST['type'] ?? 'document'; // can be document or message
$httpRequest = Request::createFromGlobals();
if ('document' === $type) {
api_protect_course_script();
}
$userId = api_get_user_id();
if (!isset($_FILES['audio_blob'], $_REQUEST['audio_dir'])) {
if ('exercise' === $tool) {
header('Content-Type: application/json');
echo json_encode([
'error' => true,
'message' => Display::return_message(
get_lang('Upload failed, please check maximum file size limits and folder rights.'),
'error'
),
]);
//Display::cleanFlashMessages();
exit;
}
/** @var UploadedFile $audioBlob */
$audioBlob = $httpRequest->files->get('audio_blob');
$type = $httpRequest->get('type');
$trackExerciseId = (int) $httpRequest->get('t_exercise');
$questionId = (int) $httpRequest->get('question');
if (empty($audioBlob)) {
Display::addFlash(
Display::return_message(
get_lang('Upload failed, please check maximum file size limits and folder rights.'),
@ -44,71 +32,49 @@ if (!isset($_FILES['audio_blob'], $_REQUEST['audio_dir'])) {
exit;
}
$file = $_FILES['audio_blob'] ?? [];
$file['file'] = $file;
$audioDir = Security::remove_XSS($_REQUEST['audio_dir']);
$em = Container::getEntityManager();
switch ($type) {
case 'document':
if (empty($audioDir)) {
$audioDir = '/';
}
case Asset::EXERCISE_ATTEMPT:
$asset = (new Asset())
->setCategory(Asset::EXERCISE_ATTEMPT)
->setTitle(time().uniqid('tea'))
->setFile($audioBlob)
;
/*$uploadedDocument = DocumentManager::upload_document(
$file,
$audioDir,
$file['name'],
null,
0,
'overwrite',
false,
in_array($tool, ['document', 'exercise']),
'file',
true,
api_get_user_id(),
$courseInfo,
api_get_session_id(),
api_get_group_id(),
'exercise' === $tool
);*/
$error = empty($uploadedDocument) || !is_array($uploadedDocument);
if (!$error) {
$newDocId = $uploadedDocument['id'];
$courseId = $uploadedDocument['c_id'];
$lpId = $_REQUEST['lp_id'] ?? null;
$lpItemId = $_REQUEST['lp_item_id'] ?? null;
$lpRepo = Container::getLpRepository();
$lp = $lpRepo->find($lpId);
if (!empty($lp) && empty($lpItemId)) {
$lpItem = new learnpathItem($lpItemId);
$lpItem->add_audio_from_documents($newDocId);
}
$data = DocumentManager::get_document_data_by_id($newDocId, $courseInfo['code']);
if ('exercise' === $tool) {
header('Content-Type: application/json');
echo json_encode([
'error' => $error,
//'message' => Display::getFlashToString(),
'fileUrl' => $data['document_url'],
]);
exit;
}
echo $data['document_url'];
}
$em->persist($asset);
$em->flush();
ChamiloSession::write("oral_expression_asset_$questionId", $asset->getId()->toRfc4122());
break;
case 'message':
if (isset($_FILES['audio_blob']['tmp_name'])) {
$file['content'] = file_get_contents($_FILES['audio_blob']['tmp_name']);
Session::write('current_audio', $file);
echo 1;
case Asset::EXERCISE_FEEDBACK:
$asset = (new Asset())
->setCategory(Asset::EXERCISE_FEEDBACK)
->setTitle(time().uniqid('tea'))
->setFile($audioBlob)
;
$em->persist($asset);
$em->flush();
$attemptFeedback = (new AttemptFeedback())
->setAsset($asset);
;
/** @var TrackExercise $exeAttempt */
$exeAttempt = Container::getTrackExerciseRepository()->find($trackExerciseId);
$attempt = $exeAttempt->getAttemptByQuestionId($questionId);
if (null === $attempt) {
exit;
}
$attempt->addAttemptFeedback($attemptFeedback);
$em->persist($attemptFeedback);
$em->flush();
break;
default:
throw new \Exception('Unexpected value');
}

@ -3,6 +3,7 @@
/* For licensing terms, see /license.txt */
use Chamilo\CoreBundle\Component\Utils\ChamiloApi;
use Chamilo\CoreBundle\Entity\Asset;
use Chamilo\CoreBundle\Entity\GradebookCategory;
use Chamilo\CoreBundle\Entity\TrackExercise;
use Chamilo\CoreBundle\Framework\Container;
@ -217,16 +218,9 @@ class ExerciseLib
$exercise_stat_info['exe_exo_id'],
$exercise_stat_info['exe_id']
);
} else {
$objQuestionTmp->initFile(
api_get_session_id(),
api_get_user_id(),
$exerciseId,
'temp_exe'
);
}
echo $objQuestionTmp->returnRecorder();
echo $objQuestionTmp->returnRecorder((int) $exercise_stat_info['exe_id']);
}
}
$form = new FormValidator('free_choice_'.$questionId);
@ -5135,10 +5129,9 @@ EOT;
public static function getOralFeedbackForm($attemptId, $questionId, $userId)
{
$view = new Template('', false, false, false, false, false, false);
$view->assign('user_id', $userId);
$view->assign('type', Asset::EXERCISE_FEEDBACK);
$view->assign('question_id', $questionId);
$view->assign('directory', "/../exercises/teacher_audio/$attemptId/");
$view->assign('file_name', "{$questionId}_{$userId}");
$view->assign('attempt', $attemptId);
$template = $view->get_template('exercise/oral_expression.tpl');
return $view->fetch($template);

@ -40,12 +40,11 @@ window.RecordAudio = (function () {
function pauseTimer() {
clearInterval(window.timerInterval);
}
function useRecordRTC(rtcInfo, fileName) {
function useRecordRTC(rtcInfo) {
$(rtcInfo.blockId).show();
var mediaConstraints = {audio: true},
recordRTC = null,
txtName = $('#audio-title-rtc'),
btnStart = $(rtcInfo.btnStartId),
btnPause = $(rtcInfo.btnPauseId),
btnPlay = $(rtcInfo.btnPlayId),
@ -61,21 +60,19 @@ window.RecordAudio = (function () {
}
var btnSaveText = btnSave ? btnSave.html() : '';
var fileExtension = '.' + recordedBlob.type.split('/')[1];
var fileName = 'oral_expression_' + rtcInfo.tExerciseId + '_' + rtcInfo.questionId;
var formData = new FormData();
formData.append('audio_blob', recordedBlob, fileName + fileExtension);
formData.append('audio_dir', rtcInfo.directory);
var courseParams = "";
if (rtcInfo.cidReq) {
courseParams = "&"+rtcInfo.cidReq;
}
formData.append('type', rtcInfo.type);
formData.append('audio_blob', recordedBlob, fileName);
formData.append('t_exercise', rtcInfo.tExerciseId);
formData.append('question', rtcInfo.questionId);
var courseParams = rtcInfo.cidReq.replaceAll('&amp;', '&');
$.ajax({
url: rtcInfo.recordAudioUrl + '?'+ $.param({
type: rtcInfo.type,
tool: (!!txtName.length ? 'document' : 'exercise')
}) + courseParams,
url: rtcInfo.recordAudioUrl + '?a=' + courseParams,
data: formData,
processData: false,
contentType: false,
@ -90,13 +87,6 @@ window.RecordAudio = (function () {
}
}
}).done(function (response) {
if (!!txtName.length) {
if (rtcInfo.reload_page == 1) {
window.location.reload();
return;
}
}
$(response.message).insertAfter($(rtcInfo.blockId).find('.well'));
}).always(function () {
if (btnSave) {
@ -105,20 +95,10 @@ window.RecordAudio = (function () {
btnStop.prop('disabled', true).addClass('hidden');
btnPause.prop('disabled', true).addClass('hidden');
btnStart.prop('disabled', false).removeClass('hidden');
txtName.prop('readonly', false);
});
}
btnStart.on('click', function () {
if (!fileName) {
fileName = txtName.val();
if (!$.trim(fileName)) {
return;
}
}
function successCallback(stream) {
stopTimer();
startTimer();
@ -130,7 +110,6 @@ window.RecordAudio = (function () {
});
recordRTC.startRecording();
txtName.prop('readonly', true);
if (btnSave) {
btnSave.prop('disabled', true).addClass('hidden');
}
@ -215,14 +194,14 @@ window.RecordAudio = (function () {
}
return {
init: function (rtcInfo, fileName) {
init: function (rtcInfo) {
$(rtcInfo.blockId).hide();
var webRTCIsEnabled = navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.getUserMedia ||
navigator.mediaDevices.getUserMedia;
if (webRTCIsEnabled) {
useRecordRTC(rtcInfo, fileName);
useRecordRTC(rtcInfo);
return;
}

@ -1,88 +0,0 @@
<div class="alert alert-warning">
<span class="fa fa-warning fa-fw" aria-hidden="true"></span> {{ 'WamiNeedFilename'|get_lang }}
</div>
<div id="record-audio-recordrtc" class="row text-center">
<form>
<div class="row">
<div class="col-sm-4 col-sm-offset-4">
<div class="form-group">
<span class="fa fa-microphone fa-5x fa-fw" aria-hidden="true"></span>
<span class="sr-only">{{ 'RecordAudio'|get_lang }}</span>
<div id="timer" style="display: none">
<h2>
<div class="label label-danger">
<span id="hour">00</span>
<span class="divider">:</span>
<span id="minute">00</span>
<span class="divider">:</span>
<span id="second">00</span>
</div>
</h2>
</div>
</div>
<div class="form-group">
<input type="text" name="audio_title" id="audio-title-rtc" class="form-control" placeholder="{{ 'InputNameHere'|get_lang }}">
</div>
</div>
</div>
<div class="text-center">
<div class="form-group">
<button class="btn btn-primary" type="button" id="btn-start-record">
<span class="fa fa-circle fa-fw" aria-hidden="true"></span> {{ 'StartRecordingAudio'|get_lang }}
</button>
<button class="btn btn-danger hidden" type="button" id="btn-stop-record" disabled>
<span class="fa fa-square fa-fw" aria-hidden="true"></span> {{ 'StopRecordingAudio'|get_lang }}
</button>
<button class="btn btn-success hidden" type="button" id="btn-save-record"
data-loadingtext="{{ 'Uploading'|get_lang }}" disabled>
<span class="fa fa-send fa-fw" aria-hidden="true"></span> {{ 'SaveRecordedAudio'|get_lang }}
</button>
</div>
<div class="form-group">
<audio class="skip hidden center-block" controls id="record-preview"></audio>
</div>
</div>
</form>
</div>
<div class="row" id="record-audio-wami">
<div class="col-sm-3 col-sm-offset-3">
<br>
<form>
<div class="form-group">
<input type="text" name="audio_title" id="audio-title-wami" class="form-control" placeholder="{{ 'InputNameHere'|get_lang }}">
</div>
<div class="form-group text-center">
<button class="btn btn-primary" type="button" id="btn-activate-wami">
<span class="fa fa-check fa-fw" aria-hidden=""></span> {{ 'Activate'|get_lang }}
</button>
</div>
</form>
</div>
<div class="col-sm-3">
<div id="record-audio-wami-container" class="wami-container"></div>
</div>
</div>
<script>
$(function () {
RecordAudio.init(
{
blockId: '#record-audio-recordrtc',
btnStartId: '#btn-start-record',
btnPauseId: '#btn-pause-record',
btnPlayId: '#btn-play-record',
btnStopId: '#btn-stop-record',
btnSaveId: '#btn-save-record',
plyrPreviewId: '#record-preview',
cidReq :'{{ course_url_params }}',
directory: '{{ directory }}',
reload_page: 1,
type: 'document',
recordAudioUrl: '{{ url('legacy_main', {name: 'inc/ajax/record_audio_rtc.ajax.php'}) }}',
},
null
);
});
</script>

@ -71,11 +71,12 @@
btnPlayId: '#btn-play-record-{{ question_id }}',
btnStopId: '#btn-stop-record-{{ question_id }}',
plyrPreviewId: '#record-preview-{{ question_id }}',
directory: '{{ directory }}',
type: 'document',
type: '{{ type }}',
tExerciseId: parseInt('{{ t_exercise_id }}') || 0,
questionId: parseInt('{{ question_id }}') || 0,
recordAudioUrl: '{{ url('legacy_main', {name: 'inc/ajax/record_audio_rtc.ajax.php'}) }}',
cidReq: '{{ course_url_params }}'
}, '{{ file_name }}');
});
if (0 === $('#hide_description_{{ question_id }}_options').length) {
$('#hide_description_{{ question_id }}').remove();

@ -1,66 +0,0 @@
<div id="record-audio-recordrtc" class="row text-center">
<form>
<div class="row">
<div class="col-sm-4 col-sm-offset-4">
<div class="form-group">
<span class="fa fa-microphone fa-5x fa-fw" aria-hidden="true"></span>
<span class="sr-only">{{ 'RecordAudio'|get_lang }}</span>
</div>
<input type="hidden" name="audio_title" id="audio-title-rtc" value="{{ audio_title }}">
</div>
</div>
<div class="text-center">
<div class="form-group">
<button class="btn btn-default" type="button" id="btn-start-record">
<span class="fa fa-circle fa-fw" aria-hidden="true"></span> {{ 'StartRecordingAudio'|get_lang }}
</button>
<button class="btn btn-danger hidden" type="button" id="btn-stop-record" disabled>
<span class="fa fa-square fa-fw" aria-hidden="true"></span> {{ 'StopRecordingAudio'|get_lang }}
</button>
</div>
<div class="form-group">
<audio class="skip hidden center-block" controls id="record-preview"></audio>
</div>
</div>
</form>
</div>
<div class="row" id="record-audio-wami">
<div class="col-sm-3 col-sm-offset-3">
<br>
<form>
<div class="form-group">
<input type="hidden" name="audio_title" id="audio-title-wami" value="{{ audio_title }}">
</div>
<div class="form-group text-center">
<button class="btn btn-default" type="button" id="btn-activate-wami">
<span class="fa fa-check fa-fw" aria-hidden=""></span> {{ 'Activate'|get_lang }}
</button>
</div>
</form>
</div>
<div class="col-sm-3">
<div id="record-audio-wami-container" class="wami-container"></div>
</div>
</div>
<script>
$(function () {
RecordAudio.init(
{
blockId: '#record-audio-recordrtc',
btnStartId: '#btn-start-record',
btnPauseId: '#btn-pause-record',
btnPlayId: '#btn-play-record',
btnStopId: '#btn-stop-record',
btnSaveId: '',
plyrPreviewId: '#record-preview',
directory: '{{ directory }}',
reload_page: '{{ reload_page }}',
type: 'message',
recordAudioUrl: '{{ url('legacy_main', {name: 'inc/ajax/record_audio_rtc.ajax.php'}) }}',
},
null
);
});
</script>

@ -9,6 +9,7 @@ namespace Chamilo\CoreBundle\Entity;
use DateTime;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
@ -470,4 +471,24 @@ class TrackExercise
return $this;
}
public function getAttemptByQuestionId(int $questionId): ?TrackEAttempt
{
$criteria = Criteria::create();
$criteria
->where(
Criteria::expr()->eq('questionId', $questionId)
)
->setMaxResults(1)
;
/** @var TrackEAttempt $attempt */
$attempt = $this->attempts->matching($criteria)->first();
if (!empty($attempt)) {
return $attempt;
}
return null;
}
}

Loading…
Cancel
Save