admin-back: ручки для админки

This commit is contained in:
tsygankoviva 2026-05-25 17:38:07 +03:00
parent aca03d59b7
commit b7557f7b3d
15 changed files with 753 additions and 50 deletions

View File

@ -111,11 +111,11 @@ async def get_form_sheets(
result=SheetFormTypeResponse( result=SheetFormTypeResponse(
form_type=form.form_type.code, form_type=form.form_type.code,
sheets=form.form_type.sheet_list, sheets=form.form_type.sheet_list,
all_sheets=form.form_type.sheet_list_with_directions,
) )
) )
@router.get("/{form_id}/sheet/{sheet}") @router.get("/{form_id}/sheet/{sheet}")
async def get_sheet( async def get_sheet(
form_id: int, form_id: int,

120
api/src/api/v1/org_unit.py Normal file
View File

@ -0,0 +1,120 @@
from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.errors import AccessDeniedException
from src.api.v1.deps import get_current_active_user, require_admin
from src.db.models.app_user import AppUser
from src.db.session import get_db
from src.domain.schemas import BaseListResponse, BaseSingleResponse, OrgUnitBaseSchema, OrgUnitListSchema, OrgUnitSchema, OrgUnitUpdateSchema, ResponseBase
from src.services.org_unit_service import OrgUnitService
router = APIRouter(prefix="/org-unit", tags=["ssp"])
@router.get("/")
async def org_unit_list(
offset: int = 0,
limit: int = 100,
load_users_count: bool = False,
db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_admin),
) -> BaseListResponse[OrgUnitListSchema]:
org_service = OrgUnitService(db)
count, units = await org_service.get_list(
user=current_user,
with_count=True,
offset=offset,
limit=limit,
load_users=load_users_count,
)
return BaseListResponse(
result=[
OrgUnitListSchema.model_validate(unit) for unit in units
],
count=count,
)
@router.get("/{org_unit_id}")
async def org_unit_id(
org_unit_id: int,
load_users: bool = False,
db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_admin),
) -> BaseSingleResponse[OrgUnitSchema]:
org_service = OrgUnitService(db)
unit = await org_service.get(
user=current_user,
org_unit_id=org_unit_id,
load_users=load_users,
)
if not unit:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ССП не найден")
return BaseSingleResponse(
result = OrgUnitSchema.model_validate(unit)
)
@router.post("/")
async def create_ssp(
org_data: OrgUnitBaseSchema,
db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_admin),
) -> BaseSingleResponse[OrgUnitSchema]:
"""Создание ССП/РФ."""
org_unit_service = OrgUnitService(db)
org_unit = await org_unit_service.create(
title=org_data.title,
is_ssp=org_data.is_ssp,
user=current_user,
)
return BaseSingleResponse(
message="ССП/РФ создана",
result=OrgUnitSchema.model_validate(org_unit),
)
@router.patch("/{org_unit_id}")
async def update_ssp(
org_unit_id: int,
org_data: OrgUnitUpdateSchema,
db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_admin),
) -> BaseSingleResponse[OrgUnitSchema]:
"""Редактирование ССП/РФ."""
org_unit_service = OrgUnitService(db)
org_unit = await org_unit_service.update(
org_unit_id=org_unit_id,
user=current_user,
**org_data.model_dump(exclude_unset=True),
)
if not org_unit:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ССП не найден")
return BaseSingleResponse(
message="ССП/РФ обновлена",
result=OrgUnitSchema.model_validate(org_unit),
)
@router.delete("/{org_unit_id}")
async def delete_ssp(
org_unit_id: int,
db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_admin),
) -> ResponseBase:
"""Удаление ССП/РФ."""
org_unit_service = OrgUnitService(db)
result = await org_unit_service.logical_delete(
org_unit_id=org_unit_id,
user=current_user,
is_active=True,
)
if not result:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ССП не найден")
return ResponseBase(success=True, message="ССП/РФ удалена")

View File

@ -1,6 +1,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from src.api.v1 import auth, users, admin, audit, forms, form_phases, projects, export from src.api.v1 import auth, users, admin, audit, forms, form_phases, projects, export, org_unit
from src.api.v1 import websocket from src.api.v1 import websocket
@ -14,3 +15,4 @@ api_router.include_router(projects.router)
api_router.include_router(form_phases.router) api_router.include_router(form_phases.router)
api_router.include_router(export.router) api_router.include_router(export.router)
api_router.include_router(websocket.router) api_router.include_router(websocket.router)
api_router.include_router(org_unit.router)

View File

@ -8,7 +8,7 @@ from src.api.v1.deps import (
) )
from src.db.session import get_db from src.db.session import get_db
from src.db.models.app_user import AppUser from src.db.models.app_user import AppUser
from src.domain.schemas import User as UserSchema from src.domain.schemas import BaseListResponse, BaseSingleResponse, User as UserSchema, UserAdminListResponse
from src.domain.schemas import ( from src.domain.schemas import (
SSPIDList, SSPIDList,
ResponseBase, ResponseBase,
@ -29,16 +29,27 @@ async def get_current_user_info(current_user: AppUser = Depends(get_current_acti
return current_user return current_user
@router.get("/", response_model=UserListResponse) @router.get("/")
async def get_users( async def get_users(
skip: int = 0, skip: int = 0,
limit: int = 100, limit: int = 100,
load_orgs: bool = False,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_admin), current_user: AppUser = Depends(require_admin),
): ) -> BaseListResponse[UserAdminListResponse]:
user_service = UserService(db) user_service = UserService(db)
users = await user_service.get_all(current_user, skip=skip, limit=limit) count, users = await user_service.get_all(
return UserListResponse(success=True, message="Список пользователей", result=users) current_user,
skip=skip,
limit=limit,
load_orgs=load_orgs,
with_count=True
)
return BaseListResponse(
count=count,
result=[UserAdminListResponse.model_validate(user) for user in users]
)
@router.get("/dfip-many-ssp/", response_model=UserSSPLinkResponse) @router.get("/dfip-many-ssp/", response_model=UserSSPLinkResponse)
@ -88,20 +99,26 @@ async def unset_many_ssp(
) )
@router.get("/{user_id}", response_model=UserSchema) @router.get("/{user_id}")
async def get_user( async def get_user(
user_id: int, user_id: int,
load_orgs: bool = False,
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_admin), current_user: AppUser = Depends(require_admin),
): ) -> UserAdminListResponse:
user_service = UserService(db) user_service = UserService(db)
user = await user_service.get(user_id, current_user) user = await user_service.get(
user_id=user_id,
current_user=current_user,
load_orgs=load_orgs,
)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, status_code=status.HTTP_404_NOT_FOUND,
detail="Пользователь не найден", detail="Пользователь не найден",
) )
return user return UserAdminListResponse.model_validate(user)
@router.put("/", response_model=UserResponse) @router.put("/", response_model=UserResponse)

View File

@ -44,6 +44,32 @@ class FormType(Base):
FormTypeEnum.FORM_4: ["plan", "seq_dfip", "approved", "contract", "booking", "q1", "q2", "q3", "q4", "totals", "contract_summary", "allocation", "reserve", "collegial", "ckk"], FormTypeEnum.FORM_4: ["plan", "seq_dfip", "approved", "contract", "booking", "q1", "q2", "q3", "q4", "totals", "contract_summary", "allocation", "reserve", "collegial", "ckk"],
} }
_sheet_list_with_directions = {
FormTypeEnum.FORM_1: [
{
"sheet_name": sheet_name,
"direction": None,
} for sheet_name in ["OPER", "SMETA"]
] + [
{
"sheet_name": sheet_name,
"direction": direction
} for sheet_name in ["AHR", "CAP"] for direction in ["Support", "Development"]
]
}
@property
def sheet_list_with_directions(self) -> dict:
return self._sheet_list_with_directions.get(
self.code,
[
{
"sheet_name": sheet_name,
"direction": None,
} for sheet_name in self.sheet_list
]
)
@property @property
def sheet_list(self): def sheet_list(self):
return self.SHEETS_BY_FORM_TYPE.get(self.code, []) return self.SHEETS_BY_FORM_TYPE.get(self.code, [])

View File

@ -22,6 +22,11 @@ class OrgUnit(Base):
is_active: Mapped[bool] = mapped_column(Boolean, default=True) is_active: Mapped[bool] = mapped_column(Boolean, default=True)
is_ssp: Mapped[bool] = mapped_column(Boolean) is_ssp: Mapped[bool] = mapped_column(Boolean)
@property
def users_count(self) -> int | None:
return len(self.users) if self.users is not None else None
# budget_forms: Mapped[list["BudgetForm"]] = relationship( # type: ignore # budget_forms: Mapped[list["BudgetForm"]] = relationship( # type: ignore
# "BudgetForm", back_populates="org_unit" # "BudgetForm", back_populates="org_unit"
# ) # )
@ -33,3 +38,4 @@ class OrgUnit(Base):
# ) # )
# vsps: Mapped[list["Vsp"]] = relationship("Vsp", back_populates="org_unit") # type: ignore # vsps: Mapped[list["Vsp"]] = relationship("Vsp", back_populates="org_unit") # type: ignore
user_orgs: Mapped[list["UserOrg"]] = relationship("UserOrg", back_populates="org_unit") user_orgs: Mapped[list["UserOrg"]] = relationship("UserOrg", back_populates="org_unit")
users: Mapped[list["AppUser"]] = relationship("AppUser", secondary="v3.user_org", back_populates="org_units")

View File

@ -1,11 +1,12 @@
from datetime import datetime from datetime import datetime
import enum import enum
from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar
from sqlalchemy.exc import MissingGreenlet
FormPhaseRole = Literal["DFIP", "EXECUTOR_RF"] FormPhaseRole = Literal["DFIP", "EXECUTOR_RF"]
from pydantic import BaseModel, ConfigDict, EmailStr, Field from pydantic import BaseModel, ConfigDict, EmailStr, Field, field_validator, model_validator
T = TypeVar("T") T = TypeVar("T")
@ -37,6 +38,16 @@ PlacementTypeLiteral = Literal[
] ]
# def serialize_missing_greenlet(cls, obj):
# result = {}
# for attr in cls.model_fields:
# try:
# result[attr] = getattr(obj, attr)
# except MissingGreenlet:
# result[attr] = None
# return result
class ResponseBase(BaseModel): class ResponseBase(BaseModel):
success: Optional[bool] = Field(True) success: Optional[bool] = Field(True)
message: Optional[str] = None message: Optional[str] = None
@ -68,6 +79,9 @@ class UserBase(BaseModel):
) )
role_id: int = 3 role_id: int = 3
model_config = ConfigDict(from_attributes=True)
class UserCreate(UserBase): class UserCreate(UserBase):
password: str | None = Field(default=None, examples=["pass123"]) password: str | None = Field(default=None, examples=["pass123"])
@ -117,6 +131,34 @@ class User(UserInDB):
pass pass
class UserAdminListResponse(UserInDB):
org_units: Optional[list["OrgUnitSchema"]] = None
@model_validator(mode='before')
@classmethod
def safe_orm_handler(cls, data: Any) -> Any:
if isinstance(data, dict):
return data
result = {
"org_units": None,
"id": data.id,
"created_at": data.created_at,
"updated_at": data.updated_at,
"is_active": data.is_active,
"email": data.email,
"username": data.username,
"full_name": data.full_name,
"role_id": data.role_id,
}
try:
result["org_units"] = data.org_units
except MissingGreenlet:
pass
return result
class UserResponse(ResponseBase): class UserResponse(ResponseBase):
result: Optional[UserInDB] = None result: Optional[UserInDB] = None
@ -172,14 +214,70 @@ class FormTypeSchemaEnum(str, enum.Enum):
FORM_4 = "FORM_4" FORM_4 = "FORM_4"
class OrgUnitSchema(BaseModel): class OrgUnitBaseSchema(BaseModel):
id: int
title: str title: str
is_active: bool
is_ssp: bool is_ssp: bool
class Config: model_config = ConfigDict(from_attributes=True)
from_attributes = True
class OrgUnitUpdateSchema(OrgUnitBaseSchema):
title: Optional[str] = None
is_ssp: Optional[bool] = None
model_config = ConfigDict(from_attributes=True)
class OrgUnitResponseSchema(OrgUnitBaseSchema):
id: int
is_active: bool
users_count: int | None = None
model_config = ConfigDict(from_attributes=True)
@model_validator(mode='before')
@classmethod
def safe_orm_handler(cls, data: Any) -> Any:
if isinstance(data, dict):
return data
result = {
"users_count": None,
"id": data.id,
"title": data.title,
"is_ssp": data.is_ssp,
"is_ssp": data.is_ssp,
"is_active": data.is_active,
}
try:
result["users_count"] = data.users_count
except MissingGreenlet:
pass
return result
class OrgUnitListSchema(OrgUnitResponseSchema):
pass
class OrgUnitSchema(OrgUnitResponseSchema):
users: list[UserBase] | None = None
@model_validator(mode='before')
@classmethod
def safe_orm_handler(cls, data: Any) -> Any:
if isinstance(data, dict):
return data
result = super().safe_orm_handler(data)
result["users"] = None
try:
result["users"] = data.users
except MissingGreenlet:
pass
return result
class BudgetFormResponse(BaseModel): class BudgetFormResponse(BaseModel):
id: int id: int
@ -191,13 +289,18 @@ class BudgetFormResponse(BaseModel):
org_unit_id: int org_unit_id: int
org_unit: OrgUnitSchema | None = None org_unit: OrgUnitSchema | None = None
class Config: model_config = ConfigDict(from_attributes=True)
from_attributes = True
class FullSheetSchema(BaseModel):
sheet_name: str
direction: str | None = None
class SheetFormTypeResponse(BaseModel): class SheetFormTypeResponse(BaseModel):
form_type: FormTypeSchemaEnum form_type: FormTypeSchemaEnum
sheets: list[str] sheets: list[str]
all_sheets: list[FullSheetSchema] | None = None
class DirectionSchemaEnum(str, enum.Enum): class DirectionSchemaEnum(str, enum.Enum):

View File

@ -0,0 +1,112 @@
from sqlalchemy import func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from src.db.models.org_unit import OrgUnit
from sqlalchemy.orm import joinedload
class OrgUnitRepository:
def __init__(self, db: AsyncSession):
self.db = db
async def get_list(
self,
offset: int | None = None,
limit: int | None = None,
is_ssp: bool | None = None,
is_active: bool | None = None,
with_count: bool = False,
load_users: bool = False,
) -> list[OrgUnit] | tuple[int, list[OrgUnit]]:
if with_count:
query = select(func.count().over().label("total_count"), OrgUnit)
else:
query = select(OrgUnit)
where = []
if is_ssp is not None:
where.append(OrgUnit.is_ssp == is_ssp)
if is_active is not None:
where.append(OrgUnit.is_active == is_active)
query = query.where(*where)
if load_users:
query = query.options(
joinedload(OrgUnit.users)
)
query = query.order_by(OrgUnit.id)
if offset is not None:
query = query.offset(offset)
if limit is not None:
query = query.limit(limit)
if with_count:
result = (await self.db.execute(query)).unique().all()
if result:
return result[0][0], [res[1] for res in result]
else:
if query.whereclause is not None:
count_query = select(func.count(OrgUnit.id)).where(
query.whereclause
)
return (await self.db.execute(count_query)).scalar(), []
else:
return (await self.db.execute(query)).scalars().unique().all()
async def get(
self,
org_unit_id: int,
load_users: bool = False
) -> OrgUnit | None:
query = select(OrgUnit).where(OrgUnit.id == org_unit_id)
if load_users:
query = query.options(
joinedload(OrgUnit.users)
)
return (await self.db.execute(query)).scalars().first()
async def create(
self,
title: str,
is_ssp: bool,
) -> OrgUnit:
"""Создание SSP."""
ssp = OrgUnit(title=title, is_ssp=is_ssp)
self.db.add(ssp)
await self.db.flush()
# await self.db.refresh(ssp)
return ssp
async def update(
self, org_unit: OrgUnit, **kwargs,
) -> OrgUnit | None:
if not org_unit:
return None
for field, value in kwargs.items():
setattr(org_unit, field, value)
await self.db.flush()
# await self.db.refresh(ssp)
return org_unit
async def logical_delete(self, org_unit_id: int, is_active: bool | None = None) -> bool:
"""Логическое удаление SSP."""
query = update(OrgUnit).where(OrgUnit.id == org_unit_id)
if is_active is not None:
query = query.where(OrgUnit.is_active == is_active)
query = query.values(is_active=False)
res = await self.db.execute(query)
await self.db.flush()
return res.rowcount > 0

View File

@ -1,8 +1,8 @@
from typing import Optional from typing import Optional
from sqlalchemy import select, text, update from sqlalchemy import delete, func, select, text, update
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload from sqlalchemy.orm import joinedload, selectinload
from src.db.models.org_unit import OrgUnit from src.db.models.org_unit import OrgUnit
@ -65,15 +65,47 @@ class UserRepository:
) )
async def get_list(self, skip: int = 0, limit: int = 100) -> list[AppUser]: async def get_list(
query = ( self,
select(AppUser) skip: int = 0,
.where(AppUser.is_active.is_(True)) limit: int = 100,
.offset(skip) with_count: bool = False,
.limit(limit) load_orgs: bool = False,
.order_by(AppUser.id) ) -> list[AppUser] | tuple[int, list[AppUser]]:
)
return (await self.db.execute(query)).scalars().all() where = [AppUser.is_active.is_(True)]
if with_count:
query = select(func.count().over().label("total_count"), AppUser)
else:
query = select(AppUser)
query = query.where(*where)
if load_orgs:
query = query.options(
joinedload(AppUser.org_units)
)
query = query.order_by(AppUser.id)
if skip is not None:
query = query.offset(skip)
if limit is not None:
query = query.limit(limit)
if with_count:
result = (await self.db.execute(query)).unique().all()
if result:
return result[0][0], [res[1] for res in result]
else:
if query.whereclause is not None:
count_query = select(func.count(AppUser.id)).where(
query.whereclause
)
return (await self.db.execute(count_query)).scalar(), []
else:
return (await self.db.execute(query)).scalars().unique().all()
async def logical_delete(self, user_id: int) -> bool: async def logical_delete(self, user_id: int) -> bool:
query = update(AppUser).where(AppUser.id == user_id).values(is_active=False) query = update(AppUser).where(AppUser.id == user_id).values(is_active=False)

View File

@ -48,7 +48,7 @@ class BudgetFormService:
user: AppUser, user: AppUser,
load_form_type: bool = False, load_form_type: bool = False,
load_org: bool = False, load_org: bool = False,
) -> list[BudgetForm] | tuple[int, list[BudgetForm]]: ) -> BudgetForm | None:
if user.role_id == UserRoleEnum.ADMIN: if user.role_id == UserRoleEnum.ADMIN:
return await self.bf_repo.get( return await self.bf_repo.get(
budget_form_id=budget_form_id, budget_form_id=budget_form_id,

View File

@ -0,0 +1,102 @@
from sqlalchemy.ext.asyncio import AsyncSession
from src.core.errors import AccessDeniedException
from src.db.models.app_user import AppUser
from src.db.models.org_unit import OrgUnit
from src.db.models.role import UserRoleEnum
from src.repository.org_unit_repository import OrgUnitRepository
class OrgUnitService:
def __init__(self, db: AsyncSession):
self.db = db
self.org_repo = OrgUnitRepository(db)
async def get_list(
self,
user: AppUser,
offset: int | None = None,
limit: int | None = None,
is_ssp: bool | None = None,
is_active: bool | None = None,
with_count: bool = False,
load_users: bool = False,
) -> list[OrgUnit] | tuple[int, list[OrgUnit]]:
if user.role_id != UserRoleEnum.ADMIN:
return (0, []) if with_count else []
return await self.org_repo.get_list(
offset=offset,
limit=limit,
with_count=with_count,
is_ssp=is_ssp,
is_active=is_active,
load_users=load_users,
)
async def get(
self,
org_unit_id: int,
user: AppUser,
load_users: bool = False,
) -> OrgUnit | None:
if user.role_id != UserRoleEnum.ADMIN:
return None
return await self.org_repo.get(
org_unit_id=org_unit_id,
load_users=load_users,
)
async def create(
self,
title: str,
is_ssp: bool,
user: AppUser,
) -> OrgUnit:
"""Создание нового ССП/РФ."""
if not self._can_create_ssp(user):
raise AccessDeniedException()
return await self.org_repo.create(
title=title,
is_ssp=is_ssp,
)
async def update(
self, org_unit_id: int, user: AppUser, **kwargs
) -> OrgUnit | None:
"""Редактирование ССП/РФ"""
#ToDo: писать логи
if not self._can_edit_ssp(user):
raise AccessDeniedException()
org_unit = await self.org_repo.get(org_unit_id=org_unit_id)
if not org_unit:
return None
return await self.org_repo.update(
org_unit=org_unit, **kwargs,
)
async def logical_delete(
self,
org_unit_id: int,
user: AppUser,
is_active: bool | None = None,
) -> bool:
"""Логическое удаление ССП/РФ"""
if not self._can_delete_ssp(user):
raise AccessDeniedException()
return bool(await self.org_repo.logical_delete(org_unit_id=org_unit_id, is_active=is_active))
def _can_create_ssp(self, user: AppUser) -> bool:
return user.role_id == UserRoleEnum.ADMIN
def _can_edit_ssp(self, user: AppUser) -> bool:
return user.role_id == UserRoleEnum.ADMIN
def _can_delete_ssp(self, user: AppUser) -> bool:
return user.role_id == UserRoleEnum.ADMIN

View File

@ -17,15 +17,32 @@ class UserService:
self.db = db self.db = db
self.user_repo = UserRepository(db) self.user_repo = UserRepository(db)
async def get_all(self, current_user: AppUser, skip: int = 0, limit: int = 100): async def get_all(
self,
current_user: AppUser,
skip: int = 0,
limit: int = 100,
with_count: bool = False,
load_orgs: bool = False,
) -> list[AppUser] | tuple[int, list[AppUser]]:
if current_user.role_id != UserRoleEnum.ADMIN: if current_user.role_id != UserRoleEnum.ADMIN:
raise AccessDeniedException() raise AccessDeniedException()
return await self.user_repo.get_list(skip=skip, limit=limit) return await self.user_repo.get_list(
skip=skip,
limit=limit,
with_count=with_count,
load_orgs=load_orgs,
)
async def get(self, user_id: int, current_user: AppUser | None = None): async def get(
self,
user_id: int,
current_user: AppUser | None = None,
load_orgs: bool = False,
):
if current_user is not None and current_user.role_id != UserRoleEnum.ADMIN: if current_user is not None and current_user.role_id != UserRoleEnum.ADMIN:
raise AccessDeniedException() raise AccessDeniedException()
return await self.user_repo.get(user_id) return await self.user_repo.get(user_id=user_id, load_orgs=load_orgs)
async def create_user(self, user_data: UserCreate, creator: AppUser): async def create_user(self, user_data: UserCreate, creator: AppUser):
if creator.role_id != UserRoleEnum.ADMIN: if creator.role_id != UserRoleEnum.ADMIN:

View File

@ -14,23 +14,32 @@ from src.main import app
@pytest.fixture @pytest.fixture
def client(): def client():
loop = asyncio.get_event_loop() db_ref = SessionLocal()
loop_ref = None
db = SessionLocal() async def _get_db():
try: nonlocal loop_ref
async def _get_db(): loop_ref = asyncio.get_running_loop()
try: try:
yield db yield db_ref
await db.flush() await db_ref.flush()
finally: except:
await db.close() await db_ref.rollback()
app.dependency_overrides[get_db] = _get_db raise
with TestClient(app) as test_client:
app.dependency_overrides[get_db] = _get_db
with TestClient(app) as test_client:
try:
yield test_client yield test_client
app.dependency_overrides.clear() finally:
finally: if db_ref is not None and loop_ref:
loop.run_until_complete(db.rollback()) async def cleanup():
loop.run_until_complete(db.close()) await db_ref.rollback()
await db_ref.close()
future = asyncio.run_coroutine_threadsafe(cleanup(), loop_ref)
future.result()
app.dependency_overrides.clear()
@pytest.fixture @pytest.fixture

View File

@ -0,0 +1,111 @@
import pydantic_core
import pytest
def test_org_units_list_smoke(client, admin_tokens, auth_headers):
response = client.get("/api/v1/org-unit/", headers=auth_headers(admin_tokens))
assert response.status_code == 200
payload = response.json()
assert "result" in payload
assert "count" in payload
assert isinstance(payload["result"], list)
def test_org_units_list_with_users_count(client, admin_tokens, auth_headers):
response = client.get(
"/api/v1/org-unit/?load_users_count=true",
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert "result" in payload
assert isinstance(payload["result"], list)
if payload["result"]:
assert "users_count" in payload["result"][0]
def test_org_units_get(client, admin_tokens, auth_headers):
response = client.get("/api/v1/org-unit/1", headers=auth_headers(admin_tokens))
assert response.status_code == 200
payload = response.json()
assert "result" in payload
assert payload["result"]["id"] == 1
def test_org_units_get_not_found(client, admin_tokens, auth_headers):
response = client.get("/api/v1/org-unit/99999", headers=auth_headers(admin_tokens))
assert response.status_code == 404
# def test_org_units_create_smoke(client, admin_tokens, auth_headers):
# suffix = uuid.uuid4().hex[:8]
# title = f"Test Org {suffix}"
# response = client.post(
# "/api/v1/org-unit/",
# json={"title": title, "is_ssp": True},
# headers=auth_headers(admin_tokens),
# )
# assert response.status_code == 200
# payload = response.json()
# assert payload["result"]["title"] == title
# assert payload["result"]["is_ssp"] is True
# def test_org_units_create_non_ssp(client, admin_tokens, auth_headers):
# suffix = uuid.uuid4().hex[:8]
# title = f"Test RF {suffix}"
# response = client.post(
# "/api/v1/org-unit/",
# json={"title": title, "is_ssp": False},
# headers=auth_headers(admin_tokens),
# )
# assert response.status_code == 200
# payload = response.json()
# assert payload["result"]["title"] == title
# assert payload["result"]["is_ssp"] is False
# def test_org_units_update_smoke(client, admin_tokens, auth_headers):
# new_title = f"Updated_{uuid.uuid4().hex[:8]}"
# response = client.patch(
# "/api/v1/org-unit/1",
# json={"title": new_title},
# headers=auth_headers(admin_tokens),
# )
# assert response.status_code == 200
# payload = response.json()
# assert payload["result"]["title"] == new_title
# assert payload["result"]["id"] == 1
def test_org_units_update_not_found_error_shape(client, admin_tokens, auth_headers):
response = client.patch(
"/api/v1/org-unit/99999",
json={"title": "No Matter"},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 404
payload = response.json()
assert isinstance(payload, dict)
assert "code" in payload
assert "message" in payload
def test_org_units_delete_not_found_error_shape(client, admin_tokens, auth_headers):
response = client.delete(
"/api/v1/org-unit/99999", headers=auth_headers(admin_tokens)
)
assert response.status_code == 404
payload = response.json()
assert isinstance(payload, dict)
assert "code" in payload
assert "message" in payload
def test_org_units_access_denied_for_non_admin(client, isp_tokens, auth_headers):
response = client.get("/api/v1/org-unit/", headers=auth_headers(isp_tokens))
assert response.status_code == 403
payload = response.json()
assert isinstance(payload, dict)
assert "code" in payload
assert "message" in payload

View File

@ -6,6 +6,52 @@ def test_users_me(client, admin_tokens, auth_headers):
assert response.status_code == 200 assert response.status_code == 200
assert response.json()["username"] == "admin" assert response.json()["username"] == "admin"
def test_users_list(client, admin_tokens, auth_headers):
response = client.get("/api/v1/users/", headers=auth_headers(admin_tokens))
assert response.status_code == 200
result = response.json()
assert "result" in result
assert isinstance(result["result"], list)
def test_users_org_list(client, admin_tokens, auth_headers):
response = client.get("/api/v1/users/?load_orgs=true", headers=auth_headers(admin_tokens))
assert response.status_code == 200
result = response.json()
assert "result" in result
assert isinstance(result["result"], list)
assert len(result["result"]) > 0
assert "org_units" in result["result"][0]
assert len(result["result"][0]["org_units"]) > 0
def test_users_get(client, admin_tokens, auth_headers):
response = client.get("/api/v1/users/me", headers=auth_headers(admin_tokens))
assert response.status_code == 200
user = response.json()
assert "id" in user
user_id = user["id"]
response = client.get(f"/api/v1/users/{user_id}", headers=auth_headers(admin_tokens))
assert response.status_code == 200
def test_user_get_org_list(client, admin_tokens, auth_headers):
response = client.get("/api/v1/users/me", headers=auth_headers(admin_tokens))
assert response.status_code == 200
user = response.json()
assert "id" in user
user_id = user["id"]
response = client.get(f"/api/v1/users/{user_id}?load_orgs=true", headers=auth_headers(admin_tokens))
assert response.status_code == 200
result = response.json()
assert "org_units" in result
assert len(result["org_units"]) > 0
# Задокуменировано что бы не было переполнения бд, так как юзер удаляется только логически # Задокуменировано что бы не было переполнения бд, так как юзер удаляется только логически
# def test_users_create_smoke(client, admin_tokens, auth_headers): # def test_users_create_smoke(client, admin_tokens, auth_headers):
# suffix = uuid.uuid4().hex[:8] # suffix = uuid.uuid4().hex[:8]