Corp/backend/api/v1/calendar/serializers.py

207 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# backend/api/v1/calendar/serializers.py
from __future__ import annotations
import re
from datetime import date, datetime, time, timedelta
from typing import Any, Dict, List, Optional
from calendar_app.models import CalendarEvent, Recurrence
from django.utils.translation import gettext_lazy as _
from rest_framework import serializers
class CalendarEventWriteSerializer(serializers.ModelSerializer):
"""Создание/обновление события календаря с полной валидацией."""
# Разрешаем пустой список для PATCH (когда меняют weekly -> daily и присылают weekdays: [])
weekdays = serializers.ListField(
child=serializers.IntegerField(min_value=0, max_value=6),
required=False,
allow_empty=True,
write_only=True,
help_text="Список дней недели для WEEKLY (0=Mon..6=Sun).",
)
department_id = serializers.IntegerField(required=False, allow_null=True)
employee_id = serializers.IntegerField(required=False, allow_null=True)
class Meta:
model = CalendarEvent
fields = (
"id",
"department_id",
"employee_id",
"title",
"description",
"start_date",
"end_date",
"start_time",
"end_time",
"all_day",
"recurrence",
"recurrence_interval",
"recurrence_until",
"recurrence_count",
"weekdays_mask",
"weekdays", # write-only
"color",
"location",
)
read_only_fields = ("id",)
# ---- helpers
def _coalesce(self, data: Dict[str, Any], instance: Optional[CalendarEvent], key: str, default=None):
if key in data:
return data.get(key)
if instance is not None:
return getattr(instance, key)
return default
@staticmethod
def _weekdays_to_mask(values: list[int]) -> int:
mask = 0
for v in values:
iv = int(v)
if not (0 <= iv <= 6):
raise ValueError("weekday out of range")
mask |= (1 << iv)
return mask
# ---- validation
def validate(self, attrs: Dict[str, Any]) -> Dict[str, Any]:
inst: Optional[CalendarEvent] = getattr(self, "instance", None)
is_create = inst is None
s_date: date = self._coalesce(attrs, inst, "start_date")
if not s_date:
raise serializers.ValidationError({"start_date": _("Обязательное поле.")})
e_date: Optional[date] = self._coalesce(attrs, inst, "end_date", None)
s_time: Optional[time] = self._coalesce(attrs, inst, "start_time", None)
e_time: Optional[time] = self._coalesce(attrs, inst, "end_time", None)
all_day_raw = self._coalesce(attrs, inst, "all_day", None)
rec: str = self._coalesce(attrs, inst, "recurrence", Recurrence.ONE_TIME)
# ВАЖНО: не используем "or 1", чтобы 0 не превратился в 1
interval_val = self._coalesce(attrs, inst, "recurrence_interval", 1)
try:
interval = int(interval_val)
except Exception:
raise serializers.ValidationError({"recurrence_interval": _("Интервал должен быть числом.")})
until: Optional[date] = self._coalesce(attrs, inst, "recurrence_until", None)
rcount: Optional[int] = self._coalesce(attrs, inst, "recurrence_count", None)
wmask: int = int(self._coalesce(attrs, inst, "weekdays_mask", 0))
weekdays_input = attrs.get("weekdays", None)
if weekdays_input is not None:
try:
wmask = self._weekdays_to_mask(weekdays_input)
except Exception:
raise serializers.ValidationError({"weekdays": _("Неверный формат дней недели (ожидаются числа 0..6).")})
attrs["weekdays_mask"] = wmask
attrs.pop("weekdays", None)
color: Optional[str] = self._coalesce(attrs, inst, "color", None)
# 1) База
if interval < 1:
raise serializers.ValidationError({"recurrence_interval": _("Интервал должен быть ≥ 1.")})
if until is not None and until < s_date:
raise serializers.ValidationError({"recurrence_until": _("Дата окончания повторения не может быть раньше даты начала.")})
# Конфликтующие параметры
if until is not None and rcount is not None:
raise serializers.ValidationError({"recurrence_count": _("Нельзя одновременно задавать recurrence_until и recurrence_count.")})
if color and not re.fullmatch(r"#([0-9A-Fa-f]{6})", color):
raise serializers.ValidationError({"color": _("Цвет должен быть в формате #RRGGBB.")})
# 2) Время / all_day
has_any_time = (s_time is not None) or (e_time is not None)
if has_any_time and (s_time is None or e_time is None):
raise serializers.ValidationError(_("Если указываете время, заполните и начало, и окончание."))
if all_day_raw is None:
attrs["all_day"] = not has_any_time
else:
if all_day_raw and has_any_time:
raise serializers.ValidationError(_("Нельзя указывать время для события на весь день."))
if (all_day_raw is False) and not has_any_time:
raise serializers.ValidationError(_("Для события с временем укажите start_time и end_time."))
all_day: bool = attrs.get("all_day", inst.all_day if inst else (not has_any_time))
# end_date по умолчанию = start_date
if e_date is None:
e_date = s_date
attrs["end_date"] = e_date
# 3) Хронология
if all_day:
if e_date < s_date:
raise serializers.ValidationError({"end_date": _("Дата окончания не может быть раньше даты начала.")})
else:
start_dt = datetime.combine(s_date, s_time)
end_dt = datetime.combine(e_date, e_time)
if end_dt <= start_dt:
raise serializers.ValidationError({"end_time": _("Окончание должно быть позже начала.")})
# 4) Повторяемость
if rec == Recurrence.HOURLY and all_day:
raise serializers.ValidationError({"recurrence": _("Ежечасное повторение возможно только для событий с временем.")})
if rec == Recurrence.WEEKLY:
# create: требуем явные дни
if is_create and wmask == 0:
raise serializers.ValidationError({"weekdays_mask": _("Для еженедельного события укажите дни недели (weekdays или weekdays_mask).")})
# update: если маска не пришла и осталась 0 — подставим день недели старта
if (not is_create) and ("weekdays_mask" not in attrs) and (wmask == 0):
wmask = 1 << s_date.weekday()
attrs["weekdays_mask"] = wmask
if wmask < 0 or wmask > 0b1111111:
raise serializers.ValidationError({"weekdays_mask": _("Недопустимая маска дней недели.")})
else:
# для НЕ weekly игнорируем присланные weekdays/маску (если вдруг присланы пустыми)
attrs.pop("weekdays", None)
# маску не трогаем, если не прислана — оставляем как есть/0
if rec not in Recurrence.values:
raise serializers.ValidationError({"recurrence": _("Недопустимый тип повторения.")})
if rcount is not None and rcount < 1:
raise serializers.ValidationError({"recurrence_count": _("Количество повторов должно быть ≥ 1.")})
return attrs
def create(self, validated_data: Dict[str, Any]) -> CalendarEvent:
validated_data.pop("weekdays", None)
return CalendarEvent.objects.create(**validated_data)
def update(self, instance: CalendarEvent, validated_data: Dict[str, Any]) -> CalendarEvent:
validated_data.pop("weekdays", None)
for f, v in validated_data.items():
setattr(instance, f, v)
instance.save()
return instance
class CalendarEventSerializer(serializers.ModelSerializer):
"""Read-сериализатор (база модели, если нужен просмотр без развёртки)."""
class Meta:
model = CalendarEvent
fields = "__all__"
class EventOccurrenceSerializer(serializers.Serializer):
"""Сериализатор одного материализованного вхождения для FullCalendar."""
id = serializers.IntegerField()
title = serializers.CharField()
start = serializers.DateTimeField(required=False)
end = serializers.DateTimeField(required=False)
allDay = serializers.BooleanField()
color = serializers.CharField(allow_null=True, required=False)
recurrence = serializers.CharField()
department_id = serializers.IntegerField(allow_null=True, required=False)