Files
ProfessorNovaandClaude Opus 5 784b76baf7 Pablan, as it stands
Self-hosted knowledge management for SMEs: a split-screen Markdown editor
whose sections an LLM refines while you write, and RAG question answering
over the documents that result. FastAPI + Postgres/pgvector on the back,
SvelteKit on the front, everything OpenAI-compatible and self-hostable.

Squashed into a single commit; the development history stays local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA43ZJda8Rbp2hKXNy8f6b
2026-09-04 09:21:37 +02:00

159 lines
4.9 KiB
Python

"""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()