fix(timezone): fix timer clock-skew
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled

This commit is contained in:
2026-05-26 12:59:43 +03:30
parent 20874b9968
commit b5ddcb76aa
3 changed files with 100 additions and 7 deletions

View File

@@ -38,6 +38,17 @@ class TimeEntryViewSet(ModelViewSet):
filterset_class = TimeEntryFilter
search_fields = ["description", "project__name", "project__client__name", "tags__name"]
@staticmethod
def _epoch_ms(value):
if timezone.is_naive(value):
value = timezone.make_aware(value, timezone.get_current_timezone())
return int(value.timestamp() * 1000)
def _serializer_context(self, *, server_now=None):
context = self.get_serializer_context()
context["server_now"] = server_now or timezone.now()
return context
@staticmethod
def _serialize_duration_ms(entry):
if entry.duration is not None:
@@ -51,8 +62,12 @@ class TimeEntryViewSet(ModelViewSet):
days_since_sunday = (local_dt.weekday() + 1) % 7
return (local_dt - timedelta(days=days_since_sunday)).date()
def _build_grouped_entries(self, entries):
serialized_entries = TimeEntrySerializer(entries, many=True, context=self.get_serializer_context()).data
def _build_grouped_entries(self, entries, *, server_now):
serialized_entries = TimeEntrySerializer(
entries,
many=True,
context=self._serializer_context(server_now=server_now),
).data
serialized_by_id = {item["id"]: item for item in serialized_entries}
weeks = []
weeks_by_key = {}
@@ -114,6 +129,7 @@ class TimeEntryViewSet(ModelViewSet):
queryset = self.filter_queryset(self.get_queryset())
paginator = self.pagination_class()
page = paginator.paginate_queryset(queryset, request, view=self)
server_now = timezone.now()
current_items_count = len(page)
has_more = (paginator.offset + current_items_count) < paginator.count
@@ -125,7 +141,19 @@ class TimeEntryViewSet(ModelViewSet):
"offset": paginator.offset,
"next_offset": paginator.offset + current_items_count if has_more else None,
"has_more": has_more,
"groups": self._build_grouped_entries(page),
"server_now_ms": self._epoch_ms(server_now),
"server_now": server_now.isoformat(),
"groups": self._build_grouped_entries(page, server_now=server_now),
}
)
@action(detail=False, methods=["get"], url_path="debug-time")
def debug_time(self, request):
server_now = timezone.now()
return Response(
{
"server_now_ms": self._epoch_ms(server_now),
"server_now": server_now.isoformat(),
}
)
@@ -148,7 +176,7 @@ class TimeEntryViewSet(ModelViewSet):
**serializer.validated_data
)
output_serializer = TimeEntrySerializer(entry, context=self.get_serializer_context())
output_serializer = TimeEntrySerializer(entry, context=self._serializer_context())
return Response(output_serializer.data, status=status.HTTP_201_CREATED)
def update(self, request, *args, **kwargs):
@@ -168,7 +196,7 @@ class TimeEntryViewSet(ModelViewSet):
**serializer.validated_data
)
output_serializer = TimeEntrySerializer(updated_entry, context=self.get_serializer_context())
output_serializer = TimeEntrySerializer(updated_entry, context=self._serializer_context())
return Response(output_serializer.data, status=status.HTTP_200_OK)
@action(detail=True, methods=["post"])
@@ -189,7 +217,7 @@ class TimeEntryViewSet(ModelViewSet):
end_time = serializer.validated_data.get("end_time")
stopped_entry = stop_time_entry(entry, end_time=end_time)
output_serializer = TimeEntrySerializer(stopped_entry, context=self.get_serializer_context())
output_serializer = TimeEntrySerializer(stopped_entry, context=self._serializer_context())
return Response(output_serializer.data, status=status.HTTP_200_OK)
def destroy(self, request, *args, **kwargs):