From 67ff7589a51b0c0f10a26e9fe7326a4d7d502eac Mon Sep 17 00:00:00 2001 From: Christian Date: Sat, 29 Apr 2023 17:08:13 -0500 Subject: [PATCH 1/4] Exercise: Add option to export results attempts in zip - refs BT#20691 --- main/admin/export_exercise_results.php | 170 +++++++++++++++++++++++++ main/admin/index.php | 5 + main/exercise/exercise.class.php | 28 ++-- main/exercise/exercise_report.php | 15 +++ main/exercise/exercise_show.php | 39 ++++-- main/inc/lib/exercise.lib.php | 72 +++++++++++ main/inc/lib/pdf.lib.php | 8 +- 7 files changed, 315 insertions(+), 22 deletions(-) create mode 100644 main/admin/export_exercise_results.php diff --git a/main/admin/export_exercise_results.php b/main/admin/export_exercise_results.php new file mode 100644 index 0000000000..8699936de2 --- /dev/null +++ b/main/admin/export_exercise_results.php @@ -0,0 +1,170 @@ + 'index.php', 'name' => get_lang('PlatformAdmin')]; + +$confirmYourChoice = addslashes(get_lang('ConfirmYourChoice')); +$htmlHeadXtra[] = " +"; + +$sessionSelectList = [0 => get_lang('Select')]; +foreach ($sessionList as $item) { + $sessionSelectList[$item['session_id']] = $item['session_name']; +} + +$courseSelectList = [0 => get_lang('Select')]; +foreach ($courseList as $item) { + $courseItemId = $item['real_id']; + $courseInfo = api_get_course_info_by_id($courseItemId); + $courseSelectList[$courseItemId] = ''; + if ($courseItemId == $courseId) { + $courseSelectList[$courseItemId] = '>    '; + } + $courseSelectList[$courseItemId] = $courseInfo['title']; +} + +// If course has changed, reset the menu default +if (!empty($courseSelectList) && !in_array($courseId, array_keys($courseSelectList))) { + $courseId = 0; +} + +$courseInfo = api_get_course_info_by_id($courseId); + +// Get exercise list for this course +$exerciseList = ExerciseLib::get_all_exercises_for_course_id( + $courseInfo, + $sessionId, + $courseId, + false +); + +$exerciseSelectList = []; +$exerciseSelectList = [0 => get_lang('Select')]; +if (is_array($exerciseList)) { + foreach ($exerciseList as $row) { + $exerciseTitle = $row['title']; + $exerciseSelectList[$row['iid']] = $exerciseTitle; + } +} + +$url = api_get_self().'?'.api_get_cidreq().'&'.http_build_query( + [ + 'session_id' => $sessionId, + 'selected_course' => $courseId, + 'exerciseId' => $exerciseId, + 'course_id_changed' => $courseIdChanged, + 'exercise_id_changed' => $exerciseIdChanged, + ] + ); + +// Form +$form = new FormValidator('export_all_results_form', 'GET', $url); +$form->addHeader(get_lang('ExportExerciseAllResults')); +$form + ->addSelect( + 'session_id', + get_lang('Session'), + $sessionSelectList, + ['onchange' => 'submit_form(this)', 'id' => 'session_id'] + ) + ->setSelected($sessionId); +$form + ->addSelect( + 'selected_course', + get_lang('Course'), + $courseSelectList, + ['onchange' => 'mark_course_id_changed(); submit_form(this);', 'id' => 'selected_course'] + ) + ->setSelected($courseId); +$form + ->addSelect( + 'exerciseId', + get_lang('Exercise'), + $exerciseSelectList + ) + ->setSelected($exerciseId); + +$form->addHidden('course_id_changed', '0'); +$form->addHidden('exercise_id_changed', '0'); +$form->addButtonExport(get_lang('Export'), 'name'); + +if ($form->validate()) { + $values = $form->getSubmitValues(); + + if (!empty($values['exerciseId']) && !empty($values['selected_course'])) { + $sessionId = (int) $values['session_id']; + $courseId = (int) $values['selected_course']; + $exerciseId = (int) $values['exerciseId']; + ExerciseLib::exportExerciseAllResultsZip($sessionId, $courseId, $exerciseId); + } +} + +Display::display_header(get_lang('ExportExerciseAllResults')); + +echo Display::return_message( + get_lang('ThisProcessCantakeALongTime'), + 'normal', + false, +); + +echo $form->display(); + +Display::display_footer(); diff --git a/main/admin/index.php b/main/admin/index.php index 46b9ed4612..684d547bd0 100644 --- a/main/admin/index.php +++ b/main/admin/index.php @@ -623,6 +623,11 @@ if (api_is_platform_admin() || ($allowCareer && api_is_session_admin())) { 'url' => 'resource_sequence.php', 'label' => get_lang('ResourcesSequencing'), ]; + $items[] = [ + 'class' => 'item-export-exercise-results', + 'url' => 'export_exercise_results.php', + 'label' => get_lang('ExportExerciseAllResults'), + ]; } $blocks['sessions']['items'] = $items; diff --git a/main/exercise/exercise.class.php b/main/exercise/exercise.class.php index 07b069a111..a6aff7e0e2 100755 --- a/main/exercise/exercise.class.php +++ b/main/exercise/exercise.class.php @@ -8426,7 +8426,7 @@ class Exercise * * @return array exercises */ - public function getExerciseAndResult($courseId, $sessionId, $quizId = []) + public function getExerciseAndResult($courseId, $sessionId, $quizId = [], $status = null) { if (empty($quizId)) { return []; @@ -8439,22 +8439,30 @@ class Exercise $ids = array_map('intval', $ids); $ids = implode(',', $ids); $track_exercises = Database::get_main_table(TABLE_STATISTIC_TRACK_E_EXERCISES); + + $condition = ''; + if (isset($status)) { + $condition .= " AND te.status = '$status' "; + } + if (0 != $sessionId) { $sql = "SELECT * FROM $track_exercises te - INNER JOIN c_quiz cq ON cq.iid = te.exe_exo_id - WHERE - te.id = %s AND - te.session_id = %s AND - cq.iid IN (%s) + INNER JOIN c_quiz cq ON cq.iid = te.exe_exo_id + WHERE + te.c_id = %s AND + te.session_id = %s AND + cq.iid IN (%s) + $condition ORDER BY cq.iid"; $sql = sprintf($sql, $courseId, $sessionId, $ids); } else { $sql = "SELECT * FROM $track_exercises te - INNER JOIN c_quiz cq ON cq.iid = te.exe_exo_id - WHERE - te.id = %s AND - cq.iid IN (%s) + INNER JOIN c_quiz cq ON cq.iid = te.exe_exo_id + WHERE + te.c_id = %s AND + cq.iid IN (%s) + $condition ORDER BY cq.iid"; $sql = sprintf($sql, $courseId, $ids); } diff --git a/main/exercise/exercise_report.php b/main/exercise/exercise_report.php index 08d7400154..32e34b12af 100755 --- a/main/exercise/exercise_report.php +++ b/main/exercise/exercise_report.php @@ -149,6 +149,16 @@ if (!empty($_REQUEST['export_report']) && $_REQUEST['export_report'] == '1') { $objExerciseTmp = new Exercise(); $exerciseExists = $objExerciseTmp->read($exercise_id); +$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : null; +switch ($action) { + case 'export_all_results': + $sessionId = api_get_session_id(); + $courseId = api_get_course_int_id(); + ExerciseLib::exportExerciseAllResultsZip($sessionId, $courseId, $exercise_id); + + break; +} + //Send student email @todo move this code in a class, library if (isset($_REQUEST['comments']) && $_REQUEST['comments'] === 'update' && @@ -438,6 +448,11 @@ if ($is_allowedToEdit && $origin !== 'learnpath') { api_get_path(WEB_CODE_PATH).'exercise/recalculate_all.php?'.api_get_cidreq()."&exercise=$exercise_id" ); + $actions .= Display::url( + Display::return_icon('export_pdf.png', get_lang('ExportExerciseAllResults'), [], ICON_SIZE_MEDIUM), + api_get_self().'?'.api_get_cidreq().'&action=export_all_results&exerciseId='.$exercise_id + ); + // clean result before a selected date icon if ($allowClean) { $actions .= Display::url( diff --git a/main/exercise/exercise_show.php b/main/exercise/exercise_show.php index 0f33d28567..01661fd73f 100755 --- a/main/exercise/exercise_show.php +++ b/main/exercise/exercise_show.php @@ -20,6 +20,7 @@ $origin = api_get_origin(); $currentUserId = api_get_user_id(); $printHeaders = 'learnpath' === $origin; $id = isset($_REQUEST['id']) ? (int) $_REQUEST['id'] : 0; //exe id +$exportTypeAllResults = ('export' === $_GET['action'] && 'all_results' === $_GET['export_type']); if (empty($id)) { api_not_allowed(true); @@ -38,15 +39,17 @@ $learnpath_id = $track_exercise_info['orig_lp_id']; $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 @@ -93,7 +96,8 @@ $is_allowedToEdit = 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( @@ -982,7 +986,24 @@ if ('export' === $action) { '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/main/inc/lib/exercise.lib.php b/main/inc/lib/exercise.lib.php index 52aaffa3f0..7aaaee482e 100644 --- a/main/inc/lib/exercise.lib.php +++ b/main/inc/lib/exercise.lib.php @@ -7150,4 +7150,76 @@ EOT; error_log("Exercise ping received: exe_id = $exeId. _user not found in session."); } + + public static function saveFileExerciseResultPdf( + int $exeId, + int $courseId, + int $sessionId + ) { + $cinfo = api_get_course_info_by_id($courseId); + $courseCode = $cinfo['code']; + $cidReq = 'cidReq='.$courseCode.'&id_session='.$sessionId.'&gidReq=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_RETURNTRANSFER, true); + $result = curl_exec($ch); + + curl_close($ch); + } + + public static function exportExerciseAllResultsZip( + int $sessionId, + int $courseId, + int $exerciseId + ) { + $objExerciseTmp = new Exercise($courseId); + $exeResults = $objExerciseTmp->getExerciseAndResult( + $courseId, + $sessionId, + $exerciseId, + '' + ); + + 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. + if (!empty($exeResults)) { + foreach ($exeResults as $exeResult) { + $exeId = (int) $exeResult['exe_id']; + ExerciseLib::saveFileExerciseResultPdf($exeId, $courseId, $sessionId); + } + } + + // 3. If export folder is not empty will be zipped. + $isFolderPathEmpty = (file_exists($exportFolderPath) && 2 == count(scandir($exportFolderPath))); + if (!$isFolderPathEmpty) { + $exportFilePath = $baseDir.$exportName.'.zip'; + $zip = new \PclZip($exportFilePath); + $zip->create($exportFolderPath, PCLZIP_OPT_REMOVE_PATH, $exportFolderPath); + rmdirr($exportFolderPath); + + DocumentManager::file_send_for_download($exportFilePath, true, $exportName.'.zip'); + exit; + } + } + + return false; + } } diff --git a/main/inc/lib/pdf.lib.php b/main/inc/lib/pdf.lib.php index 7b37453841..5e964bf97e 100755 --- a/main/inc/lib/pdf.lib.php +++ b/main/inc/lib/pdf.lib.php @@ -109,7 +109,9 @@ class PDF $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); @@ -173,9 +175,9 @@ class PDF $css, $this->params['filename'], $this->params['course_code'], - 'D', + $outputMode, $saveToFile, - null, + $fileToSave, $returnHtml, $addDefaultCss ); From 22f777cd1ba9576262e984d12c6d172c7e7bdbb8 Mon Sep 17 00:00:00 2001 From: Yannick Warnier Date: Wed, 3 May 2023 02:39:49 +0200 Subject: [PATCH 2/4] Minor: Add file-level PHPDoc header to new script --- main/admin/export_exercise_results.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main/admin/export_exercise_results.php b/main/admin/export_exercise_results.php index 8699936de2..7d82027072 100644 --- a/main/admin/export_exercise_results.php +++ b/main/admin/export_exercise_results.php @@ -1,6 +1,8 @@ Date: Wed, 3 May 2023 03:21:24 +0200 Subject: [PATCH 3/4] Update language terms --- main/lang/english/trad4all.inc.php | 3 ++- main/lang/french/trad4all.inc.php | 3 ++- main/lang/spanish/trad4all.inc.php | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/main/lang/english/trad4all.inc.php b/main/lang/english/trad4all.inc.php index b387def746..8abd225972 100644 --- a/main/lang/english/trad4all.inc.php +++ b/main/lang/english/trad4all.inc.php @@ -7291,7 +7291,7 @@ $CourseCreationUsesTemplateComment = "Set this to use the same template course ( $EnablePasswordStrengthCheckerText = "Password strength checker"; $EnablePasswordStrengthCheckerComment = "Enable this option to add a visual indicator of password strength, when the user changes his/her password. This will NOT prevent bad passwords to be added, it only acts as a visual helper."; $EnableCaptchaText = "CAPTCHA"; -$EnableCaptchaComment = "Enable a CAPTCHA on the login form to avoid password hammering"; +$EnableCaptchaComment = "Enable a CAPTCHA on the login form, inscription form and lost password form to avoid password hammering"; $CaptchaNumberOfMistakesBeforeBlockingAccountText = "CAPTCHA mistakes allowance"; $CaptchaNumberOfMistakesBeforeBlockingAccountComment = "The number of times a user can make a mistake on the CAPTCHA box before his account is locked out."; $CaptchaTimeAccountIsLockedText = "CAPTCHA account locking time"; @@ -9013,4 +9013,5 @@ $AllowSubscriptions = "Allow subscriptions"; $MaxSubscriptions = "Maximum number of subscriptions"; $NoMoreAccessible = "No longer available"; $AccessibleFrom = "Available from"; +$ExportExerciseAllResults = "Export all results from an exercise"; ?> \ No newline at end of file diff --git a/main/lang/french/trad4all.inc.php b/main/lang/french/trad4all.inc.php index f159a154e5..d38aa0fa11 100644 --- a/main/lang/french/trad4all.inc.php +++ b/main/lang/french/trad4all.inc.php @@ -7243,7 +7243,7 @@ $CourseCreationUsesTemplateComment = "Configurez ce paramètre pour utiliser le $EnablePasswordStrengthCheckerText = "Valider la complexité du mot de passe"; $EnablePasswordStrengthCheckerComment = "L'activation de cette option fera apparaître un indicateur de complexité de mot de passe quand l'utilisateur modifie son mot de passe. Ceci n'empêche *PAS* l'introduction d'un mauvais mot de passe. Il s'agit seulement d'une aide visuelle."; $EnableCaptchaText = "CAPTCHA"; -$EnableCaptchaComment = "Activer cette option fera apparaître un CAPTCHA dans le formulaire de login pour éviter les tentatives de pénétration par force brute."; +$EnableCaptchaComment = "Activer cette option fera apparaître un CAPTCHA dans les formulaires de login, d'inscription et de mot de passe perdu pour éviter les tentatives de pénétration par force brute."; $CaptchaNumberOfMistakesBeforeBlockingAccountText = "Marge d'erreur du login avec CAPTCHA"; $CaptchaNumberOfMistakesBeforeBlockingAccountComment = "Nombre de fois qu'un utillisateur peut se tromper dans l'introduction de son nom d'utilisateur et de son mot de passe avant que son compte ne soit congelé pour un certain temps."; $CaptchaTimeAccountIsLockedText = "Temps de blocage CAPTCHA"; @@ -8947,4 +8947,5 @@ $AllowSubscriptions = "Autoriser les inscriptions"; $MaxSubscriptions = "Nombre maximum d'inscriptions"; $NoMoreAccessible = "Plus accessible"; $AccessibleFrom = "Accessible à partir du"; +$ExportExerciseAllResults = "Exporter tous les résultats d'un exercice"; ?> \ No newline at end of file diff --git a/main/lang/spanish/trad4all.inc.php b/main/lang/spanish/trad4all.inc.php index ff2e0eaf97..ec083864fe 100644 --- a/main/lang/spanish/trad4all.inc.php +++ b/main/lang/spanish/trad4all.inc.php @@ -7317,7 +7317,7 @@ $CourseCreationUsesTemplateComment = "Configure este parámetro para usar el mis $EnablePasswordStrengthCheckerText = "Validar complejidad de contraseña"; $EnablePasswordStrengthCheckerComment = "Al activar esta opción, aparecerá un indicador de complejidad de contraseña cuando el usuario cambie su contraseña. Esto *NO* prohíbe el ingreso de una mala contraseña. Solamente actúa como una ayuda visual."; $EnableCaptchaText = "CAPTCHA"; -$EnableCaptchaComment = "Al activar esta opción, aparecerá un CAPTCHA en el formulario de ingreso, para evitar los intentos de ingreso por fuerza bruta"; +$EnableCaptchaComment = "Al activar esta opción, aparecerá un CAPTCHA en los formularios de ingreso, inscripcion y contraseña perdida para evitar los intentos de ingreso por fuerza bruta"; $CaptchaNumberOfMistakesBeforeBlockingAccountText = "Margen de errores en CAPTCHA"; $CaptchaNumberOfMistakesBeforeBlockingAccountComment = "Cuantas veces uno se puede equivocar al ingresar su usuario y contraseña con el CAPTCHA antes de que su cuenta quede congelada por un tiempo."; $CaptchaTimeAccountIsLockedText = "Tiempo bloqueo CAPTCHA"; @@ -9038,4 +9038,5 @@ $AllowSubscriptions = "Permitir suscripciones"; $MaxSubscriptions = "Cantidad máxima permitida de suscripciones"; $NoMoreAccessible = "No mas accesible"; $AccessibleFrom = "Accesible desde el"; +$ExportExerciseAllResults = "Exportar todos los resultados de un ejercicio"; ?> \ No newline at end of file From 9cb62c418440ce6e5a4b0c6895a69789987bf98b Mon Sep 17 00:00:00 2001 From: Yannick Warnier Date: Wed, 3 May 2023 03:25:00 +0200 Subject: [PATCH 4/4] Minor: Use existing language variable instead of a new one --- main/admin/export_exercise_results.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/admin/export_exercise_results.php b/main/admin/export_exercise_results.php index 7d82027072..38bdde5953 100644 --- a/main/admin/export_exercise_results.php +++ b/main/admin/export_exercise_results.php @@ -162,7 +162,7 @@ if ($form->validate()) { Display::display_header(get_lang('ExportExerciseAllResults')); echo Display::return_message( - get_lang('ThisProcessCantakeALongTime'), + get_lang('PleaseWaitThisCouldTakeAWhile'), 'normal', false, );