- Создан модуль calendarEventDragDrop.js с классами DragDropState и CalendarUtils - Реализовано перетаскивание событий между календарями через модальное окно - Добавлена кнопка 'Переместить все' для массового перемещения - Модальное окно отображает источник и цель перемещения - Поддержка legacy-календарей (company/department/personal) - Добавлены стили для подсветки drop-target (папки и календари) - Backend: исправлена обработка null значений в serializer - Backend: добавлен endpoint list_raw для получения немодерируемых событий - Использованы стандартные Bootstrap стили для модалов - Исправлено всплытие событий dragover/drop (stopPropagation) - Эмодзи иконки для типов календарей (����) - Toast уведомления об успехе/ошибках перемещения
14 KiB
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 календаря
Архитектура
Права доступа
-
Владелец календаря может:
- Приглашать любых пользователей
- Выдавать права
can_editиcan_manage - Отменять подписки других пользователей (через
DELETE /api/v1/calendar/subscriptions/{id}/) - Изменять права существующих подписчиков (через
PATCH /api/v1/calendar/subscriptions/{id}/)
-
Обычный пользователь может:
- Подписаться на публичный календарь (через
/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=...) после создания подписки.
Следующие шаги
Рекомендуемые улучшения:
-
Frontend UI:
- Модальное окно для управления участниками календаря
- Поиск пользователей с автодополнением
- Список текущих участников с управлением правами
- Индикация отправленных приглашений
-
Email уведомления:
- Отправка email при приглашении (опционально)
- Ссылка для быстрого перехода к календарю
-
Система приглашений с подтверждением:
- Создание модели
CalendarInvitationдля отслеживания статуса - Возможность принять/отклонить приглашение
- Срок действия приглашения
- Создание модели
-
Роли участников:
- Расширение прав: viewer, editor, admin
- Более детальная настройка разрешений
-
Аудит:
- Логирование всех приглашений
- История изменения прав участников
Тестирование
Ручное тестирование
- Создать календарь как владелец
- Пригласить пользователя через API
/invite/ - Проверить, что подписка создана с правильными правами
- Проверить, что уведомление отправлено
- Попытаться пригласить уже подписанного пользователя (должна быть ошибка 400)
- Массово пригласить несколько пользователей через
/invite-bulk/ - Проверить результат (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
Изменённые файлы
-
backend/api/v1/calendar/serializers.py
- Добавлен
CalendarInviteSerializer - Добавлен
CalendarInviteBulkSerializer
- Добавлен
-
backend/api/v1/calendar/views.py
- Добавлен метод
CalendarViewSet.invite() - Добавлен метод
CalendarViewSet.invite_bulk() - Добавлен вспомогательный метод
_send_invitation_notification()
- Добавлен метод
-
backend/docs/guides/CALENDAR_INVITATION_API.md (этот файл)
- Документация по новому API
Совместимость
- ✅ Обратная совместимость с существующим API
- ✅ Работает с legacy календарями (department/employee)
- ✅ Работает с новыми календарями (Calendar entity)
- ✅ Не требует миграций БД (используются существующие модели)
Заключение
Функция приглашения пользователей полностью реализована и готова к использованию. Она интегрируется с существующей архитектурой календарей и подписок, обеспечивает безопасность через проверку прав владельца, и включает систему уведомлений.
Статус: ✅ Готово к тестированию Требуется: Frontend UI для удобного управления участниками