from __future__ import annotations from quart import ( make_response, g, Blueprint, abort, url_for, request, ) from .services.post_data import post_data from shared.infrastructure.data_client import fetch_data from shared.infrastructure.actions import call_action from shared.contracts.dtos import CartSummaryDTO, dto_from_dict from shared.infrastructure.fragments import fetch_fragment from shared.browser.app.redis_cacher import cache_page, clear_cache from .admin.routes import register as register_admin from shared.config import config from shared.browser.app.utils.htmx import is_htmx_request from shared.sx.helpers import sx_response def register(): bp = Blueprint("post", __name__, url_prefix='/') bp.register_blueprint( register_admin() ) # Calendar blueprints now live in the events service. # Post pages link to events_url() instead of embedding calendars. @bp.url_value_preprocessor def pull_blog(endpoint, values): g.post_slug = values.get("slug") @bp.before_request async def hydrate_post_data(): slug = getattr(g, "post_slug", None) if not slug: return # not a blog route or no slug in this URL is_admin = bool((g.get("rights") or {}).get("admin")) # Always include drafts so we can check ownership below p_data = await post_data(slug, g.s, include_drafts=True) if not p_data: abort(404) return # Access control for draft posts if p_data["post"].get("status") != "published": if is_admin: pass # admin can see all drafts elif g.user and p_data["post"].get("user_id") == g.user.id: pass # author can see their own drafts else: abort(404) return g.post_data = p_data @bp.context_processor async def context(): p_data = getattr(g, "post_data", None) if p_data: from shared.infrastructure.cart_identity import current_cart_identity db_post_id = (g.post_data.get("post") or {}).get("id") post_slug = (g.post_data.get("post") or {}).get("slug", "") # Fetch container nav from relations service container_nav = await fetch_fragment("relations", "container-nav", params={ "container_type": "page", "container_id": str(db_post_id), "post_slug": post_slug, }) ctx = { **p_data, "base_title": config()["title"], "container_nav": container_nav, } # Page cart badge via HTTP post_dict = p_data.get("post") or {} if post_dict.get("is_page"): ident = current_cart_identity() summary_params = {"page_slug": post_dict["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) return ctx else: return {} @bp.get("/") @cache_page(tag="post.post_detail") async def post_detail(slug: str): from shared.sx.page import get_template_context from sx.sx_components import render_post_page, render_post_oob tctx = await get_template_context() if not is_htmx_request(): html = await render_post_page(tctx) return await make_response(html) else: sx_src = await render_post_oob(tctx) return sx_response(sx_src) @bp.post("/like/toggle/") @clear_cache(tag="post.post_detail", tag_scope="user") async def like_toggle(slug: str): from shared.utils import host_url from sx.sx_components import render_like_toggle_button like_url = host_url(url_for('blog.post.like_toggle', slug=slug)) # Get post_id from g.post_data if not g.user: return sx_response(render_like_toggle_button(slug, False, like_url), status=403) post_id = g.post_data["post"]["id"] user_id = g.user.id result = await call_action("likes", "toggle", payload={ "user_id": user_id, "target_type": "post", "target_id": post_id, }) liked = result["liked"] return sx_response(render_like_toggle_button(slug, liked, like_url)) @bp.get("/w//") async def widget_paginate(slug: str, widget_domain: str): """Proxies paginated widget requests to the appropriate fragment provider.""" page = int(request.args.get("page", 1)) post_id = g.post_data["post"]["id"] if widget_domain == "calendar": html = await fetch_fragment("events", "container-nav", params={ "container_type": "page", "container_id": str(post_id), "post_slug": slug, "page": str(page), "paginate_url": url_for( 'blog.post.widget_paginate', slug=slug, widget_domain='calendar', ), }) return await make_response(html or "") abort(404) return bp