Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
227 commits
Select commit Hold shift + click to select a range
29707e8
feat(tracking): revive detection tracking as a post-processing task
mihow Apr 28, 2026
45c2f96
fix(tracking): address PR #1272 review feedback
mihow Apr 28, 2026
1e98ea2
feat(tracking): v1 hardening — fresh-event guard, merge-into-first, a…
mihow Apr 29, 2026
6393a71
fix(occurrence): refresh determination_score when taxon unchanged
mihow Apr 29, 2026
abcc433
feat(tracking): accept event_ids in tracking task config
mihow Apr 30, 2026
8951d98
feat(tracking): admin form + template for tracking action
mihow Apr 30, 2026
10e2f0f
feat(tracking): EventAdmin action with intermediate confirmation page
mihow Apr 30, 2026
a34abf3
refactor(tracking): collection action uses same form + drop collectio…
mihow Apr 30, 2026
ded1189
test(tracking): scope-resolution + admin action coverage
mihow Apr 30, 2026
462736f
Merge origin/main into revive-tracking; adapt tracking to the post-pr…
mihow Sep 2, 2026
69312e5
Let tracking run on sessions that have a few detections without an oc…
mihow Sep 2, 2026
0357fd7
feat(occurrences): let curators split a track or pull a detection out…
mihow Sep 2, 2026
7874dfa
docs: warn that the occurrence detail endpoint serves detections newe…
mihow Sep 2, 2026
d7122e0
docs: write down how occurrence tracking works and what it traps on
mihow Sep 2, 2026
b5a3fd6
feat(occurrences): confirm a grouping is right, and merge occurrences…
mihow Sep 2, 2026
49da693
docs: record how a grouping is corrected and confirmed [skip ci]
mihow Sep 2, 2026
ca9f8f7
docs: index the idempotent tracking design [skip ci]
mihow Sep 2, 2026
d149610
docs: record that the capture sequence skips the only usable pairs [s…
mihow Sep 2, 2026
077c53d
feat(fixtures): give the demo project a session that tracking can group
mihow Sep 2, 2026
ab0fe25
feat(occurrences): tell the interface who confirmed a grouping and wh…
mihow Sep 2, 2026
320ca0f
feat(ui): let curators correct and confirm an occurrence's grouping
mihow Sep 2, 2026
d582ea2
feat(ui): draw the rest of an occurrence's path over the capture you …
mihow Sep 2, 2026
f731443
docs: record how the grouping front end works and what it traps on [s…
mihow Sep 2, 2026
9e00131
feat(occurrences): serve an occurrence's path, and say how many frame…
mihow Sep 2, 2026
ad70f92
fix(ui): keep an occurrence a track edit created reachable, so the ed…
mihow Sep 2, 2026
7f620e7
fix(tracking): keep identifications when chains are merged
mihow Sep 2, 2026
56f7572
docs: correct the claim that a fresh event carries no identifications…
mihow Sep 2, 2026
71feeb1
feat(occurrences): say on a capture whether a grouping was confirmed
mihow Sep 2, 2026
38ac0f7
fix(ui): refresh a capture's boxes after a track edit
mihow Sep 2, 2026
eb0bb8f
feat(ui): correct an occurrence's grouping from the session view, on …
mihow Sep 2, 2026
0118101
docs: add the idempotent incremental tracking design [skip ci]
mihow Sep 2, 2026
df6978d
fix(ui): keep a detection's toolbar inside the capture it belongs to
mihow Sep 2, 2026
7eac995
chore: merge occurrence-tracking work onto main for the INRAE dev-box…
mihow Sep 8, 2026
2499164
feat(ui): show frame counts and optional track statistics on the occu…
mihow Sep 9, 2026
a080c53
fix(ui): make "Show path" work from a hovered box and show its loadin…
mihow Sep 9, 2026
05a0429
feat(ui): show grouping events in the identification history and tidy…
mihow Sep 9, 2026
7aa5bd0
feat(api): describe how far, how much and how consistently a tracked …
mihow Sep 9, 2026
83e714f
feat(tracking): show in the identification history when tracking chan…
mihow Sep 9, 2026
833a358
fix(tracking): ignore the "nothing found" sentinel detections when pa…
mihow Sep 9, 2026
a945df8
feat(occurrences): store track statistics on the occurrence so the li…
mihow Sep 9, 2026
3d52e3b
Merge branch 'feat/stored-track-stats' into integration/tracking-inrae
mihow Sep 9, 2026
5e7ad3a
feat(tracking): rank the occurrences offered for a merge the way trac…
mihow Sep 9, 2026
9f1e33e
Merge branch 'feat/merge-candidates-ranked' into integration/tracking…
mihow Sep 9, 2026
2560b4f
feat(api): say whether a classification stored a feature vector witho…
mihow Sep 9, 2026
41815f1
Merge remote-tracking branch 'origin/main' into claude/revive-trackin…
mihow Sep 11, 2026
2565b54
docs(tracking): work plan from the first partner review of occurrence…
mihow Sep 13, 2026
e8692b5
docs(tracking): add the screenshot findings to the review work plan […
mihow Sep 13, 2026
349ea8f
docs(tracking): record the measured cause of the sparse merge candida…
mihow Sep 13, 2026
1296125
docs(tracking): link the review work plan to its tickets [skip ci]
mihow Sep 13, 2026
4ac58be
feat(tracking): search the adjacent captures first for merge candidat…
mihow Sep 14, 2026
8d8e1ac
feat(ui): merge several candidates at once, choose the search scope a…
mihow Sep 14, 2026
3ff6a26
feat(ui): build a track by clicking detections across captures in the…
mihow Sep 14, 2026
14726f6
feat(tracking): let track edits and the occurrence page reach undeter…
mihow Sep 14, 2026
1e88f9e
feat(tracking): flag merge candidates that share a capture with the t…
mihow Sep 15, 2026
df11641
feat(ui): gray out merge candidates that share a capture with the track
mihow Sep 15, 2026
b9c4b7d
feat(ui): move the extend-track bar above the capture and give it tra…
mihow Sep 15, 2026
950f865
fix(ui): open, list and identify occurrences that have no determinati…
mihow Sep 15, 2026
1ae8f7c
docs(tracking): record the merge-picker decisions from hands-on testi…
mihow Sep 15, 2026
49376bf
feat(tracking): offer candidates that fill a gap in the track and dro…
mihow Sep 15, 2026
6c9c703
feat(ui): list gap-filling candidates, and correct, hide and close in…
mihow Sep 15, 2026
1933202
feat(ui): show each frame's own machine label and the names given acr…
mihow Sep 15, 2026
16bf14d
fix(tracking): score gap candidates against the nearest track frame
mihow Sep 15, 2026
a4ff214
fix(ui): label the session-view occurrence dialog and keep box popove…
mihow Sep 15, 2026
fd9e8b4
feat(ui): say how far each merge candidate is in time, and merge seve…
mihow Sep 15, 2026
5c18b4c
fix(tracking): refuse a merge or add that puts two boxes from one cap…
mihow Sep 15, 2026
260dac6
fix(ui): show the server's reason when a track edit is refused
mihow Sep 15, 2026
321b003
fix(ui): show the whole message when the API sends a field error as a…
mihow Sep 15, 2026
eb06d8a
Merge the review-round tracking tools into the tracking PR
mihow Sep 15, 2026
b64d9d4
test(tracking): merge a frame from a later capture in the track-stats…
mihow Sep 15, 2026
bed3f20
Merge the track-stats test fix into the tracking PR
mihow Sep 15, 2026
d3dcba3
feat(ui): put each frame's details under its crop and never cut off t…
mihow Sep 15, 2026
05168be
feat(ui): show where selected occurrences appear on the session timeline
mihow Sep 15, 2026
50e0e68
feat(ui): put the frame menu beside "View in session" instead of abov…
mihow Sep 15, 2026
ba6bca3
Merge frame captions, timeline markers and the frame menu move into t…
mihow Sep 15, 2026
fe89ebf
feat(ui): use a pencil for each frame's edit menu
mihow Sep 15, 2026
36b6616
feat(ui): draw each occurrence's runs on the timeline as solid green …
mihow Sep 15, 2026
d6fa237
fix(ui): keep "View in session" on frames when the details open in a …
mihow Sep 15, 2026
ea5baf3
feat(ui): let the path banner jump back to the occurrence and hide th…
mihow Sep 15, 2026
124dee9
Merge the pencil frame menu, green timeline blocks, session-page fram…
mihow Sep 15, 2026
794b702
feat(ui): use Nova's Emerald 500 for timeline blocks and describe eac…
mihow Sep 15, 2026
3469555
Merge the Emerald timeline colour and run tooltips into the tracking PR
mihow Sep 15, 2026
679a19b
fix(api): return the path of occurrences the default filters hide
mihow Sep 15, 2026
4dd8a4e
fix(ui): let the track buttons reach the only frame of a one-frame track
mihow Sep 15, 2026
8c7c0f4
fix(ui): stop Escape in a popover or menu from also closing the dialo…
mihow Sep 15, 2026
2256cef
fix(ui): show a spinner while the extend choice dialog loads its frames
mihow Sep 15, 2026
e7189d2
fix(ui): keep the other selected occurrences when a frame opens in th…
mihow Sep 15, 2026
f8ce4d5
fix(ui): give the occurrence dialog on the occurrences list a title
mihow Sep 15, 2026
a68f4f9
fix: explain a two-boxes-on-one-capture refusal in plain language
mihow Sep 15, 2026
2c66578
fix: step to the very next capture with detections, not the next minute
mihow Sep 15, 2026
c22a649
fix(ui): keep the merge comparison preview off the row checkboxes
mihow Sep 15, 2026
bac4ec4
fix(ui): tidy the frame tile for a frame with no classification
mihow Sep 15, 2026
40819c9
Merge the walkthrough fixes into the tracking PR
mihow Sep 15, 2026
3c0767f
fix(ui): give the occurrence dialog a title after its move to its own…
mihow Sep 15, 2026
9f12cf9
feat(ui): show how likely each box is the same animal while extending…
mihow Sep 15, 2026
9a95b40
fix(ui): keep unlikely match boxes visible on light captures
mihow Sep 15, 2026
270fe33
fix(ui): show the time to the track's frame for captures the track al…
mihow Sep 15, 2026
ec843f6
feat: score every box on a capture against a track for extend mode
mihow Sep 15, 2026
392994d
feat(ui): preview what automatic tracking would do while extending a …
mihow Sep 15, 2026
9c0bb2a
fix(ui): keep the original match colour scale and mark previews acros…
mihow Sep 15, 2026
690a33e
fix(ui): shorten the note on previews across a gap in the track
mihow Sep 15, 2026
3ae83a9
fix(ui): say how many captures away the compared track frame is
mihow Sep 15, 2026
73716fe
fix(ui): keep match colours on boxes tracking would skip
mihow Sep 15, 2026
305871a
fix(ui): keep one selected occurrence in extend mode and make likely …
mihow Sep 15, 2026
0e1a0ae
feat(api): preview the tracker's own decision for each box in extend …
mihow Sep 15, 2026
5e7ad2d
Merge the extend-mode match suggestions into the tracking PR
mihow Sep 15, 2026
8023f75
feat(ui): lay out an occurrence's frames as a film strip with full-wi…
mihow Sep 16, 2026
1b59fc1
Merge the film-strip frame layout into the tracking PR
mihow Sep 16, 2026
d7930c5
fix(ui): show an empty bounding box for a frame whose crop is missing
mihow Sep 16, 2026
cef2d60
Merge the missing-crop placeholder into the tracking PR
mihow Sep 16, 2026
ad6e0d8
feat(ml): ask processing services for classification feature vectors …
mihow Sep 16, 2026
30a4076
feat(ui): continue past a merged track when extending in the session …
mihow Sep 16, 2026
c1021b1
Merge feature vectors by default and extend-mode advance into the tra…
mihow Sep 16, 2026
ef91210
feat(api): report every term of the tracking cost and the tracker's d…
mihow Sep 16, 2026
a074036
fix(ui): hide the timeline on sessions shorter than a minute
mihow Sep 16, 2026
cb57839
feat(ui): jump from an occurrence to its first or last frame in the s…
mihow Sep 16, 2026
b021ee5
feat(ui): show distance and similarity in the merge preview and order…
mihow Sep 16, 2026
08e9ff9
feat(ui): let people copy a link to a session from its details page
mihow Sep 16, 2026
6466a0f
feat(ui): let people copy a link to an occurrence from its Fields tab
mihow Sep 16, 2026
a524e01
feat(api): let reviewers list only the tracks they have confirmed
mihow Sep 16, 2026
fb2b04e
feat(ui): show the tracking cost, its terms and the tracker's verdict…
mihow Sep 16, 2026
e14c490
feat(ui): filter the occurrence list by whether a grouping is confirmed
mihow Sep 16, 2026
59d6ed6
Merge the merge-dialog cost details and occurrence frame links into t…
mihow Sep 16, 2026
b5e53c6
Merge session and occurrence id rows with copy-link buttons, and the …
mihow Sep 16, 2026
e152d43
Merge the confirmed-grouping filter for the occurrences list
mihow Sep 16, 2026
f476b44
fix(ui): show the session id once, in the "Session #" row with the co…
mihow Sep 16, 2026
fc35c36
Merge the confirmed-grouping filter, copy-link id rows and short-sess…
mihow Sep 16, 2026
e6375a6
feat(ui): mark the frames a track has no feature vector for
mihow Sep 16, 2026
c001e01
feat(ui): say how many of a track's frames have a feature vector
mihow Sep 16, 2026
61ef96d
feat(ui): show the moth in a track's other frames and jump straight t…
mihow Sep 16, 2026
4027e68
Merge the option to show crop pixels in the path boxes and jump to th…
mihow Sep 16, 2026
ad6e718
perf(ui): step between captures without waiting for the next one to load
mihow Sep 16, 2026
fdec850
feat(api): give the capture's feature-vector count the total it is ou…
mihow Sep 16, 2026
c0f2ffa
feat(ui): flag a capture whose boxes are missing feature vectors
mihow Sep 16, 2026
4003184
fix(ui): let clicks reach a path frame box that no live box covers
mihow Sep 16, 2026
defb00f
Merge preloading of the neighbouring captures in the session view
mihow Sep 16, 2026
f17eb6a
Merge the feature-vector indicators on frames, the tracking card and …
mihow Sep 16, 2026
a8da6b4
Merge path crops, capture preloading and feature-vector indicators in…
mihow Sep 16, 2026
e4a1702
refactor(ml): make the pipeline request config a typed schema with fe…
mihow Sep 17, 2026
b8f4d32
Merge the typed pipeline request config into the tracking PR
mihow Sep 17, 2026
25860ed
fix(ui): let the occurrence menu on a capture take clicks again, and …
mihow Sep 20, 2026
51eb8d0
Merge the occurrence-menu click fix into the tracking PR
mihow Sep 20, 2026
d5ad9c6
feat(ui): show the tracking cost as its own column in the merge picker
mihow Sep 20, 2026
fb32e23
Merge the cost column for the merge picker into the tracking PR
mihow Sep 20, 2026
280cd0e
feat(ui): draw a track's path with the moth's own pixels by default, …
mihow Sep 20, 2026
b8c9b3d
Merge the path crops default and the dismissable occurrence panel int…
mihow Sep 20, 2026
da24974
fix(ui): keep merge selections when the search scope changes
mihow Sep 20, 2026
aeb1f95
Merge the merge-selection fix into the tracking PR
mihow Sep 20, 2026
f5b3352
fix(ui): rebuild the occurrence panel on a popover and make the boxes…
mihow Sep 20, 2026
6375d2a
feat(ui): let the session timeline be used without a mouse
mihow Sep 20, 2026
1c6ab07
Merge the review fixes into the tracking PR
mihow Sep 20, 2026
f7472c1
fix(ui): take a track's path off the selection colour and name the fr…
mihow Sep 20, 2026
b39a706
Merge the path box colours and panel frame details into the tracking PR
mihow Sep 20, 2026
24f1e81
feat(ui): fade a track's path frames by distance and keep the live bo…
mihow Sep 20, 2026
e1f0759
Merge the path-frame onion skinning into the tracking branch
mihow Sep 20, 2026
18bc22d
fix(ui): fall back to a path frame's outline when its crop is missing…
mihow Sep 20, 2026
b1f17bc
Merge the path-frame crop fallback and direction fix into the trackin…
mihow Sep 20, 2026
6971692
feat(ui): link straight to a detection, re-lay out the occurrence pan…
mihow Sep 20, 2026
7bd9e88
Merge the detection permalink, panel re-layout and frame gap labels i…
mihow Sep 20, 2026
a76fdab
docs(ml): write down the proposed home for model outputs that are not…
mihow Sep 22, 2026
57bede3
Merge the algorithm-outputs design document into the tracking branch
mihow Sep 22, 2026
f25dde4
docs(ml): plan on upgrading pgvector rather than designing around the…
mihow Sep 22, 2026
7e5a0d0
Merge the pgvector upgrade decision into the tracking branch
mihow Sep 22, 2026
16d138c
docs(ml): add worked schema, model, ORM and API examples for algorith…
mihow Sep 22, 2026
74bf967
Merge the worked algorithm-output examples into the tracking branch
mihow Sep 22, 2026
36defa3
docs(ml): point the spine section at the refined model, so nobody bui…
mihow Sep 22, 2026
7cf4420
Merge the spine-section correction into the tracking branch
mihow Sep 22, 2026
d2de894
Merge main into the tracking branch
mihow Sep 22, 2026
5116068
Move the tracking interface to its own pull request
mihow Sep 22, 2026
98abd6e
feat: score tracking against confirmed tracks, with or without Django
mihow Sep 22, 2026
b0a4434
feat: add evaluate_tracking command that scores a re-link without wri…
mihow Sep 22, 2026
8d474d6
feat(exports): export tracks as one CSV row per detection
mihow Sep 22, 2026
d4d338b
test(exports): cover the export_tracks event and verified-only filters
mihow Sep 22, 2026
21c112e
docs: describe how tracking is scored against confirmed tracks [skip ci]
mihow Sep 22, 2026
04925f4
feat: put track editing behind a per-project tracking flag
mihow Sep 22, 2026
ca60cc8
feat: let project members start a tracking run through the jobs API
mihow Sep 22, 2026
7a0036e
feat: warn staff when running tracking on a project that has not opte…
mihow Sep 22, 2026
b5b7ce5
fix: refuse blank or unreadable capture times when scoring tracks
mihow Sep 22, 2026
8b10a7e
fix: keep tracking safety guards out of reach of the jobs API
mihow Sep 22, 2026
397e488
feat: let project managers start the tracking runs they create
mihow Sep 22, 2026
125cfd5
Merge the per-project tracking opt-in flag and job API support
mihow Sep 22, 2026
2c4f78a
Merge the track export format and export_tracks command
mihow Sep 22, 2026
10eab68
Merge the tracking evaluation method and command
mihow Sep 22, 2026
1d37d7a
Keep the tracks export label with the interface pull request
mihow Sep 22, 2026
597b120
feat(tracking): split tracks at session boundaries when regrouping
mihow Sep 22, 2026
38ed6a7
docs: describe how regrouping splits tracks at session boundaries [sk…
mihow Sep 22, 2026
80443f0
fix(tracking): keep the link into another session when a track piece …
mihow Sep 22, 2026
afb8b3b
Merge splitting tracks at session boundaries when regrouping
mihow Sep 22, 2026
89d9b08
style: sort the permissions import the way the pinned isort does
mihow Sep 22, 2026
2217e2f
fix: opt the demo project's tracking session into track editing
mihow Sep 23, 2026
58ecbe1
chore: start pgvector on current releases
mihow Sep 23, 2026
4047999
feat: let ML data managers start tracking runs, like the ML jobs they…
mihow Sep 23, 2026
dc818d1
feat: tell the interface whether the current user may start a trackin…
mihow Sep 23, 2026
b253740
feat: rank the occurrences a single detection could be moved to
mihow Sep 23, 2026
055cec4
fix: keep session and station counts current after tracking and track…
mihow Sep 23, 2026
76b3055
fix: refuse a tracks export for projects that have tracking turned off
mihow Sep 23, 2026
68cb1b6
fix: require the run permission to create a post-processing job
mihow Sep 23, 2026
5f4d388
fix: refuse anonymous job writes before validating the request body
mihow Sep 23, 2026
0326eb3
perf: refresh station counts in the background after a track edit
mihow Sep 23, 2026
e305a9d
fix: relink a track frame to frame after every manual edit
mihow Sep 23, 2026
60d71a2
fix: keep a cross-session track link with the track when a frame is p…
mihow Sep 23, 2026
c55dfd4
fix: keep the tracker's choice of box when a capture has two in one t…
mihow Sep 23, 2026
5b238d6
fix: use one frame order for track edits, chain links and the tracks …
mihow Sep 23, 2026
379915c
fix(tracking): clear the grouping confirmation when tracking changes …
mihow Sep 23, 2026
9b816d9
fix(tracking): say in the job when a run tracked nothing because ever…
mihow Sep 23, 2026
81e1f13
fix(tracking): name predicted tracks by their first detection in per-…
mihow Sep 23, 2026
81af84d
fix(tracking): write the job's Result line on every run so a retry re…
mihow Sep 23, 2026
6eb1955
refactor(tracking): share one helper for dropping a changed track's c…
mihow Sep 23, 2026
ca1e7ed
fix(jobs): refuse a job the user may not run before saving it
mihow Sep 23, 2026
27344f2
docs: require a maximum capture gap in the incremental tracking desig…
mihow Sep 29, 2026
97150ea
feat(occurrences): page through an occurrence's detections instead of…
mihow Sep 29, 2026
298e49b
fix(jobs): let a superuser create a tracking run with staff-only sett…
mihow Sep 29, 2026
b1f775c
Merge main into claude/revive-tracking-feature-OyMO3
mihow Sep 30, 2026
9b5d758
test(jobs): use staff-only settings that exist on this branch in the …
mihow Sep 30, 2026
7181bc2
style: wrap the superuser test's request body to the line limit
mihow Sep 30, 2026
fb3b4e6
fix(tracking): leave confirmed tracks unchanged during a tracking run
mihow Sep 30, 2026
91b24da
feat(tracking): headline tracking evaluations with IDF1 and count ide…
mihow Sep 30, 2026
a34bf5d
feat(tracking): show how the confirmed tracks differ from the rest of…
mihow Sep 30, 2026
b5140a3
fix: refuse track edits on occurrences without a session or from anot…
mihow Sep 30, 2026
59cd7ad
fix: export confirmed tracks that have no determination
mihow Sep 30, 2026
9b6efa9
test: pin that split and detach leave no link across the cut
mihow Sep 30, 2026
6aea495
fix(tracking): stop a tracking run and a reviewer edit from overwriti…
mihow Sep 30, 2026
9ab2852
Merge the tracking evaluation's identity metrics into the tracking se…
mihow Sep 30, 2026
db06a52
fix(tracking): tell a reviewer the session is busy instead of timing …
mihow Sep 30, 2026
98e56fc
Merge the tracking run and track edit safety fixes into the tracking …
mihow Sep 30, 2026
6dd2482
style: wrap the tracks export command's queryset to the line limit
mihow Oct 2, 2026
f4ad80e
fix(ml): drop a feature vector of an unexpected length instead of fai…
mihow Oct 2, 2026
b63a095
fix: call them occurrences and sessions in the messages tracking and …
mihow Oct 2, 2026
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
18 changes: 18 additions & 0 deletions ami/base/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,24 @@ def has_object_permission(self, request, view, obj: BaseModel):
return obj.check_permission(request.user, view.action)


TRACKING_NOT_ENABLED_MESSAGE = "Tracking is not enabled for this project."


class TrackingEnabled(permissions.BasePermission):
"""
Refuse an action on an object whose project has not opted into tracking.

Pair it after ObjectPermission, so a user without rights on the object gets the
usual refusal rather than a hint about the project's settings.
"""

message = TRACKING_NOT_ENABLED_MESSAGE

def has_object_permission(self, request, view, obj: BaseModel):
project = obj.get_project() if hasattr(obj, "get_project") else None
return bool(project and project.feature_flags.tracking)


class ProjectPipelineConfigPermission(ObjectPermission):
"""
Permission for the nested project pipelines route (/projects/{pk}/pipelines/).
Expand Down
2 changes: 2 additions & 0 deletions ami/exports/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ class BaseExporter(ABC):
"""Base class for all data export handlers."""

file_format = "" # To be defined in child classes
# A project feature flag that must be on before this format can be requested.
required_feature_flag: str | None = None
serializer_class = None
filter_backends = []

Expand Down
39 changes: 38 additions & 1 deletion ami/exports/format_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,19 @@


def get_export_serializer():
from ami.main.api.serializers import OccurrenceSerializer
from ami.main.api.serializers import DetectionNestedSerializer, OccurrenceSerializer

# The detail response pages its detections; an export carries every one of them.
detail_only_fields = ("grouping_summary", "first_detection", "last_detection")

class OccurrenceExportSerializer(OccurrenceSerializer):
detection_images = serializers.SerializerMethodField()
detections = DetectionNestedSerializer(many=True, read_only=True)

class Meta(OccurrenceSerializer.Meta):
# The grouping summary describes the detections as they stand at read
# time; it is not part of the occurrence record and is never exported.
fields = [name for name in OccurrenceSerializer.Meta.fields if name not in detail_only_fields]

def get_detection_images(self, obj: Occurrence):
"""Convert the generator field to a list before serialization"""
Expand Down Expand Up @@ -110,6 +119,10 @@ class OccurrenceTabularSerializer(serializers.ModelSerializer):
agreed_with_user = serializers.SerializerMethodField()
determination_matches_machine_prediction = serializers.SerializerMethodField()

# Track grouping confirmation; who confirmed it is user data and is not exported.
grouping_verified = serializers.BooleanField(read_only=True)
grouping_verified_at = serializers.DateTimeField(allow_null=True, read_only=True)

# Detection fields
best_detection_url = serializers.SerializerMethodField()
best_detection_bbox = serializers.SerializerMethodField()
Expand Down Expand Up @@ -139,6 +152,8 @@ class Meta:
"agreed_with_algorithm",
"agreed_with_user",
"determination_matches_machine_prediction",
"grouping_verified",
"grouping_verified_at",
"detections_count",
"first_appearance_timestamp",
"last_appearance_timestamp",
Expand Down Expand Up @@ -252,3 +267,25 @@ def export(self):
self.update_job_progress(records_exported)
self.update_export_stats(file_temp_path=temp_file.name)
return temp_file.name # Return the file path


class TracksCSVExporter(BaseExporter):
"""One row per detection of every occurrence in scope; see ami/exports/tracks.py for the columns."""

file_format = "csv"
required_feature_flag = "tracking"

def get_queryset(self):
return Occurrence.objects.with_real_detections().filter(project=self.project) # type: ignore[union-attr]

def export(self):
from ami.exports.tracks import write_tracks_csv

temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode="w", newline="", encoding="utf-8")
with open(temp_file.name, "w", newline="", encoding="utf-8") as csvfile:
rows = write_tracks_csv(self.queryset, csvfile, on_chunk=self.update_job_progress)
self.update_export_stats(file_temp_path=temp_file.name)
# A tracks file holds one record per detection, not per occurrence.
self.data_export.record_count = rows
self.data_export.save(update_fields=["record_count"])
return temp_file.name
24 changes: 24 additions & 0 deletions ami/exports/migrations/0002_add_tracks_csv_format.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated by Django 4.2.10 on 2026-09-22 17:00

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("exports", "0001_initial"),
]

operations = [
migrations.AlterField(
model_name="dataexport",
name="format",
field=models.CharField(
choices=[
("occurrences_api_json", "occurrences_api_json"),
("occurrences_simple_csv", "occurrences_simple_csv"),
("tracks_csv", "tracks_csv"),
],
max_length=255,
),
),
]
1 change: 1 addition & 0 deletions ami/exports/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ def get_supported_formats(cls):

ExportRegistry.register("occurrences_api_json")(format_types.JSONExporter)
ExportRegistry.register("occurrences_simple_csv")(format_types.CSVExporter)
ExportRegistry.register("tracks_csv")(format_types.TracksCSVExporter)
11 changes: 11 additions & 0 deletions ami/exports/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ def validate_format(self, value):
raise serializers.ValidationError(f"Invalid format. Supported formats are: {supported_formats}")
return value

def validate(self, attrs):
attrs = super().validate(attrs)
exporter = ExportRegistry.get_exporter(attrs.get("format"))
flag = getattr(exporter, "required_feature_flag", None)
project = attrs.get("project")
if flag and project is not None and not getattr(project.feature_flags, flag, False):
raise serializers.ValidationError(
{"format": f"The {attrs['format']} export needs the {flag} feature, which is off for this project."}
)
return attrs

def get_file_url(self, obj):
return obj.get_absolute_url(request=self.context.get("request"))

Expand Down
Loading
Loading