stages-back: круды для этапов
This commit is contained in:
parent
47fac8acbc
commit
8299b35d07
110
api/src/api/v1/form_phases.py
Normal file
110
api/src/api/v1/form_phases.py
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.app_user import AppUser
|
||||||
|
from src.domain.schemas import (
|
||||||
|
BaseListResponse,
|
||||||
|
BaseSingleResponse,
|
||||||
|
FormPhaseCreate,
|
||||||
|
FormPhaseResponse,
|
||||||
|
FormPhaseUpdate,
|
||||||
|
)
|
||||||
|
from src.services.budget_form_service import BudgetFormService
|
||||||
|
from src.services.form_phase_service import FormPhaseService
|
||||||
|
from src.api.v1.deps import get_current_active_user_with_set_db, require_admin
|
||||||
|
from src.db.session import get_db
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/stages", tags=["stages"])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/form/{form_id}")
|
||||||
|
async def get_form_phases(
|
||||||
|
form_id: int,
|
||||||
|
sheet: str | None = None,
|
||||||
|
phase_code: str | None = None,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(get_current_active_user_with_set_db),
|
||||||
|
) -> BaseListResponse[FormPhaseResponse]:
|
||||||
|
fp_service = FormPhaseService(db)
|
||||||
|
phases = await fp_service.get_list(
|
||||||
|
budget_form_id=form_id,
|
||||||
|
user=current_user,
|
||||||
|
sheet=sheet,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
return BaseListResponse(
|
||||||
|
result=[FormPhaseResponse.model_validate(p) for p in phases],
|
||||||
|
count=len(phases),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/form/{form_id}")
|
||||||
|
async def create_form_phase(
|
||||||
|
form_id: int,
|
||||||
|
body: FormPhaseCreate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
) -> BaseSingleResponse[FormPhaseResponse]:
|
||||||
|
bf_service = BudgetFormService(db)
|
||||||
|
form = await bf_service.get(budget_form_id=form_id, user=current_user, load_form_type=True)
|
||||||
|
if not form:
|
||||||
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
|
fp_service = FormPhaseService(db)
|
||||||
|
phase = await fp_service.create(
|
||||||
|
budget_form=form,
|
||||||
|
body=body,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
return BaseSingleResponse(result=FormPhaseResponse.model_validate(phase))
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/form/{form_id}/{sheet}/{phase_code}")
|
||||||
|
async def update_form_phase(
|
||||||
|
form_id: int,
|
||||||
|
sheet: str,
|
||||||
|
phase_code: str,
|
||||||
|
body: FormPhaseUpdate,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
) -> BaseSingleResponse[FormPhaseResponse]:
|
||||||
|
bf_service = BudgetFormService(db)
|
||||||
|
form = await bf_service.get(budget_form_id=form_id, user=current_user, load_form_type=True)
|
||||||
|
if not form:
|
||||||
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
|
fp_service = FormPhaseService(db)
|
||||||
|
phase = await fp_service.update(
|
||||||
|
budget_form=form,
|
||||||
|
sheet=sheet,
|
||||||
|
phase_code=phase_code,
|
||||||
|
body=body,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
if not phase:
|
||||||
|
raise HTTPException(404, "Этап не найден")
|
||||||
|
return BaseSingleResponse(result=FormPhaseResponse.model_validate(phase))
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/form/{form_id}/{sheet}/{phase_code}")
|
||||||
|
async def delete_form_phase(
|
||||||
|
form_id: int,
|
||||||
|
sheet: str,
|
||||||
|
phase_code: str,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AppUser = Depends(require_admin),
|
||||||
|
):
|
||||||
|
bf_service = BudgetFormService(db)
|
||||||
|
form = await bf_service.get(budget_form_id=form_id, user=current_user)
|
||||||
|
if not form:
|
||||||
|
raise HTTPException(404, "Форма не найдена")
|
||||||
|
|
||||||
|
fp_service = FormPhaseService(db)
|
||||||
|
deleted = await fp_service.delete(
|
||||||
|
budget_form_id=form_id,
|
||||||
|
sheet=sheet,
|
||||||
|
phase_code=phase_code,
|
||||||
|
user=current_user,
|
||||||
|
)
|
||||||
|
if not deleted:
|
||||||
|
raise HTTPException(404, "Этап не найден")
|
||||||
@ -1,6 +1,7 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter
|
||||||
|
|
||||||
from src.api.v1 import auth, users, admin, forms, projects
|
from src.api.v1 import auth, users, admin, forms, form_phases, projects
|
||||||
|
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(auth.router)
|
api_router.include_router(auth.router)
|
||||||
@ -8,3 +9,4 @@ api_router.include_router(users.router)
|
|||||||
api_router.include_router(admin.router)
|
api_router.include_router(admin.router)
|
||||||
api_router.include_router(forms.router)
|
api_router.include_router(forms.router)
|
||||||
api_router.include_router(projects.router)
|
api_router.include_router(projects.router)
|
||||||
|
api_router.include_router(form_phases.router)
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ValidationError(BaseModel):
|
class ValidationError(BaseModel):
|
||||||
field: str = Field(...)
|
field: str = Field(...)
|
||||||
description: str = Field(...)
|
description: str = Field(...)
|
||||||
@ -10,6 +12,14 @@ class BasicAppException(Exception):
|
|||||||
description: str | None = None
|
description: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationsError(BasicAppException):
|
||||||
|
errors: dict[str, list[str]]
|
||||||
|
|
||||||
|
def __init__(self, errors, *args):
|
||||||
|
super().__init__(*args)
|
||||||
|
self.errors = errors
|
||||||
|
|
||||||
|
|
||||||
class AccessDeniedException(BasicAppException):
|
class AccessDeniedException(BasicAppException):
|
||||||
description = "Доступ запрещен"
|
description = "Доступ запрещен"
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ from src.core.errors import (
|
|||||||
UserNotFoundException,
|
UserNotFoundException,
|
||||||
UsernameConflictException,
|
UsernameConflictException,
|
||||||
ValidationException,
|
ValidationException,
|
||||||
|
ValidationsError,
|
||||||
)
|
)
|
||||||
from src.core.CONSTANTS import (
|
from src.core.CONSTANTS import (
|
||||||
VALIDATION_PREFIXES,
|
VALIDATION_PREFIXES,
|
||||||
@ -141,3 +142,10 @@ def register_exception_handlers(app: FastAPI) -> None:
|
|||||||
status_code=status_code,
|
status_code=status_code,
|
||||||
content=_error_payload(code, message, field=field),
|
content=_error_payload(code, message, field=field),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.exception_handler(ValidationsError)
|
||||||
|
async def validations_error_handler(request, exc: ValidationsError):
|
||||||
|
return JSONResponse(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
content=exc.errors,
|
||||||
|
)
|
||||||
|
|||||||
@ -10,6 +10,7 @@ from src.db.models.collegial_approval import CollegialApproval
|
|||||||
from src.db.models.contract_detail import ContractDetail
|
from src.db.models.contract_detail import ContractDetail
|
||||||
from src.db.models.contract_summary import ContractSummary
|
from src.db.models.contract_summary import ContractSummary
|
||||||
from src.db.models.form_limit import FormLimit
|
from src.db.models.form_limit import FormLimit
|
||||||
|
from src.db.models.form_phase import FormPhase
|
||||||
from src.db.models.form_type import FormType
|
from src.db.models.form_type import FormType
|
||||||
from src.db.models.limit_template import LimitTemplate
|
from src.db.models.limit_template import LimitTemplate
|
||||||
from src.db.models.org_unit import OrgUnit
|
from src.db.models.org_unit import OrgUnit
|
||||||
|
|||||||
26
api/src/db/models/form_phase.py
Normal file
26
api/src/db/models/form_phase.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import ARRAY, CheckConstraint, DateTime, ForeignKey, String, Text, text
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from src.db.base import Base
|
||||||
|
|
||||||
|
|
||||||
|
class FormPhase(Base):
|
||||||
|
__tablename__ = "form_phase"
|
||||||
|
__table_args__ = (
|
||||||
|
CheckConstraint(text("cardinality(column_keys) > 0"), name="chk_v4_form_phase_columns"),
|
||||||
|
CheckConstraint(text("role != 'ADMIN'"), name="chk_v4_form_phase_no_admin"),
|
||||||
|
CheckConstraint(text("closes_at > opens_at"), name="chk_v4_form_phase_window"),
|
||||||
|
{"schema": "v3"},
|
||||||
|
)
|
||||||
|
|
||||||
|
budget_form_id: Mapped[int] = mapped_column(
|
||||||
|
ForeignKey("v3.budget_form.id", ondelete="CASCADE"), primary_key=True
|
||||||
|
)
|
||||||
|
sheet: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
phase_code: Mapped[str] = mapped_column(String, primary_key=True)
|
||||||
|
role: Mapped[str] = mapped_column(String, ForeignKey("v3.role.code"))
|
||||||
|
column_keys: Mapped[list[str]] = mapped_column(ARRAY(Text))
|
||||||
|
opens_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
|
closes_at: Mapped[datetime] = mapped_column(DateTime(timezone=True))
|
||||||
@ -49,15 +49,3 @@ class FormType(Base):
|
|||||||
@property
|
@property
|
||||||
def section_list(self):
|
def section_list(self):
|
||||||
return self.SECTIONS_BY_FORM_TYPE.get(self.code, [])
|
return self.SECTIONS_BY_FORM_TYPE.get(self.code, [])
|
||||||
|
|
||||||
# budget_forms: Mapped[list["BudgetForm"]] = relationship( # type: ignore
|
|
||||||
# "BudgetForm", back_populates="form_type"
|
|
||||||
# )
|
|
||||||
# expense_item_form_types: Mapped[list["ExpenseItemFormType"]] = relationship( # type: ignore
|
|
||||||
# "ExpenseItemFormType", back_populates="form_type"
|
|
||||||
# )
|
|
||||||
# phase_templates: Mapped[list["PhaseTemplate"]] = relationship( # type: ignore
|
|
||||||
# "PhaseTemplate", back_populates="form_type"
|
|
||||||
# )
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,8 @@ from datetime import datetime
|
|||||||
import enum
|
import enum
|
||||||
from typing import Any, Generic, Literal, Optional, TypeVar
|
from typing import Any, Generic, Literal, Optional, TypeVar
|
||||||
|
|
||||||
|
FormPhaseRole = Literal["DFIP", "EXECUTOR_RF"]
|
||||||
|
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||||
|
|
||||||
@ -250,3 +252,31 @@ class AddLineSchema(BaseModel):
|
|||||||
justification: Optional[str] = None
|
justification: Optional[str] = None
|
||||||
contract_number: Optional[str] = None
|
contract_number: Optional[str] = None
|
||||||
contract_end_date: Optional[datetime] = None
|
contract_end_date: Optional[datetime] = None
|
||||||
|
|
||||||
|
|
||||||
|
class FormPhaseResponse(BaseModel):
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
budget_form_id: int
|
||||||
|
sheet: str
|
||||||
|
phase_code: str
|
||||||
|
role: str
|
||||||
|
column_keys: list[str]
|
||||||
|
opens_at: datetime
|
||||||
|
closes_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class FormPhaseCreate(BaseModel):
|
||||||
|
sheet: str
|
||||||
|
phase_code: str
|
||||||
|
role: FormPhaseRole
|
||||||
|
column_keys: list[str]
|
||||||
|
opens_at: datetime
|
||||||
|
closes_at: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class FormPhaseUpdate(BaseModel):
|
||||||
|
role: FormPhaseRole | None = None
|
||||||
|
column_keys: list[str] | None = None
|
||||||
|
opens_at: datetime | None = None
|
||||||
|
closes_at: datetime | None = None
|
||||||
|
|||||||
84
api/src/repository/form_phase_repository.py
Normal file
84
api/src/repository/form_phase_repository.py
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from sqlalchemy import delete, select, update
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.db.models.form_phase import FormPhase
|
||||||
|
|
||||||
|
|
||||||
|
class FormPhaseRepository:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
budget_form_id: int,
|
||||||
|
sheet: str | None = None,
|
||||||
|
phase_code: str | None = None,
|
||||||
|
) -> list[FormPhase]:
|
||||||
|
query = select(FormPhase).where(FormPhase.budget_form_id == budget_form_id)
|
||||||
|
if sheet is not None:
|
||||||
|
query = query.where(FormPhase.sheet == sheet)
|
||||||
|
if phase_code is not None:
|
||||||
|
query = query.where(FormPhase.phase_code == phase_code)
|
||||||
|
return (await self.db.execute(query)).scalars().all()
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self, budget_form_id: int, sheet: str, phase_code: str
|
||||||
|
) -> FormPhase | None:
|
||||||
|
query = (
|
||||||
|
select(FormPhase)
|
||||||
|
.where(FormPhase.budget_form_id == budget_form_id)
|
||||||
|
.where(FormPhase.sheet == sheet)
|
||||||
|
.where(FormPhase.phase_code == phase_code)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
budget_form_id: int,
|
||||||
|
sheet: str,
|
||||||
|
phase_code: str,
|
||||||
|
role: str,
|
||||||
|
column_keys: list[str],
|
||||||
|
opens_at: datetime,
|
||||||
|
closes_at: datetime,
|
||||||
|
) -> FormPhase:
|
||||||
|
form_phase = FormPhase(
|
||||||
|
budget_form_id=budget_form_id,
|
||||||
|
sheet=sheet,
|
||||||
|
phase_code=phase_code,
|
||||||
|
role=role,
|
||||||
|
column_keys=column_keys,
|
||||||
|
opens_at=opens_at,
|
||||||
|
closes_at=closes_at,
|
||||||
|
)
|
||||||
|
self.db.add(form_phase)
|
||||||
|
await self.db.flush()
|
||||||
|
return form_phase
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self, budget_form_id: int, sheet: str, phase_code: str, data: dict
|
||||||
|
) -> FormPhase | None:
|
||||||
|
query = (
|
||||||
|
update(FormPhase)
|
||||||
|
.where(FormPhase.budget_form_id == budget_form_id)
|
||||||
|
.where(FormPhase.sheet == sheet)
|
||||||
|
.where(FormPhase.phase_code == phase_code)
|
||||||
|
.values(**data)
|
||||||
|
.returning(FormPhase)
|
||||||
|
)
|
||||||
|
return (await self.db.execute(query)).scalar_one_or_none()
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self, budget_form_id: int, sheet: str, phase_code: str
|
||||||
|
) -> bool:
|
||||||
|
query = (
|
||||||
|
delete(FormPhase)
|
||||||
|
.where(FormPhase.budget_form_id == budget_form_id)
|
||||||
|
.where(FormPhase.sheet == sheet)
|
||||||
|
.where(FormPhase.phase_code == phase_code)
|
||||||
|
)
|
||||||
|
result = await self.db.execute(query)
|
||||||
|
return result.rowcount > 0
|
||||||
139
api/src/services/form_phase_service.py
Normal file
139
api/src/services/form_phase_service.py
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from src.core.errors import AccessDeniedException, ValidationsError
|
||||||
|
from src.db.models.budget_form import BudgetForm
|
||||||
|
from src.db.models.role import UserRoleEnum
|
||||||
|
from src.repository.budget_form_repository import BudgetFormRepository
|
||||||
|
from src.db.models.app_user import AppUser
|
||||||
|
from src.db.models.form_phase import FormPhase
|
||||||
|
from src.domain.schemas import FormPhaseCreate, FormPhaseUpdate
|
||||||
|
from src.repository.form_phase_repository import FormPhaseRepository
|
||||||
|
|
||||||
|
|
||||||
|
class FormPhaseService:
|
||||||
|
def __init__(self, db: AsyncSession):
|
||||||
|
self.db = db
|
||||||
|
self.fp_repo = FormPhaseRepository(db)
|
||||||
|
self.bf_repo = BudgetFormRepository(db)
|
||||||
|
|
||||||
|
async def get_list(
|
||||||
|
self,
|
||||||
|
budget_form_id: int,
|
||||||
|
user: AppUser,
|
||||||
|
sheet: str | None = None,
|
||||||
|
phase_code: str | None = None,
|
||||||
|
) -> list[FormPhase]:
|
||||||
|
return await self.fp_repo.get_list(
|
||||||
|
budget_form_id=budget_form_id,
|
||||||
|
sheet=sheet,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def get(
|
||||||
|
self, budget_form_id: int, sheet: str, phase_code: str, user: AppUser
|
||||||
|
) -> FormPhase | None:
|
||||||
|
return await self.fp_repo.get(
|
||||||
|
budget_form_id=budget_form_id,
|
||||||
|
sheet=sheet,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _validate_form_phase(
|
||||||
|
self,
|
||||||
|
form_phase: FormPhaseCreate | FormPhaseUpdate,
|
||||||
|
budget_form: BudgetForm,
|
||||||
|
sheet: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
errors = {}
|
||||||
|
if budget_form.form_type.sheet_list:
|
||||||
|
sheet = sheet or form_phase.sheet
|
||||||
|
if sheet not in budget_form.form_type.sheet_list:
|
||||||
|
errors["sheet"] = [f"У формы нет листа {sheet}"]
|
||||||
|
if form_phase.column_keys and budget_form.form_type.section_list:
|
||||||
|
for column_key in form_phase.column_keys:
|
||||||
|
section = column_key.split(".")
|
||||||
|
if section[0] not in budget_form.form_type.section_list:
|
||||||
|
if "column_keys" in errors:
|
||||||
|
errors["column_keys"].append(f"Колонка {column_key} некорректна")
|
||||||
|
else:
|
||||||
|
errors["column_keys"] = [f"Колонка {column_key} некорректна"]
|
||||||
|
if errors:
|
||||||
|
raise ValidationsError(errors=errors)
|
||||||
|
|
||||||
|
|
||||||
|
async def create(
|
||||||
|
self,
|
||||||
|
body: FormPhaseCreate,
|
||||||
|
user: AppUser,
|
||||||
|
budget_form_id: int | None = None,
|
||||||
|
budget_form: BudgetForm | None = None,
|
||||||
|
) -> FormPhase:
|
||||||
|
assert budget_form or budget_form_id
|
||||||
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
|
raise AccessDeniedException
|
||||||
|
|
||||||
|
if not budget_form or budget_form.form_type is None:
|
||||||
|
budget_form = await self.bf_repo.get(
|
||||||
|
budget_form_id=budget_form_id or budget_form.id,
|
||||||
|
load_form_type=True,
|
||||||
|
)
|
||||||
|
self._validate_form_phase(
|
||||||
|
form_phase=body,
|
||||||
|
budget_form=budget_form,
|
||||||
|
)
|
||||||
|
return await self.fp_repo.create(
|
||||||
|
budget_form_id=budget_form_id or budget_form.id,
|
||||||
|
sheet=body.sheet,
|
||||||
|
phase_code=body.phase_code,
|
||||||
|
role=body.role,
|
||||||
|
column_keys=body.column_keys,
|
||||||
|
opens_at=body.opens_at,
|
||||||
|
closes_at=body.closes_at,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def update(
|
||||||
|
self,
|
||||||
|
sheet: str,
|
||||||
|
phase_code: str,
|
||||||
|
body: FormPhaseUpdate,
|
||||||
|
user: AppUser,
|
||||||
|
budget_form_id: int | None = None,
|
||||||
|
budget_form: BudgetForm | None = None,
|
||||||
|
) -> FormPhase:
|
||||||
|
assert budget_form or budget_form_id
|
||||||
|
if user.role_id != UserRoleEnum.ADMIN:
|
||||||
|
raise AccessDeniedException
|
||||||
|
|
||||||
|
if not budget_form or budget_form.form_type is None:
|
||||||
|
budget_form = await self.bf_repo.get(
|
||||||
|
budget_form_id=budget_form_id or budget_form.id,
|
||||||
|
load_form_type=True,
|
||||||
|
)
|
||||||
|
self._validate_form_phase(
|
||||||
|
form_phase=body,
|
||||||
|
budget_form=budget_form,
|
||||||
|
sheet=sheet,
|
||||||
|
)
|
||||||
|
|
||||||
|
data = body.model_dump(exclude_unset=True)
|
||||||
|
if not data:
|
||||||
|
return await self.fp_repo.get(
|
||||||
|
budget_form_id=budget_form_id or budget_form.id,
|
||||||
|
sheet=sheet,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
|
return await self.fp_repo.update(
|
||||||
|
budget_form_id=budget_form_id or budget_form.id,
|
||||||
|
sheet=sheet,
|
||||||
|
phase_code=phase_code,
|
||||||
|
data=data,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self, budget_form_id: int, sheet: str, phase_code: str, user: AppUser
|
||||||
|
) -> bool:
|
||||||
|
return await self.fp_repo.delete(
|
||||||
|
budget_form_id=budget_form_id,
|
||||||
|
sheet=sheet,
|
||||||
|
phase_code=phase_code,
|
||||||
|
)
|
||||||
@ -1,4 +1,5 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
from contextlib import contextmanager
|
||||||
import os
|
import os
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -11,12 +12,11 @@ from src.main import app
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def client():
|
def client():
|
||||||
db = SessionLocal()
|
|
||||||
loop = asyncio.get_event_loop()
|
loop = asyncio.get_event_loop()
|
||||||
|
|
||||||
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
async def _get_db():
|
async def _get_db():
|
||||||
try:
|
try:
|
||||||
@ -39,6 +39,12 @@ def admin_password() -> str:
|
|||||||
return os.getenv("OPENBAO__TEST_ADMIN_PASSWORD", "admin123")
|
return os.getenv("OPENBAO__TEST_ADMIN_PASSWORD", "admin123")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def isp_password() -> str:
|
||||||
|
# Пароль админа берём из окружения, чтобы не хардкодить локальные отличия.
|
||||||
|
return os.getenv("OPENBAO__TEST_EXECUTOR_PASSWORD", "admin123")
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def admin_tokens(client, admin_password: str):
|
def admin_tokens(client, admin_password: str):
|
||||||
response = client.post(
|
response = client.post(
|
||||||
@ -47,3 +53,13 @@ def admin_tokens(client, admin_password: str):
|
|||||||
)
|
)
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
return response.json()
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def isp_tokens(client, isp_password: str):
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/auth/login",
|
||||||
|
json={"username": "isp1", "password": isp_password},
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
return response.json()
|
||||||
|
|||||||
140
api/tests/integration/test_form_phases_api_smoke.py
Normal file
140
api/tests/integration/test_form_phases_api_smoke.py
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
def _auth_headers(tokens: dict) -> dict:
|
||||||
|
return {"Authorization": f"Bearer {tokens['access_token']}"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_phases_list_smoke(client, admin_tokens):
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/form/1", 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_form_phases_list_filter_smoke(client, admin_tokens):
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/form/1", headers=_auth_headers(admin_tokens)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "count" in payload
|
||||||
|
count = payload["count"]
|
||||||
|
|
||||||
|
response = client.get(
|
||||||
|
"/api/v1/stages/form/1?sheet=AHR&phase_code=test", headers=_auth_headers(admin_tokens)
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "count" in payload
|
||||||
|
assert payload["count"] <= count
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_phases_create_forbidden(client, isp_tokens):
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/stages/form/1",
|
||||||
|
json={
|
||||||
|
"sheet": "AHR",
|
||||||
|
"phase_code": "new_phase",
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": ["plan.q1"],
|
||||||
|
"opens_at": "2026-01-01T00:00:00Z",
|
||||||
|
"closes_at": "2026-12-31T00:00:00Z",
|
||||||
|
},
|
||||||
|
headers=_auth_headers(isp_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_phases_update_forbidden(client, isp_tokens):
|
||||||
|
response = client.patch(
|
||||||
|
"/api/v1/stages/form/1/AHR/test",
|
||||||
|
json={
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": ["plan.q1"],
|
||||||
|
"opens_at": "2026-01-01T00:00:00Z",
|
||||||
|
"closes_at": "2026-12-31T00:00:00Z",
|
||||||
|
},
|
||||||
|
headers=_auth_headers(isp_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_phases_delete_forbidden(client, isp_tokens):
|
||||||
|
response = client.delete(
|
||||||
|
"/api/v1/stages/form/1/AHR/test",
|
||||||
|
headers=_auth_headers(isp_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 403
|
||||||
|
payload = response.json()
|
||||||
|
assert isinstance(payload, dict)
|
||||||
|
assert "message" in payload
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_phases_create(client, admin_tokens):
|
||||||
|
body = {
|
||||||
|
"budget_form_id": 1,
|
||||||
|
"sheet": "AHR",
|
||||||
|
"phase_code": "new_phase",
|
||||||
|
"role": "DFIP",
|
||||||
|
"column_keys": ["plan.q1"],
|
||||||
|
"opens_at": "2026-01-01T00:00:00Z",
|
||||||
|
"closes_at": "2026-12-31T00:00:00Z",
|
||||||
|
}
|
||||||
|
response = client.post(
|
||||||
|
"/api/v1/stages/form/1",
|
||||||
|
json=body,
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
assert "success" in payload
|
||||||
|
assert payload["success"]
|
||||||
|
assert "result" in payload
|
||||||
|
assert isinstance(payload["result"], dict)
|
||||||
|
assert payload["result"] == body
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_phases_update(client, admin_tokens):
|
||||||
|
body = {
|
||||||
|
"budget_form_id": 1,
|
||||||
|
"sheet": "AHR",
|
||||||
|
"phase_code": "test",
|
||||||
|
"role": "EXECUTOR_RF",
|
||||||
|
"column_keys": ["plan.q1"],
|
||||||
|
"opens_at": "2026-01-01T00:00:00Z",
|
||||||
|
"closes_at": "2026-12-31T00:00:00Z",
|
||||||
|
}
|
||||||
|
response = client.patch(
|
||||||
|
"/api/v1/stages/form/1/AHR/test",
|
||||||
|
json=body,
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
payload = response.json()
|
||||||
|
|
||||||
|
assert "success" in payload
|
||||||
|
assert payload["success"]
|
||||||
|
assert "result" in payload
|
||||||
|
assert isinstance(payload["result"], dict)
|
||||||
|
assert payload["result"] == body
|
||||||
|
|
||||||
|
|
||||||
|
def test_form_phases_delete(client, admin_tokens):
|
||||||
|
response = client.delete(
|
||||||
|
"/api/v1/stages/form/1/AHR/test",
|
||||||
|
headers=_auth_headers(admin_tokens),
|
||||||
|
)
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user