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