- Добавлена функция toLocalDateTimeString для корректной конвертации UTC в локальное время - Исправлено отображение времени в модальном окне событий (было 21:00 UTC вместо 00:00 MSK) - Упрощена логика datetime-local inputs (удалена сложная условная логика) - Добавлен allDayAccessor в react-big-calendar для корректного отображения событий - Унифицирован EventModal для обоих календарей (~718 строк удалено)
324 lines
12 KiB
Python
324 lines
12 KiB
Python
# 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)
|