From 9b1f1e6b69ce98fc2721affd5117d52f6c081568 Mon Sep 17 00:00:00 2001 From: Christian Beeznest Date: Tue, 3 Dec 2024 00:59:10 -0500 Subject: [PATCH] Internal: Fix CSV export formatting and score calculation issues - refs BT#22096 --- public/main/inc/lib/export.lib.inc.php | 45 +++- public/main/inc/lib/sessionmanager.lib.php | 290 +++++++++------------ public/main/inc/lib/tracking.lib.php | 19 -- public/main/my_space/myStudents.php | 2 +- 4 files changed, 157 insertions(+), 199 deletions(-) diff --git a/public/main/inc/lib/export.lib.inc.php b/public/main/inc/lib/export.lib.inc.php index 07178e8ba8..3396dede15 100644 --- a/public/main/inc/lib/export.lib.inc.php +++ b/public/main/inc/lib/export.lib.inc.php @@ -26,14 +26,9 @@ class Export /** * Export tabular data to CSV-file. * - * @param array $data - * @param string $filename - * @param bool $writeOnly Whether to only write on disk or also send for download - * @param string $enclosure - * * @return mixed csv raw data | false if no data to export | string file path if success in $writeOnly mode */ - public static function arrayToCsv($data, $filename = 'export', $writeOnly = false, $enclosure = '"') + public static function arrayToCsv(array $data, string $filename = 'export', bool $writeOnly = false, string $enclosure = '"') { if (empty($data)) { return false; @@ -55,6 +50,44 @@ class Export return $filePath; } + /** + * Converts an array of data into a CSV file and optionally sends it for download. + * + * @return string|void Returns the file path if $writeOnly is true, otherwise sends the file for download and exits. + */ + public static function arrayToCsvSimple(array $data, string $filename = 'export', bool $writeOnly = false) + { + $file = api_get_path(SYS_ARCHIVE_PATH) . uniqid('') . '.csv'; + + $handle = fopen($file, 'w'); + + if ($handle === false) { + throw new \RuntimeException("Unable to create or open the file: $file"); + } + + if (is_array($data)) { + foreach ($data as $row) { + $line = ''; + if (is_array($row)) { + foreach ($row as $value) { + $line .= '"' . str_replace('"', '""', (string)$value) . '";'; + } + } + fwrite($handle, rtrim($line, ';') . "\n"); + } + } + + fclose($handle); + + if (!$writeOnly) { + DocumentManager::file_send_for_download($file, true, $filename . '.csv'); + unlink($file); + exit; + } + + return $file; + } + /** * Export tabular data to XLS-file. */ diff --git a/public/main/inc/lib/sessionmanager.lib.php b/public/main/inc/lib/sessionmanager.lib.php index 9359acf2b6..851c384018 100644 --- a/public/main/inc/lib/sessionmanager.lib.php +++ b/public/main/inc/lib/sessionmanager.lib.php @@ -10232,38 +10232,9 @@ class SessionManager */ public static function exportSessionsAsCSV(array $selectedSessions): void { - $csvHeaders = []; - $csvHeaders[] = get_lang('Session name'); - $csvHeaders[] = get_lang('Session start date'); - $csvHeaders[] = get_lang('Session end date'); - $csvHeaders[] = get_lang('Course name'); - $csvHeaders[] = get_lang('Official code'); - - if (api_sort_by_first_name()) { - $csvHeaders[] = get_lang('First name'); - $csvHeaders[] = get_lang('Last name'); - } else { - $csvHeaders[] = get_lang('Last name'); - $csvHeaders[] = get_lang('First name'); - } - - $csvHeaders[] = get_lang('Login'); - $csvHeaders[] = get_lang('Training time'); - $csvHeaders[] = get_lang('Course progress'); - $csvHeaders[] = get_lang('Exercise progress'); - $csvHeaders[] = get_lang('Exercise average'); - $csvHeaders[] = get_lang('Score'); - $csvHeaders[] = get_lang('Score').' - '.get_lang('Best attempt'); - $csvHeaders[] = get_lang('Student_publication'); - $csvHeaders[] = get_lang('Messages'); - $csvHeaders[] = get_lang('Classes'); - $csvHeaders[] = get_lang('Registration date'); - $csvHeaders[] = get_lang('FirstLogin in course'); - $csvHeaders[] = get_lang('Latest login in course'); - $csvHeaders[] = get_lang('Lp finalization date'); - $csvHeaders[] = get_lang('Quiz finalization date'); - $csvData = []; + $headersGenerated = false; + $csvHeaders = []; foreach ($selectedSessions as $sessionId) { $courses = SessionManager::get_course_list_by_session_id($sessionId); @@ -10271,30 +10242,24 @@ class SessionManager if (!empty($courses)) { foreach ($courses as $course) { $courseCode = $course['course_code']; + $courseId = $course['id']; $studentList = CourseManager::get_student_list_from_course_code( $courseCode, true, $sessionId ); - $nbStudents = count($studentList); $userIds = array_keys($studentList); - $csvContentInSession = TrackingCourseLog::getUserData( - null, - $nbStudents, - null, - null, - [], - false, - true, - $courseCode, - $sessionId, - true, - $userIds - ); - if (!empty($csvContentInSession)) { - $csvData = array_merge($csvData, $csvContentInSession); + [$generatedHeaders, $csvContent] = self::generateSessionCourseReportData($sessionId, $courseId, $userIds); + + if (!$headersGenerated) { + $csvHeaders = $generatedHeaders; + $headersGenerated = true; + } + + foreach ($csvContent as $row) { + $csvData[] = $row; } } } @@ -10302,8 +10267,8 @@ class SessionManager if (!empty($csvData)) { array_unshift($csvData, $csvHeaders); - $filename = 'export_session_courses_reports_complete_' . api_get_local_time(); - Export::arrayToCsv($csvData, $filename); + $filename = 'export_session_courses_reports_complete_' . date('Y-m-d_H-i-s') . '.csv'; + Export::arrayToCsvSimple($csvData, $filename); exit; } } @@ -10313,161 +10278,140 @@ class SessionManager */ public static function exportSessionsAsZip(array $sessionList): void { - // Create a temporary ZIP file $tempZipFile = api_get_path(SYS_ARCHIVE_PATH) . api_get_unique_id() . '.zip'; $tempDir = dirname($tempZipFile); - // Check if the directory exists and has write permissions if (!is_dir($tempDir) || !is_writable($tempDir)) { exit("The directory for creating the ZIP file does not exist or lacks write permissions: $tempDir"); } - // Create a new ZIP archive $zip = new \ZipArchive(); - - // Try to open the ZIP file for writing if ($zip->open($tempZipFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) { exit("Unable to open the ZIP file for writing: $tempZipFile"); } - $csvList = []; - - // Process each session in the list foreach ($sessionList as $sessionItemId) { - $em = Database::getManager(); - $sessionRepository = $em->getRepository(Session::class); - $session = $sessionRepository->find($sessionItemId); + $courses = SessionManager::get_course_list_by_session_id($sessionItemId); - if ($session->getNbrCourses() > 0) { - $courses = $session->getCourses(); - $courseList = []; - - // Collect courses for the session - foreach ($courses as $sessionRelCourse) { - $courseList[] = $sessionRelCourse->getCourse(); - } - - foreach ($courseList as $course) { - $courseId = $course->getId(); - $courseInfo = api_get_course_info_by_id($courseId); - $addExerciseOption = api_get_configuration_value('add_exercise_best_attempt_in_report'); - $sortByFirstName = api_sort_by_first_name(); - $bestScoreLabel = get_lang('Score') . ' - ' . get_lang('Best attempt'); - $courseCode = $courseInfo['code']; - - // Prepare CSV headers - $csvHeaders = []; - $csvHeaders[] = get_lang('Official code'); - - if ($sortByFirstName) { - $csvHeaders[] = get_lang('First name'); - $csvHeaders[] = get_lang('Last name'); - } else { - $csvHeaders[] = get_lang('Last name'); - $csvHeaders[] = get_lang('First name'); - } - - $csvHeaders[] = get_lang('Login'); - $csvHeaders[] = get_lang('Training time'); - $csvHeaders[] = get_lang('Course progress'); - $csvHeaders[] = get_lang('Exercise progress'); - $csvHeaders[] = get_lang('Exercise average'); - $csvHeaders[] = get_lang('Score'); - $csvHeaders[] = $bestScoreLabel; - - // Include exercise results if available - $exerciseResultHeaders = []; - if (!empty($addExerciseOption) && isset($addExerciseOption['courses']) && - isset($addExerciseOption['courses'][$courseCode])) { - foreach ($addExerciseOption['courses'][$courseCode] as $exerciseId) { - $exercise = new Exercise(); - $exercise->read($exerciseId); - if ($exercise->iid) { - $title = get_lang('Exercise') . ': ' . $exercise->get_formated_title(); - $csvHeaders[] = $title; - $exerciseResultHeaders[] = $title; - } - } - } - - // Add more fields to the CSV headers - $csvHeaders[] = get_lang('Student_publication'); - $csvHeaders[] = get_lang('Messages'); - $csvHeaders[] = get_lang('Classes'); - $csvHeaders[] = get_lang('Registration date'); - $csvHeaders[] = get_lang('First login in course'); - $csvHeaders[] = get_lang('Latest login in course'); - - // Get the list of students for the course + if (!empty($courses)) { + foreach ($courses as $course) { + $courseCode = $course['course_code']; + $courseId = $course['id']; $studentList = CourseManager::get_student_list_from_course_code($courseCode, true, $sessionItemId); - $nbStudents = count($studentList); - - // Pass the necessary data as parameters instead of using globals $userIds = array_keys($studentList); - // Get the user data for CSV content - $csvContentInSession = TrackingCourseLog::getUserData( - null, - $nbStudents, - null, - null, - [], - true, - true, - $courseCode, - $sessionItemId, - true, - $userIds - ); - array_unshift($csvContentInSession, $csvHeaders); + [$csvHeaders, $csvContent] = self::generateSessionCourseReportData($sessionItemId, $courseId, $userIds); + array_unshift($csvContent, $csvHeaders); - // Get session info and dates $sessionInfo = api_get_session_info($sessionItemId); - $sessionDates = SessionManager::parseSessionDates($session); - - // Add session name and dates to the CSV content - array_unshift($csvContentInSession, [get_lang('Date'), $sessionDates['access']]); - array_unshift($csvContentInSession, [get_lang('SessionName'), Security::remove_XSS($sessionInfo['name'])]); - - // Prepare CSV file information - $csvList[] = [ - 'session_id' => $sessionItemId, - 'session_name' => $session->getTitle(), - 'course_id' => $courseId, - 'course_name' => $courseInfo['name'], - 'path' => Export::arrayToCsv($csvContentInSession, '', true), // Generate the CSV - ]; - } - } - } + $courseInfo = api_get_course_info_by_id($courseId); + $csvFileName = $sessionInfo['name'] . '_' . $courseInfo['name'] . '.csv'; - // Add the generated CSV files to the ZIP archive - foreach ($csvList as $csv) { - $newFileName = $csv['session_id'] . '_' . $csv['session_name'] . '-' . $csv['course_id'] . '_' . $csv['course_name'] . '.csv'; + $csvFilePath = Export::arrayToCsvSimple($csvContent, $csvFileName, true); - if (file_exists($csv['path'])) { - $zip->addFile($csv['path'], $newFileName); + if ($csvFilePath && file_exists($csvFilePath)) { + $zip->addFile($csvFilePath, $csvFileName); + } + } } } - // Close the ZIP file - if ($zip->close() === false) { + if (!$zip->close()) { exit("Could not close the ZIP file correctly."); } - // Clean up the CSV files after adding them to the ZIP - foreach ($csvList as $csv) { - if (file_exists($csv['path'])) { - unlink($csv['path']); - } - } - - // Send the ZIP file for download if (file_exists($tempZipFile)) { DocumentManager::file_send_for_download($tempZipFile, true); - unlink($tempZipFile); // Delete the temporary ZIP file after download + unlink($tempZipFile); } else { exit("The ZIP file was not created correctly."); } } + + private static function generateSessionCourseReportData($sessionId, $courseId, $userIds): array + { + $em = Database::getManager(); + $sessionRepository = $em->getRepository(Session::class); + $session = $sessionRepository->find($sessionId); + + if (!$session instanceof Session) { + throw new \InvalidArgumentException("Invalid session object for session ID $sessionId"); + } + + $courseInfo = api_get_course_info_by_id($courseId); + $courseCode = $courseInfo['code']; + + $csvHeaders = [ + get_lang('Session name'), + get_lang('Session access dates'), + get_lang('Session display dates'), + get_lang('Course name'), + get_lang('Official code'), + get_lang('First name'), + get_lang('Last name'), + get_lang('Login'), + get_lang('Training time'), + get_lang('Course progress'), + get_lang('Exercise progress'), + get_lang('Exercise average'), + get_lang('Score'), + get_lang('Score') . ' - ' . get_lang('Best attempt'), + get_lang('Student_publication'), + get_lang('Messages'), + get_lang('Classes'), + get_lang('Registration date'), + get_lang('First login in course'), + get_lang('Latest login in course'), + ]; + + $csvData = TrackingCourseLog::getUserData( + null, + count($userIds), + null, + null, + [], + true, + true, + $courseCode, + $sessionId, + true, + $userIds + ); + + $rawCsvContent = ChamiloSession::read('csv_content'); + + if (empty($rawCsvContent)) { + throw new \RuntimeException("No CSV content found in session for course $courseCode and session $sessionId."); + } + + $csvContent = []; + foreach ($rawCsvContent as $row) { + $alignedRow = [ + $row['session_name'] ?? '', + $row['session_startdate'] ?? '', + $row['session_enddate'] ?? '', + $row['course_name'] ?? '', + $row['official_code'] ?? '', + $row['firstname'] ?? '', + $row['lastname'] ?? '', + $row['username'] ?? '', + $row['time'] ?? '', + $row['average_progress'] ?? '', + $row['exercise_progress'] ?? '', + $row['exercise_average'] ?? '', + $row['student_score'] ?? '', + $row['student_score_best'] ?? '', + $row['count_assignments'] ?? '', + $row['count_messages'] ?? '', + $row['classes'] ?? '', + $row['registered_at'] ?? '', + $row['first_connection'] ?? '', + $row['last_connection'] ?? '', + ]; + $csvContent[] = $alignedRow; + } + + return [$csvHeaders, $csvContent]; + } + } diff --git a/public/main/inc/lib/tracking.lib.php b/public/main/inc/lib/tracking.lib.php index b4daec58eb..763af042d2 100644 --- a/public/main/inc/lib/tracking.lib.php +++ b/public/main/inc/lib/tracking.lib.php @@ -2899,25 +2899,6 @@ class Tracking } $sessionCondition = api_get_session_condition($sessionId); - //$sessionId = (int) $sessionId; - /*if (count($lp_ids) > 0) { - $condition_session = " AND session_id = $sessionId "; - } else { - $condition_session = " WHERE session_id = $sessionId "; - } - - // Check the real number of LPs corresponding to the filter in the - // database (and if no list was given, get them all) - if (empty($sessionId)) { - $sql = "SELECT DISTINCT(iid), use_max_score - FROM $lp_table - WHERE - c_id = $courseId AND - (session_id = 0 OR session_id IS NULL) $condition_lp "; - } else { - - }*/ - $lp_list = $use_max_score = []; if (empty($condition_lp)) { $repo = Container::getLpRepository(); diff --git a/public/main/my_space/myStudents.php b/public/main/my_space/myStudents.php index cffe3e1da3..69fca2ae0e 100644 --- a/public/main/my_space/myStudents.php +++ b/public/main/my_space/myStudents.php @@ -350,7 +350,7 @@ switch ($action) { $studentId, $course, [], - $sId, + api_get_session_entity($sId), false, false, true