AURORA-1984: событие project_added без программы

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-08 22:27:16 +04:00
parent 209d36e832
commit 69c4a8f9c9
5 changed files with 136 additions and 26 deletions

View File

@ -482,7 +482,9 @@ class FormEventProcess:
case "project_added":
return await self.__add_project(
event_data=event_data["data"],
form_id=form_id,
user=curr_user,
sheet=sheet,
)
case "program_updated":
return await self.__update_form4_program(
@ -610,17 +612,21 @@ class FormEventProcess:
async def __add_project(
self,
event_data: dict,
form_id: int,
sheet: str,
user: AppUser,
) -> list[tuple]:
"""
data = {
name: str,
program_id: int
program_id?: int
}
"""
return await self.sheet_service.add_project(
name=event_data["name"],
program_id=event_data["program_id"],
program_id=event_data.get("program_id"),
form_id=form_id,
sheet=sheet,
user=user,
)

View File

@ -208,7 +208,9 @@ class SheetRepository:
async def add_project(
self,
name: str,
program_id: int,
program_id: int | None = None,
form_id: int | None = None,
section_code: str | None = None,
) -> list[tuple]:
return [
tuple(r) for r in (
@ -218,13 +220,17 @@ class SheetRepository:
SELECT project_id, row_type, depth, sort_order, data
FROM v3.add_form4_project(
:p_name,
:p_program_id
:p_program_id,
:p_form_id,
:p_section_code
)
"""
),
{
"p_program_id": program_id,
"p_name": name,
"p_program_id": program_id,
"p_form_id": form_id,
"p_section_code": section_code,
}
)
).all()

View File

@ -158,9 +158,30 @@ class SheetService:
async def add_project(
self,
name: str,
program_id: int,
program_id: int | None = None,
form_id: int | None = None,
sheet: str | None = None,
user: AppUser | None = None,
) -> tuple[int, list[tuple]]:
) -> tuple[int, list[tuple]] | None:
if program_id is None:
if user.role_id != UserRoleEnum.ADMIN:
user = await self.user_repo.get(
user_id=user.id,
load_orgs=True,
)
form = await self.bf_repo.get(
budget_form_id=form_id,
org_unit=[ou.id for ou in user.org_units],
)
if not form:
raise SheetValidationError("Форма не найдена")
result = await self.sheet_repo.add_project(
name=name,
program_id=None,
form_id=form_id,
section_code=sheet,
)
else:
if user.role_id != UserRoleEnum.ADMIN:
program = await self.f4_project_repo.get(
project_id=program_id,
@ -180,8 +201,10 @@ class SheetService:
if not form:
raise SheetValidationError("Программа не найдена")
result = await self.sheet_repo.add_project(
program_id=program_id,
name=name,
program_id=program_id,
form_id=None,
section_code=None,
)
if result:
return (result[0][0], [tuple(r[1:]) for r in result])

View File

@ -6,6 +6,7 @@ from pathlib import Path
import pytest
from sheet_contract import strip_nulls
from src.repository.sheet_repository import SheetRepository
from test_form4_sheet_sums import F4, SHEET, _loads
@ -169,3 +170,23 @@ def test_delete_standalone_with_line_rejected(f4):
solo = standalone(f4); f4.line(solo)
with pytest.raises(Exception, match="Нельзя удалить проект, содержащий строки бюджета"):
f4.sql("SELECT * FROM v3.del_form4_project(:p)", p=solo)
def test_rename_standalone_with_root_returns_item_and_root(f4):
solo = standalone(f4); f4.line(solo)
rows = f4.sql("SELECT row_type, data::text FROM v3.upd_form4_project(:p, 'Новое')", p=solo)
assert sorted(r[0] for r in rows) == ["ITEM", "ROOT"]
def test_delete_empty_standalone_with_root_returns_root_only(f4):
first = standalone(f4, "А"); f4.line(first)
second = standalone(f4, "Б")
rows = f4.sql("SELECT row_type FROM v3.del_form4_project(:p)", p=second)
assert [r[0] for r in rows] == ["ROOT"]
def test_repository_creates_in_section(f4):
async def _run():
return await SheetRepository(f4.db).add_project(name="Р", form_id=f4.form_id, section_code="CAP")
rows = f4.client.portal.call(_run)
assert [r[1] for r in rows] == ["ITEM"]

View File

@ -0,0 +1,54 @@
from types import SimpleNamespace
from unittest.mock import AsyncMock
import pytest
from src.db.models.role import UserRoleEnum
from src.repository.sheet_repository import SheetValidationError
from src.services.sheet_service import SheetService
ADMIN = SimpleNamespace(id=1, role_id=UserRoleEnum.ADMIN)
EXECUTOR = SimpleNamespace(id=5, role_id=UserRoleEnum.EXECUTOR_RF)
def make_service() -> SheetService:
service = SheetService(AsyncMock())
service.sheet_repo = AsyncMock()
service.bf_repo = AsyncMock()
service.user_repo = AsyncMock()
service.f4_project_repo = AsyncMock()
service.user_repo.get.return_value = SimpleNamespace(org_units=[])
return service
@pytest.mark.asyncio
async def test_admin_creates_in_section():
service = make_service()
service.sheet_repo.add_project.return_value = [(9, "ITEM", 2, 1, {})]
result = await service.add_project(name="П", form_id=7, sheet="CAP", user=ADMIN)
assert result == (9, [("ITEM", 2, 1, {})])
service.sheet_repo.add_project.assert_awaited_once_with(
name="П", program_id=None, form_id=7, section_code="CAP"
)
@pytest.mark.asyncio
async def test_foreign_executor_cannot_create_in_section():
service = make_service()
service.bf_repo.get.return_value = None
with pytest.raises(SheetValidationError):
await service.add_project(name="П", form_id=7, sheet="CAP", user=EXECUTOR)
service.sheet_repo.add_project.assert_not_awaited()
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["update_form4_project", "delete_form4_project"])
async def test_foreign_executor_cannot_change_standalone(method):
service = make_service()
service.f4_project_repo.get.return_value = SimpleNamespace(form_id=7, parent_id=None)
service.bf_repo.get.return_value = None
kwargs = {"project_id": 3, "user": EXECUTOR}
if method == "update_form4_project":
kwargs["name"] = "Н"
with pytest.raises(SheetValidationError):
await getattr(service, method)(**kwargs)