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
This commit is contained in:
parent
ef734f19c0
commit
870a08ee66
@ -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
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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):
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user