From 8f8bc4fad9c5ce1167427f183f119011a69eadc5 Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 26 Feb 2026 18:05:30 +0000 Subject: [PATCH] =?UTF-8?q?Move=20entry=5Fassociations=20to=20shared=20?= =?UTF-8?q?=E2=80=94=20fix=20events=20cross-app=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit entry_associations only uses HTTP fetch_data/call_action, no direct DB. Events app imported it via ..post.services which doesn't exist in events. Co-Authored-By: Claude Opus 4.6 --- blog/bp/post/services/entry_associations.py | 77 ++------------------- events/bp/calendar/routes.py | 2 +- events/bp/calendar_entry/routes.py | 2 +- events/bp/calendars/routes.py | 2 +- shared/services/entry_associations.py | 75 ++++++++++++++++++++ 5 files changed, 84 insertions(+), 74 deletions(-) create mode 100644 shared/services/entry_associations.py diff --git a/blog/bp/post/services/entry_associations.py b/blog/bp/post/services/entry_associations.py index f11103c..7a8c35f 100644 --- a/blog/bp/post/services/entry_associations.py +++ b/blog/bp/post/services/entry_associations.py @@ -1,71 +1,6 @@ -from __future__ import annotations - -from sqlalchemy.ext.asyncio import AsyncSession - -from shared.infrastructure.actions import call_action, ActionError -from shared.infrastructure.data_client import fetch_data -from shared.contracts.dtos import CalendarEntryDTO, dto_from_dict -from shared.services.registry import services - - -async def toggle_entry_association( - session: AsyncSession, - post_id: int, - entry_id: int -) -> tuple[bool, str | None]: - """ - Toggle association between a post and calendar entry. - Returns (is_now_associated, error_message). - """ - post = await services.blog.get_post_by_id(session, post_id) - if not post: - return False, "Post not found" - - try: - result = await call_action("events", "toggle-entry-post", payload={ - "entry_id": entry_id, "content_type": "post", "content_id": post_id, - }) - return result.get("is_associated", False), None - except ActionError as e: - return False, str(e) - - -async def get_post_entry_ids( - session: AsyncSession, - post_id: int -) -> set[int]: - """ - Get all entry IDs associated with this post. - Returns a set of entry IDs. - """ - raw = await fetch_data("events", "entry-ids-for-content", - params={"content_type": "post", "content_id": post_id}, - required=False) or [] - return set(raw) - - -async def get_associated_entries( - session: AsyncSession, - post_id: int, - page: int = 1, - per_page: int = 10 -) -> dict: - """ - Get paginated associated entries for this post. - Returns dict with entries (CalendarEntryDTOs), total_count, and has_more. - """ - raw = await fetch_data("events", "associated-entries", - params={"content_type": "post", "content_id": post_id, "page": page}, - required=False) or {"entries": [], "has_more": False} - entries = [dto_from_dict(CalendarEntryDTO, e) for e in raw.get("entries", [])] - has_more = raw.get("has_more", False) - total_count = len(entries) + (page - 1) * per_page - if has_more: - total_count += 1 - - return { - "entries": entries, - "total_count": total_count, - "has_more": has_more, - "page": page, - } +# Re-export from shared — canonical implementation lives there. +from shared.services.entry_associations import ( # noqa: F401 + toggle_entry_association, + get_post_entry_ids, + get_associated_entries, +) diff --git a/events/bp/calendar/routes.py b/events/bp/calendar/routes.py index 4bd544f..a07ff69 100644 --- a/events/bp/calendar/routes.py +++ b/events/bp/calendar/routes.py @@ -224,7 +224,7 @@ def register(): html = await render_template("_types/calendars/index.html") if post_data: - from ..post.services.entry_associations import get_associated_entries + from shared.services.entry_associations import get_associated_entries post_id = (post_data.get("post") or {}).get("id") cals = ( diff --git a/events/bp/calendar_entry/routes.py b/events/bp/calendar_entry/routes.py index ab46095..9400120 100644 --- a/events/bp/calendar_entry/routes.py +++ b/events/bp/calendar_entry/routes.py @@ -136,7 +136,7 @@ def register(): nav_oobs = [] for post in entry_posts: # Get associated entries for this post - from ..post.services.entry_associations import get_associated_entries + from shared.services.entry_associations import get_associated_entries associated_entries = await get_associated_entries(g.s, post.id) # Load calendars for this post diff --git a/events/bp/calendars/routes.py b/events/bp/calendars/routes.py index ebae1f7..f723811 100644 --- a/events/bp/calendars/routes.py +++ b/events/bp/calendars/routes.py @@ -74,7 +74,7 @@ def register(): # Blog-embedded mode: also update post nav if post_data: - from ..post.services.entry_associations import get_associated_entries + from shared.services.entry_associations import get_associated_entries cals = ( await g.s.execute( diff --git a/shared/services/entry_associations.py b/shared/services/entry_associations.py new file mode 100644 index 0000000..3052fc7 --- /dev/null +++ b/shared/services/entry_associations.py @@ -0,0 +1,75 @@ +"""Entry association helpers — shared across blog and events apps. + +Only uses HTTP-based fetch_data/call_action, no direct DB access. +""" +from __future__ import annotations + +from sqlalchemy.ext.asyncio import AsyncSession + +from shared.infrastructure.actions import call_action, ActionError +from shared.infrastructure.data_client import fetch_data +from shared.contracts.dtos import CalendarEntryDTO, dto_from_dict +from shared.services.registry import services + + +async def toggle_entry_association( + session: AsyncSession, + post_id: int, + entry_id: int +) -> tuple[bool, str | None]: + """ + Toggle association between a post and calendar entry. + Returns (is_now_associated, error_message). + """ + post = await services.blog.get_post_by_id(session, post_id) + if not post: + return False, "Post not found" + + try: + result = await call_action("events", "toggle-entry-post", payload={ + "entry_id": entry_id, "content_type": "post", "content_id": post_id, + }) + return result.get("is_associated", False), None + except ActionError as e: + return False, str(e) + + +async def get_post_entry_ids( + session: AsyncSession, + post_id: int +) -> set[int]: + """ + Get all entry IDs associated with this post. + Returns a set of entry IDs. + """ + raw = await fetch_data("events", "entry-ids-for-content", + params={"content_type": "post", "content_id": post_id}, + required=False) or [] + return set(raw) + + +async def get_associated_entries( + session: AsyncSession, + post_id: int, + page: int = 1, + per_page: int = 10 +) -> dict: + """ + Get paginated associated entries for this post. + Returns dict with entries (CalendarEntryDTOs), total_count, and has_more. + """ + raw = await fetch_data("events", "associated-entries", + params={"content_type": "post", "content_id": post_id, "page": page}, + required=False) or {"entries": [], "has_more": False} + entries = [dto_from_dict(CalendarEntryDTO, e) for e in raw.get("entries", [])] + has_more = raw.get("has_more", False) + total_count = len(entries) + (page - 1) * per_page + if has_more: + total_count += 1 + + return { + "entries": entries, + "total_count": total_count, + "has_more": has_more, + "page": page, + }