Реализовано: 1. Множественный выбор календарей через чекбоксы - Заменил <select> на чекбоксы для выбора нескольких календарей - События создаются одновременно во всех выбранных календарях - При редактировании - обновляется только первый выбранный - Цветовые индикаторы для каждого календаря 2. Современный дизайн событий - Двухцветная схема: левая граница = цвет календаря, фон = цвет события - Полупрозрачный фон события (80%) для лучшей видимости - Тени и hover эффекты для объёма - Закругленные углы (4px) - Плавные анимации (transform + transition) 3. Улучшенное отображение накладывающихся событий - dayMaxEvents: автоматически показывать '+N еще' - dayMaxEventRows: 4 - максимум 4 строки на день - moreLinkClick: 'popover' - красивый popover для остальных - eventMaxStack: 3 - до 3 событий в стеке - eventOrder: сортировка по началу, длительности, алфавиту 4. Новый CSS файл calendar-improvements.css - Современные стили для FullCalendar - Улучшенный popover с градиентом - Стили для выходных дней (розоватый фон) - Hover эффекты на события - Анимация fadeInEvent при загрузке - Улучшенная читаемость текущего дня - Адаптивность для мобильных устройств - Кастомные scrollbar для списков 5. Передача цвета календаря в события - calendar_color добавляется в extendedProps - Используется __calendar из интеграции - Отдельная обработка eventColor и calendarColor 6. Визуальные улучшения - Текст события: font-weight 500, line-height 1.3 - Отступы: paddingLeft 6px (с цветом календаря) - Тень текста для читаемости на любом фоне - Паттерн для ежегодных событий (диагональные полосы) - Индикатор текущего дня: синий круг с белым текстом Результат: ✅ События визуально различимы по календарям (левая граница) ✅ Сохранён индивидуальный цвет события (основной фон) ✅ Множество событий в день отображается компактно (+N еще) ✅ Современный Material Design стиль ✅ Плавные анимации и hover эффекты ✅ Улучшенная читаемость и UX
686 lines
28 KiB
Python
686 lines
28 KiB
Python
from __future__ import annotations
|
||
|
||
import logging
|
||
from datetime import date
|
||
from typing import Any, Dict, List, Optional, Type
|
||
|
||
from calendar_app.models import CalendarEvent
|
||
from calendar_app.services.recurrence import expand_event_occurrences
|
||
from django.core.cache import cache
|
||
from django.http import Http404
|
||
from django.utils.dateparse import parse_date
|
||
from employees.constants import DeptPerm
|
||
from rest_framework import serializers, status
|
||
from rest_framework.decorators import action as rest_action
|
||
from rest_framework.permissions import IsAuthenticated
|
||
from rest_framework.renderers import JSONRenderer
|
||
from rest_framework.response import Response
|
||
from rest_framework.viewsets import ModelViewSet
|
||
|
||
from ..permissions import AdminOrDeptAllowed
|
||
from .serializers import (
|
||
CalendarEventSerializer,
|
||
CalendarEventWriteSerializer,
|
||
CalendarSerializer,
|
||
CalendarSubscriptionSerializer,
|
||
CalendarSubscriptionWriteSerializer,
|
||
CalendarWriteSerializer,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class CalendarEventsViewSet(ModelViewSet):
|
||
"""Единый CRUD по событиям календаря (компания/отдел) с развёрткой повторяемости.
|
||
|
||
list:
|
||
Возвращает МАТЕРИАЛИЗОВАННЫЕ вхождения в окне [start, end).
|
||
Если указан department_id — события отдела, иначе — компании.
|
||
|
||
retrieve/create/update/partial_update/destroy:
|
||
Работают с объектом CalendarEvent.
|
||
"""
|
||
|
||
queryset = CalendarEvent.objects.select_related("department").order_by(
|
||
"start_date", "start_time"
|
||
)
|
||
renderer_classes = [JSONRenderer]
|
||
# ВАЖНО: Переопределяем глобальные DEFAULT_PERMISSION_CLASSES
|
||
# Используем только get_permissions() для динамического определения прав
|
||
permission_classes = []
|
||
|
||
# ---------- Внутренний пермишен для отделов ----------
|
||
|
||
class ManageCalendarPerm(AdminOrDeptAllowed):
|
||
"""Право на управление календарём отдела (для компании молчит)."""
|
||
|
||
required_code = DeptPerm.MANAGE_CALENDAR
|
||
|
||
# ---------- Подключение пермишенов ----------
|
||
|
||
def get_permissions(self):
|
||
"""Настройка прав доступа в зависимости от действия.
|
||
|
||
- list, retrieve: доступно всем авторизованным пользователям
|
||
- create, update, partial_update, destroy для компании: требуется staff/superuser
|
||
- create, update, partial_update, destroy для отдела: требуется MANAGE_CALENDAR
|
||
- create, update, partial_update, destroy для личного календаря: только владелец
|
||
"""
|
||
action = getattr(self, "action", None)
|
||
method = self.request.method if hasattr(self, "request") else None
|
||
|
||
# Если action ещё не установлен, определяем по методу
|
||
if action is None:
|
||
if method in ["GET", "HEAD", "OPTIONS"]:
|
||
return [IsAuthenticated()]
|
||
elif method == "POST":
|
||
action = "create"
|
||
elif method in ["PUT", "PATCH"]:
|
||
action = "update"
|
||
elif method == "DELETE":
|
||
action = "destroy"
|
||
|
||
# Просмотр событий доступен всем авторизованным пользователям
|
||
if action in ["list", "retrieve"]:
|
||
return [IsAuthenticated()]
|
||
|
||
# Для изменений требуются права управления календарём
|
||
emp = self._employee_id(required=False)
|
||
dep = self._dept_id(required=False)
|
||
|
||
# Личный календарь — полный контроль только у владельца
|
||
if emp is not None:
|
||
# Проверка, что текущий пользователь = владелец календаря
|
||
if self.request.user.id == emp:
|
||
return [IsAuthenticated()]
|
||
else:
|
||
# Другой пользователь не может изменять чужой личный календарь
|
||
from rest_framework.permissions import BasePermission
|
||
|
||
class DenyAll(BasePermission):
|
||
def has_permission(self, request, view):
|
||
return False
|
||
|
||
return [DenyAll()]
|
||
|
||
# Календарь компании — только администраторы
|
||
if dep is None:
|
||
from rest_framework.permissions import IsAdminUser
|
||
|
||
return [IsAdminUser()]
|
||
|
||
# Календарь отдела — требуется право MANAGE_CALENDAR
|
||
self.kwargs = dict(self.kwargs)
|
||
self.kwargs.setdefault("department_id", dep)
|
||
return [self.ManageCalendarPerm()]
|
||
|
||
# ---------- Сериализаторы ----------
|
||
|
||
def get_serializer_class(self) -> Type[serializers.Serializer]:
|
||
"""Выбор сериализатора по действию.
|
||
|
||
Returns:
|
||
Type[serializers.Serializer]: Класс сериализатора.
|
||
"""
|
||
if self.action in {"create", "update", "partial_update"}:
|
||
return CalendarEventWriteSerializer
|
||
return CalendarEventSerializer
|
||
|
||
# ---------- Утилиты ----------
|
||
|
||
def _dept_id(self, *, required: bool = False) -> Optional[int]:
|
||
"""Читает department_id из kwargs/query/body.
|
||
|
||
ВАЖНО: *не* путать pk события с department_id.
|
||
|
||
Args:
|
||
required (bool): Если True — бросить 404 при отсутствии/ошибке.
|
||
|
||
Raises:
|
||
Http404: При некорректном или отсутствующем department_id (когда required=True).
|
||
|
||
Returns:
|
||
Optional[int]: PK отдела или None (компания или личный календарь).
|
||
"""
|
||
# nested-url вида: /departments/{department_id}/events/
|
||
for key in ("department_pk", "department_id"):
|
||
if key in self.kwargs:
|
||
try:
|
||
return int(self.kwargs[key])
|
||
except Exception:
|
||
raise Http404("Некорректный department_id в URL.")
|
||
|
||
req = getattr(self, "request", None)
|
||
if req is not None:
|
||
for key in ("department_id", "department"):
|
||
val = req.query_params.get(key)
|
||
if val is not None:
|
||
try:
|
||
return int(val)
|
||
except Exception:
|
||
raise Http404("Некорректный department_id в query.")
|
||
|
||
data = getattr(req, "data", {}) if req is not None else {}
|
||
if isinstance(data, dict):
|
||
for key in ("department_id", "department"):
|
||
if key in data and data[key] is not None:
|
||
try:
|
||
return int(data[key])
|
||
except Exception:
|
||
raise Http404("Некорректный department_id в теле запроса.")
|
||
|
||
if required:
|
||
raise Http404("Не указан department_id.")
|
||
return None
|
||
|
||
def _employee_id(self, *, required: bool = False) -> Optional[int]:
|
||
"""Читает employee_id из kwargs/query/body для личного календаря.
|
||
|
||
Args:
|
||
required (bool): Если True — бросить 404 при отсутствии/ошибке.
|
||
|
||
Raises:
|
||
Http404: При некорректном или отсутствующем employee_id (когда required=True).
|
||
|
||
Returns:
|
||
Optional[int]: PK сотрудника или None (компания/отдел).
|
||
"""
|
||
# nested-url вида: /employees/{employee_id}/events/
|
||
for key in ("employee_pk", "employee_id"):
|
||
if key in self.kwargs:
|
||
try:
|
||
return int(self.kwargs[key])
|
||
except Exception:
|
||
raise Http404("Некорректный employee_id в URL.")
|
||
|
||
req = getattr(self, "request", None)
|
||
if req is not None:
|
||
for key in ("employee_id", "employee"):
|
||
val = req.query_params.get(key)
|
||
if val is not None:
|
||
try:
|
||
return int(val)
|
||
except Exception:
|
||
raise Http404("Некорректный employee_id в query.")
|
||
|
||
data = getattr(req, "data", {}) if req is not None else {}
|
||
if isinstance(data, dict):
|
||
for key in ("employee_id", "employee"):
|
||
if key in data and data[key] is not None:
|
||
try:
|
||
return int(data[key])
|
||
except Exception:
|
||
raise Http404("Некорректный employee_id в теле запроса.")
|
||
|
||
if required:
|
||
raise Http404("Не указан employee_id.")
|
||
return None
|
||
|
||
# ---------- QuerySet ----------
|
||
|
||
def get_queryset(self):
|
||
"""QS для list фильтруем по компании/отделу/сотруднику/календарю, для detail — не фильтруем.
|
||
|
||
Returns:
|
||
QuerySet: Набор для текущего действия.
|
||
"""
|
||
qs = super().get_queryset()
|
||
if getattr(self, "action", None) != "list":
|
||
# detail — нужен полный QS, чтобы по pk находить любой объект
|
||
return qs
|
||
|
||
# Новая архитектура: calendar_id имеет приоритет
|
||
calendar_id = self.request.query_params.get("calendar_id")
|
||
if calendar_id is not None:
|
||
try:
|
||
return qs.filter(calendar_id=int(calendar_id))
|
||
except ValueError:
|
||
raise Http404("Некорректный calendar_id.")
|
||
|
||
# Legacy логика: department_id / employee_id / company
|
||
dep = self._dept_id(required=False)
|
||
emp = self._employee_id(required=False)
|
||
|
||
# Приоритет: employee_id > department_id > company
|
||
if emp is not None:
|
||
# Личный календарь сотрудника (legacy)
|
||
return qs.filter(
|
||
employee_id=emp, department__isnull=True, calendar__isnull=True
|
||
)
|
||
elif dep is not None:
|
||
# Календарь отдела (legacy)
|
||
return qs.filter(
|
||
department_id=dep, employee__isnull=True, calendar__isnull=True
|
||
)
|
||
else:
|
||
# Календарь компании (legacy)
|
||
return qs.filter(
|
||
department__isnull=True, employee__isnull=True, calendar__isnull=True
|
||
)
|
||
|
||
# ---------- LIST: материализация повторов ----------
|
||
|
||
def list(self, request, *args, **kwargs):
|
||
"""Возвращает материализованные вхождения в окне [start, end).
|
||
|
||
Query params:
|
||
start (YYYY-MM-DD) — левая граница (включительно).
|
||
end (YYYY-MM-DD) — правая граница (исключительно).
|
||
|
||
Returns:
|
||
Response: Массив объектов для FullCalendar.
|
||
"""
|
||
start_str = request.query_params.get("start")
|
||
end_str = request.query_params.get("end")
|
||
if not start_str or not end_str:
|
||
return Response([], status=status.HTTP_200_OK)
|
||
|
||
r_start: Optional[date] = parse_date(start_str[:10])
|
||
r_end: Optional[date] = parse_date(end_str[:10])
|
||
if not r_start or not r_end:
|
||
return Response(
|
||
{"detail": "Некорректные параметры start/end."},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
if r_start >= r_end:
|
||
return Response([], status=status.HTTP_200_OK)
|
||
|
||
items: List[Dict[str, Any]] = []
|
||
for ev in self.get_queryset():
|
||
for oc in expand_event_occurrences(
|
||
ev, range_start=r_start, range_end=r_end, max_instances=1000
|
||
):
|
||
items.append(
|
||
{
|
||
"id": ev.id, # идентификатор «родителя»; можно заменить на oc.id, если нужен уникальный на каждое вхождение
|
||
"title": ev.title,
|
||
"start": oc.start.isoformat(),
|
||
"end": oc.end.isoformat(),
|
||
"allDay": oc.all_day,
|
||
"color": ev.color or None,
|
||
"recurrence": ev.recurrence,
|
||
"department_id": ev.department_id,
|
||
}
|
||
)
|
||
return Response(items, status=status.HTTP_200_OK)
|
||
|
||
# ---------- Создание/изменение/удаление ----------
|
||
|
||
def perform_create(self, serializer: CalendarEventWriteSerializer) -> None:
|
||
"""Сохраняет объект, подставляя department_id/employee_id из контекста и автора.
|
||
|
||
Raises:
|
||
ValidationError: Если сериализатор невалиден (поднимет сам DRF).
|
||
"""
|
||
dep = self._dept_id(required=False)
|
||
emp = self._employee_id(required=False)
|
||
|
||
# Приоритет: employee_id > department_id
|
||
if emp is not None:
|
||
serializer.save(
|
||
employee_id=emp,
|
||
department_id=None,
|
||
created_by=self.request.user,
|
||
)
|
||
else:
|
||
serializer.save(
|
||
department_id=dep
|
||
if dep is not None
|
||
else serializer.validated_data.get("department_id"),
|
||
employee_id=None,
|
||
created_by=self.request.user,
|
||
)
|
||
cache.clear()
|
||
|
||
def perform_update(self, serializer: CalendarEventWriteSerializer) -> None:
|
||
"""Обновляет событие и чистит кеш."""
|
||
serializer.save()
|
||
cache.clear()
|
||
|
||
def perform_destroy(self, instance: CalendarEvent) -> None:
|
||
"""Удаляет событие и чистит кеш."""
|
||
instance.delete()
|
||
cache.clear()
|
||
|
||
# ---------- Проверка прав ----------
|
||
|
||
@rest_action(
|
||
detail=True,
|
||
methods=["get"],
|
||
permission_classes=[IsAuthenticated],
|
||
url_path="permissions",
|
||
)
|
||
def permissions(self, request, pk=None):
|
||
"""Проверяет права пользователя на редактирование/удаление события.
|
||
|
||
Returns:
|
||
Response: {
|
||
"can_edit": bool,
|
||
"can_delete": bool,
|
||
"can_view": bool (всегда True для авторизованных)
|
||
}
|
||
"""
|
||
event = self.get_object()
|
||
user = request.user
|
||
|
||
# Просмотр доступен всем авторизованным
|
||
can_view = True
|
||
|
||
# Права на редактирование и удаление
|
||
can_edit = False
|
||
can_delete = False
|
||
|
||
# Проверка прав
|
||
if user.is_superuser or user.is_staff:
|
||
# Админы могут всё
|
||
can_edit = True
|
||
can_delete = True
|
||
elif event.employee_id is not None:
|
||
# Личное событие — редактировать может только владелец
|
||
if event.employee_id == user.id:
|
||
can_edit = True
|
||
can_delete = True
|
||
elif event.department_id is None:
|
||
# Событие компании — только админы (уже проверено выше)
|
||
pass
|
||
else:
|
||
# Событие отдела — проверяем право MANAGE_CALENDAR
|
||
try:
|
||
from employees.models import EmployeeDepartment
|
||
|
||
# Проверяем, является ли пользователь руководителем отдела
|
||
if hasattr(event.department, "head_id"):
|
||
if event.department.head_id == user.id:
|
||
can_edit = True
|
||
can_delete = True
|
||
|
||
# Проверяем наличие права MANAGE_CALENDAR
|
||
if not can_edit:
|
||
has_perm = EmployeeDepartment.objects.filter(
|
||
employee_id=user.id,
|
||
department_id=event.department_id,
|
||
is_active=True,
|
||
role__scoped_permissions__code=DeptPerm.MANAGE_CALENDAR,
|
||
).exists()
|
||
|
||
if has_perm:
|
||
can_edit = True
|
||
can_delete = True
|
||
except Exception as e:
|
||
logger.error(f"Error checking permissions: {e}")
|
||
|
||
return Response(
|
||
{"can_view": can_view, "can_edit": can_edit, "can_delete": can_delete}
|
||
)
|
||
|
||
|
||
class CalendarViewSet(ModelViewSet):
|
||
"""CRUD операции для календарей.
|
||
|
||
Доступные действия:
|
||
- list: список календарей (доступные текущему пользователю)
|
||
- create: создание нового календаря (только владельцы/админы)
|
||
- retrieve: просмотр календаря
|
||
- update/partial_update: обновление календаря (только владелец)
|
||
- destroy: удаление календаря (только владелец)
|
||
- subscribe: подписка на календарь
|
||
- unsubscribe: отписка от календаря
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
renderer_classes = [JSONRenderer]
|
||
|
||
def get_queryset(self):
|
||
"""Возвращает календари, доступные текущему пользователю."""
|
||
from calendar_app.models import Calendar
|
||
|
||
user = self.request.user
|
||
|
||
if self.action == "list":
|
||
# Для списка показываем только доступные календари
|
||
return Calendar.objects.get_available_for_user(user)
|
||
|
||
# Для остальных действий возвращаем все календари (права проверяются в permissions)
|
||
return Calendar.objects.all()
|
||
|
||
def get_serializer_class(self):
|
||
"""Выбор сериализатора по действию."""
|
||
if self.action in {"create", "update", "partial_update"}:
|
||
return CalendarWriteSerializer
|
||
return CalendarSerializer
|
||
|
||
def get_permissions(self):
|
||
"""Настройка прав доступа."""
|
||
if self.action in ["list", "retrieve"]:
|
||
return [IsAuthenticated()]
|
||
elif self.action in ["create"]:
|
||
# Создавать календари могут все авторизованные пользователи
|
||
return [IsAuthenticated()]
|
||
elif self.action in ["update", "partial_update", "destroy"]:
|
||
# Изменять/удалять могут только владельцы
|
||
return [IsAuthenticated()]
|
||
else:
|
||
return [IsAuthenticated()]
|
||
|
||
def create(self, request, *args, **kwargs):
|
||
"""Создание календаря с корректным сериализатором для ответа."""
|
||
write_serializer = self.get_serializer(data=request.data)
|
||
write_serializer.is_valid(raise_exception=True)
|
||
self.perform_create(write_serializer)
|
||
|
||
# Для ответа используем CalendarSerializer
|
||
instance = write_serializer.instance
|
||
read_serializer = CalendarSerializer(instance, context={"request": request})
|
||
|
||
from rest_framework import status
|
||
from rest_framework.response import Response
|
||
|
||
headers = self.get_success_headers(read_serializer.data)
|
||
return Response(
|
||
read_serializer.data, status=status.HTTP_201_CREATED, headers=headers
|
||
)
|
||
|
||
def perform_create(self, serializer):
|
||
"""Создание календаря."""
|
||
# Владелец определяется в serializer.save()
|
||
serializer.save()
|
||
cache.clear()
|
||
|
||
def perform_update(self, serializer):
|
||
"""Обновление календаря."""
|
||
# Проверяем права владельца
|
||
calendar = self.get_object()
|
||
user = self.request.user
|
||
|
||
if not (user.is_superuser or user.is_staff or calendar.is_owner(user)):
|
||
from rest_framework.exceptions import PermissionDenied
|
||
|
||
raise PermissionDenied("Только владелец может изменять календарь.")
|
||
|
||
serializer.save()
|
||
cache.clear()
|
||
|
||
def perform_destroy(self, instance):
|
||
"""Удаление календаря."""
|
||
user = self.request.user
|
||
|
||
if not (user.is_superuser or user.is_staff or instance.is_owner(user)):
|
||
from rest_framework.exceptions import PermissionDenied
|
||
|
||
raise PermissionDenied("Только владелец может удалять календарь.")
|
||
|
||
instance.delete()
|
||
cache.clear()
|
||
|
||
@rest_action(
|
||
detail=True,
|
||
methods=["post"],
|
||
permission_classes=[IsAuthenticated],
|
||
url_path="subscribe",
|
||
)
|
||
def subscribe(self, request, pk=None):
|
||
"""Подписка на календарь.
|
||
|
||
Body params:
|
||
can_edit (bool, optional): Право на редактирование событий
|
||
can_manage (bool, optional): Право на управление календарем
|
||
"""
|
||
from calendar_app.models import Calendar, CalendarSubscription
|
||
|
||
calendar = self.get_object()
|
||
user = request.user
|
||
|
||
# Проверяем, доступен ли календарь для подписки
|
||
if not calendar.can_user_view(user):
|
||
from rest_framework.exceptions import PermissionDenied
|
||
|
||
raise PermissionDenied("Календарь недоступен для подписки.")
|
||
|
||
# Проверяем, не подписан ли уже пользователь
|
||
existing = CalendarSubscription.objects.filter(
|
||
calendar=calendar, user=user
|
||
).first()
|
||
|
||
if existing:
|
||
return Response(
|
||
{"detail": "Вы уже подписаны на этот календарь."},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
|
||
# Создаем подписку
|
||
can_edit = request.data.get("can_edit", False)
|
||
can_manage = request.data.get("can_manage", False)
|
||
|
||
# Только владелец может выдавать права can_edit и can_manage
|
||
if (can_edit or can_manage) and not calendar.is_owner(user):
|
||
can_edit = False
|
||
can_manage = False
|
||
|
||
subscription = CalendarSubscription.objects.create(
|
||
calendar=calendar,
|
||
user=user,
|
||
can_edit=can_edit,
|
||
can_manage=can_manage,
|
||
)
|
||
|
||
serializer = CalendarSubscriptionSerializer(
|
||
subscription, context={"request": request}
|
||
)
|
||
return Response(serializer.data, status=status.HTTP_201_CREATED)
|
||
|
||
@rest_action(
|
||
detail=True,
|
||
methods=["post"],
|
||
permission_classes=[IsAuthenticated],
|
||
url_path="unsubscribe",
|
||
)
|
||
def unsubscribe(self, request, pk=None):
|
||
"""Отписка от календаря."""
|
||
from calendar_app.models import CalendarSubscription
|
||
|
||
calendar = self.get_object()
|
||
user = request.user
|
||
|
||
subscription = CalendarSubscription.objects.filter(
|
||
calendar=calendar, user=user
|
||
).first()
|
||
|
||
if not subscription:
|
||
return Response(
|
||
{"detail": "Вы не подписаны на этот календарь."},
|
||
status=status.HTTP_400_BAD_REQUEST,
|
||
)
|
||
|
||
subscription.delete()
|
||
return Response(
|
||
{"detail": "Подписка успешно отменена."},
|
||
status=status.HTTP_200_OK,
|
||
)
|
||
|
||
@rest_action(
|
||
detail=False,
|
||
methods=["get"],
|
||
permission_classes=[IsAuthenticated],
|
||
url_path="my-calendars",
|
||
)
|
||
def my_calendars(self, request):
|
||
"""Список всех календарей, доступных текущему пользователю."""
|
||
from calendar_app.models import Calendar
|
||
|
||
user = request.user
|
||
calendars = Calendar.objects.get_available_for_user(user)
|
||
|
||
serializer = CalendarSerializer(
|
||
calendars, many=True, context={"request": request}
|
||
)
|
||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||
|
||
|
||
class CalendarSubscriptionViewSet(ModelViewSet):
|
||
"""CRUD операции для подписок на календари.
|
||
|
||
Доступные действия:
|
||
- list: список подписок текущего пользователя
|
||
- create: создание новой подписки
|
||
- retrieve: просмотр подписки
|
||
- update/partial_update: обновление подписки (права доступа)
|
||
- destroy: удаление подписки
|
||
"""
|
||
|
||
permission_classes = [IsAuthenticated]
|
||
renderer_classes = [JSONRenderer]
|
||
|
||
def get_queryset(self):
|
||
"""Возвращает подписки текущего пользователя."""
|
||
from calendar_app.models import CalendarSubscription
|
||
|
||
user = self.request.user
|
||
|
||
if user.is_superuser or user.is_staff:
|
||
# Админы видят все подписки
|
||
return CalendarSubscription.objects.all()
|
||
|
||
# Обычные пользователи видят только свои подписки
|
||
return CalendarSubscription.objects.filter(user=user)
|
||
|
||
def get_serializer_class(self):
|
||
"""Выбор сериализатора по действию."""
|
||
if self.action in {"create", "update", "partial_update"}:
|
||
return CalendarSubscriptionWriteSerializer
|
||
return CalendarSubscriptionSerializer
|
||
|
||
def perform_create(self, serializer):
|
||
"""Создание подписки."""
|
||
# Пользователь устанавливается автоматически в сериализаторе
|
||
serializer.save(user=self.request.user)
|
||
cache.clear()
|
||
|
||
def perform_update(self, serializer):
|
||
"""Обновление подписки."""
|
||
subscription = self.get_object()
|
||
user = self.request.user
|
||
|
||
# Только владелец календаря может изменять права подписки
|
||
if not subscription.calendar.is_owner(user):
|
||
from rest_framework.exceptions import PermissionDenied
|
||
|
||
raise PermissionDenied(
|
||
"Только владелец календаря может изменять права подписки."
|
||
)
|
||
|
||
serializer.save()
|
||
cache.clear()
|
||
|
||
def perform_destroy(self, instance):
|
||
"""Удаление подписки."""
|
||
user = self.request.user
|
||
|
||
# Удалять подписку может либо владелец подписки, либо владелец календаря
|
||
if not (instance.user == user or instance.calendar.is_owner(user)):
|
||
from rest_framework.exceptions import PermissionDenied
|
||
|
||
raise PermissionDenied("Вы не можете удалить эту подписку.")
|
||
|
||
instance.delete()
|
||
cache.clear()
|