feat(family): an idea links to the rule topic holding its norms - set_family_topic, the readouts name the rules, the idea row shows the link (milestone 463 step 1 gap, found in step 7, #4993)
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 1m9s
CI & Build / integration (push) Successful in 1m21s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 1m29s
CI & Build / Plugin hooks (push) Successful in 15s
CI & Build / Python lint (push) Successful in 2s
CI & Build / TypeScript typecheck (push) Successful in 1m9s
CI & Build / integration (push) Successful in 1m21s
CI & Build / Python tests (push) Successful in 2m2s
CI & Build / Build & push image (push) Successful in 1m29s
Step 1 added family_ideas.topic_id but nothing could set it, so the note<->topic link the milestone promised existed only as a column. set_family_topic links or unlinks (0) a topic in a rulebook the caller owns, one idea per topic; the version does not move. get_family_idea and get_family_adoption list the topic's rules; list_family_ideas and the Family page name the topic. The model docstrings no longer claim the topic's rules are platform-scoped in retrieval. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -52,7 +52,9 @@ from scribe.models.family import (
|
||||
)
|
||||
from scribe.models.note import Note
|
||||
from scribe.models.project import Project
|
||||
from scribe.models.rulebook import Rule
|
||||
from scribe.services import access
|
||||
from scribe.services import rulebooks as rulebooks_svc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -140,6 +142,26 @@ async def _platform_slugs(session, note_id: int) -> list[str]:
|
||||
return list(rows.scalars().all())
|
||||
|
||||
|
||||
async def _topic(session, user_id: int, topic_id: int | None, *, rules: bool) -> dict | None:
|
||||
"""The rule topic holding an idea's binding norms, for a reader. Rulebooks
|
||||
belong to their owner, so a topic the reader does not own reads as no
|
||||
topic."""
|
||||
if not topic_id:
|
||||
return None
|
||||
topic = await rulebooks_svc.get_topic(topic_id, user_id)
|
||||
if topic is None:
|
||||
return None
|
||||
out = {"id": topic.id, "title": topic.title, "rulebook_id": topic.rulebook_id}
|
||||
if rules:
|
||||
out["rules"] = [
|
||||
{"id": r.id, "title": r.title} for r in (await session.execute(
|
||||
select(Rule).where(Rule.topic_id == topic.id, Rule.deleted_at.is_(None))
|
||||
.order_by(Rule.order_index.asc(), Rule.id.asc())
|
||||
)).scalars().all()
|
||||
]
|
||||
return out
|
||||
|
||||
|
||||
async def _snapshot(session, idea: FamilyIdea | None) -> dict | None:
|
||||
"""The idea's state, as a decision's before/after. Slugs, never ids: an id
|
||||
inside JSON cannot be remapped by a restore (the #3182 trap)."""
|
||||
@@ -187,6 +209,7 @@ async def get_idea(user_id: int, note_id: int) -> dict | None:
|
||||
"is_task": note.is_task,
|
||||
"project_id": note.project_id,
|
||||
"platforms": await _platform_slugs(session, note_id),
|
||||
"topic": await _topic(session, user_id, idea.topic_id, rules=True),
|
||||
"adoptions": counts,
|
||||
"decisions": [_decision_dict(d) for d in decisions],
|
||||
"undoable_decision_id": _undoable_id(decisions),
|
||||
@@ -226,6 +249,7 @@ async def list_ideas(
|
||||
"is_task": note_status is not None,
|
||||
"project_id": project_id,
|
||||
"platforms": await _platform_slugs(session, idea.note_id),
|
||||
"topic": await _topic(session, user_id, idea.topic_id, rules=False),
|
||||
})
|
||||
out.append(d)
|
||||
return out
|
||||
@@ -677,6 +701,42 @@ async def revise(
|
||||
"ledger_rows_opened": opened, "ledger_rows_closed": closed}
|
||||
|
||||
|
||||
async def set_topic(user_id: int, note_id: int, topic_id: int) -> dict:
|
||||
"""Link an idea to the rule topic holding its binding norms, or unlink it
|
||||
with topic_id 0. The note carries the idea; the topic carries the rules a
|
||||
project on its platforms must follow. One idea per topic, since a topic's
|
||||
norms belong to one standard.
|
||||
|
||||
Not a change of substance, so the version does not move: the norms were
|
||||
rules before the link and are the same rules after it. Re-linking is the
|
||||
undo, as with the references."""
|
||||
if not await access.can_write_note(user_id, note_id):
|
||||
raise ValueError(f"note {note_id} not found or no write access")
|
||||
async with async_session() as session:
|
||||
idea = await session.get(FamilyIdea, note_id)
|
||||
if idea is None:
|
||||
raise ValueError(f"#{note_id} is not a family idea")
|
||||
if topic_id:
|
||||
if await rulebooks_svc.get_topic(topic_id, user_id) is None:
|
||||
raise ValueError(f"topic {topic_id} not found in a rulebook you own")
|
||||
holder = (await session.execute(
|
||||
select(FamilyIdea.note_id, Note.title)
|
||||
.join(Note, Note.id == FamilyIdea.note_id)
|
||||
.where(FamilyIdea.topic_id == topic_id, FamilyIdea.note_id != note_id)
|
||||
)).first()
|
||||
if holder is not None:
|
||||
raise ValueError(
|
||||
f"topic {topic_id} already carries the norms of #{holder[0]} "
|
||||
f"“{holder[1]}” — unlink it there first, or merge the two ideas"
|
||||
)
|
||||
idea.topic_id = topic_id or None
|
||||
idea.updated_at = _now()
|
||||
await session.commit()
|
||||
out = idea.to_dict()
|
||||
out["topic"] = await _topic(session, user_id, idea.topic_id, rules=True)
|
||||
return out
|
||||
|
||||
|
||||
async def _existing_decision_ids(ids: list[int]) -> list[int]:
|
||||
ids = [int(i) for i in ids if i]
|
||||
if not ids:
|
||||
|
||||
Reference in New Issue
Block a user