Auto-mount defpages: eliminate Python route stubs across all 9 services
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 16s
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 16s
Defpages are now declared with absolute paths in .sx files and auto-mounted directly on the Quart app, removing ~850 lines of blueprint mount_pages calls, before_request hooks, and g.* wrapper boilerplate. A new page = one defpage declaration, nothing else. Infrastructure: - async_eval awaits coroutine results from callable dispatch - auto_mount_pages() mounts all registered defpages on the app - g._defpage_ctx pattern passes helper data to layout context Migrated: sx, account, orders, federation, cart, market, events, blog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -81,10 +81,11 @@ def create_app() -> "Quart":
|
|||||||
app.register_blueprint(register_auth_bp())
|
app.register_blueprint(register_auth_bp())
|
||||||
|
|
||||||
account_bp = register_account_bp()
|
account_bp = register_account_bp()
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(account_bp, "account")
|
|
||||||
app.register_blueprint(account_bp)
|
app.register_blueprint(account_bp)
|
||||||
|
|
||||||
|
from shared.sx.pages import auto_mount_pages
|
||||||
|
auto_mount_pages(app, "account")
|
||||||
|
|
||||||
app.register_blueprint(register_fragments())
|
app.register_blueprint(register_fragments())
|
||||||
|
|
||||||
from bp.actions.routes import register as register_actions
|
from bp.actions.routes import register as register_actions
|
||||||
|
|||||||
@@ -8,15 +8,12 @@ from __future__ import annotations
|
|||||||
from quart import (
|
from quart import (
|
||||||
Blueprint,
|
Blueprint,
|
||||||
request,
|
request,
|
||||||
redirect,
|
|
||||||
g,
|
g,
|
||||||
)
|
)
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
from shared.models import UserNewsletter
|
from shared.models import UserNewsletter
|
||||||
from shared.models.ghost_membership_entities import GhostNewsletter
|
from shared.infrastructure.fragments import fetch_fragments
|
||||||
from shared.infrastructure.urls import login_url
|
|
||||||
from shared.infrastructure.fragments import fetch_fragment, fetch_fragments
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
|
|
||||||
@@ -25,8 +22,7 @@ def register(url_prefix="/"):
|
|||||||
|
|
||||||
@account_bp.before_request
|
@account_bp.before_request
|
||||||
async def _prepare_page_data():
|
async def _prepare_page_data():
|
||||||
"""Fetch account_nav fragments and load data for defpage routes."""
|
"""Fetch account_nav fragments for layout."""
|
||||||
# Fetch account nav items for layout (was in context_processor)
|
|
||||||
events_nav, cart_nav, artdag_nav = await fetch_fragments([
|
events_nav, cart_nav, artdag_nav = await fetch_fragments([
|
||||||
("events", "account-nav-item", {}),
|
("events", "account-nav-item", {}),
|
||||||
("cart", "account-nav-item", {}),
|
("cart", "account-nav-item", {}),
|
||||||
@@ -34,48 +30,6 @@ def register(url_prefix="/"):
|
|||||||
], required=False)
|
], required=False)
|
||||||
g.account_nav = events_nav + cart_nav + artdag_nav
|
g.account_nav = events_nav + cart_nav + artdag_nav
|
||||||
|
|
||||||
if request.method != "GET":
|
|
||||||
return
|
|
||||||
|
|
||||||
endpoint = request.endpoint or ""
|
|
||||||
|
|
||||||
# Newsletters page — load newsletter data
|
|
||||||
if endpoint.endswith("defpage_newsletters"):
|
|
||||||
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": nl,
|
|
||||||
"un": un,
|
|
||||||
"subscribed": un.subscribed if un else False,
|
|
||||||
})
|
|
||||||
g.newsletters_data = newsletter_list
|
|
||||||
|
|
||||||
# Fragment page — load fragment from events service
|
|
||||||
elif endpoint.endswith("defpage_fragment_page"):
|
|
||||||
slug = request.view_args.get("slug")
|
|
||||||
if slug and g.get("user"):
|
|
||||||
fragment_html = await fetch_fragment(
|
|
||||||
"events", "account-page",
|
|
||||||
params={"slug": slug, "user_id": str(g.user.id)},
|
|
||||||
)
|
|
||||||
if not fragment_html:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
g.fragment_page_data = fragment_html
|
|
||||||
|
|
||||||
@account_bp.post("/newsletter/<int:newsletter_id>/toggle/")
|
@account_bp.post("/newsletter/<int:newsletter_id>/toggle/")
|
||||||
async def toggle_newsletter(newsletter_id: int):
|
async def toggle_newsletter(newsletter_id: int):
|
||||||
if not g.get("user"):
|
if not g.get("user"):
|
||||||
|
|||||||
@@ -75,31 +75,60 @@ def _register_account_helpers() -> None:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def _h_account_content():
|
def _h_account_content(**kw):
|
||||||
from sx.sx_components import _account_main_panel_sx
|
from sx.sx_components import _account_main_panel_sx
|
||||||
return _account_main_panel_sx({})
|
return _account_main_panel_sx({})
|
||||||
|
|
||||||
|
|
||||||
def _h_newsletters_content():
|
async def _h_newsletters_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
d = getattr(g, "newsletters_data", None)
|
from sqlalchemy import select
|
||||||
if not d:
|
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": nl,
|
||||||
|
"un": un,
|
||||||
|
"subscribed": un.subscribed if un else False,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not newsletter_list:
|
||||||
from shared.sx.helpers import sx_call
|
from shared.sx.helpers import sx_call
|
||||||
return sx_call("account-newsletter-empty")
|
return sx_call("account-newsletter-empty")
|
||||||
from shared.sx.page import get_template_context_sync
|
|
||||||
from sx.sx_components import _newsletters_panel_sx
|
from sx.sx_components import _newsletters_panel_sx
|
||||||
# Build a minimal ctx with account_url
|
|
||||||
ctx = {"account_url": getattr(g, "_account_url", None)}
|
ctx = {"account_url": getattr(g, "_account_url", None)}
|
||||||
if ctx["account_url"] is None:
|
if ctx["account_url"] is None:
|
||||||
from shared.infrastructure.urls import account_url
|
from shared.infrastructure.urls import account_url
|
||||||
ctx["account_url"] = account_url
|
ctx["account_url"] = account_url
|
||||||
return _newsletters_panel_sx(ctx, d)
|
return _newsletters_panel_sx(ctx, newsletter_list)
|
||||||
|
|
||||||
|
|
||||||
def _h_fragment_content():
|
async def _h_fragment_content(slug=None, **kw):
|
||||||
from quart import g
|
from quart import g, abort
|
||||||
frag = getattr(g, "fragment_page_data", None)
|
from shared.infrastructure.fragments import fetch_fragment
|
||||||
if not frag:
|
|
||||||
|
if not slug or not g.get("user"):
|
||||||
return ""
|
return ""
|
||||||
|
fragment_html = await fetch_fragment(
|
||||||
|
"events", "account-page",
|
||||||
|
params={"slug": slug, "user_id": str(g.user.id)},
|
||||||
|
)
|
||||||
|
if not fragment_html:
|
||||||
|
abort(404)
|
||||||
from sx.sx_components import _fragment_content
|
from sx.sx_components import _fragment_content
|
||||||
return _fragment_content(frag)
|
return _fragment_content(fragment_html)
|
||||||
|
|||||||
@@ -28,4 +28,4 @@
|
|||||||
:path "/<slug>/"
|
:path "/<slug>/"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :account
|
:layout :account
|
||||||
:content (fragment-content))
|
:content (fragment-content slug))
|
||||||
|
|||||||
17
blog/app.py
17
blog/app.py
@@ -162,6 +162,23 @@ def create_app() -> "Quart":
|
|||||||
)
|
)
|
||||||
return jsonify(resp)
|
return jsonify(resp)
|
||||||
|
|
||||||
|
# Auto-mount all defpages with absolute paths
|
||||||
|
from shared.sx.pages import auto_mount_pages
|
||||||
|
auto_mount_pages(app, "blog")
|
||||||
|
|
||||||
|
# --- Pass defpage helper data to template context for layouts ---
|
||||||
|
@app.context_processor
|
||||||
|
async def inject_blog_data():
|
||||||
|
import os
|
||||||
|
from shared.config import config as get_config
|
||||||
|
ctx = {
|
||||||
|
"blog_title": get_config()["blog_title"],
|
||||||
|
"base_title": get_config()["title"],
|
||||||
|
"unsplash_api_key": os.environ.get("UNSPLASH_ACCESS_KEY", ""),
|
||||||
|
}
|
||||||
|
ctx.update(getattr(g, '_defpage_ctx', {}))
|
||||||
|
return ctx
|
||||||
|
|
||||||
# --- debug: url rules ---
|
# --- debug: url rules ---
|
||||||
@app.get("/__rules")
|
@app.get("/__rules")
|
||||||
async def dump_rules():
|
async def dump_rules():
|
||||||
|
|||||||
@@ -3,13 +3,9 @@ from __future__ import annotations
|
|||||||
#from quart import Blueprint, g
|
#from quart import Blueprint, g
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
render_template,
|
|
||||||
make_response,
|
|
||||||
Blueprint,
|
Blueprint,
|
||||||
redirect,
|
redirect,
|
||||||
url_for,
|
url_for,
|
||||||
request,
|
|
||||||
jsonify
|
|
||||||
)
|
)
|
||||||
from shared.browser.app.redis_cacher import clear_all_cache
|
from shared.browser.app.redis_cacher import clear_all_cache
|
||||||
from shared.browser.app.authz import require_admin
|
from shared.browser.app.authz import require_admin
|
||||||
@@ -27,23 +23,6 @@ def register(url_prefix):
|
|||||||
"base_title": f"{config()['title']} settings",
|
"base_title": f"{config()['title']} settings",
|
||||||
}
|
}
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
ep = request.endpoint or ""
|
|
||||||
if "defpage_settings_home" in ep:
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _settings_main_panel_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
g.settings_content = _settings_main_panel_sx(tctx)
|
|
||||||
elif "defpage_cache_page" in ep:
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _cache_main_panel_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
g.cache_content = _cache_main_panel_sx(tctx)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "blog", names=["settings-home", "cache-page"])
|
|
||||||
|
|
||||||
@bp.post("/cache_clear/")
|
@bp.post("/cache_clear/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def cache_clear():
|
async def cache_clear():
|
||||||
@@ -54,7 +33,7 @@ def register(url_prefix):
|
|||||||
html = render_comp("cache-cleared", time_str=now.strftime("%H:%M:%S"))
|
html = render_comp("cache-cleared", time_str=now.strftime("%H:%M:%S"))
|
||||||
return sx_response(html)
|
return sx_response(html)
|
||||||
|
|
||||||
return redirect(url_for("settings.defpage_cache_page"))
|
return redirect(url_for("defpage_cache_page"))
|
||||||
return bp
|
return bp
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import re
|
import re
|
||||||
from quart import (
|
from quart import (
|
||||||
render_template,
|
|
||||||
make_response,
|
|
||||||
Blueprint,
|
Blueprint,
|
||||||
redirect,
|
redirect,
|
||||||
url_for,
|
url_for,
|
||||||
@@ -13,9 +11,7 @@ from quart import (
|
|||||||
from sqlalchemy import select, delete
|
from sqlalchemy import select, delete
|
||||||
|
|
||||||
from shared.browser.app.authz import require_admin
|
from shared.browser.app.authz import require_admin
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.browser.app.redis_cacher import invalidate_tag_cache
|
from shared.browser.app.redis_cacher import invalidate_tag_cache
|
||||||
from shared.sx.helpers import sx_response
|
|
||||||
|
|
||||||
from models.tag_group import TagGroup, TagGroupTag
|
from models.tag_group import TagGroup, TagGroupTag
|
||||||
from models.ghost_content import Tag
|
from models.ghost_content import Tag
|
||||||
@@ -46,60 +42,13 @@ async def _unassigned_tags(session):
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("tag_groups_admin", __name__, url_prefix="/settings/tag-groups")
|
bp = Blueprint("tag_groups_admin", __name__, url_prefix="/settings/tag-groups")
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
ep = request.endpoint or ""
|
|
||||||
if "defpage_tag_groups_page" in ep:
|
|
||||||
groups = list(
|
|
||||||
(await g.s.execute(
|
|
||||||
select(TagGroup).order_by(TagGroup.sort_order, TagGroup.name)
|
|
||||||
)).scalars()
|
|
||||||
)
|
|
||||||
unassigned = await _unassigned_tags(g.s)
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _tag_groups_main_panel_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx.update({"groups": groups, "unassigned_tags": unassigned})
|
|
||||||
g.tag_groups_content = _tag_groups_main_panel_sx(tctx)
|
|
||||||
elif "defpage_tag_group_edit" in ep:
|
|
||||||
tag_id = (request.view_args or {}).get("id")
|
|
||||||
tg = await g.s.get(TagGroup, tag_id)
|
|
||||||
if not tg:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
assigned_rows = list(
|
|
||||||
(await g.s.execute(
|
|
||||||
select(TagGroupTag.tag_id).where(TagGroupTag.tag_group_id == tag_id)
|
|
||||||
)).scalars()
|
|
||||||
)
|
|
||||||
all_tags = list(
|
|
||||||
(await g.s.execute(
|
|
||||||
select(Tag).where(
|
|
||||||
Tag.deleted_at.is_(None),
|
|
||||||
(Tag.visibility == "public") | (Tag.visibility.is_(None)),
|
|
||||||
).order_by(Tag.name)
|
|
||||||
)).scalars()
|
|
||||||
)
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _tag_groups_edit_main_panel_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx.update({
|
|
||||||
"group": tg,
|
|
||||||
"all_tags": all_tags,
|
|
||||||
"assigned_tag_ids": set(assigned_rows),
|
|
||||||
})
|
|
||||||
g.tag_group_edit_content = _tag_groups_edit_main_panel_sx(tctx)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "blog", names=["tag-groups-page", "tag-group-edit"])
|
|
||||||
|
|
||||||
@bp.post("/")
|
@bp.post("/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def create():
|
async def create():
|
||||||
form = await request.form
|
form = await request.form
|
||||||
name = (form.get("name") or "").strip()
|
name = (form.get("name") or "").strip()
|
||||||
if not name:
|
if not name:
|
||||||
return redirect(url_for("blog.tag_groups_admin.defpage_tag_groups_page"))
|
return redirect(url_for("defpage_tag_groups_page"))
|
||||||
|
|
||||||
slug = _slugify(name)
|
slug = _slugify(name)
|
||||||
feature_image = (form.get("feature_image") or "").strip() or None
|
feature_image = (form.get("feature_image") or "").strip() or None
|
||||||
@@ -115,14 +64,14 @@ def register():
|
|||||||
await g.s.flush()
|
await g.s.flush()
|
||||||
|
|
||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
return redirect(url_for("blog.tag_groups_admin.defpage_tag_groups_page"))
|
return redirect(url_for("defpage_tag_groups_page"))
|
||||||
|
|
||||||
@bp.post("/<int:id>/")
|
@bp.post("/<int:id>/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def save(id: int):
|
async def save(id: int):
|
||||||
tg = await g.s.get(TagGroup, id)
|
tg = await g.s.get(TagGroup, id)
|
||||||
if not tg:
|
if not tg:
|
||||||
return redirect(url_for("blog.tag_groups_admin.defpage_tag_groups_page"))
|
return redirect(url_for("defpage_tag_groups_page"))
|
||||||
|
|
||||||
form = await request.form
|
form = await request.form
|
||||||
name = (form.get("name") or "").strip()
|
name = (form.get("name") or "").strip()
|
||||||
@@ -153,7 +102,7 @@ def register():
|
|||||||
await g.s.flush()
|
await g.s.flush()
|
||||||
|
|
||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
return redirect(url_for("blog.tag_groups_admin.defpage_tag_group_edit", id=id))
|
return redirect(url_for("defpage_tag_group_edit", id=id))
|
||||||
|
|
||||||
@bp.post("/<int:id>/delete/")
|
@bp.post("/<int:id>/delete/")
|
||||||
@require_admin
|
@require_admin
|
||||||
@@ -163,6 +112,6 @@ def register():
|
|||||||
await g.s.delete(tg)
|
await g.s.delete(tg)
|
||||||
await g.s.flush()
|
await g.s.flush()
|
||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
return redirect(url_for("blog.tag_groups_admin.defpage_tag_groups_page"))
|
return redirect(url_for("defpage_tag_groups_page"))
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -53,16 +53,6 @@ def register(url_prefix, title):
|
|||||||
@blogs_bp.before_request
|
@blogs_bp.before_request
|
||||||
async def route():
|
async def route():
|
||||||
g.makeqs_factory = makeqs_factory
|
g.makeqs_factory = makeqs_factory
|
||||||
ep = request.endpoint or ""
|
|
||||||
if "defpage_new_post" in ep:
|
|
||||||
from sx.sx_components import render_editor_panel
|
|
||||||
g.editor_content = render_editor_panel()
|
|
||||||
elif "defpage_new_page" in ep:
|
|
||||||
from sx.sx_components import render_editor_panel
|
|
||||||
g.editor_page_content = render_editor_panel(is_page=True)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(blogs_bp, "blog", names=["new-post", "new-page"])
|
|
||||||
|
|
||||||
@blogs_bp.context_processor
|
@blogs_bp.context_processor
|
||||||
async def inject_root():
|
async def inject_root():
|
||||||
@@ -277,7 +267,7 @@ def register(url_prefix, title):
|
|||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
|
|
||||||
# Redirect to the edit page
|
# Redirect to the edit page
|
||||||
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=post.slug)))
|
return redirect(host_url(url_for("defpage_post_edit", slug=post.slug)))
|
||||||
|
|
||||||
|
|
||||||
@blogs_bp.post("/new-page/")
|
@blogs_bp.post("/new-page/")
|
||||||
@@ -335,7 +325,7 @@ def register(url_prefix, title):
|
|||||||
await invalidate_tag_cache("blog")
|
await invalidate_tag_cache("blog")
|
||||||
|
|
||||||
# Redirect to the page admin
|
# Redirect to the page admin
|
||||||
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=page.slug)))
|
return redirect(host_url(url_for("defpage_post_edit", slug=page.slug)))
|
||||||
|
|
||||||
|
|
||||||
@blogs_bp.get("/drafts/")
|
@blogs_bp.get("/drafts/")
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from .services.menu_items import (
|
|||||||
search_pages,
|
search_pages,
|
||||||
MenuItemError,
|
MenuItemError,
|
||||||
)
|
)
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
@@ -23,20 +22,6 @@ def register():
|
|||||||
from sx.sx_components import render_menu_items_nav_oob
|
from sx.sx_components import render_menu_items_nav_oob
|
||||||
return render_menu_items_nav_oob(menu_items)
|
return render_menu_items_nav_oob(menu_items)
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
menu_items = await get_all_menu_items(g.s)
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _menu_items_main_panel_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["menu_items"] = menu_items
|
|
||||||
g.menu_items_content = _menu_items_main_panel_sx(tctx)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "blog", names=["menu-items-page"])
|
|
||||||
|
|
||||||
@bp.get("/new/")
|
@bp.get("/new/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def new_menu_item():
|
async def new_menu_item():
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from quart import (
|
|||||||
url_for,
|
url_for,
|
||||||
)
|
)
|
||||||
from shared.browser.app.authz import require_admin, require_post_author
|
from shared.browser.app.authz import require_admin, require_post_author
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from shared.utils import host_url
|
from shared.utils import host_url
|
||||||
|
|
||||||
@@ -55,155 +54,6 @@ def _post_to_edit_dict(post) -> dict:
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
ep = request.endpoint or ""
|
|
||||||
if "defpage_post_admin" in ep:
|
|
||||||
from sqlalchemy import select
|
|
||||||
from shared.models.page_config import PageConfig
|
|
||||||
post = (g.post_data or {}).get("post", {})
|
|
||||||
features = {}
|
|
||||||
sumup_configured = False
|
|
||||||
sumup_merchant_code = ""
|
|
||||||
sumup_checkout_prefix = ""
|
|
||||||
if post.get("is_page"):
|
|
||||||
pc = (await g.s.execute(
|
|
||||||
select(PageConfig).where(
|
|
||||||
PageConfig.container_type == "page",
|
|
||||||
PageConfig.container_id == post["id"],
|
|
||||||
)
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
if pc:
|
|
||||||
features = pc.features or {}
|
|
||||||
sumup_configured = bool(pc.sumup_api_key)
|
|
||||||
sumup_merchant_code = pc.sumup_merchant_code or ""
|
|
||||||
sumup_checkout_prefix = pc.sumup_checkout_prefix or ""
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _post_admin_main_panel_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx.update({
|
|
||||||
"features": features,
|
|
||||||
"sumup_configured": sumup_configured,
|
|
||||||
"sumup_merchant_code": sumup_merchant_code,
|
|
||||||
"sumup_checkout_prefix": sumup_checkout_prefix,
|
|
||||||
})
|
|
||||||
g.post_admin_content = _post_admin_main_panel_sx(tctx)
|
|
||||||
|
|
||||||
elif "defpage_post_data" in ep:
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _post_data_content_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
g.post_data_content = _post_data_content_sx(tctx)
|
|
||||||
|
|
||||||
elif "defpage_post_preview" in ep:
|
|
||||||
from models.ghost_content import Post
|
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
post_id = g.post_data["post"]["id"]
|
|
||||||
post = (await g.s.execute(
|
|
||||||
sa_select(Post).where(Post.id == post_id)
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
preview_ctx = {}
|
|
||||||
sx_content = getattr(post, "sx_content", None) or ""
|
|
||||||
if sx_content:
|
|
||||||
from shared.sx.prettify import sx_to_pretty_sx
|
|
||||||
preview_ctx["sx_pretty"] = sx_to_pretty_sx(sx_content)
|
|
||||||
lexical_raw = getattr(post, "lexical", None) or ""
|
|
||||||
if lexical_raw:
|
|
||||||
from shared.sx.prettify import json_to_pretty_sx
|
|
||||||
preview_ctx["json_pretty"] = json_to_pretty_sx(lexical_raw)
|
|
||||||
if sx_content:
|
|
||||||
from shared.sx.parser import parse as sx_parse
|
|
||||||
from shared.sx.html import render as sx_html_render
|
|
||||||
from shared.sx.jinja_bridge import _COMPONENT_ENV
|
|
||||||
try:
|
|
||||||
parsed = sx_parse(sx_content)
|
|
||||||
preview_ctx["sx_rendered"] = sx_html_render(parsed, dict(_COMPONENT_ENV))
|
|
||||||
except Exception:
|
|
||||||
preview_ctx["sx_rendered"] = "<em>Error rendering sx</em>"
|
|
||||||
if lexical_raw:
|
|
||||||
from bp.blog.ghost.lexical_renderer import render_lexical
|
|
||||||
try:
|
|
||||||
preview_ctx["lex_rendered"] = render_lexical(lexical_raw)
|
|
||||||
except Exception:
|
|
||||||
preview_ctx["lex_rendered"] = "<em>Error rendering lexical</em>"
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _preview_main_panel_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx.update(preview_ctx)
|
|
||||||
g.post_preview_content = _preview_main_panel_sx(tctx)
|
|
||||||
|
|
||||||
elif "defpage_post_entries" in ep:
|
|
||||||
from sqlalchemy import select
|
|
||||||
from shared.models.calendars import Calendar
|
|
||||||
from ..services.entry_associations import get_post_entry_ids
|
|
||||||
post_id = g.post_data["post"]["id"]
|
|
||||||
associated_entry_ids = await get_post_entry_ids(post_id)
|
|
||||||
result = await g.s.execute(
|
|
||||||
select(Calendar)
|
|
||||||
.where(Calendar.deleted_at.is_(None))
|
|
||||||
.order_by(Calendar.name.asc())
|
|
||||||
)
|
|
||||||
all_calendars = result.scalars().all()
|
|
||||||
for calendar in all_calendars:
|
|
||||||
await g.s.refresh(calendar, ["entries", "post"])
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _post_entries_content_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["all_calendars"] = all_calendars
|
|
||||||
tctx["associated_entry_ids"] = associated_entry_ids
|
|
||||||
g.post_entries_content = _post_entries_content_sx(tctx)
|
|
||||||
|
|
||||||
elif "defpage_post_settings" in ep:
|
|
||||||
from models.ghost_content import Post
|
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
post_id = g.post_data["post"]["id"]
|
|
||||||
post = (await g.s.execute(
|
|
||||||
sa_select(Post)
|
|
||||||
.where(Post.id == post_id)
|
|
||||||
.options(selectinload(Post.tags))
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
ghost_post = _post_to_edit_dict(post) if post else {}
|
|
||||||
save_success = request.args.get("saved") == "1"
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _post_settings_content_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["ghost_post"] = ghost_post
|
|
||||||
tctx["save_success"] = save_success
|
|
||||||
g.post_settings_content = _post_settings_content_sx(tctx)
|
|
||||||
|
|
||||||
elif "defpage_post_edit" in ep:
|
|
||||||
from models.ghost_content import Post
|
|
||||||
from sqlalchemy import select as sa_select
|
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
from shared.infrastructure.data_client import fetch_data
|
|
||||||
post_id = g.post_data["post"]["id"]
|
|
||||||
post = (await g.s.execute(
|
|
||||||
sa_select(Post)
|
|
||||||
.where(Post.id == post_id)
|
|
||||||
.options(selectinload(Post.tags))
|
|
||||||
)).scalar_one_or_none()
|
|
||||||
ghost_post = _post_to_edit_dict(post) if post else {}
|
|
||||||
save_success = request.args.get("saved") == "1"
|
|
||||||
save_error = request.args.get("error", "")
|
|
||||||
raw_newsletters = await fetch_data("account", "newsletters", required=False) or []
|
|
||||||
from types import SimpleNamespace
|
|
||||||
newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters]
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _post_edit_content_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["ghost_post"] = ghost_post
|
|
||||||
tctx["save_success"] = save_success
|
|
||||||
tctx["save_error"] = save_error
|
|
||||||
tctx["newsletters"] = newsletters
|
|
||||||
g.post_edit_content = _post_edit_content_sx(tctx)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "blog", names=[
|
|
||||||
"post-admin", "post-data", "post-preview",
|
|
||||||
"post-entries", "post-settings", "post-edit",
|
|
||||||
])
|
|
||||||
|
|
||||||
@bp.put("/features/")
|
@bp.put("/features/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def update_features(slug: str):
|
async def update_features(slug: str):
|
||||||
@@ -468,7 +318,7 @@ def register():
|
|||||||
except OptimisticLockError:
|
except OptimisticLockError:
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote
|
||||||
return redirect(
|
return redirect(
|
||||||
host_url(url_for("blog.post.admin.defpage_post_settings", slug=slug))
|
host_url(url_for("defpage_post_settings", slug=slug))
|
||||||
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
|
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -479,7 +329,7 @@ def register():
|
|||||||
await invalidate_tag_cache("post.post_detail")
|
await invalidate_tag_cache("post.post_detail")
|
||||||
|
|
||||||
# Redirect using the (possibly new) slug
|
# Redirect using the (possibly new) slug
|
||||||
return redirect(host_url(url_for("blog.post.admin.defpage_post_settings", slug=post.slug)) + "?saved=1")
|
return redirect(host_url(url_for("defpage_post_settings", slug=post.slug)) + "?saved=1")
|
||||||
|
|
||||||
@bp.post("/edit/")
|
@bp.post("/edit/")
|
||||||
@require_post_author
|
@require_post_author
|
||||||
@@ -504,11 +354,11 @@ def register():
|
|||||||
try:
|
try:
|
||||||
lexical_doc = json.loads(lexical_raw)
|
lexical_doc = json.loads(lexical_raw)
|
||||||
except (json.JSONDecodeError, TypeError):
|
except (json.JSONDecodeError, TypeError):
|
||||||
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=slug)) + "?error=" + quote("Invalid JSON in editor content."))
|
return redirect(host_url(url_for("defpage_post_edit", slug=slug)) + "?error=" + quote("Invalid JSON in editor content."))
|
||||||
|
|
||||||
ok, reason = validate_lexical(lexical_doc)
|
ok, reason = validate_lexical(lexical_doc)
|
||||||
if not ok:
|
if not ok:
|
||||||
return redirect(host_url(url_for("blog.post.admin.defpage_post_edit", slug=slug)) + "?error=" + quote(reason))
|
return redirect(host_url(url_for("defpage_post_edit", slug=slug)) + "?error=" + quote(reason))
|
||||||
|
|
||||||
# Publish workflow
|
# Publish workflow
|
||||||
is_admin = bool((g.get("rights") or {}).get("admin"))
|
is_admin = bool((g.get("rights") or {}).get("admin"))
|
||||||
@@ -544,7 +394,7 @@ def register():
|
|||||||
)
|
)
|
||||||
except OptimisticLockError:
|
except OptimisticLockError:
|
||||||
return redirect(
|
return redirect(
|
||||||
host_url(url_for("blog.post.admin.defpage_post_edit", slug=slug))
|
host_url(url_for("defpage_post_edit", slug=slug))
|
||||||
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
|
+ "?error=" + quote("Someone else edited this post. Please reload and try again.")
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -560,7 +410,7 @@ def register():
|
|||||||
await invalidate_tag_cache("post.post_detail")
|
await invalidate_tag_cache("post.post_detail")
|
||||||
|
|
||||||
# Redirect to GET (PRG pattern) — use post.slug in case it changed
|
# Redirect to GET (PRG pattern) — use post.slug in case it changed
|
||||||
redirect_url = host_url(url_for("blog.post.admin.defpage_post_edit", slug=post.slug)) + "?saved=1"
|
redirect_url = host_url(url_for("defpage_post_edit", slug=post.slug)) + "?saved=1"
|
||||||
if publish_requested_msg:
|
if publish_requested_msg:
|
||||||
redirect_url += "&publish_requested=1"
|
redirect_url += "&publish_requested=1"
|
||||||
return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, make_response, request, g, abort
|
from quart import Blueprint, request, g, abort
|
||||||
from sqlalchemy import select, or_
|
from sqlalchemy import select, or_
|
||||||
from sqlalchemy.orm import selectinload
|
|
||||||
|
|
||||||
from shared.browser.app.authz import require_login
|
from shared.browser.app.authz import require_login
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
from models import Snippet
|
from models import Snippet
|
||||||
|
|
||||||
@@ -32,22 +30,6 @@ async def _visible_snippets(session):
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("snippets", __name__, url_prefix="/settings/snippets")
|
bp = Blueprint("snippets", __name__, url_prefix="/settings/snippets")
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
snippets = await _visible_snippets(g.s)
|
|
||||||
is_admin = g.rights.get("admin")
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _snippets_main_panel_sx
|
|
||||||
tctx = await get_template_context()
|
|
||||||
tctx["snippets"] = snippets
|
|
||||||
tctx["is_admin"] = is_admin
|
|
||||||
g.snippets_content = _snippets_main_panel_sx(tctx)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "blog", names=["snippets-page"])
|
|
||||||
|
|
||||||
@bp.delete("/<int:snippet_id>/")
|
@bp.delete("/<int:snippet_id>/")
|
||||||
@require_login
|
@require_login
|
||||||
async def delete_snippet(snippet_id: int):
|
async def delete_snippet(snippet_id: int):
|
||||||
|
|||||||
@@ -17,6 +17,96 @@ def _load_blog_page_files() -> None:
|
|||||||
load_page_dir(os.path.dirname(__file__), "blog")
|
load_page_dir(os.path.dirname(__file__), "blog")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared hydration helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _add_to_defpage_ctx(**kwargs: Any) -> None:
|
||||||
|
from quart import g
|
||||||
|
if not hasattr(g, '_defpage_ctx'):
|
||||||
|
g._defpage_ctx = {}
|
||||||
|
g._defpage_ctx.update(kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_post_data(slug: str | None) -> None:
|
||||||
|
"""Load post data and set g.post_data + defpage context.
|
||||||
|
|
||||||
|
Replicates post bp's hydrate_post_data + context_processor.
|
||||||
|
"""
|
||||||
|
from quart import g, abort
|
||||||
|
|
||||||
|
if hasattr(g, 'post_data') and g.post_data:
|
||||||
|
await _inject_post_context(g.post_data)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not slug:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
from bp.post.services.post_data import post_data
|
||||||
|
|
||||||
|
is_admin = bool((g.get("rights") or {}).get("admin"))
|
||||||
|
p_data = await post_data(slug, g.s, include_drafts=True)
|
||||||
|
if not p_data:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Draft access control
|
||||||
|
if p_data["post"].get("status") != "published":
|
||||||
|
if is_admin:
|
||||||
|
pass
|
||||||
|
elif g.user and p_data["post"].get("user_id") == g.user.id:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
g.post_data = p_data
|
||||||
|
g.post_slug = slug
|
||||||
|
await _inject_post_context(p_data)
|
||||||
|
|
||||||
|
|
||||||
|
async def _inject_post_context(p_data: dict) -> None:
|
||||||
|
"""Add post context_processor data to defpage context."""
|
||||||
|
from shared.config import config
|
||||||
|
from shared.infrastructure.fragments import fetch_fragment
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import CartSummaryDTO, dto_from_dict
|
||||||
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
|
||||||
|
db_post_id = p_data["post"]["id"]
|
||||||
|
post_slug = p_data["post"]["slug"]
|
||||||
|
|
||||||
|
container_nav = await fetch_fragment("relations", "container-nav", params={
|
||||||
|
"container_type": "page",
|
||||||
|
"container_id": str(db_post_id),
|
||||||
|
"post_slug": post_slug,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx: dict = {
|
||||||
|
**p_data,
|
||||||
|
"base_title": config()["title"],
|
||||||
|
"container_nav": container_nav,
|
||||||
|
}
|
||||||
|
|
||||||
|
if p_data["post"].get("is_page"):
|
||||||
|
ident = current_cart_identity()
|
||||||
|
summary_params: dict = {"page_slug": post_slug}
|
||||||
|
if ident["user_id"] is not None:
|
||||||
|
summary_params["user_id"] = ident["user_id"]
|
||||||
|
if ident["session_id"] is not None:
|
||||||
|
summary_params["session_id"] = ident["session_id"]
|
||||||
|
raw_summary = await fetch_data(
|
||||||
|
"cart", "cart-summary", params=summary_params, required=False,
|
||||||
|
)
|
||||||
|
page_summary = dto_from_dict(CartSummaryDTO, raw_summary) if raw_summary else CartSummaryDTO()
|
||||||
|
ctx["page_cart_count"] = (
|
||||||
|
page_summary.count + page_summary.calendar_count + page_summary.ticket_count
|
||||||
|
)
|
||||||
|
ctx["page_cart_total"] = float(
|
||||||
|
page_summary.total + page_summary.calendar_total + page_summary.ticket_total
|
||||||
|
)
|
||||||
|
|
||||||
|
_add_to_defpage_ctx(**ctx)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Layouts
|
# Layouts
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -110,48 +200,48 @@ def _sub_settings_oob(ctx: dict, row_id: str, child_id: str,
|
|||||||
|
|
||||||
def _cache_full(ctx: dict, **kw: Any) -> str:
|
def _cache_full(ctx: dict, **kw: Any) -> str:
|
||||||
return _sub_settings_full(ctx, "cache-row", "cache-header-child",
|
return _sub_settings_full(ctx, "cache-row", "cache-header-child",
|
||||||
"settings.defpage_cache_page", "refresh", "Cache")
|
"defpage_cache_page", "refresh", "Cache")
|
||||||
|
|
||||||
|
|
||||||
def _cache_oob(ctx: dict, **kw: Any) -> str:
|
def _cache_oob(ctx: dict, **kw: Any) -> str:
|
||||||
return _sub_settings_oob(ctx, "cache-row", "cache-header-child",
|
return _sub_settings_oob(ctx, "cache-row", "cache-header-child",
|
||||||
"settings.defpage_cache_page", "refresh", "Cache")
|
"defpage_cache_page", "refresh", "Cache")
|
||||||
|
|
||||||
|
|
||||||
# --- Snippets ---
|
# --- Snippets ---
|
||||||
|
|
||||||
def _snippets_full(ctx: dict, **kw: Any) -> str:
|
def _snippets_full(ctx: dict, **kw: Any) -> str:
|
||||||
return _sub_settings_full(ctx, "snippets-row", "snippets-header-child",
|
return _sub_settings_full(ctx, "snippets-row", "snippets-header-child",
|
||||||
"snippets.defpage_snippets_page", "puzzle-piece", "Snippets")
|
"defpage_snippets_page", "puzzle-piece", "Snippets")
|
||||||
|
|
||||||
|
|
||||||
def _snippets_oob(ctx: dict, **kw: Any) -> str:
|
def _snippets_oob(ctx: dict, **kw: Any) -> str:
|
||||||
return _sub_settings_oob(ctx, "snippets-row", "snippets-header-child",
|
return _sub_settings_oob(ctx, "snippets-row", "snippets-header-child",
|
||||||
"snippets.defpage_snippets_page", "puzzle-piece", "Snippets")
|
"defpage_snippets_page", "puzzle-piece", "Snippets")
|
||||||
|
|
||||||
|
|
||||||
# --- Menu Items ---
|
# --- Menu Items ---
|
||||||
|
|
||||||
def _menu_items_full(ctx: dict, **kw: Any) -> str:
|
def _menu_items_full(ctx: dict, **kw: Any) -> str:
|
||||||
return _sub_settings_full(ctx, "menu_items-row", "menu_items-header-child",
|
return _sub_settings_full(ctx, "menu_items-row", "menu_items-header-child",
|
||||||
"menu_items.defpage_menu_items_page", "bars", "Menu Items")
|
"defpage_menu_items_page", "bars", "Menu Items")
|
||||||
|
|
||||||
|
|
||||||
def _menu_items_oob(ctx: dict, **kw: Any) -> str:
|
def _menu_items_oob(ctx: dict, **kw: Any) -> str:
|
||||||
return _sub_settings_oob(ctx, "menu_items-row", "menu_items-header-child",
|
return _sub_settings_oob(ctx, "menu_items-row", "menu_items-header-child",
|
||||||
"menu_items.defpage_menu_items_page", "bars", "Menu Items")
|
"defpage_menu_items_page", "bars", "Menu Items")
|
||||||
|
|
||||||
|
|
||||||
# --- Tag Groups ---
|
# --- Tag Groups ---
|
||||||
|
|
||||||
def _tag_groups_full(ctx: dict, **kw: Any) -> str:
|
def _tag_groups_full(ctx: dict, **kw: Any) -> str:
|
||||||
return _sub_settings_full(ctx, "tag-groups-row", "tag-groups-header-child",
|
return _sub_settings_full(ctx, "tag-groups-row", "tag-groups-header-child",
|
||||||
"blog.tag_groups_admin.defpage_tag_groups_page", "tags", "Tag Groups")
|
"defpage_tag_groups_page", "tags", "Tag Groups")
|
||||||
|
|
||||||
|
|
||||||
def _tag_groups_oob(ctx: dict, **kw: Any) -> str:
|
def _tag_groups_oob(ctx: dict, **kw: Any) -> str:
|
||||||
return _sub_settings_oob(ctx, "tag-groups-row", "tag-groups-header-child",
|
return _sub_settings_oob(ctx, "tag-groups-row", "tag-groups-header-child",
|
||||||
"blog.tag_groups_admin.defpage_tag_groups_page", "tags", "Tag Groups")
|
"defpage_tag_groups_page", "tags", "Tag Groups")
|
||||||
|
|
||||||
|
|
||||||
# --- Tag Group Edit ---
|
# --- Tag Group Edit ---
|
||||||
@@ -165,7 +255,7 @@ def _tag_group_edit_full(ctx: dict, **kw: Any) -> str:
|
|||||||
root_hdr = root_header_sx(ctx)
|
root_hdr = root_header_sx(ctx)
|
||||||
settings_hdr = _settings_header_sx(ctx)
|
settings_hdr = _settings_header_sx(ctx)
|
||||||
sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
|
sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
|
||||||
qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id),
|
qurl("defpage_tag_group_edit", id=g_id),
|
||||||
"tags", "Tag Groups", ctx)
|
"tags", "Tag Groups", ctx)
|
||||||
return "(<> " + root_hdr + " " + settings_hdr + " " + sub_hdr + ")"
|
return "(<> " + root_hdr + " " + settings_hdr + " " + sub_hdr + ")"
|
||||||
|
|
||||||
@@ -178,14 +268,14 @@ def _tag_group_edit_oob(ctx: dict, **kw: Any) -> str:
|
|||||||
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
|
from sx.sx_components import _settings_header_sx, _sub_settings_header_sx
|
||||||
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
|
settings_hdr_oob = _settings_header_sx(ctx, oob=True)
|
||||||
sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
|
sub_hdr = _sub_settings_header_sx("tag-groups-row", "tag-groups-header-child",
|
||||||
qurl("blog.tag_groups_admin.defpage_tag_group_edit", id=g_id),
|
qurl("defpage_tag_group_edit", id=g_id),
|
||||||
"tags", "Tag Groups", ctx)
|
"tags", "Tag Groups", ctx)
|
||||||
sub_oob = oob_header_sx("root-settings-header-child", "tag-groups-header-child", sub_hdr)
|
sub_oob = oob_header_sx("root-settings-header-child", "tag-groups-header-child", sub_hdr)
|
||||||
return "(<> " + settings_hdr_oob + " " + sub_oob + ")"
|
return "(<> " + settings_hdr_oob + " " + sub_oob + ")"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Page helpers (sync functions available in .sx defpage expressions)
|
# Page helpers (async functions available in .sx defpage expressions)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _register_blog_helpers() -> None:
|
def _register_blog_helpers() -> None:
|
||||||
@@ -208,71 +298,277 @@ def _register_blog_helpers() -> None:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def _h_editor_content():
|
# --- Editor helpers ---
|
||||||
|
|
||||||
|
async def _h_editor_content(**kw):
|
||||||
|
from sx.sx_components import render_editor_panel
|
||||||
|
return render_editor_panel()
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_editor_page_content(**kw):
|
||||||
|
from sx.sx_components import render_editor_panel
|
||||||
|
return render_editor_panel(is_page=True)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Post admin helpers ---
|
||||||
|
|
||||||
|
async def _h_post_admin_content(slug=None, **kw):
|
||||||
|
await _ensure_post_data(slug)
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "editor_content", "")
|
from sqlalchemy import select
|
||||||
|
from shared.models.page_config import PageConfig
|
||||||
|
post = (g.post_data or {}).get("post", {})
|
||||||
|
features = {}
|
||||||
|
sumup_configured = False
|
||||||
|
sumup_merchant_code = ""
|
||||||
|
sumup_checkout_prefix = ""
|
||||||
|
if post.get("is_page"):
|
||||||
|
pc = (await g.s.execute(
|
||||||
|
select(PageConfig).where(
|
||||||
|
PageConfig.container_type == "page",
|
||||||
|
PageConfig.container_id == post["id"],
|
||||||
|
)
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
if pc:
|
||||||
|
features = pc.features or {}
|
||||||
|
sumup_configured = bool(pc.sumup_api_key)
|
||||||
|
sumup_merchant_code = pc.sumup_merchant_code or ""
|
||||||
|
sumup_checkout_prefix = pc.sumup_checkout_prefix or ""
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _post_admin_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx.update({
|
||||||
|
"features": features,
|
||||||
|
"sumup_configured": sumup_configured,
|
||||||
|
"sumup_merchant_code": sumup_merchant_code,
|
||||||
|
"sumup_checkout_prefix": sumup_checkout_prefix,
|
||||||
|
})
|
||||||
|
return _post_admin_main_panel_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
def _h_editor_page_content():
|
async def _h_post_data_content(slug=None, **kw):
|
||||||
|
await _ensure_post_data(slug)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _post_data_content_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
return _post_data_content_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_post_preview_content(slug=None, **kw):
|
||||||
|
await _ensure_post_data(slug)
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "editor_page_content", "")
|
from models.ghost_content import Post
|
||||||
|
from sqlalchemy import select as sa_select
|
||||||
|
post_id = g.post_data["post"]["id"]
|
||||||
|
post = (await g.s.execute(
|
||||||
|
sa_select(Post).where(Post.id == post_id)
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
preview_ctx: dict = {}
|
||||||
|
sx_content = getattr(post, "sx_content", None) or ""
|
||||||
|
if sx_content:
|
||||||
|
from shared.sx.prettify import sx_to_pretty_sx
|
||||||
|
preview_ctx["sx_pretty"] = sx_to_pretty_sx(sx_content)
|
||||||
|
lexical_raw = getattr(post, "lexical", None) or ""
|
||||||
|
if lexical_raw:
|
||||||
|
from shared.sx.prettify import json_to_pretty_sx
|
||||||
|
preview_ctx["json_pretty"] = json_to_pretty_sx(lexical_raw)
|
||||||
|
if sx_content:
|
||||||
|
from shared.sx.parser import parse as sx_parse
|
||||||
|
from shared.sx.html import render as sx_html_render
|
||||||
|
from shared.sx.jinja_bridge import _COMPONENT_ENV
|
||||||
|
try:
|
||||||
|
parsed = sx_parse(sx_content)
|
||||||
|
preview_ctx["sx_rendered"] = sx_html_render(parsed, dict(_COMPONENT_ENV))
|
||||||
|
except Exception:
|
||||||
|
preview_ctx["sx_rendered"] = "<em>Error rendering sx</em>"
|
||||||
|
if lexical_raw:
|
||||||
|
from bp.blog.ghost.lexical_renderer import render_lexical
|
||||||
|
try:
|
||||||
|
preview_ctx["lex_rendered"] = render_lexical(lexical_raw)
|
||||||
|
except Exception:
|
||||||
|
preview_ctx["lex_rendered"] = "<em>Error rendering lexical</em>"
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _preview_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx.update(preview_ctx)
|
||||||
|
return _preview_main_panel_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
def _h_post_admin_content():
|
async def _h_post_entries_content(slug=None, **kw):
|
||||||
|
await _ensure_post_data(slug)
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "post_admin_content", "")
|
from sqlalchemy import select
|
||||||
|
from shared.models.calendars import Calendar
|
||||||
|
from bp.post.services.entry_associations import get_post_entry_ids
|
||||||
|
post_id = g.post_data["post"]["id"]
|
||||||
|
associated_entry_ids = await get_post_entry_ids(post_id)
|
||||||
|
result = await g.s.execute(
|
||||||
|
select(Calendar)
|
||||||
|
.where(Calendar.deleted_at.is_(None))
|
||||||
|
.order_by(Calendar.name.asc())
|
||||||
|
)
|
||||||
|
all_calendars = result.scalars().all()
|
||||||
|
for calendar in all_calendars:
|
||||||
|
await g.s.refresh(calendar, ["entries", "post"])
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _post_entries_content_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx["all_calendars"] = all_calendars
|
||||||
|
tctx["associated_entry_ids"] = associated_entry_ids
|
||||||
|
return _post_entries_content_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
def _h_post_data_content():
|
async def _h_post_settings_content(slug=None, **kw):
|
||||||
|
await _ensure_post_data(slug)
|
||||||
|
from quart import g, request
|
||||||
|
from models.ghost_content import Post
|
||||||
|
from sqlalchemy import select as sa_select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from bp.post.admin.routes import _post_to_edit_dict
|
||||||
|
post_id = g.post_data["post"]["id"]
|
||||||
|
post = (await g.s.execute(
|
||||||
|
sa_select(Post)
|
||||||
|
.where(Post.id == post_id)
|
||||||
|
.options(selectinload(Post.tags))
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
ghost_post = _post_to_edit_dict(post) if post else {}
|
||||||
|
save_success = request.args.get("saved") == "1"
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _post_settings_content_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx["ghost_post"] = ghost_post
|
||||||
|
tctx["save_success"] = save_success
|
||||||
|
return _post_settings_content_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_post_edit_content(slug=None, **kw):
|
||||||
|
await _ensure_post_data(slug)
|
||||||
|
from quart import g, request
|
||||||
|
from models.ghost_content import Post
|
||||||
|
from sqlalchemy import select as sa_select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from bp.post.admin.routes import _post_to_edit_dict
|
||||||
|
post_id = g.post_data["post"]["id"]
|
||||||
|
post = (await g.s.execute(
|
||||||
|
sa_select(Post)
|
||||||
|
.where(Post.id == post_id)
|
||||||
|
.options(selectinload(Post.tags))
|
||||||
|
)).scalar_one_or_none()
|
||||||
|
ghost_post = _post_to_edit_dict(post) if post else {}
|
||||||
|
save_success = request.args.get("saved") == "1"
|
||||||
|
save_error = request.args.get("error", "")
|
||||||
|
raw_newsletters = await fetch_data("account", "newsletters", required=False) or []
|
||||||
|
from types import SimpleNamespace
|
||||||
|
newsletters = [SimpleNamespace(**nl) for nl in raw_newsletters]
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _post_edit_content_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx["ghost_post"] = ghost_post
|
||||||
|
tctx["save_success"] = save_success
|
||||||
|
tctx["save_error"] = save_error
|
||||||
|
tctx["newsletters"] = newsletters
|
||||||
|
return _post_edit_content_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Settings helpers ---
|
||||||
|
|
||||||
|
async def _h_settings_content(**kw):
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _settings_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
return _settings_main_panel_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_cache_content(**kw):
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _cache_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
return _cache_main_panel_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Snippets helper ---
|
||||||
|
|
||||||
|
async def _h_snippets_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "post_data_content", "")
|
from sqlalchemy import select, or_
|
||||||
|
from models import Snippet
|
||||||
|
uid = g.user.id
|
||||||
|
is_admin = g.rights.get("admin")
|
||||||
|
filters = [Snippet.user_id == uid, Snippet.visibility == "shared"]
|
||||||
|
if is_admin:
|
||||||
|
filters.append(Snippet.visibility == "admin")
|
||||||
|
rows = (await g.s.execute(
|
||||||
|
select(Snippet).where(or_(*filters)).order_by(Snippet.name)
|
||||||
|
)).scalars().all()
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _snippets_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx["snippets"] = rows
|
||||||
|
tctx["is_admin"] = is_admin
|
||||||
|
return _snippets_main_panel_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
def _h_post_preview_content():
|
# --- Menu Items helper ---
|
||||||
|
|
||||||
|
async def _h_menu_items_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "post_preview_content", "")
|
from bp.menu_items.services.menu_items import get_all_menu_items
|
||||||
|
menu_items = await get_all_menu_items(g.s)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _menu_items_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx["menu_items"] = menu_items
|
||||||
|
return _menu_items_main_panel_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
def _h_post_entries_content():
|
# --- Tag Groups helpers ---
|
||||||
|
|
||||||
|
async def _h_tag_groups_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "post_entries_content", "")
|
from sqlalchemy import select
|
||||||
|
from models.tag_group import TagGroup
|
||||||
|
from bp.blog.admin.routes import _unassigned_tags
|
||||||
|
groups = list(
|
||||||
|
(await g.s.execute(
|
||||||
|
select(TagGroup).order_by(TagGroup.sort_order, TagGroup.name)
|
||||||
|
)).scalars()
|
||||||
|
)
|
||||||
|
unassigned = await _unassigned_tags(g.s)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _tag_groups_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx.update({"groups": groups, "unassigned_tags": unassigned})
|
||||||
|
return _tag_groups_main_panel_sx(tctx)
|
||||||
|
|
||||||
|
|
||||||
def _h_post_settings_content():
|
async def _h_tag_group_edit_content(id=None, **kw):
|
||||||
from quart import g
|
from quart import g, abort
|
||||||
return getattr(g, "post_settings_content", "")
|
from sqlalchemy import select
|
||||||
|
from models.tag_group import TagGroup, TagGroupTag
|
||||||
|
from models.ghost_content import Tag
|
||||||
def _h_post_edit_content():
|
tg = await g.s.get(TagGroup, id)
|
||||||
from quart import g
|
if not tg:
|
||||||
return getattr(g, "post_edit_content", "")
|
abort(404)
|
||||||
|
assigned_rows = list(
|
||||||
|
(await g.s.execute(
|
||||||
def _h_settings_content():
|
select(TagGroupTag.tag_id).where(TagGroupTag.tag_group_id == id)
|
||||||
from quart import g
|
)).scalars()
|
||||||
return getattr(g, "settings_content", "")
|
)
|
||||||
|
all_tags = list(
|
||||||
|
(await g.s.execute(
|
||||||
def _h_cache_content():
|
select(Tag).where(
|
||||||
from quart import g
|
Tag.deleted_at.is_(None),
|
||||||
return getattr(g, "cache_content", "")
|
(Tag.visibility == "public") | (Tag.visibility.is_(None)),
|
||||||
|
).order_by(Tag.name)
|
||||||
|
)).scalars()
|
||||||
def _h_snippets_content():
|
)
|
||||||
from quart import g
|
from shared.sx.page import get_template_context
|
||||||
return getattr(g, "snippets_content", "")
|
from sx.sx_components import _tag_groups_edit_main_panel_sx
|
||||||
|
tctx = await get_template_context()
|
||||||
|
tctx.update({
|
||||||
def _h_menu_items_content():
|
"group": tg,
|
||||||
from quart import g
|
"all_tags": all_tags,
|
||||||
return getattr(g, "menu_items_content", "")
|
"assigned_tag_ids": set(assigned_rows),
|
||||||
|
})
|
||||||
|
return _tag_groups_edit_main_panel_sx(tctx)
|
||||||
def _h_tag_groups_content():
|
|
||||||
from quart import g
|
|
||||||
return getattr(g, "tag_groups_content", "")
|
|
||||||
|
|
||||||
|
|
||||||
def _h_tag_group_edit_content():
|
|
||||||
from quart import g
|
|
||||||
return getattr(g, "tag_group_edit_content", "")
|
|
||||||
|
|||||||
@@ -15,54 +15,54 @@
|
|||||||
:layout :blog
|
:layout :blog
|
||||||
:content (editor-page-content))
|
:content (editor-page-content))
|
||||||
|
|
||||||
; --- Post admin pages (nested under /<slug>/admin/) ---
|
; --- Post admin pages (absolute paths under /<slug>/admin/) ---
|
||||||
|
|
||||||
(defpage post-admin
|
(defpage post-admin
|
||||||
:path "/"
|
:path "/<slug>/admin/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout (:post-admin :selected "admin")
|
:layout (:post-admin :selected "admin")
|
||||||
:content (post-admin-content))
|
:content (post-admin-content slug))
|
||||||
|
|
||||||
(defpage post-data
|
(defpage post-data
|
||||||
:path "/data/"
|
:path "/<slug>/admin/data/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout (:post-admin :selected "data")
|
:layout (:post-admin :selected "data")
|
||||||
:content (post-data-content))
|
:content (post-data-content slug))
|
||||||
|
|
||||||
(defpage post-preview
|
(defpage post-preview
|
||||||
:path "/preview/"
|
:path "/<slug>/admin/preview/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout (:post-admin :selected "preview")
|
:layout (:post-admin :selected "preview")
|
||||||
:content (post-preview-content))
|
:content (post-preview-content slug))
|
||||||
|
|
||||||
(defpage post-entries
|
(defpage post-entries
|
||||||
:path "/entries/"
|
:path "/<slug>/admin/entries/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout (:post-admin :selected "entries")
|
:layout (:post-admin :selected "entries")
|
||||||
:content (post-entries-content))
|
:content (post-entries-content slug))
|
||||||
|
|
||||||
(defpage post-settings
|
(defpage post-settings
|
||||||
:path "/settings/"
|
:path "/<slug>/admin/settings/"
|
||||||
:auth :post_author
|
:auth :post_author
|
||||||
:layout (:post-admin :selected "settings")
|
:layout (:post-admin :selected "settings")
|
||||||
:content (post-settings-content))
|
:content (post-settings-content slug))
|
||||||
|
|
||||||
(defpage post-edit
|
(defpage post-edit
|
||||||
:path "/edit/"
|
:path "/<slug>/admin/edit/"
|
||||||
:auth :post_author
|
:auth :post_author
|
||||||
:layout (:post-admin :selected "edit")
|
:layout (:post-admin :selected "edit")
|
||||||
:content (post-edit-content))
|
:content (post-edit-content slug))
|
||||||
|
|
||||||
; --- Settings pages ---
|
; --- Settings pages (absolute paths) ---
|
||||||
|
|
||||||
(defpage settings-home
|
(defpage settings-home
|
||||||
:path "/"
|
:path "/settings/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :blog-settings
|
:layout :blog-settings
|
||||||
:content (settings-content))
|
:content (settings-content))
|
||||||
|
|
||||||
(defpage cache-page
|
(defpage cache-page
|
||||||
:path "/cache/"
|
:path "/settings/cache/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :blog-cache
|
:layout :blog-cache
|
||||||
:content (cache-content))
|
:content (cache-content))
|
||||||
@@ -70,7 +70,7 @@
|
|||||||
; --- Snippets ---
|
; --- Snippets ---
|
||||||
|
|
||||||
(defpage snippets-page
|
(defpage snippets-page
|
||||||
:path "/"
|
:path "/settings/snippets/"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :blog-snippets
|
:layout :blog-snippets
|
||||||
:content (snippets-content))
|
:content (snippets-content))
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
; --- Menu Items ---
|
; --- Menu Items ---
|
||||||
|
|
||||||
(defpage menu-items-page
|
(defpage menu-items-page
|
||||||
:path "/"
|
:path "/settings/menu_items/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :blog-menu-items
|
:layout :blog-menu-items
|
||||||
:content (menu-items-content))
|
:content (menu-items-content))
|
||||||
@@ -86,13 +86,13 @@
|
|||||||
; --- Tag Groups ---
|
; --- Tag Groups ---
|
||||||
|
|
||||||
(defpage tag-groups-page
|
(defpage tag-groups-page
|
||||||
:path "/"
|
:path "/settings/tag-groups/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :blog-tag-groups
|
:layout :blog-tag-groups
|
||||||
:content (tag-groups-content))
|
:content (tag-groups-content))
|
||||||
|
|
||||||
(defpage tag-group-edit
|
(defpage tag-group-edit
|
||||||
:path "/<int:id>/"
|
:path "/settings/tag-groups/<int:id>/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :blog-tag-group-edit
|
:layout :blog-tag-group-edit
|
||||||
:content (tag-group-edit-content))
|
:content (tag-group-edit-content id))
|
||||||
|
|||||||
15
cart/app.py
15
cart/app.py
@@ -185,8 +185,6 @@ def create_app() -> "Quart":
|
|||||||
from sxc.pages import setup_cart_pages
|
from sxc.pages import setup_cart_pages
|
||||||
setup_cart_pages()
|
setup_cart_pages()
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
|
|
||||||
# --- Blueprint registration ---
|
# --- Blueprint registration ---
|
||||||
# Static prefixes first, dynamic (page_slug) last
|
# Static prefixes first, dynamic (page_slug) last
|
||||||
|
|
||||||
@@ -196,21 +194,22 @@ def create_app() -> "Quart":
|
|||||||
url_prefix="/",
|
url_prefix="/",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cart overview at GET /
|
# Cart overview blueprint (no defpage routes, just action endpoints)
|
||||||
overview_bp = register_cart_overview(url_prefix="/")
|
overview_bp = register_cart_overview(url_prefix="/")
|
||||||
mount_pages(overview_bp, "cart", names=["cart-overview"])
|
|
||||||
app.register_blueprint(overview_bp, url_prefix="/")
|
app.register_blueprint(overview_bp, url_prefix="/")
|
||||||
|
|
||||||
# Page admin at /<page_slug>/admin/ (before page_cart catch-all)
|
# Page admin (PUT /payments/ etc.)
|
||||||
admin_bp = register_page_admin()
|
admin_bp = register_page_admin()
|
||||||
mount_pages(admin_bp, "cart", names=["cart-admin", "cart-payments"])
|
|
||||||
app.register_blueprint(admin_bp, url_prefix="/<page_slug>/admin")
|
app.register_blueprint(admin_bp, url_prefix="/<page_slug>/admin")
|
||||||
|
|
||||||
# Page cart at /<page_slug>/ (dynamic, matched last)
|
# Page cart (POST /checkout/ etc.)
|
||||||
page_cart_bp = register_page_cart(url_prefix="/")
|
page_cart_bp = register_page_cart(url_prefix="/")
|
||||||
mount_pages(page_cart_bp, "cart", names=["page-cart-view"])
|
|
||||||
app.register_blueprint(page_cart_bp, url_prefix="/<page_slug>")
|
app.register_blueprint(page_cart_bp, url_prefix="/<page_slug>")
|
||||||
|
|
||||||
|
# Auto-mount all defpages with absolute paths
|
||||||
|
from shared.sx.pages import auto_mount_pages
|
||||||
|
auto_mount_pages(app, "cart")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
|
|
||||||
if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true":
|
if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true":
|
||||||
# Redirect to overview for HTMX
|
# Redirect to overview for HTMX
|
||||||
return redirect(url_for("cart_overview.defpage_cart_overview"))
|
return redirect(url_for("defpage_cart_overview"))
|
||||||
|
|
||||||
return redirect(url_for("cart_overview.defpage_cart_overview"))
|
return redirect(url_for("defpage_cart_overview"))
|
||||||
|
|
||||||
@bp.post("/quantity/<int:product_id>/")
|
@bp.post("/quantity/<int:product_id>/")
|
||||||
async def update_quantity(product_id: int):
|
async def update_quantity(product_id: int):
|
||||||
@@ -137,7 +137,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
tickets = await get_ticket_cart_entries(g.s)
|
tickets = await get_ticket_cart_entries(g.s)
|
||||||
|
|
||||||
if not cart and not calendar_entries and not tickets:
|
if not cart and not calendar_entries and not tickets:
|
||||||
return redirect(url_for("cart_overview.defpage_cart_overview"))
|
return redirect(url_for("defpage_cart_overview"))
|
||||||
|
|
||||||
product_total = total(cart) or 0
|
product_total = total(cart) or 0
|
||||||
calendar_amount = calendar_total(calendar_entries) or 0
|
calendar_amount = calendar_total(calendar_entries) or 0
|
||||||
@@ -145,7 +145,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
cart_total = product_total + calendar_amount + ticket_amount
|
cart_total = product_total + calendar_amount + ticket_amount
|
||||||
|
|
||||||
if cart_total <= 0:
|
if cart_total <= 0:
|
||||||
return redirect(url_for("cart_overview.defpage_cart_overview"))
|
return redirect(url_for("defpage_cart_overview"))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
page_config = await resolve_page_config(g.s, cart, calendar_entries, tickets)
|
page_config = await resolve_page_config(g.s, cart, calendar_entries, tickets)
|
||||||
|
|||||||
@@ -3,24 +3,9 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, request
|
from quart import Blueprint
|
||||||
|
|
||||||
from .services import get_cart_grouped_by_page
|
|
||||||
|
|
||||||
|
|
||||||
def register(url_prefix: str) -> Blueprint:
|
def register(url_prefix: str) -> Blueprint:
|
||||||
bp = Blueprint("cart_overview", __name__, url_prefix=url_prefix)
|
bp = Blueprint("cart_overview", __name__, url_prefix=url_prefix)
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
"""Load overview data for defpage route."""
|
|
||||||
endpoint = request.endpoint or ""
|
|
||||||
if not endpoint.endswith("defpage_cart_overview"):
|
|
||||||
return
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _overview_main_panel_sx
|
|
||||||
page_groups = await get_cart_grouped_by_page(g.s)
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.overview_content = _overview_main_panel_sx(page_groups, ctx)
|
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -19,26 +19,6 @@ from .services import current_cart_identity
|
|||||||
def register(url_prefix: str) -> Blueprint:
|
def register(url_prefix: str) -> Blueprint:
|
||||||
bp = Blueprint("page_cart", __name__, url_prefix=url_prefix)
|
bp = Blueprint("page_cart", __name__, url_prefix=url_prefix)
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
"""Load page cart data for defpage route."""
|
|
||||||
endpoint = request.endpoint or ""
|
|
||||||
if not endpoint.endswith("defpage_page_cart_view"):
|
|
||||||
return
|
|
||||||
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)
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _page_cart_main_panel_sx
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.page_cart_content = _page_cart_main_panel_sx(
|
|
||||||
ctx, cart, cal_entries, page_tickets, ticket_groups,
|
|
||||||
total, calendar_total, ticket_total,
|
|
||||||
)
|
|
||||||
|
|
||||||
@bp.post("/checkout/")
|
@bp.post("/checkout/")
|
||||||
async def page_checkout():
|
async def page_checkout():
|
||||||
post = g.page_post
|
post = g.page_post
|
||||||
@@ -48,7 +28,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
page_tickets = await get_tickets_for_page(g.s, post.id)
|
page_tickets = await get_tickets_for_page(g.s, post.id)
|
||||||
|
|
||||||
if not cart and not cal_entries and not page_tickets:
|
if not cart and not cal_entries and not page_tickets:
|
||||||
return redirect(url_for("page_cart.defpage_page_cart_view"))
|
return redirect(url_for("defpage_page_cart_view"))
|
||||||
|
|
||||||
product_total_val = total(cart) or 0
|
product_total_val = total(cart) or 0
|
||||||
calendar_amount = calendar_total(cal_entries) or 0
|
calendar_amount = calendar_total(cal_entries) or 0
|
||||||
@@ -56,7 +36,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
cart_total = product_total_val + calendar_amount + ticket_amount
|
cart_total = product_total_val + calendar_amount + ticket_amount
|
||||||
|
|
||||||
if cart_total <= 0:
|
if cart_total <= 0:
|
||||||
return redirect(url_for("page_cart.defpage_page_cart_view"))
|
return redirect(url_for("defpage_page_cart_view"))
|
||||||
|
|
||||||
ident = current_cart_identity()
|
ident = current_cart_identity()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, render_template, redirect, url_for, make_response
|
from quart import Blueprint, g, redirect, url_for, make_response
|
||||||
from sqlalchemy import select, func, or_, cast, String, exists
|
from sqlalchemy import select, func, or_, cast, String, exists
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
|||||||
@@ -13,23 +13,6 @@ from shared.sx.helpers import sx_response
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("page_admin", __name__)
|
bp = Blueprint("page_admin", __name__)
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
"""Pre-render admin content for defpage routes."""
|
|
||||||
endpoint = request.endpoint or ""
|
|
||||||
if request.method != "GET":
|
|
||||||
return
|
|
||||||
if endpoint.endswith("defpage_cart_admin"):
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _cart_admin_main_panel_sx
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.cart_admin_content = _cart_admin_main_panel_sx(ctx)
|
|
||||||
elif endpoint.endswith("defpage_cart_payments"):
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _cart_payments_main_panel_sx
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.cart_payments_content = _cart_payments_main_panel_sx(ctx)
|
|
||||||
|
|
||||||
@bp.put("/payments/")
|
@bp.put("/payments/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def update_sumup(**kwargs):
|
async def update_sumup(**kwargs):
|
||||||
|
|||||||
@@ -771,7 +771,7 @@ def _cart_page_admin_header_sx(ctx: dict, page_post: Any, *, oob: bool = False,
|
|||||||
def _cart_admin_main_panel_sx(ctx: dict) -> str:
|
def _cart_admin_main_panel_sx(ctx: dict) -> str:
|
||||||
"""Admin overview panel -- links to sub-admin pages."""
|
"""Admin overview panel -- links to sub-admin pages."""
|
||||||
from quart import url_for
|
from quart import url_for
|
||||||
payments_href = url_for("page_admin.defpage_cart_payments")
|
payments_href = url_for("defpage_cart_payments")
|
||||||
return (
|
return (
|
||||||
'(div :id "main-panel"'
|
'(div :id "main-panel"'
|
||||||
' (div :class "flex items-center justify-between p-3 border-b"'
|
' (div :class "flex items-center justify-between p-3 border-b"'
|
||||||
|
|||||||
@@ -90,32 +90,48 @@ def _register_cart_helpers() -> None:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def _h_overview_content():
|
async def _h_overview_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
page_groups = getattr(g, "overview_page_groups", [])
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import _overview_main_panel_sx
|
from sx.sx_components import _overview_main_panel_sx
|
||||||
# _overview_main_panel_sx needs ctx for url helpers — use g-based approach
|
from bp.cart.services import get_cart_grouped_by_page
|
||||||
# The function reads cart_url from ctx, which we can get from template context
|
page_groups = await get_cart_grouped_by_page(g.s)
|
||||||
from shared.sx.page import get_template_context
|
ctx = await get_template_context()
|
||||||
import asyncio
|
return _overview_main_panel_sx(page_groups, ctx)
|
||||||
# Page helpers are sync — we pre-compute in before_request
|
|
||||||
return getattr(g, "overview_content", "")
|
|
||||||
|
|
||||||
|
|
||||||
def _h_page_cart_content():
|
async def _h_page_cart_content(page_slug=None, **kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "page_cart_content", "")
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _page_cart_main_panel_sx
|
||||||
|
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()
|
||||||
|
return _page_cart_main_panel_sx(
|
||||||
|
ctx, cart, cal_entries, page_tickets, ticket_groups,
|
||||||
|
total, calendar_total, ticket_total,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _h_cart_admin_content():
|
async def _h_cart_admin_content(page_slug=None, **kw):
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
from sx.sx_components import _cart_admin_main_panel_sx
|
from sx.sx_components import _cart_admin_main_panel_sx
|
||||||
|
ctx = await get_template_context()
|
||||||
|
return _cart_admin_main_panel_sx(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_cart_payments_content(page_slug=None, **kw):
|
||||||
from shared.sx.page import get_template_context
|
from shared.sx.page import get_template_context
|
||||||
# Sync helper — _cart_admin_main_panel_sx is sync, but needs ctx
|
from sx.sx_components import _cart_payments_main_panel_sx
|
||||||
# We can pre-compute in before_request, or use get_template_context_sync-like pattern
|
ctx = await get_template_context()
|
||||||
from quart import g
|
return _cart_payments_main_panel_sx(ctx)
|
||||||
return getattr(g, "cart_admin_content", "")
|
|
||||||
|
|
||||||
|
|
||||||
def _h_cart_payments_content():
|
|
||||||
from quart import g
|
|
||||||
return getattr(g, "cart_payments_content", "")
|
|
||||||
|
|||||||
@@ -7,19 +7,19 @@
|
|||||||
:content (overview-content))
|
:content (overview-content))
|
||||||
|
|
||||||
(defpage page-cart-view
|
(defpage page-cart-view
|
||||||
:path "/"
|
:path "/<page_slug>/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :cart-page
|
:layout :cart-page
|
||||||
:content (page-cart-content))
|
:content (page-cart-content))
|
||||||
|
|
||||||
(defpage cart-admin
|
(defpage cart-admin
|
||||||
:path "/"
|
: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 "/payments/"
|
:path "/<page_slug>/admin/payments/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout (:cart-admin :selected "payments")
|
:layout (:cart-admin :selected "payments")
|
||||||
:content (cart-payments-content))
|
:content (cart-payments-content))
|
||||||
|
|||||||
@@ -171,19 +171,25 @@ def create_app() -> "Quart":
|
|||||||
"markets": markets,
|
"markets": markets,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Auto-mount all defpages with absolute paths
|
||||||
|
from shared.sx.pages import auto_mount_pages
|
||||||
|
auto_mount_pages(app, "events")
|
||||||
|
|
||||||
# Tickets blueprint — user-facing ticket views and QR codes
|
# Tickets blueprint — user-facing ticket views and QR codes
|
||||||
from bp.tickets.routes import register as register_tickets
|
from bp.tickets.routes import register as register_tickets
|
||||||
tickets_bp = register_tickets()
|
tickets_bp = register_tickets()
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(tickets_bp, "events", names=["my-tickets", "ticket-detail"])
|
|
||||||
app.register_blueprint(tickets_bp)
|
app.register_blueprint(tickets_bp)
|
||||||
|
|
||||||
# Ticket admin — check-in interface (admin only)
|
# Ticket admin — check-in interface (admin only)
|
||||||
from bp.ticket_admin.routes import register as register_ticket_admin
|
from bp.ticket_admin.routes import register as register_ticket_admin
|
||||||
ticket_admin_bp = register_ticket_admin()
|
ticket_admin_bp = register_ticket_admin()
|
||||||
mount_pages(ticket_admin_bp, "events", names=["ticket-admin"])
|
|
||||||
app.register_blueprint(ticket_admin_bp)
|
app.register_blueprint(ticket_admin_bp)
|
||||||
|
|
||||||
|
# --- Pass defpage helper data to template context for layouts ---
|
||||||
|
@app.context_processor
|
||||||
|
async def inject_events_data():
|
||||||
|
return getattr(g, '_defpage_ctx', {})
|
||||||
|
|
||||||
# --- oEmbed endpoint ---
|
# --- oEmbed endpoint ---
|
||||||
@app.get("/oembed")
|
@app.get("/oembed")
|
||||||
async def oembed():
|
async def oembed():
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Routes:
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, request, render_template, make_response
|
from quart import Blueprint, g, request, make_response
|
||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
from shared.browser.app.utils.htmx import is_htmx_request
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, Blueprint, g
|
Blueprint, g, request,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -15,18 +15,6 @@ from shared.sx.helpers import sx_response
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _calendar_admin_main_panel_html
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.calendar_admin_content = _calendar_admin_main_panel_html(ctx)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "events", names=["calendar-admin"])
|
|
||||||
|
|
||||||
@bp.get("/description/")
|
@bp.get("/description/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def calendar_description_edit(calendar_slug: str, **kwargs):
|
async def calendar_description_edit(calendar_slug: str, **kwargs):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g, abort, session as qsession
|
request, make_response, Blueprint, g, abort, session as qsession
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from datetime import datetime, timezone
|
|||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response,
|
request, make_response,
|
||||||
Blueprint, g, redirect, url_for, jsonify,
|
Blueprint, g, redirect, url_for, jsonify,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import Blueprint
|
||||||
request, Blueprint, g
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _entry_admin_main_panel_html
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.entry_admin_content = _entry_admin_main_panel_html(ctx)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "events", names=["entry-admin"])
|
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ from shared.browser.app.redis_cacher import clear_cache
|
|||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g, jsonify
|
request, make_response, Blueprint, g, jsonify
|
||||||
)
|
)
|
||||||
from ..calendar_entries.services.entries import (
|
from ..calendar_entries.services.entries import (
|
||||||
svc_update_entry,
|
svc_update_entry,
|
||||||
@@ -238,19 +238,6 @@ def register():
|
|||||||
"user_ticket_counts_by_type": user_ticket_counts_by_type,
|
"user_ticket_counts_by_type": user_ticket_counts_by_type,
|
||||||
"container_nav": container_nav,
|
"container_nav": container_nav,
|
||||||
}
|
}
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _entry_main_panel_html, _entry_nav_html
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.entry_content = _entry_main_panel_html(ctx)
|
|
||||||
g.entry_menu = _entry_nav_html(ctx)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "events", names=["entry-detail"])
|
|
||||||
|
|
||||||
@bp.get("/edit/")
|
@bp.get("/edit/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def get_edit(entry_id: int, **rest):
|
async def get_edit(entry_id: int, **rest):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g
|
request, make_response, Blueprint, g
|
||||||
)
|
)
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import Blueprint
|
||||||
request, Blueprint, g
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
from sx.sx_components import _day_admin_main_panel_html
|
|
||||||
g.day_admin_content = _day_admin_main_panel_html({})
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "events", names=["day-admin"])
|
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|||||||
from datetime import datetime, timezone, date, timedelta
|
from datetime import datetime, timezone, date, timedelta
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g, abort, session as qsession
|
request, make_response, Blueprint, g, abort, session as qsession
|
||||||
)
|
)
|
||||||
|
|
||||||
from bp.calendar.services import get_visible_entries_for_period
|
from bp.calendar.services import get_visible_entries_for_period
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import (
|
from quart import (
|
||||||
request, render_template, make_response, Blueprint, g
|
request, make_response, Blueprint, g
|
||||||
)
|
)
|
||||||
|
|
||||||
from .services.markets import (
|
from .services.markets import (
|
||||||
@@ -21,18 +21,6 @@ def register():
|
|||||||
async def inject_root():
|
async def inject_root():
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _markets_main_panel_html
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.markets_content = _markets_main_panel_html(ctx)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "events", names=["events-markets"])
|
|
||||||
|
|
||||||
@bp.post("/new/")
|
@bp.post("/new/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def create_market(**kwargs):
|
async def create_market(**kwargs):
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ Routes:
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, request, render_template, make_response
|
from quart import Blueprint, g, request, make_response
|
||||||
|
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
from shared.browser.app.utils.htmx import is_htmx_request
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
|
|||||||
@@ -29,27 +29,6 @@ from shared.sx.helpers import sx_response
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("slot", __name__, url_prefix='/<int:slot_id>')
|
bp = Blueprint("slot", __name__, url_prefix='/<int:slot_id>')
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
slot_id = (request.view_args or {}).get("slot_id")
|
|
||||||
slot = await svc_get_slot(g.s, slot_id) if slot_id else None
|
|
||||||
if not slot:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
g.slot = slot
|
|
||||||
calendar = getattr(g, "calendar", None)
|
|
||||||
from sx.sx_components import render_slot_main_panel
|
|
||||||
g.slot_content = render_slot_main_panel(slot, calendar)
|
|
||||||
|
|
||||||
@bp.context_processor
|
|
||||||
async def _inject_slot():
|
|
||||||
return {"slot": getattr(g, "slot", None)}
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "events", names=["slot-detail"])
|
|
||||||
|
|
||||||
@bp.get("/edit/")
|
@bp.get("/edit/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def get_edit(slot_id: int, **kwargs):
|
async def get_edit(slot_id: int, **kwargs):
|
||||||
|
|||||||
@@ -38,18 +38,6 @@ def register():
|
|||||||
}
|
}
|
||||||
return {"slots": []}
|
return {"slots": []}
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
calendar = getattr(g, "calendar", None)
|
|
||||||
slots = await svc_list_slots(g.s, calendar.id) if calendar else []
|
|
||||||
from sx.sx_components import render_slots_table
|
|
||||||
g.slots_content = render_slots_table(slots, calendar)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "events", names=["slots-listing"])
|
|
||||||
|
|
||||||
@bp.post("/")
|
@bp.post("/")
|
||||||
@require_admin
|
@require_admin
|
||||||
@clear_cache(tag="calendars", tag_scope="all")
|
@clear_cache(tag="calendars", tag_scope="all")
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ import logging
|
|||||||
from quart import (
|
from quart import (
|
||||||
Blueprint, g, request, make_response,
|
Blueprint, g, request, make_response,
|
||||||
)
|
)
|
||||||
from sqlalchemy import select, func
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from models.calendars import CalendarEntry, Ticket, TicketType
|
from models.calendars import CalendarEntry
|
||||||
from shared.browser.app.authz import require_admin
|
from shared.browser.app.authz import require_admin
|
||||||
from shared.browser.app.redis_cacher import clear_cache
|
from shared.browser.app.redis_cacher import clear_cache
|
||||||
from shared.sx.helpers import sx_response
|
from shared.sx.helpers import sx_response
|
||||||
@@ -34,46 +34,6 @@ logger = logging.getLogger(__name__)
|
|||||||
def register() -> Blueprint:
|
def register() -> Blueprint:
|
||||||
bp = Blueprint("ticket_admin", __name__, url_prefix="/admin/tickets")
|
bp = Blueprint("ticket_admin", __name__, url_prefix="/admin/tickets")
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
# Get recent tickets
|
|
||||||
result = await g.s.execute(
|
|
||||||
select(Ticket)
|
|
||||||
.options(
|
|
||||||
selectinload(Ticket.entry).selectinload(CalendarEntry.calendar),
|
|
||||||
selectinload(Ticket.ticket_type),
|
|
||||||
)
|
|
||||||
.order_by(Ticket.created_at.desc())
|
|
||||||
.limit(50)
|
|
||||||
)
|
|
||||||
tickets = result.scalars().all()
|
|
||||||
|
|
||||||
# Stats
|
|
||||||
total = await g.s.scalar(select(func.count(Ticket.id)))
|
|
||||||
confirmed = await g.s.scalar(
|
|
||||||
select(func.count(Ticket.id)).where(Ticket.state == "confirmed")
|
|
||||||
)
|
|
||||||
checked_in = await g.s.scalar(
|
|
||||||
select(func.count(Ticket.id)).where(Ticket.state == "checked_in")
|
|
||||||
)
|
|
||||||
reserved = await g.s.scalar(
|
|
||||||
select(func.count(Ticket.id)).where(Ticket.state == "reserved")
|
|
||||||
)
|
|
||||||
|
|
||||||
stats = {
|
|
||||||
"total": total or 0,
|
|
||||||
"confirmed": confirmed or 0,
|
|
||||||
"checked_in": checked_in or 0,
|
|
||||||
"reserved": reserved or 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _ticket_admin_main_panel_html
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.ticket_admin_content = _ticket_admin_main_panel_html(ctx, tickets, stats)
|
|
||||||
|
|
||||||
@bp.get("/entry/<int:entry_id>/")
|
@bp.get("/entry/<int:entry_id>/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def entry_tickets(entry_id: int):
|
async def entry_tickets(entry_id: int):
|
||||||
|
|||||||
@@ -22,32 +22,6 @@ from shared.sx.helpers import sx_response
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("ticket_type", __name__, url_prefix='/<int:ticket_type_id>')
|
bp = Blueprint("ticket_type", __name__, url_prefix='/<int:ticket_type_id>')
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
ticket_type_id = (request.view_args or {}).get("ticket_type_id")
|
|
||||||
ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) if ticket_type_id else None
|
|
||||||
if not ticket_type:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
g.ticket_type = ticket_type
|
|
||||||
entry = getattr(g, "entry", None)
|
|
||||||
calendar = getattr(g, "calendar", None)
|
|
||||||
va = request.view_args or {}
|
|
||||||
from sx.sx_components import render_ticket_type_main_panel
|
|
||||||
g.ticket_type_content = render_ticket_type_main_panel(
|
|
||||||
ticket_type, entry, calendar,
|
|
||||||
va.get("day"), va.get("month"), va.get("year"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@bp.context_processor
|
|
||||||
async def _inject_ticket_type():
|
|
||||||
return {"ticket_type": getattr(g, "ticket_type", None)}
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "events", names=["ticket-type-detail"])
|
|
||||||
|
|
||||||
@bp.get("/edit/")
|
@bp.get("/edit/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def get_edit(ticket_type_id: int, **kwargs):
|
async def get_edit(ticket_type_id: int, **kwargs):
|
||||||
|
|||||||
@@ -35,23 +35,6 @@ def register():
|
|||||||
}
|
}
|
||||||
return {"ticket_types": []}
|
return {"ticket_types": []}
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
if "defpage_" not in (request.endpoint or ""):
|
|
||||||
return
|
|
||||||
entry = getattr(g, "entry", None)
|
|
||||||
calendar = getattr(g, "calendar", None)
|
|
||||||
ticket_types = await svc_list_ticket_types(g.s, entry.id) if entry else []
|
|
||||||
va = request.view_args or {}
|
|
||||||
from sx.sx_components import render_ticket_types_table
|
|
||||||
g.ticket_types_content = render_ticket_types_table(
|
|
||||||
ticket_types, entry, calendar,
|
|
||||||
va.get("day"), va.get("month"), va.get("year"),
|
|
||||||
)
|
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "events", names=["ticket-types-listing"])
|
|
||||||
|
|
||||||
@bp.post("/")
|
@bp.post("/")
|
||||||
@require_admin
|
@require_admin
|
||||||
@clear_cache(tag="calendars", tag_scope="all")
|
@clear_cache(tag="calendars", tag_scope="all")
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ from shared.sx.helpers import sx_response
|
|||||||
|
|
||||||
from .services.tickets import (
|
from .services.tickets import (
|
||||||
create_ticket,
|
create_ticket,
|
||||||
get_ticket_by_code,
|
|
||||||
get_user_tickets,
|
|
||||||
get_available_ticket_count,
|
get_available_ticket_count,
|
||||||
get_tickets_for_entry,
|
get_tickets_for_entry,
|
||||||
get_sold_ticket_count,
|
get_sold_ticket_count,
|
||||||
@@ -39,44 +37,6 @@ logger = logging.getLogger(__name__)
|
|||||||
def register() -> Blueprint:
|
def register() -> Blueprint:
|
||||||
bp = Blueprint("tickets", __name__, url_prefix="/tickets")
|
bp = Blueprint("tickets", __name__, url_prefix="/tickets")
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
ep = request.endpoint or ""
|
|
||||||
if "defpage_my_tickets" in ep:
|
|
||||||
ident = current_cart_identity()
|
|
||||||
tickets = await get_user_tickets(
|
|
||||||
g.s,
|
|
||||||
user_id=ident["user_id"],
|
|
||||||
session_id=ident["session_id"],
|
|
||||||
)
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _tickets_main_panel_html
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.tickets_content = _tickets_main_panel_html(ctx, tickets)
|
|
||||||
elif "defpage_ticket_detail" in ep:
|
|
||||||
code = (request.view_args or {}).get("code")
|
|
||||||
ticket = await get_ticket_by_code(g.s, code) if code else None
|
|
||||||
if not ticket:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
# Verify ownership
|
|
||||||
ident = current_cart_identity()
|
|
||||||
if ident["user_id"] is not None:
|
|
||||||
if ticket.user_id != ident["user_id"]:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
elif ident["session_id"] is not None:
|
|
||||||
if ticket.session_id != ident["session_id"]:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
else:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _ticket_detail_panel_html
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.ticket_detail_content = _ticket_detail_panel_html(ctx, ticket)
|
|
||||||
|
|
||||||
@bp.post("/buy/")
|
@bp.post("/buy/")
|
||||||
@clear_cache(tag="calendars", tag_scope="all")
|
@clear_cache(tag="calendars", tag_scope="all")
|
||||||
async def buy_tickets():
|
async def buy_tickets():
|
||||||
|
|||||||
@@ -191,11 +191,11 @@ def _calendar_nav_sx(ctx: dict) -> str:
|
|||||||
select_colours = ctx.get("select_colours", "")
|
select_colours = ctx.get("select_colours", "")
|
||||||
|
|
||||||
parts = []
|
parts = []
|
||||||
slots_href = url_for("calendar.slots.defpage_slots_listing", calendar_slug=cal_slug)
|
slots_href = url_for("defpage_slots_listing", calendar_slug=cal_slug)
|
||||||
parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock",
|
parts.append(sx_call("nav-link", href=slots_href, icon="fa fa-clock",
|
||||||
label="Slots", select_colours=select_colours))
|
label="Slots", select_colours=select_colours))
|
||||||
if is_admin:
|
if is_admin:
|
||||||
admin_href = url_for("calendar.admin.defpage_calendar_admin", calendar_slug=cal_slug)
|
admin_href = url_for("defpage_calendar_admin", calendar_slug=cal_slug)
|
||||||
parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog",
|
parts.append(sx_call("nav-link", href=admin_href, icon="fa fa-cog",
|
||||||
select_colours=select_colours))
|
select_colours=select_colours))
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
@@ -319,7 +319,7 @@ def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
nav_parts = []
|
nav_parts = []
|
||||||
if cal_slug:
|
if cal_slug:
|
||||||
for endpoint, label in [
|
for endpoint, label in [
|
||||||
("calendar.slots.defpage_slots_listing", "slots"),
|
("defpage_slots_listing", "slots"),
|
||||||
("calendar.admin.calendar_description_edit", "description"),
|
("calendar.admin.calendar_description_edit", "description"),
|
||||||
]:
|
]:
|
||||||
href = url_for(endpoint, calendar_slug=cal_slug)
|
href = url_for(endpoint, calendar_slug=cal_slug)
|
||||||
@@ -339,7 +339,7 @@ def _calendar_admin_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
|||||||
def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
def _markets_header_sx(ctx: dict, *, oob: bool = False) -> str:
|
||||||
"""Build the markets section header row."""
|
"""Build the markets section header row."""
|
||||||
from quart import url_for
|
from quart import url_for
|
||||||
link_href = url_for("markets.defpage_events_markets")
|
link_href = url_for("defpage_events_markets")
|
||||||
return sx_call("menu-row-sx", id="markets-row", level=3,
|
return sx_call("menu-row-sx", id="markets-row", level=3,
|
||||||
link_href=link_href,
|
link_href=link_href,
|
||||||
link_label_content=SxExpr(sx_call("events-markets-label")),
|
link_label_content=SxExpr(sx_call("events-markets-label")),
|
||||||
@@ -594,7 +594,7 @@ def _day_row_html(ctx: dict, entry) -> str:
|
|||||||
# Slot/Time
|
# Slot/Time
|
||||||
slot = getattr(entry, "slot", None)
|
slot = getattr(entry, "slot", None)
|
||||||
if slot:
|
if slot:
|
||||||
slot_href = url_for("calendar.slots.slot.defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id)
|
slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=slot.id)
|
||||||
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
time_start = slot.time_start.strftime("%H:%M") if slot.time_start else ""
|
||||||
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
|
time_end = f" \u2192 {slot.time_end.strftime('%H:%M')}" if slot.time_end else ""
|
||||||
slot_html = sx_call("events-day-row-slot",
|
slot_html = sx_call("events-day-row-slot",
|
||||||
@@ -774,7 +774,7 @@ def _tickets_main_panel_html(ctx: dict, tickets: list) -> str:
|
|||||||
ticket_cards = []
|
ticket_cards = []
|
||||||
if tickets:
|
if tickets:
|
||||||
for ticket in tickets:
|
for ticket in tickets:
|
||||||
href = url_for("tickets.defpage_ticket_detail", code=ticket.code)
|
href = url_for("defpage_ticket_detail", code=ticket.code)
|
||||||
entry = getattr(ticket, "entry", None)
|
entry = getattr(ticket, "entry", None)
|
||||||
entry_name = entry.name if entry else "Unknown event"
|
entry_name = entry.name if entry else "Unknown event"
|
||||||
tt = getattr(ticket, "ticket_type", None)
|
tt = getattr(ticket, "ticket_type", None)
|
||||||
@@ -819,7 +819,7 @@ def _ticket_detail_panel_html(ctx: dict, ticket) -> str:
|
|||||||
bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"}
|
bg_map = {"confirmed": "bg-emerald-50", "checked_in": "bg-blue-50", "reserved": "bg-amber-50"}
|
||||||
header_bg = bg_map.get(state, "bg-stone-50")
|
header_bg = bg_map.get(state, "bg-stone-50")
|
||||||
entry_name = entry.name if entry else "Ticket"
|
entry_name = entry.name if entry else "Ticket"
|
||||||
back_href = url_for("tickets.defpage_my_tickets")
|
back_href = url_for("defpage_my_tickets")
|
||||||
|
|
||||||
# Badge with larger sizing
|
# Badge with larger sizing
|
||||||
badge = _ticket_state_badge_html(state).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm')
|
badge = _ticket_state_badge_html(state).replace('px-2 py-0.5 text-xs', 'px-3 py-1 text-sm')
|
||||||
@@ -2165,7 +2165,7 @@ def render_slots_table(slots, calendar) -> str:
|
|||||||
rows_html = ""
|
rows_html = ""
|
||||||
if slots:
|
if slots:
|
||||||
for s in slots:
|
for s in slots:
|
||||||
slot_href = url_for("calendar.slots.slot.defpage_slot_detail", calendar_slug=cal_slug, slot_id=s.id)
|
slot_href = url_for("defpage_slot_detail", calendar_slug=cal_slug, slot_id=s.id)
|
||||||
del_url = url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id)
|
del_url = url_for("calendar.slots.slot.slot_delete", calendar_slug=cal_slug, slot_id=s.id)
|
||||||
desc = getattr(s, "description", "") or ""
|
desc = getattr(s, "description", "") or ""
|
||||||
|
|
||||||
@@ -2309,7 +2309,7 @@ def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
|
|||||||
|
|
||||||
tickets_html = ""
|
tickets_html = ""
|
||||||
for ticket in created_tickets:
|
for ticket in created_tickets:
|
||||||
href = url_for("tickets.defpage_ticket_detail", code=ticket.code)
|
href = url_for("defpage_ticket_detail", code=ticket.code)
|
||||||
tickets_html += sx_call("events-buy-result-ticket",
|
tickets_html += sx_call("events-buy-result-ticket",
|
||||||
href=href, code_short=ticket.code[:12] + "...")
|
href=href, code_short=ticket.code[:12] + "...")
|
||||||
|
|
||||||
@@ -2319,7 +2319,7 @@ def render_buy_result(entry, created_tickets, remaining, cart_count) -> str:
|
|||||||
remaining_html = sx_call("events-buy-result-remaining",
|
remaining_html = sx_call("events-buy-result-remaining",
|
||||||
text=f"{remaining} ticket{r_suffix} remaining")
|
text=f"{remaining} ticket{r_suffix} remaining")
|
||||||
|
|
||||||
my_href = url_for("tickets.defpage_my_tickets")
|
my_href = url_for("defpage_my_tickets")
|
||||||
|
|
||||||
return cart_html + sx_call("events-buy-result",
|
return cart_html + sx_call("events-buy-result",
|
||||||
entry_id=str(entry.id),
|
entry_id=str(entry.id),
|
||||||
@@ -2411,7 +2411,7 @@ def _ticket_adjust_controls(csrf, adjust_url, target, entry_id, count, *, ticket
|
|||||||
return _adj_form(1, sx_call("events-adjust-cart-plus"),
|
return _adj_form(1, sx_call("events-adjust-cart-plus"),
|
||||||
extra_cls="flex items-center")
|
extra_cls="flex items-center")
|
||||||
|
|
||||||
my_tickets_href = url_for("tickets.defpage_my_tickets")
|
my_tickets_href = url_for("defpage_my_tickets")
|
||||||
minus = _adj_form(count - 1, sx_call("events-adjust-minus"))
|
minus = _adj_form(count - 1, sx_call("events-adjust-minus"))
|
||||||
cart_icon = sx_call("events-adjust-cart-icon",
|
cart_icon = sx_call("events-adjust-cart-icon",
|
||||||
href=my_tickets_href, count=str(count))
|
href=my_tickets_href, count=str(count))
|
||||||
|
|||||||
@@ -311,6 +311,183 @@ def _markets_oob(ctx: dict, **kw: Any) -> str:
|
|||||||
return oobs
|
return oobs
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Shared hydration helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _add_to_defpage_ctx(**kwargs: Any) -> None:
|
||||||
|
"""Add data to g._defpage_ctx for the app-level context_processor."""
|
||||||
|
from quart import g
|
||||||
|
if not hasattr(g, '_defpage_ctx'):
|
||||||
|
g._defpage_ctx = {}
|
||||||
|
g._defpage_ctx.update(kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_calendar(calendar_slug: str | None) -> None:
|
||||||
|
"""Load calendar into g.calendar if not already present."""
|
||||||
|
from quart import g, abort
|
||||||
|
if hasattr(g, 'calendar'):
|
||||||
|
_add_to_defpage_ctx(calendar=g.calendar)
|
||||||
|
return
|
||||||
|
from bp.calendar.services.calendar_view import (
|
||||||
|
get_calendar_by_post_and_slug, get_calendar_by_slug,
|
||||||
|
)
|
||||||
|
post_data = getattr(g, "post_data", None)
|
||||||
|
if post_data:
|
||||||
|
post_id = (post_data.get("post") or {}).get("id")
|
||||||
|
cal = await get_calendar_by_post_and_slug(g.s, post_id, calendar_slug)
|
||||||
|
else:
|
||||||
|
cal = await get_calendar_by_slug(g.s, calendar_slug)
|
||||||
|
if not cal:
|
||||||
|
abort(404)
|
||||||
|
g.calendar = cal
|
||||||
|
g.calendar_slug = calendar_slug
|
||||||
|
_add_to_defpage_ctx(calendar=cal)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_entry(entry_id: int | None) -> None:
|
||||||
|
"""Load calendar entry into g.entry if not already present."""
|
||||||
|
from quart import g, abort
|
||||||
|
if hasattr(g, 'entry'):
|
||||||
|
_add_to_defpage_ctx(entry=g.entry)
|
||||||
|
return
|
||||||
|
from sqlalchemy import select
|
||||||
|
from models.calendars import CalendarEntry
|
||||||
|
result = await g.s.execute(
|
||||||
|
select(CalendarEntry).where(
|
||||||
|
CalendarEntry.id == entry_id,
|
||||||
|
CalendarEntry.deleted_at.is_(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
entry = result.scalar_one_or_none()
|
||||||
|
if entry is None:
|
||||||
|
abort(404)
|
||||||
|
g.entry = entry
|
||||||
|
_add_to_defpage_ctx(entry=entry)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_entry_context(entry_id: int | None) -> None:
|
||||||
|
"""Load full entry context (ticket data, posts) into g.* and _defpage_ctx."""
|
||||||
|
from quart import g
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from models.calendars import CalendarEntry
|
||||||
|
from bp.tickets.services.tickets import (
|
||||||
|
get_available_ticket_count,
|
||||||
|
get_sold_ticket_count,
|
||||||
|
get_user_reserved_count,
|
||||||
|
)
|
||||||
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from bp.calendar_entry.services.post_associations import get_entry_posts
|
||||||
|
|
||||||
|
await _ensure_entry(entry_id)
|
||||||
|
|
||||||
|
# Reload with ticket_types eagerly loaded
|
||||||
|
stmt = (
|
||||||
|
select(CalendarEntry)
|
||||||
|
.where(CalendarEntry.id == entry_id, CalendarEntry.deleted_at.is_(None))
|
||||||
|
.options(selectinload(CalendarEntry.ticket_types))
|
||||||
|
)
|
||||||
|
result = await g.s.execute(stmt)
|
||||||
|
calendar_entry = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
if calendar_entry and getattr(g, "calendar", None):
|
||||||
|
if calendar_entry.calendar_id != g.calendar.id:
|
||||||
|
calendar_entry = None
|
||||||
|
|
||||||
|
if calendar_entry:
|
||||||
|
await g.s.refresh(calendar_entry, ['slot'])
|
||||||
|
g.entry = calendar_entry
|
||||||
|
entry_posts = await get_entry_posts(g.s, calendar_entry.id)
|
||||||
|
ticket_remaining = await get_available_ticket_count(g.s, calendar_entry.id)
|
||||||
|
ticket_sold_count = await get_sold_ticket_count(g.s, calendar_entry.id)
|
||||||
|
ident = current_cart_identity()
|
||||||
|
user_ticket_count = await get_user_reserved_count(
|
||||||
|
g.s, calendar_entry.id,
|
||||||
|
user_id=ident["user_id"],
|
||||||
|
session_id=ident["session_id"],
|
||||||
|
)
|
||||||
|
user_ticket_counts_by_type = {}
|
||||||
|
if calendar_entry.ticket_types:
|
||||||
|
for tt in calendar_entry.ticket_types:
|
||||||
|
if tt.deleted_at is None:
|
||||||
|
user_ticket_counts_by_type[tt.id] = await get_user_reserved_count(
|
||||||
|
g.s, calendar_entry.id,
|
||||||
|
user_id=ident["user_id"],
|
||||||
|
session_id=ident["session_id"],
|
||||||
|
ticket_type_id=tt.id,
|
||||||
|
)
|
||||||
|
_add_to_defpage_ctx(
|
||||||
|
entry=calendar_entry,
|
||||||
|
entry_posts=entry_posts,
|
||||||
|
ticket_remaining=ticket_remaining,
|
||||||
|
ticket_sold_count=ticket_sold_count,
|
||||||
|
user_ticket_count=user_ticket_count,
|
||||||
|
user_ticket_counts_by_type=user_ticket_counts_by_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_day_data(year: int, month: int, day: int) -> None:
|
||||||
|
"""Load day-specific data for layout header functions."""
|
||||||
|
from quart import g, session as qsession
|
||||||
|
if hasattr(g, 'day_date'):
|
||||||
|
return
|
||||||
|
from datetime import date as date_cls, datetime, timezone, timedelta
|
||||||
|
from sqlalchemy import select
|
||||||
|
from bp.calendar.services import get_visible_entries_for_period
|
||||||
|
from models.calendars import CalendarSlot
|
||||||
|
|
||||||
|
calendar = getattr(g, "calendar", None)
|
||||||
|
if not calendar:
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
day_date = date_cls(year, month, day)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return
|
||||||
|
|
||||||
|
period_start = datetime(year, month, day, tzinfo=timezone.utc)
|
||||||
|
period_end = period_start + timedelta(days=1)
|
||||||
|
|
||||||
|
user = getattr(g, "user", None)
|
||||||
|
session_id = qsession.get("calendar_sid")
|
||||||
|
|
||||||
|
visible = await get_visible_entries_for_period(
|
||||||
|
sess=g.s,
|
||||||
|
calendar_id=calendar.id,
|
||||||
|
period_start=period_start,
|
||||||
|
period_end=period_end,
|
||||||
|
user=user,
|
||||||
|
session_id=session_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
weekday_attr = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"][day_date.weekday()]
|
||||||
|
stmt = (
|
||||||
|
select(CalendarSlot)
|
||||||
|
.where(
|
||||||
|
CalendarSlot.calendar_id == calendar.id,
|
||||||
|
getattr(CalendarSlot, weekday_attr) == True, # noqa: E712
|
||||||
|
CalendarSlot.deleted_at.is_(None),
|
||||||
|
)
|
||||||
|
.order_by(CalendarSlot.time_start.asc(), CalendarSlot.id.asc())
|
||||||
|
)
|
||||||
|
result = await g.s.execute(stmt)
|
||||||
|
day_slots = list(result.scalars())
|
||||||
|
|
||||||
|
g.day_date = day_date
|
||||||
|
_add_to_defpage_ctx(
|
||||||
|
qsession=qsession,
|
||||||
|
day_date=day_date,
|
||||||
|
day=day,
|
||||||
|
year=year,
|
||||||
|
month=month,
|
||||||
|
day_entries=visible.merged_entries,
|
||||||
|
user_entries=visible.user_entries,
|
||||||
|
confirmed_entries=visible.confirmed_entries,
|
||||||
|
day_slots=day_slots,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Page helpers
|
# Page helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -336,39 +513,72 @@ def _register_events_helpers() -> None:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def _h_calendar_admin_content():
|
async def _h_calendar_admin_content(calendar_slug=None, **kw):
|
||||||
|
await _ensure_calendar(calendar_slug)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _calendar_admin_main_panel_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
return _calendar_admin_main_panel_html(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_day_admin_content(calendar_slug=None, year=None, month=None, day=None, **kw):
|
||||||
|
await _ensure_calendar(calendar_slug)
|
||||||
|
if year is not None:
|
||||||
|
await _ensure_day_data(int(year), int(month), int(day))
|
||||||
|
from sx.sx_components import _day_admin_main_panel_html
|
||||||
|
return _day_admin_main_panel_html({})
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_slots_content(calendar_slug=None, **kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "calendar_admin_content", "")
|
await _ensure_calendar(calendar_slug)
|
||||||
|
calendar = getattr(g, "calendar", None)
|
||||||
|
from bp.slots.services.slots import list_slots as svc_list_slots
|
||||||
|
slots = await svc_list_slots(g.s, calendar.id) if calendar else []
|
||||||
|
_add_to_defpage_ctx(slots=slots)
|
||||||
|
from sx.sx_components import render_slots_table
|
||||||
|
return render_slots_table(slots, calendar)
|
||||||
|
|
||||||
|
|
||||||
def _h_day_admin_content():
|
async def _h_slot_content(calendar_slug=None, slot_id=None, **kw):
|
||||||
from quart import g
|
from quart import g, abort
|
||||||
return getattr(g, "day_admin_content", "")
|
await _ensure_calendar(calendar_slug)
|
||||||
|
from bp.slot.services.slot import get_slot as svc_get_slot
|
||||||
|
slot = await svc_get_slot(g.s, slot_id) if slot_id else None
|
||||||
|
if not slot:
|
||||||
|
abort(404)
|
||||||
|
g.slot = slot
|
||||||
|
_add_to_defpage_ctx(slot=slot)
|
||||||
|
calendar = getattr(g, "calendar", None)
|
||||||
|
from sx.sx_components import render_slot_main_panel
|
||||||
|
return render_slot_main_panel(slot, calendar)
|
||||||
|
|
||||||
|
|
||||||
def _h_slots_content():
|
async def _h_entry_content(calendar_slug=None, entry_id=None, **kw):
|
||||||
from quart import g
|
await _ensure_calendar(calendar_slug)
|
||||||
return getattr(g, "slots_content", "")
|
await _ensure_entry_context(entry_id)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _entry_main_panel_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
return _entry_main_panel_html(ctx)
|
||||||
|
|
||||||
|
|
||||||
def _h_slot_content():
|
async def _h_entry_menu(calendar_slug=None, entry_id=None, **kw):
|
||||||
from quart import g
|
await _ensure_calendar(calendar_slug)
|
||||||
return getattr(g, "slot_content", "")
|
await _ensure_entry_context(entry_id)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _entry_nav_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
return _entry_nav_html(ctx)
|
||||||
|
|
||||||
|
|
||||||
def _h_entry_content():
|
async def _h_entry_admin_content(calendar_slug=None, entry_id=None, **kw):
|
||||||
from quart import g
|
await _ensure_calendar(calendar_slug)
|
||||||
return getattr(g, "entry_content", "")
|
await _ensure_entry_context(entry_id)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _entry_admin_main_panel_html
|
||||||
def _h_entry_menu():
|
ctx = await get_template_context()
|
||||||
from quart import g
|
return _entry_admin_main_panel_html(ctx)
|
||||||
return getattr(g, "entry_menu", "")
|
|
||||||
|
|
||||||
|
|
||||||
def _h_entry_admin_content():
|
|
||||||
from quart import g
|
|
||||||
return getattr(g, "entry_admin_content", "")
|
|
||||||
|
|
||||||
|
|
||||||
def _h_admin_menu():
|
def _h_admin_menu():
|
||||||
@@ -376,31 +586,118 @@ def _h_admin_menu():
|
|||||||
return sx_call("events-admin-placeholder-nav")
|
return sx_call("events-admin-placeholder-nav")
|
||||||
|
|
||||||
|
|
||||||
def _h_ticket_types_content():
|
async def _h_ticket_types_content(calendar_slug=None, entry_id=None,
|
||||||
|
year=None, month=None, day=None, **kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "ticket_types_content", "")
|
await _ensure_calendar(calendar_slug)
|
||||||
|
await _ensure_entry(entry_id)
|
||||||
|
entry = getattr(g, "entry", None)
|
||||||
|
calendar = getattr(g, "calendar", None)
|
||||||
|
from bp.ticket_types.services.tickets import list_ticket_types as svc_list_ticket_types
|
||||||
|
ticket_types = await svc_list_ticket_types(g.s, entry.id) if entry else []
|
||||||
|
_add_to_defpage_ctx(ticket_types=ticket_types)
|
||||||
|
from sx.sx_components import render_ticket_types_table
|
||||||
|
return render_ticket_types_table(ticket_types, entry, calendar, day, month, year)
|
||||||
|
|
||||||
|
|
||||||
def _h_ticket_type_content():
|
async def _h_ticket_type_content(calendar_slug=None, entry_id=None,
|
||||||
|
ticket_type_id=None, year=None, month=None, day=None, **kw):
|
||||||
|
from quart import g, abort
|
||||||
|
await _ensure_calendar(calendar_slug)
|
||||||
|
await _ensure_entry(entry_id)
|
||||||
|
from bp.ticket_type.services.ticket import get_ticket_type as svc_get_ticket_type
|
||||||
|
ticket_type = await svc_get_ticket_type(g.s, ticket_type_id) if ticket_type_id else None
|
||||||
|
if not ticket_type:
|
||||||
|
abort(404)
|
||||||
|
g.ticket_type = ticket_type
|
||||||
|
_add_to_defpage_ctx(ticket_type=ticket_type)
|
||||||
|
entry = getattr(g, "entry", None)
|
||||||
|
calendar = getattr(g, "calendar", None)
|
||||||
|
from sx.sx_components import render_ticket_type_main_panel
|
||||||
|
return render_ticket_type_main_panel(ticket_type, entry, calendar, day, month, year)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_tickets_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "ticket_type_content", "")
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from bp.tickets.services.tickets import get_user_tickets
|
||||||
|
ident = current_cart_identity()
|
||||||
|
tickets = await get_user_tickets(
|
||||||
|
g.s,
|
||||||
|
user_id=ident["user_id"],
|
||||||
|
session_id=ident["session_id"],
|
||||||
|
)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _tickets_main_panel_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
return _tickets_main_panel_html(ctx, tickets)
|
||||||
|
|
||||||
|
|
||||||
def _h_tickets_content():
|
async def _h_ticket_detail_content(code=None, **kw):
|
||||||
|
from quart import g, abort
|
||||||
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from bp.tickets.services.tickets import get_ticket_by_code
|
||||||
|
ticket = await get_ticket_by_code(g.s, code) if code else None
|
||||||
|
if not ticket:
|
||||||
|
abort(404)
|
||||||
|
# Verify ownership
|
||||||
|
ident = current_cart_identity()
|
||||||
|
if ident["user_id"] is not None:
|
||||||
|
if ticket.user_id != ident["user_id"]:
|
||||||
|
abort(404)
|
||||||
|
elif ident["session_id"] is not None:
|
||||||
|
if ticket.session_id != ident["session_id"]:
|
||||||
|
abort(404)
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _ticket_detail_panel_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
return _ticket_detail_panel_html(ctx, ticket)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_ticket_admin_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "tickets_content", "")
|
from sqlalchemy import select, func
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from models.calendars import CalendarEntry, Ticket
|
||||||
|
|
||||||
|
result = await g.s.execute(
|
||||||
|
select(Ticket)
|
||||||
|
.options(
|
||||||
|
selectinload(Ticket.entry).selectinload(CalendarEntry.calendar),
|
||||||
|
selectinload(Ticket.ticket_type),
|
||||||
|
)
|
||||||
|
.order_by(Ticket.created_at.desc())
|
||||||
|
.limit(50)
|
||||||
|
)
|
||||||
|
tickets = result.scalars().all()
|
||||||
|
|
||||||
|
total = await g.s.scalar(select(func.count(Ticket.id)))
|
||||||
|
confirmed = await g.s.scalar(
|
||||||
|
select(func.count(Ticket.id)).where(Ticket.state == "confirmed")
|
||||||
|
)
|
||||||
|
checked_in = await g.s.scalar(
|
||||||
|
select(func.count(Ticket.id)).where(Ticket.state == "checked_in")
|
||||||
|
)
|
||||||
|
reserved = await g.s.scalar(
|
||||||
|
select(func.count(Ticket.id)).where(Ticket.state == "reserved")
|
||||||
|
)
|
||||||
|
stats = {
|
||||||
|
"total": total or 0,
|
||||||
|
"confirmed": confirmed or 0,
|
||||||
|
"checked_in": checked_in or 0,
|
||||||
|
"reserved": reserved or 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
from shared.sx.page import get_template_context
|
||||||
|
from sx.sx_components import _ticket_admin_main_panel_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
return _ticket_admin_main_panel_html(ctx, tickets, stats)
|
||||||
|
|
||||||
|
|
||||||
def _h_ticket_detail_content():
|
async def _h_markets_content(**kw):
|
||||||
from quart import g
|
from shared.sx.page import get_template_context
|
||||||
return getattr(g, "ticket_detail_content", "")
|
from sx.sx_components import _markets_main_panel_html
|
||||||
|
ctx = await get_template_context()
|
||||||
|
return _markets_main_panel_html(ctx)
|
||||||
def _h_ticket_admin_content():
|
|
||||||
from quart import g
|
|
||||||
return getattr(g, "ticket_admin_content", "")
|
|
||||||
|
|
||||||
|
|
||||||
def _h_markets_content():
|
|
||||||
from quart import g
|
|
||||||
return getattr(g, "markets_content", "")
|
|
||||||
|
|||||||
@@ -1,89 +1,89 @@
|
|||||||
;; Events pages — mounted on various nested blueprints
|
;; Events pages — auto-mounted with absolute paths
|
||||||
|
|
||||||
;; Calendar admin (mounted on calendar.admin bp)
|
;; Calendar admin
|
||||||
(defpage calendar-admin
|
(defpage calendar-admin
|
||||||
:path "/"
|
:path "/<slug>/<calendar_slug>/admin/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :events-calendar-admin
|
:layout :events-calendar-admin
|
||||||
:content (calendar-admin-content))
|
:content (calendar-admin-content calendar-slug))
|
||||||
|
|
||||||
;; Day admin (mounted on day.admin bp)
|
;; Day admin
|
||||||
(defpage day-admin
|
(defpage day-admin
|
||||||
:path "/"
|
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/admin/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :events-day-admin
|
:layout :events-day-admin
|
||||||
:content (day-admin-content))
|
:content (day-admin-content calendar-slug year month day))
|
||||||
|
|
||||||
;; Slots listing (mounted on slots bp)
|
;; Slots listing
|
||||||
(defpage slots-listing
|
(defpage slots-listing
|
||||||
:path "/"
|
:path "/<slug>/<calendar_slug>/slots/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :events-slots
|
:layout :events-slots
|
||||||
:content (slots-content))
|
:content (slots-content calendar-slug))
|
||||||
|
|
||||||
;; Slot detail (mounted on slot bp)
|
;; Slot detail
|
||||||
(defpage slot-detail
|
(defpage slot-detail
|
||||||
:path "/"
|
:path "/<slug>/<calendar_slug>/slots/<int:slot_id>/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :events-slot
|
:layout :events-slot
|
||||||
:content (slot-content))
|
:content (slot-content calendar-slug slot-id))
|
||||||
|
|
||||||
;; Entry detail (mounted on calendar_entry bp)
|
;; Entry detail
|
||||||
(defpage entry-detail
|
(defpage entry-detail
|
||||||
:path "/"
|
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/entries/<int:entry_id>/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :events-entry
|
:layout :events-entry
|
||||||
:content (entry-content)
|
:content (entry-content calendar-slug entry-id)
|
||||||
:menu (entry-menu))
|
:menu (entry-menu calendar-slug entry-id))
|
||||||
|
|
||||||
;; Entry admin (mounted on calendar_entry.admin bp)
|
;; Entry admin
|
||||||
(defpage entry-admin
|
(defpage entry-admin
|
||||||
:path "/"
|
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/entries/<int:entry_id>/admin/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :events-entry-admin
|
:layout :events-entry-admin
|
||||||
:content (entry-admin-content)
|
:content (entry-admin-content calendar-slug entry-id)
|
||||||
:menu (admin-menu))
|
:menu (admin-menu))
|
||||||
|
|
||||||
;; Ticket types listing (mounted on ticket_types bp)
|
;; Ticket types listing
|
||||||
(defpage ticket-types-listing
|
(defpage ticket-types-listing
|
||||||
:path "/"
|
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/entries/<int:entry_id>/ticket-types/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :events-ticket-types
|
:layout :events-ticket-types
|
||||||
:content (ticket-types-content)
|
:content (ticket-types-content calendar-slug entry-id year month day)
|
||||||
:menu (admin-menu))
|
:menu (admin-menu))
|
||||||
|
|
||||||
;; Ticket type detail (mounted on ticket_type bp)
|
;; Ticket type detail
|
||||||
(defpage ticket-type-detail
|
(defpage ticket-type-detail
|
||||||
:path "/"
|
:path "/<slug>/<calendar_slug>/day/<int:year>/<int:month>/<int:day>/entries/<int:entry_id>/ticket-types/<int:ticket_type_id>/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :events-ticket-type
|
:layout :events-ticket-type
|
||||||
:content (ticket-type-content)
|
:content (ticket-type-content calendar-slug entry-id ticket-type-id year month day)
|
||||||
:menu (admin-menu))
|
:menu (admin-menu))
|
||||||
|
|
||||||
;; My tickets (mounted on tickets bp)
|
;; My tickets
|
||||||
(defpage my-tickets
|
(defpage my-tickets
|
||||||
:path "/"
|
:path "/tickets/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :root
|
:layout :root
|
||||||
:content (tickets-content))
|
:content (tickets-content))
|
||||||
|
|
||||||
;; Ticket detail (mounted on tickets bp)
|
;; Ticket detail
|
||||||
(defpage ticket-detail
|
(defpage ticket-detail
|
||||||
:path "/<code>/"
|
:path "/tickets/<code>/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :root
|
:layout :root
|
||||||
:content (ticket-detail-content))
|
:content (ticket-detail-content code))
|
||||||
|
|
||||||
;; Ticket admin dashboard (mounted on ticket_admin bp)
|
;; Ticket admin dashboard
|
||||||
(defpage ticket-admin
|
(defpage ticket-admin
|
||||||
:path "/"
|
:path "/admin/tickets/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout :root
|
:layout :root
|
||||||
:content (ticket-admin-content))
|
:content (ticket-admin-content))
|
||||||
|
|
||||||
;; Markets (mounted on markets bp)
|
;; Markets
|
||||||
(defpage events-markets
|
(defpage events-markets
|
||||||
:path "/"
|
:path "/<slug>/markets/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :events-markets
|
:layout :events-markets
|
||||||
:content (markets-content))
|
:content (markets-content))
|
||||||
|
|||||||
@@ -94,10 +94,11 @@ def create_app() -> "Quart":
|
|||||||
app.register_blueprint(register_identity_bp())
|
app.register_blueprint(register_identity_bp())
|
||||||
|
|
||||||
social_bp = register_social_bp()
|
social_bp = register_social_bp()
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(social_bp, "federation")
|
|
||||||
app.register_blueprint(social_bp)
|
app.register_blueprint(social_bp)
|
||||||
|
|
||||||
|
from shared.sx.pages import auto_mount_pages
|
||||||
|
auto_mount_pages(app, "federation")
|
||||||
|
|
||||||
app.register_blueprint(register_fragments())
|
app.register_blueprint(register_fragments())
|
||||||
|
|
||||||
# --- home page ---
|
# --- home page ---
|
||||||
|
|||||||
@@ -32,102 +32,6 @@ def register(url_prefix="/social"):
|
|||||||
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
|
actor = await services.federation.get_actor_by_user_id(g.s, g.user.id)
|
||||||
g._social_actor = actor
|
g._social_actor = actor
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
"""Pre-render content for defpage routes."""
|
|
||||||
endpoint = request.endpoint or ""
|
|
||||||
|
|
||||||
if endpoint.endswith("defpage_home_timeline"):
|
|
||||||
actor = _require_actor()
|
|
||||||
items = await services.federation.get_home_timeline(g.s, actor.id)
|
|
||||||
from sx.sx_components import _timeline_content_sx
|
|
||||||
g.home_timeline_content = _timeline_content_sx(items, "home", actor)
|
|
||||||
|
|
||||||
elif endpoint.endswith("defpage_public_timeline"):
|
|
||||||
actor = getattr(g, "_social_actor", None)
|
|
||||||
items = await services.federation.get_public_timeline(g.s)
|
|
||||||
from sx.sx_components import _timeline_content_sx
|
|
||||||
g.public_timeline_content = _timeline_content_sx(items, "public", actor)
|
|
||||||
|
|
||||||
elif endpoint.endswith("defpage_compose_form"):
|
|
||||||
actor = _require_actor()
|
|
||||||
from sx.sx_components import _compose_content_sx
|
|
||||||
reply_to = request.args.get("reply_to")
|
|
||||||
g.compose_content = _compose_content_sx(actor, reply_to)
|
|
||||||
|
|
||||||
elif endpoint.endswith("defpage_search"):
|
|
||||||
actor = getattr(g, "_social_actor", None)
|
|
||||||
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}
|
|
||||||
from sx.sx_components import _search_content_sx
|
|
||||||
g.search_content = _search_content_sx(query, actors_list, total, 1, followed_urls, actor)
|
|
||||||
|
|
||||||
elif endpoint.endswith("defpage_following_list"):
|
|
||||||
actor = _require_actor()
|
|
||||||
actors_list, total = await services.federation.get_following(
|
|
||||||
g.s, actor.preferred_username,
|
|
||||||
)
|
|
||||||
from sx.sx_components import _following_content_sx
|
|
||||||
g.following_content = _following_content_sx(actors_list, total, actor)
|
|
||||||
|
|
||||||
elif endpoint.endswith("defpage_followers_list"):
|
|
||||||
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}
|
|
||||||
from sx.sx_components import _followers_content_sx
|
|
||||||
g.followers_content = _followers_content_sx(actors_list, total, followed_urls, actor)
|
|
||||||
|
|
||||||
elif endpoint.endswith("defpage_actor_timeline"):
|
|
||||||
actor = getattr(g, "_social_actor", None)
|
|
||||||
actor_id = request.view_args.get("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
|
|
||||||
from sx.sx_components import _actor_timeline_content_sx
|
|
||||||
g.actor_timeline_content = _actor_timeline_content_sx(remote_dto, items, is_following, actor)
|
|
||||||
|
|
||||||
elif endpoint.endswith("defpage_notifications"):
|
|
||||||
actor = _require_actor()
|
|
||||||
items = await services.federation.get_notifications(g.s, actor.id)
|
|
||||||
await services.federation.mark_notifications_read(g.s, actor.id)
|
|
||||||
from sx.sx_components import _notifications_content_sx
|
|
||||||
g.notifications_content = _notifications_content_sx(items)
|
|
||||||
|
|
||||||
# -- Timeline pagination ---------------------------------------------------
|
# -- Timeline pagination ---------------------------------------------------
|
||||||
|
|
||||||
@bp.get("/timeline")
|
@bp.get("/timeline")
|
||||||
@@ -170,7 +74,7 @@ def register(url_prefix="/social"):
|
|||||||
form = await request.form
|
form = await request.form
|
||||||
content = form.get("content", "").strip()
|
content = form.get("content", "").strip()
|
||||||
if not content:
|
if not content:
|
||||||
return redirect(url_for("social.defpage_compose_form"))
|
return redirect(url_for("defpage_compose_form"))
|
||||||
|
|
||||||
visibility = form.get("visibility", "public")
|
visibility = form.get("visibility", "public")
|
||||||
in_reply_to = form.get("in_reply_to") or None
|
in_reply_to = form.get("in_reply_to") or None
|
||||||
@@ -181,13 +85,13 @@ def register(url_prefix="/social"):
|
|||||||
visibility=visibility,
|
visibility=visibility,
|
||||||
in_reply_to=in_reply_to,
|
in_reply_to=in_reply_to,
|
||||||
)
|
)
|
||||||
return redirect(url_for("social.defpage_home_timeline"))
|
return redirect(url_for("defpage_home_timeline"))
|
||||||
|
|
||||||
@bp.post("/delete/<int:post_id>")
|
@bp.post("/delete/<int:post_id>")
|
||||||
async def delete_post(post_id: int):
|
async def delete_post(post_id: int):
|
||||||
actor = _require_actor()
|
actor = _require_actor()
|
||||||
await services.federation.delete_local_post(g.s, actor.id, post_id)
|
await services.federation.delete_local_post(g.s, actor.id, post_id)
|
||||||
return redirect(url_for("social.defpage_home_timeline"))
|
return redirect(url_for("defpage_home_timeline"))
|
||||||
|
|
||||||
# -- Search + Follow -------------------------------------------------------
|
# -- Search + Follow -------------------------------------------------------
|
||||||
|
|
||||||
@@ -223,7 +127,7 @@ def register(url_prefix="/social"):
|
|||||||
)
|
)
|
||||||
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
|
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
|
||||||
return await _actor_card_response(actor, remote_actor_url, is_followed=True)
|
return await _actor_card_response(actor, remote_actor_url, is_followed=True)
|
||||||
return redirect(request.referrer or url_for("social.defpage_search"))
|
return redirect(request.referrer or url_for("defpage_search"))
|
||||||
|
|
||||||
@bp.post("/unfollow")
|
@bp.post("/unfollow")
|
||||||
async def unfollow():
|
async def unfollow():
|
||||||
@@ -236,7 +140,7 @@ def register(url_prefix="/social"):
|
|||||||
)
|
)
|
||||||
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
|
if request.headers.get("SX-Request") or request.headers.get("HX-Request"):
|
||||||
return await _actor_card_response(actor, remote_actor_url, is_followed=False)
|
return await _actor_card_response(actor, remote_actor_url, is_followed=False)
|
||||||
return redirect(request.referrer or url_for("social.defpage_search"))
|
return redirect(request.referrer or url_for("defpage_search"))
|
||||||
|
|
||||||
async def _actor_card_response(actor, remote_actor_url, is_followed):
|
async def _actor_card_response(actor, remote_actor_url, is_followed):
|
||||||
"""Re-render a single actor card after follow/unfollow via HTMX."""
|
"""Re-render a single actor card after follow/unfollow via HTMX."""
|
||||||
@@ -414,6 +318,6 @@ def register(url_prefix="/social"):
|
|||||||
async def mark_read():
|
async def mark_read():
|
||||||
actor = _require_actor()
|
actor = _require_actor()
|
||||||
await services.federation.mark_notifications_read(g.s, actor.id)
|
await services.federation.mark_notifications_read(g.s, actor.id)
|
||||||
return redirect(url_for("social.defpage_notifications"))
|
return redirect(url_for("defpage_notifications"))
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -69,41 +69,130 @@ def _register_federation_helpers() -> None:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def _h_home_timeline_content():
|
def _get_actor():
|
||||||
|
"""Return current user's actor or None."""
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "home_timeline_content", "")
|
return getattr(g, "_social_actor", None)
|
||||||
|
|
||||||
|
|
||||||
def _h_public_timeline_content():
|
def _require_actor():
|
||||||
|
"""Return current user's actor or abort 403."""
|
||||||
|
from quart import abort
|
||||||
|
actor = _get_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 quart import g
|
||||||
return getattr(g, "public_timeline_content", "")
|
from shared.services.registry import services
|
||||||
|
actor = _require_actor()
|
||||||
|
items = await services.federation.get_home_timeline(g.s, actor.id)
|
||||||
|
from sx.sx_components import _timeline_content_sx
|
||||||
|
return _timeline_content_sx(items, "home", actor)
|
||||||
|
|
||||||
|
|
||||||
def _h_compose_content():
|
async def _h_public_timeline_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "compose_content", "")
|
from shared.services.registry import services
|
||||||
|
actor = _get_actor()
|
||||||
|
items = await services.federation.get_public_timeline(g.s)
|
||||||
|
from sx.sx_components import _timeline_content_sx
|
||||||
|
return _timeline_content_sx(items, "public", actor)
|
||||||
|
|
||||||
|
|
||||||
def _h_search_content():
|
async def _h_compose_content(**kw):
|
||||||
|
from quart import request
|
||||||
|
actor = _require_actor()
|
||||||
|
from sx.sx_components import _compose_content_sx
|
||||||
|
reply_to = request.args.get("reply_to")
|
||||||
|
return _compose_content_sx(actor, reply_to)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_search_content(**kw):
|
||||||
|
from quart import g, request
|
||||||
|
from shared.services.registry import services
|
||||||
|
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}
|
||||||
|
from sx.sx_components import _search_content_sx
|
||||||
|
return _search_content_sx(query, actors_list, total, 1, followed_urls, actor)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_following_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "search_content", "")
|
from shared.services.registry import services
|
||||||
|
actor = _require_actor()
|
||||||
|
actors_list, total = await services.federation.get_following(
|
||||||
|
g.s, actor.preferred_username,
|
||||||
|
)
|
||||||
|
from sx.sx_components import _following_content_sx
|
||||||
|
return _following_content_sx(actors_list, total, actor)
|
||||||
|
|
||||||
|
|
||||||
def _h_following_content():
|
async def _h_followers_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "following_content", "")
|
from shared.services.registry import services
|
||||||
|
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}
|
||||||
|
from sx.sx_components import _followers_content_sx
|
||||||
|
return _followers_content_sx(actors_list, total, followed_urls, actor)
|
||||||
|
|
||||||
|
|
||||||
def _h_followers_content():
|
async def _h_actor_timeline_content(id=None, **kw):
|
||||||
|
from quart import g, abort
|
||||||
|
from shared.services.registry import services
|
||||||
|
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
|
||||||
|
from sx.sx_components import _actor_timeline_content_sx
|
||||||
|
return _actor_timeline_content_sx(remote_dto, items, is_following, actor)
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_notifications_content(**kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
return getattr(g, "followers_content", "")
|
from shared.services.registry import services
|
||||||
|
actor = _require_actor()
|
||||||
|
items = await services.federation.get_notifications(g.s, actor.id)
|
||||||
def _h_actor_timeline_content():
|
await services.federation.mark_notifications_read(g.s, actor.id)
|
||||||
from quart import g
|
from sx.sx_components import _notifications_content_sx
|
||||||
return getattr(g, "actor_timeline_content", "")
|
return _notifications_content_sx(items)
|
||||||
|
|
||||||
|
|
||||||
def _h_notifications_content():
|
|
||||||
from quart import g
|
|
||||||
return getattr(g, "notifications_content", "")
|
|
||||||
|
|||||||
@@ -1,49 +1,49 @@
|
|||||||
;; Federation social pages
|
;; Federation social pages
|
||||||
|
|
||||||
(defpage home-timeline
|
(defpage home-timeline
|
||||||
:path "/"
|
:path "/social/"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (home-timeline-content))
|
:content (home-timeline-content))
|
||||||
|
|
||||||
(defpage public-timeline
|
(defpage public-timeline
|
||||||
:path "/public"
|
:path "/social/public"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (public-timeline-content))
|
:content (public-timeline-content))
|
||||||
|
|
||||||
(defpage compose-form
|
(defpage compose-form
|
||||||
:path "/compose"
|
:path "/social/compose"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (compose-content))
|
:content (compose-content))
|
||||||
|
|
||||||
(defpage search
|
(defpage search
|
||||||
:path "/search"
|
:path "/social/search"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (search-content))
|
:content (search-content))
|
||||||
|
|
||||||
(defpage following-list
|
(defpage following-list
|
||||||
:path "/following"
|
:path "/social/following"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (following-content))
|
:content (following-content))
|
||||||
|
|
||||||
(defpage followers-list
|
(defpage followers-list
|
||||||
:path "/followers"
|
:path "/social/followers"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (followers-content))
|
:content (followers-content))
|
||||||
|
|
||||||
(defpage actor-timeline
|
(defpage actor-timeline
|
||||||
:path "/actor/<int:id>"
|
:path "/social/actor/<int:id>"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (actor-timeline-content))
|
:content (actor-timeline-content id))
|
||||||
|
|
||||||
(defpage notifications
|
(defpage notifications
|
||||||
:path "/notifications"
|
:path "/social/notifications"
|
||||||
:auth :login
|
:auth :login
|
||||||
:layout :social
|
:layout :social
|
||||||
:content (notifications-content))
|
:content (notifications-content))
|
||||||
|
|||||||
@@ -103,21 +103,16 @@ def create_app() -> "Quart":
|
|||||||
from sxc.pages import setup_market_pages
|
from sxc.pages import setup_market_pages
|
||||||
setup_market_pages()
|
setup_market_pages()
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
|
|
||||||
# All markets: / — global view across all pages
|
# All markets: / — global view across all pages
|
||||||
all_markets_bp = register_all_markets()
|
all_markets_bp = register_all_markets()
|
||||||
mount_pages(all_markets_bp, "market", names=["all-markets-index"])
|
|
||||||
app.register_blueprint(all_markets_bp, url_prefix="/")
|
app.register_blueprint(all_markets_bp, url_prefix="/")
|
||||||
|
|
||||||
# Page markets: /<slug>/ — markets for a single page
|
# Page markets: /<slug>/ — markets for a single page
|
||||||
page_markets_bp = register_page_markets()
|
page_markets_bp = register_page_markets()
|
||||||
mount_pages(page_markets_bp, "market", names=["page-markets-index"])
|
|
||||||
app.register_blueprint(page_markets_bp, url_prefix="/<slug>")
|
app.register_blueprint(page_markets_bp, url_prefix="/<slug>")
|
||||||
|
|
||||||
# Page admin: /<slug>/admin/ — post-level admin for markets
|
# Page admin: /<slug>/admin/ — post-level admin for markets
|
||||||
page_admin_bp = register_page_admin()
|
page_admin_bp = register_page_admin()
|
||||||
mount_pages(page_admin_bp, "market", names=["page-admin"])
|
|
||||||
app.register_blueprint(page_admin_bp, url_prefix="/<slug>/admin")
|
app.register_blueprint(page_admin_bp, url_prefix="/<slug>/admin")
|
||||||
|
|
||||||
# Market blueprint nested under post slug: /<page_slug>/<market_slug>/
|
# Market blueprint nested under post slug: /<page_slug>/<market_slug>/
|
||||||
@@ -135,6 +130,10 @@ def create_app() -> "Quart":
|
|||||||
app.register_blueprint(register_actions())
|
app.register_blueprint(register_actions())
|
||||||
app.register_blueprint(register_data())
|
app.register_blueprint(register_data())
|
||||||
|
|
||||||
|
# Auto-mount all defpages with absolute paths
|
||||||
|
from shared.sx.pages import auto_mount_pages
|
||||||
|
auto_mount_pages(app, "market")
|
||||||
|
|
||||||
# --- Auto-inject slugs into url_for() calls ---
|
# --- Auto-inject slugs into url_for() calls ---
|
||||||
@app.url_value_preprocessor
|
@app.url_value_preprocessor
|
||||||
def pull_slugs(endpoint, values):
|
def pull_slugs(endpoint, values):
|
||||||
|
|||||||
@@ -41,19 +41,6 @@ async def _load_markets(page, per_page=20):
|
|||||||
def register() -> Blueprint:
|
def register() -> Blueprint:
|
||||||
bp = Blueprint("all_markets", __name__)
|
bp = Blueprint("all_markets", __name__)
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
"""Load all-markets data for defpage routes."""
|
|
||||||
endpoint = request.endpoint or ""
|
|
||||||
if not endpoint.endswith("defpage_all_markets_index"):
|
|
||||||
return
|
|
||||||
page = int(request.args.get("page", 1))
|
|
||||||
markets, has_more, page_info = await _load_markets(page)
|
|
||||||
g.all_markets_data = {
|
|
||||||
"markets": markets, "has_more": has_more,
|
|
||||||
"page_info": page_info, "page": page,
|
|
||||||
}
|
|
||||||
|
|
||||||
@bp.get("/all-markets")
|
@bp.get("/all-markets")
|
||||||
async def markets_fragment():
|
async def markets_fragment():
|
||||||
page = int(request.args.get("page", 1))
|
page = int(request.args.get("page", 1))
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ def register():
|
|||||||
register_product(),
|
register_product(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Mount defpage for market home (GET /)
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(browse_bp, "market", names=["market-home"])
|
|
||||||
|
|
||||||
@browse_bp.get("/all/")
|
@browse_bp.get("/all/")
|
||||||
@cache_page(tag="browse")
|
@cache_page(tag="browse")
|
||||||
async def browse_all():
|
async def browse_all():
|
||||||
|
|||||||
@@ -5,9 +5,4 @@ from quart import Blueprint
|
|||||||
|
|
||||||
def register():
|
def register():
|
||||||
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
bp = Blueprint("admin", __name__, url_prefix='/admin')
|
||||||
|
|
||||||
# Mount defpage for market admin (GET /)
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "market", names=["market-admin"])
|
|
||||||
|
|
||||||
return bp
|
return bp
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, render_template, make_response, url_for
|
from quart import Blueprint, g, make_response, url_for
|
||||||
|
|
||||||
|
|
||||||
from ..browse.routes import register as register_browse_bp
|
from ..browse.routes import register as register_browse_bp
|
||||||
|
|||||||
@@ -26,17 +26,6 @@ def _slugify(value: str, max_len: int = 255) -> str:
|
|||||||
def register():
|
def register():
|
||||||
bp = Blueprint("page_admin", __name__)
|
bp = Blueprint("page_admin", __name__)
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
"""Pre-render page admin content for defpage (async helper)."""
|
|
||||||
endpoint = request.endpoint or ""
|
|
||||||
if request.method != "GET" or not endpoint.endswith("defpage_page_admin"):
|
|
||||||
return
|
|
||||||
from shared.sx.page import get_template_context
|
|
||||||
from sx.sx_components import _markets_admin_panel_sx
|
|
||||||
ctx = await get_template_context()
|
|
||||||
g.page_admin_content = await _markets_admin_panel_sx(ctx)
|
|
||||||
|
|
||||||
@bp.post("/new/")
|
@bp.post("/new/")
|
||||||
@require_admin
|
@require_admin
|
||||||
async def create_market(**kwargs):
|
async def create_market(**kwargs):
|
||||||
|
|||||||
@@ -23,20 +23,6 @@ async def _load_markets(post_id, page, per_page=20):
|
|||||||
def register() -> Blueprint:
|
def register() -> Blueprint:
|
||||||
bp = Blueprint("page_markets", __name__)
|
bp = Blueprint("page_markets", __name__)
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
"""Load page-markets data for defpage routes."""
|
|
||||||
endpoint = request.endpoint or ""
|
|
||||||
if not endpoint.endswith("defpage_page_markets_index"):
|
|
||||||
return
|
|
||||||
post = g.post_data["post"]
|
|
||||||
page = int(request.args.get("page", 1))
|
|
||||||
markets, has_more = await _load_markets(post["id"], page)
|
|
||||||
g.page_markets_data = {
|
|
||||||
"markets": markets, "has_more": has_more,
|
|
||||||
"page": page, "post_slug": post.get("slug", ""),
|
|
||||||
}
|
|
||||||
|
|
||||||
@bp.get("/page-markets")
|
@bp.get("/page-markets")
|
||||||
async def markets_fragment():
|
async def markets_fragment():
|
||||||
post = g.post_data["post"]
|
post = g.post_data["post"]
|
||||||
|
|||||||
@@ -98,67 +98,77 @@ def _register_market_helpers() -> None:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def _h_all_markets_content():
|
async def _h_all_markets_content(**kw):
|
||||||
from quart import g, url_for, request
|
from quart import g, url_for, request
|
||||||
from shared.utils import route_prefix
|
from shared.utils import route_prefix
|
||||||
|
from shared.services.registry import services
|
||||||
|
from shared.infrastructure.data_client import fetch_data
|
||||||
|
from shared.contracts.dtos import PostDTO, dto_from_dict
|
||||||
|
|
||||||
data = getattr(g, "all_markets_data", None)
|
page = int(request.args.get("page", 1))
|
||||||
if not data:
|
markets, has_more = await services.market.list_marketplaces(
|
||||||
|
g.s, page=page, per_page=20,
|
||||||
|
)
|
||||||
|
|
||||||
|
page_info = {}
|
||||||
|
if markets:
|
||||||
|
post_ids = list({m.container_id for m in markets if m.container_type == "page"})
|
||||||
|
if post_ids:
|
||||||
|
raw_posts = await fetch_data("blog", "posts-by-ids",
|
||||||
|
params={"ids": ",".join(str(i) for i in post_ids)},
|
||||||
|
required=False) or []
|
||||||
|
for raw_p in raw_posts:
|
||||||
|
p = dto_from_dict(PostDTO, raw_p)
|
||||||
|
page_info[p.id] = {"title": p.title, "slug": p.slug}
|
||||||
|
|
||||||
|
if not markets:
|
||||||
from sx.sx_components import _no_markets_sx
|
from sx.sx_components import _no_markets_sx
|
||||||
return _no_markets_sx()
|
return _no_markets_sx()
|
||||||
|
|
||||||
markets = data["markets"]
|
|
||||||
has_more = data["has_more"]
|
|
||||||
page_info = data["page_info"]
|
|
||||||
page = data["page"]
|
|
||||||
|
|
||||||
prefix = route_prefix()
|
prefix = route_prefix()
|
||||||
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
|
next_url = prefix + url_for("all_markets.markets_fragment", page=page + 1)
|
||||||
|
|
||||||
from sx.sx_components import _market_cards_sx, _markets_grid, _no_markets_sx
|
from sx.sx_components import _market_cards_sx, _markets_grid
|
||||||
if markets:
|
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
|
||||||
cards = _market_cards_sx(markets, page_info, page, has_more, next_url)
|
content = _markets_grid(cards)
|
||||||
content = _markets_grid(cards)
|
|
||||||
else:
|
|
||||||
content = _no_markets_sx()
|
|
||||||
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
||||||
|
|
||||||
|
|
||||||
def _h_page_markets_content():
|
async def _h_page_markets_content(slug=None, **kw):
|
||||||
from quart import g, url_for
|
from quart import g, url_for, request
|
||||||
from shared.utils import route_prefix
|
from shared.utils import route_prefix
|
||||||
|
from shared.services.registry import services
|
||||||
|
|
||||||
data = getattr(g, "page_markets_data", None)
|
post = g.post_data["post"]
|
||||||
if not data:
|
page = int(request.args.get("page", 1))
|
||||||
|
markets, has_more = await services.market.list_marketplaces(
|
||||||
|
g.s, "page", post["id"], page=page, per_page=20,
|
||||||
|
)
|
||||||
|
post_slug = post.get("slug", "")
|
||||||
|
|
||||||
|
if not markets:
|
||||||
from sx.sx_components import _no_markets_sx
|
from sx.sx_components import _no_markets_sx
|
||||||
return _no_markets_sx("No markets for this page")
|
return _no_markets_sx("No markets for this page")
|
||||||
|
|
||||||
markets = data["markets"]
|
|
||||||
has_more = data["has_more"]
|
|
||||||
page = data["page"]
|
|
||||||
post_slug = data.get("post_slug", "")
|
|
||||||
|
|
||||||
prefix = route_prefix()
|
prefix = route_prefix()
|
||||||
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
|
next_url = prefix + url_for("page_markets.markets_fragment", page=page + 1)
|
||||||
|
|
||||||
from sx.sx_components import _market_cards_sx, _markets_grid, _no_markets_sx
|
from sx.sx_components import _market_cards_sx, _markets_grid
|
||||||
if markets:
|
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
|
||||||
cards = _market_cards_sx(markets, {}, page, has_more, next_url,
|
show_page_badge=False, post_slug=post_slug)
|
||||||
show_page_badge=False, post_slug=post_slug)
|
content = _markets_grid(cards)
|
||||||
content = _markets_grid(cards)
|
|
||||||
else:
|
|
||||||
content = _no_markets_sx("No markets for this page")
|
|
||||||
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
return "(<> " + content + " " + '(div :class "pb-8")' + ")"
|
||||||
|
|
||||||
|
|
||||||
def _h_page_admin_content():
|
async def _h_page_admin_content(slug=None, **kw):
|
||||||
# Content pre-rendered by before_request (async _markets_admin_panel_sx)
|
from shared.sx.page import get_template_context
|
||||||
from quart import g
|
from sx.sx_components import _markets_admin_panel_sx
|
||||||
content = getattr(g, "page_admin_content", "")
|
ctx = await get_template_context()
|
||||||
|
content = await _markets_admin_panel_sx(ctx)
|
||||||
return '(div :id "main-panel" ' + content + ')'
|
return '(div :id "main-panel" ' + content + ')'
|
||||||
|
|
||||||
|
|
||||||
def _h_market_home_content():
|
def _h_market_home_content(page_slug=None, market_slug=None, **kw):
|
||||||
from quart import g
|
from quart import g
|
||||||
post_data = getattr(g, "post_data", {})
|
post_data = getattr(g, "post_data", {})
|
||||||
post = post_data.get("post", {})
|
post = post_data.get("post", {})
|
||||||
@@ -166,5 +176,5 @@ def _h_market_home_content():
|
|||||||
return _market_landing_content_sx(post)
|
return _market_landing_content_sx(post)
|
||||||
|
|
||||||
|
|
||||||
def _h_market_admin_content():
|
def _h_market_admin_content(page_slug=None, market_slug=None, **kw):
|
||||||
return '"market admin"'
|
return '"market admin"'
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
;; Market app defpage declarations.
|
;; Market app defpage declarations.
|
||||||
;;
|
;;
|
||||||
;; all-markets-index: / — global view across all pages
|
;; all-markets-index: / — global view across all pages
|
||||||
;; page-markets-index: / (on page_markets bp, mounted at /<slug>)
|
;; page-markets-index: /<slug>/ — markets for a single page
|
||||||
;; page-admin: / (on page_admin bp, mounted at /<slug>/admin)
|
;; page-admin: /<slug>/admin/ — post-level admin for markets
|
||||||
;; market-home: / (on browse bp, mounted at /<page_slug>/<market_slug>)
|
;; market-home: /<page_slug>/<market_slug>/ — market landing page
|
||||||
;; market-admin: / (on admin bp, mounted at /<page_slug>/<market_slug>/admin)
|
;; market-admin: /<page_slug>/<market_slug>/admin/ — market admin
|
||||||
|
|
||||||
(defpage all-markets-index
|
(defpage all-markets-index
|
||||||
:path "/"
|
:path "/"
|
||||||
@@ -13,25 +13,25 @@
|
|||||||
:content (all-markets-content))
|
:content (all-markets-content))
|
||||||
|
|
||||||
(defpage page-markets-index
|
(defpage page-markets-index
|
||||||
:path "/"
|
:path "/<slug>/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :post
|
:layout :post
|
||||||
:content (page-markets-content))
|
:content (page-markets-content))
|
||||||
|
|
||||||
(defpage page-admin
|
(defpage page-admin
|
||||||
:path "/"
|
:path "/<slug>/admin/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout (:post-admin :selected "markets")
|
:layout (:post-admin :selected "markets")
|
||||||
:content (page-admin-content))
|
:content (page-admin-content))
|
||||||
|
|
||||||
(defpage market-home
|
(defpage market-home
|
||||||
:path "/"
|
:path "/<page_slug>/<market_slug>/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout :market
|
:layout :market
|
||||||
:content (market-home-content))
|
:content (market-home-content))
|
||||||
|
|
||||||
(defpage market-admin
|
(defpage market-admin
|
||||||
:path "/"
|
:path "/<page_slug>/<market_slug>/admin/"
|
||||||
:auth :admin
|
:auth :admin
|
||||||
:layout (:market-admin :selected "markets")
|
:layout (:market-admin :selected "markets")
|
||||||
:content (market-admin-content))
|
:content (market-admin-content))
|
||||||
|
|||||||
@@ -81,12 +81,13 @@ def create_app() -> "Quart":
|
|||||||
app.register_blueprint(register_actions())
|
app.register_blueprint(register_actions())
|
||||||
app.register_blueprint(register_data())
|
app.register_blueprint(register_data())
|
||||||
|
|
||||||
# Orders list at / (defpage routes mounted below)
|
# Orders list at /
|
||||||
bp = register_orders(url_prefix="/")
|
bp = register_orders(url_prefix="/")
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "orders")
|
|
||||||
app.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
|
from shared.sx.pages import auto_mount_pages
|
||||||
|
auto_mount_pages(app, "orders")
|
||||||
|
|
||||||
# Checkout webhook + return
|
# Checkout webhook + return
|
||||||
app.register_blueprint(register_checkout())
|
app.register_blueprint(register_checkout())
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, redirect, url_for, make_response, request
|
from quart import Blueprint, g, redirect, url_for, request
|
||||||
from sqlalchemy import select, func, or_, cast, String, exists
|
from sqlalchemy import select, func, or_, cast, String, exists
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
@@ -8,7 +8,6 @@ from shared.models.order import Order, OrderItem
|
|||||||
|
|
||||||
from shared.infrastructure.http_utils import vary as _vary, current_url_without_page as _current_url_without_page
|
from shared.infrastructure.http_utils import vary as _vary, current_url_without_page as _current_url_without_page
|
||||||
from shared.infrastructure.cart_identity import current_cart_identity
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
|
||||||
from bp.order.routes import register as register_order
|
from bp.order.routes import register as register_order
|
||||||
|
|
||||||
from .filters.qs import makeqs_factory, decode
|
from .filters.qs import makeqs_factory, decode
|
||||||
@@ -31,112 +30,6 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
if not ident["user_id"] and not ident["session_id"]:
|
if not ident["user_id"] and not ident["session_id"]:
|
||||||
return redirect(url_for("auth.login_form"))
|
return redirect(url_for("auth.login_form"))
|
||||||
|
|
||||||
@bp.before_request
|
|
||||||
async def _prepare_page_data():
|
|
||||||
"""Load data for defpage routes into g.*."""
|
|
||||||
if request.method != "GET":
|
|
||||||
return
|
|
||||||
|
|
||||||
endpoint = request.endpoint or ""
|
|
||||||
|
|
||||||
# Orders list page
|
|
||||||
if endpoint.endswith("defpage_orders_list"):
|
|
||||||
ident = current_cart_identity()
|
|
||||||
if ident["user_id"]:
|
|
||||||
owner_clause = Order.user_id == ident["user_id"]
|
|
||||||
elif ident["session_id"]:
|
|
||||||
owner_clause = Order.session_id == ident["session_id"]
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
|
|
||||||
q = decode()
|
|
||||||
page, search = q.page, q.search
|
|
||||||
if page < 1:
|
|
||||||
page = 1
|
|
||||||
|
|
||||||
where_clause = _search_clause(search) if search else None
|
|
||||||
|
|
||||||
count_stmt = select(func.count()).select_from(Order).where(owner_clause)
|
|
||||||
if where_clause is not None:
|
|
||||||
count_stmt = count_stmt.where(where_clause)
|
|
||||||
|
|
||||||
total_count_result = await g.s.execute(count_stmt)
|
|
||||||
total_count = total_count_result.scalar_one() or 0
|
|
||||||
total_pages = max(1, (total_count + ORDERS_PER_PAGE - 1) // ORDERS_PER_PAGE)
|
|
||||||
|
|
||||||
if page > total_pages:
|
|
||||||
page = total_pages
|
|
||||||
|
|
||||||
offset = (page - 1) * ORDERS_PER_PAGE
|
|
||||||
stmt = (
|
|
||||||
select(Order)
|
|
||||||
.where(owner_clause)
|
|
||||||
.order_by(Order.created_at.desc())
|
|
||||||
.offset(offset)
|
|
||||||
.limit(ORDERS_PER_PAGE)
|
|
||||||
)
|
|
||||||
if where_clause is not None:
|
|
||||||
stmt = stmt.where(where_clause)
|
|
||||||
|
|
||||||
result = await g.s.execute(stmt)
|
|
||||||
orders = result.scalars().all()
|
|
||||||
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
pfx = route_prefix()
|
|
||||||
qs_fn = makeqs_factory()
|
|
||||||
|
|
||||||
g.orders_page_data = {
|
|
||||||
"orders": orders,
|
|
||||||
"page": page,
|
|
||||||
"total_pages": total_pages,
|
|
||||||
"search": search,
|
|
||||||
"search_count": total_count,
|
|
||||||
"url_for_fn": url_for,
|
|
||||||
"qs_fn": qs_fn,
|
|
||||||
"list_url": pfx + url_for("orders.defpage_orders_list"),
|
|
||||||
}
|
|
||||||
|
|
||||||
# Order detail page
|
|
||||||
elif endpoint.endswith("defpage_order_detail"):
|
|
||||||
order_id = request.view_args.get("order_id")
|
|
||||||
if order_id is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
ident = current_cart_identity()
|
|
||||||
if ident["user_id"]:
|
|
||||||
owner = Order.user_id == ident["user_id"]
|
|
||||||
elif ident["session_id"]:
|
|
||||||
owner = Order.session_id == ident["session_id"]
|
|
||||||
else:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
return
|
|
||||||
|
|
||||||
result = await g.s.execute(
|
|
||||||
select(Order)
|
|
||||||
.options(selectinload(Order.items))
|
|
||||||
.where(Order.id == order_id, owner)
|
|
||||||
)
|
|
||||||
order = result.scalar_one_or_none()
|
|
||||||
if not order:
|
|
||||||
from quart import abort
|
|
||||||
abort(404)
|
|
||||||
return
|
|
||||||
|
|
||||||
from shared.utils import route_prefix
|
|
||||||
from shared.browser.app.csrf import generate_csrf_token
|
|
||||||
pfx = route_prefix()
|
|
||||||
|
|
||||||
g.order_detail_data = {
|
|
||||||
"order": order,
|
|
||||||
"calendar_entries": None,
|
|
||||||
"detail_url": pfx + url_for("orders.defpage_order_detail", order_id=order.id),
|
|
||||||
"list_url": pfx + url_for("orders.defpage_orders_list"),
|
|
||||||
"recheck_url": pfx + url_for("orders.order.order_recheck", order_id=order.id),
|
|
||||||
"pay_url": pfx + url_for("orders.order.order_pay", order_id=order.id),
|
|
||||||
"csrf_token": generate_csrf_token(),
|
|
||||||
}
|
|
||||||
|
|
||||||
@bp.get("/rows")
|
@bp.get("/rows")
|
||||||
async def orders_rows():
|
async def orders_rows():
|
||||||
"""Pagination endpoint — returns order rows for page > 1."""
|
"""Pagination endpoint — returns order rows for page > 1."""
|
||||||
|
|||||||
@@ -119,7 +119,143 @@ def _register_orders_helpers() -> None:
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def _h_orders_list_content():
|
async def _ensure_orders_list():
|
||||||
|
"""Fetch orders list data and store in g.orders_page_data."""
|
||||||
|
from quart import g, url_for
|
||||||
|
if hasattr(g, "orders_page_data"):
|
||||||
|
return
|
||||||
|
from sqlalchemy import select, func, or_, cast, String, exists
|
||||||
|
from shared.models.order import Order, OrderItem
|
||||||
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from shared.utils import route_prefix
|
||||||
|
|
||||||
|
ORDERS_PER_PAGE = 10
|
||||||
|
ident = current_cart_identity()
|
||||||
|
if ident["user_id"]:
|
||||||
|
owner_clause = Order.user_id == ident["user_id"]
|
||||||
|
elif ident["session_id"]:
|
||||||
|
owner_clause = Order.session_id == ident["session_id"]
|
||||||
|
else:
|
||||||
|
g.orders_page_data = None
|
||||||
|
return
|
||||||
|
|
||||||
|
from bp.orders.filters.qs import makeqs_factory, decode
|
||||||
|
q = decode()
|
||||||
|
page, search = q.page, q.search
|
||||||
|
if page < 1:
|
||||||
|
page = 1
|
||||||
|
|
||||||
|
where_clause = None
|
||||||
|
if search:
|
||||||
|
term = f"%{search.strip()}%"
|
||||||
|
conditions = [
|
||||||
|
Order.status.ilike(term),
|
||||||
|
Order.currency.ilike(term),
|
||||||
|
Order.sumup_checkout_id.ilike(term),
|
||||||
|
Order.sumup_status.ilike(term),
|
||||||
|
Order.description.ilike(term),
|
||||||
|
]
|
||||||
|
conditions.append(
|
||||||
|
exists(
|
||||||
|
select(1).select_from(OrderItem)
|
||||||
|
.where(OrderItem.order_id == Order.id,
|
||||||
|
or_(OrderItem.product_title.ilike(term),
|
||||||
|
OrderItem.product_slug.ilike(term)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
search_id = int(search)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
search_id = None
|
||||||
|
if search_id is not None:
|
||||||
|
conditions.append(Order.id == search_id)
|
||||||
|
else:
|
||||||
|
conditions.append(cast(Order.id, String).ilike(term))
|
||||||
|
where_clause = or_(*conditions)
|
||||||
|
|
||||||
|
count_stmt = select(func.count()).select_from(Order).where(owner_clause)
|
||||||
|
if where_clause is not None:
|
||||||
|
count_stmt = count_stmt.where(where_clause)
|
||||||
|
|
||||||
|
total_count_result = await g.s.execute(count_stmt)
|
||||||
|
total_count = total_count_result.scalar_one() or 0
|
||||||
|
total_pages = max(1, (total_count + ORDERS_PER_PAGE - 1) // ORDERS_PER_PAGE)
|
||||||
|
if page > total_pages:
|
||||||
|
page = total_pages
|
||||||
|
|
||||||
|
offset = (page - 1) * ORDERS_PER_PAGE
|
||||||
|
stmt = (
|
||||||
|
select(Order).where(owner_clause)
|
||||||
|
.order_by(Order.created_at.desc())
|
||||||
|
.offset(offset).limit(ORDERS_PER_PAGE)
|
||||||
|
)
|
||||||
|
if where_clause is not None:
|
||||||
|
stmt = stmt.where(where_clause)
|
||||||
|
|
||||||
|
result = await g.s.execute(stmt)
|
||||||
|
orders = result.scalars().all()
|
||||||
|
pfx = route_prefix()
|
||||||
|
qs_fn = makeqs_factory()
|
||||||
|
|
||||||
|
g.orders_page_data = {
|
||||||
|
"orders": orders,
|
||||||
|
"page": page,
|
||||||
|
"total_pages": total_pages,
|
||||||
|
"search": search,
|
||||||
|
"search_count": total_count,
|
||||||
|
"url_for_fn": url_for,
|
||||||
|
"qs_fn": qs_fn,
|
||||||
|
"list_url": pfx + url_for("defpage_orders_list"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_order_detail(order_id):
|
||||||
|
"""Fetch order detail data and store in g.order_detail_data."""
|
||||||
|
from quart import g, url_for, abort
|
||||||
|
if hasattr(g, "order_detail_data"):
|
||||||
|
return
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
from shared.models.order import Order
|
||||||
|
from shared.infrastructure.cart_identity import current_cart_identity
|
||||||
|
from shared.utils import route_prefix
|
||||||
|
from shared.browser.app.csrf import generate_csrf_token
|
||||||
|
|
||||||
|
if order_id is None:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
ident = current_cart_identity()
|
||||||
|
if ident["user_id"]:
|
||||||
|
owner = Order.user_id == ident["user_id"]
|
||||||
|
elif ident["session_id"]:
|
||||||
|
owner = Order.session_id == ident["session_id"]
|
||||||
|
else:
|
||||||
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
result = await g.s.execute(
|
||||||
|
select(Order).options(selectinload(Order.items))
|
||||||
|
.where(Order.id == order_id, owner)
|
||||||
|
)
|
||||||
|
order = result.scalar_one_or_none()
|
||||||
|
if not order:
|
||||||
|
abort(404)
|
||||||
|
return
|
||||||
|
|
||||||
|
pfx = route_prefix()
|
||||||
|
g.order_detail_data = {
|
||||||
|
"order": order,
|
||||||
|
"calendar_entries": None,
|
||||||
|
"detail_url": pfx + url_for("defpage_order_detail", order_id=order.id),
|
||||||
|
"list_url": pfx + url_for("defpage_orders_list"),
|
||||||
|
"recheck_url": pfx + url_for("orders.order.order_recheck", order_id=order.id),
|
||||||
|
"pay_url": pfx + url_for("orders.order.order_pay", order_id=order.id),
|
||||||
|
"csrf_token": generate_csrf_token(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def _h_orders_list_content(**kw):
|
||||||
|
await _ensure_orders_list()
|
||||||
from quart import g
|
from quart import g
|
||||||
d = getattr(g, "orders_page_data", None)
|
d = getattr(g, "orders_page_data", None)
|
||||||
if not d:
|
if not d:
|
||||||
@@ -131,7 +267,8 @@ def _h_orders_list_content():
|
|||||||
return _orders_main_panel_sx(d["orders"], rows)
|
return _orders_main_panel_sx(d["orders"], rows)
|
||||||
|
|
||||||
|
|
||||||
def _h_orders_list_filter():
|
async def _h_orders_list_filter(**kw):
|
||||||
|
await _ensure_orders_list()
|
||||||
from quart import g
|
from quart import g
|
||||||
from shared.sx.helpers import sx_call, SxExpr
|
from shared.sx.helpers import sx_call, SxExpr
|
||||||
from shared.sx.page import SEARCH_HEADERS_MOBILE
|
from shared.sx.page import SEARCH_HEADERS_MOBILE
|
||||||
@@ -148,7 +285,8 @@ def _h_orders_list_filter():
|
|||||||
return sx_call("order-list-header", search_mobile=SxExpr(search_mobile))
|
return sx_call("order-list-header", search_mobile=SxExpr(search_mobile))
|
||||||
|
|
||||||
|
|
||||||
def _h_orders_list_aside():
|
async def _h_orders_list_aside(**kw):
|
||||||
|
await _ensure_orders_list()
|
||||||
from quart import g
|
from quart import g
|
||||||
from shared.sx.helpers import sx_call
|
from shared.sx.helpers import sx_call
|
||||||
from shared.sx.page import SEARCH_HEADERS_DESKTOP
|
from shared.sx.page import SEARCH_HEADERS_DESKTOP
|
||||||
@@ -164,13 +302,15 @@ def _h_orders_list_aside():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _h_orders_list_url():
|
async def _h_orders_list_url(**kw):
|
||||||
|
await _ensure_orders_list()
|
||||||
from quart import g
|
from quart import g
|
||||||
d = getattr(g, "orders_page_data", None)
|
d = getattr(g, "orders_page_data", None)
|
||||||
return d["list_url"] if d else "/"
|
return d["list_url"] if d else "/"
|
||||||
|
|
||||||
|
|
||||||
def _h_order_detail_content():
|
async def _h_order_detail_content(order_id=None, **kw):
|
||||||
|
await _ensure_order_detail(order_id)
|
||||||
from quart import g
|
from quart import g
|
||||||
d = getattr(g, "order_detail_data", None)
|
d = getattr(g, "order_detail_data", None)
|
||||||
if not d:
|
if not d:
|
||||||
@@ -179,7 +319,8 @@ def _h_order_detail_content():
|
|||||||
return _order_main_sx(d["order"], d["calendar_entries"])
|
return _order_main_sx(d["order"], d["calendar_entries"])
|
||||||
|
|
||||||
|
|
||||||
def _h_order_detail_filter():
|
async def _h_order_detail_filter(order_id=None, **kw):
|
||||||
|
await _ensure_order_detail(order_id)
|
||||||
from quart import g
|
from quart import g
|
||||||
d = getattr(g, "order_detail_data", None)
|
d = getattr(g, "order_detail_data", None)
|
||||||
if not d:
|
if not d:
|
||||||
@@ -189,13 +330,15 @@ def _h_order_detail_filter():
|
|||||||
d["pay_url"], d["csrf_token"])
|
d["pay_url"], d["csrf_token"])
|
||||||
|
|
||||||
|
|
||||||
def _h_order_detail_url():
|
async def _h_order_detail_url(order_id=None, **kw):
|
||||||
|
await _ensure_order_detail(order_id)
|
||||||
from quart import g
|
from quart import g
|
||||||
d = getattr(g, "order_detail_data", None)
|
d = getattr(g, "order_detail_data", None)
|
||||||
return d["detail_url"] if d else "/"
|
return d["detail_url"] if d else "/"
|
||||||
|
|
||||||
|
|
||||||
def _h_order_list_url_from_detail():
|
async def _h_order_list_url_from_detail(order_id=None, **kw):
|
||||||
|
await _ensure_order_detail(order_id)
|
||||||
from quart import g
|
from quart import g
|
||||||
d = getattr(g, "order_detail_data", None)
|
d = getattr(g, "order_detail_data", None)
|
||||||
return d["list_url"] if d else "/"
|
return d["list_url"] if d else "/"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
:path "/<int:order_id>/"
|
:path "/<int:order_id>/"
|
||||||
:auth :public
|
:auth :public
|
||||||
:layout (:order-detail
|
:layout (:order-detail
|
||||||
:list-url (order-list-url-from-detail)
|
:list-url (order-list-url-from-detail order-id)
|
||||||
:detail-url (order-detail-url))
|
:detail-url (order-detail-url order-id))
|
||||||
:filter (order-detail-filter)
|
:filter (order-detail-filter order-id)
|
||||||
:content (order-detail-content))
|
:content (order-detail-content order-id))
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ Usage::
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import inspect
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
from .types import Component, Keyword, Lambda, Macro, NIL, Symbol
|
||||||
@@ -114,7 +115,10 @@ async def async_eval(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any
|
|||||||
args = [await async_eval(a, env, ctx) for a in expr[1:]]
|
args = [await async_eval(a, env, ctx) for a in expr[1:]]
|
||||||
|
|
||||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||||
return fn(*args)
|
result = fn(*args)
|
||||||
|
if inspect.iscoroutine(result):
|
||||||
|
return await result
|
||||||
|
return result
|
||||||
if isinstance(fn, Lambda):
|
if isinstance(fn, Lambda):
|
||||||
return await _async_call_lambda(fn, args, env, ctx)
|
return await _async_call_lambda(fn, args, env, ctx)
|
||||||
if isinstance(fn, Component):
|
if isinstance(fn, Component):
|
||||||
@@ -369,6 +373,8 @@ async def _asf_thread_first(expr, env, ctx):
|
|||||||
args = [result]
|
args = [result]
|
||||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||||
result = fn(*args)
|
result = fn(*args)
|
||||||
|
if inspect.iscoroutine(result):
|
||||||
|
result = await result
|
||||||
elif isinstance(fn, Lambda):
|
elif isinstance(fn, Lambda):
|
||||||
result = await _async_call_lambda(fn, args, env, ctx)
|
result = await _async_call_lambda(fn, args, env, ctx)
|
||||||
else:
|
else:
|
||||||
@@ -418,7 +424,8 @@ async def _aho_map(expr, env, ctx):
|
|||||||
if isinstance(fn, Lambda):
|
if isinstance(fn, Lambda):
|
||||||
results.append(await _async_call_lambda(fn, [item], env, ctx))
|
results.append(await _async_call_lambda(fn, [item], env, ctx))
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
results.append(fn(item))
|
r = fn(item)
|
||||||
|
results.append(await r if inspect.iscoroutine(r) else r)
|
||||||
else:
|
else:
|
||||||
raise EvalError(f"map requires callable, got {type(fn).__name__}")
|
raise EvalError(f"map requires callable, got {type(fn).__name__}")
|
||||||
return results
|
return results
|
||||||
@@ -432,7 +439,8 @@ async def _aho_map_indexed(expr, env, ctx):
|
|||||||
if isinstance(fn, Lambda):
|
if isinstance(fn, Lambda):
|
||||||
results.append(await _async_call_lambda(fn, [i, item], env, ctx))
|
results.append(await _async_call_lambda(fn, [i, item], env, ctx))
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
results.append(fn(i, item))
|
r = fn(i, item)
|
||||||
|
results.append(await r if inspect.iscoroutine(r) else r)
|
||||||
else:
|
else:
|
||||||
raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
|
raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
|
||||||
return results
|
return results
|
||||||
@@ -447,6 +455,8 @@ async def _aho_filter(expr, env, ctx):
|
|||||||
val = await _async_call_lambda(fn, [item], env, ctx)
|
val = await _async_call_lambda(fn, [item], env, ctx)
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
val = fn(item)
|
val = fn(item)
|
||||||
|
if inspect.iscoroutine(val):
|
||||||
|
val = await val
|
||||||
else:
|
else:
|
||||||
raise EvalError(f"filter requires callable, got {type(fn).__name__}")
|
raise EvalError(f"filter requires callable, got {type(fn).__name__}")
|
||||||
if val:
|
if val:
|
||||||
@@ -459,7 +469,12 @@ async def _aho_reduce(expr, env, ctx):
|
|||||||
acc = await async_eval(expr[2], env, ctx)
|
acc = await async_eval(expr[2], env, ctx)
|
||||||
coll = await async_eval(expr[3], env, ctx)
|
coll = await async_eval(expr[3], env, ctx)
|
||||||
for item in coll:
|
for item in coll:
|
||||||
acc = await _async_call_lambda(fn, [acc, item], env, ctx) if isinstance(fn, Lambda) else fn(acc, item)
|
if isinstance(fn, Lambda):
|
||||||
|
acc = await _async_call_lambda(fn, [acc, item], env, ctx)
|
||||||
|
else:
|
||||||
|
acc = fn(acc, item)
|
||||||
|
if inspect.iscoroutine(acc):
|
||||||
|
acc = await acc
|
||||||
return acc
|
return acc
|
||||||
|
|
||||||
|
|
||||||
@@ -467,7 +482,12 @@ async def _aho_some(expr, env, ctx):
|
|||||||
fn = await async_eval(expr[1], env, ctx)
|
fn = await async_eval(expr[1], env, ctx)
|
||||||
coll = await async_eval(expr[2], env, ctx)
|
coll = await async_eval(expr[2], env, ctx)
|
||||||
for item in coll:
|
for item in coll:
|
||||||
result = await _async_call_lambda(fn, [item], env, ctx) if isinstance(fn, Lambda) else fn(item)
|
if isinstance(fn, Lambda):
|
||||||
|
result = await _async_call_lambda(fn, [item], env, ctx)
|
||||||
|
else:
|
||||||
|
result = fn(item)
|
||||||
|
if inspect.iscoroutine(result):
|
||||||
|
result = await result
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
return NIL
|
return NIL
|
||||||
@@ -477,7 +497,13 @@ async def _aho_every(expr, env, ctx):
|
|||||||
fn = await async_eval(expr[1], env, ctx)
|
fn = await async_eval(expr[1], env, ctx)
|
||||||
coll = await async_eval(expr[2], env, ctx)
|
coll = await async_eval(expr[2], env, ctx)
|
||||||
for item in coll:
|
for item in coll:
|
||||||
if not (await _async_call_lambda(fn, [item], env, ctx) if isinstance(fn, Lambda) else fn(item)):
|
if isinstance(fn, Lambda):
|
||||||
|
val = await _async_call_lambda(fn, [item], env, ctx)
|
||||||
|
else:
|
||||||
|
val = fn(item)
|
||||||
|
if inspect.iscoroutine(val):
|
||||||
|
val = await val
|
||||||
|
if not val:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -489,7 +515,9 @@ async def _aho_for_each(expr, env, ctx):
|
|||||||
if isinstance(fn, Lambda):
|
if isinstance(fn, Lambda):
|
||||||
await _async_call_lambda(fn, [item], env, ctx)
|
await _async_call_lambda(fn, [item], env, ctx)
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
fn(item)
|
r = fn(item)
|
||||||
|
if inspect.iscoroutine(r):
|
||||||
|
await r
|
||||||
return NIL
|
return NIL
|
||||||
|
|
||||||
|
|
||||||
@@ -782,7 +810,10 @@ async def _arsf_map(expr, env, ctx):
|
|||||||
if isinstance(fn, Lambda):
|
if isinstance(fn, Lambda):
|
||||||
parts.append(await _arender_lambda(fn, (item,), env, ctx))
|
parts.append(await _arender_lambda(fn, (item,), env, ctx))
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
parts.append(await _arender(fn(item), env, ctx))
|
r = fn(item)
|
||||||
|
if inspect.iscoroutine(r):
|
||||||
|
r = await r
|
||||||
|
parts.append(await _arender(r, env, ctx))
|
||||||
else:
|
else:
|
||||||
parts.append(await _arender(item, env, ctx))
|
parts.append(await _arender(item, env, ctx))
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
@@ -796,7 +827,10 @@ async def _arsf_map_indexed(expr, env, ctx):
|
|||||||
if isinstance(fn, Lambda):
|
if isinstance(fn, Lambda):
|
||||||
parts.append(await _arender_lambda(fn, (i, item), env, ctx))
|
parts.append(await _arender_lambda(fn, (i, item), env, ctx))
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
parts.append(await _arender(fn(i, item), env, ctx))
|
r = fn(i, item)
|
||||||
|
if inspect.iscoroutine(r):
|
||||||
|
r = await r
|
||||||
|
parts.append(await _arender(r, env, ctx))
|
||||||
else:
|
else:
|
||||||
parts.append(await _arender(item, env, ctx))
|
parts.append(await _arender(item, env, ctx))
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
@@ -815,7 +849,10 @@ async def _arsf_for_each(expr, env, ctx):
|
|||||||
if isinstance(fn, Lambda):
|
if isinstance(fn, Lambda):
|
||||||
parts.append(await _arender_lambda(fn, (item,), env, ctx))
|
parts.append(await _arender_lambda(fn, (item,), env, ctx))
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
parts.append(await _arender(fn(item), env, ctx))
|
r = fn(item)
|
||||||
|
if inspect.iscoroutine(r):
|
||||||
|
r = await r
|
||||||
|
parts.append(await _arender(r, env, ctx))
|
||||||
else:
|
else:
|
||||||
parts.append(await _arender(item, env, ctx))
|
parts.append(await _arender(item, env, ctx))
|
||||||
return "".join(parts)
|
return "".join(parts)
|
||||||
@@ -956,7 +993,10 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
|
|||||||
args = [await async_eval(a, env, ctx) for a in expr[1:]]
|
args = [await async_eval(a, env, ctx) for a in expr[1:]]
|
||||||
|
|
||||||
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
if callable(fn) and not isinstance(fn, (Lambda, Component)):
|
||||||
return fn(*args)
|
result = fn(*args)
|
||||||
|
if inspect.iscoroutine(result):
|
||||||
|
return await result
|
||||||
|
return result
|
||||||
if isinstance(fn, Lambda):
|
if isinstance(fn, Lambda):
|
||||||
return await _async_call_lambda(fn, args, env, ctx)
|
return await _async_call_lambda(fn, args, env, ctx)
|
||||||
if isinstance(fn, Component):
|
if isinstance(fn, Component):
|
||||||
@@ -1151,7 +1191,8 @@ async def _asho_ser_map(expr, env, ctx):
|
|||||||
local[p] = v
|
local[p] = v
|
||||||
results.append(await _aser(fn.body, local, ctx))
|
results.append(await _aser(fn.body, local, ctx))
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
results.append(fn(item))
|
r = fn(item)
|
||||||
|
results.append(await r if inspect.iscoroutine(r) else r)
|
||||||
else:
|
else:
|
||||||
raise EvalError(f"map requires callable, got {type(fn).__name__}")
|
raise EvalError(f"map requires callable, got {type(fn).__name__}")
|
||||||
return results
|
return results
|
||||||
@@ -1169,7 +1210,8 @@ async def _asho_ser_map_indexed(expr, env, ctx):
|
|||||||
local[fn.params[1]] = item
|
local[fn.params[1]] = item
|
||||||
results.append(await _aser(fn.body, local, ctx))
|
results.append(await _aser(fn.body, local, ctx))
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
results.append(fn(i, item))
|
r = fn(i, item)
|
||||||
|
results.append(await r if inspect.iscoroutine(r) else r)
|
||||||
else:
|
else:
|
||||||
raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
|
raise EvalError(f"map-indexed requires callable, got {type(fn).__name__}")
|
||||||
return results
|
return results
|
||||||
@@ -1191,7 +1233,8 @@ async def _asho_ser_for_each(expr, env, ctx):
|
|||||||
local[fn.params[0]] = item
|
local[fn.params[0]] = item
|
||||||
results.append(await _aser(fn.body, local, ctx))
|
results.append(await _aser(fn.body, local, ctx))
|
||||||
elif callable(fn):
|
elif callable(fn):
|
||||||
results.append(fn(item))
|
r = fn(item)
|
||||||
|
results.append(await r if inspect.iscoroutine(r) else r)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -290,6 +290,18 @@ async def execute_page(
|
|||||||
# Blueprint mounting
|
# Blueprint mounting
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def auto_mount_pages(app: Any, service_name: str) -> None:
|
||||||
|
"""Auto-mount all registered defpages for a service directly on the app.
|
||||||
|
|
||||||
|
Pages must have absolute paths (from the service URL root).
|
||||||
|
Called once per service in app.py after setup_*_pages().
|
||||||
|
"""
|
||||||
|
pages = get_all_pages(service_name)
|
||||||
|
for page_def in pages.values():
|
||||||
|
_mount_one_page(app, service_name, page_def)
|
||||||
|
logger.info("Auto-mounted %d defpages for %s", len(pages), service_name)
|
||||||
|
|
||||||
|
|
||||||
def mount_pages(bp: Any, service_name: str,
|
def mount_pages(bp: Any, service_name: str,
|
||||||
names: set[str] | list[str] | None = None) -> None:
|
names: set[str] | list[str] | None = None) -> None:
|
||||||
"""Mount registered PageDef routes onto a Quart Blueprint.
|
"""Mount registered PageDef routes onto a Quart Blueprint.
|
||||||
|
|||||||
@@ -54,12 +54,11 @@ def create_app() -> "Quart":
|
|||||||
setup_sx_pages()
|
setup_sx_pages()
|
||||||
|
|
||||||
bp = register_pages(url_prefix="/")
|
bp = register_pages(url_prefix="/")
|
||||||
|
|
||||||
from shared.sx.pages import mount_pages
|
|
||||||
mount_pages(bp, "sx")
|
|
||||||
|
|
||||||
app.register_blueprint(bp)
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
|
from shared.sx.pages import auto_mount_pages
|
||||||
|
auto_mount_pages(app, "sx")
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user