Decouple all cross-app service calls to HTTP endpoints
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>
This commit is contained in:
@@ -8,7 +8,7 @@ from jinja2 import FileSystemLoader, ChoiceLoader
|
||||
|
||||
from shared.infrastructure.factory import create_base_app
|
||||
|
||||
from bp import register_all_events, register_calendars, register_markets, register_payments, register_page, register_fragments
|
||||
from bp import register_all_events, register_calendars, register_markets, register_payments, register_page, register_fragments, register_actions, register_data
|
||||
|
||||
|
||||
async def events_context() -> dict:
|
||||
@@ -20,20 +20,25 @@ async def events_context() -> dict:
|
||||
"""
|
||||
from shared.infrastructure.context import base_context
|
||||
from shared.services.navigation import get_navigation_tree
|
||||
from shared.services.registry import services
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from shared.infrastructure.fragments import fetch_fragments
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||
|
||||
ctx = await base_context()
|
||||
|
||||
# Fallback for _nav.html when nav-tree fragment fetch fails
|
||||
ctx["menu_items"] = await get_navigation_tree(g.s)
|
||||
|
||||
# Cart data via service (replaces cross-app HTTP API)
|
||||
# Cart data via internal data endpoint
|
||||
ident = current_cart_identity()
|
||||
summary = await services.cart.cart_summary(
|
||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
||||
)
|
||||
summary_params = {}
|
||||
if ident["user_id"] is not None:
|
||||
summary_params["user_id"] = ident["user_id"]
|
||||
if ident["session_id"] is not None:
|
||||
summary_params["session_id"] = ident["session_id"]
|
||||
raw = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||
summary = dto_from_dict(CartSummaryDTO, raw) if raw else CartSummaryDTO()
|
||||
ctx["cart_count"] = summary.count + summary.calendar_count + summary.ticket_count
|
||||
ctx["cart_total"] = float(summary.total + summary.calendar_total + summary.ticket_total)
|
||||
|
||||
@@ -58,7 +63,6 @@ async def events_context() -> dict:
|
||||
|
||||
|
||||
def create_app() -> "Quart":
|
||||
from shared.services.registry import services
|
||||
from services import register_domain_services
|
||||
|
||||
app = create_base_app(
|
||||
@@ -105,6 +109,8 @@ def create_app() -> "Quart":
|
||||
)
|
||||
|
||||
app.register_blueprint(register_fragments())
|
||||
app.register_blueprint(register_actions())
|
||||
app.register_blueprint(register_data())
|
||||
|
||||
# --- Auto-inject slug into url_for() calls ---
|
||||
@app.url_value_preprocessor
|
||||
@@ -122,31 +128,39 @@ def create_app() -> "Quart":
|
||||
# --- Load post data for slug ---
|
||||
@app.before_request
|
||||
async def hydrate_post():
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
slug = getattr(g, "post_slug", None)
|
||||
if not slug:
|
||||
return
|
||||
post = await services.blog.get_post_by_slug(g.s, slug)
|
||||
post = await fetch_data("blog", "post-by-slug", params={"slug": slug})
|
||||
if not post:
|
||||
abort(404)
|
||||
g.post_data = {
|
||||
"post": {
|
||||
"id": post.id,
|
||||
"title": post.title,
|
||||
"slug": post.slug,
|
||||
"feature_image": post.feature_image,
|
||||
"status": post.status,
|
||||
"visibility": post.visibility,
|
||||
"id": post["id"],
|
||||
"title": post["title"],
|
||||
"slug": post["slug"],
|
||||
"feature_image": post.get("feature_image"),
|
||||
"status": post["status"],
|
||||
"visibility": post["visibility"],
|
||||
},
|
||||
}
|
||||
|
||||
@app.context_processor
|
||||
async def inject_post():
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import CalendarDTO, MarketPlaceDTO, dto_from_dict
|
||||
from shared.services.registry import services
|
||||
post_data = getattr(g, "post_data", None)
|
||||
if not post_data:
|
||||
return {}
|
||||
post_id = post_data["post"]["id"]
|
||||
# Calendar data is local (events owns it)
|
||||
calendars = await services.calendar.calendars_for_container(g.s, "page", post_id)
|
||||
markets = await services.market.marketplaces_for_container(g.s, "page", post_id)
|
||||
# Market data is cross-app
|
||||
raw_markets = await fetch_data("market", "marketplaces-for-container",
|
||||
params={"type": "page", "id": post_id}, required=False) or []
|
||||
markets = [dto_from_dict(MarketPlaceDTO, m) for m in raw_markets]
|
||||
return {
|
||||
**post_data,
|
||||
"calendars": calendars,
|
||||
@@ -168,6 +182,7 @@ def create_app() -> "Quart":
|
||||
from quart import jsonify
|
||||
from shared.infrastructure.urls import events_url
|
||||
from shared.infrastructure.oembed import build_oembed_response
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
|
||||
url = request.args.get("url", "")
|
||||
if not url:
|
||||
@@ -178,15 +193,15 @@ def create_app() -> "Quart":
|
||||
if not slug:
|
||||
return jsonify({"error": "could not extract slug"}), 404
|
||||
|
||||
post = await services.blog.get_post_by_slug(g.s, slug)
|
||||
post = await fetch_data("blog", "post-by-slug", params={"slug": slug})
|
||||
if not post:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
|
||||
resp = build_oembed_response(
|
||||
title=post.title,
|
||||
title=post["title"],
|
||||
oembed_type="link",
|
||||
thumbnail_url=post.feature_image,
|
||||
url=events_url(f"/{post.slug}"),
|
||||
thumbnail_url=post.get("feature_image"),
|
||||
url=events_url(f"/{post['slug']}"),
|
||||
)
|
||||
return jsonify(resp)
|
||||
|
||||
|
||||
@@ -4,3 +4,5 @@ from .markets.routes import register as register_markets
|
||||
from .payments.routes import register as register_payments
|
||||
from .page.routes import register as register_page
|
||||
from .fragments import register_fragments
|
||||
from .actions import register_actions
|
||||
from .data import register_data
|
||||
|
||||
1
events/bp/actions/__init__.py
Normal file
1
events/bp/actions/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .routes import register as register_actions
|
||||
131
events/bp/actions/routes.py
Normal file
131
events/bp/actions/routes.py
Normal file
@@ -0,0 +1,131 @@
|
||||
"""Events app action endpoints.
|
||||
|
||||
Exposes write operations at ``/internal/actions/<action_name>`` for
|
||||
cross-app callers (cart, blog) via the internal action client.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import Blueprint, g, jsonify, request
|
||||
|
||||
from shared.infrastructure.actions import ACTION_HEADER
|
||||
from shared.services.registry import services
|
||||
|
||||
|
||||
def register() -> Blueprint:
|
||||
bp = Blueprint("actions", __name__, url_prefix="/internal/actions")
|
||||
|
||||
@bp.before_request
|
||||
async def _require_action_header():
|
||||
if not request.headers.get(ACTION_HEADER):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
|
||||
_handlers: dict[str, object] = {}
|
||||
|
||||
@bp.post("/<action_name>")
|
||||
async def handle_action(action_name: str):
|
||||
handler = _handlers.get(action_name)
|
||||
if handler is None:
|
||||
return jsonify({"error": "unknown action"}), 404
|
||||
result = await handler()
|
||||
return jsonify(result)
|
||||
|
||||
# --- adjust-ticket-quantity ---
|
||||
async def _adjust_ticket_quantity():
|
||||
data = await request.get_json()
|
||||
await services.calendar.adjust_ticket_quantity(
|
||||
g.s,
|
||||
data["entry_id"],
|
||||
data["count"],
|
||||
user_id=data.get("user_id"),
|
||||
session_id=data.get("session_id"),
|
||||
ticket_type_id=data.get("ticket_type_id"),
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
_handlers["adjust-ticket-quantity"] = _adjust_ticket_quantity
|
||||
|
||||
# --- claim-entries-for-order ---
|
||||
async def _claim_entries():
|
||||
data = await request.get_json()
|
||||
await services.calendar.claim_entries_for_order(
|
||||
g.s,
|
||||
data["order_id"],
|
||||
data.get("user_id"),
|
||||
data.get("session_id"),
|
||||
data.get("page_post_id"),
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
_handlers["claim-entries-for-order"] = _claim_entries
|
||||
|
||||
# --- claim-tickets-for-order ---
|
||||
async def _claim_tickets():
|
||||
data = await request.get_json()
|
||||
await services.calendar.claim_tickets_for_order(
|
||||
g.s,
|
||||
data["order_id"],
|
||||
data.get("user_id"),
|
||||
data.get("session_id"),
|
||||
data.get("page_post_id"),
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
_handlers["claim-tickets-for-order"] = _claim_tickets
|
||||
|
||||
# --- confirm-entries-for-order ---
|
||||
async def _confirm_entries():
|
||||
data = await request.get_json()
|
||||
await services.calendar.confirm_entries_for_order(
|
||||
g.s,
|
||||
data["order_id"],
|
||||
data.get("user_id"),
|
||||
data.get("session_id"),
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
_handlers["confirm-entries-for-order"] = _confirm_entries
|
||||
|
||||
# --- confirm-tickets-for-order ---
|
||||
async def _confirm_tickets():
|
||||
data = await request.get_json()
|
||||
await services.calendar.confirm_tickets_for_order(
|
||||
g.s, data["order_id"],
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
_handlers["confirm-tickets-for-order"] = _confirm_tickets
|
||||
|
||||
# --- toggle-entry-post ---
|
||||
async def _toggle_entry_post():
|
||||
data = await request.get_json()
|
||||
is_associated = await services.calendar.toggle_entry_post(
|
||||
g.s,
|
||||
data["entry_id"],
|
||||
data["content_type"],
|
||||
data["content_id"],
|
||||
)
|
||||
return {"is_associated": is_associated}
|
||||
|
||||
_handlers["toggle-entry-post"] = _toggle_entry_post
|
||||
|
||||
# --- adopt-entries-for-user ---
|
||||
async def _adopt_entries():
|
||||
data = await request.get_json()
|
||||
await services.calendar.adopt_entries_for_user(
|
||||
g.s, data["user_id"], data["session_id"],
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
_handlers["adopt-entries-for-user"] = _adopt_entries
|
||||
|
||||
# --- adopt-tickets-for-user ---
|
||||
async def _adopt_tickets():
|
||||
data = await request.get_json()
|
||||
await services.calendar.adopt_tickets_for_user(
|
||||
g.s, data["user_id"], data["session_id"],
|
||||
)
|
||||
return {"ok": True}
|
||||
|
||||
_handlers["adopt-tickets-for-user"] = _adopt_tickets
|
||||
|
||||
return bp
|
||||
@@ -15,6 +15,8 @@ from quart import Blueprint, g, request, render_template, render_template_string
|
||||
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import CartSummaryDTO, PostDTO, dto_from_dict
|
||||
from shared.services.registry import services
|
||||
|
||||
|
||||
@@ -47,8 +49,11 @@ def register() -> Blueprint:
|
||||
if e.calendar_container_type == "page" and e.calendar_container_id
|
||||
})
|
||||
if post_ids:
|
||||
posts = await services.blog.get_posts_by_ids(g.s, post_ids)
|
||||
for p in posts:
|
||||
raw_posts = await fetch_data("blog", "posts-by-ids",
|
||||
params={"ids": ",".join(str(i) for i in post_ids)},
|
||||
required=False) or []
|
||||
for raw_p in raw_posts:
|
||||
p = dto_from_dict(PostDTO, raw_p)
|
||||
page_info[p.id] = {"title": p.title, "slug": p.slug}
|
||||
|
||||
return entries, has_more, pending_tickets, page_info
|
||||
@@ -121,9 +126,13 @@ def register() -> Blueprint:
|
||||
entry = await services.calendar.entry_by_id(g.s, entry_id)
|
||||
|
||||
# Updated cart count for OOB mini-cart
|
||||
summary = await services.cart.cart_summary(
|
||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
||||
)
|
||||
summary_params = {}
|
||||
if ident["user_id"] is not None:
|
||||
summary_params["user_id"] = ident["user_id"]
|
||||
if ident["session_id"] is not None:
|
||||
summary_params["session_id"] = ident["session_id"]
|
||||
raw_summary = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||
|
||||
# Render widget + OOB cart-mini
|
||||
|
||||
@@ -219,13 +219,18 @@ def register():
|
||||
select(sa_func.count()).select_from(CalendarEntry).where(*cal_filters)
|
||||
) or 0
|
||||
|
||||
# Get product cart count via service (same DB, no HTTP needed)
|
||||
# Get product cart count via HTTP
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from shared.services.registry import services
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||
ident = current_cart_identity()
|
||||
cart_summary = await services.cart.cart_summary(
|
||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
||||
)
|
||||
summary_params = {}
|
||||
if ident["user_id"] is not None:
|
||||
summary_params["user_id"] = ident["user_id"]
|
||||
if ident["session_id"] is not None:
|
||||
summary_params["session_id"] = ident["session_id"]
|
||||
raw_summary = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||
cart_summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||
product_count = cart_summary.count
|
||||
total_count = product_count + cal_count
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ from sqlalchemy import select
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from models.calendars import CalendarEntry, CalendarEntryPost
|
||||
from shared.services.registry import services
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||
|
||||
|
||||
async def add_post_to_entry(
|
||||
@@ -28,8 +29,8 @@ async def add_post_to_entry(
|
||||
return False, "Calendar entry not found"
|
||||
|
||||
# Check if post exists
|
||||
post = await services.blog.get_post_by_id(session, post_id)
|
||||
if not post:
|
||||
raw = await fetch_data("blog", "post-by-id", params={"id": post_id}, required=False)
|
||||
if not raw:
|
||||
return False, "Post not found"
|
||||
|
||||
# Check if association already exists
|
||||
@@ -103,7 +104,10 @@ async def get_entry_posts(
|
||||
post_ids = list(result.scalars().all())
|
||||
if not post_ids:
|
||||
return []
|
||||
posts = await services.blog.get_posts_by_ids(session, post_ids)
|
||||
raw_posts = await fetch_data("blog", "posts-by-ids",
|
||||
params={"ids": ",".join(str(i) for i in post_ids)},
|
||||
required=False) or []
|
||||
posts = [dto_from_dict(PostDTO, p) for p in raw_posts]
|
||||
return sorted(posts, key=lambda p: (p.title or ""))
|
||||
|
||||
|
||||
@@ -118,4 +122,8 @@ async def search_posts(
|
||||
If query is empty, returns all posts in published order.
|
||||
Returns (post_dtos, total_count).
|
||||
"""
|
||||
return await services.blog.search_posts(session, query, page, per_page)
|
||||
raw = await fetch_data("blog", "search-posts",
|
||||
params={"query": query, "page": page, "per_page": per_page},
|
||||
required=False) or {"posts": [], "total": 0}
|
||||
posts = [dto_from_dict(PostDTO, p) for p in raw.get("posts", [])]
|
||||
return posts, raw.get("total", 0)
|
||||
|
||||
@@ -4,7 +4,8 @@ from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models.calendars import Calendar
|
||||
from shared.services.registry import services
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||
from shared.services.relationships import attach_child, detach_child
|
||||
import unicodedata
|
||||
import re
|
||||
@@ -49,7 +50,8 @@ 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)
|
||||
raw = await fetch_data("blog", "post-by-slug", params={"slug": post_slug}, required=False)
|
||||
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||
if not post:
|
||||
return False
|
||||
|
||||
@@ -84,7 +86,8 @@ 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 services.blog.get_post_by_id(sess, post_id)
|
||||
raw = await fetch_data("blog", "post-by-id", params={"id": post_id}, required=False)
|
||||
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||
if not post:
|
||||
raise CalendarError(f"Post {post_id} does not exist.")
|
||||
|
||||
|
||||
1
events/bp/data/__init__.py
Normal file
1
events/bp/data/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .routes import register as register_data
|
||||
144
events/bp/data/routes.py
Normal file
144
events/bp/data/routes.py
Normal file
@@ -0,0 +1,144 @@
|
||||
"""Events app data endpoints.
|
||||
|
||||
Exposes read-only JSON queries at ``/internal/data/<query_name>`` for
|
||||
cross-app callers via the internal data client.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from quart import Blueprint, g, jsonify, request
|
||||
|
||||
from shared.infrastructure.data_client import DATA_HEADER
|
||||
from shared.contracts.dtos import dto_to_dict
|
||||
from shared.services.registry import services
|
||||
|
||||
|
||||
def register() -> Blueprint:
|
||||
bp = Blueprint("data", __name__, url_prefix="/internal/data")
|
||||
|
||||
@bp.before_request
|
||||
async def _require_data_header():
|
||||
if not request.headers.get(DATA_HEADER):
|
||||
return jsonify({"error": "forbidden"}), 403
|
||||
|
||||
_handlers: dict[str, object] = {}
|
||||
|
||||
@bp.get("/<query_name>")
|
||||
async def handle_query(query_name: str):
|
||||
handler = _handlers.get(query_name)
|
||||
if handler is None:
|
||||
return jsonify({"error": "unknown query"}), 404
|
||||
result = await handler()
|
||||
return jsonify(result)
|
||||
|
||||
# --- pending-entries ---
|
||||
async def _pending_entries():
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
session_id = request.args.get("session_id")
|
||||
entries = await services.calendar.pending_entries(
|
||||
g.s, user_id=user_id, session_id=session_id,
|
||||
)
|
||||
return [dto_to_dict(e) for e in entries]
|
||||
|
||||
_handlers["pending-entries"] = _pending_entries
|
||||
|
||||
# --- pending-tickets ---
|
||||
async def _pending_tickets():
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
session_id = request.args.get("session_id")
|
||||
tickets = await services.calendar.pending_tickets(
|
||||
g.s, user_id=user_id, session_id=session_id,
|
||||
)
|
||||
return [dto_to_dict(t) for t in tickets]
|
||||
|
||||
_handlers["pending-tickets"] = _pending_tickets
|
||||
|
||||
# --- entries-for-page ---
|
||||
async def _entries_for_page():
|
||||
page_id = request.args.get("page_id", type=int)
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
session_id = request.args.get("session_id")
|
||||
entries = await services.calendar.entries_for_page(
|
||||
g.s, page_id, user_id=user_id, session_id=session_id,
|
||||
)
|
||||
return [dto_to_dict(e) for e in entries]
|
||||
|
||||
_handlers["entries-for-page"] = _entries_for_page
|
||||
|
||||
# --- tickets-for-page ---
|
||||
async def _tickets_for_page():
|
||||
page_id = request.args.get("page_id", type=int)
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
session_id = request.args.get("session_id")
|
||||
tickets = await services.calendar.tickets_for_page(
|
||||
g.s, page_id, user_id=user_id, session_id=session_id,
|
||||
)
|
||||
return [dto_to_dict(t) for t in tickets]
|
||||
|
||||
_handlers["tickets-for-page"] = _tickets_for_page
|
||||
|
||||
# --- entries-for-order ---
|
||||
async def _entries_for_order():
|
||||
order_id = request.args.get("order_id", type=int)
|
||||
entries = await services.calendar.get_entries_for_order(g.s, order_id)
|
||||
return [dto_to_dict(e) for e in entries]
|
||||
|
||||
_handlers["entries-for-order"] = _entries_for_order
|
||||
|
||||
# --- tickets-for-order ---
|
||||
async def _tickets_for_order():
|
||||
order_id = request.args.get("order_id", type=int)
|
||||
tickets = await services.calendar.get_tickets_for_order(g.s, order_id)
|
||||
return [dto_to_dict(t) for t in tickets]
|
||||
|
||||
_handlers["tickets-for-order"] = _tickets_for_order
|
||||
|
||||
# --- entry-ids-for-content ---
|
||||
async def _entry_ids_for_content():
|
||||
content_type = request.args.get("content_type", "")
|
||||
content_id = request.args.get("content_id", type=int)
|
||||
ids = await services.calendar.entry_ids_for_content(g.s, content_type, content_id)
|
||||
return list(ids)
|
||||
|
||||
_handlers["entry-ids-for-content"] = _entry_ids_for_content
|
||||
|
||||
# --- associated-entries ---
|
||||
async def _associated_entries():
|
||||
content_type = request.args.get("content_type", "")
|
||||
content_id = request.args.get("content_id", type=int)
|
||||
page = request.args.get("page", 1, type=int)
|
||||
entries, has_more = await services.calendar.associated_entries(
|
||||
g.s, content_type, content_id, page,
|
||||
)
|
||||
return {"entries": [dto_to_dict(e) for e in entries], "has_more": has_more}
|
||||
|
||||
_handlers["associated-entries"] = _associated_entries
|
||||
|
||||
# --- calendars-for-container ---
|
||||
async def _calendars_for_container():
|
||||
container_type = request.args.get("type", "")
|
||||
container_id = request.args.get("id", type=int)
|
||||
calendars = await services.calendar.calendars_for_container(
|
||||
g.s, container_type, container_id,
|
||||
)
|
||||
return [dto_to_dict(c) for c in calendars]
|
||||
|
||||
_handlers["calendars-for-container"] = _calendars_for_container
|
||||
|
||||
# --- visible-entries-for-period ---
|
||||
async def _visible_entries_for_period():
|
||||
from datetime import datetime
|
||||
calendar_id = request.args.get("calendar_id", type=int)
|
||||
period_start = datetime.fromisoformat(request.args.get("period_start", ""))
|
||||
period_end = datetime.fromisoformat(request.args.get("period_end", ""))
|
||||
user_id = request.args.get("user_id", type=int)
|
||||
is_admin = request.args.get("is_admin", "false").lower() == "true"
|
||||
session_id = request.args.get("session_id")
|
||||
entries = await services.calendar.visible_entries_for_period(
|
||||
g.s, calendar_id, period_start, period_end,
|
||||
user_id=user_id, is_admin=is_admin, session_id=session_id,
|
||||
)
|
||||
return [dto_to_dict(e) for e in entries]
|
||||
|
||||
_handlers["visible-entries-for-period"] = _visible_entries_for_period
|
||||
|
||||
return bp
|
||||
@@ -9,6 +9,8 @@ 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
|
||||
|
||||
|
||||
@@ -139,7 +141,8 @@ def register():
|
||||
parts = []
|
||||
for s in slugs:
|
||||
parts.append(f"<!-- fragment:{s} -->")
|
||||
post = await services.blog.get_post_by_slug(g.s, 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,
|
||||
@@ -157,7 +160,8 @@ def register():
|
||||
# Single mode
|
||||
if not slug:
|
||||
return ""
|
||||
post = await services.blog.get_post_by_slug(g.s, slug)
|
||||
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(
|
||||
|
||||
@@ -5,8 +5,9 @@ import unicodedata
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from shared.contracts.dtos import MarketPlaceDTO
|
||||
from shared.services.registry import services
|
||||
from shared.contracts.dtos import MarketPlaceDTO, PostDTO, dto_from_dict
|
||||
from shared.infrastructure.actions import call_action, ActionError
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
|
||||
|
||||
class MarketError(ValueError):
|
||||
@@ -37,21 +38,33 @@ 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 services.blog.get_post_by_id(sess, post_id)
|
||||
raw = await fetch_data("blog", "post-by-id", params={"id": post_id}, required=False)
|
||||
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||
if not post:
|
||||
raise MarketError(f"Post {post_id} does not exist.")
|
||||
if not post.is_page:
|
||||
raise MarketError("Markets can only be created on pages, not posts.")
|
||||
|
||||
try:
|
||||
return await services.market.create_marketplace(sess, "page", post_id, name, slug)
|
||||
except ValueError as e:
|
||||
result = await call_action("market", "create-marketplace", payload={
|
||||
"container_type": "page", "container_id": post_id,
|
||||
"name": name, "slug": slug,
|
||||
})
|
||||
return MarketPlaceDTO(**result)
|
||||
except ActionError as e:
|
||||
raise MarketError(str(e)) from e
|
||||
|
||||
|
||||
async def soft_delete(sess: AsyncSession, post_slug: str, market_slug: str) -> bool:
|
||||
post = await services.blog.get_post_by_slug(sess, post_slug)
|
||||
raw = await fetch_data("blog", "post-by-slug", params={"slug": post_slug}, required=False)
|
||||
post = dto_from_dict(PostDTO, raw) if raw else None
|
||||
if not post:
|
||||
return False
|
||||
|
||||
return await services.market.soft_delete_marketplace(sess, "page", post.id, market_slug)
|
||||
try:
|
||||
result = await call_action("market", "soft-delete-marketplace", payload={
|
||||
"container_type": "page", "container_id": post.id, "slug": market_slug,
|
||||
})
|
||||
return result.get("deleted", False)
|
||||
except ActionError:
|
||||
return False
|
||||
|
||||
@@ -12,6 +12,8 @@ from quart import Blueprint, g, request, render_template, render_template_string
|
||||
|
||||
from shared.browser.app.utils.htmx import is_htmx_request
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||
from shared.services.registry import services
|
||||
|
||||
|
||||
@@ -107,9 +109,13 @@ def register() -> Blueprint:
|
||||
entry = await services.calendar.entry_by_id(g.s, entry_id)
|
||||
|
||||
# Updated cart count for OOB mini-cart
|
||||
summary = await services.cart.cart_summary(
|
||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
||||
)
|
||||
summary_params = {}
|
||||
if ident["user_id"] is not None:
|
||||
summary_params["user_id"] = ident["user_id"]
|
||||
if ident["session_id"] is not None:
|
||||
summary_params["session_id"] = ident["session_id"]
|
||||
raw_summary = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||
|
||||
# Render widget + OOB cart-mini
|
||||
|
||||
@@ -287,10 +287,15 @@ def register() -> Blueprint:
|
||||
)
|
||||
|
||||
# Compute cart count for OOB mini-cart update
|
||||
from shared.services.registry import services
|
||||
summary = await services.cart.cart_summary(
|
||||
g.s, user_id=ident["user_id"], session_id=ident["session_id"],
|
||||
)
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||
summary_params = {}
|
||||
if ident["user_id"] is not None:
|
||||
summary_params["user_id"] = ident["user_id"]
|
||||
if ident["session_id"] is not None:
|
||||
summary_params["session_id"] = ident["session_id"]
|
||||
raw_summary = await fetch_data("cart", "cart-summary", params=summary_params, required=False)
|
||||
summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||
cart_count = summary.count + summary.calendar_count + summary.ticket_count
|
||||
|
||||
html = await render_template(
|
||||
|
||||
@@ -7,23 +7,14 @@ def register_domain_services() -> None:
|
||||
|
||||
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.
|
||||
Cross-app calls go over HTTP via call_action() / fetch_data().
|
||||
"""
|
||||
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()
|
||||
|
||||
# Federation needed for AP shared infrastructure (activitypub blueprint)
|
||||
if not services.has("federation"):
|
||||
from shared.services.federation_impl import SqlFederationService
|
||||
services.federation = SqlFederationService()
|
||||
|
||||
Reference in New Issue
Block a user