Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
306 changes: 175 additions & 131 deletions backend/api/schema/filter_sets/annotation_spectrogram.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.db.models import QuerySet, Exists, OuterRef, Subquery
from typing import Optional, TypedDict

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
Expand All @@ -8,13 +10,31 @@
AnnotationFileRange,
AnnotationTask,
Annotation,
AnnotationPhase,
AnnotationCampaign,
Confidence,
Label,
Detector,
AnnotationPhase,
)
from backend.api.schema.enums import AnnotationPhaseType, AnnotationTaskStatus
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")
Expand Down Expand Up @@ -45,147 +65,108 @@ class Meta:
def fake(self, queryset, _1, _2):
return queryset

def filter_queryset(self, queryset: QuerySet[Spectrogram]):
queryset = super().filter_queryset(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_on_file_ranges(
self, queryset: QuerySet[Spectrogram]
) -> tuple[QuerySet[Spectrogram], QuerySet[Annotation]]:
filter_data = self.get_filter_data()

queryset, file_ranges, tasks, annotations = self._get_querysets_for_filter(
queryset, only_assigned=self.data.get("only_assigned", False)
# Filter: only_assigned [bool]
filter_only_assigned: bool = (
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
)

# 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")
)
# => QuerySet[AnnotationFileRange] & QuerySet[Annotation]
file_ranges: QuerySet[AnnotationFileRange] = AnnotationFileRange.objects.all()
annotations: QuerySet[Annotation] = Annotation.objects.all()
if filter_data["annotator"]:
file_ranges = AnnotationFileRange.objects.filter_viewable_by(
user=filter_data["annotator"]
)
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)

q = Exists(
Subquery(
annotations.filter(
spectrogram_id=OuterRef("id"),
)
)
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
)
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:
file_ranges = file_ranges.filter(
annotation_phase__annotation_campaign_id=campaign_id
annotation_phase__annotation_campaign=filter_data["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_data["annotation_campaign"]
)
spectrograms = spectrograms.filter(
analysis__annotation_campaigns__id=campaign_id
queryset = queryset.filter(
analysis__annotation_campaigns=filter_data["annotation_campaign"]
)

if phase_type and campaign_id:
if phase_type == AnnotationPhase.Type.ANNOTATION:
if filter_data["phase"]:
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"]
)
if filter_data["annotator"]:
annotations = annotations.filter(annotator=filter_data["annotator"])
elif filter_data["annotator"]:
annotations = annotations.filter(
annotation_phase__phase=phase_type,
annotation_phase__annotation_campaign_id=campaign_id,
~Q(
annotator=filter_data["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 or not annotator_can_see_all:
queryset = queryset.filter(
Exists(
file_ranges.filter(
from_datetime__lte=OuterRef("start"),
Expand All @@ -194,4 +175,67 @@ def _get_querysets_for_filter(
)
)

return spectrograms, file_ranges, tasks, annotations
# Filter on task status
if filter_data["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_data["annotation_tasks__status"]
== AnnotationTask.Status.FINISHED
):
queryset = queryset.filter(query)
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"]:
annotations = annotations.filter(
label=filter_data["annotations__label"]
)
if filter_data["annotations__confidence"]:
annotations = annotations.filter(
confidence=filter_data["annotations__confidence"]
)
if filter_data["annotations__annotator"]:
annotations = annotations.filter(
annotator=filter_data["annotations__annotator"]
)
if filter_data["annotations__detector"]:
annotations = annotations.filter(
detector_configuration__detector=filter_data[
"annotations__detector"
]
)
if filter_data["annotations__acoustic_features__exists"] is not None:
annotations = annotations.filter(
acoustic_features__isnull=not filter_data[
"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()
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion frontend/docs/dev/installation/docker.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading