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
This commit is contained in:
co-authored by
Claude Opus 5
parent
68d3a43191
commit
784b76baf7
@@ -0,0 +1,158 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user