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.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): 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."}) 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 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."}) 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)