Merge pull request 'create-form-back: в ручку создания задач передается список ссп, поправил падающий тест' (#41) from create-form-back into test

Reviewed-on: #41
Reviewed-by: Raykov-MS <RaykovMS@avt.rshb.ru>
This commit is contained in:
tsygankoviva 2026-06-08 16:38:15 +03:00
commit d43b6faa2a
7 changed files with 79 additions and 20 deletions

View File

@ -363,24 +363,29 @@ async def add_form(
form: FormCreateSchema,
db: AsyncSession = Depends(get_db),
current_user: AppUser = Depends(require_admin),
) -> BaseSingleResponse[BudgetFormResponse]:
) -> BaseListResponse[BudgetFormResponse]:
t0 = time.perf_counter()
bf_service = BudgetFormService(db)
org_service = OrgUnitService(db)
org_unit = await org_service.get(org_unit_id=form.org_unit_id, user=current_user)
if not org_unit:
raise HTTPException(404, f"ССП {form.org_unit_id} не найден")
org_units = await org_service.get_list(user=current_user, org_unit_id=form.org_unit_ids)
org_unit_ids = [o.id for o in org_units]
if len(org_units) != len(form.org_unit_ids):
not_found_ids = ", ".join(
[str(oid) for oid in set(form.org_unit_ids) - set(org_unit_ids)],
)
raise HTTPException(404, f"ССП {not_found_ids} не найдены")
new_form = await bf_service.create(
new_forms = await bf_service.bulk_create(
form_type_code=form.form_type_code,
year=form.year,
org_unit_id=org_unit.id,
org_unit_ids=org_unit_ids,
)
db_ms = (time.perf_counter() - t0) * 1000
response.headers["X-DB-Time-Ms"] = f"{db_ms:.2f}"
return BaseSingleResponse(
result = BudgetFormResponse.model_validate(new_form),
return BaseListResponse(
result=[BudgetFormResponse.model_validate(new_form) for new_form in new_forms],
count=len(new_forms),
)

View File

@ -223,7 +223,7 @@ class FormTypeSchemaEnum(str, enum.Enum):
class FormCreateSchema(BaseModel):
year: int
form_type_code: FormTypeSchemaEnum
org_unit_id: int
org_unit_ids: list[int] = Field(..., min_length=1)
class OrgUnitBaseSchema(BaseModel):

View File

@ -20,6 +20,7 @@ class OrgUnitRepository:
limit: int | None = None,
is_ssp: bool | None = None,
is_active: bool | None = None,
org_unit_id: int | list[int] | None = None,
with_count: bool = False,
load_users: bool = False,
) -> list[OrgUnit] | tuple[int, list[OrgUnit]]:
@ -32,7 +33,11 @@ class OrgUnitRepository:
where.append(OrgUnit.is_ssp == is_ssp)
if is_active is not None:
where.append(OrgUnit.is_active == is_active)
if org_unit_id is not None:
if isinstance(org_unit_id, int):
where.append(OrgUnit.id == org_unit_id)
else:
where.append(OrgUnit.id.in_(org_unit_id))
query = query.where(*where)
if load_users:

View File

@ -1,4 +1,6 @@
import asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from src.repository.user_repository import UserRepository
@ -76,4 +78,21 @@ class BudgetFormService:
form_type_code=form_type_code,
year=year,
org_unit_id=org_unit_id,
)
)
async def bulk_create(
self,
form_type_code: str,
year: int,
org_unit_ids: list[int],
) -> BudgetForm | None:
return await asyncio.gather(
*[
self.create(
form_type_code=form_type_code,
year=year,
org_unit_id=org_unit_id,
)
for org_unit_id in org_unit_ids
]
)

View File

@ -20,6 +20,7 @@ class OrgUnitService:
limit: int | None = None,
is_ssp: bool | None = None,
is_active: bool | None = None,
org_unit_id: int | list[int] | None = None,
with_count: bool = False,
load_users: bool = False,
) -> list[OrgUnit] | tuple[int, list[OrgUnit]]:
@ -33,6 +34,7 @@ class OrgUnitService:
is_ssp=is_ssp,
is_active=is_active,
load_users=load_users,
org_unit_id=org_unit_id,
)
async def get(

View File

@ -140,17 +140,20 @@ def test_forms_delete_line_not_found_error_shape(client, admin_tokens, auth_head
def test_forms_add_form_success(client, admin_tokens, auth_headers):
response = client.post(
"/api/v1/form/",
json={"year": 2025, "form_type_code": "FORM_1", "org_unit_id": 1},
json={"year": 2025, "form_type_code": "FORM_1", "org_unit_ids": [1]},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 200
payload = response.json()
assert "result" in payload
assert isinstance(payload["result"], dict)
assert payload["result"]["year"] == 2025
assert "id" in payload["result"]
form_id = payload["result"]["id"]
assert "result" in payload
assert isinstance(payload["result"], list)
assert len(payload["result"]) == 1
assert "year" in payload["result"][0]
assert payload["result"][0]["year"] == 2025
assert "id" in payload["result"][0]
form_id = payload["result"][0]["id"]
response = client.get(
f"/api/v1/form/{form_id}",
headers=auth_headers(admin_tokens),
@ -161,7 +164,7 @@ def test_forms_add_form_success(client, admin_tokens, auth_headers):
def test_forms_add_form_org_unit_not_found(client, admin_tokens, auth_headers):
response = client.post(
"/api/v1/form/",
json={"year": 2025, "form_type_code": "FORM_1", "org_unit_id": 999},
json={"year": 2025, "form_type_code": "FORM_1", "org_unit_ids": [1, 999]},
headers=auth_headers(admin_tokens),
)
assert response.status_code == 404
@ -173,7 +176,7 @@ def test_forms_add_form_org_unit_not_found(client, admin_tokens, auth_headers):
def test_forms_add_form_forbidden_not_admin(client, isp_tokens, auth_headers):
response = client.post(
"/api/v1/form/",
json={"year": 2025, "form_type_code": "FORM_1", "org_unit_id": 1},
json={"year": 2025, "form_type_code": "FORM_1", "org_unit_ids": [1]},
headers=auth_headers(isp_tokens),
)
assert response.status_code == 403

View File

@ -14,6 +14,7 @@ def _build_form(
form_type_code: FormTypeEnum = FormTypeEnum.FORM_1,
sheet_list: list[str] | None = None,
section_list: list[str] | None = None,
sheet_list_with_directions: list[dict] | None = None,
) -> SimpleNamespace:
return SimpleNamespace(
id=1,
@ -25,6 +26,22 @@ def _build_form(
code=form_type_code,
sheet_list=sheet_list or ["AHR", "SMETA"],
section_list=section_list or ["plan", "q1"],
sheet_list_with_directions=sheet_list_with_directions or [
{
"sheet_name": "AHR",
"direction": "Support",
},
{
"sheet_name": "AHR",
"direction": "Development",
},
{
"sheet_name": "SMETA",
"direction": None,
},
],
),
)
@ -50,7 +67,15 @@ async def test_export_sheet_rejects_direction_for_unsupported_sheet():
async def test_export_sheet_rejects_sections_for_unsupported_sheet():
service = ExportService(AsyncMock())
service.budget_form_service.get = AsyncMock(
return_value=_build_form(sheet_list=["SMETA"])
return_value=_build_form(
sheet_list=["SMETA"],
sheet_list_with_directions=[
{
"sheet_name": "SMETA",
"direction": None,
}
],
)
)
service.sheet_service.get = AsyncMock(return_value=[])