From 870a08ee66a9e42ce20db21acd12d8c887c5ecc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 11 Feb 2026 08:18:47 +0300 Subject: [PATCH] feat(calendar): add API layer for optional calendars Phase 2: API implementation - Updated CalendarEventWriteSerializer to support calendar_id parameter - Added CalendarSerializer and CalendarWriteSerializer for Calendar CRUD - Added CalendarSubscriptionSerializer for subscription management - Created CalendarViewSet with subscribe/unsubscribe actions - Created CalendarSubscriptionViewSet for managing subscriptions - Updated CalendarEventsViewSet to support calendar_id filtering - Added CalendarManager with get_available_for_user() method - Added permission methods to Calendar model (is_owner, can_user_view, can_user_edit, can_user_manage) - Registered new ViewSets in URL routing API endpoints: - GET /api/v1/calendar/calendars/ - list available calendars - POST /api/v1/calendar/calendars/ - create new calendar - GET /api/v1/calendar/calendars/{id}/ - retrieve calendar details - PATCH /api/v1/calendar/calendars/{id}/ - update calendar - DELETE /api/v1/calendar/calendars/{id}/ - delete calendar - POST /api/v1/calendar/calendars/{id}/subscribe/ - subscribe to calendar - POST /api/v1/calendar/calendars/{id}/unsubscribe/ - unsubscribe from calendar - GET /api/v1/calendar/calendars/my-calendars/ - list user's calendars - GET /api/v1/calendar/subscriptions/ - list user's subscriptions - POST /api/v1/calendar/subscriptions/ - create subscription - GET /api/v1/calendar/events/?calendar_id={id} - filter events by calendar Backward compatibility: all legacy endpoints continue to work --- backend/api/v1/calendar/serializers.py | 259 ++++++++++++++++++++++- backend/api/v1/calendar/views.py | 275 ++++++++++++++++++++++++- backend/api/v1/urls.py | 12 +- backend/calendar_app/models.py | 110 ++++++++++ 4 files changed, 647 insertions(+), 9 deletions(-) diff --git a/backend/api/v1/calendar/serializers.py b/backend/api/v1/calendar/serializers.py index 99bb7282..03d33a29 100644 --- a/backend/api/v1/calendar/serializers.py +++ b/backend/api/v1/calendar/serializers.py @@ -5,7 +5,7 @@ import re from datetime import date, datetime, time, timedelta from typing import Any, Dict, List, Optional -from calendar_app.models import CalendarEvent, Recurrence +from calendar_app.models import Calendar, CalendarEvent, CalendarSubscription, Recurrence from django.utils.translation import gettext_lazy as _ from rest_framework import serializers @@ -21,6 +21,7 @@ class CalendarEventWriteSerializer(serializers.ModelSerializer): write_only=True, help_text="Список дней недели для WEEKLY (0=Mon..6=Sun).", ) + calendar_id = serializers.IntegerField(required=False, allow_null=True) department_id = serializers.IntegerField(required=False, allow_null=True) employee_id = serializers.IntegerField(required=False, allow_null=True) @@ -175,10 +176,86 @@ class CalendarEventWriteSerializer(serializers.ModelSerializer): def create(self, validated_data: Dict[str, Any]) -> CalendarEvent: validated_data.pop("weekdays", None) + + # Поддержка calendar_id (новая архитектура) + calendar_id = validated_data.pop("calendar_id", None) + if calendar_id is not None: + from calendar_app.models import Calendar + try: + calendar = Calendar.objects.get(id=calendar_id) + validated_data["calendar"] = calendar + except Calendar.DoesNotExist: + raise serializers.ValidationError( + {"calendar_id": _("Календарь с ID {} не найден.").format(calendar_id)} + ) + + # Legacy поддержка department_id и employee_id + department_id = validated_data.pop("department_id", None) + employee_id = validated_data.pop("employee_id", None) + + if department_id is not None: + from employees.models import Department + try: + department = Department.objects.get(id=department_id) + validated_data["department"] = department + except Department.DoesNotExist: + raise serializers.ValidationError( + {"department_id": _("Отдел с ID {} не найден.").format(department_id)} + ) + + if employee_id is not None: + from django.contrib.auth import get_user_model + User = get_user_model() + try: + employee = User.objects.get(id=employee_id) + validated_data["employee"] = employee + except User.DoesNotExist: + raise serializers.ValidationError( + {"employee_id": _("Сотрудник с ID {} не найден.").format(employee_id)} + ) + return CalendarEvent.objects.create(**validated_data) def update(self, instance: CalendarEvent, validated_data: Dict[str, Any]) -> CalendarEvent: validated_data.pop("weekdays", None) + + # Поддержка calendar_id (новая архитектура) + calendar_id = validated_data.pop("calendar_id", None) + if calendar_id is not None: + from calendar_app.models import Calendar + try: + calendar = Calendar.objects.get(id=calendar_id) + validated_data["calendar"] = calendar + except Calendar.DoesNotExist: + raise serializers.ValidationError( + {"calendar_id": _("Календарь с ID {} не найден.").format(calendar_id)} + ) + + # Legacy поддержка department_id и employee_id + department_id = validated_data.pop("department_id", None) + employee_id = validated_data.pop("employee_id", None) + + if department_id is not None: + from employees.models import Department + try: + department = Department.objects.get(id=department_id) + validated_data["department"] = department + except Department.DoesNotExist: + raise serializers.ValidationError( + {"department_id": _("Отдел с ID {} не найден.").format(department_id)} + ) + + if employee_id is not None: + from django.contrib.auth import get_user_model + User = get_user_model() + try: + employee = User.objects.get(id=employee_id) + validated_data["employee"] = employee + except User.DoesNotExist: + raise serializers.ValidationError( + {"employee_id": _("Сотрудник с ID {} не найден.").format(employee_id)} + ) + for f, v in validated_data.items(): setattr(instance, f, v) instance.save() @@ -204,3 +281,183 @@ class EventOccurrenceSerializer(serializers.Serializer): color = serializers.CharField(allow_null=True, required=False) recurrence = serializers.CharField() department_id = serializers.IntegerField(allow_null=True, required=False) + + +# ===== Сериализаторы для новых моделей Calendar и CalendarSubscription ===== + + +class CalendarSerializer(serializers.ModelSerializer): + """Сериализатор для календаря (чтение).""" + + owner_user_name = serializers.SerializerMethodField() + owner_department_name = serializers.SerializerMethodField() + event_count = serializers.IntegerField(read_only=True) + subscriber_count = serializers.IntegerField(read_only=True) + is_subscribed = serializers.SerializerMethodField() + user_can_edit = serializers.SerializerMethodField() + + class Meta: + model = Calendar + fields = ( + "id", + "title", + "description", + "color", + "icon", + "visibility", + "owner_user", + "owner_user_name", + "owner_department", + "owner_department_name", + "auto_subscribe_new_users", + "auto_subscribe_department_members", + "is_active", + "created_at", + "updated_at", + "event_count", + "subscriber_count", + "is_subscribed", + "user_can_edit", + ) + read_only_fields = ("id", "created_at", "updated_at") + + def get_owner_user_name(self, obj): + """Возвращает имя владельца-пользователя.""" + if obj.owner_user: + return obj.owner_user.get_full_name() or obj.owner_user.username + return None + + def get_owner_department_name(self, obj): + """Возвращает название отдела-владельца.""" + return obj.owner_department.name if obj.owner_department else None + + def get_is_subscribed(self, obj): + """Проверяет, подписан ли текущий пользователь.""" + request = self.context.get("request") + if request and request.user.is_authenticated: + return obj.subscriptions.filter(user=request.user).exists() + return False + + def get_user_can_edit(self, obj): + """Проверяет, может ли текущий пользователь редактировать календарь.""" + request = self.context.get("request") + if not request or not request.user.is_authenticated: + return False + + user = request.user + + # Админы могут всё + if user.is_superuser or user.is_staff: + return True + + # Владелец может редактировать + if obj.owner_user_id == user.id: + return True + + # Проверяем подписку с правом редактирования + subscription = obj.subscriptions.filter(user=user).first() + if subscription and (subscription.can_edit or subscription.can_manage): + return True + + return False + + +class CalendarWriteSerializer(serializers.ModelSerializer): + """Сериализатор для создания/обновления календаря.""" + + class Meta: + model = Calendar + fields = ( + "id", + "title", + "description", + "color", + "icon", + "visibility", + "owner_user", + "owner_department", + "auto_subscribe_new_users", + "auto_subscribe_department_members", + "is_active", + ) + read_only_fields = ("id",) + + def validate(self, attrs): + """Валидация: нельзя одновременно указывать owner_user и owner_department.""" + owner_user = attrs.get("owner_user") + owner_department = attrs.get("owner_department") + + # Для update нужно учитывать существующие значения + if self.instance: + owner_user = owner_user if "owner_user" in attrs else self.instance.owner_user + owner_department = owner_department if "owner_department" in attrs else self.instance.owner_department + + if owner_user and owner_department: + raise serializers.ValidationError( + _("Календарь не может одновременно принадлежать пользователю и отделу.") + ) + + return attrs + + +class CalendarSubscriptionSerializer(serializers.ModelSerializer): + """Сериализатор для подписки на календарь (чтение).""" + + calendar_title = serializers.CharField(source="calendar.title", read_only=True) + calendar_color = serializers.CharField(source="calendar.color", read_only=True) + user_name = serializers.SerializerMethodField() + + class Meta: + model = CalendarSubscription + fields = ( + "id", + "calendar", + "calendar_title", + "calendar_color", + "user", + "user_name", + "is_visible", + "color_override", + "can_edit", + "can_manage", + "notify_on_new_event", + "notify_on_event_change", + "subscribed_at", + ) + read_only_fields = ("id", "subscribed_at") + + def get_user_name(self, obj): + """Возвращает имя пользователя.""" + return obj.user.get_full_name() or obj.user.username + + +class CalendarSubscriptionWriteSerializer(serializers.ModelSerializer): + """Сериализатор для создания/обновления подписки.""" + + class Meta: + model = CalendarSubscription + fields = ( + "id", + "calendar", + "user", + "is_visible", + "color_override", + "can_edit", + "can_manage", + "notify_on_new_event", + "notify_on_event_change", + ) + read_only_fields = ("id",) + + def validate(self, attrs): + """Валидация подписки.""" + calendar = attrs.get("calendar") or (self.instance.calendar if self.instance else None) + user = attrs.get("user") or (self.instance.user if self.instance else None) + + # Проверяем, что пользователь не подписывается на свой личный календарь дважды + if calendar and user and calendar.owner_user_id == user.id: + # Владелец может иметь подписку, но это optional + pass + + return attrs + diff --git a/backend/api/v1/calendar/views.py b/backend/api/v1/calendar/views.py index 637a9b2c..ec49d11e 100644 --- a/backend/api/v1/calendar/views.py +++ b/backend/api/v1/calendar/views.py @@ -17,6 +17,10 @@ from calendar_app.models import CalendarEvent from .serializers import ( CalendarEventSerializer, CalendarEventWriteSerializer, + CalendarSerializer, + CalendarWriteSerializer, + CalendarSubscriptionSerializer, + CalendarSubscriptionWriteSerializer, ) from calendar_app.services.recurrence import expand_event_occurrences @@ -212,7 +216,7 @@ class CalendarEventsViewSet(ModelViewSet): # ---------- QuerySet ---------- def get_queryset(self): - """QS для list фильтруем по компании/отделу/сотруднику, для detail — не фильтруем. + """QS для list фильтруем по компании/отделу/сотруднику/календарю, для detail — не фильтруем. Returns: QuerySet: Набор для текущего действия. @@ -222,19 +226,28 @@ class CalendarEventsViewSet(ModelViewSet): # 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: - # Личный календарь сотрудника - return qs.filter(employee_id=emp, department__isnull=True) + # Личный календарь сотрудника (legacy) + return qs.filter(employee_id=emp, department__isnull=True, calendar__isnull=True) elif dep is not None: - # Календарь отдела - return qs.filter(department_id=dep, employee__isnull=True) + # Календарь отдела (legacy) + return qs.filter(department_id=dep, employee__isnull=True, calendar__isnull=True) else: - # Календарь компании - return qs.filter(department__isnull=True, employee__isnull=True) + # Календарь компании (legacy) + return qs.filter(department__isnull=True, employee__isnull=True, calendar__isnull=True) # ---------- LIST: материализация повторов ---------- @@ -388,3 +401,251 @@ class CalendarEventsViewSet(ModelViewSet): 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 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() diff --git a/backend/api/v1/urls.py b/backend/api/v1/urls.py index 63347878..297fb4c3 100644 --- a/backend/api/v1/urls.py +++ b/backend/api/v1/urls.py @@ -1,7 +1,11 @@ from django.urls import include, path from rest_framework.routers import DefaultRouter -from .calendar.views import CalendarEventsViewSet +from .calendar.views import ( + CalendarEventsViewSet, + CalendarViewSet, + CalendarSubscriptionViewSet, +) from .employees.views import (DepartmentRoleViewSet, DepartmentViewSet, EmployeeActionViewSet, EmployeeViewSet, GroupViewSet, PositionViewSet, @@ -19,6 +23,12 @@ app_name = "v1" router = DefaultRouter() router.register(r"calendar/events", CalendarEventsViewSet, basename="events") +router.register(r"calendar/calendars", CalendarViewSet, basename="calendars") +router.register( + r"calendar/subscriptions", + CalendarSubscriptionViewSet, + basename="subscriptions", +) router.register(r"documents", DocumentViewSet, basename="documents") diff --git a/backend/calendar_app/models.py b/backend/calendar_app/models.py index aca40254..7a9daf47 100644 --- a/backend/calendar_app/models.py +++ b/backend/calendar_app/models.py @@ -6,6 +6,7 @@ from datetime import datetime from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError from django.db import models +from django.db.models import Q from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -31,6 +32,41 @@ class CalendarVisibility(models.TextChoices): CUSTOM = "custom", _("Настраиваемый (через права)") +class CalendarManager(models.Manager): + """Менеджер для модели Calendar с дополнительными методами.""" + + def get_available_for_user(self, user): + """Возвращает календари, доступные для просмотра пользователю.""" + if user.is_superuser or user.is_staff: + # Админы видят все активные календари + return self.filter(is_active=True) + + # Публичные календари + q = Q(visibility=CalendarVisibility.PUBLIC, is_active=True) + + # Календари, где пользователь владелец + q |= Q(owner_user=user, is_active=True) + + # Календари отделов, где пользователь член + user_departments = user.employee_departments.filter( + is_active=True + ).values_list("department_id", flat=True) + + q |= Q( + owner_department_id__in=user_departments, + visibility=CalendarVisibility.DEPARTMENT, + is_active=True + ) + + # Календари с подпиской пользователя + subscribed_calendar_ids = user.calendar_subscriptions.values_list( + "calendar_id", flat=True + ) + q |= Q(id__in=subscribed_calendar_ids, is_active=True) + + return self.filter(q).distinct() + + class Calendar(models.Model): """Настраиваемый календарь (опциональный, расширенная функциональность). @@ -111,6 +147,8 @@ class Calendar(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + objects = CalendarManager() + class Meta: verbose_name = _("Календарь") verbose_name_plural = _("Календари") @@ -149,6 +187,78 @@ class Calendar(models.Model): def is_department(self): """Календарь отдела.""" return bool(self.owner_department_id) + + def is_owner(self, user): + """Проверяет, является ли пользователь владельцем календаря.""" + if self.owner_user_id: + return self.owner_user_id == user.id + if self.owner_department_id: + # Владелец отдела = руководитель отдела + return self.owner_department.head_id == user.id if hasattr(self.owner_department, 'head_id') else False + # Глобальный календарь — владелец только админы + return user.is_superuser or user.is_staff + + def can_user_view(self, user): + """Проверяет, может ли пользователь просматривать календарь.""" + if not self.is_active: + return False + + if user.is_superuser or user.is_staff: + return True + + # Владелец всегда может просматривать + if self.is_owner(user): + return True + + # Публичный календарь + if self.visibility == CalendarVisibility.PUBLIC: + return True + + # Календарь отдела + if self.visibility == CalendarVisibility.DEPARTMENT and self.owner_department_id: + # Проверяем, является ли пользователь членом отдела + return user.employee_departments.filter( + department_id=self.owner_department_id, + is_active=True + ).exists() + + # Приватный или настраиваемый — проверяем подписку + if self.visibility in [CalendarVisibility.PRIVATE, CalendarVisibility.CUSTOM]: + return self.subscriptions.filter(user=user).exists() + + return False + + def can_user_edit(self, user): + """Проверяет, может ли пользователь редактировать события в календаре.""" + if user.is_superuser or user.is_staff: + return True + + # Владелец всегда может редактировать + if self.is_owner(user): + return True + + # Проверяем подписку с правом редактирования + subscription = self.subscriptions.filter(user=user).first() + if subscription: + return subscription.can_edit + + return False + + def can_user_manage(self, user): + """Проверяет, может ли пользователь управлять календарем.""" + if user.is_superuser or user.is_staff: + return True + + # Владелец всегда может управлять + if self.is_owner(user): + return True + + # Проверяем подписку с правом управления + subscription = self.subscriptions.filter(user=user).first() + if subscription: + return subscription.can_manage + + return False class CalendarSubscription(models.Model):