Drafter reuse-recall (both halves) + the multi-user ACL work it exposed #78
@@ -209,16 +209,28 @@ def notes_visibility_clause(user_id: int, scope: str = "own"):
|
|||||||
|
|
||||||
|
|
||||||
async def owner_names_for(user_ids: set[int]) -> dict[int, str]:
|
async def owner_names_for(user_ids: set[int]) -> dict[int, str]:
|
||||||
"""{user_id: username} in one query. Empty input costs nothing."""
|
"""{user_id: username} in one query. Empty input costs nothing.
|
||||||
|
|
||||||
|
Fails soft: on a lookup error the caller gets no names and renders "another
|
||||||
|
user" instead. Losing an attribution is a cosmetic downgrade, and the part
|
||||||
|
that matters — that the record ISN'T the caller's — comes from comparing
|
||||||
|
owner ids, not from this. Failing the whole search over a username would be
|
||||||
|
the worse trade.
|
||||||
|
"""
|
||||||
if not user_ids:
|
if not user_ids:
|
||||||
return {}
|
return {}
|
||||||
async with async_session() as session:
|
try:
|
||||||
rows = (
|
async with async_session() as session:
|
||||||
await session.execute(
|
rows = (
|
||||||
select(User.id, User.username).where(User.id.in_(user_ids))
|
await session.execute(
|
||||||
)
|
select(User.id, User.username).where(User.id.in_(user_ids))
|
||||||
).all()
|
)
|
||||||
return {uid: name for uid, name in rows}
|
).all()
|
||||||
|
return {uid: name for uid, name in rows}
|
||||||
|
except Exception:
|
||||||
|
logger.warning("Owner-name lookup failed; falling back to unnamed "
|
||||||
|
"attribution", exc_info=True)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
def _my_group_ids(user_id: int):
|
def _my_group_ids(user_id: int):
|
||||||
|
|||||||
@@ -18,13 +18,18 @@ def _reset_user_ctx():
|
|||||||
|
|
||||||
|
|
||||||
def _fake_note(*, id: int, title: str, body: str = "",
|
def _fake_note(*, id: int, title: str, body: str = "",
|
||||||
tags: list[str] | None = None, is_task: bool = False) -> MagicMock:
|
tags: list[str] | None = None, is_task: bool = False,
|
||||||
|
user_id: int = 7) -> MagicMock:
|
||||||
note = MagicMock()
|
note = MagicMock()
|
||||||
note.id = id
|
note.id = id
|
||||||
note.title = title
|
note.title = title
|
||||||
note.body = body
|
note.body = body
|
||||||
note.tags = tags or []
|
note.tags = tags or []
|
||||||
note.is_task = is_task
|
note.is_task = is_task
|
||||||
|
# A real int, matching the bound caller by default: results compare it to
|
||||||
|
# decide whether to attach a shared/owner marker, and an auto-MagicMock would
|
||||||
|
# read as "someone else's" and send the tool looking up a username.
|
||||||
|
note.user_id = user_id
|
||||||
return note
|
return note
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,13 @@ def _rule(rid, title, topic_id):
|
|||||||
return r
|
return r
|
||||||
|
|
||||||
|
|
||||||
def _note(nid, title):
|
def _note(nid, title, user_id=1):
|
||||||
n = MagicMock()
|
n = MagicMock()
|
||||||
n.id, n.title = nid, title
|
n.id, n.title = nid, title
|
||||||
|
# Real int, defaulting to the caller used in these tests: the injected menu
|
||||||
|
# compares it to decide whether the line needs a "shared by …" attribution,
|
||||||
|
# and an auto-MagicMock would read as another user's note.
|
||||||
|
n.user_id = user_id
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user