Skip to content

Commit 335f824

Browse files
bcgov-brwangwmuldergov
authored andcommitted
DBC22-6238: added filters to timelapse api (#1304)
* DBC22-6238: added filters to timelapse api * DBC22-6238: added test cases for timelapse endpoint
1 parent 04ab4f8 commit 335f824

2 files changed

Lines changed: 85 additions & 5 deletions

File tree

src/backend/apps/webcam/tests/test_webcam_api.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,46 @@ def test_cameras_list_filtering(self, mock_requests_get):
110110
webcams_list = response.json().get('webcams', [])
111111
webcams_list_length = len(webcams_list)
112112
assert webcams_list_length == 0
113+
114+
@patch('apps.webcam.views.get_image_list')
115+
def test_timelapse_filtering(self, mock_get_image_list):
116+
pk = 67
117+
url = f"/api/webcams/{pk}/timelapse/"
118+
119+
mock_timestamps = [
120+
"20260419140000", # 07:00 Vancouver (UTC-7)
121+
"20260419150000", # 08:00 Vancouver
122+
"20260419160000", # 09:00 Vancouver
123+
"20260419170000", # 10:00 Vancouver
124+
"20260419180000", # 11:00 Vancouver
125+
"20260419230000", # 16:00 Vancouver
126+
"20260420000000", # 17:00 Vancouver
127+
"20260420010000", # 18:00 Vancouver
128+
"20260420020000", # 19:00 Vancouver (outside range)
129+
]
130+
131+
# No filtering - returns all images
132+
mock_get_image_list.return_value = mock_timestamps
133+
response = self.client.get(url)
134+
assert response.status_code == 200
135+
result = response.json()
136+
assert len(result) == len(mock_timestamps)
137+
138+
# Filter by date and time in Vancouver timezone
139+
mock_get_image_list.return_value = mock_timestamps
140+
response = self.client.get(url, {
141+
'from': '2026-04-19',
142+
'to': '2026-04-19',
143+
'time_from': '08:00',
144+
'time_to': '18:00',
145+
'timezone': 'America/Vancouver',
146+
})
147+
assert response.status_code == 200
148+
result = response.json()
149+
# 08:00-18:00 Vancouver = 15:00-01:00 UTC
150+
# Expected: 20260419150000 through 20260420010000 (inclusive)
151+
assert len(result) == 7
152+
assert "20260419140000" not in result # 07:00 Vancouver - before range
153+
assert "20260419150000" in result # 08:00 Vancouver - start of range
154+
assert "20260420010000" in result # 18:00 Vancouver - end of range
155+
assert "20260420020000" not in result # 19:00 Vancouver - after

src/backend/apps/webcam/views.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from apps.webcam.models import Webcam
44
from apps.webcam.serializers import WebcamSerializer
55
from rest_framework import viewsets
6-
from datetime import datetime, time, date
6+
from datetime import datetime, time, date, timedelta
77
from urllib.parse import urlparse
88
from rest_framework.decorators import action
99
from rest_framework.response import Response
@@ -129,9 +129,46 @@ def _original_image_impl(self, pk):
129129

130130
return FileResponse(open(image_path, 'rb'), content_type='image/jpeg')
131131

132-
def _timelapse_impl(self, pk):
132+
def _timelapse_impl(self, request, pk):
133+
from_date_str = request.query_params.get("from")
134+
to_date_str = request.query_params.get("to")
135+
from_time_str = request.query_params.get("time_from")
136+
to_time_str = request.query_params.get("time_to")
137+
timezone_str = request.query_params.get("timezone", "UTC")
138+
133139
timestamps = get_image_list(pk, "TIMELAPSE_HOURS")
134-
return Response(timestamps, status=status.HTTP_200_OK)
140+
141+
# If no filter params provided, return full list unfiltered
142+
if not any([from_date_str, to_date_str, from_time_str, to_time_str, timezone_str]):
143+
return Response(timestamps, status=status.HTTP_200_OK)
144+
145+
tz = ZoneInfo(timezone_str)
146+
147+
from_date = parse_date(from_date_str) if from_date_str else None
148+
to_date = parse_date(to_date_str) if to_date_str else None
149+
from_time = datetime.strptime(from_time_str, "%H:%M").time() if from_time_str else time.min
150+
to_time = datetime.strptime(to_time_str, "%H:%M").time() if to_time_str else time.max
151+
152+
# Default: last 24 hours in the requested timezone
153+
now_local = datetime.now(tz)
154+
default_from_dt = now_local - timedelta(hours=24)
155+
156+
base_from_date = from_date or default_from_dt.date()
157+
base_to_date = to_date or now_local.date()
158+
159+
local_from_dt = datetime.combine(base_from_date, from_time, tzinfo=tz)
160+
local_to_dt = datetime.combine(base_to_date, to_time, tzinfo=tz)
161+
162+
from_dt_utc = local_from_dt.astimezone(ZoneInfo("UTC"))
163+
to_dt_utc = local_to_dt.astimezone(ZoneInfo("UTC"))
164+
165+
filtered_images = []
166+
for timestamp in timestamps:
167+
img_dt = datetime.strptime(timestamp, "%Y%m%d%H%M%S").replace(tzinfo=ZoneInfo("UTC"))
168+
if from_dt_utc <= img_dt <= to_dt_utc:
169+
filtered_images.append(timestamp)
170+
171+
return Response(filtered_images, status=status.HTTP_200_OK)
135172

136173
def _timelapse_image_impl(self, request, pk=None, filename=None):
137174
# Environment variables
@@ -304,7 +341,7 @@ def timelapse_admin(self, request, pk=None):
304341
user = request.user
305342
if not user.is_authenticated or not user.is_staff:
306343
return Response({"detail": "Admin access required."}, status=status.HTTP_403_FORBIDDEN)
307-
return self._timelapse_impl(pk)
344+
return self._timelapse_impl(request, pk)
308345

309346
@action(
310347
detail=True,
@@ -348,7 +385,7 @@ def original_image_public(self, request, pk=None):
348385
)
349386
def timelapse_public(self, request, pk=None):
350387
# forward to the real admin logic
351-
return self._timelapse_impl(pk)
388+
return self._timelapse_impl(request, pk)
352389

353390
@action(
354391
detail=True,

0 commit comments

Comments
 (0)