import datetime import typing from django.utils import timezone from rest_framework import serializers from apps.schedules.models import OnCallSchedule, ShiftSwapRequest from common.api_helpers.custom_fields import OrganizationFilteredPrimaryKeyRelatedField, TimeZoneAwareDatetimeField from common.api_helpers.mixins import EagerLoadingMixin if typing.TYPE_CHECKING: from apps.user_management.models import User class BaseShiftSwapRequestListSerializer(EagerLoadingMixin, serializers.ModelSerializer): id = serializers.CharField(read_only=True, source="public_primary_key") schedule = OrganizationFilteredPrimaryKeyRelatedField(queryset=OnCallSchedule.objects) created_at = TimeZoneAwareDatetimeField(read_only=True) updated_at = TimeZoneAwareDatetimeField(read_only=True) swap_start = TimeZoneAwareDatetimeField() swap_end = TimeZoneAwareDatetimeField() beneficiary = serializers.CharField(read_only=True, source="beneficiary.public_primary_key") benefactor = serializers.SerializerMethodField(read_only=True) SELECT_RELATED = [ "schedule", "beneficiary", "benefactor", ] class Meta: model = ShiftSwapRequest fields = [ "id", "created_at", "updated_at", "status", "schedule", "swap_start", "swap_end", "description", "beneficiary", "benefactor", ] read_only_fields = [ "status", ] class ShiftSwapRequestListSerializer(BaseShiftSwapRequestListSerializer): def get_benefactor(self, obj: ShiftSwapRequest) -> str | None: return obj.benefactor.public_primary_key if obj.benefactor else None class ShiftSwapRequestSerializer(ShiftSwapRequestListSerializer): class Meta(ShiftSwapRequestListSerializer.Meta): fields = ShiftSwapRequestListSerializer.Meta.fields + [ "shifts", ] read_only_fields = ShiftSwapRequestListSerializer.Meta.read_only_fields + [ "shifts", ] @staticmethod def validate_start_and_end_times(swap_start: datetime.datetime, swap_end: datetime.datetime) -> None: if timezone.now() > swap_start: raise serializers.ValidationError("swap_start must be a datetime in the future") if swap_start > swap_end: raise serializers.ValidationError("swap_end must occur after swap_start") def validate(self, data): swap_start = data.get("swap_start", None) swap_end = data.get("swap_end", None) if self.partial: # self.partial is true when it's a "partial update" aka PATCH # if any time related field is specified then we will enforce that they must all be specified time_fields = [swap_start, swap_end] any_time_fields_specified = any(time_fields) all_time_fields_specified = all(time_fields) if any_time_fields_specified and not all_time_fields_specified: raise serializers.ValidationError( "when doing a partial update on time related fields, both start and end times must be specified" ) elif all_time_fields_specified: self.validate_start_and_end_times(swap_start, swap_end) else: self.validate_start_and_end_times(swap_start, swap_end) # TODO: we should validate that the beneficiary actually has shifts for the specified schedule # between swap_start and swap_end return data class ShiftSwapRequestExpandedUsersListSerializer(BaseShiftSwapRequestListSerializer): beneficiary = serializers.SerializerMethodField(read_only=True) benefactor = serializers.SerializerMethodField(read_only=True) def _serialize_user(self, user: "User") -> dict | None: user_data = None if user: organization = ( self.context["request"].auth.organization if self.context.get("request") else user.organization ) user_data = { "display_name": user.username, "email": user.email, "pk": user.public_primary_key, "avatar_full": user.avatar_full_url(organization), } return user_data def get_benefactor(self, obj: ShiftSwapRequest) -> dict | None: return self._serialize_user(obj.benefactor) def get_beneficiary(self, obj: ShiftSwapRequest) -> dict | None: return self._serialize_user(obj.beneficiary) class ShiftSwapRequestExpandedUsersSerializer( ShiftSwapRequestExpandedUsersListSerializer, ShiftSwapRequestSerializer, ): pass