Skip to content
Merged
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
43 changes: 43 additions & 0 deletions src/backend/apps/webcam/tests/test_webcam_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,46 @@ def test_cameras_list_filtering(self, mock_requests_get):
webcams_list = response.json().get('webcams', [])
webcams_list_length = len(webcams_list)
assert webcams_list_length == 0

@patch('apps.webcam.views.get_image_list')
def test_timelapse_filtering(self, mock_get_image_list):
pk = 67
url = f"/api/webcams/{pk}/timelapse/"

mock_timestamps = [
"20260419140000", # 07:00 Vancouver (UTC-7)
"20260419150000", # 08:00 Vancouver
"20260419160000", # 09:00 Vancouver
"20260419170000", # 10:00 Vancouver
"20260419180000", # 11:00 Vancouver
"20260419230000", # 16:00 Vancouver
"20260420000000", # 17:00 Vancouver
"20260420010000", # 18:00 Vancouver
"20260420020000", # 19:00 Vancouver (outside range)
]

# No filtering - returns all images
mock_get_image_list.return_value = mock_timestamps
response = self.client.get(url)
assert response.status_code == 200
result = response.json()
assert len(result) == len(mock_timestamps)

# Filter by date and time in Vancouver timezone
mock_get_image_list.return_value = mock_timestamps
response = self.client.get(url, {
'from': '2026-04-19',
'to': '2026-04-19',
'time_from': '08:00',
'time_to': '18:00',
'timezone': 'America/Vancouver',
})
assert response.status_code == 200
result = response.json()
# 08:00-18:00 Vancouver = 15:00-01:00 UTC
# Expected: 20260419150000 through 20260420010000 (inclusive)
assert len(result) == 7
assert "20260419140000" not in result # 07:00 Vancouver - before range
assert "20260419150000" in result # 08:00 Vancouver - start of range
assert "20260420010000" in result # 18:00 Vancouver - end of range
assert "20260420020000" not in result # 19:00 Vancouver - after
47 changes: 42 additions & 5 deletions src/backend/apps/webcam/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from apps.webcam.models import Webcam
from apps.webcam.serializers import WebcamSerializer
from rest_framework import viewsets
from datetime import datetime, time, date
from datetime import datetime, time, date, timedelta
from urllib.parse import urlparse
from rest_framework.decorators import action
from rest_framework.response import Response
Expand Down Expand Up @@ -129,9 +129,46 @@ def _original_image_impl(self, pk):

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

def _timelapse_impl(self, pk):
def _timelapse_impl(self, request, pk):
from_date_str = request.query_params.get("from")
to_date_str = request.query_params.get("to")
from_time_str = request.query_params.get("time_from")
to_time_str = request.query_params.get("time_to")
timezone_str = request.query_params.get("timezone", "UTC")

timestamps = get_image_list(pk, "TIMELAPSE_HOURS")
return Response(timestamps, status=status.HTTP_200_OK)

# If no filter params provided, return full list unfiltered
if not any([from_date_str, to_date_str, from_time_str, to_time_str, timezone_str]):
return Response(timestamps, status=status.HTTP_200_OK)

tz = ZoneInfo(timezone_str)

from_date = parse_date(from_date_str) if from_date_str else None
to_date = parse_date(to_date_str) if to_date_str else None
from_time = datetime.strptime(from_time_str, "%H:%M").time() if from_time_str else time.min
to_time = datetime.strptime(to_time_str, "%H:%M").time() if to_time_str else time.max

# Default: last 24 hours in the requested timezone
now_local = datetime.now(tz)
default_from_dt = now_local - timedelta(hours=24)

base_from_date = from_date or default_from_dt.date()
base_to_date = to_date or now_local.date()

local_from_dt = datetime.combine(base_from_date, from_time, tzinfo=tz)
local_to_dt = datetime.combine(base_to_date, to_time, tzinfo=tz)

from_dt_utc = local_from_dt.astimezone(ZoneInfo("UTC"))
to_dt_utc = local_to_dt.astimezone(ZoneInfo("UTC"))

filtered_images = []
for timestamp in timestamps:
img_dt = datetime.strptime(timestamp, "%Y%m%d%H%M%S").replace(tzinfo=ZoneInfo("UTC"))
if from_dt_utc <= img_dt <= to_dt_utc:
filtered_images.append(timestamp)

return Response(filtered_images, status=status.HTTP_200_OK)

def _timelapse_image_impl(self, request, pk=None, filename=None):
# Environment variables
Expand Down Expand Up @@ -304,7 +341,7 @@ def timelapse_admin(self, request, pk=None):
user = request.user
if not user.is_authenticated or not user.is_staff:
return Response({"detail": "Admin access required."}, status=status.HTTP_403_FORBIDDEN)
return self._timelapse_impl(pk)
return self._timelapse_impl(request, pk)

@action(
detail=True,
Expand Down Expand Up @@ -348,7 +385,7 @@ def original_image_public(self, request, pk=None):
)
def timelapse_public(self, request, pk=None):
# forward to the real admin logic
return self._timelapse_impl(pk)
return self._timelapse_impl(request, pk)

@action(
detail=True,
Expand Down
Loading