Большое обновление по модулю заявок и интеграции с отделами.
## API (DRF)
* `requests_app.views.RequestViewSet`
* `create()`: валидируем write-сериализатором, **отдаём read-сериализатор** → `employee` в ответе теперь объект, а не PK (чинит падение `TypeError: 'int' object is not subscriptable`).
* `perform_create()`: для не-админов принудительно подставляем текущего пользователя.
* `perform_update()`: явные проверки финальности и владения.
* `destroy()`: подключён объектный пермишен `NotFinalOrStaff` через `get_permissions()` — **нельзя удалять финальные заявки** не-админам.
* `get_permissions()`:
* исправлены ветки `comments/approve/reject/cancel/destroy`, убран некорректный «битовый OR» классов пермишенов;
* для `comments` — специальный пермишен (владелец без прав НЕ имеет доступа), соответствует тестам.
* для `approve/reject/retrieve/comments` добавлена интеграция с `AdminOrDeptAllowed` (департаментные права) — основа для отдел-скоупа.
* `get_queryset()`: добавлен **департаментный скоуп** — «свои» + заявки отделов, где у пользователя есть право `view_request` (через `EmployeeDepartment → role.scoped_permissions`).
* `get_object()`: для `retrieve/approve/reject/comments/cancel` объект берётся из полного набора и затем прогоняется через `check_object_permissions()` → возвращаем **403, а не 404** при нехватке прав.
* `pagination_class = None` — `list` стабильно возвращает массив, без обёртки `{count, results…}` (устраняет типовую ошибку парсинга в тесте).
* `requests_app.permissions`
* `CommentsPermission`: политика «только staff/право отдела», **владелец без прав — 403**.
* `NotFinalOrStaff`: объектный пермишен для запрета удаления финальных заявок.
* `requests_app.serializers`
* `RequestWriteSerializer`:
* делаем поля `employee/status/approver/department` **необязательными**;
* в `to_internal_value()` для обычных пользователей **тихо удаляем** запрещённые поля до полевой валидации (чтобы не ловить 400 на несуществующих PK);
* в `create()` для обычных пользователей проставляется текущий пользователь; статус берётся из `default` модели.
## Фронтенд (server-rendered views)
* Добавлены FBV, работающие через наш `backend/api/client.py`:
* `my_requests`, `all_requests`, `request_create`, `request_detail`,
* `request_process` (approve/reject), `request_cancel`,
* `request_comment_add`, `request_comment_delete`.
* Проксируем аутентификацию, корректно отображаем ошибки API, поддерживаем multipart для вложений.
## Тесты
* Добавлен модуль `tests/api/v1/requests_app/test_requests_departments.py` с чек-листом интеграции заявок и отделов:
* видимость по отделам, детальный просмотр, approve/reject по отдел-правам,
* доступ к комментариям по отдел-правам,
* валидация подмены `department` при создании.
* Базовые API-тесты теперь проходят:
* `tests/api/v1/requests_app/test_requests_api.py` — **все зелёные**.
* Департаментные тесты **частично красные** (починим завтра):
```
FAILED tests/api/v1/requests_app/test_requests_departments.py::test_dept_viewer_cannot_retrieve_other_department_request - AssertionError: b'{"detail":"No Request matches the given query."}'
FAILED tests/api/v1/requests_app/test_requests_departments.py::test_dept_processor_can_approve_own_department_request - AssertionError: b'{"detail":"У вас недостаточно прав для данного действия."}'
FAILED tests/api/v1/requests_app/test_requests_departments.py::test_dept_comment_viewer_can_read_comments_in_department - AssertionError: b'{"detail":"Недостаточно прав для доступа к комментариям."}'
FAILED tests/api/v1/requests_app/test_requests_departments.py::test_dept_comment_adder_can_post_in_department - AssertionError: b'{"detail":"Недостаточно прав для доступа к комментариям."}'
```
Причины на текущем этапе:
* `retrieve` местами ещё даёт 404 вместо 403 при отсутствии отдел-прав — добьём маршрут через `get_object()`/пермишены.
* Для `approve/reject` и `comments` требуется довести проверку `AdminOrDeptAllowed(required_code=...)` до полной логики «И» по кодам и убедиться, что в тестовых фикстурах корректно создаются связки ролей/прав отдела.
## Прочее
* Мелкие чистки импортов и докстрингов; типизация (Google-style) по новым функциям/классам.
---
**Итог по прогону:**
`tests/api/v1/requests_app/` — **20 passed, 5 failed** (см. список выше). Базовые сценарии готовы, департаментные — почти готовы; добьём завтра строгую выдачу 403 и все ветки отдел-прав для `retrieve/approve/reject/comments`.
125 lines
4.6 KiB
Python
125 lines
4.6 KiB
Python
# api/v1/requests_app/permissions.py
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Optional
|
|
|
|
from api.v1.permissions import AdminOrDeptAllowed
|
|
from rest_framework.permissions import BasePermission
|
|
from rest_framework.request import Request
|
|
|
|
|
|
class CommentsPermission(BasePermission):
|
|
"""Права доступа к экшену `comments`.
|
|
|
|
- GET/HEAD: staff или право `requests_app.view_requestcomment`.
|
|
- POST: staff или право `requests_app.add_requestcomment`.
|
|
- Владелец заявки без модельных прав доступа не имеет.
|
|
|
|
Raises:
|
|
Ничего не выбрасывает: при отказе возвращает False → DRF вернёт 403.
|
|
"""
|
|
|
|
message = "Недостаточно прав для доступа к комментариям."
|
|
|
|
def has_permission(self, request: Request, view: Any) -> bool:
|
|
"""Проверка уровня запроса.
|
|
|
|
Args:
|
|
request (Request): Текущий запрос.
|
|
view (Any): Текущий вьюсет.
|
|
|
|
Returns:
|
|
bool: True, если пользователь аутентифицирован.
|
|
"""
|
|
return bool(getattr(request.user, "is_authenticated", False))
|
|
|
|
def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
|
|
"""Объектная проверка на уровне конкретной заявки.
|
|
|
|
Args:
|
|
request (Request): Текущий запрос.
|
|
view (Any): Текущий вьюсет.
|
|
obj (Any): Экземпляр заявки (EmployeeRequest).
|
|
|
|
Returns:
|
|
bool: Разрешение/запрет.
|
|
"""
|
|
user = request.user
|
|
if getattr(user, "is_staff", False):
|
|
return True
|
|
|
|
if request.method in {"GET", "HEAD"}:
|
|
return user.has_perm("requests_app.view_requestcomment")
|
|
|
|
if request.method == "POST":
|
|
return user.has_perm("requests_app.add_requestcomment")
|
|
|
|
return False
|
|
|
|
|
|
class NotFinalOrStaff(BasePermission):
|
|
"""Запрещает удаление финальной заявки для не-админов.
|
|
|
|
- Разрешает любые методы, кроме DELETE, без ограничений.
|
|
- Для DELETE: разрешено, если заявка не финальная, либо пользователь — staff.
|
|
|
|
Атрибуты:
|
|
message (str): Текст ошибки для 403 Forbidden.
|
|
|
|
Notes:
|
|
Пермишены не выбрасывают исключения — они возвращают False,
|
|
что приводит к 403 Forbidden.
|
|
"""
|
|
|
|
message = "Финальная заявка не может быть удалена."
|
|
|
|
def has_object_permission(self, request: Request, view: Any, obj: Any) -> bool:
|
|
"""Объектная проверка.
|
|
|
|
Args:
|
|
request (Request): Текущий запрос DRF.
|
|
view (Any): Экземпляр вьюсета.
|
|
obj (Any): Экземпляр удаляемого объекта (заявки).
|
|
|
|
Returns:
|
|
bool: True, если доступ разрешён; иначе False.
|
|
"""
|
|
if request.method != "DELETE":
|
|
return True
|
|
is_final = bool(getattr(obj, "is_final", False))
|
|
is_staff = bool(getattr(request.user, "is_staff", False))
|
|
return (not is_final) or is_staff
|
|
|
|
|
|
class DeptViewRequest(AdminOrDeptAllowed):
|
|
"""Чтение заявки: staff ИЛИ право отдела 'view_request'."""
|
|
|
|
required_code = "view_request"
|
|
|
|
|
|
class DeptCanProcess(AdminOrDeptAllowed):
|
|
"""Обработка заявок: staff ИЛИ право отдела 'can_process_requests'."""
|
|
|
|
required_code = "can_process_requests"
|
|
|
|
|
|
class DeptChangeRequest(AdminOrDeptAllowed):
|
|
"""Правка заявок: staff ИЛИ право отдела 'change_request'."""
|
|
|
|
required_code = "change_request"
|
|
|
|
|
|
class DeptComments(AdminOrDeptAllowed):
|
|
"""Комментарии: staff ИЛИ право отдела
|
|
- GET/HEAD: 'view_requestcomment'
|
|
- POST: 'add_requestcomment'
|
|
"""
|
|
|
|
def _required_code(self, view: Any, request: Request) -> Optional[str]:
|
|
# Метод-чувствительная логика: GET/HEAD → view, POST → add
|
|
if request.method in {"GET", "HEAD"}:
|
|
return "view_requestcomment"
|
|
if request.method == "POST":
|
|
return "add_requestcomment"
|
|
return None # другие методы не поддерживаем
|