Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions ami/main/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ class Meta:
"updated_at",
"latitude",
"longitude",
"time_zone",
"first_date",
"last_date",
"device",
Expand Down Expand Up @@ -234,6 +235,7 @@ class Meta:
"id",
"name",
"details",
"time_zone",
]


Expand All @@ -247,6 +249,7 @@ class Meta:
"details",
"latitude",
"longitude",
"time_zone",
"events_count",
# "captures_count",
# "detections_count",
Expand Down
21 changes: 21 additions & 0 deletions ami/main/migrations/0079_deployment_time_zone.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("main", "0078_classification_applied_to"),
]

operations = [
migrations.AddField(
model_name="deployment",
name="time_zone",
field=models.CharField(
default=settings.TIME_ZONE,
help_text="IANA time zone for this deployment (e.g., 'America/Los_Angeles'). Used as metadata for interpreting local timestamps.",
max_length=64,
),
),
]
19 changes: 19 additions & 0 deletions ami/main/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import urllib.parse
from io import BytesIO
from typing import Final, final # noqa: F401
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError

import PIL.Image
import pydantic
Expand Down Expand Up @@ -56,6 +57,15 @@
_POST_TITLE_MAX_LENGTH: Final = 80


def validate_iana_time_zone(value: str) -> None:
if not value:
return
try:
ZoneInfo(value)
except ZoneInfoNotFoundError as exc:
raise ValidationError(f"Invalid IANA time zone: {value!r}.") from exc


class TaxonRank(OrderedEnum):
KINGDOM = "KINGDOM"
PHYLUM = "PHYLUM"
Expand Down Expand Up @@ -606,6 +616,15 @@ class Deployment(BaseModel):
latitude = models.FloatField(null=True, blank=True)
longitude = models.FloatField(null=True, blank=True)
image = models.ImageField(upload_to="deployments", blank=True, null=True)
time_zone = models.CharField(
max_length=64,
default=settings.TIME_ZONE,
help_text=(
"IANA time zone for this deployment (e.g., 'America/Los_Angeles'). "
"Used as metadata for interpreting local timestamps."
),
validators=[validate_iana_time_zone],
)

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

Expand Down
39 changes: 39 additions & 0 deletions ami/main/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@
from io import BytesIO

from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ValidationError
from django.core.files.uploadedfile import SimpleUploadedFile
from django.db import connection, models
from django.test import TestCase, override_settings
from guardian.shortcuts import assign_perm, get_perms, remove_perm
from PIL import Image
from rest_framework import status
from rest_framework import serializers
from rest_framework.test import APIRequestFactory, APITestCase
from rich import print

from ami.exports.models import DataExport
from ami.jobs.models import VALID_JOB_TYPES, Job
from ami.main.api.serializers import (
DeploymentListSerializer,
DeploymentNestedSerializer,
DeploymentNestedSerializerWithLocationAndCounts,
DeploymentSerializer,
)
from ami.main.models import (
Classification,
Deployment,
Expand Down Expand Up @@ -46,6 +54,37 @@
logger = logging.getLogger(__name__)


class TestTimeZoneNormalization(TestCase):
def test_deployment_invalid_time_zone_raises(self):
project = Project.objects.create(name="TZ Project", create_defaults=False)
serializer = DeploymentSerializer(
data={"name": "D1", "project_id": project.pk, "time_zone": "Mars/Phobos"},
context={"request": APIRequestFactory().post("/")},
)
self.assertFalse(serializer.is_valid())
self.assertIn("time_zone", serializer.errors)

def test_deployment_serializers_expose_time_zone(self):
project = Project.objects.create(name="TZ Project", create_defaults=False)
deployment = Deployment.objects.create(project=project, name="D1", time_zone="UTC")

for serializer_cls in (
DeploymentListSerializer,
DeploymentNestedSerializer,
DeploymentNestedSerializerWithLocationAndCounts,
DeploymentSerializer,
):
self.assertIn("time_zone", serializer_cls.Meta.fields)

class DeploymentTimeZoneOnlySerializer(serializers.ModelSerializer):
class Meta:
model = Deployment
fields = ("time_zone",)

data = DeploymentTimeZoneOnlySerializer(deployment).data
self.assertEqual(data["time_zone"], "UTC")


class TestProjectSetup(TestCase):
def test_project_creation(self):
project = Project.objects.create(name="New Project with Defaults", create_defaults=True)
Expand Down