solve merge conflicts
This commit is contained in:
commit
816f0e06bb
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 src.api.v1 import auth, users, admin, audit, forms, projects
|
||||
from src.api.v1 import auth, users, admin, audit, forms, form_phases, projects
|
||||
|
||||
|
||||
api_router = APIRouter()
|
||||
api_router.include_router(auth.router)
|
||||
@ -9,3 +10,4 @@ api_router.include_router(admin.router)
|
||||
api_router.include_router(audit.router)
|
||||
api_router.include_router(forms.router)
|
||||
api_router.include_router(projects.router)
|
||||
api_router.include_router(form_phases.router)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
|
||||
|
||||
class ValidationError(BaseModel):
|
||||
field: str = Field(...)
|
||||
description: str = Field(...)
|
||||
@ -10,6 +12,14 @@ class BasicAppException(Exception):
|
||||
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):
|
||||
description = "Доступ запрещен"
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ from src.core.errors import (
|
||||
UserNotFoundException,
|
||||
UsernameConflictException,
|
||||
ValidationException,
|
||||
ValidationsError,
|
||||
)
|
||||
from src.core.CONSTANTS import (
|
||||
VALIDATION_PREFIXES,
|
||||
@ -141,3 +142,10 @@ def register_exception_handlers(app: FastAPI) -> None:
|
||||
status_code=status_code,
|
||||
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_summary import ContractSummary
|
||||
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.limit_template import LimitTemplate
|
||||
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
|
||||
def section_list(self):
|
||||
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
|
||||
from typing import Any, Dict, Generic, List, Literal, Optional, TypeVar
|
||||
|
||||
FormPhaseRole = Literal["DFIP", "EXECUTOR_RF"]
|
||||
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, EmailStr, Field
|
||||
|
||||
@ -252,6 +254,34 @@ class AddLineSchema(BaseModel):
|
||||
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
|
||||
|
||||
|
||||
class AuditLogBase(BaseModel):
|
||||
"""Базовая схема записи аудита (единый формат вывода как у auditlog)."""
|
||||
|
||||
|
||||
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
|
||||
from contextlib import contextmanager
|
||||
import os
|
||||
|
||||
import pytest
|
||||
@ -11,12 +12,11 @@ from src.main import app
|
||||
|
||||
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
db = SessionLocal()
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
async def _get_db():
|
||||
try:
|
||||
@ -39,6 +39,12 @@ def admin_password() -> str:
|
||||
return os.getenv("OPENBAO__TEST_ADMIN_PASSWORD", "admin123")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isp_password() -> str:
|
||||
# Пароль админа берём из окружения, чтобы не хардкодить локальные отличия.
|
||||
return os.getenv("OPENBAO__TEST_EXECUTOR_PASSWORD", "admin123")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def admin_tokens(client, admin_password: str):
|
||||
response = client.post(
|
||||
@ -49,6 +55,16 @@ def admin_tokens(client, admin_password: str):
|
||||
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()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth_headers():
|
||||
def _build(tokens: dict) -> dict:
|
||||
|
||||
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