@@ -874,10 +874,57 @@ def save(self, update_calculated_fields=True, regroup_async=True, *args, **kwarg
874
874
# ami.tasks.model_task.delay("Project", self.project.pk, "update_children_project")
875
875
876
876
877
+ class EventQuerySet (models .QuerySet ):
878
+ def with_taxa_count (self , project : Project | None = None , request : Request | None = None ):
879
+ """
880
+ Annotate each event with the number of distinct taxa observed,
881
+ filtered by classification threshold and the project's default
882
+ include/exclude taxa settings.
883
+ """
884
+ if project is None :
885
+ return self
886
+
887
+ classification_threshold = get_default_classification_threshold (project , request )
888
+
889
+ # Start with a base filter for classification score
890
+ filter_q = models .Q (
891
+ occurrences__determination_score__gte = classification_threshold ,
892
+ )
893
+
894
+ # Apply include/exclude taxa from project defaults
895
+ include_taxa = project .default_filters_include_taxa .all ()
896
+ exclude_taxa = project .default_filters_exclude_taxa .all ()
897
+
898
+ if include_taxa .exists ():
899
+ include_filter = models .Q (occurrences__determination__in = include_taxa )
900
+ for taxon in include_taxa :
901
+ include_filter |= models .Q (occurrences__determination__parents_json__contains = [{"id" : taxon .pk }])
902
+ filter_q &= include_filter
903
+
904
+ if exclude_taxa .exists ():
905
+ exclude_filter = models .Q (occurrences__determination__in = exclude_taxa )
906
+ for taxon in exclude_taxa :
907
+ exclude_filter |= models .Q (occurrences__determination__parents_json__contains = [{"id" : taxon .pk }])
908
+ filter_q &= ~ exclude_filter
909
+
910
+ return self .annotate (
911
+ taxa_count = models .Count (
912
+ "occurrences__determination" ,
913
+ distinct = True ,
914
+ filter = filter_q ,
915
+ )
916
+ )
917
+
918
+
919
+ class EventManager (models .Manager .from_queryset (EventQuerySet )):
920
+ pass
921
+
922
+
877
923
@final
878
924
class Event (BaseModel ):
879
925
"""A monitoring session"""
880
926
927
+ objects : EventManager = EventManager ()
881
928
group_by = models .CharField (
882
929
max_length = 255 ,
883
930
db_index = True ,
@@ -1499,24 +1546,54 @@ def delete_source_image(sender, instance, **kwargs):
1499
1546
1500
1547
1501
1548
class SourceImageQuerySet (models .QuerySet ):
1502
- def with_occurrences_count (self , classification_threshold : float = 0 ):
1549
+ def _build_default_taxa_filter (
1550
+ self ,
1551
+ classification_threshold : float = 0 ,
1552
+ project : Project | None = None ,
1553
+ ) -> Q :
1554
+ """
1555
+ Build a reusable Q filter that applies the classification threshold
1556
+ and the project's default include/exclude taxa settings.
1557
+ """
1558
+ filter_q = Q (detections__occurrence__determination_score__gte = classification_threshold )
1559
+
1560
+ if not project :
1561
+ return filter_q
1562
+
1563
+ include_taxa = project .default_filters_include_taxa .all ()
1564
+ exclude_taxa = project .default_filters_exclude_taxa .all ()
1565
+
1566
+ if include_taxa .exists ():
1567
+ include_q = Q (detections__occurrence__determination__in = include_taxa )
1568
+ for taxon in include_taxa :
1569
+ include_q |= Q (detections__occurrence__determination__parents_json__contains = [{"id" : taxon .pk }])
1570
+ filter_q &= include_q
1571
+
1572
+ if exclude_taxa .exists ():
1573
+ exclude_q = Q (detections__occurrence__determination__in = exclude_taxa )
1574
+ for taxon in exclude_taxa :
1575
+ exclude_q |= Q (detections__occurrence__determination__parents_json__contains = [{"id" : taxon .pk }])
1576
+ filter_q &= ~ exclude_q
1577
+
1578
+ return filter_q
1579
+
1580
+ def with_occurrences_count (self , classification_threshold : float = 0 , project : Project | None = None ):
1581
+ filter_q = self ._build_default_taxa_filter (classification_threshold , project )
1503
1582
return self .annotate (
1504
1583
occurrences_count = models .Count (
1505
1584
"detections__occurrence" ,
1506
- filter = models .Q (
1507
- detections__occurrence__determination_score__gte = classification_threshold ,
1508
- ),
1585
+ filter = filter_q ,
1509
1586
distinct = True ,
1510
1587
)
1511
1588
)
1512
1589
1513
- def with_taxa_count (self , classification_threshold : float = 0 ):
1590
+ def with_taxa_count (self , classification_threshold : float = 0 , project : Project | None = None ):
1591
+ filter_q = self ._build_default_taxa_filter (classification_threshold , project )
1592
+
1514
1593
return self .annotate (
1515
1594
taxa_count = models .Count (
1516
1595
"detections__occurrence__determination" ,
1517
- filter = models .Q (
1518
- detections__occurrence__determination_score__gte = classification_threshold ,
1519
- ),
1596
+ filter = filter_q ,
1520
1597
distinct = True ,
1521
1598
)
1522
1599
)
@@ -2466,6 +2543,33 @@ def filter_by_score_threshold(self, project: Project | None = None, request: Req
2466
2543
score_threshold = get_default_classification_threshold (project , request )
2467
2544
return self .filter (determination_score__gte = score_threshold )
2468
2545
2546
+ def filter_by_project_default_taxa (self , project : Project | None = None , request : Request | None = None ):
2547
+ if project is None :
2548
+ return self
2549
+ if request is not None :
2550
+ apply_defaults = request .query_params .get ("apply_defaults" , "true" ).lower ()
2551
+ if apply_defaults == "false" :
2552
+ return self
2553
+ qs = self
2554
+ include_taxa = project .default_filters_include_taxa .all ()
2555
+ exclude_taxa = project .default_filters_exclude_taxa .all ()
2556
+
2557
+ include_filter = Q ()
2558
+ if include_taxa .exists ():
2559
+ include_filter = Q (determination__in = include_taxa )
2560
+ for taxon in include_taxa :
2561
+ include_filter |= Q (determination__parents_json__contains = [{"id" : taxon .pk }])
2562
+ qs = qs .filter (include_filter )
2563
+
2564
+ exclude_filter = Q ()
2565
+ if exclude_taxa .exists ():
2566
+ exclude_filter = Q (determination__in = exclude_taxa )
2567
+ for taxon in exclude_taxa :
2568
+ exclude_filter |= Q (determination__parents_json__contains = [{"id" : taxon .pk }])
2569
+ qs = qs .exclude (exclude_filter )
2570
+
2571
+ return qs
2572
+
2469
2573
2470
2574
class OccurrenceManager (models .Manager .from_queryset (OccurrenceQuerySet )):
2471
2575
def get_queryset (self ):
@@ -2732,6 +2836,33 @@ def with_occurrence_counts(self, project: Project):
2732
2836
2733
2837
return qs .annotate (occurrence_count = models .Count ("occurrences" , distinct = True ))
2734
2838
2839
+ def filter_by_project_default_taxa (self , project : Project | None = None , request : Request | None = None ):
2840
+ """
2841
+ Filter taxa according to a project's default include and exclude settings,
2842
+ keeping taxa in the include set along with their descendants
2843
+ and removing taxa in the exclude set along with their descendants.
2844
+ """
2845
+ if project is None :
2846
+ return self
2847
+
2848
+ qs = self
2849
+ include_taxa = project .default_filters_include_taxa .all ()
2850
+ exclude_taxa = project .default_filters_exclude_taxa .all ()
2851
+
2852
+ if include_taxa .exists ():
2853
+ include_filter = Q (id__in = include_taxa )
2854
+ for taxon in include_taxa :
2855
+ include_filter |= Q (parents_json__contains = [{"id" : taxon .pk }])
2856
+ qs = qs .filter (include_filter )
2857
+
2858
+ if exclude_taxa .exists ():
2859
+ exclude_filter = Q (id__in = exclude_taxa )
2860
+ for taxon in exclude_taxa :
2861
+ exclude_filter |= Q (parents_json__contains = [{"id" : taxon .pk }])
2862
+ qs = qs .exclude (exclude_filter )
2863
+
2864
+ return qs
2865
+
2735
2866
2736
2867
@final
2737
2868
class TaxonManager (models .Manager .from_queryset (TaxonQuerySet )):
0 commit comments