Replace every direct cross-app services.* call with HTTP-based communication: call_action() for writes, fetch_data() for reads. Each app now registers only its own domain service. Infrastructure: - shared/infrastructure/actions.py — POST client for /internal/actions/ - shared/infrastructure/data_client.py — GET client for /internal/data/ - shared/contracts/dtos.py — dto_to_dict/dto_from_dict serialization Action endpoints (writes): - events: 8 handlers (ticket adjust, claim/confirm, toggle, adopt) - market: 2 handlers (create/soft-delete marketplace) - cart: 1 handler (adopt cart for user) Data endpoints (reads): - blog: 4 (post-by-slug/id, posts-by-ids, search-posts) - events: 10 (pending entries/tickets, entries/tickets for page/order, entry-ids, associated-entries, calendars, visible-entries-for-period) - market: 1 (marketplaces-for-container) - cart: 1 (cart-summary) Service registration cleanup: - blog→blog+federation, events→calendar+federation, market→market+federation, cart→cart only, federation→federation only, account→nothing - Stubs reduced to minimal StubFederationService Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
184 lines
6.8 KiB
Python
184 lines
6.8 KiB
Python
"""Events app fragment endpoints.
|
|
|
|
Exposes HTML fragments at ``/internal/fragments/<type>`` for consumption
|
|
by other coop apps via the fragment client.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from quart import Blueprint, Response, g, render_template, request
|
|
|
|
from shared.infrastructure.fragments import FRAGMENT_HEADER
|
|
from shared.infrastructure.data_client import fetch_data
|
|
from shared.contracts.dtos import PostDTO, dto_from_dict
|
|
from shared.services.registry import services
|
|
|
|
|
|
def register():
|
|
bp = Blueprint("fragments", __name__, url_prefix="/internal/fragments")
|
|
|
|
_handlers: dict[str, object] = {}
|
|
|
|
@bp.before_request
|
|
async def _require_fragment_header():
|
|
if not request.headers.get(FRAGMENT_HEADER):
|
|
return Response("", status=403)
|
|
|
|
@bp.get("/<fragment_type>")
|
|
async def get_fragment(fragment_type: str):
|
|
handler = _handlers.get(fragment_type)
|
|
if handler is None:
|
|
return Response("", status=200, content_type="text/html")
|
|
html = await handler()
|
|
return Response(html, status=200, content_type="text/html")
|
|
|
|
# --- container-nav fragment: calendar entries + calendar links -----------
|
|
|
|
async def _container_nav_handler():
|
|
container_type = request.args.get("container_type", "page")
|
|
container_id = int(request.args.get("container_id", 0))
|
|
post_slug = request.args.get("post_slug", "")
|
|
paginate_url_base = request.args.get("paginate_url", "")
|
|
page = int(request.args.get("page", 1))
|
|
exclude = request.args.get("exclude", "")
|
|
excludes = [e.strip() for e in exclude.split(",") if e.strip()]
|
|
|
|
html_parts = []
|
|
|
|
# Calendar entries nav
|
|
if not any(e.startswith("calendar") for e in excludes):
|
|
entries, has_more = await services.calendar.associated_entries(
|
|
g.s, container_type, container_id, page,
|
|
)
|
|
if entries:
|
|
html_parts.append(await render_template(
|
|
"fragments/container_nav_entries.html",
|
|
entries=entries, has_more=has_more,
|
|
page=page, post_slug=post_slug,
|
|
paginate_url_base=paginate_url_base,
|
|
))
|
|
|
|
# Calendar links nav
|
|
if not any(e.startswith("calendar") for e in excludes):
|
|
calendars = await services.calendar.calendars_for_container(
|
|
g.s, container_type, container_id,
|
|
)
|
|
if calendars:
|
|
html_parts.append(await render_template(
|
|
"fragments/container_nav_calendars.html",
|
|
calendars=calendars, post_slug=post_slug,
|
|
))
|
|
|
|
return "\n".join(html_parts)
|
|
|
|
_handlers["container-nav"] = _container_nav_handler
|
|
|
|
# --- container-cards fragment: entries for blog listing cards ------------
|
|
|
|
async def _container_cards_handler():
|
|
post_ids_raw = request.args.get("post_ids", "")
|
|
post_slugs_raw = request.args.get("post_slugs", "")
|
|
post_ids = [int(x) for x in post_ids_raw.split(",") if x.strip()]
|
|
post_slugs = [x.strip() for x in post_slugs_raw.split(",") if x.strip()]
|
|
if not post_ids:
|
|
return ""
|
|
|
|
# Build post_id -> slug mapping
|
|
slug_map = {}
|
|
for i, pid in enumerate(post_ids):
|
|
slug_map[pid] = post_slugs[i] if i < len(post_slugs) else ""
|
|
|
|
batch = await services.calendar.confirmed_entries_for_posts(g.s, post_ids)
|
|
return await render_template(
|
|
"fragments/container_cards_entries.html",
|
|
batch=batch, post_ids=post_ids, slug_map=slug_map,
|
|
)
|
|
|
|
_handlers["container-cards"] = _container_cards_handler
|
|
|
|
# --- account-nav-item fragment: tickets + bookings links for account nav -
|
|
|
|
async def _account_nav_item_handler():
|
|
return await render_template("fragments/account_nav_items.html")
|
|
|
|
_handlers["account-nav-item"] = _account_nav_item_handler
|
|
|
|
# --- account-page fragment: tickets or bookings panel --------------------
|
|
|
|
async def _account_page_handler():
|
|
slug = request.args.get("slug", "")
|
|
user_id = request.args.get("user_id", type=int)
|
|
if not user_id:
|
|
return ""
|
|
|
|
if slug == "tickets":
|
|
tickets = await services.calendar.user_tickets(g.s, user_id=user_id)
|
|
return await render_template(
|
|
"fragments/account_page_tickets.html",
|
|
tickets=tickets,
|
|
)
|
|
elif slug == "bookings":
|
|
bookings = await services.calendar.user_bookings(g.s, user_id=user_id)
|
|
return await render_template(
|
|
"fragments/account_page_bookings.html",
|
|
bookings=bookings,
|
|
)
|
|
return ""
|
|
|
|
_handlers["account-page"] = _account_page_handler
|
|
|
|
# --- link-card fragment: event page preview card ----------------------------
|
|
|
|
async def _link_card_handler():
|
|
from shared.infrastructure.urls import events_url
|
|
|
|
slug = request.args.get("slug", "")
|
|
keys_raw = request.args.get("keys", "")
|
|
|
|
# Batch mode
|
|
if keys_raw:
|
|
slugs = [k.strip() for k in keys_raw.split(",") if k.strip()]
|
|
parts = []
|
|
for s in slugs:
|
|
parts.append(f"<!-- fragment:{s} -->")
|
|
raw = await fetch_data("blog", "post-by-slug", params={"slug": s}, required=False)
|
|
post = dto_from_dict(PostDTO, raw) if raw else None
|
|
if post:
|
|
calendars = await services.calendar.calendars_for_container(
|
|
g.s, "page", post.id,
|
|
)
|
|
cal_names = ", ".join(c.name for c in calendars) if calendars else ""
|
|
parts.append(await render_template(
|
|
"fragments/link_card.html",
|
|
title=post.title,
|
|
feature_image=post.feature_image,
|
|
calendar_names=cal_names,
|
|
link=events_url(f"/{post.slug}"),
|
|
))
|
|
return "\n".join(parts)
|
|
|
|
# Single mode
|
|
if not slug:
|
|
return ""
|
|
raw = await fetch_data("blog", "post-by-slug", params={"slug": slug}, required=False)
|
|
post = dto_from_dict(PostDTO, raw) if raw else None
|
|
if not post:
|
|
return ""
|
|
calendars = await services.calendar.calendars_for_container(
|
|
g.s, "page", post.id,
|
|
)
|
|
cal_names = ", ".join(c.name for c in calendars) if calendars else ""
|
|
return await render_template(
|
|
"fragments/link_card.html",
|
|
title=post.title,
|
|
feature_image=post.feature_image,
|
|
calendar_names=cal_names,
|
|
link=events_url(f"/{post.slug}"),
|
|
)
|
|
|
|
_handlers["link-card"] = _link_card_handler
|
|
|
|
bp._fragment_handlers = _handlers
|
|
|
|
return bp
|