from __future__ import annotations from quart import ( render_template, make_response, g, Blueprint, abort, url_for, request, ) from .services.post_data import post_data from .services.post_operations import toggle_post_like from shared.services.registry import services from shared.infrastructure.fragments import fetch_fragment, fetch_fragments 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 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 fragments from events + market paginate_url = url_for( 'blog.post.widget_paginate', slug=post_slug, widget_domain='calendar', ) nav_params = { "container_type": "page", "container_id": str(db_post_id), "post_slug": post_slug, "paginate_url": paginate_url, } events_nav_html, market_nav_html = await fetch_fragments([ ("events", "container-nav", nav_params), ("market", "container-nav", nav_params), ]) container_nav_html = events_nav_html + market_nav_html ctx = { **p_data, "base_title": f"{config()['title']} {p_data['post']['title']}", "container_nav_html": container_nav_html, } # Page cart badge via service post_dict = p_data.get("post") or {} if post_dict.get("is_page"): ident = current_cart_identity() page_summary = await services.cart.cart_summary( g.s, user_id=ident["user_id"], session_id=ident["session_id"], page_slug=post_dict["slug"], ) 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): # Determine which template to use based on request type if not is_htmx_request(): # Normal browser request: full page with layout html = await render_template("_types/post/index.html") else: # HTMX request: main panel + OOB elements html = await render_template("_types/post/_oob_elements.html") return await make_response(html) @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 # Get post_id from g.post_data if not g.user: html = await render_template( "_types/browse/like/button.html", slug=slug, liked=False, like_url=host_url(url_for('blog.post.like_toggle', slug=slug)), item_type='post', ) resp = make_response(html, 403) return resp post_id = g.post_data["post"]["id"] user_id = g.user.id liked, error = await toggle_post_like(g.s, user_id, post_id) if error: resp = make_response(error, 404) return resp html = await render_template( "_types/browse/like/button.html", slug=slug, liked=liked, like_url=host_url(url_for('blog.post.like_toggle', slug=slug)), item_type='post', ) return html @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