Merge pull request 'F3 Read/Write endpoints' (#2) from form_3 into form_124
Reviewed-on: #2 Reviewed-by: tsygankoviva <tsygankov.itis@gmail.com>
This commit is contained in:
commit
79ad4a444c
@ -1,20 +1,42 @@
|
|||||||
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException, Response
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.api.v1.deps import get_current_active_user_with_set_db
|
from src.api.v1.deps import get_current_active_user_with_set_db
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.session import get_db
|
from src.db.session import get_db
|
||||||
from src.domain.schemas import BaseListResponse
|
from src.domain.schemas import (
|
||||||
|
AddForm3LineBody,
|
||||||
|
AddProjectBody,
|
||||||
|
BaseListResponse,
|
||||||
|
BaseSingleResponse,
|
||||||
|
CellPatch,
|
||||||
|
CellsPatch,
|
||||||
|
SheetResponse,
|
||||||
|
UpdProjectBody,
|
||||||
|
)
|
||||||
from src.services.project_service import ProjectService
|
from src.services.project_service import ProjectService
|
||||||
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/projects", tags=["forms"])
|
router = APIRouter(tags=["forms"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
def _parse_sections(sections: Optional[str]) -> Optional[list[str]]:
|
||||||
|
if not sections:
|
||||||
|
return None
|
||||||
|
return [s.strip() for s in sections.split(",") if s.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def _rows_to_payload(rows: list[tuple]) -> list[dict]:
|
||||||
|
return [
|
||||||
|
{"row_type": row[0], "depth": row[1], "sort_order": row[2], "data": row[3]}
|
||||||
|
for row in rows
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/projects")
|
||||||
async def get_projects(
|
async def get_projects(
|
||||||
offset: int = 0,
|
offset: int = 0,
|
||||||
limit: int = 100,
|
limit: int = 100,
|
||||||
@ -22,7 +44,7 @@ async def get_projects(
|
|||||||
branch_id: Optional[int] = None,
|
branch_id: Optional[int] = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
) -> BaseListResponse[str]:
|
) -> BaseListResponse[dict]:
|
||||||
project_service = ProjectService(db)
|
project_service = ProjectService(db)
|
||||||
count, projects = await project_service.get_list(
|
count, projects = await project_service.get_list(
|
||||||
user=current_user,
|
user=current_user,
|
||||||
@ -32,3 +54,189 @@ async def get_projects(
|
|||||||
offset=offset,
|
offset=offset,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
|
return BaseListResponse(result=projects, count=count)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/projects/{project_id}")
|
||||||
|
async def get_project(
|
||||||
|
project_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> BaseSingleResponse[dict]:
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
project = await project_service.get(project_id=project_id, user=current_user)
|
||||||
|
if not project:
|
||||||
|
raise HTTPException(404, "Project not found")
|
||||||
|
return BaseSingleResponse(result=project)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/projects/{project_id}/report/{year}/{report_type}")
|
||||||
|
async def get_project_report(
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
response: Response,
|
||||||
|
sections: Optional[str] = None,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> BaseListResponse[SheetResponse]:
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
rows = await project_service.get_report_rows(
|
||||||
|
project_id=project_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
sections=_parse_sections(sections),
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
|
return BaseListResponse(count=len(rows), result=_rows_to_payload(rows))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/rf-rollup/{branch_id}/{year}")
|
||||||
|
async def get_rf_rollup(
|
||||||
|
branch_id: int,
|
||||||
|
year: int,
|
||||||
|
response: Response,
|
||||||
|
sections: Optional[str] = None,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> BaseListResponse[SheetResponse]:
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
t0 = time.perf_counter()
|
||||||
|
rows = await project_service.get_rf_rollup_rows(
|
||||||
|
branch_id=branch_id,
|
||||||
|
year=year,
|
||||||
|
sections=_parse_sections(sections),
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
db_ms = (time.perf_counter() - t0) * 1000
|
||||||
|
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
|
||||||
|
return BaseListResponse(count=len(rows), result=_rows_to_payload(rows))
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/projects/{project_id}/report/{year}/{report_type}/cell")
|
||||||
|
async def upd_form3_cell(
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
body: CellPatch,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> list[SheetResponse]:
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
rows = await project_service.upd_form3_cell(
|
||||||
|
project_id=project_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
line_id=body.line_id,
|
||||||
|
column=body.column,
|
||||||
|
value=body.value,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
return _rows_to_payload(rows)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/projects/{project_id}/report/{year}/{report_type}/cells")
|
||||||
|
async def upd_form3_cells(
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
body: CellsPatch,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> list[SheetResponse]:
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
rows = await project_service.upd_form3_cells(
|
||||||
|
project_id=project_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
changes=[change.model_dump() for change in body.changes],
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
return _rows_to_payload(rows)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/projects/{project_id}/report/{year}/{report_type}/line")
|
||||||
|
async def add_form3_line(
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
body: AddForm3LineBody,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> list[SheetResponse]:
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
rows = await project_service.add_form3_line(
|
||||||
|
project_id=project_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
expense_item_id=body.expense_item_id,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
return _rows_to_payload(rows)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/projects/{project_id}/report/{year}/{report_type}/line/{line_id}")
|
||||||
|
async def del_form3_line(
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
line_id: int,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> list[SheetResponse]:
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
rows = await project_service.del_form3_line(
|
||||||
|
project_id=project_id,
|
||||||
|
year=year,
|
||||||
|
report_type=report_type,
|
||||||
|
line_id=line_id,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
return _rows_to_payload(rows)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/project/{project_id}")
|
||||||
|
async def upd_project(
|
||||||
|
project_id: int,
|
||||||
|
body: UpdProjectBody,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> dict:
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
result = await project_service.upd_project(
|
||||||
|
project_id=project_id,
|
||||||
|
column=body.column,
|
||||||
|
value=body.value,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/projects")
|
||||||
|
async def add_project(
|
||||||
|
body: AddProjectBody,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> dict:
|
||||||
|
project_service = ProjectService(db)
|
||||||
|
project_id, limit_report_id, current_expenses_report_id = await project_service.add_project(
|
||||||
|
name=body.name,
|
||||||
|
year=body.year,
|
||||||
|
org_unit_id=body.branch_id,
|
||||||
|
level=body.level,
|
||||||
|
parent_id=body.parent_id,
|
||||||
|
project_type=body.project_type,
|
||||||
|
vsp_format=body.vsp_format,
|
||||||
|
placement_type=body.placement_type,
|
||||||
|
object_address=body.object_address,
|
||||||
|
staff_count=body.staff_count,
|
||||||
|
total_area=body.total_area,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"project_id": project_id,
|
||||||
|
"limit_report_id": limit_report_id,
|
||||||
|
"current_expenses_report_id": current_expenses_report_id,
|
||||||
|
}
|
||||||
@ -1,11 +1,38 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import enum
|
import enum
|
||||||
from typing import Generic, Optional, TypeVar
|
from typing import Any, Generic, Literal, Optional, TypeVar
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||||
|
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
ProjectTypeLiteral = Literal[
|
||||||
|
"Открытие ВСП",
|
||||||
|
"Закрытие ВСП",
|
||||||
|
"Переезд ВСП",
|
||||||
|
"Реновация РФ",
|
||||||
|
"Открытие УРМ",
|
||||||
|
]
|
||||||
|
|
||||||
|
VspFormatLiteral = Literal[
|
||||||
|
"Флагманский",
|
||||||
|
"Типовой",
|
||||||
|
"Розничный",
|
||||||
|
"МСБ",
|
||||||
|
"Лёгкий",
|
||||||
|
"Мини",
|
||||||
|
"Розничный-киоск",
|
||||||
|
"МБО",
|
||||||
|
"Офис самообслуживания",
|
||||||
|
"Другое",
|
||||||
|
]
|
||||||
|
|
||||||
|
PlacementTypeLiteral = Literal[
|
||||||
|
"Собственность",
|
||||||
|
"Аренда",
|
||||||
|
"Субаренда",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class ResponseBase(BaseModel):
|
class ResponseBase(BaseModel):
|
||||||
success: Optional[bool] = Field(True)
|
success: Optional[bool] = Field(True)
|
||||||
@ -146,3 +173,41 @@ class SheetResponse(BaseModel):
|
|||||||
depth: int
|
depth: int
|
||||||
data: dict
|
data: dict
|
||||||
sort_order: int | None = None
|
sort_order: int | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class CellPatch(BaseModel):
|
||||||
|
line_id: int
|
||||||
|
column: str
|
||||||
|
value: Any = None
|
||||||
|
|
||||||
|
|
||||||
|
class CellsPatch(BaseModel):
|
||||||
|
changes: list[CellPatch]
|
||||||
|
|
||||||
|
|
||||||
|
class AddForm3LineBody(BaseModel):
|
||||||
|
expense_item_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class UpdProjectBody(BaseModel):
|
||||||
|
column: str
|
||||||
|
value: Any = None
|
||||||
|
|
||||||
|
|
||||||
|
class AddProjectBody(BaseModel):
|
||||||
|
name: str = Field(
|
||||||
|
...,
|
||||||
|
max_length=30,
|
||||||
|
pattern=r"^[^+\-\/\\=&*\s]{1,30}$",
|
||||||
|
)
|
||||||
|
year: int
|
||||||
|
branch_id: int
|
||||||
|
level: Literal["project", "program"] = "project"
|
||||||
|
parent_id: Optional[int] = None
|
||||||
|
ssp_id: Optional[int] = None
|
||||||
|
project_type: Optional[ProjectTypeLiteral] = None
|
||||||
|
vsp_format: Optional[VspFormatLiteral] = None
|
||||||
|
placement_type: Optional[PlacementTypeLiteral] = None
|
||||||
|
object_address: Optional[str] = None
|
||||||
|
staff_count: Optional[int] = None
|
||||||
|
total_area: Optional[float] = None
|
||||||
@ -1,8 +1,360 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
from sqlalchemy import exists, false, func, select, text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.org_unit import OrgUnit
|
||||||
|
from src.db.models.project import Project
|
||||||
|
from src.db.models.rf_project_report import RfProjectReport
|
||||||
|
from src.db.models.rf_project_report_line import RfProjectReportLine
|
||||||
|
|
||||||
|
|
||||||
class ProjectRepository:
|
class ProjectRepository:
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.db = db
|
self.db = db
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _serialize_project(project: Project, org_unit_name: str | None, report_count: int) -> dict:
|
||||||
|
return {
|
||||||
|
"id": project.id,
|
||||||
|
"name": project.name,
|
||||||
|
"level": project.level,
|
||||||
|
"parent_id": project.parent_id,
|
||||||
|
"project_type": project.project_type,
|
||||||
|
"vsp_format": project.vsp_format,
|
||||||
|
"placement_type": project.placement_type,
|
||||||
|
"object_address": project.object_address,
|
||||||
|
"staff_count": project.staff_count,
|
||||||
|
"total_area": float(project.total_area) if project.total_area is not None else None,
|
||||||
|
"org_unit_id": project.org_unit_id,
|
||||||
|
"org_unit_name": org_unit_name,
|
||||||
|
"report_count": int(report_count),
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _apply_project_filters(query, year: int | None, branch_id: int | None, org_unit_ids: list[int] | None):
|
||||||
|
if year is not None:
|
||||||
|
query = query.where(
|
||||||
|
exists(
|
||||||
|
select(1).where(
|
||||||
|
RfProjectReport.project_id == Project.id,
|
||||||
|
RfProjectReport.year == year,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if branch_id is not None:
|
||||||
|
query = query.where(Project.org_unit_id == branch_id)
|
||||||
|
if org_unit_ids is not None:
|
||||||
|
if len(org_unit_ids) == 0:
|
||||||
|
query = query.where(false())
|
||||||
|
else:
|
||||||
|
query = query.where(Project.org_unit_id.in_(org_unit_ids))
|
||||||
|
return query
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
year: int | None = None,
|
||||||
|
branch_id: int | None = None,
|
||||||
|
offset: int | None = None,
|
||||||
|
limit: int | None = None,
|
||||||
|
with_count: bool = False,
|
||||||
|
org_unit_ids: list[int] | None = None,
|
||||||
|
) -> list[dict] | tuple[int, list[dict]]:
|
||||||
|
report_count_subq = (
|
||||||
|
select(func.count(RfProjectReport.id))
|
||||||
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
Project,
|
||||||
|
OrgUnit.title.label("org_unit_name"),
|
||||||
|
report_count_subq.label("report_count"),
|
||||||
|
)
|
||||||
|
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
||||||
|
.order_by(Project.id)
|
||||||
|
)
|
||||||
|
query = self._apply_project_filters(query, year, branch_id, org_unit_ids)
|
||||||
|
if offset is not None:
|
||||||
|
query = query.offset(offset)
|
||||||
|
if limit is not None:
|
||||||
|
query = query.limit(limit)
|
||||||
|
|
||||||
|
rows = (await self.db.execute(query)).all()
|
||||||
|
payload = [self._serialize_project(row[0], row[1], row[2]) for row in rows]
|
||||||
|
if not with_count:
|
||||||
|
return payload
|
||||||
|
|
||||||
|
count_query = select(func.count(Project.id))
|
||||||
|
count_query = self._apply_project_filters(count_query, year, branch_id, org_unit_ids)
|
||||||
|
total = int((await self.db.execute(count_query)).scalar() or 0)
|
||||||
|
return total, payload
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
org_unit_ids: list[int] | None = None,
|
||||||
|
) -> dict | None:
|
||||||
|
report_count_subq = (
|
||||||
|
select(func.count(RfProjectReport.id))
|
||||||
|
.where(RfProjectReport.project_id == Project.id)
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
Project,
|
||||||
|
OrgUnit.title.label("org_unit_name"),
|
||||||
|
report_count_subq.label("report_count"),
|
||||||
|
)
|
||||||
|
.outerjoin(OrgUnit, OrgUnit.id == Project.org_unit_id)
|
||||||
|
.where(Project.id == project_id)
|
||||||
|
)
|
||||||
|
if org_unit_ids is not None:
|
||||||
|
if len(org_unit_ids) == 0:
|
||||||
|
return None
|
||||||
|
query = query.where(Project.org_unit_id.in_(org_unit_ids))
|
||||||
|
row = (await self.db.execute(query)).first()
|
||||||
|
if not row:
|
||||||
|
return None
|
||||||
|
return self._serialize_project(row[0], row[1], row[2])
|
||||||
|
|
||||||
|
async def get_reports(self, project_id: int) -> list[dict]:
|
||||||
|
line_count_subq = (
|
||||||
|
select(func.count(RfProjectReportLine.id))
|
||||||
|
.where(RfProjectReportLine.rf_project_report_id == RfProjectReport.id)
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
query = (
|
||||||
|
select(
|
||||||
|
RfProjectReport.id,
|
||||||
|
RfProjectReport.year,
|
||||||
|
RfProjectReport.report_type,
|
||||||
|
RfProjectReport.created_by,
|
||||||
|
RfProjectReport.created_at,
|
||||||
|
RfProjectReport.updated_by,
|
||||||
|
RfProjectReport.updated_at,
|
||||||
|
line_count_subq.label("line_count"),
|
||||||
|
)
|
||||||
|
.where(RfProjectReport.project_id == project_id)
|
||||||
|
.order_by(RfProjectReport.year, RfProjectReport.report_type)
|
||||||
|
)
|
||||||
|
rows = (await self.db.execute(query)).all()
|
||||||
|
reports: list[dict] = []
|
||||||
|
for row in rows:
|
||||||
|
reports.append(
|
||||||
|
{
|
||||||
|
"id": row[0],
|
||||||
|
"year": row[1],
|
||||||
|
"report_type": row[2],
|
||||||
|
"created_by": row[3],
|
||||||
|
"created_at": row[4].isoformat() if row[4] is not None else None,
|
||||||
|
"updated_by": row[5],
|
||||||
|
"updated_at": row[6].isoformat() if row[6] is not None else None,
|
||||||
|
"line_count": int(row[7]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return reports
|
||||||
|
|
||||||
|
async def resolve_report_id(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
org_unit_ids: list[int] | None = None,
|
||||||
|
) -> int | None:
|
||||||
|
query = (
|
||||||
|
select(RfProjectReport.id)
|
||||||
|
.join(Project, Project.id == RfProjectReport.project_id)
|
||||||
|
.where(
|
||||||
|
Project.id == project_id,
|
||||||
|
RfProjectReport.year == year,
|
||||||
|
RfProjectReport.report_type == report_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if org_unit_ids is not None:
|
||||||
|
if len(org_unit_ids) == 0:
|
||||||
|
return None
|
||||||
|
query = query.where(Project.org_unit_id.in_(org_unit_ids))
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
async def get_report_rows(self, report_id: int, sections: list[str] | None = None) -> list[tuple]:
|
||||||
|
query = text(
|
||||||
|
"""
|
||||||
|
SELECT row_type, depth, sort_order, data
|
||||||
|
FROM v3.v_form3_report_jsonb(CAST(:report_id AS INT), CAST(:sections AS TEXT[]))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query, {"report_id": report_id, "sections": sections})).all()
|
||||||
|
|
||||||
|
async def get_rf_rollup_rows(
|
||||||
|
self,
|
||||||
|
branch_id: int,
|
||||||
|
year: int,
|
||||||
|
sections: list[str] | None = None,
|
||||||
|
) -> list[tuple]:
|
||||||
|
query = text(
|
||||||
|
"""
|
||||||
|
SELECT row_type, depth, sort_order, data
|
||||||
|
FROM v3.v_form3_rf_rollup_jsonb(CAST(:branch_id AS INT), CAST(:year AS INT), CAST(:sections AS TEXT[]))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
await self.db.execute(
|
||||||
|
query,
|
||||||
|
{"branch_id": branch_id, "year": year, "sections": sections},
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
async def upd_form3_cell(
|
||||||
|
self,
|
||||||
|
report_id: int,
|
||||||
|
line_id: int,
|
||||||
|
column: str,
|
||||||
|
value,
|
||||||
|
) -> list[tuple]:
|
||||||
|
query = text(
|
||||||
|
"""
|
||||||
|
SELECT row_type, depth, sort_order, data
|
||||||
|
FROM v3.upd_form3_cell(
|
||||||
|
CAST(:report_id AS INT),
|
||||||
|
CAST(:line_id AS INT),
|
||||||
|
CAST(:column AS TEXT),
|
||||||
|
CAST(:value AS JSONB)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = (
|
||||||
|
await self.db.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"report_id": report_id,
|
||||||
|
"line_id": line_id,
|
||||||
|
"column": column,
|
||||||
|
"value": json.dumps(value),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
await self.db.commit()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
async def upd_form3_cells(self, report_id: int, changes: list[dict]) -> list[tuple]:
|
||||||
|
query = text(
|
||||||
|
"""
|
||||||
|
SELECT row_type, depth, sort_order, data
|
||||||
|
FROM v3.upd_form3_cells(CAST(:report_id AS INT), CAST(:changes AS JSONB))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = (
|
||||||
|
await self.db.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"report_id": report_id,
|
||||||
|
"changes": json.dumps(changes),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
await self.db.commit()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
async def add_form3_line(self, report_id: int, expense_item_id: int) -> list[tuple]:
|
||||||
|
query = text(
|
||||||
|
"""
|
||||||
|
SELECT row_type, depth, sort_order, data
|
||||||
|
FROM v3.add_form3_line(CAST(:report_id AS INT), CAST(:expense_item_id AS INT))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = (
|
||||||
|
await self.db.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"report_id": report_id,
|
||||||
|
"expense_item_id": expense_item_id,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
await self.db.commit()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
async def del_form3_line(self, line_id: int) -> list[tuple]:
|
||||||
|
query = text(
|
||||||
|
"""
|
||||||
|
SELECT row_type, depth, sort_order, data
|
||||||
|
FROM v3.del_form3_line(CAST(:line_id AS INT))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = (await self.db.execute(query, {"line_id": line_id})).all()
|
||||||
|
await self.db.commit()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
async def upd_project(self, project_id: int, column: str, value):
|
||||||
|
query = text(
|
||||||
|
"""
|
||||||
|
SELECT v3.upd_project(CAST(:project_id AS INT), CAST(:column AS TEXT), CAST(:value AS JSONB))
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
result = (
|
||||||
|
await self.db.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"project_id": project_id,
|
||||||
|
"column": column,
|
||||||
|
"value": json.dumps(value),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).scalar_one()
|
||||||
|
await self.db.commit()
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def add_project(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
year: int,
|
||||||
|
org_unit_id: int,
|
||||||
|
level: str = "project",
|
||||||
|
parent_id: int | None = None,
|
||||||
|
project_type: str | None = None,
|
||||||
|
vsp_format: str | None = None,
|
||||||
|
placement_type: str | None = None,
|
||||||
|
object_address: str | None = None,
|
||||||
|
staff_count: int | None = None,
|
||||||
|
total_area: float | None = None,
|
||||||
|
) -> tuple[int, int, int]:
|
||||||
|
query = text(
|
||||||
|
"""
|
||||||
|
SELECT project_id, limit_report_id, current_expenses_report_id
|
||||||
|
FROM v3.add_project(
|
||||||
|
CAST(:name AS VARCHAR),
|
||||||
|
CAST(:year AS INT),
|
||||||
|
CAST(:org_unit_id AS INT),
|
||||||
|
CAST(:level AS VARCHAR),
|
||||||
|
CAST(:parent_id AS INT),
|
||||||
|
CAST(:project_type AS VARCHAR),
|
||||||
|
CAST(:vsp_format AS VARCHAR),
|
||||||
|
CAST(:placement_type AS VARCHAR),
|
||||||
|
CAST(:object_address AS VARCHAR),
|
||||||
|
CAST(:staff_count AS INT),
|
||||||
|
CAST(:total_area AS NUMERIC)
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
row = (
|
||||||
|
await self.db.execute(
|
||||||
|
query,
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"year": year,
|
||||||
|
"org_unit_id": org_unit_id,
|
||||||
|
"level": level,
|
||||||
|
"parent_id": parent_id,
|
||||||
|
"project_type": project_type,
|
||||||
|
"vsp_format": vsp_format,
|
||||||
|
"placement_type": placement_type,
|
||||||
|
"object_address": object_address,
|
||||||
|
"staff_count": staff_count,
|
||||||
|
"total_area": total_area,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
await self.db.commit()
|
||||||
|
return int(row[0]), int(row[1]), int(row[2])
|
||||||
|
|||||||
@ -1,15 +1,12 @@
|
|||||||
|
|
||||||
from sqlalchemy import func, select
|
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from src.db.models.app_user import AppUser
|
from src.db.models.app_user import AppUser
|
||||||
from src.db.models.project import Project
|
|
||||||
from src.db.models.role import UserRoleEnum
|
from src.db.models.role import UserRoleEnum
|
||||||
|
from src.core.errors import AccessDeniedException, ValidationException
|
||||||
from src.repository.project_repository import ProjectRepository
|
from src.repository.project_repository import ProjectRepository
|
||||||
from src.repository.user_repository import UserRepository
|
from src.repository.user_repository import UserRepository
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ProjectService:
|
class ProjectService:
|
||||||
def __init__(self, db: AsyncSession):
|
def __init__(self, db: AsyncSession):
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -17,13 +14,178 @@ class ProjectService:
|
|||||||
self.user_repo = UserRepository(db)
|
self.user_repo = UserRepository(db)
|
||||||
|
|
||||||
async def get_list(
|
async def get_list(
|
||||||
self,
|
self,
|
||||||
user: AppUser,
|
user: AppUser,
|
||||||
year: int | None = None,
|
year: int | None = None,
|
||||||
branch_id: int | None = None,
|
branch_id: int | None = None,
|
||||||
offset: int | None = None,
|
offset: int | None = None,
|
||||||
limit: int | None = None,
|
limit: int | None = None,
|
||||||
with_count: bool = False,
|
with_count: bool = False,
|
||||||
with_org_unit: bool = False,
|
with_org_unit: bool = False,
|
||||||
) -> list[Project] | tuple[int, list[Project]]:
|
) -> list[dict] | tuple[int, list[dict]]:
|
||||||
pass
|
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||||
|
return await self.project_repo.get_list(
|
||||||
|
year=year,
|
||||||
|
branch_id=branch_id,
|
||||||
|
offset=offset,
|
||||||
|
limit=limit,
|
||||||
|
with_count=with_count,
|
||||||
|
org_unit_ids=org_unit_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get(self, project_id: int, user: AppUser) -> dict | None:
|
||||||
|
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||||
|
project = await self.project_repo.get(project_id=project_id, org_unit_ids=org_unit_ids)
|
||||||
|
if not project:
|
||||||
|
return None
|
||||||
|
project["reports"] = await self.project_repo.get_reports(project_id=project_id)
|
||||||
|
return project
|
||||||
|
|
||||||
|
async def resolve_report_id(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
user: AppUser,
|
||||||
|
) -> int | None:
|
||||||
|
normalized_type = report_type.upper()
|
||||||
|
if normalized_type not in ("LIMIT", "CURRENT_EXPENSES"):
|
||||||
|
raise ValidationException("report_type должен быть LIMIT или CURRENT_EXPENSES")
|
||||||
|
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||||
|
return await self.project_repo.resolve_report_id(
|
||||||
|
project_id=project_id,
|
||||||
|
year=year,
|
||||||
|
report_type=normalized_type,
|
||||||
|
org_unit_ids=org_unit_ids,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get_report_rows(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
sections: list[str] | None,
|
||||||
|
user: AppUser,
|
||||||
|
) -> list[tuple]:
|
||||||
|
report_id = await self.resolve_report_id(project_id, year, report_type, user)
|
||||||
|
if not report_id:
|
||||||
|
raise ValidationException("Отчёт не найден")
|
||||||
|
return await self.project_repo.get_report_rows(report_id=report_id, sections=sections)
|
||||||
|
|
||||||
|
async def get_rf_rollup_rows(
|
||||||
|
self,
|
||||||
|
branch_id: int,
|
||||||
|
year: int,
|
||||||
|
sections: list[str] | None,
|
||||||
|
user: AppUser,
|
||||||
|
) -> list[tuple]:
|
||||||
|
org_unit_ids = await self._allowed_org_unit_ids(user)
|
||||||
|
if org_unit_ids is not None and branch_id not in org_unit_ids:
|
||||||
|
raise AccessDeniedException()
|
||||||
|
return await self.project_repo.get_rf_rollup_rows(
|
||||||
|
branch_id=branch_id,
|
||||||
|
year=year,
|
||||||
|
sections=sections,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def upd_form3_cell(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
line_id: int,
|
||||||
|
column: str,
|
||||||
|
value,
|
||||||
|
user: AppUser,
|
||||||
|
) -> list[tuple]:
|
||||||
|
report_id = await self.resolve_report_id(project_id, year, report_type, user)
|
||||||
|
if not report_id:
|
||||||
|
raise ValidationException("Отчёт не найден")
|
||||||
|
return await self.project_repo.upd_form3_cell(report_id, line_id, column, value)
|
||||||
|
|
||||||
|
async def upd_form3_cells(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
changes: list[dict],
|
||||||
|
user: AppUser,
|
||||||
|
) -> list[tuple]:
|
||||||
|
report_id = await self.resolve_report_id(project_id, year, report_type, user)
|
||||||
|
if not report_id:
|
||||||
|
raise ValidationException("Отчёт не найден")
|
||||||
|
return await self.project_repo.upd_form3_cells(report_id, changes)
|
||||||
|
|
||||||
|
async def add_form3_line(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
expense_item_id: int,
|
||||||
|
user: AppUser,
|
||||||
|
) -> list[tuple]:
|
||||||
|
report_id = await self.resolve_report_id(project_id, year, report_type, user)
|
||||||
|
if not report_id:
|
||||||
|
raise ValidationException("Отчёт не найден")
|
||||||
|
return await self.project_repo.add_form3_line(report_id, expense_item_id)
|
||||||
|
|
||||||
|
async def del_form3_line(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
year: int,
|
||||||
|
report_type: str,
|
||||||
|
line_id: int,
|
||||||
|
user: AppUser,
|
||||||
|
) -> list[tuple]:
|
||||||
|
report_id = await self.resolve_report_id(project_id, year, report_type, user)
|
||||||
|
if not report_id:
|
||||||
|
raise ValidationException("Отчёт не найден")
|
||||||
|
return await self.project_repo.del_form3_line(line_id)
|
||||||
|
|
||||||
|
async def upd_project(
|
||||||
|
self,
|
||||||
|
project_id: int,
|
||||||
|
column: str,
|
||||||
|
value,
|
||||||
|
user: AppUser,
|
||||||
|
):
|
||||||
|
project = await self.get(project_id, user)
|
||||||
|
if not project:
|
||||||
|
raise ValidationException("Проект не найден")
|
||||||
|
return await self.project_repo.upd_project(project_id, column, value)
|
||||||
|
|
||||||
|
async def add_project(
|
||||||
|
self,
|
||||||
|
name: str,
|
||||||
|
year: int,
|
||||||
|
org_unit_id: int,
|
||||||
|
level: str = "project",
|
||||||
|
parent_id: int | None = None,
|
||||||
|
project_type: str | None = None,
|
||||||
|
vsp_format: str | None = None,
|
||||||
|
placement_type: str | None = None,
|
||||||
|
object_address: str | None = None,
|
||||||
|
staff_count: int | None = None,
|
||||||
|
total_area: float | None = None,
|
||||||
|
) -> tuple[int, int, int]:
|
||||||
|
return await self.project_repo.add_project(
|
||||||
|
name=name,
|
||||||
|
year=year,
|
||||||
|
org_unit_id=org_unit_id,
|
||||||
|
level=level,
|
||||||
|
parent_id=parent_id,
|
||||||
|
project_type=project_type,
|
||||||
|
vsp_format=vsp_format,
|
||||||
|
placement_type=placement_type,
|
||||||
|
object_address=object_address,
|
||||||
|
staff_count=staff_count,
|
||||||
|
total_area=total_area,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _allowed_org_unit_ids(self, user: AppUser) -> list[int] | None:
|
||||||
|
if user.role_id == UserRoleEnum.ADMIN:
|
||||||
|
return None
|
||||||
|
db_user = await self.user_repo.get(user_id=user.id, load_orgs=True)
|
||||||
|
if not db_user:
|
||||||
|
return []
|
||||||
|
return [ou.id for ou in db_user.org_units]
|
||||||
|
|||||||
@ -1,38 +1,11 @@
|
|||||||
import os
|
import os
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from alembic import command
|
|
||||||
from alembic.config import Config
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
# Keep a single sqlite target for app + alembic during tests.
|
|
||||||
TEST_DATABASE_URL = "sqlite+aiosqlite:///./app.db"
|
|
||||||
os.environ["DATABASE_URL"] = TEST_DATABASE_URL
|
|
||||||
os.environ["OPENBAO__SETTINGS__DATABASE_URL"] = TEST_DATABASE_URL
|
|
||||||
os.environ["OPENBAO__SETTINGS_TEST__DATABASE_URL"] = TEST_DATABASE_URL
|
|
||||||
os.environ["OPENBAO__SETTINGS__DEBUG"] = "true"
|
|
||||||
os.environ["OPENBAO__SETTINGS_TEST__DEBUG"] = "true"
|
|
||||||
|
|
||||||
from src.main import app
|
from src.main import app
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session", autouse=True)
|
|
||||||
def migrate_db():
|
|
||||||
db_files = [Path("app.db"), Path("dfip_budget_planing.db"), Path("dfip_new_schema.db")]
|
|
||||||
for db_file in db_files:
|
|
||||||
if db_file.exists():
|
|
||||||
os.remove(db_file)
|
|
||||||
|
|
||||||
alembic_cfg = Config("alembic.ini")
|
|
||||||
command.upgrade(alembic_cfg, "head")
|
|
||||||
yield
|
|
||||||
|
|
||||||
for db_file in db_files:
|
|
||||||
if db_file.exists():
|
|
||||||
os.remove(db_file)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client():
|
def client():
|
||||||
with TestClient(app) as test_client:
|
with TestClient(app) as test_client:
|
||||||
@ -40,10 +13,16 @@ def client():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def admin_tokens(client):
|
def admin_password() -> str:
|
||||||
|
# Пароль админа берём из окружения, чтобы не хардкодить локальные отличия.
|
||||||
|
return os.getenv("OPENBAO__TEST_ADMIN_PASSWORD", "admin123")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def admin_tokens(client, admin_password: str):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={"username": "admin", "password": "admin"},
|
json={"username": "admin", "password": admin_password},
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
def test_login_success(client):
|
def test_login_success(client, admin_password):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={"username": "admin", "password": "admin"},
|
json={"username": "admin", "password": admin_password},
|
||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
data = response.json()
|
||||||
@ -9,10 +9,10 @@ def test_login_success(client):
|
|||||||
assert "refresh_token" in data
|
assert "refresh_token" in data
|
||||||
|
|
||||||
|
|
||||||
def test_login_invalid_password(client):
|
def test_login_invalid_password(client, admin_password):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
"/api/v1/auth/login",
|
"/api/v1/auth/login",
|
||||||
json={"username": "admin", "password": "wrong"},
|
json={"username": "admin", "password": f"{admin_password}_wrong"},
|
||||||
)
|
)
|
||||||
assert response.status_code == 401
|
assert response.status_code == 401
|
||||||
|
|
||||||
|
|||||||
123
api/tests/integration/test_projects_api_smoke.py
Normal file
123
api/tests/integration/test_projects_api_smoke.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def _auth_headers(tokens: dict) -> dict:
|
||||||
|
return {"Authorization": f"Bearer {tokens['access_token']}"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_projects_list_smoke(client, admin_tokens):
|
||||||
|
response = client.get("/api/v1/projects", 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 _create_project(client, admin_tokens) -> tuple[int, str]:
|
||||||
|
name = f"PT_{uuid.uuid4().hex[:10]}"
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/projects",
|
||||||
|
json={"name": name, "year": 2026, "branch_id": 1},
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
return payload["project_id"], name
|
||||||
|
|
||||||
|
|
||||||
|
def _add_line(client, admin_tokens, project_id: int) -> int:
|
||||||
|
for expense_item_id in range(1, 80):
|
||||||
|
response = client.post(
|
||||||
|
f"/api/v1/projects/{project_id}/report/2026/LIMIT/line",
|
||||||
|
json={"expense_item_id": expense_item_id},
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
if response.status_code != 200:
|
||||||
|
continue
|
||||||
|
rows = response.json()
|
||||||
|
input_row = next(
|
||||||
|
(
|
||||||
|
row
|
||||||
|
for row in rows
|
||||||
|
if row.get("row_type") == "INPUT" and row.get("data", {}).get("line_id")
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if input_row:
|
||||||
|
return int(input_row["data"]["line_id"])
|
||||||
|
|
||||||
|
pytest.skip("Не удалось подобрать expense_item_id для add_form3_line в текущей БД")
|
||||||
|
|
||||||
|
|
||||||
|
def test_projects_write_smoke(client, admin_tokens):
|
||||||
|
project_id, name = _create_project(client, admin_tokens)
|
||||||
|
|
||||||
|
upd_project_response = client.patch(
|
||||||
|
f"/api/v1/project/{project_id}",
|
||||||
|
json={"column": "name", "value": f"{name}_U"},
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert upd_project_response.status_code == 200
|
||||||
|
|
||||||
|
line_id = _add_line(client, admin_tokens, project_id)
|
||||||
|
|
||||||
|
upd_cell_response = client.patch(
|
||||||
|
f"/api/v1/projects/{project_id}/report/2026/LIMIT/cell",
|
||||||
|
json={"line_id": line_id, "column": "q1.m1", "value": 100},
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert upd_cell_response.status_code == 200
|
||||||
|
assert isinstance(upd_cell_response.json(), list)
|
||||||
|
|
||||||
|
upd_cells_response = client.patch(
|
||||||
|
f"/api/v1/projects/{project_id}/report/2026/LIMIT/cells",
|
||||||
|
json={
|
||||||
|
"changes": [
|
||||||
|
{"line_id": line_id, "column": "q1.m2", "value": 200},
|
||||||
|
{"line_id": line_id, "column": "q1.m3", "value": 300},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert upd_cells_response.status_code == 200
|
||||||
|
assert isinstance(upd_cells_response.json(), list)
|
||||||
|
|
||||||
|
del_line_response = client.delete(
|
||||||
|
f"/api/v1/projects/{project_id}/report/2026/LIMIT/line/{line_id}",
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert del_line_response.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
def test_projects_write_invalid_report_type_error_shape(client, admin_tokens):
|
||||||
|
project_id, _ = _create_project(client, admin_tokens)
|
||||||
|
line_id = _add_line(client, admin_tokens, project_id)
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/api/v1/projects/{project_id}/report/2026/BAD_TYPE/cell",
|
||||||
|
json={"line_id": line_id, "column": "q1.m1", "value": 100},
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "detail" in payload
|
||||||
|
assert isinstance(payload["detail"], str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_projects_write_report_not_found_error_shape(client, admin_tokens):
|
||||||
|
project_id, _ = _create_project(client, admin_tokens)
|
||||||
|
line_id = _add_line(client, admin_tokens, project_id)
|
||||||
|
|
||||||
|
response = client.patch(
|
||||||
|
f"/api/v1/projects/{project_id}/report/2099/LIMIT/cell",
|
||||||
|
json={"line_id": line_id, "column": "q1.m1", "value": 100},
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 400
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert payload.get("detail") == "Отчёт не найден"
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
import uuid
|
||||||
|
|
||||||
|
|
||||||
def _auth_headers(tokens: dict) -> dict:
|
def _auth_headers(tokens: dict) -> dict:
|
||||||
return {"Authorization": f"Bearer {tokens['access_token']}"}
|
return {"Authorization": f"Bearer {tokens['access_token']}"}
|
||||||
|
|
||||||
@ -8,10 +11,12 @@ def test_users_me(client, admin_tokens):
|
|||||||
assert response.json()["username"] == "admin"
|
assert response.json()["username"] == "admin"
|
||||||
|
|
||||||
|
|
||||||
def test_users_create_and_list(client, admin_tokens):
|
def test_users_create_smoke(client, admin_tokens):
|
||||||
|
suffix = uuid.uuid4().hex[:8]
|
||||||
|
username = f"user_{suffix}"
|
||||||
create_payload = {
|
create_payload = {
|
||||||
"email": "user1@example.com",
|
"email": f"{username}@example.com",
|
||||||
"username": "user1",
|
"username": username,
|
||||||
"password": "pass123",
|
"password": "pass123",
|
||||||
"full_name": "User One",
|
"full_name": "User One",
|
||||||
"role_id": 2,
|
"role_id": 2,
|
||||||
@ -22,9 +27,4 @@ def test_users_create_and_list(client, admin_tokens):
|
|||||||
headers=_auth_headers(admin_tokens),
|
headers=_auth_headers(admin_tokens),
|
||||||
)
|
)
|
||||||
assert created.status_code == 200
|
assert created.status_code == 200
|
||||||
|
assert created.json()["result"]["username"] == username
|
||||||
listed = client.get("/api/v1/users/", headers=_auth_headers(admin_tokens))
|
|
||||||
assert listed.status_code == 200
|
|
||||||
usernames = [row["username"] for row in listed.json()["result"]]
|
|
||||||
assert "admin" in usernames
|
|
||||||
assert "user1" in usernames
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user