Corp/backend/api/v1/schedule/serializers.py
igor_nadein 45870fba86 fix(calendar): timezone handling для datetime-local inputs
- Добавлена функция toLocalDateTimeString для корректной конвертации UTC в локальное время
- Исправлено отображение времени в модальном окне событий (было 21:00 UTC вместо 00:00 MSK)
- Упрощена логика datetime-local inputs (удалена сложная условная логика)
- Добавлен allDayAccessor в react-big-calendar для корректного отображения событий
- Унифицирован EventModal для обоих календарей (~718 строк удалено)
2026-02-25 00:38:54 +03:00

324 lines
12 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/schedule/serializers.py
"""
Serializers для django-scheduler.
Чистые, проверенные временем модели.
"""
from rest_framework import serializers
from schedule.models import Calendar, Event, Rule, Occurrence, EventRelation, CalendarRelation
from django.contrib.contenttypes.models import ContentType
import pytz
class CalendarRelationSerializer(serializers.ModelSerializer):
"""Сериализатор для CalendarRelation (участники календаря)."""
user_id = serializers.IntegerField(write_only=True, required=False)
user = serializers.SerializerMethodField(read_only=True)
class Meta:
model = CalendarRelation
fields = ['id', 'calendar', 'user_id', 'user', 'distinction', 'inheritable']
read_only_fields = ['id']
def get_user(self, obj):
"""Информация о связанном пользователе."""
if obj.content_object:
return {
'id': obj.content_object.id,
'username': getattr(obj.content_object, 'username', str(obj.content_object)),
'first_name': getattr(obj.content_object, 'first_name', ''),
'last_name': getattr(obj.content_object, 'last_name', ''),
}
return None
def create(self, validated_data):
"""Создать связь календаря с пользователем."""
from django.contrib.auth import get_user_model
User = get_user_model()
user_id = validated_data.pop('user_id')
calendar = validated_data['calendar']
distinction = validated_data.get('distinction', 'viewer')
inheritable = validated_data.get('inheritable', True)
user = User.objects.get(id=user_id)
# Используем встроенный метод create_relation
calendar.create_relation(user, distinction=distinction, inheritable=inheritable)
# Возвращаем созданную relation
ct = ContentType.objects.get_for_model(User)
return CalendarRelation.objects.get(
calendar=calendar,
content_type=ct,
object_id=user_id,
distinction=distinction
)
class CalendarSerializer(serializers.ModelSerializer):
"""Сериализатор для Calendar (django-scheduler) с совместимостью EUSRR."""
# Совместимость с EUSRR API - принимаем title, конвертируем в name
title = serializers.CharField(source='name', required=False)
events_count = serializers.SerializerMethodField()
owner = serializers.SerializerMethodField()
participants = serializers.SerializerMethodField()
user_role = serializers.SerializerMethodField()
class Meta:
model = Calendar
fields = ['id', 'name', 'title', 'slug', 'events_count', 'owner', 'participants', 'user_role']
read_only_fields = ['id', 'owner', 'participants', 'user_role']
extra_kwargs = {
'slug': {'required': False}, # Сделаем auto-generated если не указан
}
def get_events_count(self, obj):
"""Количество событий в календаре."""
return obj.event_set.count()
def get_owner(self, obj):
"""Получить владельца календаря."""
try:
from django.contrib.auth import get_user_model
User = get_user_model()
ct = ContentType.objects.get_for_model(User)
owner_relation = CalendarRelation.objects.filter(
calendar=obj,
content_type=ct,
distinction='owner'
).first()
if owner_relation and owner_relation.content_object:
return {
'id': owner_relation.content_object.id,
'username': getattr(owner_relation.content_object, 'username', str(owner_relation.content_object)),
'full_name': str(owner_relation.content_object),
}
except Exception:
pass
return None
def get_participants(self, obj):
"""Список всех участников календаря."""
from django.contrib.auth import get_user_model
User = get_user_model()
ct = ContentType.objects.get_for_model(User)
relations = CalendarRelation.objects.filter(
calendar=obj,
content_type=ct
).select_related('content_type')
participants = []
for rel in relations:
if rel.content_object:
participants.append({
'id': rel.content_object.id,
'username': getattr(rel.content_object, 'username', str(rel.content_object)),
'full_name': str(rel.content_object),
'role': rel.distinction,
})
return participants
def get_user_role(self, obj):
"""Роль текущего пользователя для этого календаря."""
request = self.context.get('request')
if not request or not request.user.is_authenticated:
return None
from django.contrib.auth import get_user_model
User = get_user_model()
ct = ContentType.objects.get_for_model(User)
relation = CalendarRelation.objects.filter(
calendar=obj,
content_type=ct,
object_id=request.user.id
).first()
return relation.distinction if relation else None
def create(self, validated_data):
"""Создать календарь с автогенерацией slug из name/title."""
from django.utils.text import slugify
# Если slug не указан - генерируем из name
if 'slug' not in validated_data or not validated_data.get('slug'):
name = validated_data.get('name', 'calendar')
base_slug = slugify(name)
if not base_slug:
base_slug = 'calendar'
# Проверяем уникальность slug
slug = base_slug
counter = 1
while Calendar.objects.filter(slug=slug).exists():
slug = f"{base_slug}-{counter}"
counter += 1
validated_data['slug'] = slug
return super().create(validated_data)
class RuleSerializer(serializers.ModelSerializer):
"""Сериализатор для Rule (rrule RFC 5545)."""
# Удобные поля для frontend
frequency_display = serializers.CharField(source='get_frequency_display', read_only=True)
class Meta:
model = Rule
fields = ['id', 'name', 'description', 'frequency', 'frequency_display', 'params']
read_only_fields = ['id', 'frequency_display']
def to_representation(self, instance):
"""Десериализация params из JSON-строки в dict."""
import json
data = super().to_representation(instance)
# Парсим params из строки в объект для frontend
if data.get('params'):
try:
if isinstance(data['params'], str):
data['params'] = json.loads(data['params'])
except (json.JSONDecodeError, TypeError):
data['params'] = {}
else:
data['params'] = {}
return data
def validate_frequency(self, value):
"""Валидация frequency."""
valid_frequencies = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
if value not in valid_frequencies:
raise serializers.ValidationError(
f'Invalid frequency. Must be one of: {", ".join(valid_frequencies)}'
)
return value
class EventSerializer(serializers.ModelSerializer):
"""Сериализатор для Event (django-scheduler)."""
calendar_name = serializers.CharField(source='calendar.name', read_only=True)
rule_description = serializers.CharField(source='rule.description', read_only=True, allow_null=True)
class Meta:
model = Event
fields = [
'id',
'title',
'description',
'start',
'end',
'calendar',
'calendar_name',
'rule',
'rule_description',
'end_recurring_period',
'color_event',
'creator',
'created_on',
'updated_on',
]
read_only_fields = ['id', 'created_on', 'updated_on']
def validate(self, attrs):
"""Валидация start/end."""
start = attrs.get('start')
end = attrs.get('end')
if end and start and end < start:
raise serializers.ValidationError({
'end': 'Дата окончания не может быть раньше даты начала.'
})
return attrs
class EventListSerializer(EventSerializer):
"""Упрощённый сериализатор для списка событий."""
class Meta(EventSerializer.Meta):
fields = [
'id',
'title',
'description',
'start',
'end',
'calendar',
'calendar_name',
'color_event',
'rule',
'end_recurring_period',
]
class OccurrenceSerializer(serializers.ModelSerializer):
"""Сериализатор для Occurrence (материализованные вхождения)."""
event_title = serializers.CharField(source='event.title', read_only=True)
event_id = serializers.IntegerField(source='event.id', read_only=True)
class Meta:
model = Occurrence
fields = [
'id',
'event',
'event_id',
'event_title',
'title',
'description',
'start',
'end',
'cancelled',
'original_start',
'original_end',
'created_on',
'updated_on',
]
read_only_fields = ['id', 'created_on', 'updated_on']
class EventRelationSerializer(serializers.ModelSerializer):
"""Сериализатор для EventRelation (участники встреч)."""
event_title = serializers.CharField(source='event.title', read_only=True)
user_name = serializers.SerializerMethodField()
content_type = serializers.PrimaryKeyRelatedField(
queryset=ContentType.objects.all(),
required=False # Делаем необязательным
)
class Meta:
model = EventRelation
fields = [
'id',
'event',
'event_title',
'content_type',
'object_id',
'distinction',
'user_name',
]
read_only_fields = ['id']
def get_user_name(self, obj):
"""Имя связанного объекта."""
if obj.content_object:
return str(obj.content_object)
return None
def create(self, validated_data):
"""Автоматически устанавливает content_type для User."""
if 'content_type' not in validated_data:
from django.contrib.auth import get_user_model
User = get_user_model()
validated_data['content_type'] = ContentType.objects.get_for_model(User)
return super().create(validated_data)