diff --git a/app.py b/app.py index cad4ad8..4572c9e 100644 --- a/app.py +++ b/app.py @@ -5,7 +5,6 @@ from pathlib import Path from quart import g, abort from jinja2 import FileSystemLoader, ChoiceLoader -from sqlalchemy import select from shared.infrastructure.factory import create_base_app @@ -17,34 +16,36 @@ async def events_context() -> dict: Events app context processor. - menu_items: direct DB query via glue layer - - cart_count/cart_total: fetched from cart internal API + - cart_count/cart_total: via cart service (shared DB) """ from shared.infrastructure.context import base_context - from glue.services.navigation import get_navigation_tree - from shared.infrastructure.internal_api import get as api_get + from shared.services.navigation import get_navigation_tree + from shared.services.registry import services + from shared.infrastructure.cart_identity import current_cart_identity ctx = await base_context() ctx["menu_items"] = await get_navigation_tree(g.s) - # Cart data from cart API (includes both product + calendar counts) - cart_data = await api_get("cart", "/internal/cart/summary", forward_session=True) - if cart_data: - ctx["cart_count"] = cart_data.get("count", 0) + cart_data.get("calendar_count", 0) - ctx["cart_total"] = cart_data.get("total", 0) + cart_data.get("calendar_total", 0) - else: - ctx["cart_count"] = 0 - ctx["cart_total"] = 0 + # Cart data via service (replaces cross-app HTTP API) + ident = current_cart_identity() + summary = await services.cart.cart_summary( + g.s, user_id=ident["user_id"], session_id=ident["session_id"], + ) + ctx["cart_count"] = summary.count + summary.calendar_count + ctx["cart_total"] = float(summary.total + summary.calendar_total) return ctx def create_app() -> "Quart": - from shared.models.ghost_content import Post - from models.calendars import Calendar - from shared.models.market_place import MarketPlace + from services import register_domain_services - app = create_base_app("events", context_fn=events_context) + app = create_base_app( + "events", + context_fn=events_context, + domain_services_fn=register_domain_services, + ) # App-specific templates override shared templates app_templates = str(Path(__file__).resolve().parent / "templates") @@ -90,11 +91,7 @@ def create_app() -> "Quart": slug = getattr(g, "post_slug", None) if not slug: return - post = ( - await g.s.execute( - select(Post).where(Post.slug == slug) - ) - ).scalar_one_or_none() + post = await services.blog.get_post_by_slug(g.s, slug) if not post: abort(404) g.post_data = { @@ -114,20 +111,8 @@ def create_app() -> "Quart": if not post_data: return {} post_id = post_data["post"]["id"] - calendars = ( - await g.s.execute( - select(Calendar) - .where(Calendar.container_type == "page", Calendar.container_id == post_id, Calendar.deleted_at.is_(None)) - .order_by(Calendar.name.asc()) - ) - ).scalars().all() - markets = ( - await g.s.execute( - select(MarketPlace) - .where(MarketPlace.container_type == "page", MarketPlace.container_id == post_id, MarketPlace.deleted_at.is_(None)) - .order_by(MarketPlace.name.asc()) - ) - ).scalars().all() + calendars = await services.calendar.calendars_for_container(g.s, "page", post_id) + markets = await services.market.marketplaces_for_container(g.s, "page", post_id) return { **post_data, "calendars": calendars, diff --git a/bp/calendar_entry/services/post_associations.py b/bp/calendar_entry/services/post_associations.py index 8796b73..d96cf7d 100644 --- a/bp/calendar_entry/services/post_associations.py +++ b/bp/calendar_entry/services/post_associations.py @@ -5,7 +5,7 @@ from sqlalchemy import select from sqlalchemy.sql import func from models.calendars import CalendarEntry, CalendarEntryPost -from shared.models.ghost_content import Post +from shared.services.registry import services async def add_post_to_entry( @@ -28,9 +28,7 @@ async def add_post_to_entry( return False, "Calendar entry not found" # Check if post exists - post = await session.scalar( - select(Post).where(Post.id == post_id) - ) + post = await services.blog.get_post_by_id(session, post_id) if not post: return False, "Post not found" @@ -91,20 +89,22 @@ async def remove_post_from_entry( async def get_entry_posts( session: AsyncSession, entry_id: int -) -> list[Post]: +) -> list: """ - Get all posts associated with a calendar entry. + Get all posts (as PostDTOs) associated with a calendar entry. """ result = await session.execute( - select(Post) - .join(CalendarEntryPost, (CalendarEntryPost.content_id == Post.id) & (CalendarEntryPost.content_type == "post")) - .where( + select(CalendarEntryPost.content_id).where( CalendarEntryPost.entry_id == entry_id, - CalendarEntryPost.deleted_at.is_(None) + CalendarEntryPost.content_type == "post", + CalendarEntryPost.deleted_at.is_(None), ) - .order_by(Post.title) ) - return list(result.scalars().all()) + post_ids = list(result.scalars().all()) + if not post_ids: + return [] + posts = await services.blog.get_posts_by_ids(session, post_ids) + return sorted(posts, key=lambda p: (p.title or "")) async def search_posts( @@ -112,29 +112,10 @@ async def search_posts( query: str, page: int = 1, per_page: int = 10 -) -> tuple[list[Post], int]: +) -> tuple[list, int]: """ Search for posts by title with pagination. If query is empty, returns all posts in published order. - Returns (posts, total_count). + Returns (post_dtos, total_count). """ - # Build base query - if query: - # Search by title - count_stmt = select(func.count(Post.id)).where(Post.title.ilike(f"%{query}%")) - posts_stmt = select(Post).where(Post.title.ilike(f"%{query}%")).order_by(Post.title) - else: - # All posts in published order (newest first) - count_stmt = select(func.count(Post.id)) - posts_stmt = select(Post).order_by(Post.published_at.desc().nullslast()) - - # Count total - count_result = await session.execute(count_stmt) - total = count_result.scalar() or 0 - - # Get paginated results - offset = (page - 1) * per_page - result = await session.execute( - posts_stmt.limit(per_page).offset(offset) - ) - return list(result.scalars().all()), total + return await services.blog.search_posts(session, query, page, per_page) diff --git a/bp/calendars/services/calendars.py b/bp/calendars/services/calendars.py index 576be72..2e8a94b 100644 --- a/bp/calendars/services/calendars.py +++ b/bp/calendars/services/calendars.py @@ -4,8 +4,8 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from models.calendars import Calendar -from shared.models.ghost_content import Post # for FK existence checks -from glue.services.relationships import attach_child, detach_child +from shared.services.registry import services +from shared.services.relationships import attach_child, detach_child import unicodedata import re @@ -49,13 +49,15 @@ def slugify(value: str, max_len: int = 255) -> str: async def soft_delete(sess: AsyncSession, post_slug: str, calendar_slug: str) -> bool: + post = await services.blog.get_post_by_slug(sess, post_slug) + if not post: + return False + cal = ( await sess.execute( - select(Calendar) - .join(Post, Calendar.container_id == Post.id) - .where(Calendar.container_type == "page") - .where( - Post.slug == post_slug, + select(Calendar).where( + Calendar.container_type == "page", + Calendar.container_id == post.id, Calendar.slug == calendar_slug, Calendar.deleted_at.is_(None), ) @@ -82,7 +84,7 @@ async def create_calendar(sess: AsyncSession, post_id: int, name: str) -> Calend slug=slugify(name) # Ensure post exists (avoid silent FK errors in some DBs) - post = (await sess.execute(select(Post).where(Post.id == post_id))).scalar_one_or_none() + post = await services.blog.get_post_by_id(sess, post_id) if not post: raise CalendarError(f"Post {post_id} does not exist.") diff --git a/bp/markets/routes.py b/bp/markets/routes.py index 3ce15ad..bac523f 100644 --- a/bp/markets/routes.py +++ b/bp/markets/routes.py @@ -3,9 +3,6 @@ from __future__ import annotations from quart import ( request, render_template, make_response, Blueprint, g ) -from sqlalchemy import select - -from shared.models.market_place import MarketPlace from .services.markets import ( create_market as svc_create_market, diff --git a/bp/markets/services/markets.py b/bp/markets/services/markets.py index 24efa78..950baa0 100644 --- a/bp/markets/services/markets.py +++ b/bp/markets/services/markets.py @@ -7,9 +7,9 @@ from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from shared.models.market_place import MarketPlace -from shared.models.ghost_content import Post from shared.browser.app.utils import utcnow -from glue.services.relationships import attach_child, detach_child +from shared.services.registry import services +from shared.services.relationships import attach_child, detach_child class MarketError(ValueError): @@ -40,7 +40,7 @@ async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPl raise MarketError("Market name must not be empty.") slug = slugify(name) - post = (await sess.execute(select(Post).where(Post.id == post_id))).scalar_one_or_none() + post = await services.blog.get_post_by_id(sess, post_id) if not post: raise MarketError(f"Post {post_id} does not exist.") if not post.is_page: @@ -68,13 +68,15 @@ async def create_market(sess: AsyncSession, post_id: int, name: str) -> MarketPl async def soft_delete(sess: AsyncSession, post_slug: str, market_slug: str) -> bool: + post = await services.blog.get_post_by_slug(sess, post_slug) + if not post: + return False + market = ( await sess.execute( - select(MarketPlace) - .join(Post, MarketPlace.container_id == Post.id) - .where(MarketPlace.container_type == "page") - .where( - Post.slug == post_slug, + select(MarketPlace).where( + MarketPlace.container_type == "page", + MarketPlace.container_id == post.id, MarketPlace.slug == market_slug, MarketPlace.deleted_at.is_(None), ) diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..0de5e8d --- /dev/null +++ b/services/__init__.py @@ -0,0 +1,26 @@ +"""Events app service registration.""" +from __future__ import annotations + + +def register_domain_services() -> None: + """Register services for the events app. + + Events owns: Calendar, CalendarEntry, CalendarSlot, TicketType, + Ticket, CalendarEntryPost. + Standard deployment registers all 4 services as real DB impls + (shared DB). For composable deployments, swap non-owned services + with stubs from shared.services.stubs. + """ + from shared.services.registry import services + from shared.services.blog_impl import SqlBlogService + from shared.services.calendar_impl import SqlCalendarService + from shared.services.market_impl import SqlMarketService + from shared.services.cart_impl import SqlCartService + + services.calendar = SqlCalendarService() + if not services.has("blog"): + services.blog = SqlBlogService() + if not services.has("market"): + services.market = SqlMarketService() + if not services.has("cart"): + services.cart = SqlCartService() diff --git a/shared b/shared index ea7dc97..70b1c7d 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit ea7dc9723a8f86b82b42ee9631fca096d0ec11bb +Subproject commit 70b1c7de1007f1759a20a5d17eaab83f96d26b72