161 Commits

Author SHA1 Message Date
e24caabb76 **feat: управление отделами и ролями (API + шаблоны) + правки тестов**
### 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-блоки в едином стиле с «должностями», обновлены и позеленели тесты (остальные регрессии устранены).
2025-09-03 17:17:44 +03:00
016ee1ef50 feat(employees): переработка формы редактирования, создание должности и мобильный выбор групп + валидации и UX-полировка
Основное
- Переписан шаблон `templates/employees/_employee_edit.html`:
  - Разделы: «Личные данные», «Контакты», «Должность» (аккордеон-блоки с единым отступом `.py-3` в заголовках).
  - Поля: first_name, last_name, patronymic, gender (коды 1/2, допускается `null`), birth_date.
  - Контакты: phone_number, email, telegram, whatsapp, wechat.
  - Валидация:
    - требуем минимум один контакт (любой из: email/phone_number/telegram/whatsapp/wechat);
    - e-mail валидируется HTML5 + regex-фолбэком; live-очистка ошибок.
  - Должность:
    - выпадающий список из `all_positions`;
    - доступ к редактированию/созданию скрывается/показывается по флагам контекста (см. ниже);
    - «Создать должность» (name/description) + выбор групп прав.
  - Выбор групп прав:
    - мобильный модальный пикер (поиск, чекбоксы, чипсы выбранных значений, скрытый input с csv id);
    - модал центрируется (`.modal-dialog-centered`), выносится в `document.body`, задран z-index (`.modal{z-index:1080}`, backdrop 1075), убран «просвет» (принудительный `background-color`).
  - Кнопки: индикаторы загрузки (label + spinner), аккуратная блокировка в процессе.

JS (внутри того же шаблона)
- Сборка payload для `PATCH`:
  - отправляем только изменённые поля;
  - для `gender`, `birth_date`, `position_id` при очистке отправляем `null`;
  - `gender` приводим к числу;
  - поддержка `skills_ids` (мультиселект): сравнение с `data-init`, отправка массива только при изменении;
  - `avatar`: чтение файла → dataURL (Base64ImageField), очистка через пустую строку.
- Валидация перед отправкой:
  - `ensureOneContact()` — хотя бы один контакт обязателен;
  - `validateEmail()` — валидный `email`, если поле непустое.
- Обработка ошибок API: подсветка конкретных полей (`.is-invalid` + `.invalid-feedback`), fallback-alert для общих ошибок.
- Успех: закрытие обёртки `#editFormWrap` (если есть collapse) + `location.reload()`.
- Блок «Создать должность»:
  - `POST` в `positions_create_url` с `name/description` и (если разрешено) `groups`;
  - затем `PATCH` сотрудника на `endpoint` с `position_id` созданной должности;
  - обновление селекта должностей.
- Исправления:
  - валидация e-mail перенесена внутрь IIFE (был reference error на `form`);
  - удалён двойной вызов `ensureOneContact()`; оставлена единая связка `ensureOneContact() || !validateEmail()`.

Контекст (ожидается из context_processors, без правок views_front)
- Передавать в шаблон (минимум):
  - `all_positions`: список объектов `{id, name}` (для выпадающего списка);
  - `all_groups`: список объектов `{id, name}` (для модального выбора);
  - `positions_create_url`: URL `POST /api/v1/positions/` (создание должности);
  - флаги доступа:
    - `can_edit_position` — право менять должность сотрудника (или глобальные staff/superuser);
    - `can_create_position` — право создавать должности;
    - `can_assign_position_groups` — право назначать групповые права должности.
  - Также в шаблоне допускаются `request.user.is_staff`/`is_superuser` (поддержаны как запасной сигнал в условиях показа).

API
- Используется уже имеющийся `PositionViewSet` (`/api/v1/positions/`):
  - создание должности с полем `groups` (список id) через `PositionSerializer`.
- Добавлен простой эндпоинт групп (если ещё не было):
  - `GET /api/v1/groups/` (list, опционально `?search=` по `name`);
  - сериализатор: `id`, `name`.
  - Примечание: в шаблоне сейчас используется предзагруженный `all_groups`; эндпоинт нужен для возможной ajax-подгрузки/фильтрации в будущем.

UI/стили
- Единый вертикальный ритм заголовков секций через `.py-3`.
- Модальный выбор групп по центру экрана; корректный stacking-context и непрозрачность.
- Мобильная дружелюбность: полноэкранный на sm-и ниже (`modal-fullscreen-sm-down`), поиск, чеклист, чипсы выбранных.

Как проверить
1) Откройте карточку сотрудника → «Редактировать».
2) Смените/очистите пол, дату рождения, должность — убедитесь, что очистка отправляет `null`, а изменение — нужное значение.
3) Удалите все контакты → форма подсветит ошибки и не отправится; добавьте валидный e-mail — ошибки исчезнут.
4) Создайте новую должность с группами → должность появится в селекте и применится сотруднику.
5) Проверить поведение прав:
   - без `can_edit_position`/staff/superuser — блок должности только для чтения;
   - без `can_assign_position_groups` — кнопка выбора групп скрыта.

Refs: DRF serializers (`EmployeeSerializer`, `PositionSerializer`, `Base64ImageField`), `PositionViewSet` (create + groups).
2025-09-01 17:15:05 +03:00
da183c8fbd мелкие доработки тестов и шаблонов 2025-08-29 17:03:18 +03:00
c78d1e5fea Ключевые изменения
- **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
2025-08-27 17:58:46 +03:00
2fd3b1e562 feat(feed): посты и комментарии — модели, ограничения, 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` — это все посты,
  когда-либо опубликованные этим пользователем (компания/отдел).
2025-08-26 13:13:16 +03:00
d7f0090c76 **feat(employees): Skill API + EmployeeAction API с бизнес-эффектами, роутинг и тесты**
### 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).
2025-08-26 09:00:01 +03:00
cfc76e43b7 **feat(employees): Position & Department API, доработка прав головы отдела, усиление пермишенов и полный набор тестов**
### Что сделано

**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**, корректно выстроена модель прав (глава/роль/глобальные), а сценарии смены/снятия главы полностью покрыты тестами.
2025-08-25 17:01:58 +03:00
5af1f9ec91 feat(api): регистрация/верификация через API, whitelist в middleware, iOS-экран подтверждения выхода
**Что сделано**

* **API**

  * Добавлен `POST /api/v1/auth/register/` — создание пользователя, генерация кода и письмо (повторная отправка, если email есть, но не подтверждён).
  * `POST /api/v1/auth/verify-email/` возвращает `user_id` при успехе.
  * `POST /api/v1/auth/resend-email/` — без изменений по контракту.
* **Фронт (views\_front)**

  * `RegisterView.form_valid` теперь вызывает API `auth/register` (без прямой работы с ORM).
  * `EmailVerifyView.post` переводён на API `auth/verify-email` + автологин при успехе.
  * Повторная отправка кода (`resend_email`) уже через API, добавлена защита от частых запросов по сессии.
  * Исправлена сериализация телефона (E.164/строка) при отправке в API.
* **Клиент API**

  * Новый модуль `employees/api_client.py`: безопасная сериализация payload, удобные ошибки (`api_bad_json status=… body=…`) и абсолютные URL через `request.build_absolute_uri`.
* **Middleware**

  * `AuthRequiredMiddleware` теперь **пропускает**: `/api/v1/**`, статику/медиа, страницы логина/регистрации/сброса пароля, админ-логин и `OPTIONS`. Остальное для анонима — редирект на `login`.
* **Аутентификация/выход**

  * Страница подтверждения выхода (в стиле iOS bottom sheet) + `ConfirmLogoutView`. Пункт «Выйти» ведёт на GET `/auth/logout/`, далее POST подтверждения.
  * Шаблон выхода принимает `next` из контекста (без обращений к `request.GET.next` в шаблоне).
* **UX/мелочи**

  * Устранены 500 при отсутствии `password_reset` — добавлены/учтены URL’ы потока сброса (или безопасные ссылки).
  * Уточнены сообщения об ошибках при регистрации/верификации.

**Как проверить**

1. Аноним может вызвать:

   * `POST /api/v1/auth/register/` → `201 {"ok":true}` / `200 {"ok":true,"resent":true}` / `400 {"error":"email_taken"}`.
   * `POST /api/v1/auth/verify-email/` с корректным кодом → `200 {"ok":true,"user_id":...}`.
   * `POST /api/v1/auth/resend-email/` → `200 {"ok":true}`.
2. UI:

   * Регистрация в форме → обращение к API, письмо с кодом, переход на ввод кода.
   * Ввод кода → API, автологин, редирект в ленту.
   * «Выйти» в меню → страница подтверждения → POST → сообщение «Вы вышли…» и редирект на логин/`next`.
3. Проверить, что `/api/v1/**` не редиректится на логин (middleware whitelist работает).
4. Телефон в форме не ломает JSON (нет `TypeError: PhoneNumber is not JSON serializable`).

**Миграции**: нет.
2025-08-20 17:49:38 +03:00
b401e86249 доработка стилей 2025-08-20 11:19:02 +03:00
Игорь Надеин
a42204dda0 0 2025-08-05 15:30:41 +03:00
Игорь Надеин
6fb2cdcea0 feat: enhance Employee model and integrate Telegram/WhatsApp bot
- 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
2025-07-30 08:15:53 +03:00