Files
qlockify-backend-deployment/apps/time_entries/api/serializers.py
Amirhossein Khalili b5ddcb76aa
Some checks failed
Backend CI/CD / test (push) Has been cancelled
Backend CI/CD / deploy (push) Has been cancelled
fix(timezone): fix timer clock-skew
2026-05-26 12:59:49 +03:30

220 lines
8.8 KiB
Python

from rest_framework import serializers
from django.utils import timezone
from core.serializers.base import BaseModelSerializer
from apps.time_entries.models import TimeEntry
from apps.projects.models import Project
from apps.projects.services.access import ensure_project_access
from apps.tags.models import Tag
class TimeEntryProjectDetailsSerializer(serializers.Serializer):
id = serializers.UUIDField(read_only=True)
name = serializers.CharField(read_only=True)
is_deleted = serializers.BooleanField(read_only=True)
client_name = serializers.CharField(read_only=True, allow_null=True)
class TimeEntryTagDetailsSerializer(serializers.Serializer):
id = serializers.UUIDField(read_only=True)
name = serializers.CharField(read_only=True)
color = serializers.CharField(read_only=True)
is_deleted = serializers.BooleanField(read_only=True)
class TimeEntrySerializer(BaseModelSerializer):
"""
Output serializer for TimeEntry.
"""
project = serializers.UUIDField(source="project_id", allow_null=True, read_only=True)
tags = serializers.SerializerMethodField()
project_details = serializers.SerializerMethodField()
tag_details = serializers.SerializerMethodField()
start_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S")
end_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S", allow_null=True)
start_time_ms = serializers.SerializerMethodField()
end_time_ms = serializers.SerializerMethodField()
server_now_ms = serializers.SerializerMethodField()
@staticmethod
def _epoch_ms(value):
if value is None:
return None
if timezone.is_naive(value):
value = timezone.make_aware(value, timezone.get_current_timezone())
return int(value.timestamp() * 1000)
def get_start_time_ms(self, obj):
return self._epoch_ms(obj.start_time)
def get_end_time_ms(self, obj):
return self._epoch_ms(obj.end_time)
def get_server_now_ms(self, obj):
server_now = self.context.get("server_now") or timezone.now()
return self._epoch_ms(server_now)
def get_tags(self, obj):
return [str(tag.id) for tag in Tag.all_objects.filter(time_entries=obj).order_by("-updated_at", "-created_at")]
def get_project_details(self, obj):
if not obj.project_id:
return None
project = Project.all_objects.select_related("client").filter(id=obj.project_id).first()
if not project:
return None
return TimeEntryProjectDetailsSerializer(
{
"id": project.id,
"name": project.name,
"is_deleted": project.is_deleted,
"client_name": project.client.name if project.client else None,
}
).data
def get_tag_details(self, obj):
tags = Tag.all_objects.filter(time_entries=obj).order_by("-updated_at", "-created_at")
return TimeEntryTagDetailsSerializer(
[
{
"id": tag.id,
"name": tag.name,
"color": tag.color,
"is_deleted": tag.is_deleted,
}
for tag in tags
],
many=True,
).data
class Meta:
model = TimeEntry
fields = BaseModelSerializer.Meta.fields + (
"workspace",
"user",
"project",
"project_details",
"description",
"start_time",
"start_time_ms",
"end_time",
"end_time_ms",
"server_now_ms",
"duration",
"tags",
"tag_details",
"is_billable",
"hourly_rate",
"currency",
)
read_only_fields = fields
class TimeEntryCreateSerializer(serializers.Serializer):
"""
Validates input data for creating/starting a time entry.
"""
workspace_id = serializers.UUIDField()
project_id = serializers.UUIDField(required=False, allow_null=True)
start_time = serializers.DateTimeField(required=False)
end_time = serializers.DateTimeField(required=False, allow_null=True)
description = serializers.CharField(required=False, allow_blank=True, default="")
tags = serializers.ListField(child=serializers.UUIDField(), required=False)
is_billable = serializers.BooleanField(default=False)
def validate(self, attrs):
user = self.context.get("request").user if self.context.get("request") else None
workspace_id = attrs.get("workspace_id")
start_time = attrs.get("start_time")
end_time = attrs.get("end_time")
if end_time is not None and start_time is None:
raise serializers.ValidationError({"start_time": "Start time is required when end time is provided."})
project_id = attrs.pop("project_id", serializers.empty)
if project_id is not serializers.empty:
if project_id is None:
attrs["project"] = None
else:
project = Project.objects.filter(id=project_id).first()
if not project:
raise serializers.ValidationError({"project_id": "Selected project is unavailable."})
if workspace_id and str(project.workspace_id) != str(workspace_id):
raise serializers.ValidationError({"project_id": "Selected project is unavailable."})
if user:
ensure_project_access(user, project)
attrs["project"] = project
tag_ids = attrs.pop("tags", serializers.empty)
if tag_ids is not serializers.empty:
active_tags = list(Tag.objects.filter(id__in=tag_ids))
active_tag_ids = {str(tag.id) for tag in active_tags}
missing_ids = [str(tag_id) for tag_id in tag_ids if str(tag_id) not in active_tag_ids]
if missing_ids:
raise serializers.ValidationError({"tags": "One or more selected tags are unavailable."})
attrs["tags"] = active_tags
return attrs
class TimeEntryUpdateSerializer(serializers.Serializer):
"""
Validates input data for updating an existing time entry.
"""
project_id = serializers.UUIDField(required=False, allow_null=True)
start_time = serializers.DateTimeField(required=False)
end_time = serializers.DateTimeField(required=False, allow_null=True)
description = serializers.CharField(required=False, allow_blank=True)
tags = serializers.ListField(child=serializers.UUIDField(), required=False)
is_billable = serializers.BooleanField(required=False)
def validate(self, attrs):
entry = self.instance
user = self.context.get("request").user if self.context.get("request") else None
project_id = attrs.pop("project_id", serializers.empty)
if project_id is not serializers.empty:
current_project = Project.all_objects.filter(id=entry.project_id).first() if entry and entry.project_id else None
if project_id is None:
attrs["project"] = None
elif current_project and str(current_project.id) == str(project_id):
attrs["project"] = current_project
else:
project = Project.objects.filter(id=project_id).first()
if not project:
raise serializers.ValidationError({"project_id": "Selected project is unavailable."})
if entry and str(project.workspace_id) != str(entry.workspace_id):
raise serializers.ValidationError({"project_id": "Selected project is unavailable."})
if user:
ensure_project_access(user, project)
attrs["project"] = project
tag_ids = attrs.pop("tags", serializers.empty)
if tag_ids is not serializers.empty:
active_tags = list(Tag.objects.filter(id__in=tag_ids))
active_tag_ids = {str(tag.id) for tag in active_tags}
current_tag_ids = {
str(tag_id)
for tag_id in Tag.all_objects.filter(time_entries=entry).values_list("id", flat=True)
} if entry else set()
requested_tag_ids = [str(tag_id) for tag_id in tag_ids]
missing_ids = [tag_id for tag_id in requested_tag_ids if tag_id not in active_tag_ids]
if missing_ids:
if not set(missing_ids).issubset(current_tag_ids):
raise serializers.ValidationError({"tags": "One or more selected tags are unavailable."})
attrs["tags"] = list(Tag.all_objects.filter(id__in=tag_ids))
else:
attrs["tags"] = active_tags
return attrs
class TimeEntryStopSerializer(serializers.Serializer):
"""
Optional specific serializer for stopping a timer manually.
"""
end_time = serializers.DateTimeField(required=False, allow_null=True)