From 53534643a7ed26b52c7ad05ac3bb079df73c8d93 Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Mon, 28 Sep 2026 11:40:42 +0200 Subject: [PATCH 1/5] fix(phase): annotation filters --- .../filter_sets/annotation_spectrogram.py | 262 +++++++++--------- frontend/src/components/base/Form/index.tsx | 21 +- .../AnnotationTask/AnnotationsFilter.tsx | 36 +-- .../_detailLayout/phase.$phaseType.tsx | 2 +- poetry.lock | 18 +- 5 files changed, 172 insertions(+), 167 deletions(-) diff --git a/backend/api/schema/filter_sets/annotation_spectrogram.py b/backend/api/schema/filter_sets/annotation_spectrogram.py index b68e8b2e2..15ded7649 100644 --- a/backend/api/schema/filter_sets/annotation_spectrogram.py +++ b/backend/api/schema/filter_sets/annotation_spectrogram.py @@ -1,4 +1,6 @@ -from django.db.models import QuerySet, Exists, OuterRef, Subquery +from typing import Optional + +from django.db.models import QuerySet, OuterRef, Q, Exists from django_extension.filters import ExtendedFilterSet, IDFilter from django_filters import OrderingFilter, filters from graphene_django import filter @@ -8,8 +10,11 @@ AnnotationFileRange, AnnotationTask, Annotation, - AnnotationPhase, AnnotationCampaign, + Confidence, + Label, + Detector, + AnnotationPhase, ) from backend.api.schema.enums import AnnotationPhaseType, AnnotationTaskStatus from backend.aplose.models import User @@ -46,146 +51,94 @@ def fake(self, queryset, _1, _2): return queryset def filter_queryset(self, queryset: QuerySet[Spectrogram]): - queryset = super().filter_queryset(queryset) - - queryset, file_ranges, tasks, annotations = self._get_querysets_for_filter( - queryset, only_assigned=self.data.get("only_assigned", False) + queryset: QuerySet[Spectrogram] = super().filter_queryset(queryset) + + # Filter: phase [AnnotationPhase.Type] + filter_phase: Optional[AnnotationPhase.Type] = self.data.get("phase") + # Filter: annotation_campaign [ID] + filter_annotation_campaign: Optional[ + AnnotationCampaign + ] = AnnotationCampaign.objects.filter( + id=self.data.get("annotation_campaign") + ).first() + # Filter: annotator [ID] + filter_annotator: Optional[User] = User.objects.filter( + id=self.data.get("annotator") + ).first() + + # Filter: annotation_tasks__status [AnnotationTaskStatus] + filter_annotation_tasks__status: Optional[AnnotationTaskStatus] = self.data.get( + "annotation_tasks__status" ) - # Filter on task status - status = self.data.get("annotation_tasks__status") - if status: - # Filter through existing file range - only assigned tasks have status - queryset = queryset.filter( - Exists( - file_ranges.filter( - from_datetime__lte=OuterRef("start"), - to_datetime__gte=OuterRef("end"), - ) - ) - ) - q = Exists( - tasks.filter( - status=AnnotationTask.Status.FINISHED, spectrogram_id=OuterRef("id") - ) - ) - if status == AnnotationTask.Status.FINISHED: - queryset = queryset.filter(q) - if status == AnnotationTask.Status.CREATED: - queryset = queryset.filter(~q) - - # Filter on annotations status - if self.data.get("annotations__exists") is not None: - # Filter through existing file range - only assigned tasks can have annotations - or not - queryset = queryset.filter( - Exists( - file_ranges.filter( - from_datetime__lte=OuterRef("start"), - to_datetime__gte=OuterRef("end"), - ) - ) - ) - - label = self.data.get("annotations__label") - if label: - annotations = annotations.filter(label__id=label) - - confidence = self.data.get("annotations__confidence") - if confidence: - annotations = annotations.filter(confidence__id=confidence) - - features_exists = self.data.get("annotations__acoustic_features__exists") - if features_exists: - annotations = annotations.filter( - acoustic_features__isnull=not features_exists - ) - - detector = self.data.get("annotations__detector") - if detector: - annotations = annotations.filter( - detector_configuration__detector_id=detector - ) - - a_annotator = self.data.get("annotations__annotator") - if a_annotator: - annotations = annotations.filter(annotator_id=a_annotator) + # Filter: annotations__exists [bool] + filter_annotations__exists: Optional[bool] = self.data.get( + "annotations__exists" + ) + # Filter: annotations__confidence [ID] + filter_annotations__confidence: Optional[ + Confidence + ] = Confidence.objects.filter( + pk=self.data.get("annotations__confidence") + ).first() + # Filter: annotations__label [ID] + filter_annotations__label: Optional[Label] = Label.objects.filter( + pk=self.data.get("annotations__label") + ).first() + # Filter: annotations__acoustic_features__exists [bool] + filter_annotations__acoustic_features__exists: Optional[bool] = self.data.get( + "annotations__acoustic_features__exists" + ) + # Filter: annotations__detector [ID] + filter_annotations__detector: Optional[Detector] = Detector.objects.filter( + pk=self.data.get("annotations__detector") + ).first() + # Filter: annotations__annotator [ID] + filter_annotations__annotator: Optional[User] = User.objects.filter( + pk=self.data.get("annotations__annotator") + ).first() + + # Filter: only_assigned [bool] + filter_only_assigned: bool = ( + self.data.get("only_assigned", False) is not False + or filter_annotations__exists is not None + or filter_annotation_tasks__status is not None + ) - q = Exists( - Subquery( - annotations.filter( - spectrogram_id=OuterRef("id"), - ) - ) + # => QuerySet[AnnotationFileRange] & QuerySet[Annotation] + file_ranges: QuerySet[AnnotationFileRange] = AnnotationFileRange.objects.all() + annotations: QuerySet[Annotation] = Annotation.objects.all() + if filter_annotator: + file_ranges = AnnotationFileRange.objects.filter_viewable_by( + user=filter_annotator ) - if self.data.get("annotations__exists"): - queryset = queryset.filter(q) - else: - queryset = queryset.filter(~q) - - return queryset.distinct() - - def _get_querysets_for_filter( - self, queryset: QuerySet[Spectrogram], only_assigned=False - ) -> tuple[ - QuerySet[Spectrogram], - QuerySet[AnnotationFileRange], - QuerySet[AnnotationTask], - QuerySet[Annotation], - ]: - can_see_unassigned = False - - spectrograms = queryset - file_ranges = AnnotationFileRange.objects.all() - tasks = AnnotationTask.objects.all() - annotations = Annotation.objects.all() - - phase_type = self.data.get("phase") - if phase_type: - file_ranges = file_ranges.filter(annotation_phase__phase=phase_type) - tasks = tasks.filter(annotation_phase__phase=phase_type) - - campaign_id = self.data.get("annotation_campaign") - if campaign_id: + if filter_annotation_campaign: file_ranges = file_ranges.filter( - annotation_phase__annotation_campaign_id=campaign_id + annotation_phase__annotation_campaign=filter_annotation_campaign ) - tasks = tasks.filter(annotation_phase__annotation_campaign_id=campaign_id) annotations = annotations.filter( - annotation_phase__annotation_campaign_id=campaign_id + annotation_phase__annotation_campaign=filter_annotation_campaign ) - spectrograms = spectrograms.filter( - analysis__annotation_campaigns__id=campaign_id + queryset = queryset.filter( + analysis__annotation_campaigns=filter_annotation_campaign ) - - if phase_type and campaign_id: - if phase_type == AnnotationPhase.Type.ANNOTATION: + if filter_phase: + file_ranges = file_ranges.filter(annotation_phase__phase=filter_phase) + if filter_phase == AnnotationPhase.Type.ANNOTATION: + annotations = annotations.filter(annotation_phase__phase=filter_phase) + if filter_annotator: + annotations = annotations.filter(annotator=filter_annotator) + elif filter_annotator: annotations = annotations.filter( - annotation_phase__phase=phase_type, - annotation_phase__annotation_campaign_id=campaign_id, + ~Q( + annotator=filter_annotator, + annotation_phase__phase=AnnotationPhase.Type.ANNOTATION, + ) ) - annotator_id = self.data.get("annotator") - if annotator_id: - user = User.objects.get(pk=annotator_id) - file_ranges = file_ranges.filter(annotator=user) - tasks = tasks.filter(annotator=user) - if phase_type == AnnotationPhase.Type.ANNOTATION: - annotations = annotations.filter(annotator=user) - if user.is_superuser or user.is_staff: - can_see_unassigned = True - if campaign_id: - campaign = AnnotationCampaign.objects.get(pk=campaign_id) - if campaign.owner_id == user.id: - can_see_unassigned = True - if ( - phase_type - and campaign.phases.get(phase=phase_type).created_by_id == user.id - ): - can_see_unassigned = True - - if only_assigned or not can_see_unassigned: - # Filter through existing file range - spectrograms = spectrograms.filter( + # Filter assigned spectrograms + if filter_only_assigned: + queryset = queryset.filter( Exists( file_ranges.filter( from_datetime__lte=OuterRef("start"), @@ -194,4 +147,51 @@ def _get_querysets_for_filter( ) ) - return spectrograms, file_ranges, tasks, annotations + # Filter on task status + if filter_annotation_tasks__status: + tasks_ids = [] + for fr in file_ranges: + tasks_ids += fr.tasks.values_list("id", flat=True) + tasks: QuerySet[AnnotationTask] = AnnotationTask.objects.filter( + id__in=tasks_ids + ) + finished_task_spectrogram_ids = tasks.filter( + status=AnnotationTask.Status.FINISHED + ).values_list("spectrogram_id", flat=True) + query = Q(id__in=finished_task_spectrogram_ids) + if filter_annotation_tasks__status == AnnotationTask.Status.FINISHED: + queryset = queryset.filter(query) + if filter_annotation_tasks__status == AnnotationTask.Status.CREATED: + queryset = queryset.filter(~query) # Created task may not exist at all + + if filter_annotations__exists is not None: + if filter_annotations__exists: + if filter_annotations__label: + annotations = annotations.filter(label=filter_annotations__label) + if filter_annotations__confidence: + annotations = annotations.filter( + confidence=filter_annotations__confidence + ) + if filter_annotations__annotator: + annotations = annotations.filter( + annotator=filter_annotations__annotator + ) + if filter_annotations__detector: + annotations = annotations.filter( + detector_configuration__detector=filter_annotations__detector + ) + if filter_annotations__acoustic_features__exists is not None: + annotations = annotations.filter( + acoustic_features__isnull=not filter_annotations__acoustic_features__exists + ) + + annotations_spectrogram_ids = annotations.values_list( + "spectrogram_id", flat=True + ).distinct() + queryset = queryset.filter(id__in=annotations_spectrogram_ids) + else: + queryset = queryset.filter( + ~Q(id__in=annotations.values_list("spectrogram_id", flat=True)) + ) + + return queryset.distinct() diff --git a/frontend/src/components/base/Form/index.tsx b/frontend/src/components/base/Form/index.tsx index 30ac70576..13bb075e7 100644 --- a/frontend/src/components/base/Form/index.tsx +++ b/frontend/src/components/base/Form/index.tsx @@ -12,15 +12,15 @@ export type FormProps = Omit & { gqlErrors?: (ErrorType | null)[] | null }; -export const Form: React.FC = React.memo(({ - className, - horizontal, - center, - gqlErrors, - errors, - children, - ...props - }) => { +export const Form = React.forwardRef(({ + className, + horizontal, + center, + gqlErrors, + errors, + children, + ...props + }, ref) => { const computedErrors = useMemo(() => errors || cleanGqlErrors(gqlErrors), [ errors, gqlErrors ]) const classes = useMemo(() => { @@ -31,7 +31,8 @@ export const Form: React.FC = React.memo(({ return classes }, [ className, horizontal, center ]) - return { children } diff --git a/frontend/src/features/AnnotationTask/AnnotationsFilter.tsx b/frontend/src/features/AnnotationTask/AnnotationsFilter.tsx index cd7f6614d..ec96a4074 100644 --- a/frontend/src/features/AnnotationTask/AnnotationsFilter.tsx +++ b/frontend/src/features/AnnotationTask/AnnotationsFilter.tsx @@ -1,4 +1,4 @@ -import React, { type FormEvent, Fragment, useCallback, useState } from 'react'; +import React, { type FormEvent, Fragment, useCallback, useRef, useState } from 'react'; import { AnnotationPhaseType } from '@/api'; import { ConfidenceComponent } from '@/features/Confidence'; import { LabelComponent } from '@/features/Labels'; @@ -41,10 +41,13 @@ export const AnnotationsFilterModal: React.FC = () => { withAcousticFeatures, }), }); + const { user } = useLoaderData({from: '/_authenticated'}) + const formRef = useRef(null) const routeParams = Route.useParams() const navigate = useNavigate(); const { campaign, labels, confidences } = useLoaderData({ from: '/_authenticated/annotation-campaign/$campaignID' }) + const [ tmpWithAF, setTmpWithAF ] = useState(withAcousticFeatures ?? null); const [ tmpWithAnnotations, setTmpWithAnnotations ] = useState(withAnnotations ?? null); const update = useCallback((data: Pick) => { @@ -60,18 +63,18 @@ export const AnnotationsFilterModal: React.FC = () => { }) }, [ navigate, routeParams ]) - const onSubmit = useCallback((event: BaseUIEvent>) => { - event.preventDefault(); - const formData = new FormData(event.currentTarget); + const onSubmitRef = useCallback(() => { + if (!formRef.current) return + const formData = new FormData(formRef.current); if (tmpWithAnnotations == true) { const withAcousticFeatures = formData.get('withAcousticFeatures') as string || undefined update({ withAnnotations: true, withAcousticFeatures: withAcousticFeatures === undefined ? undefined : withAcousticFeatures === 'true', - annotationLabel: formData.get('annotationLabel') as string, - annotationConfidence: formData.get('annotationConfidence') as string, - annotationDetector: formData.get('annotationDetector') as string, - annotationAnnotator: formData.get('annotationAnnotator') as string, + annotationLabel: formData.get('annotationLabel') as string || undefined, + annotationConfidence: formData.get('annotationConfidence') as string || undefined, + annotationDetector: formData.get('annotationDetector') as string || undefined, + annotationAnnotator: formData.get('annotationAnnotator') as string || undefined, }) } else { update({ @@ -83,9 +86,10 @@ export const AnnotationsFilterModal: React.FC = () => { annotationAnnotator: undefined, }) } - }, [ update, labels, tmpWithAnnotations, confidences ]) + }, [ update, labels, tmpWithAnnotations, confidences, formRef ]) const onReset = useCallback((event: BaseUIEvent>) => { + console.debug('onReset!!!') event.preventDefault(); update({ withAnnotations: undefined, @@ -98,13 +102,12 @@ export const AnnotationsFilterModal: React.FC = () => { }, [ update ]) return -
+ With annotations - + Unset With Without @@ -137,7 +140,7 @@ export const AnnotationsFilterModal: React.FC = () => { Filter by annotator - a.id !== user.id) } disabled={ tmpWithAnnotations !== true } defaultValueString={ annotationAnnotator ?? undefined }/> @@ -146,7 +149,8 @@ export const AnnotationsFilterModal: React.FC = () => { Acoustic features - Unset With @@ -156,7 +160,7 @@ export const AnnotationsFilterModal: React.FC = () => { Reset - Filter + Filter
diff --git a/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase.$phaseType.tsx b/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase.$phaseType.tsx index 8292e9f72..83a6941ab 100644 --- a/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase.$phaseType.tsx +++ b/frontend/src/routes/_authenticated/annotation-campaign/$campaignID/_detailLayout/phase.$phaseType.tsx @@ -119,7 +119,7 @@ const AnnotationCampaignPhaseDetail: React.FC = () => { - { search.withAnnotations + { search.withAnnotations !== undefined && search.withAnnotations !== null ? : } diff --git a/poetry.lock b/poetry.lock index 15518cf18..f9aef96f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.5.1 and should not be changed by hand. [[package]] name = "anyio" @@ -1511,7 +1511,7 @@ comm = ">=0.1.1" debugpy = ">=1.6.5" ipython = ">=7.23.1" jupyter-client = ">=8.0.0" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0 || >=5.1.dev0" matplotlib-inline = ">=0.1" nest-asyncio = ">=1.4" packaging = ">=22" @@ -1687,7 +1687,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""} -jsonschema-specifications = ">=2023.03.6" +jsonschema-specifications = ">=2023.3.6" referencing = ">=0.28.4" rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""} rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""} @@ -1727,7 +1727,7 @@ files = [ ] [package.dependencies] -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0 || >=5.1.dev0" python-dateutil = ">=2.8.2" pyzmq = ">=23.0" tornado = ">=6.2" @@ -1816,7 +1816,7 @@ anyio = ">=3.1.0" argon2-cffi = ">=21.1" jinja2 = ">=3.0.3" jupyter-client = ">=7.4.4" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0 || >=5.1.dev0" jupyter-events = ">=0.11.0" jupyter-server-terminals = ">=0.4.4" nbconvert = ">=6.4.4" @@ -1870,7 +1870,7 @@ files = [ [package.dependencies] async-lru = ">=1.0.0" httpx = ">=0.25.0,<1" -ipykernel = ">=6.5.0,<6.30.0 || >6.30.0" +ipykernel = ">=6.5.0,!=6.30.0" jinja2 = ">=3.0.3" jupyter-core = "*" jupyter-lsp = ">=2.0.0" @@ -2293,7 +2293,7 @@ files = [ [package.dependencies] jupyter-client = ">=6.1.12" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0 || >=5.1.dev0" nbformat = ">=5.1" traitlets = ">=5.4" @@ -2354,7 +2354,7 @@ files = [ [package.dependencies] fastjsonschema = ">=2.15" jsonschema = ">=2.6" -jupyter-core = ">=4.12,<5.0.dev0 || >=5.1.dev0" +jupyter-core = ">=4.12,<5.0 || >=5.1.dev0" traitlets = ">=5.1" [package.extras] @@ -3060,7 +3060,7 @@ files = [ astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} -isort = ">=4.2.5,<5.13 || >5.13,<7" +isort = ">=4.2.5,!=5.13,<7" mccabe = ">=0.6,<0.8" platformdirs = ">=2.2" tomlkit = ">=0.10.1" From 5ce6db930598aba86c22e596fc1bbcfde39f3f73 Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Mon, 28 Sep 2026 12:04:57 +0200 Subject: [PATCH 2/5] lint --- .../filter_sets/annotation_spectrogram.py | 171 ++++++++++-------- 1 file changed, 93 insertions(+), 78 deletions(-) diff --git a/backend/api/schema/filter_sets/annotation_spectrogram.py b/backend/api/schema/filter_sets/annotation_spectrogram.py index 15ded7649..25e232ce5 100644 --- a/backend/api/schema/filter_sets/annotation_spectrogram.py +++ b/backend/api/schema/filter_sets/annotation_spectrogram.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Optional, TypedDict from django.db.models import QuerySet, OuterRef, Q, Exists from django_extension.filters import ExtendedFilterSet, IDFilter @@ -20,6 +20,21 @@ from backend.aplose.models import User +class FilterData(TypedDict): + phase: Optional[AnnotationPhase.Type] + annotation_campaign: Optional[AnnotationCampaign] + annotator: Optional[User] + + annotation_tasks__status: Optional[AnnotationTask.Status] + annotations__exists: Optional[bool] + annotations__confidence: Optional[Confidence] + annotations__label: Optional[Label] + annotations__acoustic_features__exists: Optional[bool] + annotations__detector: Optional[Detector] + annotations__annotator: Optional[User] + only_assigned: Optional[bool] + + class AnnotationSpectrogramFilterSet(ExtendedFilterSet): phase = filter.TypedFilter(AnnotationPhaseType, method="fake") @@ -50,88 +65,79 @@ class Meta: def fake(self, queryset, _1, _2): return queryset + def get_filter_data(self) -> FilterData: + phase = self.data.get("phase") + annotation_tasks__status = self.data.get("annotation_tasks__status") + return { + "phase": AnnotationPhase.Type(phase) if phase is not None else phase, + "annotation_campaign": AnnotationCampaign.objects.filter( + id=self.data.get("annotation_campaign") + ).first(), + "annotator": User.objects.filter(id=self.data.get("annotator")).first(), + "annotation_tasks__status": AnnotationTask.Status(annotation_tasks__status) + if annotation_tasks__status is not None + else annotation_tasks__status, + "annotations__exists": self.data.get("annotations__exists"), + "annotations__acoustic_features__exists": self.data.get( + "annotations__acoustic_features__exists" + ), + "annotations__confidence": Confidence.objects.filter( + id=self.data.get("annotations__confidence") + ).first(), + "annotations__label": Label.objects.filter( + id=self.data.get("annotations__label") + ).first(), + "annotations__detector": Detector.objects.filter( + id=self.data.get("annotations__detector") + ).first(), + "annotations__annotator": User.objects.filter( + id=self.data.get("annotations__annotator") + ).first(), + "only_assigned": self.data.get("only_assigned"), + } + def filter_queryset(self, queryset: QuerySet[Spectrogram]): queryset: QuerySet[Spectrogram] = super().filter_queryset(queryset) - - # Filter: phase [AnnotationPhase.Type] - filter_phase: Optional[AnnotationPhase.Type] = self.data.get("phase") - # Filter: annotation_campaign [ID] - filter_annotation_campaign: Optional[ - AnnotationCampaign - ] = AnnotationCampaign.objects.filter( - id=self.data.get("annotation_campaign") - ).first() - # Filter: annotator [ID] - filter_annotator: Optional[User] = User.objects.filter( - id=self.data.get("annotator") - ).first() - - # Filter: annotation_tasks__status [AnnotationTaskStatus] - filter_annotation_tasks__status: Optional[AnnotationTaskStatus] = self.data.get( - "annotation_tasks__status" - ) - - # Filter: annotations__exists [bool] - filter_annotations__exists: Optional[bool] = self.data.get( - "annotations__exists" - ) - # Filter: annotations__confidence [ID] - filter_annotations__confidence: Optional[ - Confidence - ] = Confidence.objects.filter( - pk=self.data.get("annotations__confidence") - ).first() - # Filter: annotations__label [ID] - filter_annotations__label: Optional[Label] = Label.objects.filter( - pk=self.data.get("annotations__label") - ).first() - # Filter: annotations__acoustic_features__exists [bool] - filter_annotations__acoustic_features__exists: Optional[bool] = self.data.get( - "annotations__acoustic_features__exists" - ) - # Filter: annotations__detector [ID] - filter_annotations__detector: Optional[Detector] = Detector.objects.filter( - pk=self.data.get("annotations__detector") - ).first() - # Filter: annotations__annotator [ID] - filter_annotations__annotator: Optional[User] = User.objects.filter( - pk=self.data.get("annotations__annotator") - ).first() + filter_data = self.get_filter_data() # Filter: only_assigned [bool] filter_only_assigned: bool = ( - self.data.get("only_assigned", False) is not False - or filter_annotations__exists is not None - or filter_annotation_tasks__status is not None + filter_data["only_assigned"] is not False + or filter_data["annotations__exists"] is not None + or filter_data["annotation_tasks__status"] is not None ) # => QuerySet[AnnotationFileRange] & QuerySet[Annotation] file_ranges: QuerySet[AnnotationFileRange] = AnnotationFileRange.objects.all() annotations: QuerySet[Annotation] = Annotation.objects.all() - if filter_annotator: + if filter_data["annotator"]: file_ranges = AnnotationFileRange.objects.filter_viewable_by( - user=filter_annotator + user=filter_data["annotator"] ) - if filter_annotation_campaign: + if filter_data["annotation_campaign"]: file_ranges = file_ranges.filter( - annotation_phase__annotation_campaign=filter_annotation_campaign + annotation_phase__annotation_campaign=filter_data["annotation_campaign"] ) annotations = annotations.filter( - annotation_phase__annotation_campaign=filter_annotation_campaign + annotation_phase__annotation_campaign=filter_data["annotation_campaign"] ) queryset = queryset.filter( - analysis__annotation_campaigns=filter_annotation_campaign + analysis__annotation_campaigns=filter_data["annotation_campaign"] + ) + if filter_data["phase"]: + file_ranges = file_ranges.filter( + annotation_phase__phase=filter_data["phase"] ) - if filter_phase: - file_ranges = file_ranges.filter(annotation_phase__phase=filter_phase) - if filter_phase == AnnotationPhase.Type.ANNOTATION: - annotations = annotations.filter(annotation_phase__phase=filter_phase) - if filter_annotator: - annotations = annotations.filter(annotator=filter_annotator) - elif filter_annotator: + if filter_data["phase"] == AnnotationPhase.Type.ANNOTATION: + annotations = annotations.filter( + annotation_phase__phase=filter_data["phase"] + ) + if filter_data["annotator"]: + annotations = annotations.filter(annotator=filter_data["annotator"]) + elif filter_data["annotator"]: annotations = annotations.filter( ~Q( - annotator=filter_annotator, + annotator=filter_data["annotator"], annotation_phase__phase=AnnotationPhase.Type.ANNOTATION, ) ) @@ -148,7 +154,7 @@ def filter_queryset(self, queryset: QuerySet[Spectrogram]): ) # Filter on task status - if filter_annotation_tasks__status: + if filter_data["annotation_tasks__status"]: tasks_ids = [] for fr in file_ranges: tasks_ids += fr.tasks.values_list("id", flat=True) @@ -159,30 +165,39 @@ def filter_queryset(self, queryset: QuerySet[Spectrogram]): status=AnnotationTask.Status.FINISHED ).values_list("spectrogram_id", flat=True) query = Q(id__in=finished_task_spectrogram_ids) - if filter_annotation_tasks__status == AnnotationTask.Status.FINISHED: + if ( + filter_data["annotation_tasks__status"] + == AnnotationTask.Status.FINISHED + ): queryset = queryset.filter(query) - if filter_annotation_tasks__status == AnnotationTask.Status.CREATED: + if filter_data["annotation_tasks__status"] == AnnotationTask.Status.CREATED: queryset = queryset.filter(~query) # Created task may not exist at all - if filter_annotations__exists is not None: - if filter_annotations__exists: - if filter_annotations__label: - annotations = annotations.filter(label=filter_annotations__label) - if filter_annotations__confidence: + if filter_data["annotations__exists"] is not None: + if filter_data["annotations__exists"]: + if filter_data["annotations__label"]: + annotations = annotations.filter( + label=filter_data["annotations__label"] + ) + if filter_data["annotations__confidence"]: annotations = annotations.filter( - confidence=filter_annotations__confidence + confidence=filter_data["annotations__confidence"] ) - if filter_annotations__annotator: + if filter_data["annotations__annotator"]: annotations = annotations.filter( - annotator=filter_annotations__annotator + annotator=filter_data["annotations__annotator"] ) - if filter_annotations__detector: + if filter_data["annotations__detector"]: annotations = annotations.filter( - detector_configuration__detector=filter_annotations__detector + detector_configuration__detector=filter_data[ + "annotations__detector" + ] ) - if filter_annotations__acoustic_features__exists is not None: + if filter_data["annotations__acoustic_features__exists"] is not None: annotations = annotations.filter( - acoustic_features__isnull=not filter_annotations__acoustic_features__exists + acoustic_features__isnull=not filter_data[ + "annotations__acoustic_features__exists" + ] ) annotations_spectrogram_ids = annotations.values_list( From d2af598fd6ea5e12d165df47e9add2f95fc598ee Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Mon, 28 Sep 2026 12:20:18 +0200 Subject: [PATCH 3/5] fix: dataset folder in docker installation --- frontend/docs/dev/installation/docker.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/docs/dev/installation/docker.md b/frontend/docs/dev/installation/docker.md index ccd078c2c..f20866d4a 100644 --- a/frontend/docs/dev/installation/docker.md +++ b/frontend/docs/dev/installation/docker.md @@ -48,7 +48,7 @@ In the `docker-compose.yml` file: To access the audio files and spectrogram, APLOSE mount the volume where the data is located inside its containers (see `osmose_back` and `osmose_front` services volumes). -For development purpose we use a `/volumes/datawork` folder in the project root folder. This can be changed at any moment. Just be sure to update it both in front and back services. +For development purpose we use a `/volumes/datawork/dataset` folder in the project root folder. This can be changed at any moment. Just be sure to update it both in front and back services. ::: info Note The format for volume mount is [local mount]:[container mount], only the local mount should be changed. From ce0a4bdb2d89466de438d156cb3dd85bb54740c2 Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Mon, 28 Sep 2026 15:53:03 +0200 Subject: [PATCH 4/5] fix(backend): test on annotation spectrogram list --- .../schema/filter_sets/annotation_spectrogram.py | 12 ++++++++++-- .../all_annotation_spectrograms.py | 16 ---------------- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/backend/api/schema/filter_sets/annotation_spectrogram.py b/backend/api/schema/filter_sets/annotation_spectrogram.py index 25e232ce5..391a9ecc9 100644 --- a/backend/api/schema/filter_sets/annotation_spectrogram.py +++ b/backend/api/schema/filter_sets/annotation_spectrogram.py @@ -102,10 +102,11 @@ def filter_queryset(self, queryset: QuerySet[Spectrogram]): # Filter: only_assigned [bool] filter_only_assigned: bool = ( - filter_data["only_assigned"] is not False + filter_data['only_assigned'] or filter_data["annotations__exists"] is not None or filter_data["annotation_tasks__status"] is not None ) + annotator_can_see_all = filter_data['annotator'].is_staff or filter_data['annotator'].is_superuser # => QuerySet[AnnotationFileRange] & QuerySet[Annotation] file_ranges: QuerySet[AnnotationFileRange] = AnnotationFileRange.objects.all() @@ -114,7 +115,10 @@ def filter_queryset(self, queryset: QuerySet[Spectrogram]): file_ranges = AnnotationFileRange.objects.filter_viewable_by( user=filter_data["annotator"] ) + if filter_only_assigned: + file_ranges = file_ranges.filter(annotator=filter_data['annotator']) if filter_data["annotation_campaign"]: + annotator_can_see_all = annotator_can_see_all or filter_data['annotation_campaign'].owner_id == filter_data['annotator'].id file_ranges = file_ranges.filter( annotation_phase__annotation_campaign=filter_data["annotation_campaign"] ) @@ -128,6 +132,10 @@ def filter_queryset(self, queryset: QuerySet[Spectrogram]): file_ranges = file_ranges.filter( annotation_phase__phase=filter_data["phase"] ) + if filter_data['annotation_campaign']: + phase = filter_data['annotation_campaign'].phases.filter(phase=filter_data["phase"]).first() + if phase: + annotator_can_see_all = annotator_can_see_all or phase.created_by_id == filter_data['annotator'].id if filter_data["phase"] == AnnotationPhase.Type.ANNOTATION: annotations = annotations.filter( annotation_phase__phase=filter_data["phase"] @@ -143,7 +151,7 @@ def filter_queryset(self, queryset: QuerySet[Spectrogram]): ) # Filter assigned spectrograms - if filter_only_assigned: + if filter_only_assigned or not annotator_can_see_all: queryset = queryset.filter( Exists( file_ranges.filter( diff --git a/backend/api/tests/schema/annotation_spectrogram/all_annotation_spectrograms.py b/backend/api/tests/schema/annotation_spectrogram/all_annotation_spectrograms.py index decc75d51..983665aef 100644 --- a/backend/api/tests/schema/annotation_spectrogram/all_annotation_spectrograms.py +++ b/backend/api/tests/schema/annotation_spectrogram/all_annotation_spectrograms.py @@ -343,22 +343,6 @@ def test_connected_admin__label(self): content = json.loads(response.content)["data"]["allAnnotationSpectrograms"] self.assertEqual(content["totalCount"], 1) - def test_connected_admin__confidence_empty(self): - response = self.gql_query( - QUERY, - user=User.objects.get(username="admin"), - variables={ - **VARIABLES, - "annotatorID": 1, - "withAnnotations": True, - "annotationConfidence": 3, - }, - ) - self.assertResponseNoErrors(response) - - content = json.loads(response.content)["data"]["allAnnotationSpectrograms"] - self.assertEqual(content["totalCount"], 0) - def test_connected_admin__confidence(self): response = self.gql_query( QUERY, From 79953958ec38da66195153b7737e871a13b63298 Mon Sep 17 00:00:00 2001 From: Elodie MORIN Date: Mon, 28 Sep 2026 16:04:55 +0200 Subject: [PATCH 5/5] fix(backend): test on annotation spectrogram list --- .../filter_sets/annotation_spectrogram.py | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/backend/api/schema/filter_sets/annotation_spectrogram.py b/backend/api/schema/filter_sets/annotation_spectrogram.py index 391a9ecc9..8b829ddbb 100644 --- a/backend/api/schema/filter_sets/annotation_spectrogram.py +++ b/backend/api/schema/filter_sets/annotation_spectrogram.py @@ -96,17 +96,20 @@ def get_filter_data(self) -> FilterData: "only_assigned": self.data.get("only_assigned"), } - def filter_queryset(self, queryset: QuerySet[Spectrogram]): - queryset: QuerySet[Spectrogram] = super().filter_queryset(queryset) + def filter_on_file_ranges( + self, queryset: QuerySet[Spectrogram] + ) -> tuple[QuerySet[Spectrogram], QuerySet[Annotation]]: filter_data = self.get_filter_data() # Filter: only_assigned [bool] filter_only_assigned: bool = ( - filter_data['only_assigned'] + filter_data["only_assigned"] or filter_data["annotations__exists"] is not None or filter_data["annotation_tasks__status"] is not None ) - annotator_can_see_all = filter_data['annotator'].is_staff or filter_data['annotator'].is_superuser + annotator_can_see_all = ( + filter_data["annotator"].is_staff or filter_data["annotator"].is_superuser + ) # => QuerySet[AnnotationFileRange] & QuerySet[Annotation] file_ranges: QuerySet[AnnotationFileRange] = AnnotationFileRange.objects.all() @@ -116,9 +119,13 @@ def filter_queryset(self, queryset: QuerySet[Spectrogram]): user=filter_data["annotator"] ) if filter_only_assigned: - file_ranges = file_ranges.filter(annotator=filter_data['annotator']) + file_ranges = file_ranges.filter(annotator=filter_data["annotator"]) if filter_data["annotation_campaign"]: - annotator_can_see_all = annotator_can_see_all or filter_data['annotation_campaign'].owner_id == filter_data['annotator'].id + annotator_can_see_all = ( + annotator_can_see_all + or filter_data["annotation_campaign"].owner_id + == filter_data["annotator"].id + ) file_ranges = file_ranges.filter( annotation_phase__annotation_campaign=filter_data["annotation_campaign"] ) @@ -132,10 +139,17 @@ def filter_queryset(self, queryset: QuerySet[Spectrogram]): file_ranges = file_ranges.filter( annotation_phase__phase=filter_data["phase"] ) - if filter_data['annotation_campaign']: - phase = filter_data['annotation_campaign'].phases.filter(phase=filter_data["phase"]).first() + if filter_data["annotation_campaign"]: + phase = ( + filter_data["annotation_campaign"] + .phases.filter(phase=filter_data["phase"]) + .first() + ) if phase: - annotator_can_see_all = annotator_can_see_all or phase.created_by_id == filter_data['annotator'].id + annotator_can_see_all = ( + annotator_can_see_all + or phase.created_by_id == filter_data["annotator"].id + ) if filter_data["phase"] == AnnotationPhase.Type.ANNOTATION: annotations = annotations.filter( annotation_phase__phase=filter_data["phase"] @@ -181,6 +195,13 @@ def filter_queryset(self, queryset: QuerySet[Spectrogram]): if filter_data["annotation_tasks__status"] == AnnotationTask.Status.CREATED: queryset = queryset.filter(~query) # Created task may not exist at all + return queryset, annotations + + def filter_queryset(self, queryset: QuerySet[Spectrogram]): + queryset: QuerySet[Spectrogram] = super().filter_queryset(queryset) + queryset, annotations = self.filter_on_file_ranges(queryset) + filter_data = self.get_filter_data() + if filter_data["annotations__exists"] is not None: if filter_data["annotations__exists"]: if filter_data["annotations__label"]: