diff --git a/assets/vue/components/assignments/TeacherSubmissionList.vue b/assets/vue/components/assignments/TeacherSubmissionList.vue index c82235375c8..814ad9e9d15 100644 --- a/assets/vue/components/assignments/TeacherSubmissionList.vue +++ b/assets/vue/components/assignments/TeacherSubmissionList.vue @@ -338,6 +338,7 @@ const canUseAiTaskGrader = computed(() => { return !!(canEdit && notStudentView && aiHelpersEnabled.value && taskGraderEnabled.value) }) + watch( loadParams, () => { @@ -489,16 +490,19 @@ async function viewSubmission(item) { } function saveCorrection(item) { - if (item?.downloadUrl) { - const link = document.createElement("a") - link.href = item.downloadUrl - link.download = "" - document.body.appendChild(link) - link.click() - document.body.removeChild(link) - } else { + if (!item?.iid) { notification.showErrorNotification(t("No download available")) + return } + const params = new URLSearchParams({ + cid: course.value?.id ?? 0, + sid: session.value?.id ?? 0, + }) + const link = document.createElement("a") + link.href = `/assignments/submissions/${item.iid}/download?${params}` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) } function editSubmission(item) { diff --git a/src/CoreBundle/Controller/PlatformConfigurationController.php b/src/CoreBundle/Controller/PlatformConfigurationController.php index 409723f6a97..47b6cb29ab8 100644 --- a/src/CoreBundle/Controller/PlatformConfigurationController.php +++ b/src/CoreBundle/Controller/PlatformConfigurationController.php @@ -179,6 +179,7 @@ public function list( 'language.show_different_course_language', 'workflows.allow_users_to_create_courses', 'work.allow_only_one_student_publication_per_user', + 'work.add_fullname_in_file_download', 'course.course_creation_form_hide_course_code', 'course.course_creation_form_set_course_category_mandatory', 'display.hide_logout_button', diff --git a/src/CoreBundle/Controller/StudentPublicationController.php b/src/CoreBundle/Controller/StudentPublicationController.php index ff7f98a915a..3b98e642431 100644 --- a/src/CoreBundle/Controller/StudentPublicationController.php +++ b/src/CoreBundle/Controller/StudentPublicationController.php @@ -36,6 +36,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Security\Http\Attribute\IsGranted; @@ -170,6 +171,62 @@ public function getAssignmentSubmissionsForTeacher( ]); } + #[Route('/submissions/{id}/download', name: 'chamilo_core_assignment_submission_download', methods: ['GET'])] + public function downloadSubmission( + int $id, + CStudentPublicationRepository $repo, + ResourceNodeRepository $resourceNodeRepository, + SettingsManager $settingsManager + ): Response { + $submission = $repo->find($id); + + if (!$submission) { + throw $this->createNotFoundException('Submission not found.'); + } + + $this->denyAccessUnlessGranted('VIEW', $submission->getResourceNode()); + + $resourceNode = $submission->getResourceNode(); + $resourceFile = $resourceNode?->getFirstResourceFile(); + + if (!$resourceFile) { + throw $this->createNotFoundException('No file attached to this submission.'); + } + + try { + $path = $resourceNodeRepository->getFilename($resourceFile); + $content = $resourceNodeRepository->getFileSystem()->read($path); + } catch (Throwable) { + throw $this->createNotFoundException('File could not be read.'); + } + + $originalName = $resourceFile->getOriginalName(); + $addFullname = 'true' === $settingsManager->getSetting('work.add_fullname_in_file_download'); + + if ($addFullname) { + $user = $submission->getUser(); + $fullname = $this->cleanFilename( + trim(($user->getFirstname() ?? '').' '.($user->getLastname() ?? '')) + ); + $filename = $fullname.'_'.$originalName; + } else { + $filename = $originalName; + } + + $asciiFilename = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $filename) ?: $filename; + + $response = new Response($content); + $disposition = $response->headers->makeDisposition( + ResponseHeaderBag::DISPOSITION_ATTACHMENT, + $asciiFilename, + $asciiFilename + ); + $response->headers->set('Content-Disposition', $disposition); + $response->headers->set('Content-Type', $resourceFile->getMimeType() ?: 'application/octet-stream'); + + return $response; + } + #[Route('/submissions/{id}', name: 'chamilo_core_assignment_student_submission_delete', methods: ['DELETE'])] public function deleteSubmission( int $id, @@ -436,7 +493,8 @@ public function deleteAllCorrections( public function downloadAssignmentPackage( int $assignmentId, CStudentPublicationRepository $repo, - ResourceNodeRepository $resourceNodeRepository + ResourceNodeRepository $resourceNodeRepository, + SettingsManager $settingsManager ): Response { $assignment = $repo->find($assignmentId); @@ -444,6 +502,8 @@ public function downloadAssignmentPackage( throw $this->createNotFoundException('Assignment not found.'); } + $addFullname = 'true' === $settingsManager->getSetting('work.add_fullname_in_file_download'); + [$submissions] = $repo->findAllSubmissionsByAssignment($assignmentId, 1, 10000); $zipPath = api_get_path(SYS_ARCHIVE_PATH).uniqid('assignment_', true).'.zip'; $zip = new ZipArchive(); @@ -463,7 +523,16 @@ public function downloadAssignmentPackage( $path = $resourceNodeRepository->getFilename($resourceFile); $content = $resourceNodeRepository->getFileSystem()->read($path); - $filename = \sprintf('%s_%s_%s', $sentDate, $user->getUsername(), $resourceFile->getOriginalName()); + if ($addFullname) { + + $fullname = $this->cleanFilename( + trim(($user->getFirstname() ?? '').' '.($user->getLastname() ?? '')) + ); + $filename = \sprintf('%s_%s_%s', $sentDate, $fullname, $resourceFile->getOriginalName()); + } else { + $filename = \sprintf('%s_%s_%s', $sentDate, $user->getUsername(), $resourceFile->getOriginalName()); + } + $zip->addFromString($filename, $content); } catch (Throwable) { continue; diff --git a/src/CoreBundle/Settings/WorkSettingsSchema.php b/src/CoreBundle/Settings/WorkSettingsSchema.php index ee2b4929cd9..3ed31220d0d 100644 --- a/src/CoreBundle/Settings/WorkSettingsSchema.php +++ b/src/CoreBundle/Settings/WorkSettingsSchema.php @@ -31,6 +31,7 @@ public function buildSettings(AbstractSettingsBuilder $builder): void 'my_courses_show_pending_work' => 'false', 'allow_compilatio_tool' => 'false', 'compilatio_tool' => '', + 'add_fullname_in_file_download' => 'false', ] ) ; @@ -52,6 +53,7 @@ public function buildForm(FormBuilderInterface $builder): void ->add('my_courses_show_pending_work', YesNoType::class) ->add('allow_compilatio_tool', YesNoType::class) ->add('compilatio_tool', TextareaType::class) + ->add('add_fullname_in_file_download', YesNoType::class) ; $this->updateFormFieldsFromSettingsInfo($builder);