Corp/backend/api/v1/schedule/serializers.py
igor_nadein e7e0e2e290 feat: детальное отображение параметров повторяющихся событий
Backend:
- Добавлено поле rule_data в EventSerializer
- get_rule_data() возвращает частоту, параметры и описание правила
- Использует rule.get_params() для получения распарсенных параметров

Frontend:
- Добавлены константы FREQUENCY_LABELS и WEEKDAY_LABELS для русификации
- Новый блок с детальной информацией о повторении в синем фоне
- Отображается частота (WEEKLY, MONTHLY и т.д.)
- Дни недели для еженедельных событий (ПН, ВТ, СР...)
- Количество повторений (count)
- Дата окончания повторения (end_recurring_period)
- Подсказка о невозможности редактирования параметров

Проблема: пользователь не видел детали повторяющихся событий.
Решение: красивый информационный блок с полной информацией о правиле.
2026-02-25 02:20:24 +03:00

442 lines
16 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: dict {"byweekday": [0, 4], "interval": 1}
- django-scheduler: string "byweekday:0,4;interval:1"
"""
VALID_FREQUENCIES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
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 _dict_to_params_string(self, params_dict):
"""
Преобразует dict в строковый формат django-scheduler.
Args:
params_dict: {"byweekday": [0, 3], "interval": 1}
Returns:
"byweekday:0,3;interval:1"
"""
if not params_dict or not isinstance(params_dict, dict):
return ""
parts = []
for key, value in params_dict.items():
if isinstance(value, (list, tuple)):
value_str = ",".join(str(v) for v in value)
else:
value_str = str(value)
parts.append(f"{key}:{value_str}")
return ";".join(parts)
def _params_string_to_dict(self, params_str):
"""
Преобразует строку django-scheduler в dict.
Args:
params_str: "byweekday:0,3;interval:1"
Returns:
{"byweekday": [0, 3], "interval": 1}
"""
if not params_str:
return {}
result = {}
for param_pair in params_str.split(";"):
if ":" not in param_pair:
continue
key, value_str = param_pair.split(":", 1)
key = key.strip()
# Если значение содержит запятые - это список
if "," in value_str:
try:
result[key] = [int(v.strip()) for v in value_str.split(",")]
except ValueError:
result[key] = [v.strip() for v in value_str.split(",")]
else:
# Пробуем преобразовать в число
try:
result[key] = int(value_str.strip())
except ValueError:
result[key] = value_str.strip()
return result
def _parse_params_input(self, params):
"""
Парсит params из любого формата в dict.
Поддерживает:
- dict (прямо от JSON)
- JSON-string (от некоторых клиентов)
- django-scheduler string (для обратной совместимости)
Args:
params: dict, JSON-string или django-scheduler string
Returns:
dict с параметрами
"""
import json
# Уже dict - возвращаем как есть
if isinstance(params, dict):
return params
# Пробуем распарсить как JSON-строку
if isinstance(params, str):
# Если начинается с { - это JSON
if params.strip().startswith('{'):
try:
return json.loads(params)
except (json.JSONDecodeError, TypeError):
pass
# Иначе пробуем как django-scheduler формат
return self._params_string_to_dict(params)
return {}
def to_representation(self, instance):
"""Преобразует params из БД (django-scheduler format) в dict для frontend."""
data = super().to_representation(instance)
data['params'] = self._params_string_to_dict(data.get('params', ''))
return data
def to_internal_value(self, data):
"""Преобразует params от frontend (dict/JSON) в django-scheduler format для БД."""
# Парсим params в dict из любого формата
params_dict = self._parse_params_input(data.get('params'))
# Вызываем родительский метод для валидации остальных полей
validated_data = super().to_internal_value(data)
# Конвертируем dict в формат django-scheduler для сохранения в БД
validated_data['params'] = self._dict_to_params_string(params_dict)
return validated_data
def validate_frequency(self, value):
"""Валидация поля frequency."""
if value not in self.VALID_FREQUENCIES:
raise serializers.ValidationError(
f'Invalid frequency. Must be one of: {", ".join(self.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)
rule_data = serializers.SerializerMethodField()
class Meta:
model = Event
fields = [
'id',
'title',
'description',
'start',
'end',
'calendar',
'calendar_name',
'rule',
'rule_description',
'rule_data',
'end_recurring_period',
'color_event',
'creator',
'created_on',
'updated_on',
]
read_only_fields = ['id', 'created_on', 'updated_on']
def get_rule_data(self, obj):
"""Возвращает детальную информацию о правиле повторения."""
if not obj.rule:
return None
rule = obj.rule
params = rule.get_params()
return {
'frequency': rule.frequency,
'params': params,
'description': rule.description,
}
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)