Corp/docs/in-progress/PERSONAL_CALENDAR_FEATURE.md
Игорь Надеин 48631f43fd Реорганизация документации и скриптов проекта
- Создана структура docs/ с категориями: completed, guides, architecture, diagnostic, in-progress, reports
- Перемещено 47+ MD файлов из корня проекта в docs/
- Удалено 6 устаревших/дублирующихся файлов
- Создана структура backend/scripts/ для тестов и утилит
- Перемещено 35 Python скриптов в backend/scripts/
- Создана структура backend/docs/ для технической документации
- Перемещено 12 MD файлов backend в backend/docs/
- Перемещены LDAP тестовые данные в backend/tests/fixtures/
- Добавлены README файлы для навигации
- Добавлены правила в .github/copilot-instructions.md для размещения документации

Результат:
- Корень проекта: только README.md
- Корень backend/: только manage.py и Django приложения
- Вся документация организована в структурированные папки
2025-12-26 13:49:51 +03:00

9.9 KiB
Raw Blame History

Личный календарь сотрудников

Обзор изменений

Добавлен третий тип календаря — Личный календарь сотрудника. Теперь система поддерживает:

  1. Календарь компании (company) — глобальные события, доступ только у администраторов
  2. Календарь отдела (dept) — события отдела, требуется право MANAGE_CALENDAR
  3. Личный календарь (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

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

Сценарии для проверки:

  1. Переключение на личный календарь:

    • Открыть dropdown → выбрать "Личный"
    • Должны отобразиться только личные события текущего пользователя
  2. Создание личного события:

    • В режиме "Личный" создать событие
    • Событие должно сохраниться с employee_id=текущий_пользователь
  3. Редактирование личного события:

    • Владелец может редактировать/удалять свои события
    • ПКМ на событии → контекстное меню должно показывать кнопки
  4. Изоляция календарей:

    • События личного календаря не должны видеть другие пользователи
    • Администраторы могут видеть, но не редактировать чужие личные события
  5. Проверка прав:

    • Попытка редактировать чужое личное событие должна вернуть 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_id
  • backend/api/v1/calendar/views.py — логика личных календарей
  • backend/calendar_app/migrations/0010_calendarevent_employee_and_more.py — миграция

Frontend:

  • backend/templates/base.html — meta-тег user-id
  • backend/templates/includes/components/calendar_desktop.html — пункт "Личный"
  • backend/templates/includes/components/calendar_mobile.html — пункт "Личный"
  • backend/static/js/components/calendarWidget.js — полная поддержка личных календарей

Дальнейшие улучшения

  • Возможность "поделиться" личным событием с коллегами
  • Синхронизация с внешними календарями (Google, Outlook)
  • Уведомления о личных событиях
  • Экспорт личного календаря в iCal формат