Skip to content

Default taxa filter follow-up: handle cached counts and add default taxa to projects#1045

Merged
mihow merged 11 commits intomainfrom
fix/default-filters-followup-default-taxa-and-cache
Apr 17, 2026
Merged

Default taxa filter follow-up: handle cached counts and add default taxa to projects#1045
mihow merged 11 commits intomainfrom
fix/default-filters-followup-default-taxa-and-cache

Conversation

@mohamedelabbas1996
Copy link
Copy Markdown
Contributor

@mohamedelabbas1996 mohamedelabbas1996 commented Nov 11, 2025

Summary

Makes sure project-level default filters (score threshold plus include/exclude taxa) are applied consistently wherever the UI shows counts — taxa pages, sessions, stations, and images — so users see the same totals for the same project regardless of which view they're on.

This PR continues the default filters follow-up work from #1029 by adding environment-variable–based default include and exclude taxa to projects.
It also ensures that all cached count fields (detections, occurrences, and taxa counts) consistently respect the project's default filters across the Deployment, Event, and SourceImage models.

Finally, this PR fixes an inconsistency in the Taxon Detail view, where the project's default score threshold was not being applied causing the direct occurrence count on the detail page to diverge from the count shown in the Taxon List view.

List of Changes

  • Added DEFAULT_INCLUDE_TAXA and DEFAULT_EXCLUDE_TAXA environment variables and updated get_or_create_default_project() to attach these default taxa to newly created default projects.

  • Fixed get_or_create_default_project() to explicitly call Project.objects.create_related_defaults(project)get_or_create bypasses the custom ProjectManager.create() override, so the default device, site, deployment, collection, and processing service were not being created.

  • Added signals to refresh cached counts when:

    • project's default_filters_score_threshold changes
    • include taxa are added/removed/cleared
    • exclude taxa are added/removed/cleared
  • Moved the cached-count fan-out off the request/save path: the signals now enqueue a new refresh_project_cached_counts Celery task via transaction.on_commit.

  • Applied project default filters to detection counting in:

    • Deployment.get_detections_count()

    • Event.get_detections_count()

    • SourceImage.get_detections_count()

  • Aligned Deployment.get_detections_count() and Event.get_detections_count() with SourceImage.get_detections_count() by excluding null-bbox placeholder rows (NULL_DETECTIONS_FILTER).

  • Extended update_detection_counts() to accept an optional project kwarg and updated Project.update_related_calculated_fields() to refresh the cached SourceImage.detections_count in addition to Events and Deployments, so the stored count stays consistent with the filter-aware getter.

  • Fixed get_taxa_observed() to allow separate application of score vs taxa filters

Related Issues

#994

Detailed Description

This PR completes the default taxa filter follow-up tasks by addressing three issues:

1. Environment-based default include/exclude taxa

Default include/exclude taxa can now be provided via environment variables (DEFAULT_INCLUDE_TAXA, DEFAULT_EXCLUDE_TAXA).
These are automatically assigned to default projects during creation.

2. Cached count updates more efficiently

Cached fields for occurrences, detections, and taxa counts in Deployment, Event, and SourceImage are now updated only when the project's default filter values change.
Signals detect changes to: default score threshold, default include taxa and default exclude taxa and only then trigger recalculation of cached counts. The recalculation itself runs in a Celery task (refresh_project_cached_counts) dispatched on transaction commit, so the request/save path is not blocked by the fan-out across every event, deployment, and source image in the project.

3. Taxon Detail view filter consistency fix

The Taxon Detail view previously skipped the default score threshold, causing mismatched occurrence counts compared to the Taxon List view.
This has now been corrected, ensuring consistent filtering across both views.

How to Test the Changes

  1. Set environment variables for default taxa
DEFAULT_INCLUDE_TAXA="TaxonA,TaxonB"
DEFAULT_EXCLUDE_TAXA="TaxonC"

call get_or_create_default_project(user)
Confirm include/exclude taxa are assigned, and that the project also has a default device, site, deployment, collection, and processing service.
2.
a. Update the project's default score threshold (default_filters_score_threshold) from the Project admin page antenna ui.
Verify that detections, occurrences, and taxa counts update consistently in:

  • Session (Event) list view

  • Session detail view

  • Stations (Deployment) list view

  • Images list (cached detections_count on source images)

b. Modify the project's default include or exclude taxa (add/remove taxa).

Again, verify that all cached counts update correctly across:

  • Event list & detail views

  • Deployment (stations) list view

  • Images list

  1. Visit Taxon List and Taxon Detail views and confirm direct occurrence counts now match.

Screenshots

N/A

Deployment Notes

Make sure to set the environment variables for default project taxa:
DEFAULT_INCLUDE_TAXA
DEFAULT_EXCLUDE_TAXA

Checklist

  • I have tested these changes appropriately.
  • I have added and/or modified relevant tests.
  • I updated relevant documentation or comments.
  • I have verified that this PR follows the project's coding standards.
  • Any dependent changes have already been merged to main.

Summary by CodeRabbit

  • New Features

    • Configurable default taxa include/exclude lists via environment settings
    • Detection counts now respect project default filters across deployments, events, and images
  • Improvements

    • More granular control: separate toggles for taxa-based and score-based default filters
    • Automatic refresh of cached counts when project filter settings or taxa selections change

@netlify
Copy link
Copy Markdown

netlify Bot commented Nov 11, 2025

Deploy Preview for antenna-preview canceled.

Name Link
🔨 Latest commit 8235922
🔍 Latest deploy log https://app.netlify.com/projects/antenna-preview/deploys/69e183e4bf272b0008060916

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Nov 11, 2025

📝 Walkthrough

Walkthrough

Introduces granular default filter flags, environment-backed default include/exclude taxa, project-level default filter initialization, filtered detection count methods, and signals to refresh cached counts when project default filters change.

Changes

Cohort / File(s) Summary
Configuration & Settings
config/settings/base.py
Added DEFAULT_INCLUDE_TAXA and DEFAULT_EXCLUDE_TAXA environment-backed list settings.
Model defaults & counts
ami/main/models.py
Added get_project_default_filters(); changed get_or_create_default_project() to apply default taxa when creating the scratch project; added/updated Deployment.get_detections_count(), Event.get_detections_count(), SourceImage.get_detections_count() and adjusted update_calculated_fields() to use filtered counts.
Query filter builder
ami/main/models_future/filters.py
build_occurrence_default_filters_q() gains apply_default_score_filter and apply_default_taxa_filter booleans (default True); score and taxa clauses are applied conditionally based on these flags.
API Viewset
ami/main/api/views.py
TaxonViewSet.get_taxa_observed() signature now takes apply_default_score_filter and apply_default_taxa_filter; get_queryset() (retrieve) calls it with apply_default_score_filter=True, apply_default_taxa_filter=False; default-filters Q built with the new flags and applied to base filter.
Signals & cache refresh
ami/main/signals.py
Added refresh_cached_counts_for_project() plus pre_save/post_save (cache_old_threshold, threshold_updated) and m2m_changed (include_taxa_updated, exclude_taxa_updated) handlers to refresh cached counts when default filters change.

Sequence Diagram

sequenceDiagram
    actor Client
    participant ViewSet as TaxonViewSet
    participant Filters as build_occurrence_default_filters_q
    participant Models as Project/Deployment/Event
    participant Signals as Signal Handlers

    Client->>ViewSet: retrieve(project_id)
    ViewSet->>ViewSet: get_queryset()
    ViewSet->>ViewSet: get_taxa_observed(apply_default_score_filter=True, apply_default_taxa_filter=False)
    ViewSet->>Filters: build_occurrence_default_filters_q(project, ..., apply_default_score_filter=True, apply_default_taxa_filter=False)
    Note over Filters: score filter applied (if enabled)\ntaxa filter skipped
    Filters-->>ViewSet: Q object
    ViewSet->>Models: query taxa/occurrences using Q
    Models-->>ViewSet: taxa queryset
    ViewSet-->>Client: response

    Note over Signals: When project defaults change
    Models->>Signals: pre_save (cache_old_threshold)
    Signals->>Signals: store old threshold
    Models->>Signals: post_save (threshold_updated)
    Signals->>Models: refresh_cached_counts_for_project() [if changed]
    Signals->>Models: update_related_calculated_fields()
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • Follow-up fixes to default filters #994 — Implements env-driven default include/exclude taxa, project default filters helper, granular default-filter flags, and signal-driven cached-count refresh which align with this PR’s changes.

Possibly related PRs

Suggested labels

backend

Poem

🐰 I nibble on defaults, tidy and neat,

When filters change, I drum my feet,
Cached counts wake, then hop away,
Taxa align for the bright new day,
Hooray for fresh queries — quick and sweet.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main objectives: handling cached counts and adding default taxa to projects, which aligns with the substantial changes across multiple files.
Docstring Coverage ✅ Passed Docstring coverage is 86.36% which is sufficient. The required threshold is 80.00%.
Description check ✅ Passed The PR description is comprehensive, well-structured, and follows the template with all required sections filled out appropriately.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/default-filters-followup-default-taxa-and-cache

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mohamedelabbas1996 mohamedelabbas1996 self-assigned this Nov 11, 2025
@mohamedelabbas1996 mohamedelabbas1996 marked this pull request as ready for review November 18, 2025 14:57
@mohamedelabbas1996 mohamedelabbas1996 changed the title [Draft] Default taxa filter follow-up: handle cached counts and add default taxa to projects Default taxa filter follow-up: handle cached counts and add default taxa to projects Nov 18, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (5)
ami/main/signals.py (1)

8-11: Default-filter signals correctly trigger cache refresh; consider linter and perf tweaks

The new refresh_cached_counts_for_project, threshold-change, and include/exclude-taxa m2m signals are wired correctly and line up with Project.update_related_calculated_fields(); behavior looks sound.

Two small follow-ups:

  • To satisfy Ruff’s ARG001 without changing behavior, you can rename unused args to underscore variants, e.g.:
    def cache_old_threshold(_sender, instance, **_kwargs): ...
    def threshold_updated(_sender, instance, **_kwargs): ...
    def include_taxa_updated(_sender, instance: Project, action, **_kwargs): ...
    def exclude_taxa_updated(_sender, instance: Project, action, **_kwargs): ...
  • project.update_related_calculated_fields() can be expensive for large projects (iterates all events/deployments and saves). If this becomes a bottleneck, consider deferring to a background job or at least wrapping with some timing/metrics to monitor impact.

Also applies to: 133-191

ami/main/models_future/filters.py (1)

128-135: New per-component default-filter flags are well structured; docstring could mention them

The addition of apply_default_score_filter and apply_default_taxa_filter preserves existing behavior when left at their defaults and cleanly allows callers to toggle score vs taxa components independently. Early returns for project is None and apply_defaults=false keep existing call sites safe.

Consider updating the function docstring/Examples section to briefly describe these two flags and show a usage where only score or only taxa defaults are applied, so future readers don’t have to infer the new behavior from the implementation.

Also applies to: 191-213

ami/main/models.py (2)

153-162: Default taxa application to new “Scratch Project” is straightforward; consider logging unknown taxa

get_project_default_filters() and the updated get_or_create_default_project() cleanly wire the env-configured taxa into a new default project via the include/exclude M2M fields; behavior on existing projects is unchanged.

If you want to make ops misconfigurations easier to detect, a small enhancement would be to log any names in settings.DEFAULT_INCLUDE_TAXA/DEFAULT_EXCLUDE_TAXA that don’t resolve to a Taxon, e.g. by comparing the configured name lists to the Taxon.objects.filter(...).values_list("name", flat=True) results before returning.

Also applies to: 164-186


704-715: Detection-count helpers correctly respect project defaults; align bulk updater semantics and return types

The new get_detections_count methods on Deployment, Event, and SourceImage correctly centralize counting through build_occurrence_default_filters_q, so cached detections_count now reflects project default filters when a project is present, and falls back to unfiltered counts when not.

Two follow-ups to consider:

  • The type hints for Deployment.get_detections_count and Event.get_detections_count are -> int | None, but the implementations always return an int (count()), even when there is no project. Tightening these to -> int would better match actual behavior.
  • SourceImage.update_calculated_fields() now sets detections_count using the default-filtered helper, while update_detection_counts() still bulk-updates detections_count as the raw number of detections (no default filters). Because Event.timeline() uses update_detection_counts(qs, null_only=True) before aggregating detections_count, older rows (with null counts) will be populated with unfiltered counts, while newer rows updated via get_detections_count() will be filtered. If the intent is that detections_count everywhere represents the default-filtered value, it would be worth updating update_detection_counts() to apply the same filtering (or documenting that it intentionally uses raw counts and only for legacy backfill).

Also applies to: 919-922, 1085-1095, 1799-1813, 1970-1993

ami/main/api/views.py (1)

1478-1483: Taxon detail/list default-filter handling is consistent; consider documenting new flags

The revised get_taxa_observed flow looks good:

  • List view: still filters taxa by filter_by_project_default_taxa(...) and calls get_taxa_observed with default flags, so both score and taxa defaults apply (unless apply_defaults=false is passed, in which case build_occurrence_default_filters_q returns Q() and base_filter is unchanged).
  • Detail view: calls get_taxa_observed(..., apply_default_score_filter=True, apply_default_taxa_filter=False), so the project’s score threshold now applies while taxa include/exclude are not re-applied inside the occurrence subqueries. That matches the intent of aligning detail counts with list behavior for included taxa while still allowing detail on excluded taxa.

The use of build_occurrence_default_filters_q with the new flags keeps the semantics consistent with other parts of the codebase. As a small improvement, you might extend the get_taxa_observed docstring to mention the two flags and how they’re used for list vs detail views, since the signature is now doing a bit more work than the current docstring reflects.

Also applies to: 1499-1505, 1523-1529, 1537-1538

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 562f734 and 7d99ebd.

📒 Files selected for processing (5)
  • ami/main/api/views.py (3 hunks)
  • ami/main/models.py (6 hunks)
  • ami/main/models_future/filters.py (2 hunks)
  • ami/main/signals.py (2 hunks)
  • config/settings/base.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
ami/main/models_future/filters.py (1)
ami/utils/requests.py (1)
  • get_default_classification_threshold (124-145)
ami/main/api/views.py (1)
ami/main/models_future/filters.py (1)
  • build_occurrence_default_filters_q (128-214)
ami/main/models.py (1)
ami/main/models_future/filters.py (1)
  • build_occurrence_default_filters_q (128-214)
ami/main/signals.py (1)
ami/main/models.py (2)
  • Project (249-489)
  • update_related_calculated_fields (331-341)
🪛 Ruff (0.14.5)
ami/main/signals.py

142-142: Unused function argument: sender

(ARG001)


142-142: Unused function argument: kwargs

(ARG001)


163-163: Unused function argument: sender

(ARG001)


163-163: Unused function argument: kwargs

(ARG001)


179-179: Unused function argument: sender

(ARG001)


179-179: Unused function argument: kwargs

(ARG001)


187-187: Unused function argument: sender

(ARG001)


187-187: Unused function argument: kwargs

(ARG001)

🔇 Additional comments (1)
config/settings/base.py (1)

451-453: Env-configured default taxa lists look correct

Reading DEFAULT_INCLUDE_TAXA/DEFAULT_EXCLUDE_TAXA via env.list with list defaults is consistent with the existing settings usage; no issues from a settings perspective.

@mihow mihow self-assigned this Nov 24, 2025
…d-cache

Resolved SourceImage.get_detections_count conflict by combining both changes:
apply project default filters AND exclude null-bbox placeholder detections (from #1093).
Preserved get_was_processed method added in main.

Co-Authored-By: Claude <noreply@anthropic.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 17, 2026

Deploy Preview for antenna-ssec canceled.

Name Link
🔨 Latest commit 8235922
🔍 Latest deploy log https://app.netlify.com/projects/antenna-ssec/deploys/69e183e4110097000894e22c

@mihow
Copy link
Copy Markdown
Collaborator

mihow commented Apr 17, 2026

Claude says: (on @mihow's behalf)

Merged main into the branch to resolve conflicts (commit e0245fb) — pushed directly since it was a clean combination. One conflict in SourceImage.get_detections_count resolved by keeping both the PR's default-filter application and main's NULL_DETECTIONS_FILTER exclusion (from #1093). Preserved the get_was_processed method added in main.

Existing test classes pass on the merged branch (45 tests across TestFilter*, TestTaxonomy*, TestEvents, TestDuplicateFieldsOnChildren, TestProjectSetup).

Takeaway review

Risks

  1. Sync fan-out on Project.post_save. refresh_cached_counts_for_project iterates every Event and every Deployment in the project and calls update_calculated_fields(save=True) on each, inline in the HTTP request. For a project with hundreds of events this will block. Needs to be transaction.on_commit(lambda: refresh_task.delay(project.pk)).
  2. Count inconsistency regression. PR's stated goal is making counts consistent, but SourceImage.get_detections_count excludes NULL_DETECTIONS_FILTER (placeholder no-detection records from Implement null detections (calculate what has been processed or not) #1093) while the new Deployment.get_detections_count and Event.get_detections_count do not. Net: sum-of-images != event count != deployment count. Fix: exclude NULL_DETECTIONS_FILTER in all three.
  3. Deployment/Event counts drop detections without occurrences. New Deployment.get_detections_count filters via occurrence_accessor=\"occurrence\", meaning unclassified detections (no occurrence yet) are silently excluded. Previously was a raw count. Decide whether this is intentional and document; otherwise detections after first scan but before grouping vanish from the count.
  4. Silent env-var failures. Taxon.objects.filter(name__in=settings.DEFAULT_INCLUDE_TAXA) returns empty if a configured name doesn't exist in the DB. A typo produces no warning. Add a log warning when the configured-names set has entries with no DB match.
  5. No tests added. 180 lines of behavior change with zero new test coverage. CI green = "didn't break existing tests," not "new behavior works." See minimum test set below.

Minimum test set to add before merge

Three tests, ~150 lines total, each protecting a specific claim:

  1. Taxon list vs detail count parity — the PR's stated bug fix. Create project with default filters, hit /api/v2/taxa/ and /api/v2/taxa/<id>/, assert occurrences_count matches on both.
  2. Detection count consistency across Deployment/Event/SourceImage — creates the inconsistency described in risk Bump pre-commit/action from 2.0.0 to 3.0.0 #2. Currently will fail, which is what we want.
  3. Signal idempotency — save Project without changing threshold, assert refresh_cached_counts_for_project not called; save with threshold change, assert called once. Catches accidental cascades (e.g. during data migrations).

Not recommended to add to this PR

Adding post-processing refresh (auto-update counts after ML job completion) — different hook point (Job, not Project), different fan-out profile (many jobs complete → needs Celery, not sync), and this PR is already bundling three concerns. Tracked separately as a follow-up.

Scope observation

Three loosely-related concerns bundled (env-var taxa, signal refresh, view filter refactor). If this can't land quickly, the env-var piece is tiny and isolated — safe to split off and merge today.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ami/main/api/views.py (1)

1503-1515: ⚠️ Potential issue | 🟠 Major

Apply the same score filter to example_occurrences.

The retrieve annotations now ignore below-threshold occurrences, but the prefetch right below still pulls any matching occurrence. That can return a preview that was not counted, e.g. occurrences_count=0 while example_occurrences is non-empty.

🔧 Suggested change
                 Prefetch(
                     "occurrences",
-                    queryset=Occurrence.objects.filter(self.get_occurrence_filters(project))[:1],
+                    queryset=Occurrence.objects.filter(self.get_occurrence_filters(project))
+                    .filter_by_score_threshold(project, self.request)[:1],
                     to_attr="example_occurrences",
                 )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ami/main/api/views.py` around lines 1503 - 1515, The prefetch for
example_occurrences can return occurrences that were excluded by the score
filter applied in get_taxa_observed, causing mismatched counts; update the
Prefetch queryset so it uses the same occurrence filter logic as
get_taxa_observed (i.e., include the default score threshold) instead of the
broader Occurrence.objects.filter(self.get_occurrence_filters(project)) used
now. Concretely, change the Prefetch/queryset expression that builds
example_occurrences to apply the same score-aware filters (for example by
calling the same helper or by passing an argument like apply_score_filter=True
to self.get_occurrence_filters or reusing the filter used by get_taxa_observed)
so the prefetch only pulls occurrences that would be counted by
get_taxa_observed.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ami/main/models.py`:
- Around line 1935-1951: The stored detections_count writers must use the same
default-filter semantics as SourceImage.get_detections_count(); update
Project.update_related_calculated_fields(), update_detection_counts(), and any
backfill paths to compute counts through SourceImage.get_detections_count() (or
apply build_occurrence_default_filters_q with occurrence_accessor="occurrence")
instead of raw self.detections.count(), and ensure Project.default-filter
changes trigger recomputation of related cached counts so saved detections_count
never diverges from get_detections_count().
- Around line 174-185: The new scratch Project created via
Project.objects.get_or_create(...) is not running your custom creation logic so
related defaults (device, site, deployment, collection) are never created; after
detecting created is True in the block that calls get_project_default_filters()
and sets taxa, call create_related_defaults(project) immediately (before
project.save()) to fully initialize the project, and update the Project
docstring/comment if it still claims related objects are created on save to
reflect this explicit initialization step.

---

Outside diff comments:
In `@ami/main/api/views.py`:
- Around line 1503-1515: The prefetch for example_occurrences can return
occurrences that were excluded by the score filter applied in get_taxa_observed,
causing mismatched counts; update the Prefetch queryset so it uses the same
occurrence filter logic as get_taxa_observed (i.e., include the default score
threshold) instead of the broader
Occurrence.objects.filter(self.get_occurrence_filters(project)) used now.
Concretely, change the Prefetch/queryset expression that builds
example_occurrences to apply the same score-aware filters (for example by
calling the same helper or by passing an argument like apply_score_filter=True
to self.get_occurrence_filters or reusing the filter used by get_taxa_observed)
so the prefetch only pulls occurrences that would be counted by
get_taxa_observed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e16a65b8-aad9-4a94-bf7b-17c3643ffe0d

📥 Commits

Reviewing files that changed from the base of the PR and between 7d99ebd and e0245fb.

📒 Files selected for processing (3)
  • ami/main/api/views.py
  • ami/main/models.py
  • config/settings/base.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • config/settings/base.py

Comment thread ami/main/models.py
Comment thread ami/main/models.py
Follow-up fixes on top of the cached-counts work:

1. Detection-count hierarchy consistency. Deployment.get_detections_count
   and Event.get_detections_count now exclude NULL_DETECTIONS_FILTER
   (null-bbox placeholder rows) to match SourceImage.get_detections_count.
   Without this, sum(image counts) != event count != deployment count when
   an image had been processed with no detections found.

2. Move the default-filter refresh signal off the request thread. The
   refresh fans out update_calculated_fields across every Event and
   Deployment in a project, which could stall a Project save with hundreds
   of events. Wrap the call in transaction.on_commit and dispatch a new
   Celery task (ami.main.tasks.refresh_project_cached_counts) so the save
   returns immediately and the work only runs if the transaction commits.

3. Three targeted tests in TestCachedCountsDefaultFilters:
   - taxa list/detail parity on occurrences_count under a score threshold
   - Deployment/Event/SourceImage detection-count hierarchy consistency,
     including an explicit null-bbox placeholder that must not be counted
   - signal idempotency: Project.save without a threshold change enqueues
     no refresh task; a threshold change enqueues exactly one.

Co-Authored-By: Claude <noreply@anthropic.com>
@mihow
Copy link
Copy Markdown
Collaborator

mihow commented Apr 17, 2026

Claude says: (on @mihow's behalf)

Pushed b1ecba67 addressing the three top takeaways from the previous review:

  1. NULL_DETECTIONS_FILTER consistencyDeployment.get_detections_count and Event.get_detections_count now both call .exclude(NULL_DETECTIONS_FILTER), matching SourceImage.get_detections_count. Prevents the hierarchy drift where sum(image counts) ≠ event count ≠ deployment count once any image had a null-bbox placeholder row.
  2. Signal fan-out moved off the request thread — the default-filter refresh_cached_counts_for_project helper now wraps a new refresh_project_cached_counts Celery task in transaction.on_commit(...). Project saves return immediately instead of blocking on an N-event × M-deployment recompute, and the task only fires if the transaction actually commits.
  3. Three targeted tests in TestCachedCountsDefaultFilters (ami/main/tests.py):
    • taxa list/detail occurrences_count parity under a score threshold
    • SourceImage/Event/Deployment detection-count hierarchy consistency, with an explicit null-bbox row asserted to be excluded everywhere
    • signal idempotency: unrelated Project save enqueues 0 tasks; threshold change enqueues exactly 1 (via captureOnCommitCallbacks)

28 tests pass locally (TestTaxonomy, TestTaxonomyViews, TestEvents, TestDuplicateFieldsOnChildren, TestProjectSetup, TestCachedCountsDefaultFilters). PR is mergeable.

Auto-refresh on ML job completion stays out of this PR — tracked as a follow-up stub at docs/claude/prompts/refresh-counts-after-ml-job.md for a dedicated PR.

…e cache

- get_or_create_default_project now calls ProjectManager.create_related_defaults
  after a get_or_create, since get_or_create bypasses the manager's create()
  override. New scratch projects get the default device, site, deployment,
  collection, and processing service instead of only taxa defaults.
- update_detection_counts accepts an optional project kwarg and applies the
  project's default-filter Q so the bulk subquery matches
  SourceImage.get_detections_count().
- Project.update_related_calculated_fields now refreshes SourceImage cached
  detection counts in addition to Event and Deployment, keeping the hierarchy
  consistent after a default-filters change.
- New regression test verifies SourceImage.detections_count tracks
  get_detections_count() across threshold changes.

Co-Authored-By: Claude <noreply@anthropic.com>
@mihow mihow merged commit 938841e into main Apr 17, 2026
7 checks passed
@mihow mihow deleted the fix/default-filters-followup-default-taxa-and-cache branch April 17, 2026 05:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants