From 6f603c2cc7ce84eac8f90c9bcb250491e8e7900f Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Wed, 9 Sep 2026 14:30:44 +0200 Subject: [PATCH] fix(frontend): precise phase name on CSV export --- backend/api/views/download.py | 14 ++++---- frontend/src/api/download/api.ts | 52 +++++++++++++++--------------- frontend/src/api/download/hooks.ts | 50 +++++++++++++--------------- frontend/src/service/function.ts | 12 +++++-- 4 files changed, 66 insertions(+), 62 deletions(-) diff --git a/backend/api/views/download.py b/backend/api/views/download.py index 2ecccc213..ff78fa74e 100644 --- a/backend/api/views/download.py +++ b/backend/api/views/download.py @@ -377,7 +377,7 @@ class DownloadViewSet(ViewSet): url_path="analysis-export/(?P[^/.]+)", url_name="analysis-export", ) - def download_analysis_export(self, request, pk=None): + def download_analysis_export(self, _request, pk=None): """ Download analysis export For legacy analysis: audio metadata and spectro config csv @@ -431,7 +431,7 @@ def download_phase_annotations(self, request, pk=None): campaign = phase.annotation_campaign response = HttpResponse(content_type="text/csv") - filename = f"{campaign.name.replace(' ', '_')}_status.csv" + filename = f"{campaign.name.replace(' ', '_')}_{AnnotationPhase.Type(phase.phase).label}_annotations.csv" response["Content-Disposition"] = f'attachment; filename="{filename}"' validate_users = list( @@ -451,7 +451,7 @@ def download_phase_annotations(self, request, pk=None): writer = csv.DictWriter(response, fieldnames=headers) writer.writeheader() - def map_validations(user: str) -> [str, Case]: + def map_validations(user: str) -> tuple[str, Case]: validation_sub = AnnotationValidation.objects.filter( annotator__username=user, annotation_id=OuterRef("id"), @@ -465,7 +465,7 @@ def map_validations(user: str) -> [str, Case]: default=None, output_field=models.BooleanField(null=True), ) - return [user, query] + return user, query results = ( _get_annotations_for_report(phase) @@ -497,7 +497,7 @@ def download_phase_progression(self, request, pk=None): campaign = phase.annotation_campaign response = HttpResponse(content_type="text/csv") - filename = f"{campaign.name.replace(' ', '_')}_status.csv" + filename = f"{campaign.name.replace(' ', '_')}_{AnnotationPhase.Type(phase.phase).label}_status.csv" response["Content-Disposition"] = f'attachment; filename="{filename}"' # Headers @@ -521,7 +521,7 @@ def download_phase_progression(self, request, pk=None): status=AnnotationTask.Status.FINISHED, ) - def map_annotators(user: str) -> [str, Case]: + def map_annotators(user: str) -> tuple[str, Case]: task_sub = finished_tasks.filter( spectrogram_id=OuterRef("pk"), annotator__username=user ) @@ -536,7 +536,7 @@ def map_annotators(user: str) -> [str, Case]: default=models.Value("UNASSIGNED"), output_field=models.CharField(), ) - return [user, query] + return user, query data = dict(map(map_annotators, annotators)) diff --git a/frontend/src/api/download/api.ts b/frontend/src/api/download/api.ts index 784ff8536..63f57bbee 100644 --- a/frontend/src/api/download/api.ts +++ b/frontend/src/api/download/api.ts @@ -4,34 +4,34 @@ import type { SpectrogramAnalysisNode } from '@/api/types.gql-generated'; export const DownloadRestAPI = restAPI.injectEndpoints({ - endpoints: builder => ({ + endpoints: builder => ({ - downloadAnalysis: builder.mutation>({ - query: ({ id, name }) => { - return { - url: `/api/download/analysis-export/${ id }/`, - responseHandler: getDownloadResponseHandler(`${ name }.zip`), - } - }, - }), + downloadAnalysis: builder.mutation>({ + query: ({ id, name }) => { + return { + url: `/api/download/analysis-export/${ id }/`, + responseHandler: getDownloadResponseHandler(`${ name }.zip`), + } + }, + }), - downloadAnnotations: builder.mutation({ - query: ({ phaseID, campaignName }) => { - return { - url: `/api/download/phase-annotations/${ phaseID }/`, - responseHandler: getDownloadResponseHandler(`${ campaignName.replaceAll(' ', '_') }_results.csv`), - } - }, - }), + downloadAnnotations: builder.mutation({ + query: ({ phaseID }) => { + return { + url: `/api/download/phase-annotations/${ phaseID }/`, + responseHandler: getDownloadResponseHandler(), + } + }, + }), - downloadProgress: builder.mutation({ - query: ({ phaseID, campaignName }) => { - return { - url: `/api/download/phase-progression/${ phaseID }/`, - responseHandler: getDownloadResponseHandler(`${ campaignName.replaceAll(' ', '_') }_status.csv`), - } - }, - }), + downloadProgress: builder.mutation({ + query: ({ phaseID }) => { + return { + url: `/api/download/phase-progression/${ phaseID }/`, + responseHandler: getDownloadResponseHandler(), + } + }, + }), - }), + }), }) diff --git a/frontend/src/api/download/hooks.ts b/frontend/src/api/download/hooks.ts index bf0cfc0fe..59fdaf922 100644 --- a/frontend/src/api/download/hooks.ts +++ b/frontend/src/api/download/hooks.ts @@ -3,42 +3,38 @@ import { useCallback } from 'react'; import { useLoaderData } from '@tanstack/react-router'; const { - downloadAnalysis, - downloadAnnotations, - downloadProgress, + downloadAnalysis, + downloadAnnotations, + downloadProgress, } = DownloadRestAPI.endpoints export const useDownloadAnalysis = downloadAnalysis.useMutation export const useDownloadAnnotations = () => { - const { campaign } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID' }) - const { phase } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase/$phaseType' }) - const [ method, info ] = downloadAnnotations.useMutation() + const { phase } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase/$phaseType' }) + const [ method, info ] = downloadAnnotations.useMutation() - return { - downloadAnnotations: useCallback(() => { - return method({ - phaseID: phase.id, - campaignName: campaign.name, - }).unwrap() - }, [ method, campaign, phase ]), - ...info, - } + return { + downloadAnnotations: useCallback(() => { + return method({ + phaseID: phase.id, + }).unwrap() + }, [ method, phase ]), + ...info, + } } export const useDownloadProgress = () => { - const { campaign } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID' }) - const { phase } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase/$phaseType' }) - const [ method, info ] = downloadProgress.useMutation() + const { phase } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase/$phaseType' }) + const [ method, info ] = downloadProgress.useMutation() - return { - downloadProgress: useCallback(() => { - return method({ - phaseID: phase.id, - campaignName: campaign.name, - }).unwrap() - }, [ method, campaign, phase ]), - ...info, - } + return { + downloadProgress: useCallback(() => { + return method({ + phaseID: phase.id, + }).unwrap() + }, [ method, phase ]), + ...info, + } } \ No newline at end of file diff --git a/frontend/src/service/function.ts b/frontend/src/service/function.ts index 096d72eca..05bb7bd2a 100644 --- a/frontend/src/service/function.ts +++ b/frontend/src/service/function.ts @@ -93,14 +93,22 @@ function downloadFile(filename: string, type: string, blob: Blob) { a.click(); } -export async function downloadResponseHandler(response: Response, filename: string) { +export async function downloadResponseHandler(response: Response, filename?: string) { // TODO: reject errors correctly (catchable) - like a standard API error + console.debug(response.headers) if (response.status !== 200) return `[${ response.status }] ${ response.statusText }`; const type = response.headers.get('content-type') if (!type) throw new Error('No file type provided') + if (!filename) { + const contentDispositionHeader = response.headers.get('content-disposition') + if (!contentDispositionHeader) throw new Error('No Content-Disposition header') + const filenameRegExp = new RegExp(/filename="(\S*)"/g).exec(contentDispositionHeader) + if (!filenameRegExp) throw new Error('No filename in Content-Disposition header') + filename = filenameRegExp[1] + } downloadFile(filename, type, await response.blob()) } -export function getDownloadResponseHandler(filename: string) { +export function getDownloadResponseHandler(filename?: string) { return (response: Response) => downloadResponseHandler(response, filename) } \ No newline at end of file