9.9 KiB
Личный календарь сотрудников
Обзор изменений
Добавлен третий тип календаря — Личный календарь сотрудника. Теперь система поддерживает:
- Календарь компании (company) — глобальные события, доступ только у администраторов
- Календарь отдела (dept) — события отдела, требуется право
MANAGE_CALENDAR - Личный календарь (personal) — НОВОЕ: индивидуальный календарь каждого сотрудника
Функциональность
Личный календарь
- ✅ Каждый сотрудник имеет собственный личный календарь
- ✅ Полный контроль над своими событиями (создание, редактирование, удаление)
- ✅ События личного календаря видны только владельцу
- ✅ Другие пользователи не могут изменять чужие личные события
- ✅ Администраторы также не могут редактировать личные календари (только просмотр)
Изменения в Backend
1. Модель CalendarEvent
Добавлено поле employee:
employee = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name="personal_calendar_events",
verbose_name=_("Сотрудник"),
null=True,
blank=True,
help_text=_("Если задано — личное событие сотрудника."),
)
Логика:
department=None, employee=None→ календарь компанииdepartment=set, employee=None→ календарь отделаdepartment=None, employee=set→ личный календарь сотрудника
Валидация:
- Событие не может одновременно принадлежать отделу и сотруднику
- Добавлены свойства
is_personalи обновлёнis_company
2. Сериализаторы
CalendarEventWriteSerializer:
- Добавлено поле
employee_idвfields - Поддержка создания/обновления личных событий
3. Views (CalendarEventsViewSet)
Новые методы:
def _employee_id(self, *, required: bool = False) -> Optional[int]:
"""Извлекает employee_id из kwargs/query/body"""
Обновлённые методы:
get_queryset():
# Приоритет: employee_id > department_id > company
if emp is not None:
return qs.filter(employee_id=emp, department__isnull=True)
elif dep is not None:
return qs.filter(department_id=dep, employee__isnull=True)
else:
return qs.filter(department__isnull=True, employee__isnull=True)
get_permissions():
# Личный календарь — полный контроль только у владельца
if emp is not None:
if self.request.user.id == emp:
return [IsAuthenticated()]
else:
return [DenyAll()] # Другие пользователи не могут изменять
perform_create():
if emp is not None:
serializer.save(
employee_id=emp,
department_id=None,
created_by=self.request.user,
)
permissions endpoint:
elif event.employee_id is not None:
# Личное событие — редактировать может только владелец
if event.employee_id == user.id:
can_edit = True
can_delete = True
Изменения в Frontend
1. Dropdown выбора календаря
Desktop (calendar_desktop.html):
<li><button class="dropdown-item" type="button" data-cal="personal">Личный</button></li>
<li><hr class="dropdown-divider"></li>
<li><button class="dropdown-item" type="button" data-cal="company">Компания</button></li>
Mobile (calendar_mobile.html): — аналогично
2. JavaScript (calendarWidget.js)
State:
const state = {
type: 'company', // 'company' | 'dept' | 'personal'
deptId: null,
employeeId: null
};
eventsUrl():
const eventsUrl = (deptId = null, employeeId = null) => {
const u = new URL(API_EVENTS, location.origin);
if (employeeId != null) {
u.searchParams.set('employee_id', String(employeeId));
} else if (deptId != null) {
u.searchParams.set('department_id', String(deptId));
}
return u.pathname + u.search;
};
handleChooserClick():
else if (type === 'personal') {
state.type = 'personal';
state.deptId = null;
const userMeta = document.querySelector('meta[name="user-id"]');
state.employeeId = userMeta ? userMeta.content : null;
}
fetchEventsCombined():
if (state.type === 'personal' && state.employeeId) {
params.employee_id = state.employeeId;
}
Form submit:
if (state.type === 'personal') {
payload.employee_id = Number(state.employeeId);
} else if (state.type === 'dept') {
payload.department_id = Number(state.deptId);
}
3. Base Template
Добавлен meta-тег с ID пользователя:
{% if user.is_authenticated %}
<meta name="user-id" content="{{ user.id }}">
{% endif %}
Миграции
Создана миграция 0010_calendarevent_employee_and_more.py:
- Добавляет поле
employeeв модельCalendarEvent - Изменяет
help_textполяdepartment
Тестирование
Сценарии для проверки:
-
Переключение на личный календарь:
- Открыть dropdown → выбрать "Личный"
- Должны отобразиться только личные события текущего пользователя
-
Создание личного события:
- В режиме "Личный" создать событие
- Событие должно сохраниться с
employee_id=текущий_пользователь
-
Редактирование личного события:
- Владелец может редактировать/удалять свои события
- ПКМ на событии → контекстное меню должно показывать кнопки
-
Изоляция календарей:
- События личного календаря не должны видеть другие пользователи
- Администраторы могут видеть, но не редактировать чужие личные события
-
Проверка прав:
- Попытка редактировать чужое личное событие должна вернуть 403
- API endpoint
/permissions/должен возвращатьcan_edit=falseдля чужих событий
API Примеры
Получить личные события
GET /api/v1/calendar/events/?employee_id=123&start=2025-11-01&end=2025-11-30
Authorization: Bearer <token>
Создать личное событие
POST /api/v1/calendar/events/?employee_id=123
Content-Type: application/json
Authorization: Bearer <token>
{
"employee_id": 123,
"title": "Личная встреча",
"start_date": "2025-11-29",
"all_day": true,
"recurrence": "one_time"
}
Проверить права на событие
GET /api/v1/calendar/events/456/permissions/
Authorization: Bearer <token>
Response:
{
"can_view": true,
"can_edit": true, // true только для владельца
"can_delete": true
}
Безопасность
✅ Изоляция данных: события фильтруются по employee_id в QuerySet
✅ Проверка прав: get_permissions() проверяет владельца перед изменениями
✅ Валидация: модель запрещает одновременную привязку к отделу и сотруднику
✅ API защита: все endpoints требуют авторизации
Совместимость
✅ Обратная совместимость сохранена ✅ Существующие события компании/отделов работают без изменений ✅ Миграция безопасна (добавляет nullable поле)
Файлы изменённые
Backend:
backend/calendar_app/models.py— добавлено полеemployee, валидация, свойстваbackend/api/v1/calendar/serializers.py— добавлено полеemployee_idbackend/api/v1/calendar/views.py— логика личных календарейbackend/calendar_app/migrations/0010_calendarevent_employee_and_more.py— миграция
Frontend:
backend/templates/base.html— meta-тегuser-idbackend/templates/includes/components/calendar_desktop.html— пункт "Личный"backend/templates/includes/components/calendar_mobile.html— пункт "Личный"backend/static/js/components/calendarWidget.js— полная поддержка личных календарей
Дальнейшие улучшения
- Возможность "поделиться" личным событием с коллегами
- Синхронизация с внешними календарями (Google, Outlook)
- Уведомления о личных событиях
- Экспорт личного календаря в iCal формат