diff --git a/public/main/exercise/exercise.class.php b/public/main/exercise/exercise.class.php index 020e4ebb755..12f745501c2 100644 --- a/public/main/exercise/exercise.class.php +++ b/public/main/exercise/exercise.class.php @@ -8072,7 +8072,7 @@ public function getExerciseAndResult($courseId, $sessionId, $quizId = []) INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id WHERE - te.id = %s AND + te.c_id = %d AND te.session_id = %s AND cq.id IN (%s) ORDER BY cq.id"; @@ -8082,7 +8082,7 @@ public function getExerciseAndResult($courseId, $sessionId, $quizId = []) $sql = "SELECT * FROM $track_exercises te INNER JOIN c_quiz cq ON cq.id = te.exe_exo_id AND te.c_id = cq.c_id WHERE - te.id = %s AND + te.c_id = %d AND cq.id IN (%s) ORDER BY cq.id"; $sql = sprintf($sql, $courseId, $ids); diff --git a/public/main/exercise/exercise_report.php b/public/main/exercise/exercise_report.php index 9ef79915af3..1c4cc3ce04d 100644 --- a/public/main/exercise/exercise_report.php +++ b/public/main/exercise/exercise_report.php @@ -62,6 +62,7 @@ $exercise_id = isset($_REQUEST['exerciseId']) ? (int) $_REQUEST['exerciseId'] : (isset($_GET['id']) ? (int) $_GET['id'] : 0); $locked = api_resource_is_locked_by_gradebook($exercise_id, LINK_EXERCISE); $sessionId = api_get_session_id(); +$action = $_REQUEST['action'] ?? null; if (empty($exercise_id)) { api_not_allowed(true); @@ -94,6 +95,27 @@ $parameters['path'] = Security::remove_XSS($_GET['path']); } +switch ($action) { + case 'export_all_results': + $sessionId = api_get_session_id(); + $courseId = api_get_course_int_id(); + ExerciseLib::exportExerciseAllResultsZip($sessionId, $courseId, $exercise_id); + + break; + case 'export_pdf': + $exerciseId = (int) $_GET['exerciseId']; + $attemptId = (int) $_GET['attemptId']; + $userId = (int) $_GET['userId']; + $urlExportPdf = api_get_path(WEB_PATH).'main/exercise/exercise_show.php?'.api_get_cidreq().'&id='.$attemptId.'&action=export&export_type=result_pdf'; + + if (!$exerciseId || !$attemptId) { + api_not_allowed(true); + } + + header('Location: '.$urlExportPdf); + exit; +} + if (!empty($_REQUEST['export_report']) && '1' == $_REQUEST['export_report']) { if (api_is_platform_admin() || api_is_course_admin() || api_is_course_tutor() || api_is_session_general_coach() @@ -431,9 +453,13 @@ $actions .= ''. Display::getMdiIcon('content-save', 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Export')).''; $actions .= Display::url( - Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('RecalculateResults')), + Display::getMdiIcon(ActionIcon::REFRESH, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Recalculate Results')), api_get_path(WEB_CODE_PATH).'exercise/recalculate_all.php?'.api_get_cidreq()."&exercise=$exercise_id" ); + $actions .= Display::url( + Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_MEDIUM, get_lang('Export all attempts')), + api_get_self().'?'.api_get_cidreq().'&action=export_all_results&exerciseId='.$exercise_id + ); // clean result before a selected date icon if ($allowClean) { diff --git a/public/main/exercise/exercise_show.php b/public/main/exercise/exercise_show.php index 4e282686528..33844852610 100644 --- a/public/main/exercise/exercise_show.php +++ b/public/main/exercise/exercise_show.php @@ -21,6 +21,7 @@ $currentUserId = api_get_user_id(); $printHeaders = 'learnpath' === $origin; $id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0; //exe id +$exportTypeAllResults = ('export' === $_GET['action'] && (in_array($_GET['export_type'], ['all_results', 'result_pdf']))); if (empty($id)) { api_not_allowed(true); @@ -39,15 +40,17 @@ $learnpath_item_id = $track_exercise_info['orig_lp_item_id']; $lp_item_view_id = $track_exercise_info['orig_lp_item_view_id']; $isBossOfStudent = false; -if (api_is_student_boss()) { - // Check if boss has access to user info. - if (UserManager::userIsBossOfStudent($currentUserId, $student_id)) { - $isBossOfStudent = true; +if (!$exportTypeAllResults) { + if (api_is_student_boss()) { + // Check if boss has access to user info. + if (UserManager::userIsBossOfStudent($currentUserId, $student_id)) { + $isBossOfStudent = true; + } else { + api_not_allowed($printHeaders); + } } else { - api_not_allowed($printHeaders); + api_protect_course_script($printHeaders, false, true); } -} else { - api_protect_course_script($printHeaders, false, true); } // Database table definitions @@ -80,6 +83,7 @@ if (empty($questionList)) { $questionList = Session::read('questionList'); } +/* @var Exercise $objExercise */ if (empty($objExercise)) { $objExercise = Session::read('objExercise'); } @@ -93,7 +97,8 @@ api_is_course_tutor() || api_is_session_admin() || api_is_drh() || - api_is_student_boss(); + api_is_student_boss() || + $exportTypeAllResults; if (!empty($sessionId) && !$is_allowedToEdit) { if (api_is_course_session_coach( @@ -973,7 +978,24 @@ class="exercise_mark_select" 'orientation' => 'P', ]; $pdf = new PDF('A4', $params['orientation'], $params); - $pdf->html_to_pdf_with_template($content, false, false, true); + if ('all_results' === $_GET['export_type']) { + $sessionId = api_get_session_id(); + $courseId = api_get_course_int_id(); + $exportName = 'S'.$sessionId.'-C'.$courseId.'-T'.$exercise_id; + $baseDir = api_get_path(SYS_ARCHIVE_PATH); + $folderName = 'pdfexport-'.$exportName; + $exportFolderPath = $baseDir.$folderName; + if (!is_dir($exportFolderPath)) { + @mkdir($exportFolderPath); + } + $pdfFileName = $user_info['firstname'].' '.$user_info['lastname'].'-attemptId'.$id.'.pdf'; + $pdfFileName = api_replace_dangerous_char($pdfFileName); + $fileNameToSave = $exportFolderPath.'/'.$pdfFileName; + $pdf->html_to_pdf_with_template($content, true, false, true, [], 'F', $fileNameToSave); + } else { + $pdf->html_to_pdf_with_template($content, false, false, true); + } + exit; } diff --git a/public/main/inc/lib/exercise.lib.php b/public/main/inc/lib/exercise.lib.php index c363f6cef2d..918f890dc9b 100644 --- a/public/main/inc/lib/exercise.lib.php +++ b/public/main/inc/lib/exercise.lib.php @@ -2351,6 +2351,13 @@ public static function get_exam_results_data( ] ); + $exportPdfUrl = api_get_path(WEB_CODE_PATH).'exercise/exercise_report.php?'. + api_get_cidreq().'&exerciseId='.$exercise_id.'&action=export_pdf&attemptId='.$id.'&userId='.(int) $results[$i]['exe_user_id']; + $actions .= '' + .Display::getMdiIcon(ActionIcon::EXPORT_PDF, 'ch-tool-icon', null, ICON_SIZE_SMALL, get_lang('Export to PDF')) + .''; + + $filterByUser = isset($_GET['filter_by_user']) ? (int) $_GET['filter_by_user'] : 0; $delete_link = 'getExerciseAndResult( + $courseId, + $sessionId, + $exerciseId + ); + + $exportOk = false; + if (!empty($exeResults)) { + $exportName = 'S'.$sessionId.'-C'.$courseId.'-T'.$exerciseId; + $baseDir = api_get_path(SYS_ARCHIVE_PATH); + $folderName = 'pdfexport-'.$exportName; + $exportFolderPath = $baseDir.$folderName; + + // 1. Cleans the export folder if it exists. + if (is_dir($exportFolderPath)) { + rmdirr($exportFolderPath); + } + + // 2. Create the pdfs inside a new export folder path. + foreach ($exeResults as $exeResult) { + $exeId = (int) $exeResult['exe_id']; + self::saveFileExerciseResultPdf($exeId, $courseId, $sessionId); + } + + // 3. If export folder is not empty will be zipped. + $isFolderPathEmpty = (file_exists($exportFolderPath) && 2 == count(scandir($exportFolderPath))); + if (is_dir($exportFolderPath) && !$isFolderPathEmpty) { + $exportOk = true; + $exportFilePath = $baseDir.$exportName.'.zip'; + $zip = new \ZipArchive(); + if ($zip->open($exportFilePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) === true) { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($exportFolderPath), + RecursiveIteratorIterator::LEAVES_ONLY + ); + + foreach ($files as $name => $file) { + if (!$file->isDir()) { + $filePath = $file->getRealPath(); + $relativePath = substr($filePath, strlen($exportFolderPath) + 1); + $zip->addFile($filePath, $relativePath); + } + } + + $zip->close(); + } else { + throw new Exception('Failed to create ZIP file'); + } + + rmdirr($exportFolderPath); + + if (!empty($mainPath) && file_exists($exportFilePath)) { + @rename($exportFilePath, $mainPath.'/'.$exportName.'.zip'); + } else { + DocumentManager::file_send_for_download($exportFilePath, true, $exportName.'.zip'); + exit; + } + } + } + + if (empty($mainPath) && !$exportOk) { + Display::addFlash( + Display::return_message( + get_lang('ExportExerciseNoResult'), + 'warning', + false + ) + ); + } + + return false; + } + + /** + * Generates and saves a PDF file for a specific exercise attempt result. + */ + public static function saveFileExerciseResultPdf( + int $exeId, + int $courseId, + int $sessionId + ): void + { + $cidReq = 'cid='.$courseId.'&sid='.$sessionId.'&gid=0&gradebook=0'; + $url = api_get_path(WEB_PATH).'main/exercise/exercise_show.php?'.$cidReq.'&id='.$exeId.'&action=export&export_type=all_results'; + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_COOKIE, session_id()); + curl_setopt($ch, CURLOPT_AUTOREFERER, true); + curl_setopt($ch, CURLOPT_COOKIESESSION, true); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false); + curl_setopt($ch, CURLOPT_HEADER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + + $result = curl_exec($ch); + + if (false === $result) { + error_log('saveFileExerciseResultPdf error: '.curl_error($ch)); + } + + curl_close($ch); + } } diff --git a/public/main/inc/lib/pdf.lib.php b/public/main/inc/lib/pdf.lib.php index 991df08071a..9c10155b7e2 100644 --- a/public/main/inc/lib/pdf.lib.php +++ b/public/main/inc/lib/pdf.lib.php @@ -102,7 +102,9 @@ public function html_to_pdf_with_template( $saveToFile = false, $returnHtml = false, $addDefaultCss = false, - $extraRows = [] + $extraRows = [], + $outputMode = 'D', + $fileToSave = null ) { if (empty($this->template)) { $tpl = new Template('', false, false, false, false, true, false); @@ -170,9 +172,9 @@ public function html_to_pdf_with_template( $css, $this->params['filename'], $this->params['course_code'], - 'D', + $outputMode, $saveToFile, - null, + $fileToSave, $returnHtml, $addDefaultCss );