5
5
from django .contrib .postgres .search import TrigramSimilarity
6
6
from django .core import exceptions
7
7
from django .db import models
8
- from django .db .models import Prefetch
8
+ from django .db .models import Prefetch , Q
9
9
from django .db .models .functions import Coalesce
10
10
from django .db .models .query import QuerySet
11
11
from django .forms import BooleanField , CharField , IntegerField
59
59
SourceImage ,
60
60
SourceImageCollection ,
61
61
SourceImageUpload ,
62
+ TaxaList ,
62
63
Taxon ,
63
64
User ,
64
65
update_detection_counts ,
89
90
SourceImageUploadSerializer ,
90
91
StorageSourceSerializer ,
91
92
StorageStatusSerializer ,
93
+ TaxaListSerializer ,
92
94
TaxonListSerializer ,
93
95
TaxonSearchResultSerializer ,
94
96
TaxonSerializer ,
@@ -967,6 +969,38 @@ def filter_queryset(self, request, queryset, view):
967
969
return queryset
968
970
969
971
972
+ class OccurrenceTaxaListFilter (filters .BaseFilterBackend ):
973
+ """
974
+ Filters occurrences based on a TaxaList.
975
+
976
+ Queries for all occurrences where the determination taxon is either:
977
+ - Directly in the requested TaxaList.
978
+ - A descendant (child or deeper) of any taxon in the TaxaList, recursively.
979
+
980
+ """
981
+
982
+ query_param = "taxa_list_id"
983
+
984
+ def filter_queryset (self , request , queryset , view ):
985
+ taxalist_id = IntegerField (required = False ).clean (request .query_params .get (self .query_param ))
986
+ if taxalist_id :
987
+ taxa_list = TaxaList .objects .filter (id = taxalist_id ).first ()
988
+ if taxa_list :
989
+ taxa = taxa_list .taxa .all () # Get taxalist taxon objects
990
+
991
+ # filter by the exact determination
992
+ query_filter = Q (determination__in = taxa )
993
+
994
+ # filter by the taxon's children
995
+ for taxon in taxa :
996
+ query_filter |= Q (determination__parents_json__contains = [{"id" : taxon .pk }])
997
+
998
+ queryset = queryset .filter (query_filter )
999
+ return queryset
1000
+
1001
+ return queryset
1002
+
1003
+
970
1004
class TaxonCollectionFilter (filters .BaseFilterBackend ):
971
1005
"""
972
1006
Filter taxa by the collection their occurrences belong to.
@@ -999,6 +1033,7 @@ class OccurrenceViewSet(DefaultViewSet, ProjectMixin):
999
1033
OccurrenceDateFilter ,
1000
1034
OccurrenceVerified ,
1001
1035
OccurrenceVerifiedByMeFilter ,
1036
+ OccurrenceTaxaListFilter ,
1002
1037
]
1003
1038
filterset_fields = [
1004
1039
"event" ,
@@ -1030,9 +1065,9 @@ def get_serializer_class(self):
1030
1065
else :
1031
1066
return OccurrenceSerializer
1032
1067
1033
- def get_queryset (self ) -> QuerySet :
1068
+ def get_queryset (self ) -> QuerySet [ "Occurrence" ] :
1034
1069
project = self .get_active_project ()
1035
- qs = super ().get_queryset ()
1070
+ qs = super ().get_queryset (). valid () # type: ignore
1036
1071
if project :
1037
1072
qs = qs .filter (project = project )
1038
1073
qs = qs .select_related (
@@ -1046,10 +1081,7 @@ def get_queryset(self) -> QuerySet:
1046
1081
if self .action == "list" :
1047
1082
qs = (
1048
1083
qs .all ()
1049
- .exclude (detections = None )
1050
- .exclude (event = None )
1051
1084
.filter (determination_score__gte = get_active_classification_threshold (self .request ))
1052
- .exclude (first_appearance_timestamp = None ) # This must come after annotations
1053
1085
.order_by ("-determination_score" )
1054
1086
)
1055
1087
@@ -1067,14 +1099,45 @@ def list(self, request, *args, **kwargs):
1067
1099
return super ().list (request , * args , ** kwargs )
1068
1100
1069
1101
1102
+ class TaxonTaxaListFilter (filters .BaseFilterBackend ):
1103
+ """
1104
+ Filters taxa based on a TaxaList Similar to `OccurrenceTaxaListFilter`.
1105
+
1106
+ Queries for all taxa that are either:
1107
+ - Directly in the requested TaxaList.
1108
+ - A descendant (child or deeper) of any taxon in the TaxaList, recursively.
1109
+ """
1110
+
1111
+ query_param = "taxa_list_id"
1112
+
1113
+ def filter_queryset (self , request , queryset , view ):
1114
+ taxalist_id = IntegerField (required = False ).clean (request .query_params .get (self .query_param ))
1115
+ if taxalist_id :
1116
+ taxa_list = TaxaList .objects .filter (id = taxalist_id ).first ()
1117
+ if taxa_list :
1118
+ taxa = taxa_list .taxa .all () # Get taxa in the TaxaList
1119
+ query_filter = Q (id__in = taxa )
1120
+ for taxon in taxa :
1121
+ query_filter |= Q (parents_json__contains = [{"id" : taxon .pk }])
1122
+
1123
+ queryset = queryset .filter (query_filter )
1124
+ return queryset
1125
+
1126
+ return queryset
1127
+
1128
+
1070
1129
class TaxonViewSet (DefaultViewSet , ProjectMixin ):
1071
1130
"""
1072
1131
API endpoint that allows taxa to be viewed or edited.
1073
1132
"""
1074
1133
1075
1134
queryset = Taxon .objects .all ().defer ("notes" )
1076
1135
serializer_class = TaxonSerializer
1077
- filter_backends = DefaultViewSetMixin .filter_backends + [CustomTaxonFilter , TaxonCollectionFilter ]
1136
+ filter_backends = DefaultViewSetMixin .filter_backends + [
1137
+ CustomTaxonFilter ,
1138
+ TaxonCollectionFilter ,
1139
+ TaxonTaxaListFilter ,
1140
+ ]
1078
1141
filterset_fields = [
1079
1142
"name" ,
1080
1143
"rank" ,
@@ -1286,6 +1349,19 @@ def list(self, request, *args, **kwargs):
1286
1349
return super ().list (request , * args , ** kwargs )
1287
1350
1288
1351
1352
+ class TaxaListViewSet (viewsets .ModelViewSet , ProjectMixin ):
1353
+ queryset = TaxaList .objects .all ()
1354
+
1355
+ def get_queryset (self ):
1356
+ qs = super ().get_queryset ()
1357
+ project = self .get_active_project ()
1358
+ if project :
1359
+ return qs .filter (projects = project )
1360
+ return qs
1361
+
1362
+ serializer_class = TaxaListSerializer
1363
+
1364
+
1289
1365
class ClassificationViewSet (DefaultViewSet , ProjectMixin ):
1290
1366
"""
1291
1367
API endpoint for viewing and adding classification results from a model.
@@ -1343,11 +1419,7 @@ def get(self, request):
1343
1419
"events_count" : Event .objects .filter (deployment__project = project , deployment__isnull = False ).count (),
1344
1420
"captures_count" : SourceImage .objects .filter (deployment__project = project ).count (),
1345
1421
# "detections_count": Detection.objects.filter(occurrence__project=project).count(),
1346
- "occurrences_count" : Occurrence .objects .filter (
1347
- project = project ,
1348
- # determination_score__gte=confidence_threshold,
1349
- event__isnull = False ,
1350
- ).count (),
1422
+ "occurrences_count" : Occurrence .objects .valid ().filter (project = project ).count (), # type: ignore
1351
1423
"taxa_count" : Occurrence .objects .all ().unique_taxa (project = project ).count (), # type: ignore
1352
1424
}
1353
1425
else :
@@ -1357,10 +1429,7 @@ def get(self, request):
1357
1429
"events_count" : Event .objects .filter (deployment__isnull = False ).count (),
1358
1430
"captures_count" : SourceImage .objects .count (),
1359
1431
# "detections_count": Detection.objects.count(),
1360
- "occurrences_count" : Occurrence .objects .filter (
1361
- # determination_score__gte=confidence_threshold,
1362
- event__isnull = False
1363
- ).count (),
1432
+ "occurrences_count" : Occurrence .objects .valid ().count (), # type: ignore
1364
1433
"taxa_count" : Occurrence .objects .all ().unique_taxa ().count (), # type: ignore
1365
1434
"last_updated" : timezone .now (),
1366
1435
}
0 commit comments