Реализовано: 1. Множественный выбор календарей через чекбоксы - Заменил <select> на чекбоксы для выбора нескольких календарей - События создаются одновременно во всех выбранных календарях - При редактировании - обновляется только первый выбранный - Цветовые индикаторы для каждого календаря 2. Современный дизайн событий - Двухцветная схема: левая граница = цвет календаря, фон = цвет события - Полупрозрачный фон события (80%) для лучшей видимости - Тени и hover эффекты для объёма - Закругленные углы (4px) - Плавные анимации (transform + transition) 3. Улучшенное отображение накладывающихся событий - dayMaxEvents: автоматически показывать '+N еще' - dayMaxEventRows: 4 - максимум 4 строки на день - moreLinkClick: 'popover' - красивый popover для остальных - eventMaxStack: 3 - до 3 событий в стеке - eventOrder: сортировка по началу, длительности, алфавиту 4. Новый CSS файл calendar-improvements.css - Современные стили для FullCalendar - Улучшенный popover с градиентом - Стили для выходных дней (розоватый фон) - Hover эффекты на события - Анимация fadeInEvent при загрузке - Улучшенная читаемость текущего дня - Адаптивность для мобильных устройств - Кастомные scrollbar для списков 5. Передача цвета календаря в события - calendar_color добавляется в extendedProps - Используется __calendar из интеграции - Отдельная обработка eventColor и calendarColor 6. Визуальные улучшения - Текст события: font-weight 500, line-height 1.3 - Отступы: paddingLeft 6px (с цветом календаря) - Тень текста для читаемости на любом фоне - Паттерн для ежегодных событий (диагональные полосы) - Индикатор текущего дня: синий круг с белым текстом Результат: ✅ События визуально различимы по календарям (левая граница) ✅ Сохранён индивидуальный цвет события (основной фон) ✅ Множество событий в день отображается компактно (+N еще) ✅ Современный Material Design стиль ✅ Плавные анимации и hover эффекты ✅ Улучшенная читаемость и UX
26 KiB
Анализ приложения календаря EUSRR
Дата анализа: 11 февраля 2026 г. Задача: Изучить приложение календаря и оценить возможность создания пользовательских календарей (глобального, локального или календаря отдела)
📋 Содержание
- Текущая архитектура
- Модель данных
- API и права доступа
- Frontend реализация
- Оценка возможностей
- Рекомендации
🏗️ Текущая архитектура
Структура приложения
backend/calendar_app/
├── models.py # Модель CalendarEvent
├── admin.py # Админка Django
├── signals.py # Автосигналы (дни рождения)
├── notification_signals.py # Уведомления о событиях
├── apps.py # Конфигурация приложения
└── services/
└── recurrence.py # Логика повторяющихся событий
backend/api/v1/calendar/
├── views.py # CalendarEventsViewSet (REST API)
└── serializers.py # Сериализаторы событий
backend/templates/includes/components/
├── calendar_desktop.html # Десктопный виджет
├── calendar_mobile.html # Мобильный виджет
├── calendar_modal_create.html # Модалка создания
└── calendar_modal_details.html # Модалка деталей
backend/static/js/
├── api/calendarApi.js # API клиент
└── components/calendarWidget.js # Логика виджета FullCalendar
📊 Модель данных
CalendarEvent
Основная модель события с тремя типами области видимости:
class CalendarEvent(models.Model):
# Область видимости (взаимоисключающие)
department = ForeignKey("employees.Department", null=True, blank=True)
employee = ForeignKey(User, null=True, blank=True) # ✅ УЖЕ РЕАЛИЗОВАНО
# Основные поля
title = CharField(max_length=200)
description = TextField(blank=True)
# Даты и время
start_date = DateField()
end_date = DateField(null=True, blank=True)
start_time = TimeField(null=True, blank=True)
end_time = TimeField(null=True, blank=True)
all_day = BooleanField(default=True)
# Повторяемость
recurrence = CharField(choices=Recurrence.choices) # one_time, hourly, daily, weekly, monthly, annual
recurrence_interval = PositiveSmallIntegerField(default=1)
recurrence_count = PositiveIntegerField(null=True, blank=True)
recurrence_until = DateField(null=True, blank=True)
weekdays_mask = PositiveSmallIntegerField(default=0) # Для weekly: битовая маска дней
# Отображение
color = CharField(max_length=7, blank=True) # HEX цвет
location = CharField(max_length=200, blank=True)
# Служебные
created_by = ForeignKey(User, on_delete=SET_NULL)
created_at = DateTimeField(auto_now_add=True)
source = CharField(max_length=120, blank=True, db_index=True) # Для системных событий
Типы календарей
1. 🌐 Календарь компании (Глобальный)
department = NULLиemployee = NULL- Видны всем сотрудникам
- Редактируют только администраторы (
is_staff=Trueилиis_superuser=True) - Примеры: Праздники, корпоративы, общие собрания
2. 🏢 Календарь отдела (Локальный)
department = <PK отдела>иemployee = NULL- Видны всем авторизованным пользователям
- Редактируют: администраторы или пользователи с правом
MANAGE_CALENDARна отдел - Примеры: Планёрки отдела, дедлайны проектов
3. 👤 Личный календарь (Персональный) ✅
department = NULLиemployee = <PK сотрудника>- СТАТУС: Полностью реализован в коде!
- Видны всем авторизованным (для просмотра)
- Редактирует только владелец (
employee_id == request.user.id) - Примеры: Личные встречи, отпуска, напоминания
Валидация модели
def clean(self):
# ✅ Взаимоисключение: нельзя одновременно задавать отдел И сотрудника
if self.department_id and self.employee_id:
raise ValidationError("Событие не может одновременно принадлежать отделу и сотруднику.")
# Валидация дат/времени
# Валидация повторяемости
# Автоматическая настройка all_day
🔌 API и права доступа
REST API Endpoint
/api/v1/calendar/events/
Поддерживаемые методы
| Метод | Действие | Права доступа |
|---|---|---|
GET /events/ |
Список событий (материализованные повторы) | Авторизованные пользователи |
GET /events/{id}/ |
Детали события | Авторизованные пользователи |
POST /events/ |
Создание события | Зависит от типа* |
PATCH /events/{id}/ |
Обновление события | Зависит от типа* |
DELETE /events/{id}/ |
Удаление события | Зависит от типа* |
GET /events/{id}/permissions/ |
Проверка прав на событие | Авторизованные пользователи |
*Права на изменение:
def get_permissions(self):
# Просмотр: всем авторизованным
if action in ["list", "retrieve"]:
return [IsAuthenticated()]
# Личный календарь: только владелец
if emp is not None:
if request.user.id == emp:
return [IsAuthenticated()]
else:
return [DenyAll()] # Другие не могут редактировать
# Календарь компании: только администраторы
if dep is None:
return [IsAdminUser()]
# Календарь отдела: право MANAGE_CALENDAR
return [ManageCalendarPerm()] # AdminOrDeptAllowed с required_code=MANAGE_CALENDAR
Фильтрация событий
GET параметры:
?start=2025-01-01 # Обязательный: начало диапазона (YYYY-MM-DD)
?end=2025-01-31 # Обязательный: конец диапазона (YYYY-MM-DD)
?department_id=5 # Опциональный: календарь отдела
?employee_id=123 # Опциональный: личный календарь сотрудника
Приоритет фильтрации: employee_id > department_id > company
Примеры запросов:
# Календарь компании
GET /api/v1/calendar/events/?start=2025-11-01&end=2025-11-30
# Календарь отдела HR
GET /api/v1/calendar/events/?start=2025-11-01&end=2025-11-30&department_id=5
# Личный календарь сотрудника
GET /api/v1/calendar/events/?start=2025-11-01&end=2025-11-30&employee_id=123
Создание событий
POST /api/v1/calendar/events/
Content-Type: application/json
Authorization: Bearer <token>
# Компания (только админы)
{
"title": "Новогодний корпоратив",
"start_date": "2025-12-31",
"all_day": true,
"recurrence": "one_time"
}
# Отдел (с правом MANAGE_CALENDAR)
{
"department_id": 5,
"title": "Планёрка отдела",
"start_date": "2025-11-15",
"start_time": "10:00",
"end_time": "11:00",
"all_day": false,
"recurrence": "weekly",
"weekdays": [0, 2, 4] # Пн, Ср, Пт
}
# Личный (только владелец)
{
"employee_id": 123,
"title": "Встреча с клиентом",
"start_date": "2025-11-20",
"start_time": "14:00",
"end_time": "15:00",
"all_day": false,
"recurrence": "one_time"
}
🎨 Frontend реализация
Виджет календаря (FullCalendar)
Файл: backend/static/js/components/calendarWidget.js
State управление:
const state = {
type: 'company', // 'company' | 'dept' | 'personal'
deptId: null, // PK отдела (если type='dept')
employeeId: null // PK сотрудника (если type='personal')
};
URL формирование:
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;
};
UI компоненты:
Десктоп: calendar_desktop.html
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle">
Компания
</button>
<ul class="dropdown-menu" id="calendarChooserMenu">
<li><button data-cal="personal">Личный</button></li>
<li><hr class="dropdown-divider"></li>
<li><button data-cal="company">Компания</button></li>
<li><hr class="dropdown-divider"></li>
{# Пункты отделов добавляются динамически из JS #}
</ul>
</div>
Мобильный: calendar_mobile.html (аналогичная структура в offcanvas)
Загрузка событий:
// ✅ Поддержка всех трёх типов календарей
async function fetchEventsAllCalendars(start, end) {
const sources = [
// Компания
{ params: { start, end }, label: 'Компания', type: 'company', id: null },
// Все отделы пользователя
...departments.map(d => ({
params: { start, end, department_id: d.id },
label: d.name,
type: 'dept',
id: d.id
})),
// Личный календарь (если есть employee_id)
{
params: { start, end, employee_id: currentEmployeeId },
label: 'Личный',
type: 'personal',
id: currentEmployeeId
}
];
// Параллельная загрузка всех источников
const results = await Promise.allSettled(
sources.map(s => getCalendarEvents(s.params))
);
return results.flatMap(/* ... */);
}
Модалки
Создание события: calendar_modal_create.html
- Форма с полями: title, description, start_date, end_date, start_time, end_time
- Выбор повторяемости (recurrence)
- Выбор цвета (color picker)
- Автоматическая подстановка department_id/employee_id из текущего state
Просмотр/редактирование: calendar_modal_details.html
- Отображение всех деталей события
- Кнопки редактирования/удаления (если есть права)
- Проверка прав через
GET /events/{id}/permissions/
✅ Оценка возможностей
Что УЖЕ реализовано:
✅ 1. Календарь компании (Глобальный)
- Backend: Полностью реализован
- API:
/api/v1/calendar/events/без параметров - Frontend: Работает в виджете (кнопка "Компания")
- Права: Просмотр — все, редактирование — только админы
- Статус: 🟢 Готово к использованию
✅ 2. Календарь отдела (Локальный)
- Backend: Полностью реализован
- API:
/api/v1/calendar/events/?department_id=X - Frontend: Динамическое меню отделов в виджете
- Права: Просмотр — все, редактирование — с правом
MANAGE_CALENDAR - Статус: 🟢 Готово к использованию
✅ 3. Личный календарь (Персональный)
- Backend: Полностью реализован (поле
employeeв модели) - API:
/api/v1/calendar/events/?employee_id=X - Frontend: ⚠️ Кнопка в меню есть, но требуется тестирование
- Права: Просмотр — все, редактирование — только владелец
- Статус: 🟡 Реализовано, требует проверки работы UI
Что работает из коробки:
- ✅ Создание событий всех трёх типов через API
- ✅ Повторяющиеся события (hourly, daily, weekly, monthly, annual)
- ✅ Материализация повторов в диапазоне дат
- ✅ Права доступа с автоматической проверкой
- ✅ Валидация дат, времени, повторяемости
- ✅ Уведомления при создании/изменении/удалении событий
- ✅ Автосоздание событий (например, дни рождения через signals)
- ✅ FullCalendar интеграция на фронте
- ✅ Адаптивный UI (desktop + mobile)
Что может потребовать доработки:
🟡 1. UI личного календаря
Что есть:
- Кнопка "Личный" в выпадающем меню
- Логика переключения state
- API запросы с
employee_id
Что проверить:
- Корректное получение
employee_idтекущего пользователя - Отображение событий в FullCalendar
- Создание личных событий через модалку
- Проверка прав доступа (только владелец может редактировать)
Код для проверки:
// В calendarWidget.js, строка ~535
const userMeta = document.querySelector('meta[name="user-id"]');
const currentEmployeeId = userMeta ? userMeta.content : null;
Требуется:
- Убедиться, что в шаблонах есть
<meta name="user-id" content="{{ user.id }}"> - Протестировать переключение на личный календарь
- Протестировать создание/редактирование личных событий
🟡 2. Модалка создания
Текущее поведение:
- При создании события подставляется текущий
department_idилиemployee_idиз state
Что может понадобиться:
- Явный выбор типа календаря в форме создания (если нужен)
- Визуальная индикация, для какого календаря создаётся событие
- Валидация на фронте перед отправкой
🟡 3. Фильтры и поиск
Что отсутствует:
- Поиск событий по названию
- Фильтрация по типу повторяемости
- Экспорт в iCal/Google Calendar
Приоритет: Низкий (можно добавить позже)
🎯 Рекомендации
Немедленные действия (Priority: High)
1. Проверка meta-тега user-id
Файлы для проверки:
backend/templates/base.htmlили аналогичный base-шаблон- Любые страницы, где используется календарь
Добавить, если отсутствует:
{% if user.is_authenticated %}
<meta name="user-id" content="{{ user.id }}">
{% endif %}
2. Тестирование личного календаря
Сценарии:
# 1. GET личных событий
GET /api/v1/calendar/events/?employee_id=<user_id>&start=2025-11-01&end=2025-11-30
# 2. POST личного события
POST /api/v1/calendar/events/
{"employee_id": <user_id>, "title": "Тест", "start_date": "2025-11-15", "all_day": true}
# 3. PATCH личного события (владелец)
PATCH /api/v1/calendar/events/<event_id>/
{"title": "Обновлённое название"}
# 4. PATCH личного события (НЕ владелец) — должно вернуть 403
3. Проверка прав доступа
Матрица прав:
| Тип календаря | Просмотр | Создание | Редактирование | Удаление |
|---|---|---|---|---|
| Компания | Все авторизованные | Только админы | Только админы | Только админы |
| Отдел | Все авторизованные | С правом MANAGE_CALENDAR | С правом MANAGE_CALENDAR | С правом MANAGE_CALENDAR |
| Личный | Все авторизованные | Только владелец | Только владелец | Только владелец |
Код тестов: Уже существует в backend/tests/api/v1/calendar_app/test_calendar_api.py
Краткосрочные улучшения (Priority: Medium)
1. UI индикаторы
<!-- В модалке создания события -->
<div class="alert alert-info">
<i class="bi-info-circle"></i>
Создаётся событие для: <strong id="eventScope">Компания</strong>
</div>
// Обновление при переключении календаря
function updateScopeIndicator() {
const scope = state.type === 'personal' ? 'Личного календаря'
: state.type === 'dept' ? departments.find(d => d.id === state.deptId)?.name
: 'Компании';
document.getElementById('eventScope').textContent = scope;
}
2. Цветовая дифференциация
// В calendarWidget.js
function getEventColor(event) {
if (event.color) return event.color;
// Дефолтные цвета по типу
if (event.employee_id) return '#6C757D'; // Серый для личных
if (event.department_id) return '#0D6EFD'; // Синий для отдела
return '#198754'; // Зелёный для компании
}
3. Уведомления на фронте
// После успешного создания
showToast('Событие создано', `"${title}" добавлено в ${scopeName}`, 'success');
Долгосрочные улучшения (Priority: Low)
1. Расширенные фильтры
- Поиск по названию/описанию
- Фильтр по цвету
- Фильтр по типу повторяемости
- Группировка по отделам
2. Интеграции
- Экспорт в .ics (iCal)
- Синхронизация с Google Calendar
- Outlook интеграция
- Email-напоминания
3. Аналитика
- Статистика по событиям отдела
- Загруженность календарей
- Отчёты по посещаемости
📝 Примеры использования
Создание глобального события (админ)
import requests
url = "https://example.com/api/v1/calendar/events/"
headers = {"Authorization": "Bearer <admin_token>"}
data = {
"title": "Корпоративный хакатон",
"description": "Командный хакатон по разработке новых фич",
"start_date": "2025-12-15",
"end_date": "2025-12-16",
"all_day": true,
"color": "#FF5733",
"location": "Офис, конференц-зал",
"recurrence": "one_time"
}
response = requests.post(url, headers=headers, json=data)
print(response.json())
Создание события отдела (с правом MANAGE_CALENDAR)
data = {
"department_id": 5, # HR отдел
"title": "Планёрка HR",
"start_date": "2025-11-18",
"start_time": "10:00",
"end_time": "10:30",
"all_day": false,
"recurrence": "weekly",
"weekdays": [0], # Каждый понедельник
"recurrence_until": "2026-01-01"
}
response = requests.post(url, headers=headers, json=data)
Создание личного события
data = {
"employee_id": 123,
"title": "Встреча с заказчиком",
"start_date": "2025-11-20",
"start_time": "14:00",
"end_time": "15:30",
"all_day": false,
"color": "#6C757D",
"location": "Online, Zoom",
"recurrence": "one_time"
}
response = requests.post(url, headers=headers, json=data)
🔍 Тестирование
Checklist для проверки личного календаря:
- Meta-тег:
<meta name="user-id">присутствует на страницах с календарём - UI: Кнопка "Личный" появляется в меню выбора календаря
- API GET:
/api/v1/calendar/events/?employee_id=<user_id>возвращает события - API POST: Создание личного события работает
- Права: Только владелец может редактировать свои события
- Права: Другие пользователи НЕ могут редактировать чужие личные события
- FullCalendar: События отображаются корректно
- Переключение: Смена календаря через dropdown работает
- Модалка: Создание события попадает в нужный календарь
- Цвета: Личные события визуально отличаются от других
Запуск тестов:
# Backend тесты
.venv/Scripts/python backend/manage.py test calendar_app
# API тесты
.venv/Scripts/python -m pytest backend/tests/api/v1/calendar_app/ -v
# Конкретный тест личного календаря
.venv/Scripts/python -m pytest backend/tests/api/v1/calendar_app/test_calendar_api.py::TestPersonalCalendar -v
🎉 Итоговая оценка
Глобальный календарь: 🟢 100% готов
- Backend: ✅
- API: ✅
- Frontend: ✅
- Права: ✅
Календарь отдела: 🟢 100% готов
- Backend: ✅
- API: ✅
- Frontend: ✅
- Права: ✅
Личный календарь: 🟡 95% готов
- Backend: ✅
- API: ✅
- Frontend: ✅ (требует проверки)
- Права: ✅
- Осталось: Проверить UI и meta-теги
📚 Дополнительная документация
Связанные файлы:
- Реализация личного календаря
- Модель CalendarEvent
- API ViewSet
- Сериализаторы
- Виджет FullCalendar
- API клиент
Тесты:
Выводы:
- ✅ Все три типа календарей (глобальный, отдела, личный) полностью реализованы на backend
- ✅ API работает корректно с автоматической проверкой прав
- ✅ Frontend виджет поддерживает все три типа
- 🟡 Требуется проверка UI для личного календаря (добавить meta-тег, протестировать)
- ✅ Система готова к продакшену после финальной проверки
Следующий шаг: Тестирование личного календаря в браузере и добавление meta-тега user-id в base-шаблон.