Corp/backend/docs/guides/CALENDAR_INVITATION_API.md
Игорь Надеин 28e2f1615f feat: реализован drag & drop для перемещения событий между календарями
- Создан модуль calendarEventDragDrop.js с классами DragDropState и CalendarUtils
- Реализовано перетаскивание событий между календарями через модальное окно
- Добавлена кнопка 'Переместить все' для массового перемещения
- Модальное окно отображает источник и цель перемещения
- Поддержка legacy-календарей (company/department/personal)
- Добавлены стили для подсветки drop-target (папки и календари)
- Backend: исправлена обработка null значений в serializer
- Backend: добавлен endpoint list_raw для получения немодерируемых событий
- Использованы стандартные Bootstrap стили для модалов
- Исправлено всплытие событий dragover/drop (stopPropagation)
- Эмодзи иконки для типов календарей (����)
- Toast уведомления об успехе/ошибках перемещения
2026-02-12 15:59:53 +03:00

14 KiB
Raw Blame History

API для приглашения пользователей в календарь

Дата создания: 12 февраля 2026 Ветка: feature/optional-calendars Автор: GitHub Copilot

Обзор

Добавлена функциональность активного приглашения пользователей в календарь владельцем. Теперь владелец календаря может приглашать пользователей и сразу выдавать им права на редактирование и управление.

Что было добавлено

1. Новые сериализаторы

CalendarInviteSerializer

Сериализатор для приглашения одного пользователя.

Поля:

  • user_id (int, optional) - ID пользователя для приглашения
  • username (str, optional) - Username пользователя для приглашения
  • can_edit (bool, default=False) - Право на редактирование событий
  • can_manage (bool, default=False) - Право на управление календарем
  • notify (bool, default=True) - Отправить уведомление о приглашении

Валидация:

  • Должен быть указан либо user_id, либо username (не оба)

CalendarInviteBulkSerializer

Сериализатор для массового приглашения пользователей.

Поля:

  • user_ids (list[int], optional) - Список ID пользователей
  • usernames (list[str], optional) - Список username пользователей
  • can_edit (bool, default=False) - Право на редактирование событий
  • can_manage (bool, default=False) - Право на управление календарем
  • notify (bool, default=True) - Отправить уведомления о приглашении

Валидация:

  • Должен быть указан либо user_ids, либо usernames (не оба)
  • Списки не могут быть пустыми

2. Новые API endpoints

POST /api/v1/calendar/{id}/invite/

Приглашение одного пользователя в календарь.

Права доступа: Только владелец календаря

Request Body:

{
  "user_id": 123,
  "can_edit": true,
  "can_manage": false,
  "notify": true
}

или

{
  "username": "ivanov",
  "can_edit": true,
  "can_manage": false,
  "notify": true
}

Response (201 Created):

{
  "id": 45,
  "calendar": 12,
  "calendar_title": "Мой календарь",
  "calendar_color": "#3788d8",
  "user": 123,
  "user_name": "Иван Иванов",
  "is_visible": true,
  "color_override": null,
  "can_edit": true,
  "can_manage": false,
  "notify_on_new_event": true,
  "notify_on_event_change": true,
  "subscribed_at": "2026-02-12T10:30:00Z"
}

Возможные ошибки:

  • 403 Forbidden - Пользователь не является владельцем календаря
  • 404 Not Found - Пользователь с указанным ID/username не найден
  • 400 Bad Request - Пользователь уже подписан на календарь

POST /api/v1/calendar/{id}/invite-bulk/

Массовое приглашение пользователей в календарь.

Права доступа: Только владелец календаря

Request Body:

{
  "user_ids": [123, 456, 789],
  "can_edit": true,
  "can_manage": false,
  "notify": true
}

или

{
  "usernames": ["ivanov", "petrov", "sidorov"],
  "can_edit": true,
  "can_manage": false,
  "notify": true
}

Response (201 Created):

{
  "created": [
    {
      "id": 45,
      "calendar": 12,
      "user": 123,
      "user_name": "Иван Иванов",
      "can_edit": true,
      "can_manage": false,
      ...
    },
    {
      "id": 46,
      "calendar": 12,
      "user": 456,
      "user_name": "Петр Петров",
      "can_edit": true,
      "can_manage": false,
      ...
    }
  ],
  "already_subscribed": [
    {
      "user_id": 789,
      "username": "sidorov",
      "subscription_id": 40
    }
  ],
  "errors": [],
  "total_created": 2,
  "total_already_subscribed": 1,
  "total_errors": 0
}

Особенности:

  • Владелец автоматически исключается из списка приглашаемых
  • Пользователи, которые уже подписаны, пропускаются (не ошибка)
  • Если ни один пользователь не найден - 404 Not Found
  • Ошибки отдельных пользователей не блокируют остальные приглашения

3. Уведомления

При приглашении (если notify=true) пользователю отправляется уведомление:

Тип: calendar_invitation Заголовок: Приглашение в календарь: {calendar_title} Текст: {owner_name} пригласил вас в календарь "{calendar_title}" с правами: {permissions}.

Связанный объект:

  • related_object_type: "calendar"
  • related_object_id: ID календаря

Архитектура

Права доступа

  1. Владелец календаря может:

    • Приглашать любых пользователей
    • Выдавать права can_edit и can_manage
    • Отменять подписки других пользователей (через DELETE /api/v1/calendar/subscriptions/{id}/)
    • Изменять права существующих подписчиков (через PATCH /api/v1/calendar/subscriptions/{id}/)
  2. Обычный пользователь может:

    • Подписаться на публичный календарь (через /subscribe/)
    • Отписаться от календаря (через /unsubscribe/)
    • Изменить свои настройки отображения (цвет, видимость)
    • НЕ может выдавать себе права can_edit и can_manage

Проверки безопасности

  • ✅ Проверка владельца календаря перед приглашением
  • ✅ Защита от приглашения самого себя
  • ✅ Проверка на дублирование подписок
  • ✅ Автоматическое исключение владельца из массового приглашения
  • ✅ Транзакционная безопасность при массовом приглашении

Примеры использования

JavaScript (Frontend)

// Приглашение одного пользователя
async function inviteUser(calendarId, userId, canEdit = false) {
    const response = await fetch(`/api/v1/calendar/${calendarId}/invite/`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCsrfToken()
        },
        body: JSON.stringify({
            user_id: userId,
            can_edit: canEdit,
            can_manage: false,
            notify: true
        })
    });

    if (!response.ok) {
        const error = await response.json();
        throw new Error(error.detail);
    }

    return await response.json();
}

// Массовое приглашение
async function inviteBulk(calendarId, userIds, canEdit = false) {
    const response = await fetch(`/api/v1/calendar/${calendarId}/invite-bulk/`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
            'X-CSRFToken': getCsrfToken()
        },
        body: JSON.stringify({
            user_ids: userIds,
            can_edit: canEdit,
            can_manage: false,
            notify: true
        })
    });

    const result = await response.json();
    console.log(`Приглашено: ${result.total_created}, уже подписаны: ${result.total_already_subscribed}`);
    return result;
}

Python (тесты/скрипты)

import requests

# Приглашение одного пользователя
response = requests.post(
    'http://localhost:9000/api/v1/calendar/12/invite/',
    json={
        'username': 'ivanov',
        'can_edit': True,
        'can_manage': False,
        'notify': True
    },
    headers={'Authorization': f'Bearer {token}'}
)

if response.status_code == 201:
    subscription = response.json()
    print(f"Пользователь приглашен: {subscription['user_name']}")

# Массовое приглашение
response = requests.post(
    'http://localhost:9000/api/v1/calendar/12/invite-bulk/',
    json={
        'user_ids': [123, 456, 789],
        'can_edit': True,
        'notify': True
    },
    headers={'Authorization': f'Bearer {token}'}
)

result = response.json()
print(f"Создано подписок: {result['total_created']}")
print(f"Уже подписаны: {result['total_already_subscribed']}")
print(f"Ошибки: {result['total_errors']}")

Интеграция с существующей системой

CalendarSubscription модель

Используется существующая модель без изменений:

  • can_edit - право на редактирование событий
  • can_manage - право на управление календарем
  • is_visible - видимость календаря для пользователя

Notifications система

Интегрируется с существующей системой уведомлений через модель Notification:

  • Тип: calendar_invitation
  • Связь с календарем через related_object_id

Cache invalidation

Автоматически инвалидирует кеш подписок через invalidate_subscription_cache(user_id=...) после создания подписки.


Следующие шаги

Рекомендуемые улучшения:

  1. Frontend UI:

    • Модальное окно для управления участниками календаря
    • Поиск пользователей с автодополнением
    • Список текущих участников с управлением правами
    • Индикация отправленных приглашений
  2. Email уведомления:

    • Отправка email при приглашении (опционально)
    • Ссылка для быстрого перехода к календарю
  3. Система приглашений с подтверждением:

    • Создание модели CalendarInvitation для отслеживания статуса
    • Возможность принять/отклонить приглашение
    • Срок действия приглашения
  4. Роли участников:

    • Расширение прав: viewer, editor, admin
    • Более детальная настройка разрешений
  5. Аудит:

    • Логирование всех приглашений
    • История изменения прав участников

Тестирование

Ручное тестирование

  1. Создать календарь как владелец
  2. Пригласить пользователя через API /invite/
  3. Проверить, что подписка создана с правильными правами
  4. Проверить, что уведомление отправлено
  5. Попытаться пригласить уже подписанного пользователя (должна быть ошибка 400)
  6. Массово пригласить несколько пользователей через /invite-bulk/
  7. Проверить результат (created, already_subscribed, errors)

Unit тесты (TODO)

# backend/calendar_app/tests/test_invitation.py

def test_invite_user_as_owner():
    """Владелец может пригласить пользователя."""
    pass

def test_invite_user_not_owner():
    """Не-владелец не может приглашать."""
    pass

def test_invite_already_subscribed():
    """Нельзя пригласить уже подписанного."""
    pass

def test_invite_bulk():
    """Массовое приглашение работает корректно."""
    pass

Изменённые файлы

  1. backend/api/v1/calendar/serializers.py

    • Добавлен CalendarInviteSerializer
    • Добавлен CalendarInviteBulkSerializer
  2. backend/api/v1/calendar/views.py

    • Добавлен метод CalendarViewSet.invite()
    • Добавлен метод CalendarViewSet.invite_bulk()
    • Добавлен вспомогательный метод _send_invitation_notification()
  3. backend/docs/guides/CALENDAR_INVITATION_API.md (этот файл)

    • Документация по новому API

Совместимость

  • ✅ Обратная совместимость с существующим API
  • ✅ Работает с legacy календарями (department/employee)
  • ✅ Работает с новыми календарями (Calendar entity)
  • ✅ Не требует миграций БД (используются существующие модели)

Заключение

Функция приглашения пользователей полностью реализована и готова к использованию. Она интегрируется с существующей архитектурой календарей и подписок, обеспечивает безопасность через проверку прав владельца, и включает систему уведомлений.

Статус: ✅ Готово к тестированию Требуется: Frontend UI для удобного управления участниками