"""Схемы API администрирования `/admin/*` (spec users-admin, design D-8): пользователи, роли, матрица прав. Поля — как в DBML v5.15 (`app_user`, `role`, `permission`); `external_id` через API не задаётся (появится при первом входе через РАИСА), поэтому тела запросов запрещают лишние поля. """ import datetime import uuid from typing import Self from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator from src.models.enums import BusinessSegment, PermissionAction, PermissionObject from src.schemas.directories import NAME_MAX_LENGTH, RegionalOfficeOut # Поля `app_user`, которые не могут быть NULL: явный `null` в PATCH — ошибка валидации, а не «очистить» _NOT_NULL_UPDATE_FIELDS = ("full_name", "regional_office_id", "is_active") def _strip_full_name(value: str) -> str: stripped = value.strip() if not stripped: raise ValueError("ФИО не может быть пустым") return stripped def _blank_to_none(value: str | None) -> str | None: if value is None: return None return value.strip() or None class UserCreate(BaseModel): """Создание пользователя: email уникален без учёта регистра (проверяет сервис), роли — коды активных ролей.""" model_config = ConfigDict(extra="forbid") email: EmailStr full_name: str = Field(min_length=1, max_length=NAME_MAX_LENGTH) regional_office_id: uuid.UUID employee_no: str | None = None business_segment: BusinessSegment | None = None roles: list[str] = Field(default_factory=list) @field_validator("full_name") @classmethod def _full_name(cls, value: str) -> str: return _strip_full_name(value) @field_validator("employee_no") @classmethod def _employee_no(cls, value: str | None) -> str | None: return _blank_to_none(value) class UserUpdate(BaseModel): """Изменение пользователя; «поле передано» — по `model_fields_set`. `employee_no` и `business_segment` можно очистить (`null`); `full_name`, `regional_office_id`, `is_active` — нет. """ model_config = ConfigDict(extra="forbid") full_name: str | None = Field(default=None, min_length=1, max_length=NAME_MAX_LENGTH) employee_no: str | None = None regional_office_id: uuid.UUID | None = None business_segment: BusinessSegment | None = None is_active: bool | None = None @field_validator("full_name") @classmethod def _full_name(cls, value: str | None) -> str | None: return None if value is None else _strip_full_name(value) @field_validator("employee_no") @classmethod def _employee_no(cls, value: str | None) -> str | None: return _blank_to_none(value) @model_validator(mode="after") def _no_null_for_required(self) -> Self: nulls = [ name for name in _NOT_NULL_UPDATE_FIELDS if name in self.model_fields_set and getattr(self, name) is None ] if nulls: raise ValueError(f"поля не могут быть null: {', '.join(nulls)}") return self class UserOut(BaseModel): """Пользователь для администратора: филиал целиком и коды назначенных ролей (по алфавиту).""" model_config = ConfigDict(use_enum_values=True) id: uuid.UUID email: str | None full_name: str employee_no: str | None regional_office: RegionalOfficeOut business_segment: BusinessSegment | None is_active: bool roles: list[str] created_at: datetime.datetime | None updated_at: datetime.datetime | None class UserListQuery(BaseModel): """Фильтры списка пользователей: `q` — подстрока email или ФИО без учёта регистра, `is_active`.""" q: str | None = None is_active: bool | None = None class RolesSet(BaseModel): """Новый набор ролей пользователя (коды); заменяет прежний целиком.""" model_config = ConfigDict(extra="forbid") roles: list[str] class RoleOut(BaseModel): """Роль с кодами её прав (по алфавиту).""" code: str name: str description: str | None is_system: bool is_active: bool permissions: list[str] class PermissionOut(BaseModel): """Право из справочника (только чтение: новые коды не создаются).""" model_config = ConfigDict(from_attributes=True, use_enum_values=True) code: str object_type: PermissionObject action: PermissionAction name: str description: str | None class PermissionsSet(BaseModel): """Новый набор прав роли (коды); заменяет прежний целиком.""" model_config = ConfigDict(extra="forbid") permissions: list[str]