189 lines
7.7 KiB
Python
189 lines
7.7 KiB
Python
from rest_framework import serializers
|
|
|
|
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)
|
|
|
|
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",
|
|
"end_time",
|
|
"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()
|
|
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")
|
|
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)
|