Ключевые изменения
* Перевели доступ к объектам отдела (посты) на **ролевую модель внутри отдела**:
* Используются `DepartmentRole.scoped_permissions` (M2M к `DepartmentPermission`).
* Проверка прав идёт по **коду** из `DeptPerm` (`CREATE_POST`/`MANAGE_FEED`), а не по `auth.Permission`.
* **Company-посты** теперь контролируются **модельными правами** (`add_post`, `change_post`, `delete_post`) или статусом staff/superuser.
* Убраны остатки старой логики `publish_company_post` / `publish_department_post` (как глобальные права).
Пермишены / утилиты
* `user_has_dept_perm(user, dept, code)`:
* true для staff/superuser и для руководителя отдела;
* иначе проверка активной связи `EmployeeDepartment(is_active=True)` и наличия `role__scoped_permissions__code=...`.
* ВАЖНО: используем поле **`code`**, не `codename`.
* `has_dept_perm(user, department_id, code)`:
* те же правила, оптимизирован поиск (select\_related для роли).
* `AdminOrDeptAllowed`:
* Корректно извлекает `department_id` из `kwargs`/`data`/`query_params` и из объекта (`obj.department_id/department.id`).
* Если код не задан — SAFE-методы разрешены (чтение), небезопасные запрещены.
* `AdminOrActionOrModelPerms`:
* staff/superuser — полный доступ;
* поддержка явного `required_perm_code`/`required_perms_by_action`;
* SAFE-методы требуют `view_<model>`, небезопасные — стандартная карта DjangoModelPermissions.
* `IsSelfOrStaff`/`IsSelfOrStaffOrModelPerms`:
* Владелец объекта (по `author_id`/`author` и др.) или staff/superuser может изменять/удалять;
* SAFE-методы — для авторизованных.
API: PostViewSet
* Обновлён `get_permissions()`:
* `list/retrieve/create/like/unlike` → `IsAuthenticated`;
* `update/partial_update/destroy` → `AdminOrDeptAllowed` с `required_code_map`:
* `update/partial_update/destroy` → `DeptPerm.MANAGE_FEED`.
* Бизнес-проверки:
* `perform_create`:
* `type=department` → разрешено staff/superuser, head отдела, либо роль с `DeptPerm.CREATE_POST`;
* `type=company` → нужны модельные права `add_post` (или staff).
* `author` проставляется на `request.user`.
* `perform_update/perform_destroy`:
* dept-посты: staff/superuser, head, либо роль с `DeptPerm.MANAGE_FEED`;
* company-посты: модельные права `change_post`/`delete_post` (или staff).
* Производительность:
* Аннотация `is_liked` (через `Exists`).
* Аннотация `last_comment_id` + батч-подтягивание последнего комментария в `list()`.
* Лайки:
* `like/unlike` — идемпотентные; счётчик корректируется атомарно.
API: CommentViewSet
* `permission_classes = [IsAuthenticated, (IsSelfOrStaff | AdminOrActionOrModelPerms)]`.
* `perform_create` всегда ставит `author=request.user`.
* Фильтры `?post=`/`?author=` и сортировка `created_at ASC`.
* Обновлён `IsSelfOrStaff` для чтения настроек владельца с вью (например, `owner_id_attrs=("author_id",)`).
Тесты
* Добавлен/переписан комплект для новой модели доступа (фид):
* **Аноним**: все эндпоинты → 401.
* **Обычный пользователь**:
* `GET posts/comments` → 200;
* `POST comment`, `like/unlike` → 201/200;
* `POST/PUT/PATCH/DELETE dept-post` → 403 (без роли/прав).
* **Роль отдела**:
* С `DeptPerm.CREATE_POST` — может создать пост **в своём отделе**; **в чужом** → 403.
* С `DeptPerm.MANAGE_FEED` — может редактировать/удалять **в своём отделе**.
* **Head отдела** — полный доступ к постам своего отдела.
* **Staff/superuser** — полный доступ; `pin/unpin` только staff/superuser.
* **Глобальные модельные права на company**:
* `add_post`/`change_post`/`delete_post` → CRUD **только** для company-постов; dept-посты по-прежнему 403.
* **Комментарии**:
* Создание — любой аутентифицированный;
* Изменение/удаление — автор или staff/superuser.
* **Лайки**:
* Идемпотентность `like/unlike`, корректная синхронизация `is_liked`.
* **Валидация**:
* `type=department` требует `department`;
* роль в одном отделе не даёт создавать в другом → 403.
* Тестовые хелперы:
* `_ensure_permission(model, codename, name)` — создаёт/возвращает `auth.Permission` для company-кейсов;
* `_ensure_dept_permission(code)` и `_give_dept_role_with_perms(...)` — для департаментных прав (через `DepartmentPermission`/`DepartmentRole.scoped_permissions`).
Fixed / багфиксы
* `FieldError: Unsupported lookup 'codename'…` — заменён на фильтр по `scoped_permissions__code`.
* `IntegrityError: NOT NULL constraint failed: feed_post.author_id` — в `perform_create` постов/комментариев author задаётся принудительно.
* Несоответствие тестам по правам чтения/изменения комментариев — исправлено через корректный пермишен владельца.
Файлы (основные)
* `api/v1/permissions.py`: `AdminOrDeptAllowed`, `AdminOrActionOrModelPerms`, `IsSelfOrStaff`(+вариант `IsSelfOrStaffOrModelPerms`), `user_has_dept_perm`, `has_dept_perm`.
* `api/v1/feed/views.py`: `PostViewSet`, `CommentViewSet` (правки прав, `perform_*`, аннотации, like/unlike).
* `tests/api/v1/feed/test_posts.py`, `tests/api/v1/feed/test_comments.py` (и/или `test_feed_api.py`): полный набор сценариев под новую политику.
* Хелперы тестов: функции создания отделов, прав и ролей, генераторы payload.
Итог
* Политика доступа фида приведена к спецификации: **dept-права — через роли внутри отдела**, **company — через модельные права**.
* Исправлены ошибки в проверках прав и авторстве, покрыто автотестами (CRUD, лайки, комменты, валидация, пермишены).
* Производительность чтения ленты улучшена (аннотации `is_liked`, `last_comment_id` без N+1).
— Переверстал backend/templates/employees/department_detail.html под раскладку как в employee_detail:
• левая колонка (md-4): «Команда отдела» (круглое окно) + инфо блока;
• правая колонка (md-8): collapse-блок «Настройки отдела» в стиле ios-card + «Публикации отдела».
— «Колесо команды»:
• круглое окно с 4 колонками аватарок ВСЕХ участников отдела; каждый следующий ряд смещён на 0.5 размера аватарки;
• аватарки круглые, без подписей; поддержка тёмной/светлой темы;
• автоскролл с дуговым индикатором прогресса по правой полуокружности;
• hover — пауза автоскролла, включается ручной скролл (колёсико/тач);
• клик по аватарке — фокус: аватарка растягивается на весь круг; под кругом центрировано показываются имя/роль/email;
• если в отделе нет сотрудников — в круге показывается руководитель;
• a11y: aria-атрибуты для живого региона и описаний.
— Роли/кнопки:
• кнопки Invite / Set role / Remove показываются только при наличии прав (is_superuser|is_staff|can_manage|can_assign_roles|head);
• fixed: «Назначить роль» больше не падает с NoReverseMatch — action формируется из шаблона `/employees/0/departments/<dept>/set-role/` и подменяется на выбранный empId по клику;
• формы модалок: мелкие правки, валидация, улучшения UX (ajax-поиск, активное состояние, блокировка submit, плавный скролл к collapse на мобильных).
— Настройки отдела:
• оформлен collapse-блок «Настройки отдела» в стиле ios-card (как в редакторе профиля);
• добавлена кнопка-шестерёнка в инфо-блоке для быстрого раскрытия;
• include исправлен: `{% include "employees/_department_controls.html" with dept=dept edit_form=edit_form head_choices=head_choices %}`.
— Стили и скрипты:
• локализованы в `{% block extra_js %}` (можно вынести в base позже);
• CSS-переменные для размеров, цветов и дуги; учитывает базовый шаблон и смену темы.
Files:
M backend/templates/employees/department_detail.html
BREAKING CHANGE: нет
Backend / API
- DepartmentViewSet:
- добавлены экшены POST /departments/{id}/add_member/ и /remove_member/
• включают/выключают членство (is_active) без изменения роли
• идемпотентны: add_member активирует существующую связь или создаёт, remove_member не трогает роль
- set_member_role: не создаёт членство, 404 если сотрудник не состоит в отделе; роль проверяется на принадлежность отделу; возвращает {employee_id, role_id|null, is_active}
- get_permissions: исправлено пересечение прав
• add_member/remove_member → только ManagePerm (DeptPerm.MANAGE)
• set_member_role → только AssignRolePerm (DeptPerm.ASSIGN_ROLE)
• set_head → ChangeHeadPerm
• update/partial_update/destroy → ManagePerm
- queryset/поиск/сортировка: аннотации для employees_count, поиск по имени/описанию/ФИО руководителя, сортировка по name/id
- Исправления валидации head в partial_update/set_head: «ослабление» требования email_verified, если меняет текущий руководитель
Frontend / Views & Templates
- employees/views_front.py:
- department_detail: берёт агрегат /api/v1/departments/{id}/ui-context/; формирует members_exclude_ids (участники+руководитель)
- добавлены прокси-вьюхи department_add_member / department_remove_member (POST) — ходят в API, показывают messages и редиректят обратно
- employees_list: поддержан формат ?format=json для фронт-поиска (без прямого запроса в DRF → нет 401)
- set_member_role: прокси, защита от изменения собственной роли
- templates/employees/department_detail.html:
- «голая» шестерёнка в правом нижнем углу инфоблока; запоминает состояние (localStorage), аккуратные отступы
- описание отдела: white-space: pre-line + overflow-wrap:anywhere (учёт переносов и длинных слов), уменьшен верхний «воздух»
- секция «Команда отдела» переделана в стилях блока «Профиль» (как в employee_detail.html):
• строки-элементы с аватаром, ФИО, текущей ролью
• форма смены роли (если есть права)
• две отдельные кнопки: «Пригласить» (добавить) и «Удалить»
- два модальных окна:
• Пригласить (add): поиск по всем сотрудникам, исключая уже состоящих/руководителя (members_exclude_ids)
• Удалить (remove): поиск только по сотрудникам текущего отдела; исключаем самого себя
• список с аватарками, выбор подсвечивается; сабмит не активен до выбора; клавиши Enter/Escape
- JS для модалок: поиск через фронтовую employees_list (?format=json), дебаунс, «портал» меню в body, обработка ошибок
- templates/employees/_department_controls.html:
- «Новая роль»: фикс разворота/скрытия формы, сброс состояний
- выпадающее меню ролей рендерится в body (не клипается родителями)
- кнопка «Отмена» теперь полноценная кнопка (btn btn-link), работает
- чипсы прав + скрытые permission_ids синхронизируются корректно
Тесты
- tests/api/v1/employees/test_department_membership_separation.py (новые/обновлённые):
- test_add_member_requires_manage_and_does_not_assign_role
- test_remove_member_requires_manage_and_does_not_touch_role
- test_set_member_role_requires_assign_not_manage
- test_set_member_role_does_not_create_membership_and_does_not_toggle_active
- test_all_three_actions_require_auth_and_permissions
- фиксы фабрик/тест-данных: гарантированно уникальные email/phone_number (устранён IntegrityError: UNIQUE constraint failed)
- поправлены ожидания статусов (401 для неаутентифицированных, 403 без прав, 200 при корректных правах); для set_member_role — 404, если членства нет
Bugfixes / прочее
- VariableDoesNotExist (list) в department_detail: использован безопасный контекст и корректный default
- 401 при поиске сотрудников из шаблона: переведено на фронтовую прокси-вьюху (?format=json), вместо прямого запроса в DRF
- мелкие правки стилей и отступов, соответствие стилю «Профиль»
Как проверить
1) API-права:
- без аутентификации: все 3 экшена → 401
- с аутентификацией, но без прав: add/remove → 403; set_member_role → 403
- с DeptPerm.MANAGE: add/remove → 200; set_member_role → 403
- с DeptPerm.ASSIGN_ROLE: set_member_role → 200; add/remove → 403
2) set_member_role не создаёт членство и не меняет is_active; add/remove не трогают роль
3) UI:
- шестерёнка показывает/скрывает настройки; состояние помнится
- модалки «Пригласить»/«Удалить»: поиск, выбор, сабмит; аватарки; корректные фильтры (исключения и текущий отдел)
- «Отмена» работает, кнопки и выпадающие списки не клипаются
BREAKING CHANGES: нет
Migrations: нет
### Backend / API
* **DepartmentViewSet**
* Вернул поведение поиска/сортировки как в старой реализации: `?search=` по имени/описанию/ФИО руководителя; `?ordering=name|id` (по умолчанию `name`).
* Добавил аннотацию **`employees_count`**: считает уникальных активных участников по связям `EmployeeDepartment`, и **прибавляет руководителя**, если он не входит в активные линкы.
* `partial_update`: смена руководителя требует `change_department_head` **только если значение реально меняется**; остальное редактируется по `manage_department`.
* `set_head`:
* `POST /departments/{id}/set_head` — установка нового руководителя (200 + сериализованный отдел).
* `head_id=null|""` — снятие руководителя (204).
* Валидация: **разрешаем** назначение любому существующему и активному (`is_active=True`) пользователю; поле `email_verified` не блокирует назначение.
* `set_member_role`:
* `POST /departments/{id}/set_member_role` — создание/обновление линка участника с отделом.
* Валидирует, что `role_id` принадлежит текущему отделу.
* Возвращает JSON строго в формате, который ждут тесты: `{"employee_id", "role_id", "is_active"}`.
* **DepartmentRoleViewSet**
* Доступ на изменение ролей отделов только при скоуп-праве **`assign_department_role`** (через `AdminOrDeptAllowed`).
* Новые эндпоинты:
* `GET /department-roles/perm_choices/` — отдает справочник прав из `DeptPerm.CHOICES`; при необходимости создаёт записи в БД (id + code + name).
* `GET /department-roles/{id}/perms/` — текущие права роли (id, code, name).
* `POST /department-roles/{id}/set_perms` — полная замена прав по `permission_ids` **или** `permission_codes`.
* Использование константного набора прав **`DeptPerm`**: `MANAGE`, `CHANGE_HEAD`, `ASSIGN_ROLE` (+ `CHOICES`).
* **Сериализаторы**
* `DepartmentSerializer`: `head` — вложенно (кратко), `head_id` — write-only (`PrimaryKeyRelatedField`), `employees_count` — read-only (берётся из аннотации).
* `EmployeeBriefSerializer` — для краткого отображения руководителя и кандидатов.
* **Права**
* Везде применён общий пермишен `AdminOrDeptAllowed(required_code=...)` для скоуп-прав по отделу:
* `manage_department` — редактирование/удаление отдела,
* `change_department_head` — назначение/снятие руководителя,
* `assign_department_role` — операции с ролями и назначениями участникам.
### Frontend / Templates
* **Новый частичный шаблон** `backend/templates/employees/_department_controls.html`
* Разбит на **3 блока**:
1. *Управление отделом* (название/описание) — доступно при `can_manage`.
2. *Роли отдела* — доступно при `can_assign_roles`.
* Кнопка «Создать роль» открывает панель с формой + модал выбора прав.
* Редактирование существующей роли: выпадающий список ролей, изменение названия и прав.
* **Одна кнопка «Сохранить изменения»**: JS сам определяет, что изменилось, и вызывает нужные операции (`op=update` и/или `op=set_perms`) последовательно.
* Отдельно — «Удалить роль».
* Выбор прав выполнен через модальные окна, как в блоке с должностями в `_employee_edit.html`; визуал выбранных прав — «чипсы».
3. *Назначение руководителя* — доступно при `can_change_head` (выпадающий список кандидатов).
* Разметка и стили согласованы с `_employee_edit.html` (карточки, ios-аккордеон, кнопки, модалки). Дополнительный CSS **не** вводился — всё опирается на базовые стили проекта и Bootstrap.
* **`department_detail` (view для фронта)**:
* Сначала грузит отдел, затем роли, сотрудников и посты.
* Собирает `head_choices` и `links` для шаблона.
* Подтягивает `dept_perm_choices` из `GET /api/v1/department-roles/perm_choices/`.
### Тесты
* Полностью переписаны/обновлены:
* `tests/api/v1/employees/test_departments.py`
* Поиск/сортировка по имени.
* `employees_count` считает head, если его нет в активных линках.
* `set_head`: валидирует активность; снятие — 204; изменения head без доп. права — 403; сохранение того же head — 200.
* `set_member_role`: «счастливый путь», запрет чужих ролей, формат ответа.
* `tests/api/v1/employees/test_department_roles.py` и `test_department_roles_extra.py`
* Список с фильтром по отделу, стабильный `ordering`.
* Создание/обновление/удаление с учётом скоуп-прав.
* `perm_choices`, `perms`, `set_perms` (валидаторы ids/codes).
* `tests/api/v1/employees/test_department_head_rights.py`
* Руководитель может править отдел без явных ролей.
* Руководитель может менять head и **теряет** права после снятия.
* Фиксы фикстур:
* `make_user` всегда создаёт пользователя с валидным телефоном, `email` в нижнем регистре, отключена отсылка писем; приводим к активному состоянию, чтобы не мешать правам.
* Убраны ошибки:
* `NameError: User is not defined` → везде используется `get_user_model()`.
* `NoReverseMatch` в шаблоне → корректные URL’ы: `employees:department_edit`, `employees:department_set_head`, `employees:edit_department_role`.
### Прочее
* Повсеместно добавлены докстринги для новых классов и функций (соответствие внутреннему гайдлайну).
* Без миграций данных/схемы.
**Итог:** добавлены управляемые через API роли отделов с выбором прав (скоуп-прав), унифицированы проверки доступа, восстановлено ожидаемое поведение списка/деталей отделов, подготовлены UI-блоки в едином стиле с «должностями», обновлены и позеленели тесты (остальные регрессии устранены).
- **EmployeeActionSerializer**
- Добавлен `history[]` со снапшотами simple-history и diff по полям: `action`, `date`, `comment`, `extra`, `employee_id`.
- Локализация названий событий через `employees.constants.ACTION_CHOICES`.
- Поля `action_display`, `date_display` для удобного фронтового рендера.
- **EmployeeSerializer**
- Возвращает `actions` (массив событий) с историей, **только** если в контексте выставлено `include_action_history=True`.
- Навыки: чтение — `skills` как список объектов `{id, name}`, запись — через `skills_ids`.
- Должность: чтение — объект `{id, name}`, запись — через `position_id`.
- Отделы: добавлен флаг `is_head` (сотрудник — руководитель отдела).
- **EmployeeListSerializer**
- Те же правки для навыков/должности и `is_head`, без `actions` (для производительности).
- **Views (EmployeeViewSet)**
- `get_queryset`: префетч `actions` добавляется **только** для `retrieve/me`, иначе не тянем историю.
- `get_serializer_context`: для `retrieve` и `me` выставляем `include_action_history=True`.
- Экшен `me`: выборка с `Prefetch("actions", order_by -date)` и передачей `include_action_history=True`.
- **Разное**
- Используем `ACTION_CHOICES` для «человеческих» названий событий в истории.
- `HistoryActionMixin` оставлен без изменений; история объектов доступна по `/{basename}/{pk}/history/`.
Почему так
- История кадровых событий бывает тяжёлой → грузим и отдаём её только там, где нужно (`retrieve/me`).
- Явное разделение read/write для навыков и должности упрощает фронту жизнь и убирает двусмысленность.
- `is_head` позволяет фронту не вычислять этот флаг отдельно.
Файлы
- `backend/api/v1/employees/serializers.py`
- `backend/api/v1/employees/views.py`
- (использованы константы из) `backend/employees/constants.py`
Тесты/проверка
1) GET `/api/v1/employees/1/` → поле `actions[]` заполнено, внутри каждого элемента `history[]` со снапшотами и переводами событий.
2) GET `/api/v1/employees/?page=1` → `actions` отсутствуют (по замыслу), навыки и должность приходят в «красивом» виде.
3) GET `/api/v1/employees/me/` → аналогично `retrieve`, с историей.
* UI (templates/employees/employee\_detail.html)
* Навыки: больше не итерируем по строке, выводим объекты `{id,name}` (`s.name`).
Защита от пустых списков.
* Отделы: убрал «аватарки» у элементов списка, оставил иконку у кнопки раздела.
Роль сотрудника показывается «Название отдела — Роль».
Для руководителя добавлен понятный бейдж с иконкой `bi-award-fill` + tooltip «Руководитель отдела».
* Кадровые события: добавлен раздел «Кадровые события» (для аутентифицированных).
Отрисовывает список всех событий (дата, комментарий, extra), а также историю каждого события (`action.history`) в `<details>`.
В истории показываем только название события и новые значения полей (без «создано/изменено» и без зачёркнутых старых значений).
Перевод кодов событий (`hired`, `dismissed`, …) в человекочитаемый вид согласно `employees/constants.py`.
* Аккордеон: кнопка и выпадающая панель объединены в один блок `.ios-acc` единой ширины.
Добавлена разделительная линия между заголовком и контентом (`.ios-acc-body { border-top: ... }`).
Исправлены дёрганья при анимации: убраны внешние отступы у `.collapse`, возврат стандартной скорости Bootstrap (`.35s ease`), `overflow:hidden` на фазе `collapsing`.
* Views (employees/views.py — веб-приложение)
* В `employee_detail` и `employee_me` дополнительно запрашиваем полную историю кадровых событий:
`GET v1/employee-actions/?employee=<id>&ordering=-date` и передаём в шаблон `employee_actions`.
Тем самым отображаем все события, а не только последнее.
* Шаблон умеет брать события из `employee_actions`, а при отсутствии — из `emp.actions`.
* API (backend/api/v1/employees/serializers.py)
* `EmployeeActionSerializer`: добавлен `history` (MethodField) с формированием массива изменений из `HistoricalRecords` (без старых значений; только `changes[field].new`, дата/пользователь).
Добален `action_display` и `date_display` для удобного рендера на фронте.
* `EmployeeSerializer`: возвращает `departments` через `get_departments()` (id, name, role\_id, role\_name, is\_head) и `skills` как массив объектов `{id,name}`.
* Прочее/фиксы
* Безопасные проверки на пустые коллекции (контакты, навыки, отделы, события).
* Устранена ошибка шаблона «Failed lookup…» при попытке обратиться к отсутствующим ключам.
* Не изменяем скорость анимации: возвращён дефолт Bootstrap; корректное закрытие панелей.
Refs: employees/constants.py (ACTION\_CHOICES), templates/employees/employee\_detail.html, employees/views.py, api/v1/employees/serializers.py
### Skill
* Добавлен `SkillViewSet`:
* CRUD: `/api/v1/skills/`
* Любой авторизованный может **создавать** навыки.
* Изменение/удаление — только `staff/superuser` или при наличии модельных прав:
* `employees.change_skill` (PATCH/PUT, а также `merge`)
* `employees.delete_skill` (DELETE)
* Поиск/сортировка: `?search=` по `name`, `?ordering=name|-name`, без пагинации.
* Экшены:
* `POST /api/v1/skills/bulk_create` — массовое создание (`names: string[]`, нормализация, дедуп).
* `POST /api/v1/skills/merge` — объединение навыков:
* body: `{ source_id, target_id, reassign=true, delete_source=true }`
* переносит связь сотрудников `source → target`, опционально удаляет `source`.
* идемпотентно, учитывает случаи когда у сотрудника уже есть `target`.
* Сериализатор: `SkillSerializer`.
* Роутинг: зарегистрирован `skills`.
### EmployeeAction
* Добавлен `EmployeeActionViewSet`:
* CRUD: `/api/v1/employee-actions/`
* Чтение — для авторизованных.
* Запись — `staff/superuser` или держатели модельных прав:
* `employees.add_employeeaction`
* `employees.change_employeeaction`
* `employees.delete_employeeaction`
* Фильтры: `?employee=<id>`, `?date_from=`, `?date_to=`.
* Сортировка: по умолчанию `-date`.
* Бизнес-эффекты:
* `ACTION_DISMISSED` — деактивация сотрудника (`employee.is_active=False`) и всех активных `EmployeeDepartment` с выставлением `date_to=today`.
* Любое иное действие — активирует сотрудника (`employee.is_active=True`), без автоматического восстановления отделов.
* Сериализатор: `EmployeeActionSerializer` (если `date` не передан — проставляется `now()`).
* Роутинг: зарегистрирован `employee-actions`.
### Тесты
* `tests/api/v1/employees/test_skills.py`:
* список/поиск/сортировка;
* создание обычным пользователем;
* права на обновление/удаление (включая staff bypass);
* `bulk_create` (нормализация и дедуп);
* `merge` (переназначение, параметры `reassign/delete_source`, идемпотентность).
* `tests/api/v1/employees/test_employee_actions.py`:
* доступ и фильтрация по сотруднику;
* создание с правами/без, staff bypass;
* эффекты увольнения (деактивация сотрудника и связей);
* активация при неувольняющем действии;
* эффекты при изменении существующего события на `DISMISSED`;
* удаление с правами.
* **Учёт авто-создаваемых событий**: проверки построены на изменении/baseline, а не на «пустой истории».
### Изменённые файлы (основные)
* `backend/api/v1/employees/serializers.py` — `SkillSerializer`, `EmployeeActionSerializer`
* `backend/api/v1/employees/views.py` — `SkillViewSet`, `EmployeeActionViewSet`
* `backend/api/v1/urls.py` — роуты `skills`, `employee-actions`
* `tests/api/v1/employees/test_skills.py` — новый
* `tests/api/v1/employees/test_employee_actions.py` — обновлён с учётом baseline и добавленными сценариями
### Заметки
* Миграций нет (использованы существующие модели).
* Поведение по автособытиям при создании сотрудника поддержано в тестах (baseline).
### Что сделано
**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**, корректно выстроена модель прав (глава/роль/глобальные), а сценарии смены/снятия главы полностью покрыты тестами.