Eliminate Python page helpers from account, federation, and cart
All three services now fetch page data via (service ...) IO primitives in .sx defpages instead of Python middleman functions. - Account: newsletters-data → AccountPageService.newsletters_data - Federation: 8 page helpers → FederationPageService methods (timeline, compose, search, following, followers, notifications) - Cart: 4 page helpers → CartPageService methods (overview, page-cart, admin, payments) - Serializers moved to service modules, thin delegates kept for routes - ~520 lines of Python page helpers removed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,9 +3,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
|
|
||||||
def register_domain_services() -> None:
|
def register_domain_services() -> None:
|
||||||
"""Register services for the account app.
|
"""Register services for the account app."""
|
||||||
|
from shared.services.registry import services
|
||||||
Account is a consumer-only dashboard app. It has no own domain.
|
from .account_page import AccountPageService
|
||||||
All cross-app data comes via fragments and HTTP data endpoints.
|
services.register("account_page", AccountPageService())
|
||||||
"""
|
|
||||||
pass
|
|
||||||
|
|||||||
40
account/services/account_page.py
Normal file
40
account/services/account_page.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Account page data service — provides serialized dicts for .sx defpages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
class AccountPageService:
|
||||||
|
"""Service for account page data, callable via (service "account-page" ...)."""
|
||||||
|
|
||||||
|
async def newsletters_data(self, session, **kw):
|
||||||
|
"""Return newsletter list with user subscription status."""
|
||||||
|
from quart import g
|
||||||
|
from sqlalchemy import select
|
||||||
|
from shared.models import UserNewsletter
|
||||||
|
from shared.models.ghost_membership_entities import GhostNewsletter
|
||||||
|
|
||||||
|
result = await session.execute(
|
||||||
|
select(GhostNewsletter).order_by(GhostNewsletter.name)
|
||||||
|
)
|
||||||
|
all_newsletters = result.scalars().all()
|
||||||
|
|
||||||
|
sub_result = await session.execute(
|
||||||
|
select(UserNewsletter).where(
|
||||||
|
UserNewsletter.user_id == g.user.id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
user_subs = {un.newsletter_id: un for un in sub_result.scalars().all()}
|
||||||
|
|
||||||
|
newsletter_list = []
|
||||||
|
for nl in all_newsletters:
|
||||||
|
un = user_subs.get(nl.id)
|
||||||
|
newsletter_list.append({
|
||||||
|
"newsletter": {"id": nl.id, "name": nl.name, "description": nl.description},
|
||||||
|
"un": {"newsletter_id": un.newsletter_id, "subscribed": un.subscribed} if un else None,
|
||||||
|
"subscribed": un.subscribed if un else False,
|
||||||
|
})
|
||||||
|
|
||||||
|
from shared.infrastructure.urls import account_url
|
||||||
|
return {
|
||||||
|
"newsletter_list": newsletter_list,
|
||||||
|
"account_url": account_url(""),
|
||||||
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
"""Account defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
"""Account defpage setup — registers layouts and loads .sx pages."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def setup_account_pages() -> None:
|
def setup_account_pages() -> None:
|
||||||
"""Register account-specific layouts, page helpers, and load page definitions."""
|
"""Register account-specific layouts and load page definitions."""
|
||||||
_register_account_layouts()
|
_register_account_layouts()
|
||||||
_register_account_helpers()
|
|
||||||
_load_account_page_files()
|
_load_account_page_files()
|
||||||
|
|
||||||
|
|
||||||
@@ -90,50 +89,3 @@ def _as_sx_nav(ctx: dict) -> Any:
|
|||||||
return _as_sx(ctx.get("account_nav"))
|
return _as_sx(ctx.get("account_nav"))
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Page helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _register_account_helpers() -> None:
|
|
||||||
from shared.sx.pages import register_page_helpers
|
|
||||||
|
|
||||||
register_page_helpers("account", {
|
|
||||||
"newsletters-data": _h_newsletters_data,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_newsletters_data(**kw):
|
|
||||||
"""Fetch newsletter data — returns dict merged into defpage env."""
|
|
||||||
from quart import g
|
|
||||||
from sqlalchemy import select
|
|
||||||
from shared.models import UserNewsletter
|
|
||||||
from shared.models.ghost_membership_entities import GhostNewsletter
|
|
||||||
|
|
||||||
result = await g.s.execute(
|
|
||||||
select(GhostNewsletter).order_by(GhostNewsletter.name)
|
|
||||||
)
|
|
||||||
all_newsletters = result.scalars().all()
|
|
||||||
|
|
||||||
sub_result = await g.s.execute(
|
|
||||||
select(UserNewsletter).where(
|
|
||||||
UserNewsletter.user_id == g.user.id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
user_subs = {un.newsletter_id: un for un in sub_result.scalars().all()}
|
|
||||||
|
|
||||||
newsletter_list = []
|
|
||||||
for nl in all_newsletters:
|
|
||||||
un = user_subs.get(nl.id)
|
|
||||||
newsletter_list.append({
|
|
||||||
"newsletter": {"id": nl.id, "name": nl.name, "description": nl.description},
|
|
||||||
"un": {"newsletter_id": un.newsletter_id, "subscribed": un.subscribed} if un else None,
|
|
||||||
"subscribed": un.subscribed if un else False,
|
|
||||||
})
|
|
||||||
|
|
||||||
account_url = getattr(g, "_account_url", None)
|
|
||||||
if account_url is None:
|
|
||||||
from shared.infrastructure.urls import account_url as _account_url
|
|
||||||
account_url = _account_url
|
|
||||||
account_url_str = account_url("") if callable(account_url) else str(account_url or "")
|
|
||||||
|
|
||||||
return {"newsletter-list": newsletter_list, "account-url": account_url_str}
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
:path "/newsletters/"
|
:path "/newsletters/"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :account
|
:layout :account
|
||||||
:data (newsletters-data)
|
:data (service "account-page" "newsletters-data")
|
||||||
:content (~account-newsletters-content
|
:content (~account-newsletters-content
|
||||||
:newsletter-list newsletter-list
|
:newsletter-list newsletter-list
|
||||||
:account-url account-url))
|
:account-url account-url))
|
||||||
|
|||||||
@@ -12,3 +12,6 @@ def register_domain_services() -> None:
|
|||||||
from shared.services.cart_impl import SqlCartService
|
from shared.services.cart_impl import SqlCartService
|
||||||
|
|
||||||
services.cart = SqlCartService()
|
services.cart = SqlCartService()
|
||||||
|
|
||||||
|
from .cart_page import CartPageService
|
||||||
|
services.register("cart_page", CartPageService())
|
||||||
|
|||||||
187
cart/services/cart_page.py
Normal file
187
cart/services/cart_page.py
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
"""Cart page data service — provides serialized dicts for .sx defpages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_cart_item(item: Any) -> dict:
|
||||||
|
from quart import url_for
|
||||||
|
from shared.infrastructure.urls import market_product_url
|
||||||
|
|
||||||
|
p = item.product if hasattr(item, "product") else item
|
||||||
|
slug = p.slug if hasattr(p, "slug") else ""
|
||||||
|
unit_price = getattr(p, "special_price", None) or getattr(p, "regular_price", None)
|
||||||
|
currency = getattr(p, "regular_price_currency", "GBP") or "GBP"
|
||||||
|
return {
|
||||||
|
"slug": slug,
|
||||||
|
"title": p.title if hasattr(p, "title") else "",
|
||||||
|
"image": p.image if hasattr(p, "image") else None,
|
||||||
|
"brand": getattr(p, "brand", None),
|
||||||
|
"is_deleted": getattr(item, "is_deleted", False),
|
||||||
|
"unit_price": float(unit_price) if unit_price else None,
|
||||||
|
"special_price": float(p.special_price) if getattr(p, "special_price", None) else None,
|
||||||
|
"regular_price": float(p.regular_price) if getattr(p, "regular_price", None) else None,
|
||||||
|
"currency": currency,
|
||||||
|
"quantity": item.quantity,
|
||||||
|
"product_id": p.id,
|
||||||
|
"product_url": market_product_url(slug),
|
||||||
|
"qty_url": url_for("cart_global.update_quantity", product_id=p.id),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_cal_entry(e: Any) -> dict:
|
||||||
|
name = getattr(e, "name", None) or getattr(e, "calendar_name", "")
|
||||||
|
start = e.start_at if hasattr(e, "start_at") else ""
|
||||||
|
end = getattr(e, "end_at", None)
|
||||||
|
cost = getattr(e, "cost", 0) or 0
|
||||||
|
end_str = f" \u2013 {end}" if end else ""
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"date_str": f"{start}{end_str}",
|
||||||
|
"cost": float(cost),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_ticket_group(tg: Any) -> dict:
|
||||||
|
name = tg.entry_name if hasattr(tg, "entry_name") else tg.get("entry_name", "")
|
||||||
|
tt_name = tg.ticket_type_name if hasattr(tg, "ticket_type_name") else tg.get("ticket_type_name", "")
|
||||||
|
price = tg.price if hasattr(tg, "price") else tg.get("price", 0)
|
||||||
|
quantity = tg.quantity if hasattr(tg, "quantity") else tg.get("quantity", 0)
|
||||||
|
line_total = tg.line_total if hasattr(tg, "line_total") else tg.get("line_total", 0)
|
||||||
|
entry_id = tg.entry_id if hasattr(tg, "entry_id") else tg.get("entry_id", "")
|
||||||
|
tt_id = tg.ticket_type_id if hasattr(tg, "ticket_type_id") else tg.get("ticket_type_id", "")
|
||||||
|
start_at = tg.entry_start_at if hasattr(tg, "entry_start_at") else tg.get("entry_start_at")
|
||||||
|
end_at = tg.entry_end_at if hasattr(tg, "entry_end_at") else tg.get("entry_end_at")
|
||||||
|
|
||||||
|
date_str = start_at.strftime("%-d %b %Y, %H:%M") if start_at else ""
|
||||||
|
if end_at:
|
||||||
|
date_str += f" \u2013 {end_at.strftime('%-d %b %Y, %H:%M')}"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"entry_name": name,
|
||||||
|
"ticket_type_name": tt_name or None,
|
||||||
|
"price": float(price or 0),
|
||||||
|
"quantity": quantity,
|
||||||
|
"line_total": float(line_total or 0),
|
||||||
|
"entry_id": entry_id,
|
||||||
|
"ticket_type_id": tt_id or None,
|
||||||
|
"date_str": date_str,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_page_group(grp: Any) -> dict | None:
|
||||||
|
post = grp.get("post") if isinstance(grp, dict) else getattr(grp, "post", None)
|
||||||
|
cart_items = grp.get("cart_items", []) if isinstance(grp, dict) else getattr(grp, "cart_items", [])
|
||||||
|
cal_entries = grp.get("calendar_entries", []) if isinstance(grp, dict) else getattr(grp, "calendar_entries", [])
|
||||||
|
tickets = grp.get("tickets", []) if isinstance(grp, dict) else getattr(grp, "tickets", [])
|
||||||
|
|
||||||
|
if not cart_items and not cal_entries and not tickets:
|
||||||
|
return None
|
||||||
|
|
||||||
|
post_data = None
|
||||||
|
if post:
|
||||||
|
post_data = {
|
||||||
|
"slug": post.slug if hasattr(post, "slug") else post.get("slug", ""),
|
||||||
|
"title": post.title if hasattr(post, "title") else post.get("title", ""),
|
||||||
|
"feature_image": post.feature_image if hasattr(post, "feature_image") else post.get("feature_image"),
|
||||||
|
}
|
||||||
|
market_place = grp.get("market_place") if isinstance(grp, dict) else getattr(grp, "market_place", None)
|
||||||
|
mp_data = None
|
||||||
|
if market_place:
|
||||||
|
mp_data = {"name": market_place.name if hasattr(market_place, "name") else market_place.get("name", "")}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"post": post_data,
|
||||||
|
"product_count": grp.get("product_count", 0) if isinstance(grp, dict) else getattr(grp, "product_count", 0),
|
||||||
|
"calendar_count": grp.get("calendar_count", 0) if isinstance(grp, dict) else getattr(grp, "calendar_count", 0),
|
||||||
|
"ticket_count": grp.get("ticket_count", 0) if isinstance(grp, dict) else getattr(grp, "ticket_count", 0),
|
||||||
|
"total": float(grp.get("total", 0) if isinstance(grp, dict) else getattr(grp, "total", 0)),
|
||||||
|
"market_place": mp_data,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class CartPageService:
|
||||||
|
"""Service for cart page data, callable via (service "cart-page" ...)."""
|
||||||
|
|
||||||
|
async def overview_data(self, session, **kw):
|
||||||
|
from shared.infrastructure.urls import cart_url
|
||||||
|
from bp.cart.services import get_cart_grouped_by_page
|
||||||
|
|
||||||
|
page_groups = await get_cart_grouped_by_page(session)
|
||||||
|
grp_dicts = [d for d in (_serialize_page_group(grp) for grp in page_groups) if d]
|
||||||
|
return {
|
||||||
|
"page_groups": grp_dicts,
|
||||||
|
"cart_url_base": cart_url(""),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def page_cart_data(self, session, **kw):
|
||||||
|
from quart import g, request, url_for
|
||||||
|
from shared.infrastructure.urls import login_url
|
||||||
|
from shared.utils import route_prefix
|
||||||
|
from bp.cart.services import total, calendar_total, ticket_total
|
||||||
|
from bp.cart.services.page_cart import (
|
||||||
|
get_cart_for_page, get_calendar_entries_for_page, get_tickets_for_page,
|
||||||
|
)
|
||||||
|
from bp.cart.services.ticket_groups import group_tickets
|
||||||
|
|
||||||
|
post = g.page_post
|
||||||
|
cart = await get_cart_for_page(session, post.id)
|
||||||
|
cal_entries = await get_calendar_entries_for_page(session, post.id)
|
||||||
|
page_tickets = await get_tickets_for_page(session, post.id)
|
||||||
|
ticket_groups = group_tickets(page_tickets)
|
||||||
|
|
||||||
|
# Build summary data
|
||||||
|
product_qty = sum(ci.quantity for ci in cart) if cart else 0
|
||||||
|
ticket_qty = len(page_tickets) if page_tickets else 0
|
||||||
|
item_count = product_qty + ticket_qty
|
||||||
|
|
||||||
|
product_total = total(cart) or 0
|
||||||
|
cal_total = calendar_total(cal_entries) or 0
|
||||||
|
tk_total = ticket_total(page_tickets) or 0
|
||||||
|
grand = float(product_total) + float(cal_total) + float(tk_total)
|
||||||
|
|
||||||
|
symbol = "\u00a3"
|
||||||
|
if cart and hasattr(cart[0], "product") and getattr(cart[0].product, "regular_price_currency", None):
|
||||||
|
cur = cart[0].product.regular_price_currency
|
||||||
|
symbol = "\u00a3" if cur == "GBP" else cur
|
||||||
|
|
||||||
|
user = getattr(g, "user", None)
|
||||||
|
page_post = getattr(g, "page_post", None)
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"item_count": item_count,
|
||||||
|
"grand_total": grand,
|
||||||
|
"symbol": symbol,
|
||||||
|
"is_logged_in": bool(user),
|
||||||
|
}
|
||||||
|
|
||||||
|
if user:
|
||||||
|
if page_post:
|
||||||
|
action = url_for("page_cart.page_checkout")
|
||||||
|
else:
|
||||||
|
action = url_for("cart_global.checkout")
|
||||||
|
summary["checkout_action"] = route_prefix() + action
|
||||||
|
summary["user_email"] = user.email
|
||||||
|
else:
|
||||||
|
summary["login_href"] = login_url(request.url)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cart_items": [_serialize_cart_item(i) for i in cart],
|
||||||
|
"cal_entries": [_serialize_cal_entry(e) for e in cal_entries],
|
||||||
|
"ticket_groups": [_serialize_ticket_group(tg) for tg in ticket_groups],
|
||||||
|
"summary": summary,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def payments_data(self, session, **kw):
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
|
||||||
|
ctx = await get_template_context()
|
||||||
|
page_config = ctx.get("page_config")
|
||||||
|
pc_data = None
|
||||||
|
if page_config:
|
||||||
|
pc_data = {
|
||||||
|
"sumup_api_key": bool(getattr(page_config, "sumup_api_key", None)),
|
||||||
|
"sumup_merchant_code": getattr(page_config, "sumup_merchant_code", None) or "",
|
||||||
|
"sumup_checkout_prefix": getattr(page_config, "sumup_checkout_prefix", None) or "",
|
||||||
|
}
|
||||||
|
return {"page_config": pc_data}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Cart defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
"""Cart defpage setup — registers layouts and loads .sx pages."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@@ -8,9 +8,8 @@ from shared.sx.parser import SxExpr
|
|||||||
|
|
||||||
|
|
||||||
def setup_cart_pages() -> None:
|
def setup_cart_pages() -> None:
|
||||||
"""Register cart-specific layouts, page helpers, and load page definitions."""
|
"""Register cart-specific layouts and load page definitions."""
|
||||||
_register_cart_layouts()
|
_register_cart_layouts()
|
||||||
_register_cart_helpers()
|
|
||||||
_load_cart_page_files()
|
_load_cart_page_files()
|
||||||
|
|
||||||
|
|
||||||
@@ -118,7 +117,7 @@ async def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = F
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Order serialization helpers
|
# Order serialization helpers (used by route render functions below)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _serialize_order(order: Any) -> dict:
|
def _serialize_order(order: Any) -> dict:
|
||||||
@@ -352,239 +351,3 @@ async def _cart_admin_oob(ctx: dict, **kw: Any) -> str:
|
|||||||
return await _cart_page_admin_header_sx(ctx, page_post, oob=True, selected=selected)
|
return await _cart_page_admin_header_sx(ctx, page_post, oob=True, selected=selected)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Page helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _register_cart_helpers() -> None:
|
|
||||||
from shared.sx.pages import register_page_helpers
|
|
||||||
|
|
||||||
register_page_helpers("cart", {
|
|
||||||
"overview-content": _h_overview_content,
|
|
||||||
"page-cart-content": _h_page_cart_content,
|
|
||||||
"cart-admin-content": _h_cart_admin_content,
|
|
||||||
"cart-payments-content": _h_cart_payments_content,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Serialization helpers
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
def _serialize_cart_item(item: Any) -> dict:
|
|
||||||
"""Serialize a cart item + product for SX defcomps."""
|
|
||||||
from quart import url_for
|
|
||||||
from shared.infrastructure.urls import market_product_url
|
|
||||||
|
|
||||||
p = item.product if hasattr(item, "product") else item
|
|
||||||
slug = p.slug if hasattr(p, "slug") else ""
|
|
||||||
unit_price = getattr(p, "special_price", None) or getattr(p, "regular_price", None)
|
|
||||||
currency = getattr(p, "regular_price_currency", "GBP") or "GBP"
|
|
||||||
return {
|
|
||||||
"slug": slug,
|
|
||||||
"title": p.title if hasattr(p, "title") else "",
|
|
||||||
"image": p.image if hasattr(p, "image") else None,
|
|
||||||
"brand": getattr(p, "brand", None),
|
|
||||||
"is_deleted": getattr(item, "is_deleted", False),
|
|
||||||
"unit_price": float(unit_price) if unit_price else None,
|
|
||||||
"special_price": float(p.special_price) if getattr(p, "special_price", None) else None,
|
|
||||||
"regular_price": float(p.regular_price) if getattr(p, "regular_price", None) else None,
|
|
||||||
"currency": currency,
|
|
||||||
"quantity": item.quantity,
|
|
||||||
"product_id": p.id,
|
|
||||||
"product_url": market_product_url(slug),
|
|
||||||
"qty_url": url_for("cart_global.update_quantity", product_id=p.id),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_cal_entry(e: Any) -> dict:
|
|
||||||
"""Serialize a calendar entry for SX defcomps."""
|
|
||||||
name = getattr(e, "name", None) or getattr(e, "calendar_name", "")
|
|
||||||
start = e.start_at if hasattr(e, "start_at") else ""
|
|
||||||
end = getattr(e, "end_at", None)
|
|
||||||
cost = getattr(e, "cost", 0) or 0
|
|
||||||
end_str = f" \u2013 {end}" if end else ""
|
|
||||||
return {
|
|
||||||
"name": name,
|
|
||||||
"date_str": f"{start}{end_str}",
|
|
||||||
"cost": float(cost),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_ticket_group(tg: Any) -> dict:
|
|
||||||
"""Serialize a ticket group for SX defcomps."""
|
|
||||||
name = tg.entry_name if hasattr(tg, "entry_name") else tg.get("entry_name", "")
|
|
||||||
tt_name = tg.ticket_type_name if hasattr(tg, "ticket_type_name") else tg.get("ticket_type_name", "")
|
|
||||||
price = tg.price if hasattr(tg, "price") else tg.get("price", 0)
|
|
||||||
quantity = tg.quantity if hasattr(tg, "quantity") else tg.get("quantity", 0)
|
|
||||||
line_total = tg.line_total if hasattr(tg, "line_total") else tg.get("line_total", 0)
|
|
||||||
entry_id = tg.entry_id if hasattr(tg, "entry_id") else tg.get("entry_id", "")
|
|
||||||
tt_id = tg.ticket_type_id if hasattr(tg, "ticket_type_id") else tg.get("ticket_type_id", "")
|
|
||||||
start_at = tg.entry_start_at if hasattr(tg, "entry_start_at") else tg.get("entry_start_at")
|
|
||||||
end_at = tg.entry_end_at if hasattr(tg, "entry_end_at") else tg.get("entry_end_at")
|
|
||||||
|
|
||||||
date_str = start_at.strftime("%-d %b %Y, %H:%M") if start_at else ""
|
|
||||||
if end_at:
|
|
||||||
date_str += f" \u2013 {end_at.strftime('%-d %b %Y, %H:%M')}"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"entry_name": name,
|
|
||||||
"ticket_type_name": tt_name or None,
|
|
||||||
"price": float(price or 0),
|
|
||||||
"quantity": quantity,
|
|
||||||
"line_total": float(line_total or 0),
|
|
||||||
"entry_id": entry_id,
|
|
||||||
"ticket_type_id": tt_id or None,
|
|
||||||
"date_str": date_str,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_page_group(grp: Any) -> dict:
|
|
||||||
"""Serialize a page group for SX defcomps."""
|
|
||||||
post = grp.get("post") if isinstance(grp, dict) else getattr(grp, "post", None)
|
|
||||||
cart_items = grp.get("cart_items", []) if isinstance(grp, dict) else getattr(grp, "cart_items", [])
|
|
||||||
cal_entries = grp.get("calendar_entries", []) if isinstance(grp, dict) else getattr(grp, "calendar_entries", [])
|
|
||||||
tickets = grp.get("tickets", []) if isinstance(grp, dict) else getattr(grp, "tickets", [])
|
|
||||||
|
|
||||||
if not cart_items and not cal_entries and not tickets:
|
|
||||||
return None
|
|
||||||
|
|
||||||
post_data = None
|
|
||||||
if post:
|
|
||||||
post_data = {
|
|
||||||
"slug": post.slug if hasattr(post, "slug") else post.get("slug", ""),
|
|
||||||
"title": post.title if hasattr(post, "title") else post.get("title", ""),
|
|
||||||
"feature_image": post.feature_image if hasattr(post, "feature_image") else post.get("feature_image"),
|
|
||||||
}
|
|
||||||
market_place = grp.get("market_place") if isinstance(grp, dict) else getattr(grp, "market_place", None)
|
|
||||||
mp_data = None
|
|
||||||
if market_place:
|
|
||||||
mp_data = {"name": market_place.name if hasattr(market_place, "name") else market_place.get("name", "")}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"post": post_data,
|
|
||||||
"product_count": grp.get("product_count", 0) if isinstance(grp, dict) else getattr(grp, "product_count", 0),
|
|
||||||
"calendar_count": grp.get("calendar_count", 0) if isinstance(grp, dict) else getattr(grp, "calendar_count", 0),
|
|
||||||
"ticket_count": grp.get("ticket_count", 0) if isinstance(grp, dict) else getattr(grp, "ticket_count", 0),
|
|
||||||
"total": float(grp.get("total", 0) if isinstance(grp, dict) else getattr(grp, "total", 0)),
|
|
||||||
"market_place": mp_data,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _build_summary_data(ctx: dict, cart: list, cal_entries: list, tickets: list,
|
|
||||||
total_fn, cal_total_fn, ticket_total_fn) -> dict:
|
|
||||||
"""Build cart summary data dict for SX defcomps."""
|
|
||||||
from quart import g, request, url_for
|
|
||||||
from shared.infrastructure.urls import login_url
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
|
|
||||||
product_qty = sum(ci.quantity for ci in cart) if cart else 0
|
|
||||||
ticket_qty = len(tickets) if tickets else 0
|
|
||||||
item_count = product_qty + ticket_qty
|
|
||||||
|
|
||||||
product_total = total_fn(cart) or 0
|
|
||||||
cal_total = cal_total_fn(cal_entries) or 0
|
|
||||||
tk_total = ticket_total_fn(tickets) or 0
|
|
||||||
grand = float(product_total) + float(cal_total) + float(tk_total)
|
|
||||||
|
|
||||||
symbol = "\u00a3"
|
|
||||||
if cart and hasattr(cart[0], "product") and getattr(cart[0].product, "regular_price_currency", None):
|
|
||||||
cur = cart[0].product.regular_price_currency
|
|
||||||
symbol = "\u00a3" if cur == "GBP" else cur
|
|
||||||
|
|
||||||
user = getattr(g, "user", None)
|
|
||||||
page_post = ctx.get("page_post")
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"item_count": item_count,
|
|
||||||
"grand_total": grand,
|
|
||||||
"symbol": symbol,
|
|
||||||
"is_logged_in": bool(user),
|
|
||||||
}
|
|
||||||
|
|
||||||
if user:
|
|
||||||
if page_post:
|
|
||||||
action = url_for("page_cart.page_checkout")
|
|
||||||
else:
|
|
||||||
action = url_for("cart_global.checkout")
|
|
||||||
result["checkout_action"] = route_prefix() + action
|
|
||||||
result["user_email"] = user.email
|
|
||||||
else:
|
|
||||||
result["login_href"] = login_url(request.url)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Page helper implementations
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
async def _h_overview_content(**kw):
|
|
||||||
from quart import g
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
from shared.infrastructure.urls import cart_url
|
|
||||||
from bp.cart.services import get_cart_grouped_by_page
|
|
||||||
|
|
||||||
page_groups = await get_cart_grouped_by_page(g.s)
|
|
||||||
grp_dicts = [d for d in (_serialize_page_group(grp) for grp in page_groups) if d]
|
|
||||||
return await render_to_sx("cart-overview-content",
|
|
||||||
page_groups=grp_dicts,
|
|
||||||
cart_url_base=cart_url(""))
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_page_cart_content(page_slug=None, **kw):
|
|
||||||
from quart import g
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
from shared.sx.parser import SxExpr
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from bp.cart.services import total, calendar_total, ticket_total
|
|
||||||
from bp.cart.services.page_cart import (
|
|
||||||
get_cart_for_page, get_calendar_entries_for_page, get_tickets_for_page,
|
|
||||||
)
|
|
||||||
from bp.cart.services.ticket_groups import group_tickets
|
|
||||||
|
|
||||||
post = g.page_post
|
|
||||||
cart = await get_cart_for_page(g.s, post.id)
|
|
||||||
cal_entries = await get_calendar_entries_for_page(g.s, post.id)
|
|
||||||
page_tickets = await get_tickets_for_page(g.s, post.id)
|
|
||||||
ticket_groups = group_tickets(page_tickets)
|
|
||||||
|
|
||||||
ctx = await get_template_context()
|
|
||||||
sd = _build_summary_data(ctx, cart, cal_entries, page_tickets,
|
|
||||||
total, calendar_total, ticket_total)
|
|
||||||
|
|
||||||
summary_sx = await render_to_sx("cart-summary-from-data",
|
|
||||||
item_count=sd["item_count"],
|
|
||||||
grand_total=sd["grand_total"],
|
|
||||||
symbol=sd["symbol"],
|
|
||||||
is_logged_in=sd["is_logged_in"],
|
|
||||||
checkout_action=sd.get("checkout_action"),
|
|
||||||
login_href=sd.get("login_href"),
|
|
||||||
user_email=sd.get("user_email"))
|
|
||||||
|
|
||||||
return await render_to_sx("cart-page-cart-content",
|
|
||||||
cart_items=[_serialize_cart_item(i) for i in cart],
|
|
||||||
cal_entries=[_serialize_cal_entry(e) for e in cal_entries],
|
|
||||||
ticket_groups=[_serialize_ticket_group(tg) for tg in ticket_groups],
|
|
||||||
summary=SxExpr(summary_sx))
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_cart_admin_content(page_slug=None, **kw):
|
|
||||||
return '(~cart-admin-content)'
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_cart_payments_content(page_slug=None, **kw):
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
|
|
||||||
ctx = await get_template_context()
|
|
||||||
page_config = ctx.get("page_config")
|
|
||||||
pc_data = None
|
|
||||||
if page_config:
|
|
||||||
pc_data = {
|
|
||||||
"sumup_api_key": bool(getattr(page_config, "sumup_api_key", None)),
|
|
||||||
"sumup_merchant_code": getattr(page_config, "sumup_merchant_code", None) or "",
|
|
||||||
"sumup_checkout_prefix": getattr(page_config, "sumup_checkout_prefix", None) or "",
|
|
||||||
}
|
|
||||||
return await render_to_sx("cart-payments-content",
|
|
||||||
page_config=pc_data)
|
|
||||||
|
|||||||
@@ -1,25 +1,43 @@
|
|||||||
;; Cart app defpage declarations.
|
;; Cart app defpage declarations.
|
||||||
|
;; All data fetching via (service ...) IO primitives, no Python helpers.
|
||||||
|
|
||||||
(defpage cart-overview
|
(defpage cart-overview
|
||||||
:path "/"
|
:path "/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :root
|
:layout :root
|
||||||
:content (overview-content))
|
:data (service "cart-page" "overview-data")
|
||||||
|
:content (~cart-overview-content
|
||||||
|
:page-groups page-groups
|
||||||
|
:cart-url-base cart-url-base))
|
||||||
|
|
||||||
(defpage page-cart-view
|
(defpage page-cart-view
|
||||||
:path "/<page_slug>/"
|
:path "/<page_slug>/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :cart-page
|
:layout :cart-page
|
||||||
:content (page-cart-content))
|
:data (service "cart-page" "page-cart-data")
|
||||||
|
:content (~cart-page-cart-content
|
||||||
|
:cart-items cart-items
|
||||||
|
:cal-entries cal-entries
|
||||||
|
:ticket-groups ticket-groups
|
||||||
|
:summary (~cart-summary-from-data
|
||||||
|
:item-count (get summary "item_count")
|
||||||
|
:grand-total (get summary "grand_total")
|
||||||
|
:symbol (get summary "symbol")
|
||||||
|
:is-logged-in (get summary "is_logged_in")
|
||||||
|
:checkout-action (get summary "checkout_action")
|
||||||
|
:login-href (get summary "login_href")
|
||||||
|
:user-email (get summary "user_email"))))
|
||||||
|
|
||||||
(defpage cart-admin
|
(defpage cart-admin
|
||||||
:path "/<page_slug>/admin/"
|
:path "/<page_slug>/admin/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :cart-admin
|
:layout :cart-admin
|
||||||
:content (cart-admin-content))
|
:content (~cart-admin-content))
|
||||||
|
|
||||||
(defpage cart-payments
|
(defpage cart-payments
|
||||||
:path "/<page_slug>/admin/payments/"
|
:path "/<page_slug>/admin/payments/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout (:cart-admin :selected "payments")
|
:layout (:cart-admin :selected "payments")
|
||||||
:content (cart-payments-content))
|
:data (service "cart-page" "payments-data")
|
||||||
|
:content (~cart-payments-content
|
||||||
|
:page-config page-config))
|
||||||
|
|||||||
@@ -13,3 +13,6 @@ def register_domain_services() -> None:
|
|||||||
from shared.services.federation_impl import SqlFederationService
|
from shared.services.federation_impl import SqlFederationService
|
||||||
|
|
||||||
services.federation = SqlFederationService()
|
services.federation = SqlFederationService()
|
||||||
|
|
||||||
|
from .federation_page import FederationPageService
|
||||||
|
services.register("federation_page", FederationPageService())
|
||||||
|
|||||||
205
federation/services/federation_page.py
Normal file
205
federation/services/federation_page.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
"""Federation page data service — provides serialized dicts for .sx defpages."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_actor(actor) -> dict | None:
|
||||||
|
if not actor:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"id": actor.id,
|
||||||
|
"preferred_username": actor.preferred_username,
|
||||||
|
"display_name": getattr(actor, "display_name", None),
|
||||||
|
"icon_url": getattr(actor, "icon_url", None),
|
||||||
|
"summary": getattr(actor, "summary", None),
|
||||||
|
"actor_url": getattr(actor, "actor_url", ""),
|
||||||
|
"domain": getattr(actor, "domain", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_timeline_item(item) -> dict:
|
||||||
|
published = getattr(item, "published", None)
|
||||||
|
return {
|
||||||
|
"object_id": getattr(item, "object_id", "") or "",
|
||||||
|
"author_inbox": getattr(item, "author_inbox", "") or "",
|
||||||
|
"actor_icon": getattr(item, "actor_icon", None),
|
||||||
|
"actor_name": getattr(item, "actor_name", "?"),
|
||||||
|
"actor_username": getattr(item, "actor_username", ""),
|
||||||
|
"actor_domain": getattr(item, "actor_domain", ""),
|
||||||
|
"content": getattr(item, "content", ""),
|
||||||
|
"summary": getattr(item, "summary", None),
|
||||||
|
"published": published.strftime("%b %d, %H:%M") if published else "",
|
||||||
|
"before_cursor": published.isoformat() if published else "",
|
||||||
|
"url": getattr(item, "url", None),
|
||||||
|
"post_type": getattr(item, "post_type", ""),
|
||||||
|
"boosted_by": getattr(item, "boosted_by", None),
|
||||||
|
"like_count": getattr(item, "like_count", 0) or 0,
|
||||||
|
"boost_count": getattr(item, "boost_count", 0) or 0,
|
||||||
|
"liked_by_me": getattr(item, "liked_by_me", False),
|
||||||
|
"boosted_by_me": getattr(item, "boosted_by_me", False),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _serialize_remote_actor(a) -> dict:
|
||||||
|
return {
|
||||||
|
"id": getattr(a, "id", None),
|
||||||
|
"display_name": getattr(a, "display_name", None) or getattr(a, "preferred_username", ""),
|
||||||
|
"preferred_username": getattr(a, "preferred_username", ""),
|
||||||
|
"domain": getattr(a, "domain", ""),
|
||||||
|
"icon_url": getattr(a, "icon_url", None),
|
||||||
|
"actor_url": getattr(a, "actor_url", ""),
|
||||||
|
"summary": getattr(a, "summary", None),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_actor():
|
||||||
|
from quart import g
|
||||||
|
return getattr(g, "_social_actor", None)
|
||||||
|
|
||||||
|
|
||||||
|
def _require_actor():
|
||||||
|
from quart import abort
|
||||||
|
actor = _get_actor()
|
||||||
|
if not actor:
|
||||||
|
abort(403, "You need to choose a federation username first")
|
||||||
|
return actor
|
||||||
|
|
||||||
|
|
||||||
|
class FederationPageService:
|
||||||
|
"""Service for federation page data, callable via (service "federation-page" ...)."""
|
||||||
|
|
||||||
|
async def home_timeline_data(self, session, **kw):
|
||||||
|
actor = _require_actor()
|
||||||
|
from shared.services.registry import services
|
||||||
|
items = await services.federation.get_home_timeline(session, actor.id)
|
||||||
|
return {
|
||||||
|
"items": [_serialize_timeline_item(i) for i in items],
|
||||||
|
"timeline_type": "home",
|
||||||
|
"actor": _serialize_actor(actor),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def public_timeline_data(self, session, **kw):
|
||||||
|
actor = _get_actor()
|
||||||
|
from shared.services.registry import services
|
||||||
|
items = await services.federation.get_public_timeline(session)
|
||||||
|
return {
|
||||||
|
"items": [_serialize_timeline_item(i) for i in items],
|
||||||
|
"timeline_type": "public",
|
||||||
|
"actor": _serialize_actor(actor),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def compose_data(self, session, **kw):
|
||||||
|
from quart import request
|
||||||
|
_require_actor()
|
||||||
|
reply_to = request.args.get("reply_to")
|
||||||
|
return {"reply_to": reply_to or None}
|
||||||
|
|
||||||
|
async def search_data(self, session, **kw):
|
||||||
|
from quart import request
|
||||||
|
actor = _get_actor()
|
||||||
|
from shared.services.registry import services
|
||||||
|
query = request.args.get("q", "").strip()
|
||||||
|
actors_list = []
|
||||||
|
total = 0
|
||||||
|
followed_urls: list[str] = []
|
||||||
|
if query:
|
||||||
|
actors_list, total = await services.federation.search_actors(session, query)
|
||||||
|
if actor:
|
||||||
|
following, _ = await services.federation.get_following(
|
||||||
|
session, actor.preferred_username, page=1, per_page=1000,
|
||||||
|
)
|
||||||
|
followed_urls = [a.actor_url for a in following]
|
||||||
|
return {
|
||||||
|
"query": query,
|
||||||
|
"actors": [_serialize_remote_actor(a) for a in actors_list],
|
||||||
|
"total": total,
|
||||||
|
"followed_urls": followed_urls,
|
||||||
|
"actor": _serialize_actor(actor),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def following_data(self, session, **kw):
|
||||||
|
actor = _require_actor()
|
||||||
|
from shared.services.registry import services
|
||||||
|
actors_list, total = await services.federation.get_following(
|
||||||
|
session, actor.preferred_username,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"actors": [_serialize_remote_actor(a) for a in actors_list],
|
||||||
|
"total": total,
|
||||||
|
"actor": _serialize_actor(actor),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def followers_data(self, session, **kw):
|
||||||
|
actor = _require_actor()
|
||||||
|
from shared.services.registry import services
|
||||||
|
actors_list, total = await services.federation.get_followers_paginated(
|
||||||
|
session, actor.preferred_username,
|
||||||
|
)
|
||||||
|
following, _ = await services.federation.get_following(
|
||||||
|
session, actor.preferred_username, page=1, per_page=1000,
|
||||||
|
)
|
||||||
|
followed_urls = [a.actor_url for a in following]
|
||||||
|
return {
|
||||||
|
"actors": [_serialize_remote_actor(a) for a in actors_list],
|
||||||
|
"total": total,
|
||||||
|
"followed_urls": followed_urls,
|
||||||
|
"actor": _serialize_actor(actor),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def actor_timeline_data(self, session, *, id=None, **kw):
|
||||||
|
from quart import abort
|
||||||
|
from sqlalchemy import select as sa_select
|
||||||
|
from shared.models.federation import RemoteActor
|
||||||
|
from shared.services.registry import services
|
||||||
|
from shared.services.federation_impl import _remote_actor_to_dto
|
||||||
|
|
||||||
|
actor = _get_actor()
|
||||||
|
actor_id = id
|
||||||
|
remote = (
|
||||||
|
await session.execute(
|
||||||
|
sa_select(RemoteActor).where(RemoteActor.id == actor_id)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
if not remote:
|
||||||
|
abort(404)
|
||||||
|
remote_dto = _remote_actor_to_dto(remote)
|
||||||
|
items = await services.federation.get_actor_timeline(session, actor_id)
|
||||||
|
is_following = False
|
||||||
|
if actor:
|
||||||
|
from shared.models.federation import APFollowing
|
||||||
|
existing = (
|
||||||
|
await session.execute(
|
||||||
|
sa_select(APFollowing).where(
|
||||||
|
APFollowing.actor_profile_id == actor.id,
|
||||||
|
APFollowing.remote_actor_id == actor_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).scalar_one_or_none()
|
||||||
|
is_following = existing is not None
|
||||||
|
return {
|
||||||
|
"remote_actor": _serialize_remote_actor(remote_dto),
|
||||||
|
"items": [_serialize_timeline_item(i) for i in items],
|
||||||
|
"is_following": is_following,
|
||||||
|
"actor": _serialize_actor(actor),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def notifications_data(self, session, **kw):
|
||||||
|
actor = _require_actor()
|
||||||
|
from shared.services.registry import services
|
||||||
|
items = await services.federation.get_notifications(session, actor.id)
|
||||||
|
await services.federation.mark_notifications_read(session, actor.id)
|
||||||
|
|
||||||
|
notif_dicts = []
|
||||||
|
for n in items:
|
||||||
|
created = getattr(n, "created_at", None)
|
||||||
|
notif_dicts.append({
|
||||||
|
"from_actor_name": getattr(n, "from_actor_name", "?"),
|
||||||
|
"from_actor_username": getattr(n, "from_actor_username", ""),
|
||||||
|
"from_actor_domain": getattr(n, "from_actor_domain", ""),
|
||||||
|
"from_actor_icon": getattr(n, "from_actor_icon", None),
|
||||||
|
"notification_type": getattr(n, "notification_type", ""),
|
||||||
|
"target_content_preview": getattr(n, "target_content_preview", None),
|
||||||
|
"created_at_formatted": created.strftime("%b %d, %H:%M") if created else "",
|
||||||
|
"read": getattr(n, "read", True),
|
||||||
|
"app_domain": getattr(n, "app_domain", ""),
|
||||||
|
})
|
||||||
|
return {"notifications": notif_dicts}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
"""Federation defpage setup — registers layouts, page helpers, and loads .sx pages."""
|
"""Federation defpage setup — registers layouts and loads .sx pages."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
def setup_federation_pages() -> None:
|
def setup_federation_pages() -> None:
|
||||||
"""Register federation-specific layouts, page helpers, and load page definitions."""
|
"""Register federation-specific layouts and load page definitions."""
|
||||||
_register_federation_layouts()
|
_register_federation_layouts()
|
||||||
_register_federation_helpers()
|
|
||||||
_load_federation_page_files()
|
_load_federation_page_files()
|
||||||
|
|
||||||
|
|
||||||
@@ -55,74 +54,25 @@ async def _social_oob(ctx: dict, **kw: Any) -> str:
|
|||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Page helpers
|
# Serializers and helpers — still used by layouts and route handlers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _register_federation_helpers() -> None:
|
|
||||||
from shared.sx.pages import register_page_helpers
|
|
||||||
|
|
||||||
register_page_helpers("federation", {
|
|
||||||
"home-timeline-content": _h_home_timeline_content,
|
|
||||||
"public-timeline-content": _h_public_timeline_content,
|
|
||||||
"compose-content": _h_compose_content,
|
|
||||||
"search-content": _h_search_content,
|
|
||||||
"following-content": _h_following_content,
|
|
||||||
"followers-content": _h_followers_content,
|
|
||||||
"actor-timeline-content": _h_actor_timeline_content,
|
|
||||||
"notifications-content": _h_notifications_content,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_actor(actor) -> dict | None:
|
def _serialize_actor(actor) -> dict | None:
|
||||||
"""Serialize an actor profile to a dict for sx defcomps."""
|
"""Serialize an actor profile to a dict for sx defcomps."""
|
||||||
if not actor:
|
from services.federation_page import _serialize_actor as _impl
|
||||||
return None
|
return _impl(actor)
|
||||||
return {
|
|
||||||
"id": actor.id,
|
|
||||||
"preferred_username": actor.preferred_username,
|
|
||||||
"display_name": getattr(actor, "display_name", None),
|
|
||||||
"icon_url": getattr(actor, "icon_url", None),
|
|
||||||
"summary": getattr(actor, "summary", None),
|
|
||||||
"actor_url": getattr(actor, "actor_url", ""),
|
|
||||||
"domain": getattr(actor, "domain", ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_timeline_item(item) -> dict:
|
def _serialize_timeline_item(item) -> dict:
|
||||||
"""Serialize a timeline item DTO to a dict for sx defcomps."""
|
"""Serialize a timeline item DTO to a dict for sx defcomps."""
|
||||||
published = getattr(item, "published", None)
|
from services.federation_page import _serialize_timeline_item as _impl
|
||||||
return {
|
return _impl(item)
|
||||||
"object_id": getattr(item, "object_id", "") or "",
|
|
||||||
"author_inbox": getattr(item, "author_inbox", "") or "",
|
|
||||||
"actor_icon": getattr(item, "actor_icon", None),
|
|
||||||
"actor_name": getattr(item, "actor_name", "?"),
|
|
||||||
"actor_username": getattr(item, "actor_username", ""),
|
|
||||||
"actor_domain": getattr(item, "actor_domain", ""),
|
|
||||||
"content": getattr(item, "content", ""),
|
|
||||||
"summary": getattr(item, "summary", None),
|
|
||||||
"published": published.strftime("%b %d, %H:%M") if published else "",
|
|
||||||
"before_cursor": published.isoformat() if published else "",
|
|
||||||
"url": getattr(item, "url", None),
|
|
||||||
"post_type": getattr(item, "post_type", ""),
|
|
||||||
"boosted_by": getattr(item, "boosted_by", None),
|
|
||||||
"like_count": getattr(item, "like_count", 0) or 0,
|
|
||||||
"boost_count": getattr(item, "boost_count", 0) or 0,
|
|
||||||
"liked_by_me": getattr(item, "liked_by_me", False),
|
|
||||||
"boosted_by_me": getattr(item, "boosted_by_me", False),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _serialize_remote_actor(a) -> dict:
|
def _serialize_remote_actor(a) -> dict:
|
||||||
"""Serialize a remote actor DTO to a dict for sx defcomps."""
|
"""Serialize a remote actor DTO to a dict for sx defcomps."""
|
||||||
return {
|
from services.federation_page import _serialize_remote_actor as _impl
|
||||||
"id": getattr(a, "id", None),
|
return _impl(a)
|
||||||
"display_name": getattr(a, "display_name", None) or getattr(a, "preferred_username", ""),
|
|
||||||
"preferred_username": getattr(a, "preferred_username", ""),
|
|
||||||
"domain": getattr(a, "domain", ""),
|
|
||||||
"icon_url": getattr(a, "icon_url", None),
|
|
||||||
"actor_url": getattr(a, "actor_url", ""),
|
|
||||||
"summary": getattr(a, "summary", None),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def _social_page(ctx: dict, actor, *, content: str,
|
async def _social_page(ctx: dict, actor, *, content: str,
|
||||||
@@ -155,156 +105,3 @@ def _require_actor():
|
|||||||
if not actor:
|
if not actor:
|
||||||
abort(403, "You need to choose a federation username first")
|
abort(403, "You need to choose a federation username first")
|
||||||
return actor
|
return actor
|
||||||
|
|
||||||
|
|
||||||
async def _h_home_timeline_content(**kw):
|
|
||||||
from quart import g
|
|
||||||
from shared.services.registry import services
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
actor = _require_actor()
|
|
||||||
items = await services.federation.get_home_timeline(g.s, actor.id)
|
|
||||||
return await render_to_sx("federation-timeline-content",
|
|
||||||
items=[_serialize_timeline_item(i) for i in items],
|
|
||||||
timeline_type="home",
|
|
||||||
actor=_serialize_actor(actor))
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_public_timeline_content(**kw):
|
|
||||||
from quart import g
|
|
||||||
from shared.services.registry import services
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
actor = _get_actor()
|
|
||||||
items = await services.federation.get_public_timeline(g.s)
|
|
||||||
return await render_to_sx("federation-timeline-content",
|
|
||||||
items=[_serialize_timeline_item(i) for i in items],
|
|
||||||
timeline_type="public",
|
|
||||||
actor=_serialize_actor(actor))
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_compose_content(**kw):
|
|
||||||
from quart import request
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
_require_actor()
|
|
||||||
reply_to = request.args.get("reply_to")
|
|
||||||
return await render_to_sx("federation-compose-content",
|
|
||||||
reply_to=reply_to or None)
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_search_content(**kw):
|
|
||||||
from quart import g, request
|
|
||||||
from shared.services.registry import services
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
actor = _get_actor()
|
|
||||||
query = request.args.get("q", "").strip()
|
|
||||||
actors_list = []
|
|
||||||
total = 0
|
|
||||||
followed_urls: set[str] = set()
|
|
||||||
if query:
|
|
||||||
actors_list, total = await services.federation.search_actors(g.s, query)
|
|
||||||
if actor:
|
|
||||||
following, _ = await services.federation.get_following(
|
|
||||||
g.s, actor.preferred_username, page=1, per_page=1000,
|
|
||||||
)
|
|
||||||
followed_urls = {a.actor_url for a in following}
|
|
||||||
return await render_to_sx("federation-search-content",
|
|
||||||
query=query,
|
|
||||||
actors=[_serialize_remote_actor(a) for a in actors_list],
|
|
||||||
total=total,
|
|
||||||
followed_urls=list(followed_urls),
|
|
||||||
actor=_serialize_actor(actor))
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_following_content(**kw):
|
|
||||||
from quart import g
|
|
||||||
from shared.services.registry import services
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
actor = _require_actor()
|
|
||||||
actors_list, total = await services.federation.get_following(
|
|
||||||
g.s, actor.preferred_username,
|
|
||||||
)
|
|
||||||
return await render_to_sx("federation-following-content",
|
|
||||||
actors=[_serialize_remote_actor(a) for a in actors_list],
|
|
||||||
total=total,
|
|
||||||
actor=_serialize_actor(actor))
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_followers_content(**kw):
|
|
||||||
from quart import g
|
|
||||||
from shared.services.registry import services
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
actor = _require_actor()
|
|
||||||
actors_list, total = await services.federation.get_followers_paginated(
|
|
||||||
g.s, actor.preferred_username,
|
|
||||||
)
|
|
||||||
following, _ = await services.federation.get_following(
|
|
||||||
g.s, actor.preferred_username, page=1, per_page=1000,
|
|
||||||
)
|
|
||||||
followed_urls = {a.actor_url for a in following}
|
|
||||||
return await render_to_sx("federation-followers-content",
|
|
||||||
actors=[_serialize_remote_actor(a) for a in actors_list],
|
|
||||||
total=total,
|
|
||||||
followed_urls=list(followed_urls),
|
|
||||||
actor=_serialize_actor(actor))
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_actor_timeline_content(id=None, **kw):
|
|
||||||
from quart import g, abort
|
|
||||||
from shared.services.registry import services
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
actor = _get_actor()
|
|
||||||
actor_id = id
|
|
||||||
from shared.models.federation import RemoteActor
|
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
remote = (
|
|
||||||
await g.s.execute(
|
|
||||||
sa_select(RemoteActor).where(RemoteActor.id == actor_id)
|
|
||||||
)
|
|
||||||
).scalar_one_or_none()
|
|
||||||
if not remote:
|
|
||||||
abort(404)
|
|
||||||
from shared.services.federation_impl import _remote_actor_to_dto
|
|
||||||
remote_dto = _remote_actor_to_dto(remote)
|
|
||||||
items = await services.federation.get_actor_timeline(g.s, actor_id)
|
|
||||||
is_following = False
|
|
||||||
if actor:
|
|
||||||
from shared.models.federation import APFollowing
|
|
||||||
existing = (
|
|
||||||
await g.s.execute(
|
|
||||||
sa_select(APFollowing).where(
|
|
||||||
APFollowing.actor_profile_id == actor.id,
|
|
||||||
APFollowing.remote_actor_id == actor_id,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
).scalar_one_or_none()
|
|
||||||
is_following = existing is not None
|
|
||||||
return await render_to_sx("federation-actor-timeline-content",
|
|
||||||
remote_actor=_serialize_remote_actor(remote_dto),
|
|
||||||
items=[_serialize_timeline_item(i) for i in items],
|
|
||||||
is_following=is_following,
|
|
||||||
actor=_serialize_actor(actor))
|
|
||||||
|
|
||||||
|
|
||||||
async def _h_notifications_content(**kw):
|
|
||||||
from quart import g
|
|
||||||
from shared.services.registry import services
|
|
||||||
from shared.sx.helpers import render_to_sx
|
|
||||||
actor = _require_actor()
|
|
||||||
items = await services.federation.get_notifications(g.s, actor.id)
|
|
||||||
await services.federation.mark_notifications_read(g.s, actor.id)
|
|
||||||
|
|
||||||
notif_dicts = []
|
|
||||||
for n in items:
|
|
||||||
created = getattr(n, "created_at", None)
|
|
||||||
notif_dicts.append({
|
|
||||||
"from_actor_name": getattr(n, "from_actor_name", "?"),
|
|
||||||
"from_actor_username": getattr(n, "from_actor_username", ""),
|
|
||||||
"from_actor_domain": getattr(n, "from_actor_domain", ""),
|
|
||||||
"from_actor_icon": getattr(n, "from_actor_icon", None),
|
|
||||||
"notification_type": getattr(n, "notification_type", ""),
|
|
||||||
"target_content_preview": getattr(n, "target_content_preview", None),
|
|
||||||
"created_at_formatted": created.strftime("%b %d, %H:%M") if created else "",
|
|
||||||
"read": getattr(n, "read", True),
|
|
||||||
"app_domain": getattr(n, "app_domain", ""),
|
|
||||||
})
|
|
||||||
return await render_to_sx("federation-notifications-content",
|
|
||||||
notifications=notif_dicts)
|
|
||||||
|
|||||||
@@ -1,49 +1,82 @@
|
|||||||
;; Federation social pages
|
;; Federation social pages
|
||||||
|
;; All data fetching via (service ...) IO primitives, no Python helpers.
|
||||||
|
|
||||||
(defpage home-timeline
|
(defpage home-timeline
|
||||||
:path "/social/"
|
:path "/social/"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (home-timeline-content))
|
:data (service "federation-page" "home-timeline-data")
|
||||||
|
:content (~federation-timeline-content
|
||||||
|
:items items
|
||||||
|
:timeline-type timeline-type
|
||||||
|
:actor actor))
|
||||||
|
|
||||||
(defpage public-timeline
|
(defpage public-timeline
|
||||||
:path "/social/public"
|
:path "/social/public"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (public-timeline-content))
|
:data (service "federation-page" "public-timeline-data")
|
||||||
|
:content (~federation-timeline-content
|
||||||
|
:items items
|
||||||
|
:timeline-type timeline-type
|
||||||
|
:actor actor))
|
||||||
|
|
||||||
(defpage compose-form
|
(defpage compose-form
|
||||||
:path "/social/compose"
|
:path "/social/compose"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (compose-content))
|
:data (service "federation-page" "compose-data")
|
||||||
|
:content (~federation-compose-content
|
||||||
|
:reply-to reply-to))
|
||||||
|
|
||||||
(defpage search
|
(defpage search
|
||||||
:path "/social/search"
|
:path "/social/search"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (search-content))
|
:data (service "federation-page" "search-data")
|
||||||
|
:content (~federation-search-content
|
||||||
|
:query query
|
||||||
|
:actors actors
|
||||||
|
:total total
|
||||||
|
:followed-urls followed-urls
|
||||||
|
:actor actor))
|
||||||
|
|
||||||
(defpage following-list
|
(defpage following-list
|
||||||
:path "/social/following"
|
:path "/social/following"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (following-content))
|
:data (service "federation-page" "following-data")
|
||||||
|
:content (~federation-following-content
|
||||||
|
:actors actors
|
||||||
|
:total total
|
||||||
|
:actor actor))
|
||||||
|
|
||||||
(defpage followers-list
|
(defpage followers-list
|
||||||
:path "/social/followers"
|
:path "/social/followers"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (followers-content))
|
:data (service "federation-page" "followers-data")
|
||||||
|
:content (~federation-followers-content
|
||||||
|
:actors actors
|
||||||
|
:total total
|
||||||
|
:followed-urls followed-urls
|
||||||
|
:actor actor))
|
||||||
|
|
||||||
(defpage actor-timeline
|
(defpage actor-timeline
|
||||||
:path "/social/actor/<int:id>"
|
:path "/social/actor/<int:id>"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (actor-timeline-content id))
|
:data (service "federation-page" "actor-timeline-data" :id id)
|
||||||
|
:content (~federation-actor-timeline-content
|
||||||
|
:remote-actor remote-actor
|
||||||
|
:items items
|
||||||
|
:is-following is-following
|
||||||
|
:actor actor))
|
||||||
|
|
||||||
(defpage notifications
|
(defpage notifications
|
||||||
:path "/social/notifications"
|
:path "/social/notifications"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (notifications-content))
|
:data (service "federation-page" "notifications-data")
|
||||||
|
:content (~federation-notifications-content
|
||||||
|
:notifications notifications))
|
||||||
|
|||||||
Reference in New Issue
Block a user