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:
|
||||
"""Register services for the account app.
|
||||
|
||||
Account is a consumer-only dashboard app. It has no own domain.
|
||||
All cross-app data comes via fragments and HTTP data endpoints.
|
||||
"""
|
||||
pass
|
||||
"""Register services for the account app."""
|
||||
from shared.services.registry import services
|
||||
from .account_page import AccountPageService
|
||||
services.register("account_page", AccountPageService())
|
||||
|
||||
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 typing import Any
|
||||
|
||||
|
||||
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_helpers()
|
||||
_load_account_page_files()
|
||||
|
||||
|
||||
@@ -90,50 +89,3 @@ def _as_sx_nav(ctx: dict) -> Any:
|
||||
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/"
|
||||
:auth :login
|
||||
:layout :account
|
||||
:data (newsletters-data)
|
||||
:data (service "account-page" "newsletters-data")
|
||||
:content (~account-newsletters-content
|
||||
:newsletter-list newsletter-list
|
||||
:account-url account-url))
|
||||
|
||||
@@ -12,3 +12,6 @@ def register_domain_services() -> None:
|
||||
from shared.services.cart_impl import 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 typing import Any
|
||||
@@ -8,9 +8,8 @@ from shared.sx.parser import SxExpr
|
||||
|
||||
|
||||
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_helpers()
|
||||
_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:
|
||||
@@ -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)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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.
|
||||
;; All data fetching via (service ...) IO primitives, no Python helpers.
|
||||
|
||||
(defpage cart-overview
|
||||
:path "/"
|
||||
:auth :public
|
||||
: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
|
||||
:path "/<page_slug>/"
|
||||
:auth :public
|
||||
: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
|
||||
:path "/<page_slug>/admin/"
|
||||
:auth :admin
|
||||
:layout :cart-admin
|
||||
:content (cart-admin-content))
|
||||
:content (~cart-admin-content))
|
||||
|
||||
(defpage cart-payments
|
||||
:path "/<page_slug>/admin/payments/"
|
||||
:auth :admin
|
||||
: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
|
||||
|
||||
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 typing import Any
|
||||
|
||||
|
||||
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_helpers()
|
||||
_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:
|
||||
"""Serialize an actor profile to a dict for sx defcomps."""
|
||||
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", ""),
|
||||
}
|
||||
from services.federation_page import _serialize_actor as _impl
|
||||
return _impl(actor)
|
||||
|
||||
|
||||
def _serialize_timeline_item(item) -> dict:
|
||||
"""Serialize a timeline item DTO to a dict for sx defcomps."""
|
||||
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),
|
||||
}
|
||||
from services.federation_page import _serialize_timeline_item as _impl
|
||||
return _impl(item)
|
||||
|
||||
|
||||
def _serialize_remote_actor(a) -> dict:
|
||||
"""Serialize a remote actor DTO to a dict for sx defcomps."""
|
||||
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),
|
||||
}
|
||||
from services.federation_page import _serialize_remote_actor as _impl
|
||||
return _impl(a)
|
||||
|
||||
|
||||
async def _social_page(ctx: dict, actor, *, content: str,
|
||||
@@ -155,156 +105,3 @@ def _require_actor():
|
||||
if not actor:
|
||||
abort(403, "You need to choose a federation username first")
|
||||
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
|
||||
;; All data fetching via (service ...) IO primitives, no Python helpers.
|
||||
|
||||
(defpage home-timeline
|
||||
:path "/social/"
|
||||
:auth :login
|
||||
: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
|
||||
:path "/social/public"
|
||||
:auth :public
|
||||
: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
|
||||
:path "/social/compose"
|
||||
:auth :login
|
||||
:layout :social
|
||||
:content (compose-content))
|
||||
:data (service "federation-page" "compose-data")
|
||||
:content (~federation-compose-content
|
||||
:reply-to reply-to))
|
||||
|
||||
(defpage search
|
||||
:path "/social/search"
|
||||
:auth :public
|
||||
: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
|
||||
:path "/social/following"
|
||||
:auth :login
|
||||
:layout :social
|
||||
:content (following-content))
|
||||
:data (service "federation-page" "following-data")
|
||||
:content (~federation-following-content
|
||||
:actors actors
|
||||
:total total
|
||||
:actor actor))
|
||||
|
||||
(defpage followers-list
|
||||
:path "/social/followers"
|
||||
:auth :login
|
||||
: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
|
||||
:path "/social/actor/<int:id>"
|
||||
:auth :public
|
||||
: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
|
||||
:path "/social/notifications"
|
||||
:auth :login
|
||||
:layout :social
|
||||
:content (notifications-content))
|
||||
:data (service "federation-page" "notifications-data")
|
||||
:content (~federation-notifications-content
|
||||
:notifications notifications))
|
||||
|
||||
Reference in New Issue
Block a user