Skip to content

Commit 48185a7

Browse files
committed
feat: add deployment time zone metadata
1 parent 0b6eb9c commit 48185a7

File tree

4 files changed

+65
-0
lines changed

4 files changed

+65
-0
lines changed

ami/main/api/serializers.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ class Meta:
185185
"updated_at",
186186
"latitude",
187187
"longitude",
188+
"time_zone",
188189
"first_date",
189190
"last_date",
190191
"device",
@@ -234,6 +235,7 @@ class Meta:
234235
"id",
235236
"name",
236237
"details",
238+
"time_zone",
237239
]
238240

239241

@@ -247,6 +249,7 @@ class Meta:
247249
"details",
248250
"latitude",
249251
"longitude",
252+
"time_zone",
250253
"events_count",
251254
# "captures_count",
252255
# "detections_count",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from django.conf import settings
2+
from django.db import migrations, models
3+
4+
5+
class Migration(migrations.Migration):
6+
7+
dependencies = [
8+
("main", "0078_classification_applied_to"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="deployment",
14+
name="time_zone",
15+
field=models.CharField(
16+
default=settings.TIME_ZONE,
17+
help_text="IANA time zone for this deployment. Naive datetimes are interpreted in this zone before being stored as UTC.",
18+
max_length=64,
19+
),
20+
),
21+
]

ami/main/models.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import urllib.parse
99
from io import BytesIO
1010
from typing import Final, final # noqa: F401
11+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
1112

1213
import PIL.Image
1314
import pydantic
@@ -606,6 +607,14 @@ class Deployment(BaseModel):
606607
latitude = models.FloatField(null=True, blank=True)
607608
longitude = models.FloatField(null=True, blank=True)
608609
image = models.ImageField(upload_to="deployments", blank=True, null=True)
610+
time_zone = models.CharField(
611+
max_length=64,
612+
default=settings.TIME_ZONE,
613+
help_text=(
614+
"IANA time zone for this deployment. Naive datetimes are interpreted in this zone "
615+
"before being stored as UTC."
616+
),
617+
)
609618

610619
project = models.ForeignKey(Project, on_delete=models.SET_NULL, null=True, related_name="deployments")
611620

@@ -662,6 +671,14 @@ class Deployment(BaseModel):
662671
class Meta:
663672
ordering = ["name"]
664673

674+
def clean(self):
675+
super().clean()
676+
if self.time_zone:
677+
try:
678+
ZoneInfo(self.time_zone)
679+
except ZoneInfoNotFoundError as exc:
680+
raise ValidationError({"time_zone": f"Invalid IANA time zone '{self.time_zone}': {exc}"}) from exc
681+
665682
def taxa(self) -> models.QuerySet["Taxon"]:
666683
return Taxon.objects.filter(Q(occurrences__deployment=self)).distinct()
667684

ami/main/tests.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from io import BytesIO
55

66
from django.contrib.auth.models import AnonymousUser
7+
from django.core.exceptions import ValidationError
78
from django.core.files.uploadedfile import SimpleUploadedFile
89
from django.db import connection, models
910
from django.test import TestCase, override_settings
@@ -15,6 +16,7 @@
1516

1617
from ami.exports.models import DataExport
1718
from ami.jobs.models import VALID_JOB_TYPES, Job
19+
from ami.main.api.serializers import DeploymentSerializer
1820
from ami.main.models import (
1921
Classification,
2022
Deployment,
@@ -46,6 +48,28 @@
4648
logger = logging.getLogger(__name__)
4749

4850

51+
class TestTimeZoneNormalization(TestCase):
52+
def test_deployment_invalid_time_zone_raises(self):
53+
project = Project.objects.create(name="TZ Project", create_defaults=False)
54+
deployment = Deployment(project=project, name="D1", time_zone="Mars/Phobos")
55+
with self.assertRaises(ValidationError):
56+
deployment.full_clean()
57+
58+
def test_deployment_serializer_exposes_time_zone(self):
59+
project = Project.objects.create(name="TZ Project", create_defaults=False)
60+
deployment = Deployment.objects.create(project=project, name="D1", time_zone="UTC")
61+
62+
class MinimalDeploymentSerializer(DeploymentSerializer):
63+
class Meta(DeploymentSerializer.Meta):
64+
fields = ("id", "time_zone")
65+
66+
request = APIRequestFactory().get("/")
67+
request.user = AnonymousUser()
68+
69+
data = MinimalDeploymentSerializer(deployment, context={"request": request}).data
70+
self.assertEqual(data["time_zone"], "UTC")
71+
72+
4973
class TestProjectSetup(TestCase):
5074
def test_project_creation(self):
5175
project = Project.objects.create(name="New Project with Defaults", create_defaults=True)

0 commit comments

Comments
 (0)