Skip to content

API for Occurrence Sets - #1544

Draft
mihow wants to merge 9 commits into
feat/occurrence-set-modelfrom
feat/occurrence-set-api
Draft

mihow wants to merge 9 commits into
feat/occurrence-set-modelfrom
feat/occurrence-set-api

Conversation

@mihow

@mihow mihow commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

#1543 adds occurrence sets: named groups of occurrences that one project keeps and uses again. This PR lets people and the UI work with them over the REST API. A user can create a set, add occurrences to it or remove them, freeze it so that results that refer to it stay true, and copy a frozen set to make a new version. The occurrence list can show only the members of a set, or leave them out.

The API follows the model in #1543 and the design on the saved-sets epic (#1548, design notes). Each set belongs to one project, and that project's roles decide who can change it. Any project member can read a project's sets. ML data managers and project managers create, edit, freeze and copy them, as they manage capture sets.

List of Changes

Change (effect) How Notes
People can list, read, create, rename, describe and delete sets OccurrenceSetViewSet in ami/main/api/occurrence_set.py, at /api/v2/occurrences/sets/ The list needs project_id
Pickers can list a project's sets in one request GET .../sets/choices/?project_id= returns names, methods and dates, up to 100, most recently updated first Same pattern as capture set choices
People can add or remove occurrences in bulk POST .../sets/{id}/add/ and .../remove/ with {"occurrence_ids": [...]}, 1 to 1,000 ids. The ids are checked with one query; any id outside the set's project refuses the whole request 400 on a frozen set
People can freeze a set POST .../sets/{id}/freeze/ records frozen_at and frozen_by. A frozen set refuses add, remove and a second freeze. It can still be renamed No unfreeze
People can copy a set to make a new version POST .../sets/{id}/copy/ with an optional name. It returns the new set, not frozen, with the same members and copied_from set Needs the create permission
The UI can show how old a set is and which actions apply Each set returns method, kwargs, occurrences_count, populated_at, frozen_at, frozen_by (name and image only), copied_from, and user_permissions (update, delete, freeze, copy)
The occurrence list can include or exclude a set occurrence_set_id and not_occurrence_set_id, as OccurrenceSetFilter in OCCURRENCE_FILTER_BACKENDS, documented in the OpenAPI schema The stats endpoint and exports can run the same backend
Permissions come from the set's own project ObjectPermission with the model's checks. add, remove and freeze need the update permission; copy needs create Replaces the earlier OccurrenceSetPermission and its shared-set rules
The OpenAPI schema stays readable ENUM_NAME_OVERRIDES names the capture set and occurrence set method enums

Endpoints

Method and path What it does
GET /occurrences/sets/?project_id= List a project's sets, with member counts
GET /occurrences/sets/choices/?project_id= Names and dates for pickers
POST /occurrences/sets/ Create a set: {"name", "description", "project"}. method is manual and kwargs must be empty
GET, PATCH, DELETE /occurrences/sets/{id}/ Read, rename or describe, delete. The project cannot change
POST /occurrences/sets/{id}/add/, .../remove/ Change the members: {"occurrence_ids": [...]}
POST /occurrences/sets/{id}/freeze/ Freeze the set
POST /occurrences/sets/{id}/copy/ Copy the set: optional {"name"}
GET /occurrences/?project_id=&occurrence_set_id= Only the members of a set
GET /occurrences/?project_id=&not_occurrence_set_id= Everything except the members of a set

Detailed Description

Errors.

  • Invalid input gives a 400: bad or foreign ids, an empty list, more than 1,000 ids, kwargs on a manual set, a change of project, an occurrence_set_id that is not a positive number.
  • Refused lifecycle actions also give a 400 with the reason, for example "A frozen set cannot be edited. Copy it to make a set that can change."
  • The model checks the frozen state again under a row lock, so a freeze cannot land in the middle of an edit.

Query counts are measured in tests with strict counts:

  • List: 10 queries for 3 sets and for 6 sets. Permissions are read once per project, not once per set.
  • choices: 10 queries.
  • add with 7 ids: 12 queries.

Tests. ami/main/models_future/tests/test_occurrence_set_api.py covers:

  • the endpoints and the errors;
  • the include and exclude filters;
  • a permission matrix (superuser, ML data manager, project manager, basic member, non-member, anonymous) for every action;
  • draft-project visibility.

The model tests from #1543 stay in test_occurrence_set.py. The earlier test file for the many-to-many API is removed.

What comes next

This is the order of work from #1548:

Note for the stacked branches (@mohamedelabbas1996)

  • Filter names: occurrence_set_id and not_occurrence_set_id on the occurrence list. They are also available to exports, through OccurrenceSetFilter in ami.main.api.views.
  • Creating a set takes project, not project_id, and no occurrences. Create the set, then POST .../add/ with the ids. Scoring and training should use a frozen set, or freeze a copy, and record the set's id and frozen_at.
  • One project per set: for_project() and the shared-set rules are gone. Scope with project=....
  • Endpoints: add, remove, freeze and copy are as listed above.

Open questions

  1. Should roles beyond the curators (for example identifiers, for flagging) edit hand-made sets?
  2. Should freezing have its own permission?
  3. Curators can delete a frozen set. A score that refers to it would then name a deleted set.
  4. Scoring and training jobs could refer to a set by a foreign key on Job or by a model reference in the job settings.
  5. When the occurrence list is filtered by a set, the project's default filters still apply. A hand-picked occurrence below the score threshold is hidden unless the caller passes apply_defaults=false, so the UI may need to show "N of M shown".
  6. Should create also accept a first list of occurrence_ids, so the UI can save a selection in one request?

How to Test the Changes

docker compose run --rm django python manage.py test ami.main.models_future.tests.test_occurrence_set ami.main.models_future.tests.test_occurrence_set_api ami.users ami.exports
docker compose run --rm django python manage.py makemigrations --check --dry-run

Deployment Notes

There are no new migrations in this PR; it uses the ones in #1543.

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. (Stacks on Introduce Occurrence Sets (manually curated only) #1543.)

🤖 Generated with Claude Code

https://claude.ai/code/session_01MeUoKCfkiZaZjhja7e6AGh

mihow and others added 4 commits October 20, 2026 14:34
…h the API

Adds /api/v2/occurrences/sets/ so curators can save a list of occurrences
as a named set, list a project's sets with their size, and pick one from a
choices endpoint that mirrors the capture set picker.

The project and the occurrences are set once, on create: both are refused
on PATCH and there is no PUT. Occurrence ids are checked against the
project in one query and the error names up to ten that are missing or
outside it. The project for the create permission is read from the body
only, so the project checked is the one written to. Public sets are
offered to every project but cannot be changed here. Row permissions are
resolved against the active project, as for taxa lists, without a query
per row.

Split from #1492. Part of #763.

Co-Authored-By: mohamedelabbas1996 <hack1996man@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeUoKCfkiZaZjhja7e6AGh
Adds an occurrence_set_id query parameter to the occurrence list and the
occurrence stats endpoints, named like collection_id and taxa_list_id. It
is a filter backend rather than a FilterSet field so the export framework
can run the same filter later. The value is parsed with
SingleParamSerializer, so a non-integer returns 400.

The filter only narrows: the active project and the project's default
filters still apply, so a set never shows an occurrence the list would
otherwise hide.

Split from #1492. Part of #763.

Co-Authored-By: mohamedelabbas1996 <hack1996man@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeUoKCfkiZaZjhja7e6AGh
… set under the request's project

A public set, one with no project, was listed with a details link that returned 403 for
every caller, because the generic object check refuses anything without a project. The
detail queryset already limits a caller to sets of projects they can see plus public sets,
so retrieve now needs no further check. Editing a public set is still refused.

A set shared by two projects (only possible through the Django admin) reported edit
permissions on its row from the project in the request, while the edit itself was checked
against the set's first project, so the offered buttons could fail and a curator of the
first project could edit a set another project also used. An edit is now checked against
the request's project when the set belongs to it, and against the set's first project
otherwise. Tests cover public reads for every kind of caller and both directions of the
shared-set case.

Co-Authored-By: mohamedelabbas1996 <hack1996man@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeUoKCfkiZaZjhja7e6AGh
…n to

The occurrence list's "Create set" button reads the collection-level
user_permissions from the set choices response, so a refactor of the
paginator or the role grants could hide the button from curators or offer
it to basic members without any test failing. This test fixes the answer
for the owner, an ML data manager, a superuser, a basic member, a
non-member and an anonymous visitor.

Co-Authored-By: mohamedelabbas1996 <hack1996man@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeUoKCfkiZaZjhja7e6AGh
@mihow mihow added backend occurrence-sets Saved lists of occurrences: create, filter, export, and score or train against them labels Oct 20, 2026
@netlify

netlify Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for antenna-preview canceled.

Name Link
🔨 Latest commit fff8db3
🔍 Latest deploy log https://app.netlify.com/projects/antenna-preview/deploys/6ad9669a63ab3f63dcdc8d79

@coderabbitai

coderabbitai Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@mihow mihow changed the title Create, list and choose occurrence sets through the API, and filter the occurrence list by one API for Occurrence Sets Oct 21, 2026
mihow and others added 2 commits October 21, 2026 15:18
…I behaviour

The API sets a set's project and occurrences on create only. The
docstrings, error text and test names now say that this is how the API
creates sets today, with editing coming from curated sets (#1538) and
saved-filter sets (#1539), instead of describing every set as fixed.

Co-Authored-By: mohamedelabbas1996 <hack1996man@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeUoKCfkiZaZjhja7e6AGh
mihow and others added 3 commits October 21, 2026 18:11
Brings in the one-project model with the saved-set lifecycle (#1543).

# Conflicts:
#	ami/main/models_future/occurrence_sets.py
The API follows the model from #1543: each set has one project, so the
OccurrenceSetPermission class and the many-to-many and public-set handling
are gone, and permissions come from the set's own project.

/api/v2/occurrences/sets/ (now in ami/main/api/occurrence_set.py) has
list, retrieve, create, update, delete, choices, and the lifecycle
actions: add and remove (a list of ids, checked with one query, refused
on a frozen set), freeze and copy. The occurrence list takes
occurrence_set_id and not_occurrence_set_id, documented in the OpenAPI
schema. Names the two "method" enums in the schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeUoKCfkiZaZjhja7e6AGh
…trix

Replaces the tests for the many-to-many API. Model tests stay in
test_occurrence_set.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MeUoKCfkiZaZjhja7e6AGh
@mihow

mihow commented Oct 29, 2026

Copy link
Copy Markdown
Collaborator Author

Claude says: A note on permissions, for when this PR is next picked up. There are now three ways to avoid the per-row permission cost:

  1. NoObjectPermissionsMixin (Filter captures and sessions by device type, site and date #1561, ami/base/serializers.py) returns an empty user_permissions list. It is for read-only nested rows and choice lists that never need permissions.
  2. OccurrenceSetPermissionsMixin in this PR caches permissions on the serializer for each request.
  3. Stop every list endpoint from looking up permissions once per row #1430 resolves each project's permissions once per request for all list endpoints.

#1430 and the cache in this PR solve the same problem. It may be simpler to adopt the #1430 approach here once it lands, instead of keeping a separate cache. The occurrence set choices actions in #1492 and #1501 can use the mixin for their choice serializers, as the job, device and site choices do.

@netlify

netlify Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for antenna-ssec ready!

Name Link
🔨 Latest commit fff8db3
🔍 Latest deploy log https://app.netlify.com/projects/antenna-ssec/deploys/6ae44f5fed87758d42c81af0
😎 Deploy Preview https://deploy-preview-1544--antenna-ssec.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend occurrence-sets Saved lists of occurrences: create, filter, export, and score or train against them

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant