"""Document rows to API shapes. Every endpoint in the package answers with `summary` or `detail`, so the per-request fields (why this user sees it, what they may do, what is still open) are computed in exactly one place. """ import uuid from typing import Any from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.api.documents.access import access_reason, can_edit from app.api.documents.schemas import ( DepartmentRef, DocumentDetail, DocumentSummary, ReviewOut, ) from app.models import Department, DocPermission, Document, ReviewRequest, User def document_fields(document: Document, user: User) -> dict[str, Any]: """Built explicitly rather than via model_validate: access_reason and can_edit are per-request, so there is nothing to read them from.""" return { "id": document.id, "title": document.title, "status": document.status, "visibility": document.visibility, "department_id": document.department_id, "created_at": document.created_at, "updated_at": document.updated_at, "access_reason": access_reason(document, user), "can_edit": can_edit(document, user), "open_reviews": len(document.open_reviews), "is_builtin": document.is_builtin, } def summary(document: Document, user: User) -> DocumentSummary: return DocumentSummary(**document_fields(document, user)) def detail( document: Document, user: User, *, reviews: list[ReviewOut] | None = None, shared_departments: list[DepartmentRef] | None = None, ) -> DocumentDetail: return DocumentDetail( **document_fields(document, user), content_md=document.content_md, reviews=reviews or [], shared_departments=shared_departments or [], ) async def resolve_reviews( db: AsyncSession, document: Document, user: User ) -> list[ReviewOut]: """The document's requests with the names filled in. One query for every name involved, rather than three relationships loaded with every document: the names are needed on the detail page only, while the requests themselves ride along everywhere (they decide who may edit). """ if not document.reviews: return [] wanted = { person_id for review in document.reviews for person_id in ( review.requester_id, review.reviewer_id, review.resolved_by_id, ) if person_id is not None } names = dict( (await db.execute(select(User.id, User.name).where(User.id.in_(wanted)))).all() ) return [ ReviewOut( id=review.id, question=review.question, requester_name=names.get(review.requester_id), reviewer_id=review.reviewer_id, reviewer_name=names.get(review.reviewer_id), created_at=review.created_at, resolved_at=review.resolved_at, resolved_by_name=names.get(review.resolved_by_id), is_mine=review.reviewer_id == user.id, ) for review in document.reviews ] async def resolve_shared_departments( db: AsyncSession, document: Document ) -> list[DepartmentRef]: """The additional departments this document is shared with (its `doc_permissions` grants), resolved to names for display.""" rows = ( await db.execute( select(Department.id, Department.name) .join(DocPermission, DocPermission.department_id == Department.id) .where(DocPermission.document_id == document.id) .order_by(Department.name) ) ).all() return [DepartmentRef(id=row.id, name=row.name) for row in rows] async def granted_department_ids( db: AsyncSession, document_id: uuid.UUID ) -> set[uuid.UUID]: return set( ( await db.execute( select(DocPermission.department_id).where( DocPermission.document_id == document_id ) ) ) .scalars() .all() ) async def full_detail( db: AsyncSession, document: Document, user: User ) -> DocumentDetail: """The detail with everything resolved — for the endpoints that answer with a document the UI is about to render in full.""" return detail( document, user, reviews=await resolve_reviews(db, document, user), shared_departments=await resolve_shared_departments(db, document), ) async def open_review_for( db: AsyncSession, document: Document, reviewer_id: uuid.UUID ) -> ReviewRequest | None: """An unanswered request on this document addressed to `reviewer_id`.""" return ( await db.execute( select(ReviewRequest).where( ReviewRequest.document_id == document.id, ReviewRequest.reviewer_id == reviewer_id, ReviewRequest.resolved_at.is_(None), ) ) ).scalar_one_or_none()