293 lines
11 KiB
Python
Raw Permalink 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/views.py
"""
ViewSets для django-scheduler (чистый, проверенный код).
Параллельно работают со старым calendar_app.
"""
from django.contrib.contenttypes.models import ContentType
from django.db.models import Q
from django.utils.dateparse import parse_datetime
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from schedule.models import (
Calendar,
Event,
EventRelation,
)
from schedule.periods import Period
from scheduling.models import CalendarBinding
from scheduling.services import (
filter_events_for_user,
user_can_delete_event,
user_can_edit_event,
user_can_view_calendar,
)
from ..permissions import (
CanEditCalendar,
)
from ..serializers import (
EventListSerializer,
EventSerializer,
)
from .common import _admin_requested_all_scope, _user_accessible_calendar_ids
class ScheduleEventViewSet(viewsets.ModelViewSet):
"""CRUD для django-scheduler Event.
Endpoints:
- GET /api/v1/schedule/events/ - список
- GET /api/v1/schedule/events/?calendar=1 - события календаря
- GET /api/v1/schedule/events/?start=2026-02-01&end=2026-03-01 - в диапазоне
- POST /api/v1/schedule/events/ - создать
- GET /api/v1/schedule/events/{id}/ - детали
- PUT/PATCH /api/v1/schedule/events/{id}/ - обновить
- DELETE /api/v1/schedule/events/{id}/ - удалить
"""
queryset = Event.objects.select_related(
"calendar",
"calendar__binding",
"calendar__binding__context_content_type",
"rule",
"creator",
)
permission_classes = [IsAuthenticated, CanEditCalendar]
renderer_classes = [JSONRenderer]
def get_serializer_class(self):
"""Выбор сериализатора."""
if self.action == "list":
return EventListSerializer
return EventSerializer
def get_queryset(self):
"""Фильтрация событий.
Пользователь видит события из:
1. Календарей, где он участник (CalendarRelation)
2. Событий, где он участник (EventRelation)
"""
from django.contrib.auth import get_user_model
User = get_user_model()
qs = (
super()
.get_queryset()
.select_related(
"calendar",
"calendar__binding",
"calendar__binding__context_content_type",
"rule",
"creator",
)
)
user = self.request.user
calendar_id = self.request.query_params.get("calendar")
show_all_scope = _admin_requested_all_scope(self.request)
# Полный обзор для админа включается только явным scope=all
# либо на detail actions, где нужен прямой доступ.
if not ((user.is_staff or user.is_superuser) and show_all_scope):
from django.contrib.auth import get_user_model
User = get_user_model()
ct = ContentType.objects.get_for_model(User)
accessible_calendar_ids = _user_accessible_calendar_ids(user)
participant_event_ids = EventRelation.objects.filter(
content_type=ct,
object_id=user.id,
).values_list("event_id", flat=True)
visibility_filter = Q(calendar_id__in=accessible_calendar_ids) | Q(
id__in=participant_event_ids
)
if self.action != "list":
visibility_filter |= Q(
calendar__binding__type=CalendarBinding.BindingType.DEPARTMENT
)
if calendar_id:
try:
calendar = Calendar.objects.select_related("binding").get(
id=calendar_id
)
except Calendar.DoesNotExist:
calendar = None
if calendar and user_can_view_calendar(user, calendar):
visibility_filter |= Q(calendar_id=calendar.id)
qs = qs.filter(visibility_filter)
# Фильтр по календарю
if calendar_id:
qs = qs.filter(calendar_id=calendar_id)
if search := self.request.query_params.get("search", "").strip():
qs = qs.filter(
Q(title__icontains=search) | Q(description__icontains=search)
)
# Фильтр по диапазону дат
start_str = self.request.query_params.get("start")
end_str = self.request.query_params.get("end")
if start_str and end_str:
start_dt = parse_datetime(start_str)
end_dt = parse_datetime(end_str)
if start_dt and end_dt:
# События, которые попадают в диапазон
qs = qs.filter(start__lte=end_dt, end__gte=start_dt)
return qs.order_by("start")
def perform_create(self, serializer):
"""Установка creator при создании."""
serializer.save(creator=self.request.user)
@action(detail=False, methods=["get"], url_path="occurrences")
def occurrences(self, request):
"""Получить материализованные вхождения в диапазоне.
Query params:
- calendar: ID календаря (необязательно,
если не указан - все доступные календари)
- start: начало диапазона (ISO 8601)
- end: конец диапазона (ISO 8601)
Возвращает все вхождения recurring событий + обычные события.
"""
calendar_id = request.query_params.get("calendar")
start_str = request.query_params.get("start")
end_str = request.query_params.get("end")
if not all([start_str, end_str]):
return Response(
{"detail": "Требуются параметры: start, end"},
status=status.HTTP_400_BAD_REQUEST,
)
start_dt = parse_datetime(start_str)
end_dt = parse_datetime(end_str)
if not start_dt or not end_dt:
return Response(
{"detail": "Некорректный формат дат (используйте ISO 8601)"},
status=status.HTTP_400_BAD_REQUEST,
)
events = filter_events_for_user(
Event.objects.select_related("calendar", "rule"),
request.user,
allow_admin_all=_admin_requested_all_scope(request),
)
# Если указан конкретный календарь - фильтруем по нему. Прямое
# приглашение в событие также даёт доступ к его occurrences, даже если
# пользователь не состоит в календаре целиком.
if calendar_id:
try:
calendar = Calendar.objects.select_related("binding").get(
id=calendar_id
)
except Calendar.DoesNotExist:
return Response(
{"detail": "Календарь не найден"},
status=status.HTTP_404_NOT_FOUND,
)
events = events.filter(calendar_id=calendar.id)
if (
not user_can_view_calendar(request.user, calendar)
and not events.exists()
):
return Response(
{"detail": "Нет доступа к календарю"},
status=status.HTTP_403_FORBIDDEN,
)
# Результат
occurrences = []
period = Period(events, start_dt, end_dt)
for occurrence in period.get_occurrences():
# Создаем уникальный ID для каждого вхождения
# (комбинация event_id и start)
unique_id = f"{occurrence.event.id}_{occurrence.start.isoformat()}"
occurrences.append(
{
"id": unique_id, # Уникальный ID для каждого вхождения
"title": occurrence.title or occurrence.event.title,
"description": occurrence.event.description,
"start": occurrence.start.isoformat(),
"end": occurrence.end.isoformat(),
"calendar": occurrence.event.calendar_id,
"color_event": occurrence.event.color_event,
# ID оригинального события
"event_id": occurrence.event.id,
"can_edit": user_can_edit_event(request.user, occurrence.event),
"can_delete": user_can_delete_event(request.user, occurrence.event),
"is_recurring": occurrence.event.rule_id is not None,
"rule": occurrence.event.rule_id,
"end_recurring_period": (
occurrence.event.end_recurring_period.isoformat()
if occurrence.event.end_recurring_period
else None
),
}
)
return Response(occurrences)
@action(detail=False, methods=["get"], url_path="my-events")
def my_events(self, request):
"""Получить события текущего пользователя (где он участник).
Query params:
- start: начало диапазона (ISO 8601, необязательно)
- end: конец диапазона (ISO 8601, необязательно)
Возвращает только события,
где пользователь является участником (EventRelation).
"""
from django.contrib.auth import get_user_model
User = get_user_model()
user = request.user
ct = ContentType.objects.get_for_model(User)
# ID событий где пользователь участник
participant_event_ids = EventRelation.objects.filter(
content_type=ct, object_id=user.id
).values_list("event_id", flat=True)
# Фильтруем события
qs = self.queryset.filter(id__in=participant_event_ids)
# Фильтр по диапазону дат (если указан)
start_str = request.query_params.get("start")
end_str = request.query_params.get("end")
if start_str and end_str:
start_dt = parse_datetime(start_str)
end_dt = parse_datetime(end_str)
if start_dt and end_dt:
qs = qs.filter(start__lte=end_dt, end__gte=start_dt)
qs = qs.order_by("start")
# Используем EventListSerializer для детальной информации
serializer = EventListSerializer(qs, many=True)
return Response(serializer.data)