Skip to content

Commit 9127ffb

Browse files
annavikmihow
andauthored
Setup default filters form (#929)
* feat: setup default filters form * fix: update project type * chore: add db migration --------- Co-authored-by: Michael Bunsen <notbot@gmail.com>
1 parent 28571b0 commit 9127ffb

File tree

21 files changed

+551
-133
lines changed

21 files changed

+551
-133
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Generated by Django 4.2.10 on 2025-08-29 04:29
2+
3+
import ami.main.models
4+
from django.db import migrations
5+
import django_pydantic_field.fields
6+
7+
8+
class Migration(migrations.Migration):
9+
dependencies = [
10+
("main", "0069_merge_20250818_1201"),
11+
]
12+
13+
operations = [
14+
migrations.AlterField(
15+
model_name="project",
16+
name="feature_flags",
17+
field=django_pydantic_field.fields.PydanticSchemaField(
18+
blank=True,
19+
config=None,
20+
default={
21+
"auto_process_manual_uploads": False,
22+
"default_filters": False,
23+
"reprocess_existing_detections": False,
24+
"tags": False,
25+
},
26+
schema=ami.main.models.ProjectFeatureFlags,
27+
),
28+
),
29+
]

ami/main/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ class ProjectFeatureFlags(pydantic.BaseModel):
207207
tags: bool = False # Whether the project supports tagging taxa
208208
auto_process_manual_uploads: bool = False # Whether to automatically process uploaded images
209209
reprocess_existing_detections: bool = False # Whether to reprocess existing detections
210+
default_filters: bool = False # Whether to show default filters form in UI
210211

211212

212213
default_feature_flags = ProjectFeatureFlags()

ui/src/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { Jobs } from 'pages/jobs/jobs'
2323
import { Occurrences } from 'pages/occurrences/occurrences'
2424
import { Algorithms } from 'pages/project/algorithms/algorithms'
2525
import { Collections } from 'pages/project/collections/collections'
26+
import { DefaultFilters } from 'pages/project/default-filters/default-filters'
2627
import { Devices } from 'pages/project/entities/devices'
2728
import { Sites } from 'pages/project/entities/sites'
2829
import { Exports } from 'pages/project/exports/exports'
@@ -118,6 +119,7 @@ export const App = () => (
118119
<Route path="sites" element={<Sites />} />
119120
<Route path="devices" element={<Devices />} />
120121
<Route path="general" element={<General />} />
122+
<Route path="default-filters" element={<DefaultFilters />} />
121123
<Route path="storage" element={<Storage />} />
122124
</Route>
123125
<Route path="jobs/:id?" element={<Jobs />} />
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Taxon } from 'data-services/models/taxa'
2+
import { PlusIcon } from 'lucide-react'
3+
import { Button, Popover } from 'nova-ui-kit'
4+
import { useState } from 'react'
5+
import { TaxonSearch } from './taxon-search'
6+
7+
export const AddTaxon = ({ onAdd }: { onAdd: (taxon?: Taxon) => void }) => {
8+
const [open, setOpen] = useState(false)
9+
10+
return (
11+
<Popover.Root open={open} onOpenChange={setOpen}>
12+
<Popover.Trigger asChild>
13+
<Button
14+
variant="outline"
15+
role="combobox"
16+
aria-expanded={open}
17+
className="w-full justify-between px-4 text-muted-foreground font-normal"
18+
>
19+
<>
20+
<span>Add taxon</span>
21+
<PlusIcon className="h-4 w-4 ml-2" />
22+
</>
23+
</Button>
24+
</Popover.Trigger>
25+
<Popover.Content
26+
avoidCollisions={false}
27+
className="w-auto p-0 overflow-hidden"
28+
style={{ maxHeight: 'var(--radix-popover-content-available-height)' }}
29+
>
30+
<TaxonSearch
31+
onTaxonChange={(taxon) => {
32+
onAdd(taxon)
33+
setOpen(false)
34+
}}
35+
/>
36+
</Popover.Content>
37+
</Popover.Root>
38+
)
39+
}

ui/src/components/taxon-search/taxon-select.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,11 @@ export const TaxonSelect = ({
2121
<Popover.Root open={open} onOpenChange={setOpen}>
2222
<Popover.Trigger asChild>
2323
<Button
24-
variant="outline"
25-
role="combobox"
2624
aria-expanded={open}
2725
className="w-full justify-between px-4 text-muted-foreground font-normal"
26+
role="combobox"
27+
type="button"
28+
variant="outline"
2829
>
2930
<>
3031
<span>{triggerLabel}</span>

ui/src/data-services/hooks/projects/useProjectDetails.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,27 @@
11
import { API_ROUTES, API_URL } from 'data-services/constants'
2-
import { Project, ServerProject } from 'data-services/models/project'
2+
import { ServerProject } from 'data-services/models/project'
3+
import { ProjectDetails } from 'data-services/models/project-details'
34
import { useMemo } from 'react'
45
import { useAuthorizedQuery } from '../auth/useAuthorizedQuery'
56

6-
const convertServerRecord = (record: ServerProject) => new Project(record)
7+
const convertServerRecord = (record: ServerProject) =>
8+
new ProjectDetails(record)
79

810
export const useProjectDetails = (
911
projectId: string,
1012
useInternalCache?: boolean
1113
): {
12-
project?: Project
14+
project?: ProjectDetails
1315
isLoading: boolean
1416
isFetching: boolean
1517
error?: unknown
1618
} => {
17-
const { data, isLoading, isFetching, error } = useAuthorizedQuery<Project>({
18-
queryKey: [API_ROUTES.PROJECTS, projectId],
19-
url: `${API_URL}/${API_ROUTES.PROJECTS}/${projectId}/`,
20-
staleTime: useInternalCache ? Infinity : undefined,
21-
})
19+
const { data, isLoading, isFetching, error } =
20+
useAuthorizedQuery<ProjectDetails>({
21+
queryKey: [API_ROUTES.PROJECTS, projectId],
22+
url: `${API_URL}/${API_ROUTES.PROJECTS}/${projectId}/`,
23+
staleTime: useInternalCache ? Infinity : undefined,
24+
})
2225

2326
const project = useMemo(
2427
() => (data ? convertServerRecord(data) : undefined),

ui/src/data-services/hooks/projects/useUpdateProject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export const useUpdateProject = (id: string) => {
2222
}
2323
),
2424
onSuccess: () => {
25-
queryClient.invalidateQueries([API_ROUTES.PROJECTS])
25+
queryClient.invalidateQueries([API_ROUTES.PROJECTS, id])
2626
setTimeout(reset, SUCCESS_TIMEOUT)
2727
},
2828
})
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useMutation, useQueryClient } from '@tanstack/react-query'
2+
import axios from 'axios'
3+
import { API_ROUTES, API_URL, SUCCESS_TIMEOUT } from 'data-services/constants'
4+
import { getAuthHeader } from 'data-services/utils'
5+
import { useUser } from 'utils/user/userContext'
6+
7+
const convertToServerFieldValues = (fieldValues: any) => ({
8+
settings: {
9+
session_time_gap_seconds: fieldValues.sessionTimeGapSeconds,
10+
default_filters_score_threshold: fieldValues.scoreThreshold,
11+
default_filters_include_taxa_ids: fieldValues.includeTaxa.map(
12+
(taxon: any) => taxon.id
13+
),
14+
default_filters_exclude_taxa_ids: fieldValues.excludeTaxa.map(
15+
(taxon: any) => taxon.id
16+
),
17+
},
18+
})
19+
20+
export const useUpdateProjectSettings = (id: string) => {
21+
const { user } = useUser()
22+
const queryClient = useQueryClient()
23+
24+
const { mutateAsync, isLoading, isSuccess, reset, error } = useMutation({
25+
mutationFn: (fieldValues: any) =>
26+
axios.patch(
27+
`${API_URL}/${API_ROUTES.PROJECTS}/${id}/`,
28+
convertToServerFieldValues(fieldValues),
29+
{
30+
headers: {
31+
...getAuthHeader(user),
32+
},
33+
}
34+
),
35+
onSuccess: () => {
36+
queryClient.invalidateQueries([API_ROUTES.PROJECTS, id])
37+
setTimeout(reset, SUCCESS_TIMEOUT)
38+
},
39+
})
40+
41+
return { updateProjectSettings: mutateAsync, isLoading, isSuccess, error }
42+
}

ui/src/data-services/hooks/taxa-tags/useAssignTags.ts

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,18 @@ export const useAssignTags = (id: string, onSuccess?: () => void) => {
1010
const queryClient = useQueryClient()
1111

1212
const { mutate, isLoading, error, isSuccess, reset } = useMutation({
13-
mutationFn: ({ projectId, tags }: { projectId: string; tags: Tag[] }) => {
14-
const data = new FormData()
15-
data.append('tag_ids', JSON.stringify(tags.map((tag) => tag.id)))
16-
17-
return axios.post(
13+
mutationFn: ({ projectId, tags }: { projectId: string; tags: Tag[] }) =>
14+
axios.post(
1815
`${API_URL}/${API_ROUTES.SPECIES}/${id}/assign_tags/?project_id=${projectId}`,
19-
JSON.stringify({
16+
{
2017
tag_ids: tags.map((tag) => tag.id),
21-
}),
18+
},
2219
{
2320
headers: {
2421
...getAuthHeader(user),
25-
'Content-Type': 'application/json',
2622
},
2723
}
28-
)
29-
},
24+
),
3025
onSuccess: () => {
3126
queryClient.invalidateQueries([API_ROUTES.SPECIES])
3227
queryClient.invalidateQueries([API_ROUTES.SPECIES, id])
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Project } from './project'
2+
3+
export type ServerProject = any // TODO: Update this type
4+
5+
interface SummaryData {
6+
title: string
7+
data: {
8+
x: (string | number)[]
9+
y: number[]
10+
tickvals?: (string | number)[]
11+
ticktext?: string[]
12+
}
13+
type: any
14+
orientation: 'h' | 'v'
15+
}
16+
17+
interface Settings {
18+
sessionTimeGapSeconds: number
19+
scoreThreshold: number
20+
includeTaxa: { id: string; name: string }[]
21+
excludeTaxa: { id: string; name: string }[]
22+
}
23+
24+
export class ProjectDetails extends Project {
25+
public constructor(project: ServerProject) {
26+
super(project)
27+
}
28+
29+
get settings(): Settings {
30+
const includeTaxa = this._project.settings.default_filters_include_taxa.map(
31+
(taxon: any) => ({
32+
id: `${taxon.id}`,
33+
name: taxon.name,
34+
})
35+
)
36+
const excludeTaxa = this._project.settings.default_filters_exclude_taxa.map(
37+
(taxon: any) => ({
38+
id: `${taxon.id}`,
39+
name: taxon.name,
40+
})
41+
)
42+
43+
return {
44+
sessionTimeGapSeconds: this._project.settings.session_time_gap_seconds,
45+
scoreThreshold: this._project.settings.default_filters_score_threshold,
46+
includeTaxa,
47+
excludeTaxa,
48+
}
49+
}
50+
51+
get summaryData(): SummaryData[] {
52+
return this._project.summary_data
53+
}
54+
}

0 commit comments

Comments
 (0)