Skip to content

Commit d166865

Browse files
Merge branch 'main' into feat/restrict-ml-processing-jobs
2 parents a9b39d7 + c2e5bb3 commit d166865

File tree

21 files changed

+535
-326
lines changed

21 files changed

+535
-326
lines changed

ami/main/models.py

Lines changed: 121 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3039,7 +3039,7 @@ def html(self) -> str:
30393039

30403040

30413041
_SOURCE_IMAGE_SAMPLING_METHODS = [
3042-
"common_combined",
3042+
"common_combined", # Deprecated
30433043
"random",
30443044
"stratified_random",
30453045
"interval",
@@ -3049,6 +3049,7 @@ def html(self) -> str:
30493049
"last_and_random_from_each_event",
30503050
"greatest_file_size_from_each_event",
30513051
"detections_only",
3052+
"full",
30523053
]
30533054

30543055

@@ -3174,7 +3175,16 @@ def taxa_count(self) -> int | None:
31743175
# This should always be pre-populated using queryset annotations
31753176
return None
31763177

3177-
def get_queryset(self):
3178+
def get_queryset(
3179+
self,
3180+
hour_start: int | None = None,
3181+
hour_end: int | None = None,
3182+
month_start: int | None = None,
3183+
month_end: int | None = None,
3184+
date_start: str | None = None,
3185+
date_end: str | None = None,
3186+
deployment_ids: list[int] | None = None,
3187+
):
31783188
return SourceImage.objects.filter(project=self.project)
31793189

31803190
@classmethod
@@ -3201,33 +3211,17 @@ def populate_sample(self, job: "Job | None" = None):
32013211
self.save()
32023212
task_logger.info(f"Done sampling and saving captures to {self}")
32033213

3204-
def sample_random(self, size: int = 100):
3205-
"""Create a random sample of source images"""
3206-
3207-
qs = self.get_queryset()
3208-
return qs.order_by("?")[:size]
3209-
3210-
def sample_manual(self, image_ids: list[int]):
3211-
"""Create a sample of source images based on a list of source image IDs"""
3212-
3213-
qs = self.get_queryset()
3214-
return qs.filter(id__in=image_ids)
3215-
3216-
def sample_common_combined(
3214+
def _filter_sample(
32173215
self,
3218-
minute_interval: int | None = None,
3219-
max_num: int | None = None,
3220-
shuffle: bool = True, # This is applicable if max_num is set and minute_interval is not set
3216+
qs: models.QuerySet,
32213217
hour_start: int | None = None,
32223218
hour_end: int | None = None,
32233219
month_start: int | None = None,
32243220
month_end: int | None = None,
32253221
date_start: str | None = None,
32263222
date_end: str | None = None,
32273223
deployment_ids: list[int] | None = None,
3228-
) -> models.QuerySet | typing.Generator[SourceImage, None, None]:
3229-
qs = self.get_queryset()
3230-
3224+
):
32313225
if deployment_ids is not None:
32323226
qs = qs.filter(deployment__in=deployment_ids)
32333227
if date_start is not None:
@@ -3252,6 +3246,66 @@ def sample_common_combined(
32523246
elif hour_end is not None:
32533247
qs = qs.filter(timestamp__hour__lte=hour_end)
32543248

3249+
return qs
3250+
3251+
def sample_random(
3252+
self,
3253+
size: int = 100,
3254+
hour_start: int | None = None,
3255+
hour_end: int | None = None,
3256+
month_start: int | None = None,
3257+
month_end: int | None = None,
3258+
date_start: str | None = None,
3259+
date_end: str | None = None,
3260+
deployment_ids: list[int] | None = None,
3261+
):
3262+
"""Create a random sample of source images"""
3263+
3264+
qs = self.get_queryset()
3265+
qs = self._filter_sample(
3266+
qs=qs,
3267+
hour_start=hour_start,
3268+
hour_end=hour_end,
3269+
month_start=month_start,
3270+
month_end=month_end,
3271+
date_start=date_start,
3272+
date_end=date_end,
3273+
deployment_ids=deployment_ids,
3274+
)
3275+
return qs.order_by("?")[:size]
3276+
3277+
def sample_manual(self, image_ids: list[int]):
3278+
"""Create a sample of source images based on a list of source image IDs"""
3279+
3280+
qs = self.get_queryset()
3281+
return qs.filter(id__in=image_ids)
3282+
3283+
# Deprecated
3284+
def sample_common_combined(
3285+
self,
3286+
minute_interval: int | None = None,
3287+
max_num: int | None = None,
3288+
shuffle: bool = True, # This is applicable if max_num is set and minute_interval is not set
3289+
hour_start: int | None = None,
3290+
hour_end: int | None = None,
3291+
month_start: int | None = None,
3292+
month_end: int | None = None,
3293+
date_start: str | None = None,
3294+
date_end: str | None = None,
3295+
deployment_ids: list[int] | None = None,
3296+
) -> models.QuerySet | typing.Generator[SourceImage, None, None]:
3297+
qs = self.get_queryset()
3298+
qs = self._filter_sample(
3299+
qs=qs,
3300+
hour_start=hour_start,
3301+
hour_end=hour_end,
3302+
month_start=month_start,
3303+
month_end=month_end,
3304+
date_start=date_start,
3305+
date_end=date_end,
3306+
deployment_ids=deployment_ids,
3307+
)
3308+
32553309
if minute_interval is not None:
32563310
# @TODO can this be done in the database and return a queryset?
32573311
# this currently returns a list of source images
@@ -3267,11 +3321,31 @@ def sample_common_combined(
32673321
return qs
32683322

32693323
def sample_interval(
3270-
self, minute_interval: int = 10, exclude_events: list[int] = [], deployment_id: int | None = None
3324+
self,
3325+
minute_interval: int = 10,
3326+
exclude_events: list[int] = [],
3327+
deployment_id: int | None = None, # Deprecated
3328+
hour_start: int | None = None,
3329+
hour_end: int | None = None,
3330+
month_start: int | None = None,
3331+
month_end: int | None = None,
3332+
date_start: str | None = None,
3333+
date_end: str | None = None,
3334+
deployment_ids: list[int] | None = None,
32713335
):
32723336
"""Create a sample of source images based on a time interval"""
32733337

32743338
qs = self.get_queryset()
3339+
qs = self._filter_sample(
3340+
qs=qs,
3341+
hour_start=hour_start,
3342+
hour_end=hour_end,
3343+
month_start=month_start,
3344+
month_end=month_end,
3345+
date_start=date_start,
3346+
date_end=date_end,
3347+
deployment_ids=deployment_ids,
3348+
)
32753349
if deployment_id:
32763350
qs = qs.filter(deployment=deployment_id)
32773351
if exclude_events:
@@ -3331,6 +3405,31 @@ def sample_detections_only(self):
33313405
qs = self.get_queryset()
33323406
return qs.filter(detections__isnull=False).distinct()
33333407

3408+
def sample_full(
3409+
self,
3410+
hour_start: int | None = None,
3411+
hour_end: int | None = None,
3412+
month_start: int | None = None,
3413+
month_end: int | None = None,
3414+
date_start: str | None = None,
3415+
date_end: str | None = None,
3416+
deployment_ids: list[int] | None = None,
3417+
):
3418+
"""Sample all source images"""
3419+
3420+
qs = self.get_queryset()
3421+
qs = self._filter_sample(
3422+
qs=qs,
3423+
hour_start=hour_start,
3424+
hour_end=hour_end,
3425+
month_start=month_start,
3426+
month_end=month_end,
3427+
date_start=date_start,
3428+
date_end=date_end,
3429+
deployment_ids=deployment_ids,
3430+
)
3431+
return qs.all().distinct()
3432+
33343433
@classmethod
33353434
def get_or_create_starred_collection(cls, project: Project) -> "SourceImageCollection":
33363435
"""
Lines changed: 14 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,16 @@
1-
import { format } from 'date-fns'
2-
import { AlertCircleIcon, Calendar as CalendarIcon } from 'lucide-react'
3-
import { Button, Calendar, Popover } from 'nova-ui-kit'
4-
import { useState } from 'react'
1+
import { DatePicker } from 'design-system/components/select/date-picker'
52
import { FilterProps } from './types'
63

7-
const dateToLabel = (date: Date) => {
8-
try {
9-
return format(date, 'yyyy-MM-dd')
10-
} catch {
11-
return 'Invalid date'
12-
}
13-
}
14-
15-
export const DateFilter = ({ error, onAdd, onClear, value }: FilterProps) => {
16-
const [open, setOpen] = useState(false)
17-
const selected = value ? new Date(value) : undefined
18-
19-
const triggerLabel = (() => {
20-
if (!value) {
21-
return 'Select a date'
22-
}
23-
24-
return value
25-
})()
26-
27-
return (
28-
<Popover.Root open={open} onOpenChange={setOpen}>
29-
<Popover.Trigger asChild>
30-
<Button
31-
variant="outline"
32-
role="combobox"
33-
aria-expanded={open}
34-
className="w-full justify-between px-4 text-muted-foreground font-normal"
35-
>
36-
<>
37-
<span>{triggerLabel}</span>
38-
{selected && error ? (
39-
<AlertCircleIcon className="w-4 w-4 text-destructive" />
40-
) : (
41-
<CalendarIcon className="w-4 w-4" />
42-
)}
43-
</>
44-
</Button>
45-
</Popover.Trigger>
46-
<Popover.Content className="w-auto p-0 overflow-hidden">
47-
<Calendar
48-
mode="single"
49-
selected={selected}
50-
onSelect={(date) => {
51-
if (date) {
52-
onAdd(dateToLabel(date))
53-
} else {
54-
onClear()
55-
}
56-
setOpen(false)
57-
}}
58-
/>
59-
</Popover.Content>
60-
</Popover.Root>
61-
)
62-
}
4+
export const DateFilter = ({ error, onAdd, onClear, value }: FilterProps) => (
5+
<DatePicker
6+
error={error}
7+
value={value}
8+
onValueChange={(date) => {
9+
if (date) {
10+
onAdd(date)
11+
} else {
12+
onClear()
13+
}
14+
}}
15+
/>
16+
)

ui/src/components/header/user-info-dialog/user-info-form/user-email-field.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { FormField } from 'components/form/form-field'
22
import { FormError } from 'components/form/layout/layout'
33
import { FormConfig } from 'components/form/types'
44
import { useUpdateUserEmail } from 'data-services/hooks/auth/useUpdateUserEmail'
5-
import { Button, ButtonTheme } from 'design-system/components/button/button'
6-
import { IconType } from 'design-system/components/icon/icon'
5+
import { SaveButton } from 'design-system/components/button/save-button'
76
import {
87
EditableInput,
98
InputContent,
109
InputValue,
1110
} from 'design-system/components/input/input'
11+
import { Button } from 'nova-ui-kit'
1212
import { useState } from 'react'
1313
import { useForm } from 'react-hook-form'
1414
import { STRING, translate } from 'utils/language'
@@ -91,19 +91,10 @@ const UpdateEmailForm = ({ onCancel }: { onCancel: () => void }) => {
9191
control={control}
9292
/>
9393
<div className={styles.miniFormActions}>
94-
<Button
95-
label={translate(STRING.CANCEL)}
96-
theme={ButtonTheme.Plain}
97-
onClick={() => onCancel()}
98-
/>
99-
<Button
100-
label={translate(STRING.SAVE)}
101-
icon={isSuccess ? IconType.RadixCheck : undefined}
102-
type="submit"
103-
theme={ButtonTheme.Success}
104-
loading={isLoading}
105-
disabled={isLoading || isSuccess}
106-
/>
94+
<Button onClick={() => onCancel()} size="small" variant="ghost">
95+
<span>{translate(STRING.CANCEL)}</span>
96+
</Button>
97+
<SaveButton isLoading={isLoading} isSuccess={isSuccess} />
10798
</div>
10899
</div>
109100
</form>

ui/src/components/header/user-info-dialog/user-info-form/user-info-form.module.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,6 @@
1717
.miniFormActions {
1818
display: flex;
1919
justify-content: flex-end;
20-
gap: 16px;
20+
gap: 8px;
2121
padding-top: 16px;
2222
}

ui/src/components/header/user-info-dialog/user-info-form/user-info-form.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import {
88
} from 'components/form/layout/layout'
99
import { FormConfig } from 'components/form/types'
1010
import { useUpdateUserInfo } from 'data-services/hooks/auth/useUpdateUserInfo'
11-
import { Button, ButtonTheme } from 'design-system/components/button/button'
12-
import { IconType } from 'design-system/components/icon/icon'
11+
import { SaveButton } from 'design-system/components/button/save-button'
1312
import { InputContent } from 'design-system/components/input/input'
1413
import { useRef } from 'react'
1514
import { useForm } from 'react-hook-form'
@@ -115,16 +114,14 @@ export const UserInfoForm = ({ userInfo }: { userInfo: UserInfo }) => {
115114
</FormRow>
116115
</FormSection>
117116
<FormActions>
118-
<Button
119-
label={isSuccess ? translate(STRING.SAVED) : translate(STRING.SAVE)}
120-
icon={isSuccess ? IconType.RadixCheck : undefined}
117+
<SaveButton
118+
isLoading={isLoading}
119+
isSuccess={isSuccess}
121120
onClick={() => {
122121
formRef.current?.dispatchEvent(
123122
new Event('submit', { cancelable: true, bubbles: true })
124123
)
125124
}}
126-
theme={ButtonTheme.Success}
127-
loading={isLoading}
128125
/>
129126
</FormActions>
130127
</>

ui/src/components/header/user-info-dialog/user-info-form/user-password-field.tsx

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import { FormField } from 'components/form/form-field'
22
import { FormError } from 'components/form/layout/layout'
33
import { FormConfig } from 'components/form/types'
44
import { useUpdateUserPassword } from 'data-services/hooks/auth/useUpdateUserPassword'
5-
import { Button, ButtonTheme } from 'design-system/components/button/button'
6-
import { IconType } from 'design-system/components/icon/icon'
5+
import { SaveButton } from 'design-system/components/button/save-button'
76
import {
87
EditableInput,
98
InputContent,
109
InputValue,
1110
} from 'design-system/components/input/input'
11+
import { Button } from 'nova-ui-kit'
1212
import { useState } from 'react'
1313
import { useForm } from 'react-hook-form'
1414
import { STRING, translate } from 'utils/language'
@@ -99,19 +99,10 @@ const UpdatePasswordForm = ({ onCancel }: { onCancel: () => void }) => {
9999
control={control}
100100
/>
101101
<div className={styles.miniFormActions}>
102-
<Button
103-
label={translate(STRING.CANCEL)}
104-
theme={ButtonTheme.Plain}
105-
onClick={() => onCancel()}
106-
/>
107-
<Button
108-
label={translate(STRING.SAVE)}
109-
icon={isSuccess ? IconType.RadixCheck : undefined}
110-
type="submit"
111-
theme={ButtonTheme.Success}
112-
loading={isLoading}
113-
disabled={isLoading || isSuccess}
114-
/>
102+
<Button size="small" variant="ghost" onClick={() => onCancel()}>
103+
<span>{translate(STRING.CANCEL)}</span>
104+
</Button>
105+
<SaveButton isLoading={isLoading} isSuccess={isSuccess} />
115106
</div>
116107
</div>
117108
</form>

0 commit comments

Comments
 (0)