from __future__ import annotations import logging from datetime import date from typing import Any, Dict, List, Optional, Type from calendar_app.cache import ( invalidate_calendar_cache, invalidate_event_cache, invalidate_subscription_cache, ) from calendar_app.models import CalendarEvent from calendar_app.services.recurrence import expand_event_occurrences 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, CalendarInviteBulkSerializer, CalendarInviteSerializer, 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 для новой архитектуры: проверка через calendar.can_user_edit() - create, update, partial_update, destroy для компании (legacy): требуется staff/superuser - create, update, partial_update, destroy для отдела (legacy): требуется MANAGE_CALENDAR - create, update, partial_update, destroy для личного календаря (legacy): только владелец """ 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()] # Для изменений проверяем новую архитектуру (calendar_id) from rest_framework.permissions import BasePermission class CalendarEditPermission(BasePermission): """Проверка прав редактирования через календари.""" def has_permission(self, request, view): from calendar_app.models import Calendar # Пытаемся получить calendar_id из разных источников calendar_id = None # 1. Из тела запроса (create) if hasattr(request, "data") and isinstance(request.data, dict): calendar_id = request.data.get("calendar_id") # 2. Из query параметров if calendar_id is None: calendar_id = request.query_params.get("calendar_id") # 3. Для update/delete из объекта события if calendar_id is None and hasattr(view, "get_object"): try: event = view.get_object() calendar_id = event.calendar_id except Exception: pass # Если calendar_id указан — используем новую логику if calendar_id is not None: try: calendar = Calendar.objects.get(id=int(calendar_id)) return calendar.can_user_edit(request.user) except (Calendar.DoesNotExist, ValueError): return False # Если calendar_id не указан — используем legacy логику return None # Пропускаем дальше к legacy проверкам # Проверяем через новую архитектуру calendar_perm = CalendarEditPermission() has_perm = calendar_perm.has_permission(self.request, self) # Если новая архитектура вернула результат — используем его if has_perm is not None: if has_perm: return [IsAuthenticated()] else: # Нет прав на редактирование календаря class DenyAll(BasePermission): def has_permission(self, request, view): return False return [DenyAll()] # LEGACY ЛОГИКА: calendar_id не указан, используем старую логику 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: # Другой пользователь не может изменять чужой личный календарь class DenyAll(BasePermission): def has_permission(self, request, view): return False return [DenyAll()] # Календарь компании (legacy) — только администраторы 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/list_raw фильтруем по компании/отделу/сотруднику/календарю, для detail — не фильтруем. Returns: QuerySet: Набор для текущего действия. """ qs = super().get_queryset() action = getattr(self, "action", None) # Фильтруем для list и list_raw, для остальных (detail и т.д.) — полный QS if action not in ("list", "list_raw"): # 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 ): # 🔧 FIX: Автоматически конвертируем многодневные события в allDay # для корректного отображения в FullCalendar dayGridMonth all_day = oc.all_day start = oc.start end = oc.end if not all_day and start and end: # Проверяем длительность duration = end - start if duration.days > 1: # Многодневное событие -> делаем allDay и убираем время all_day = True start = start.replace(hour=0, minute=0, second=0, microsecond=0) end = end.replace(hour=0, minute=0, second=0, microsecond=0) items.append( { "id": ev.id, "title": ev.title, "start": start.isoformat() if start else None, "end": end.isoformat() if end else None, "allDay": all_day, "color": ev.color or None, "recurrence": ev.recurrence, "department_id": ev.department_id, } ) return Response(items, status=status.HTTP_200_OK) @rest_action(detail=False, methods=["GET"]) def list_raw(self, request, *args, **kwargs): """Возвращает список событий БЕЗ материализации вхождений. Используется для выбора события при перемещении между календарями. Поддерживает те же параметры фильтрации, что и get_queryset(): - calendar_id (новая архитектура) - department_id (legacy) - employee_id (legacy) - без параметров = события компании (legacy) Returns: Response: Массив объектов CalendarEvent (сериализованных). """ queryset = self.get_queryset() serializer = self.get_serializer(queryset, many=True) return Response(serializer.data, 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, ) invalidate_event_cache() def perform_update(self, serializer: CalendarEventWriteSerializer) -> None: """Обновляет событие и чистит кеш.""" serializer.save() invalidate_event_cache() def perform_destroy(self, instance: CalendarEvent) -> None: """Удаляет событие и чистит кеш.""" instance.delete() invalidate_event_cache() # ---------- Проверка прав ---------- @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": # Для списка показываем только доступные календари # Сначала получаем доступные, потом оптимизируем qs = Calendar.objects.get_available_for_user(user) else: # Для остальных действий возвращаем все календари # (права проверяются в permissions) qs = Calendar.objects.all() # Оптимизация N+1 запросов для всех случаев return qs.select_related( "owner_user", "owner_department", "created_by" ).prefetch_related("subscriptions", "subscriptions__user") 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() invalidate_calendar_cache() 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("Только владелец может изменять календарь.") instance = serializer.save() invalidate_calendar_cache(calendar_id=instance.id) 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("Только владелец может удалять календарь.") calendar_id = instance.id instance.delete() invalidate_calendar_cache(calendar_id=calendar_id) @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) @rest_action( detail=True, methods=["post"], permission_classes=[IsAuthenticated], url_path="invite", ) def invite(self, request, pk=None): """Приглашение пользователя в календарь. Только владелец календаря может приглашать пользователей. Body params: user_id (int, optional): ID пользователя для приглашения username (str, optional): Username пользователя can_edit (bool, optional): Право редактирования событий can_manage (bool, optional): Право управления календарем notify (bool, optional): Отправить уведомление (default: True) """ from calendar_app.models import CalendarSubscription from django.contrib.auth import get_user_model User = get_user_model() calendar = self.get_object() owner = request.user # Проверяем, что пользователь - владелец календаря if not calendar.is_owner(owner): from rest_framework.exceptions import PermissionDenied raise PermissionDenied( "Только владелец календаря может приглашать пользователей." ) # Валидация данных serializer = CalendarInviteSerializer(data=request.data) serializer.is_valid(raise_exception=True) # Получаем пользователя по user_id или username user_id = serializer.validated_data.get("user_id") username = serializer.validated_data.get("username") try: if user_id: invited_user = User.objects.get(id=user_id) else: invited_user = User.objects.get(username=username) except User.DoesNotExist: return Response( {"detail": "Пользователь не найден."}, status=status.HTTP_404_NOT_FOUND, ) # Проверяем, не приглашает ли владелец сам себя if invited_user.id == owner.id: return Response( {"detail": "Вы не можете пригласить самого себя."}, status=status.HTTP_400_BAD_REQUEST, ) # Проверяем, не подписан ли уже пользователь existing = CalendarSubscription.objects.filter( calendar=calendar, user=invited_user ).first() if existing: return Response( { "detail": "Пользователь уже подписан на этот календарь.", "subscription_id": existing.id, }, status=status.HTTP_400_BAD_REQUEST, ) # Создаем подписку с правами can_edit = serializer.validated_data.get("can_edit", False) can_manage = serializer.validated_data.get("can_manage", False) notify = serializer.validated_data.get("notify", True) subscription = CalendarSubscription.objects.create( calendar=calendar, user=invited_user, can_edit=can_edit, can_manage=can_manage, is_visible=True, ) # Отправляем уведомление if notify: self._send_invitation_notification( calendar=calendar, invited_user=invited_user, owner=owner, can_edit=can_edit, can_manage=can_manage, ) invalidate_subscription_cache(user_id=invited_user.id) response_serializer = CalendarSubscriptionSerializer( subscription, context={"request": request} ) return Response(response_serializer.data, status=status.HTTP_201_CREATED) @rest_action( detail=True, methods=["post"], permission_classes=[IsAuthenticated], url_path="invite-bulk", ) def invite_bulk(self, request, pk=None): """Массовое приглашение пользователей в календарь. Body params: user_ids (list[int], optional): Список ID пользователей usernames (list[str], optional): Список username can_edit (bool, optional): Право редактирования событий can_manage (bool, optional): Право управления календарем notify (bool, optional): Отправить уведомления (default: True) """ from calendar_app.models import CalendarSubscription from django.contrib.auth import get_user_model User = get_user_model() calendar = self.get_object() owner = request.user # Проверяем, что пользователь - владелец календаря if not calendar.is_owner(owner): from rest_framework.exceptions import PermissionDenied raise PermissionDenied( "Только владелец календаря может приглашать пользователей." ) # Валидация данных serializer = CalendarInviteBulkSerializer(data=request.data) serializer.is_valid(raise_exception=True) # Получаем список пользователей user_ids = serializer.validated_data.get("user_ids") usernames = serializer.validated_data.get("usernames") can_edit = serializer.validated_data.get("can_edit", False) can_manage = serializer.validated_data.get("can_manage", False) notify = serializer.validated_data.get("notify", True) # Находим пользователей if user_ids: users = User.objects.filter(id__in=user_ids) else: users = User.objects.filter(username__in=usernames) if not users.exists(): return Response( {"detail": "Ни один пользователь не найден."}, status=status.HTTP_404_NOT_FOUND, ) # Исключаем владельца из списка users = users.exclude(id=owner.id) # Создаем подписки created_subscriptions = [] already_subscribed = [] errors = [] for user in users: # Проверяем, не подписан ли уже existing = CalendarSubscription.objects.filter( calendar=calendar, user=user ).first() if existing: already_subscribed.append( { "user_id": user.id, "username": user.username, "subscription_id": existing.id, } ) continue try: subscription = CalendarSubscription.objects.create( calendar=calendar, user=user, can_edit=can_edit, can_manage=can_manage, is_visible=True, ) # Отправляем уведомление if notify: self._send_invitation_notification( calendar=calendar, invited_user=user, owner=owner, can_edit=can_edit, can_manage=can_manage, ) invalidate_subscription_cache(user_id=user.id) created_subscriptions.append( CalendarSubscriptionSerializer( subscription, context={"request": request} ).data ) except Exception as e: logger.error(f"Error creating subscription for user {user.id}: {e}") errors.append( {"user_id": user.id, "username": user.username, "error": str(e)} ) return Response( { "created": created_subscriptions, "already_subscribed": already_subscribed, "errors": errors, "total_created": len(created_subscriptions), "total_already_subscribed": len(already_subscribed), "total_errors": len(errors), }, status=status.HTTP_201_CREATED if created_subscriptions else status.HTTP_200_OK, ) def _send_invitation_notification( self, calendar, invited_user, owner, can_edit, can_manage ): """Отправляет уведомление о приглашении в календарь.""" try: from notifications.models import ( Notification, NotificationCategory, NotificationType, ) # Получаем или создаем категорию category, _ = NotificationCategory.objects.get_or_create( code="calendar", defaults={ "name": "Календарь", "description": "Уведомления связанные с календарем", "icon": "calendar", }, ) # Получаем или создаем тип уведомления notification_type, _ = NotificationType.objects.get_or_create( code="calendar_invitation", defaults={ "category": category, "name": "Приглашение в календарь", "description": "Уведомление о приглашении в календарь", }, ) # Формируем текст уведомления permissions_text = [] if can_edit: permissions_text.append("редактирование событий") if can_manage: permissions_text.append("управление календарем") permissions_str = ( f" с правами: {', '.join(permissions_text)}" if permissions_text else "" ) owner_name = owner.get_full_name() or owner.username Notification.objects.create( recipient=invited_user, title=f"Приглашение в календарь: {calendar.title}", message=f"{owner_name} пригласил вас в календарь " f'"{calendar.title}"{permissions_str}.', notification_type=notification_type, content_object=calendar, ) logger.info( f"Sent invitation notification to user {invited_user.id} " f"for calendar {calendar.id}" ) except Exception as e: logger.error(f"Error sending invitation notification: {e}") 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 from django.db.models import Q user = self.request.user if user.is_superuser or user.is_staff: # Админы видят все подписки return CalendarSubscription.objects.all() # Обычные пользователи видят: # 1. Свои подписки (user=user) # 2. Подписки на календари, где они владельцы return CalendarSubscription.objects.filter( Q(user=user) # Свои подписки | Q(calendar__owner_user=user) # Владелец календаря ).distinct() def get_serializer_class(self): """Выбор сериализатора по действию.""" if self.action in {"create", "update", "partial_update"}: return CalendarSubscriptionWriteSerializer return CalendarSubscriptionSerializer def perform_create(self, serializer): """Создание подписки.""" # Пользователь устанавливается автоматически в сериализаторе subscription = serializer.save(user=self.request.user) invalidate_subscription_cache(user_id=subscription.user_id) def perform_update(self, serializer): """Обновление подписки.""" subscription = self.get_object() user = self.request.user # Получаем изменяемые поля validated_data = serializer.validated_data # Проверяем, какие поля пытаются изменить permission_fields = {"can_edit", "can_manage"} personal_fields = {"is_visible", "color_override"} changing_permissions = any( field in validated_data for field in permission_fields ) changing_personal = any(field in validated_data for field in personal_fields) # Права (can_edit, can_manage) - только владелец календаря if changing_permissions and not subscription.calendar.is_owner(user): from rest_framework.exceptions import PermissionDenied raise PermissionDenied( "Только владелец календаря может изменять права подписки." ) # Личные настройки (is_visible, color_override) - владелец подписки is_subscription_owner = subscription.user == user is_calendar_owner = subscription.calendar.is_owner(user) if changing_personal and not is_subscription_owner: if not is_calendar_owner: from rest_framework.exceptions import PermissionDenied raise PermissionDenied( "Вы можете изменять только свои личные настройки." ) updated = serializer.save() invalidate_subscription_cache(user_id=updated.user_id) 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("Вы не можете удалить эту подписку.") user_id = instance.user_id instance.delete() invalidate_subscription_cache(user_id=user_id)