Full-CRUD sweep: update endpoints for tree, source, citation, relationship, media

Closes the rule #8 gap at the API layer: PATCH endpoints + service updates for Tree (name/description/visibility), Source, Citation (page/detail/confidence), Relationship (qualifier/notes), and Media (title/attachment) — editor-gated and audited. Every core entity now has create/read/update/delete. Edit UIs for these land in the frontend batch. 37 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Justin Paul <justin@jpaul.me>
This commit is contained in:
2026-06-07 09:53:17 -04:00
parent 26df03cfd7
commit cf5518c7ec
15 changed files with 366 additions and 5 deletions
@@ -107,6 +107,44 @@ async def list_relationships_for_person(
return list((await session.execute(stmt)).scalars().all())
async def update_relationship(
session: AsyncSession, *, actor: User, tree: Tree, relationship_id: uuid.UUID, changes: dict
) -> Relationship:
if not await privacy.can_edit_tree(session, user_id=actor.id, tree=tree):
raise Forbidden("not an editor of this tree")
relationship = (
await session.execute(
select(Relationship).where(
Relationship.id == relationship_id,
Relationship.tree_id == tree.id,
Relationship.deleted_at.is_(None),
)
)
).scalar_one_or_none()
if relationship is None:
raise NotFound("relationship not found")
if (
"qualifier" in changes
and changes["qualifier"] is not None
and relationship.type is not RelationshipType.parent_child
):
raise Conflict("qualifier only applies to parent_child relationships")
for key in {"qualifier", "notes"} & changes.keys():
setattr(relationship, key, changes[key])
record_audit(
session,
action="update",
entity_type="Relationship",
entity_id=relationship.id,
tree_id=tree.id,
actor_user_id=actor.id,
after=changes,
)
await session.commit()
await session.refresh(relationship)
return relationship
async def delete_relationship(
session: AsyncSession, *, actor: User, tree: Tree, relationship_id: uuid.UUID
) -> None: