AURORA-1984: событие project_added без программы
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
209d36e832
commit
69c4a8f9c9
@ -482,7 +482,9 @@ class FormEventProcess:
|
|||||||
case "project_added":
|
case "project_added":
|
||||||
return await self.__add_project(
|
return await self.__add_project(
|
||||||
event_data=event_data["data"],
|
event_data=event_data["data"],
|
||||||
|
form_id=form_id,
|
||||||
user=curr_user,
|
user=curr_user,
|
||||||
|
sheet=sheet,
|
||||||
)
|
)
|
||||||
case "program_updated":
|
case "program_updated":
|
||||||
return await self.__update_form4_program(
|
return await self.__update_form4_program(
|
||||||
@ -610,17 +612,21 @@ class FormEventProcess:
|
|||||||
async def __add_project(
|
async def __add_project(
|
||||||
self,
|
self,
|
||||||
event_data: dict,
|
event_data: dict,
|
||||||
|
form_id: int,
|
||||||
|
sheet: str,
|
||||||
user: AppUser,
|
user: AppUser,
|
||||||
) -> list[tuple]:
|
) -> list[tuple]:
|
||||||
"""
|
"""
|
||||||
data = {
|
data = {
|
||||||
name: str,
|
name: str,
|
||||||
program_id: int
|
program_id?: int
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
return await self.sheet_service.add_project(
|
return await self.sheet_service.add_project(
|
||||||
name=event_data["name"],
|
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,
|
user=user,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -208,7 +208,9 @@ class SheetRepository:
|
|||||||
async def add_project(
|
async def add_project(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
program_id: int,
|
program_id: int | None = None,
|
||||||
|
form_id: int | None = None,
|
||||||
|
section_code: str | None = None,
|
||||||
) -> list[tuple]:
|
) -> list[tuple]:
|
||||||
return [
|
return [
|
||||||
tuple(r) for r in (
|
tuple(r) for r in (
|
||||||
@ -218,13 +220,17 @@ class SheetRepository:
|
|||||||
SELECT project_id, row_type, depth, sort_order, data
|
SELECT project_id, row_type, depth, sort_order, data
|
||||||
FROM v3.add_form4_project(
|
FROM v3.add_form4_project(
|
||||||
:p_name,
|
:p_name,
|
||||||
:p_program_id
|
:p_program_id,
|
||||||
|
:p_form_id,
|
||||||
|
:p_section_code
|
||||||
)
|
)
|
||||||
"""
|
"""
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
"p_program_id": program_id,
|
|
||||||
"p_name": name,
|
"p_name": name,
|
||||||
|
"p_program_id": program_id,
|
||||||
|
"p_form_id": form_id,
|
||||||
|
"p_section_code": section_code,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
).all()
|
).all()
|
||||||
|
|||||||
@ -158,31 +158,54 @@ class SheetService:
|
|||||||
async def add_project(
|
async def add_project(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
program_id: int,
|
program_id: int | None = None,
|
||||||
|
form_id: int | None = None,
|
||||||
|
sheet: str | None = None,
|
||||||
user: AppUser | None = None,
|
user: AppUser | None = None,
|
||||||
) -> tuple[int, list[tuple]]:
|
) -> tuple[int, list[tuple]] | None:
|
||||||
if user.role_id != UserRoleEnum.ADMIN:
|
if program_id is None:
|
||||||
program = await self.f4_project_repo.get(
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
project_id=program_id,
|
user = await self.user_repo.get(
|
||||||
level=Form4ProjectLevelEnum.PROGRAM,
|
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,
|
||||||
)
|
)
|
||||||
if not program:
|
else:
|
||||||
raise SheetValidationError("Программа не найдена")
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
|
program = await self.f4_project_repo.get(
|
||||||
|
project_id=program_id,
|
||||||
|
level=Form4ProjectLevelEnum.PROGRAM,
|
||||||
|
)
|
||||||
|
if not program:
|
||||||
|
raise SheetValidationError("Программа не найдена")
|
||||||
|
|
||||||
user = await self.user_repo.get(
|
user = await self.user_repo.get(
|
||||||
user_id=user.id,
|
user_id=user.id,
|
||||||
load_orgs=True,
|
load_orgs=True,
|
||||||
|
)
|
||||||
|
form = await self.bf_repo.get(
|
||||||
|
budget_form_id=program.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=program_id,
|
||||||
|
form_id=None,
|
||||||
|
section_code=None,
|
||||||
)
|
)
|
||||||
form = await self.bf_repo.get(
|
|
||||||
budget_form_id=program.form_id,
|
|
||||||
org_unit=[ou.id for ou in user.org_units],
|
|
||||||
)
|
|
||||||
if not form:
|
|
||||||
raise SheetValidationError("Программа не найдена")
|
|
||||||
result = await self.sheet_repo.add_project(
|
|
||||||
program_id=program_id,
|
|
||||||
name=name,
|
|
||||||
)
|
|
||||||
if result:
|
if result:
|
||||||
return (result[0][0], [tuple(r[1:]) for r in result])
|
return (result[0][0], [tuple(r[1:]) for r in result])
|
||||||
return None
|
return None
|
||||||
|
|||||||
@ -6,6 +6,7 @@ from pathlib import Path
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from sheet_contract import strip_nulls
|
from sheet_contract import strip_nulls
|
||||||
|
from src.repository.sheet_repository import SheetRepository
|
||||||
from test_form4_sheet_sums import F4, SHEET, _loads
|
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)
|
solo = standalone(f4); f4.line(solo)
|
||||||
with pytest.raises(Exception, match="Нельзя удалить проект, содержащий строки бюджета"):
|
with pytest.raises(Exception, match="Нельзя удалить проект, содержащий строки бюджета"):
|
||||||
f4.sql("SELECT * FROM v3.del_form4_project(:p)", p=solo)
|
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"]
|
||||||
|
|||||||
54
api/tests/unit/test_sheet_service_form4_standalone.py
Normal file
54
api/tests/unit/test_sheet_service_form4_standalone.py
Normal 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)
|
||||||
Loading…
x
Reference in New Issue
Block a user