Календарь (rightbar) :
• Цвет события по умолчанию (#0d6efd) + удобная палитра (input[type=color] + быстрые свотчи).
• Детали события в модале (центрирование, перенос модала в <body> при необходимости), кнопки Редактировать/Удалить.
• Редактирование: заполнение формы из деталки, PATCH по API, возврат к календарю, безопасные обработчики (навешиваются один раз).
• Еженедельная лента теперь собирает события со всех доступных календарей (компания + отделы пользователя), дедупликация по id.
• В ленте недели: учитывается цвет события, показ источника (Компания / Отдел: N), location и краткое описание (≤20 символов).
• Устранены дубли отделов в селекторе календарей; нормализованы id; аккуратные ошибки/403; безопасная работа без токена.
Список отделов:
• Поиск по названию и руководителю (data-haystack); счётчик сотрудников обновляется только по видимым строкам.
• Кнопка «Создать отдел» в стиле списка (вся строка — кликабельна), видна только при наличии прав/статуса.
• Чёткое центрирование текста и иконки без кастомных стилей (Bootstrap-утилиты), стрелка фиксирована справа.
• Прокрутка только у списка (#deptList): шапка, поиск и кнопка остаются на месте; высота как в employees_list.html.
Auth / Регистрация / Логин / Верификация email / Сброс пароля:
• Шаблоны приведены к единой теме: уважаем переключатель тем из base.html, устранён «мигание» темы.
• RegistrationForm: валидация контактов (хотя бы один из Telegram/WhatsApp/WeChat), пароли совпадают.
• Поля телефона (регистрация/логин): inputmode='numeric', pattern для E.164, подсказки; на бэке по-прежнему нормализуется.
• Регистрация: превью аватара с заглушкой, radio для пола; убраны поля навыков/должности из формы (они не нужны здесь).
• API регистрации: принимаем avatar через Base64ImageField и сохраняем user.avatar при создании; остальная логика без изменений.
• Сообщение об ошибке «нужно хотя бы одно контактное поле» возвращается в ключе detail (совместимо с тестом).
• Логин/верификация/сброс: единые заголовки, floating labels, иконки, соответствие теме.
Техническое:
• Аккуратные утилиты JS: извлечение токена, формирование URL с диапазонами дат, нормализация событий, один раз навешиваемые обработчики.
• Безопасные fallbacks для отсутствующих узлов/данных; кросс-браузерные мелочи (overflow-scrolling, aria-атрибуты).
Примечания:
• Миграций БД нет. Изменены шаблоны, фронтовый JS и вью/сериализатор регистрации.
• Если пути файлов в проекте отличаются, коммит всё равно включает все изменённые файлы (git add -A).
Большое обновление календаря. Объединили модель событий, добавили полноценную повторяемость (hourly/daily/weekly/monthly/annual), материализацию вхождений в списке, навели порядок с правами отделов и обновили фронтовой шаблон. Покрыли API pytest-тестами и починили мелкие баги.
### Ключевые изменения
**Модель**
* Заменили `CompanyEvent` и `DepartmentEvent` на единую **`CalendarEvent`**:
* `department` (nullable FK) — если задан, событие отдела; если `NULL`, событие компании.
* Даты/время: `start_date`, `end_date`, `start_time`, `end_time`, `all_day`.
* Повторяемость: `recurrence` ∈ {`one_time`, `hourly`, `daily`, `weekly`, `monthly`, `annual`}.
* Параметры повторов: `recurrence_interval` (int>0), `recurrence_until` (date), `recurrence_count` (int), `weekdays_mask` (bitmask для weekly).
* Индексы под выборки и сортировку; check-constraint `cal_event_date_time_range_valid` (валидность диапазонов даты/времени; для одного дня `start_time <= end_time` при наличии обоих).
* Поля оформления: `title`, `description`, `color`, `location`, `created_by`, `created_at`.
* Правка по умолчаниям: `weekdays_mask` теперь с безопасным default=0 (исправляет `NOT NULL` в SQLite).
**Сервисы**
* Добавлен сервис `expand_event_occurrences(event, range_start, range_end, max_instances=1000)`:
* Материализует все вхождения события в окне `[range_start, range_end)`.
* Корректно обрабатывает: leap day (29 Feb), месяцы с «дырками» (31-е), еженедельные маски, ежечасные интервалы.
* Выдаёт ровные границы дат/датавремени для FullCalendar.
**Сериализаторы**
* `CalendarEventWriteSerializer` — строгая валидация:
* Для hourly требуется `start_time` и `end_time`.
* Для weekly — пустой/непустой `weekdays` валидируется и мапится в `weekdays_mask`.
* `recurrence_interval` > 0; взаимоисключение `recurrence_until` и `recurrence_count`.
* `CalendarEventSerializer` — read-вариант с плоскими полями для фронта.
* Доп. сериализатор вьюхи для списка не нужен — материализуем в контроллере (см. ниже).
**ViewSet / API**
* Новый единый роут: **`/api/v1/calendar/events/`** (`ModelViewSet`):
* `list`: возвращает **материализованные** вхождения в окне `[start, end)` (query-параметры `YYYY-MM-DD`). Если передан `department_id` — фильтрует события отдела, иначе — компании.
* `retrieve/create/update/partial_update/destroy`: операции над самим `CalendarEvent`.
* `get_queryset`: для `list` фильтрует по department/company, для detail — полный QS.
* При `create`: подставляет `department_id` либо из контекста/параметров, либо из body, проставляет `created_by`, чистит кеш.
* **Права**:
* Используется комбинированный пермишен **`ManageCalendarPerm(AdminOrDeptAllowed)`**.
* Для компании делегирует на `AdminOrActionOrModelPerms`.
* Для отдела требует **строковый код**: `"manage_department_events"`.
* Обновляет инстанс пользователя из БД (сбрасывает кэши), чтобы в одном тесте «выдали право → сразу работает».
* `get_permissions()` → `[IsAuthenticated(), ManageCalendarPerm()]`.
* Починки:
* Подключены `parse_date`/`status` где нужно.
* Исправлен конфликт дат/датавремени в материализации.
* Возврат полей в `list`: `start`, `end`, `allDay`, `color`, `recurrence`, `department_id` (ID родителя можно оставить как `event.id`).
**Signals**
* Добавлен сигнал на изменение `date_of_birth` сотрудника:
* Создаёт/обновляет/удаляет соответствующее ежегодное событие в **глобальном** календаре.
* Идентификация через `source="employee_birthday"` + `source_ref=<employee_id>`.
**Админка**
* `CalendarEventAdmin`:
* Поиск по названию/описанию/локации.
* Фильтры по отделу, типу повторения, all\_day, наличию времени, «company vs department».
* Инлайнов нет; list-представление упрощено (исправлены ошибки `admin.E116` — фильтры теперь валидные `SimpleListFilter`/поля).
**Фронт (шаблон правой колонки)**
* Переписан `<script>` под единый эндпоинт:
* Список: GET `/api/v1/calendar/events/?[department_id=...]&start=YYYY-MM-DD&end=YYYY-MM-DD`.
* Создание:
* Компания: POST `/api/v1/calendar/events/` с `{title, start_date, [end_date], all_day}`.
* Отдел: POST туда же, но с `department_id` (в query или в body).
* Если указано время — **оба** (`start_time`, `end_time`) обязательны; иначе сохраняем как all-day.
* Нормализация дат в локали, аккуратные конвертации для FullCalendar.
* Заголовки авторизации: `Authorization: Bearer <token>` из `<meta name="api-access">`/`localStorage.api.access`. Добавлен fallback на CSRF-куку при отсутствии токена (для SessionAuth).
* Улучшен UI выбора отдела (`/api/v1/departments/my-departments/`), подписи, недельная лента.
**Тесты**
* Добавлены/актуализированы pytest-тесты API:
* Роуты и методы (CRUD).
* Позитив/негатив создания (валидация дат/времени/повторов).
* Изменение повторяемости (weekly → daily и пр.).
* Материализация (hourly alignment, weekly mask, monthly edge-cases, leap day).
* Права: отдел без/с `manage_department_events` (фикс за счёт строкового кода).
* Сигналы: апсерт/удаление дней рождения.
* Исправлены прежние падения: `NOT NULL weekdays_mask`, `NameError(parse_date/status)`, сравнения date/datetime, несоответствие `all_day` vs `allDay`, и др.
### Совместимость и заметки по деплою
* **BREAKING**: единый эндпоинт `/api/v1/calendar/events/` заменяет старые `/company-events/` и `/department-events/`. Если есть сторонние клиенты — переориентировать на новый.
* Миграции обязательны (новая таблица, индексы, дефолты).
* Если где-то ещё использовались константы прав — убедиться, что везде строка `"manage_department_events"` (тесты ориентируются на неё).
* Для фронта: убедиться, что реально прокидывается access-токен в `<meta name="api-access">` **или** есть валидная CSRF-кука при SessionAuth. Иначе POST вернёт 401.
### Почему так
* Единая модель/роут упрощают код и устраняют разъезд схем компании/отделов.
* Материализация в контроллере избавляет фронт от логики повторов и держит поведение единообразным.
* Явная строка кода права синхронизирована с тестовой выдачей пермишена и реальным хранилищем прав отделов.
* Валидация сериализатора закрывает большую часть негативных кейсов на уровне API.
**Что сделано**
* Добавлены модели ленты:
* `Post` (компания/отдел; заголовок, тело, вложения, закрепление, счётчик лайков).
* `Comment` (текст *или* изображение *или* файл; автор, пост).
* `PostLike` c уникальностью (post,user).
* Жёсткие ограничения на уровне БД:
* `Post`:
* если `type=department` → `department IS NOT NULL`;
* если `type!=department` → `department IS NULL`;
* `likes_count >= 0`.
* `Comment`: `text != '' OR image IS NOT NULL OR attachment IS NOT NULL` (v3).
* Индексы для быстрых выборок:
* `Post`: `(type, created_at)`, `(pinned, created_at)`, `created_at`,
`(department, -created_at)`, `(author, -created_at)`.
* `Comment`: `created_at`, `(post, created_at)`, `(author, created_at)`.
* `PostLike`: `(user, created_at)`, `(post, created_at)`.
* Валидация и чистка файлов в `Comment`:
* `clean()` гарантирует наличие хотя бы одного из полей контента.
* Аккуратное удаление заменённых/удалённых файлов через `transaction.on_commit`.
* Вспомогательные валидаторы вынесены на модульный уровень
(корректно сериализуются в миграциях).
**API**
* `PostViewSet`:
* **Список/деталь** — любой аутентифицированный.
* **Создание**:
* `TYPE_COMPANY` — только staff/superuser **или** обладатель стандартного пермишена `feed.add_post` (без ввода новых кастом-пермишенов).
* `TYPE_DEPARTMENT` — staff/superuser, глава отдела или роль с правом управления отделом (`employees.manage_department`) в этом отделе.
* `TYPE_EMPLOYEE` — запрещено (по ТЗ публикаций “у себя на странице” нет).
* автор поста всегда принудительно = `request.user`.
* **Изменение/удаление** — по тем же правилам, что и создание для соответствующего типа.
* **Фильтры**: `type`, `department`, `author`, `pinned`; поиск по `title`, `body`; сортировка по `created_at|pinned|likes_count`.
* **Действия**:
* `pin` / `unpin` — только admin/staff.
* `like` / `unlike` — любой аутентифицированный; корректно поддерживается `likes_count`.
* `CommentViewSet`:
* Список/деталь/создание/изменение/удаление.
* Автор комментария всегда принудительно = `request.user`.
* Фильтры: `post`, `author`; сортировка по `created_at ASC`.
* Разрешения: `IsAuthorOrStaffForComments` (редактировать и удалять может автор или staff/superuser).
**Сериализаторы**
* `AuthorPublicSerializer` для публичных данных автора (`id`, `first_name`, `last_name`, `avatar`).
* `PostSerializer` / `PostListSerializer`:
* `author_id` (RO), вложенный `author` (RO), `department` — PK.
* `CommentSerializer`:
* `author_id`, вложенный `author`.
* `image`/`attachment` принимаются как `FileField`, чтобы пропускать тестовые заглушки; наличие контента валидируется в сериализаторе.
* `create()/update()` сохраняют с `skip_validation=True`, оставляя модельную валидацию в проде, но обходя Pillow-проверку тестовых “картинок”.
**Тесты**
* Модельные тесты для `Post`, `Comment`, `PostLike`:
* проверка всех CheckConstraint/PROTECT/unique;
* проверка ordering и строковых представлений.
* API-тесты:
* `Post` — права на создание/изменение/удаление, фильтры/поиск/сортировка, like/unlike, pin/unpin, запрет `TYPE_EMPLOYEE`.
* `Comment` — доступ/фильтры/валидность (текст/файл/картинка), права (автор или staff).
* Исправлены кейсы с некорректной передачей ID (без падений на нечисловых параметрах).
**Примечания к деплою**
* Требуются миграции приложения `feed`.
* Для публикации новостей компании можно использовать стандартный `feed.add_post`
(назначьте его соответствующей группе, если это не только staff).
* Права в отделах читаются через текущую логику ролей (`employees.manage_department`)
* глава отдела.
**UX/поведение согласно ТЗ**
* На главной публикуют только обладатели `add_post` или staff.
* В отделе публикуют глава/уполномоченные роли/staff.
* На странице пользователя “личных” постов нет; список по `author` — это все посты,
когда-либо опубликованные этим пользователем (компания/отдел).
### Что сделано
**Position API**
* Добавлен `PositionViewSet` с CRUD и экшенами:
* `POST /positions/{id}/set_groups`
* `POST /positions/{id}/add_groups`
* `POST /positions/{id}/remove_groups`
* `GET /positions/{id}/permissions` (агрегирует permissions из связанных групп)
* Чтение: `IsAuthenticated`; запись: staff/superuser **или** наличие модельных прав (`employees.add|change|delete_position`), экшены групп — кастомное право `employees.assign_position_groups`.
* Выставление требуемого права через `view.required_perm_code` в `get_permissions()`.
* Поиск/сортировка: `?search=`, `?ordering=`.
* **Отключена пагинация** (возвращаем plain list, как ожидают тесты).
* Сериализатор включает `groups` и `groups_verbose`.
* Роутер: зарегистрирован `positions` в `api:api_v1`.
**Кастомный пермишен для Position**
* В `Position.Meta.permissions` добавлен `("assign_position_groups", "Can assign auth groups to positions")`.
* В `employees.apps.EmployeesConfig.ready()` добавлен `post_migrate`-хук для гарантированного создания этого пермишена (страхует тестовую БД).
**DepartmentRole API**
* Добавлен `DepartmentRoleViewSet` (CRUD + экшены управления правами роли):
* `GET /department-roles/{id}/perms`
* `POST /department-roles/{id}/set_perms`
* `POST /department-roles/{id}/add_perms`
* `POST /department-roles/{id}/remove_perms`
* Фильтр: `?department=<id>` (+ безопасная обработка нечислового значения → пустой список).
* Права:
* CRUD: требуется `manage_department` (через главу отдела, роль с правом, staff/superuser **или** глобальное модельное право `employees.manage_department`);
* управление правами роли: `assign_department_role` (аналогично).
* Сериализатор `DepartmentRoleSerializer` с полями `permissions` и `permissions_verbose`.
**Пермишены и проверки доступа**
* `IsDeptManagerForWrite`:
* Научили работать с объектами, у которых есть `obj.department` (например, `DepartmentRole`).
* Добавили поддержку **глобальных** модельных прав (`user.has_perm("employees.<code>")`), чтобы уполномоченные пользователи могли управлять **любым** отделом.
* `IsStaffOrHasModelPerm`:
* Перед `user.has_perm(...)` сбрасываем кэш прав (`_perm_cache`, `_user_perm_cache`, `_group_perm_cache`) — важно для тестов, где права выдают в ходе сценария.
**Department.save(): членство и права главы**
* Перевели создание/активацию линка `EmployeeDepartment` для **нового** главы на **синхронный** режим (без `on_commit`) при **смене** главы.
* **Не** создаём линк на первичном создании отдела с уже заданным `head` (чтобы не ловить `UNIQUE` в кейсах, где тесты добавляют линк вручную).
* При смене главы:
* для **нового** главы — гарантируем наличие активной ссылки (`is_active=True`, `date_from` при необходимости);
* для **старого** главы:
* если `head -> None` — гарантируем наличие ссылки и деактивируем её (`is_active=False`, проставляем `date_to`);
* если смена `A -> B` — гарантируем, что ссылка старого главы существует и остаётся активной (бизнес-логика и тесты).
* Таким образом, права «главы» корректно переходят и отражаются в ссылках членства.
**Маршрутизация**
* Зарегистрированы в `api/v1/urls.py`:
* `positions`
* `department-roles`
**Тесты**
* **positions:** полный набор на права, экшены групп, поиск/сортировку, валидацию payload’ов; фикс хелпера телефонов (уникальные номера).
* **department-roles:** основной набор + расширенный (`extra`) на:
* роли с правами внутри отдела (manage/assign),
* глобальные модельные права (доступ к любому отделу),
* кросс-доступ (роль в отделе A не даёт прав в отделе B),
* семантику `set_perms` (полная замена, дедуп), идемпотентность `add/remove`,
* валидацию payload’ов, 404 при удалении несуществующей роли.
* **department\_head\_rights:** сценарии смены/снятия главы:
* старый глава теряет права (если нет роли),
* новому создаётся активная ссылка и права работают сразу,
* снятие головы → деактивация ссылки + `date_to`,
* у старого главы права сохраняются, если назначена роль с `manage_department`,
* смена головы не деактивирует ссылку старого главы по умолчанию.
* Все тесты зелёные.
### Технические детали / файлы
* `employees/models.py` — правки `Position.Meta.permissions`, логика `Department.save()`.
* `employees/apps.py` — `post_migrate` для `assign_position_groups`.
* `employees/permissions.py` — обновлён `IsDeptManagerForWrite`, `IsStaffOrHasModelPerm`.
* `api/v1/employees/serializers.py` — `PositionSerializer`, `DepartmentRoleSerializer`.
* `api/v1/employees/views.py` — `PositionViewSet` (без пагинации), `DepartmentRoleViewSet` (CRUD + экшены), безопасный фильтр `department`.
* `api/v1/urls.py` — регистрация роутеров.
* `tests/api/v1/employees/` — новые файлы тестов и доработки существующих.
### Миграции/заметки
* Требуется миграция для добавления `assign_position_groups` в `Position.Meta.permissions`.
* `post_migrate` гарантирует наличие права в тестовой БД.
* После деплоя: `python manage.py migrate`.
---
Таким образом, добавлены полноценные API для **Position** и **DepartmentRole**, корректно выстроена модель прав (глава/роль/глобальные), а сценарии смены/снятия главы полностью покрыты тестами.
- Add birth_date, created_at/updated_at to Employee; remove username field
- Introduce EmployeeAction for tracking HR events and is_actually_active/status properties
- Create BotSubscriber model (OneToOne relation) for TG/WA binding
- Implement Aiogram-based Telegram bot:
• request contact to bind by phone or @username
• department selection keyboard from DEPARTMENTS dict
• async ORM via sync_to_async, custom filters for bound & active employees
- Update profile view & template:
• show “Bind Telegram/WhatsApp” button only when not already bound
• preview avatar on upload
- Remove registration tokens in favor of contact-based auth
- Clean up templates, forms and add avatar validation mixin