diff --git a/public/main/exercise/exercise.class.php b/public/main/exercise/exercise.class.php index cb3584a10b..bc5456f25e 100644 --- a/public/main/exercise/exercise.class.php +++ b/public/main/exercise/exercise.class.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, diff --git a/public/main/exercise/export/scorm/scorm_classes.php b/public/main/exercise/export/scorm/scorm_classes.php index 6518221722..4087a8df7d 100644 --- a/public/main/exercise/export/scorm/scorm_classes.php +++ b/public/main/exercise/export/scorm/scorm_classes.php @@ -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 = ''.get_lang('This learning object or activity is not SCORM compliant. That\'s why it is not exportable.').''; return [$js, $html]; diff --git a/public/main/exercise/oral_expression.class.php b/public/main/exercise/oral_expression.class.php index 4516771a4e..384364b86d 100644 --- a/public/main/exercise/oral_expression.class.php +++ b/public/main/exercise/oral_expression.class.php @@ -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(); } } diff --git a/public/main/inc/ajax/record_audio_rtc.ajax.php b/public/main/inc/ajax/record_audio_rtc.ajax.php index 4e09f4209d..2f95ea64d2 100644 --- a/public/main/inc/ajax/record_audio_rtc.ajax.php +++ b/public/main/inc/ajax/record_audio_rtc.ajax.php @@ -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'); } diff --git a/public/main/inc/lib/exercise.lib.php b/public/main/inc/lib/exercise.lib.php index 0161f46d5a..c218654634 100644 --- a/public/main/inc/lib/exercise.lib.php +++ b/public/main/inc/lib/exercise.lib.php @@ -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); diff --git a/public/main/inc/lib/javascript/record_audio/record_audio.js b/public/main/inc/lib/javascript/record_audio/record_audio.js index f8553123fa..b59708c5b8 100644 --- a/public/main/inc/lib/javascript/record_audio/record_audio.js +++ b/public/main/inc/lib/javascript/record_audio/record_audio.js @@ -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('&', '&'); $.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; } diff --git a/public/main/template/default/document/record_audio.html.twig b/public/main/template/default/document/record_audio.html.twig deleted file mode 100644 index a26da4c04f..0000000000 --- a/public/main/template/default/document/record_audio.html.twig +++ /dev/null @@ -1,88 +0,0 @@ -
- {{ 'WamiNeedFilename'|get_lang }} -
- -
-
-
-
-
- - {{ 'RecordAudio'|get_lang }} - -
-
- -
-
-
-
-
- - - -
-
- -
-
-
-
- -
-
-
-
-
- -
-
- -
-
-
-
-
-
-
- - diff --git a/public/main/template/default/exercise/oral_expression.html.twig b/public/main/template/default/exercise/oral_expression.html.twig index 25b5c59eb7..ac1f549979 100644 --- a/public/main/template/default/exercise/oral_expression.html.twig +++ b/public/main/template/default/exercise/oral_expression.html.twig @@ -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(); diff --git a/public/main/template/default/message/record_audio.html.twig b/public/main/template/default/message/record_audio.html.twig deleted file mode 100644 index 7bd8fd05a1..0000000000 --- a/public/main/template/default/message/record_audio.html.twig +++ /dev/null @@ -1,66 +0,0 @@ -
-
-
-
-
- - {{ 'RecordAudio'|get_lang }} -
- -
-
-
-
- - -
-
- -
-
-
-
- -
-
-
-
-
- -
-
- -
-
-
-
-
-
-
- - diff --git a/src/CoreBundle/Entity/TrackExercise.php b/src/CoreBundle/Entity/TrackExercise.php index 2cdefbd1dd..c92b1fd2b5 100644 --- a/src/CoreBundle/Entity/TrackExercise.php +++ b/src/CoreBundle/Entity/TrackExercise.php @@ -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; + } }