feat: implement final approval gate workflow and checklist
This commit is contained in:
@@ -0,0 +1,378 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID
|
||||
|
||||
from src.domain.contracts import (
|
||||
AgentJobType,
|
||||
AssetStatus,
|
||||
AssetType,
|
||||
ArticleWorkflowStatus,
|
||||
ClaimRiskLevel,
|
||||
ClaimSupportStatus,
|
||||
ContentReviewKind,
|
||||
FinalApprovalChecklistItemSummary,
|
||||
FinalApprovalChecklistResponse,
|
||||
FinalApprovalRequest,
|
||||
FinalApprovalResponse,
|
||||
FinalRevisionRequest,
|
||||
FinalRevisionRequestResponse,
|
||||
PlanReviewStatus,
|
||||
)
|
||||
|
||||
|
||||
def get_final_approval_checklist(
|
||||
repository: object,
|
||||
*,
|
||||
article_id: UUID,
|
||||
request: FinalApprovalRequest | None = None,
|
||||
) -> FinalApprovalChecklistResponse:
|
||||
repository.articles.get(article_id)
|
||||
latest_draft = repository.article_drafts.latest_for_article(article_id)
|
||||
approved_plan = _approved_plan(repository, article_id=article_id)
|
||||
evidence = repository.evidence_items.list_for_article(article_id)
|
||||
assets = repository.assets.list_for_article(article_id)
|
||||
unresolved = repository.content_reviews.list_unresolved_for_article(article_id=article_id)
|
||||
high_risk_unsupported = _high_risk_unsupported_claims(
|
||||
repository,
|
||||
article_id=article_id,
|
||||
draft=latest_draft,
|
||||
)
|
||||
settings = request.publishing_settings if request is not None else None
|
||||
requested_draft_version = request.draft_version if request is not None else None
|
||||
|
||||
checklist_items: list[FinalApprovalChecklistItemSummary] = []
|
||||
blockers: list[str] = []
|
||||
completed_items: list[str] = []
|
||||
|
||||
def add_item(key: str, title: str, completed: bool, blocker: str | None = None) -> None:
|
||||
checklist_items.append(
|
||||
FinalApprovalChecklistItemSummary(
|
||||
key=key,
|
||||
title=title,
|
||||
completed=completed,
|
||||
blocker=blocker,
|
||||
)
|
||||
)
|
||||
if completed:
|
||||
completed_items.append(title)
|
||||
elif blocker:
|
||||
blockers.append(blocker)
|
||||
|
||||
has_draft = latest_draft is not None
|
||||
add_item(
|
||||
key="exact_draft_version",
|
||||
title="Exact draft version selected",
|
||||
completed=has_draft and requested_draft_version == latest_draft.version,
|
||||
blocker=(
|
||||
None
|
||||
if has_draft and requested_draft_version == latest_draft.version
|
||||
else (
|
||||
"Draft version is required for final approval."
|
||||
if not has_draft
|
||||
else f"Final approval requires latest draft version {latest_draft.version}."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
add_item(
|
||||
key="plan_followed",
|
||||
title="Plan followed",
|
||||
completed=has_draft and _draft_contains_approved_plan(approved_plan, latest_draft.body_markdown),
|
||||
blocker="Approved plan sections are not fully reflected in the assembled draft.",
|
||||
)
|
||||
add_item(
|
||||
key="evidence_reviewed",
|
||||
title="Evidence reviewed",
|
||||
completed=bool(evidence) and all(item.review_status != "PENDING" for item in evidence),
|
||||
blocker="Evidence review is incomplete (there are pending evidence items).",
|
||||
)
|
||||
add_item(
|
||||
key="no_high_risk_unsupported_claims",
|
||||
title="No high-risk unsupported claims remain",
|
||||
completed=not high_risk_unsupported,
|
||||
blocker=(
|
||||
None
|
||||
if not high_risk_unsupported
|
||||
else (
|
||||
"High-risk unsupported claims must be resolved: "
|
||||
+ "; ".join(high_risk_unsupported[:3])
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
add_item(
|
||||
key="seo_metadata_approved",
|
||||
title="SEO metadata approved",
|
||||
completed=has_draft and _has_seo_metadata(latest_draft) and not _has_unresolved_meta_issues(unresolved),
|
||||
blocker="SEO metadata is incomplete or has unresolved review issues.",
|
||||
)
|
||||
|
||||
required_assets = [asset for asset in assets if asset.status != AssetStatus.REJECTED]
|
||||
unapproved_required_assets = [
|
||||
asset
|
||||
for asset in required_assets
|
||||
if asset.status != AssetStatus.APPROVED or not asset.file_url
|
||||
]
|
||||
add_item(
|
||||
key="images_assets_approved",
|
||||
title="Images/assets approved",
|
||||
completed=bool(required_assets) and not unapproved_required_assets,
|
||||
blocker=(
|
||||
"Required assets are missing."
|
||||
if not required_assets
|
||||
else f"{len(unapproved_required_assets)} required assets are not approved."
|
||||
),
|
||||
)
|
||||
|
||||
required_table_diagrams = [
|
||||
asset
|
||||
for asset in required_assets
|
||||
if asset.asset_type in {
|
||||
AssetType.DIAGRAM,
|
||||
AssetType.TABLE,
|
||||
AssetType.FLOWCHART,
|
||||
AssetType.COMPARISON_MATRIX,
|
||||
AssetType.ARCHITECTURE_DIAGRAM,
|
||||
}
|
||||
]
|
||||
unapproved_table_diagrams = [
|
||||
asset
|
||||
for asset in required_table_diagrams
|
||||
if asset.status != AssetStatus.APPROVED or not asset.file_url
|
||||
]
|
||||
add_item(
|
||||
key="tables_and_diagrams_approved",
|
||||
title="Tables and diagrams approved",
|
||||
completed=bool(required_table_diagrams) and not unapproved_table_diagrams,
|
||||
blocker=(
|
||||
"Required table/diagram assets are missing."
|
||||
if not required_table_diagrams
|
||||
else f"{len(unapproved_table_diagrams)} table/diagram assets are not approved."
|
||||
),
|
||||
)
|
||||
|
||||
has_internal_links = has_draft and _has_internal_links(latest_draft.body_markdown)
|
||||
add_item(
|
||||
key="internal_links_approved",
|
||||
title="Internal links approved",
|
||||
completed=bool(has_internal_links) and not _has_unresolved_internal_link_issue(unresolved),
|
||||
blocker="Draft must include approved internal links.",
|
||||
)
|
||||
|
||||
frontmatter = settings.frontmatter if settings is not None else None
|
||||
add_item(
|
||||
key="frontmatter_fields_selected",
|
||||
title="Frontmatter fields selected",
|
||||
completed=isinstance(frontmatter, dict) and bool(frontmatter),
|
||||
blocker="Required publishing settings are missing: frontmatter fields.",
|
||||
)
|
||||
content_path = settings.content_path if settings is not None else None
|
||||
add_item(
|
||||
key="content_path_selected",
|
||||
title="Content path selected",
|
||||
completed=bool(content_path and content_path.strip()),
|
||||
blocker="Required publishing settings are missing: content path.",
|
||||
)
|
||||
author = settings.author if settings is not None else None
|
||||
add_item(
|
||||
key="author_selected",
|
||||
title="Author selected",
|
||||
completed=bool(author and author.strip()),
|
||||
blocker="Required publishing settings are missing: author.",
|
||||
)
|
||||
publishing_mode = settings.publishing_mode if settings is not None else None
|
||||
add_item(
|
||||
key="publishing_mode_selected",
|
||||
title="Publishing mode selected",
|
||||
completed=bool(publishing_mode and publishing_mode.strip()),
|
||||
blocker="Required publishing settings are missing: publishing mode.",
|
||||
)
|
||||
|
||||
can_approve = len(blockers) == 0
|
||||
return FinalApprovalChecklistResponse(
|
||||
article_id=article_id,
|
||||
draft_version=latest_draft.version if latest_draft is not None else None,
|
||||
requested_draft_version=requested_draft_version,
|
||||
can_approve=can_approve,
|
||||
completed_items=completed_items,
|
||||
blockers=blockers,
|
||||
items=checklist_items,
|
||||
)
|
||||
|
||||
|
||||
def approve_final_article(
|
||||
repository: object,
|
||||
*,
|
||||
article_id: UUID,
|
||||
request: FinalApprovalRequest,
|
||||
actor_user_id: UUID,
|
||||
) -> FinalApprovalResponse:
|
||||
article = repository.articles.get(article_id)
|
||||
checklist = get_final_approval_checklist(
|
||||
repository,
|
||||
article_id=article_id,
|
||||
request=request,
|
||||
)
|
||||
if not checklist.can_approve:
|
||||
raise PermissionError("; ".join(checklist.blockers))
|
||||
|
||||
now = _now()
|
||||
updated = repository.articles.update_status(
|
||||
article_id=article_id,
|
||||
status=ArticleWorkflowStatus.PUBLISH_DRY_RUN_REQUIRED,
|
||||
updated_at=now,
|
||||
)
|
||||
repository.articles.create_workflow_event(
|
||||
article_id=article_id,
|
||||
event_type="FINAL_APPROVAL_GRANTED",
|
||||
from_status=article.status,
|
||||
to_status=ArticleWorkflowStatus.PUBLISH_DRY_RUN_REQUIRED,
|
||||
actor_user_id=actor_user_id,
|
||||
payload={
|
||||
"draft_version": checklist.draft_version,
|
||||
"publishing_settings": (
|
||||
request.publishing_settings.model_dump(mode="json")
|
||||
if request.publishing_settings is not None
|
||||
else {}
|
||||
),
|
||||
"completed_items": checklist.completed_items,
|
||||
},
|
||||
created_at=now,
|
||||
)
|
||||
return FinalApprovalResponse(article=updated, checklist=checklist)
|
||||
|
||||
|
||||
def request_final_revision(
|
||||
repository: object,
|
||||
*,
|
||||
article_id: UUID,
|
||||
request: FinalRevisionRequest,
|
||||
actor_user_id: UUID,
|
||||
) -> FinalRevisionRequestResponse:
|
||||
article = repository.articles.get(article_id)
|
||||
checklist_request = FinalApprovalRequest(
|
||||
draft_version=request.draft_version,
|
||||
publishing_settings=None,
|
||||
)
|
||||
checklist = get_final_approval_checklist(
|
||||
repository,
|
||||
article_id=article_id,
|
||||
request=checklist_request,
|
||||
)
|
||||
now = _now()
|
||||
updated = repository.articles.update_status(
|
||||
article_id=article_id,
|
||||
status=ArticleWorkflowStatus.FINAL_REVISION_REQUIRED,
|
||||
updated_at=now,
|
||||
)
|
||||
repository.articles.create_workflow_event(
|
||||
article_id=article_id,
|
||||
event_type="FINAL_REVISION_REQUESTED",
|
||||
from_status=article.status,
|
||||
to_status=ArticleWorkflowStatus.FINAL_REVISION_REQUIRED,
|
||||
actor_user_id=actor_user_id,
|
||||
payload={
|
||||
"reason": (request.reason or "").strip(),
|
||||
"draft_version": checklist.draft_version,
|
||||
"blockers": checklist.blockers,
|
||||
"completed_items": checklist.completed_items,
|
||||
},
|
||||
created_at=now,
|
||||
)
|
||||
return FinalRevisionRequestResponse(article=updated, checklist=checklist)
|
||||
|
||||
|
||||
def _approved_plan(repository: object, *, article_id: UUID) -> object | None:
|
||||
plans = repository.article_plans.list_for_article(article_id)
|
||||
approved_plans = [plan for plan in plans if plan.status == PlanReviewStatus.APPROVED]
|
||||
if not approved_plans:
|
||||
return None
|
||||
approved_plans.sort(key=lambda plan: plan.version)
|
||||
return approved_plans[-1]
|
||||
|
||||
|
||||
def _draft_contains_approved_plan(approved_plan: object | None, body_markdown: str) -> bool:
|
||||
if approved_plan is None or not approved_plan.sections:
|
||||
return False
|
||||
for section in approved_plan.sections:
|
||||
if f"## {section.heading}" not in body_markdown:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _has_seo_metadata(draft: object) -> bool:
|
||||
return bool(draft.meta_title and draft.meta_title.strip()) and bool(
|
||||
draft.meta_description and draft.meta_description.strip()
|
||||
)
|
||||
|
||||
|
||||
def _has_unresolved_meta_issues(unresolved: list[object]) -> bool:
|
||||
return any(
|
||||
item.review_kind == ContentReviewKind.SEO
|
||||
and item.location in {"meta_title", "meta_description", "title"}
|
||||
for item in unresolved
|
||||
)
|
||||
|
||||
|
||||
def _has_internal_links(markdown: str) -> bool:
|
||||
return re.search(r"\]\(/[^)]+\)", markdown) is not None
|
||||
|
||||
|
||||
def _has_unresolved_internal_link_issue(unresolved: list[object]) -> bool:
|
||||
for item in unresolved:
|
||||
if item.suggestion_key == "internal-link":
|
||||
return True
|
||||
if "internal link" in item.message.lower():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _high_risk_unsupported_claims(
|
||||
repository: object,
|
||||
*,
|
||||
article_id: UUID,
|
||||
draft: object | None,
|
||||
) -> list[str]:
|
||||
claims: list[str] = []
|
||||
for claim in repository.claims.list_for_article(article_id):
|
||||
if (
|
||||
claim.support_status == ClaimSupportStatus.UNSUPPORTED
|
||||
and claim.risk_level == ClaimRiskLevel.HIGH
|
||||
):
|
||||
claims.append(claim.claim_text)
|
||||
|
||||
for job in repository.agent_jobs.list_for_article(article_id):
|
||||
if job.job_type != AgentJobType.SECTION_SCAFFOLD:
|
||||
continue
|
||||
raw_claims = job.payload.get("unsupported_claims")
|
||||
if not isinstance(raw_claims, list):
|
||||
continue
|
||||
for raw in raw_claims:
|
||||
if isinstance(raw, dict):
|
||||
risk_level = str(raw.get("risk_level", "")).lower()
|
||||
claim_text = str(raw.get("claim_text", "")).strip()
|
||||
if risk_level == "high" and claim_text:
|
||||
claims.append(claim_text)
|
||||
|
||||
if draft is not None:
|
||||
for warning in draft.unsupported_claim_warnings:
|
||||
if "high-risk" in warning.lower():
|
||||
claims.append(warning)
|
||||
|
||||
deduped: list[str] = []
|
||||
seen: set[str] = set()
|
||||
for claim in claims:
|
||||
normalized = claim.strip()
|
||||
if not normalized:
|
||||
continue
|
||||
if normalized in seen:
|
||||
continue
|
||||
seen.add(normalized)
|
||||
deduped.append(normalized)
|
||||
return deduped
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(UTC)
|
||||
@@ -57,6 +57,11 @@ from .models import (
|
||||
EvidenceMatrixResponse,
|
||||
EvidenceResponse,
|
||||
EvidenceUpdateRequest,
|
||||
FinalApprovalChecklistItemSummary,
|
||||
FinalApprovalChecklistResponse,
|
||||
FinalApprovalPublishingSettings,
|
||||
FinalApprovalRequest,
|
||||
FinalApprovalResponse,
|
||||
PlanSectionSummary,
|
||||
PlanSectionEditRequest,
|
||||
PlanListResponse,
|
||||
@@ -78,6 +83,8 @@ from .models import (
|
||||
SeoReviewReportResponse,
|
||||
LanguageReviewRunResponse,
|
||||
LanguageReviewReportResponse,
|
||||
FinalRevisionRequest,
|
||||
FinalRevisionRequestResponse,
|
||||
FinalReviewIssueSummary,
|
||||
FinalReviewIssuesResponse,
|
||||
RunnerFileRef,
|
||||
@@ -149,6 +156,13 @@ __all__ = [
|
||||
"EvidenceMatrixResponse",
|
||||
"EvidenceResponse",
|
||||
"EvidenceUpdateRequest",
|
||||
"FinalApprovalChecklistItemSummary",
|
||||
"FinalApprovalChecklistResponse",
|
||||
"FinalApprovalPublishingSettings",
|
||||
"FinalApprovalRequest",
|
||||
"FinalApprovalResponse",
|
||||
"FinalRevisionRequest",
|
||||
"FinalRevisionRequestResponse",
|
||||
"FinalReviewIssueSummary",
|
||||
"FinalReviewIssuesResponse",
|
||||
"LanguageReviewRunResponse",
|
||||
|
||||
@@ -598,6 +598,50 @@ class FinalReviewIssuesResponse(ContractModel):
|
||||
unresolved_issues: list[FinalReviewIssueSummary] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FinalApprovalPublishingSettings(ContractModel):
|
||||
content_path: str | None = None
|
||||
author: str | None = None
|
||||
publishing_mode: str | None = None
|
||||
frontmatter: JsonObject | None = None
|
||||
|
||||
|
||||
class FinalApprovalRequest(ContractModel):
|
||||
draft_version: int | None = Field(default=None, ge=1)
|
||||
publishing_settings: FinalApprovalPublishingSettings | None = None
|
||||
|
||||
|
||||
class FinalRevisionRequest(ContractModel):
|
||||
reason: str | None = None
|
||||
draft_version: int | None = Field(default=None, ge=1)
|
||||
|
||||
|
||||
class FinalApprovalChecklistItemSummary(ContractModel):
|
||||
key: str = Field(min_length=1)
|
||||
title: str = Field(min_length=1)
|
||||
completed: bool
|
||||
blocker: str | None = None
|
||||
|
||||
|
||||
class FinalApprovalChecklistResponse(ContractModel):
|
||||
article_id: UUID
|
||||
draft_version: int | None = Field(default=None, ge=1)
|
||||
requested_draft_version: int | None = Field(default=None, ge=1)
|
||||
can_approve: bool
|
||||
completed_items: list[str] = Field(default_factory=list)
|
||||
blockers: list[str] = Field(default_factory=list)
|
||||
items: list[FinalApprovalChecklistItemSummary] = Field(default_factory=list)
|
||||
|
||||
|
||||
class FinalApprovalResponse(ContractModel):
|
||||
article: ArticleSummary
|
||||
checklist: FinalApprovalChecklistResponse
|
||||
|
||||
|
||||
class FinalRevisionRequestResponse(ContractModel):
|
||||
article: ArticleSummary
|
||||
checklist: FinalApprovalChecklistResponse
|
||||
|
||||
|
||||
class AssetGenerateSpecsResponse(ContractModel):
|
||||
assets: list[AssetSummary] = Field(default_factory=list)
|
||||
|
||||
|
||||
@@ -58,6 +58,11 @@ from .models import (
|
||||
ContentReviewIssueSummary,
|
||||
ContentReviewReportSummary,
|
||||
ContentReviewSuggestionSummary,
|
||||
FinalApprovalChecklistItemSummary,
|
||||
FinalApprovalChecklistResponse,
|
||||
FinalApprovalPublishingSettings,
|
||||
FinalApprovalRequest,
|
||||
FinalApprovalResponse,
|
||||
EvidenceSummary,
|
||||
EvidenceCreateRequest,
|
||||
EvidenceMatrixResponse,
|
||||
@@ -84,6 +89,8 @@ from .models import (
|
||||
ReviewSuggestionEditRequest,
|
||||
ReviewActionResponse,
|
||||
ReviewSummary,
|
||||
FinalRevisionRequest,
|
||||
FinalRevisionRequestResponse,
|
||||
SeoReviewReportResponse,
|
||||
SeoReviewRunResponse,
|
||||
RunnerFileRef,
|
||||
@@ -167,6 +174,13 @@ CONTRACT_SCHEMA_MODELS: tuple[type[BaseModel], ...] = (
|
||||
ContentReviewIssueSummary,
|
||||
ContentReviewReportSummary,
|
||||
ContentReviewSuggestionSummary,
|
||||
FinalApprovalChecklistItemSummary,
|
||||
FinalApprovalChecklistResponse,
|
||||
FinalApprovalPublishingSettings,
|
||||
FinalApprovalRequest,
|
||||
FinalApprovalResponse,
|
||||
FinalRevisionRequest,
|
||||
FinalRevisionRequestResponse,
|
||||
FinalReviewIssueSummary,
|
||||
FinalReviewIssuesResponse,
|
||||
LanguageReviewReportResponse,
|
||||
|
||||
@@ -15,6 +15,7 @@ from src.presentation.routes.auth import router as auth_router
|
||||
from src.presentation.routes.boundary_questions import router as boundary_questions_router
|
||||
from src.presentation.routes.drafts import router as drafts_router
|
||||
from src.presentation.routes.evidence import router as evidence_router
|
||||
from src.presentation.routes.final_approval import router as final_approval_router
|
||||
from src.presentation.routes.plans import router as plans_router
|
||||
from src.presentation.routes.reviews import router as reviews_router
|
||||
from src.presentation.routes.sites import router as sites_router
|
||||
@@ -29,6 +30,7 @@ app.include_router(plans_router)
|
||||
app.include_router(evidence_router)
|
||||
app.include_router(drafts_router)
|
||||
app.include_router(reviews_router)
|
||||
app.include_router(final_approval_router)
|
||||
app.include_router(agent_jobs_router)
|
||||
app.include_router(internal_agent_jobs_router)
|
||||
app.include_router(sites_router)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
|
||||
from src.application.final_approval import (
|
||||
approve_final_article,
|
||||
get_final_approval_checklist,
|
||||
request_final_revision,
|
||||
)
|
||||
from src.domain.auth import EDITOR_OR_ADMIN_ROLES
|
||||
from src.domain.contracts import (
|
||||
CurrentUser,
|
||||
FinalApprovalChecklistResponse,
|
||||
FinalApprovalRequest,
|
||||
FinalApprovalResponse,
|
||||
FinalRevisionRequest,
|
||||
FinalRevisionRequestResponse,
|
||||
)
|
||||
from src.infrastructure.repositories import BackendRepository
|
||||
from src.presentation.dependencies import get_repository, require_roles
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["final-approval"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/articles/{article_id}/final-approval/checklist",
|
||||
response_model=FinalApprovalChecklistResponse,
|
||||
)
|
||||
def get_article_final_approval_checklist(
|
||||
article_id: UUID,
|
||||
_: CurrentUser = Depends(require_roles(EDITOR_OR_ADMIN_ROLES)),
|
||||
repository: BackendRepository = Depends(get_repository),
|
||||
) -> FinalApprovalChecklistResponse:
|
||||
try:
|
||||
return get_final_approval_checklist(
|
||||
repository,
|
||||
article_id=article_id,
|
||||
request=None,
|
||||
)
|
||||
except LookupError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") from error
|
||||
|
||||
|
||||
@router.post(
|
||||
"/articles/{article_id}/final-approval",
|
||||
response_model=FinalApprovalResponse,
|
||||
)
|
||||
def post_article_final_approval(
|
||||
article_id: UUID,
|
||||
request: FinalApprovalRequest,
|
||||
current_user: CurrentUser = Depends(require_roles(EDITOR_OR_ADMIN_ROLES)),
|
||||
repository: BackendRepository = Depends(get_repository),
|
||||
) -> FinalApprovalResponse:
|
||||
try:
|
||||
return approve_final_article(
|
||||
repository,
|
||||
article_id=article_id,
|
||||
request=request,
|
||||
actor_user_id=current_user.id,
|
||||
)
|
||||
except LookupError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") from error
|
||||
except PermissionError as error:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail=str(error)) from error
|
||||
|
||||
|
||||
@router.post(
|
||||
"/articles/{article_id}/final-revision-request",
|
||||
response_model=FinalRevisionRequestResponse,
|
||||
)
|
||||
def post_article_final_revision_request(
|
||||
article_id: UUID,
|
||||
request: FinalRevisionRequest,
|
||||
current_user: CurrentUser = Depends(require_roles(EDITOR_OR_ADMIN_ROLES)),
|
||||
repository: BackendRepository = Depends(get_repository),
|
||||
) -> FinalRevisionRequestResponse:
|
||||
try:
|
||||
return request_final_revision(
|
||||
repository,
|
||||
article_id=article_id,
|
||||
request=request,
|
||||
actor_user_id=current_user.id,
|
||||
)
|
||||
except LookupError as error:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Not Found") from error
|
||||
|
||||
Reference in New Issue
Block a user