Rename product blueprint URL param slug → product_slug
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 57s

The app-level url_value_preprocessor pops "slug" and "page_slug"
into g.post_slug, with page_slug overwriting the product slug.
Renaming the blueprint param to product_slug eliminates the
collision entirely. Views now use g.product_slug (set by blueprint
preprocessor) and have clean signatures with no **_kw hacks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-24 10:25:00 +00:00
parent 291c829c7f
commit d92d4840ed
3 changed files with 24 additions and 33 deletions

View File

@@ -22,20 +22,12 @@ from .services.product_operations import toggle_product_like, massage_full_produ
def register(): def register():
bp = Blueprint("product", __name__, url_prefix="/product/<slug>") bp = Blueprint("product", __name__, url_prefix="/product/<product_slug>")
@bp.url_value_preprocessor @bp.url_value_preprocessor
def pull_blog(endpoint, values): def pull_product_slug(endpoint, values):
# App-level preprocessor pops both "slug" and "page_slug" into g.post_slug, # product_slug is distinct from the app-level "slug"/"page_slug" params,
# with page_slug overwriting slug. We need the original product slug which # so it won't be popped by the app-level preprocessor in app.py.
# the app preprocessor popped first — but it's gone. Instead, extract the g.product_slug = values.pop("product_slug", None)
# product slug from the request path: .../product/<slug>/...
from quart import request as req
parts = req.path.rstrip("/").split("/")
try:
idx = parts.index("product")
g.product_slug = parts[idx + 1] if idx + 1 < len(parts) else None
except (ValueError, IndexError):
g.product_slug = None
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
# BEFORE REQUEST: Slug or numeric ID resolver # BEFORE REQUEST: Slug or numeric ID resolver
@@ -71,7 +63,7 @@ def register():
if not is_post: if not is_post:
canon = canonical_html_slug(product["slug"]) canon = canonical_html_slug(product["slug"])
return redirect( return redirect(
host_url(url_for("market.browse.product.product_detail", slug=canon)) host_url(url_for("market.browse.product.product_detail", product_slug=canon))
) )
g.item_data = {"d": product, "slug": product["slug"], "liked": False} g.item_data = {"d": product, "slug": product["slug"], "liked": False}
@@ -84,7 +76,7 @@ def register():
canon = canonical_html_slug(raw_slug) canon = canonical_html_slug(raw_slug)
if canon != raw_slug and not is_post: if canon != raw_slug and not is_post:
return redirect( return redirect(
host_url(url_for("market.browse.product.product_detail", slug=canon)) host_url(url_for("market.browse.product.product_detail", product_slug=canon))
) )
# hydrate full product # hydrate full product
@@ -111,7 +103,7 @@ def register():
# ───────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────
@bp.get("/") @bp.get("/")
@cache_page(tag="browse") @cache_page(tag="browse")
async def product_detail(slug: str): async def product_detail():
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
# Determine which template to use based on request type # Determine which template to use based on request type
@@ -126,9 +118,8 @@ def register():
@bp.post("/like/toggle/") @bp.post("/like/toggle/")
@clear_cache(tag="browse", tag_scope="user") @clear_cache(tag="browse", tag_scope="user")
async def like_toggle(slug): async def like_toggle():
# Use slug from URL parameter (set by url_prefix="/product/<slug>") product_slug = g.product_slug
product_slug = slug
if not g.user: if not g.user:
html = await render_template( html = await render_template(
@@ -157,7 +148,7 @@ def register():
@bp.get("/admin/") @bp.get("/admin/")
async def admin(slug: str): async def admin():
from shared.browser.app.utils.htmx import is_htmx_request from shared.browser.app.utils.htmx import is_htmx_request
if not is_htmx_request(): if not is_htmx_request():
@@ -177,7 +168,7 @@ def register():
@bp.post("/cart/") @bp.post("/cart/")
@clear_cache(tag="browse", tag_scope="user") @clear_cache(tag="browse", tag_scope="user")
async def cart(**_kw): async def cart():
slug = g.product_slug slug = g.product_slug
# make sure product exists (we *allow* deleted_at != None later if you want) # make sure product exists (we *allow* deleted_at != None later if you want)
product_id = await g.s.scalar( product_id = await g.s.scalar(

2
shared

Submodule shared updated: 2a9dfaa749...0d40dfaeca

View File

@@ -7,9 +7,9 @@
{% if not quantity %} {% if not quantity %}
<form <form
action="{{ url_for('market.browse.product.cart', slug=slug) }}" action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
method="post" method="post"
hx-post="{{ url_for('market.browse.product.cart', slug=slug) }}" hx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
hx-target="#cart-mini" hx-target="#cart-mini"
hx-swap="outerHTML" hx-swap="outerHTML"
class="rounded flex items-center" class="rounded flex items-center"
@@ -38,9 +38,9 @@
<div class="rounded flex items-center gap-2"> <div class="rounded flex items-center gap-2">
<!-- minus --> <!-- minus -->
<form <form
action="{{ url_for('market.browse.product.cart', slug=slug) }}" action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
method="post" method="post"
hx-post="{{ url_for('market.browse.product.cart', slug=slug) }}" hx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
hx-target="#cart-mini" hx-target="#cart-mini"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
@@ -80,9 +80,9 @@
<!-- plus --> <!-- plus -->
<form <form
action="{{ url_for('market.browse.product.cart', slug=slug) }}" action="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
method="post" method="post"
hx-post="{{ url_for('market.browse.product.cart', slug=slug) }}" hx-post="{{ url_for('market.browse.product.cart', product_slug=slug) }}"
hx-target="#cart-mini" hx-target="#cart-mini"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
@@ -139,7 +139,7 @@
<div class="flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3"> <div class="flex flex-col sm:flex-row sm:items-start justify-between gap-2 sm:gap-3">
<div class="min-w-0"> <div class="min-w-0">
<h2 class="text-sm sm:text-base md:text-lg font-semibold text-stone-900"> <h2 class="text-sm sm:text-base md:text-lg font-semibold text-stone-900">
{% set href=url_for('market.browse.product.product_detail', slug=p.slug) %} {% set href=url_for('market.browse.product.product_detail', product_slug=p.slug) %}
<a <a
href="{{ href }}" href="{{ href }}"
hx_get="{{href}}" hx_get="{{href}}"
@@ -189,9 +189,9 @@
<div class="flex items-center gap-2 text-xs sm:text-sm text-stone-700"> <div class="flex items-center gap-2 text-xs sm:text-sm text-stone-700">
<span class="text-[0.65rem] sm:text-xs uppercase tracking-wide text-stone-500">Quantity</span> <span class="text-[0.65rem] sm:text-xs uppercase tracking-wide text-stone-500">Quantity</span>
<form <form
action="{{ url_for('market.browse.product.cart', slug=p.slug) }}" action="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
method="post" method="post"
hx-post="{{ url_for('market.browse.product.cart', slug=p.slug) }}" hx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
hx-target="#cart-mini" hx-target="#cart-mini"
hx-swap="outerHTML" hx-swap="outerHTML"
> >
@@ -212,9 +212,9 @@
{{ item.quantity }} {{ item.quantity }}
</span> </span>
<form <form
action="{{ url_for('market.browse.product.cart', slug=p.slug) }}" action="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
method="post" method="post"
hx-post="{{ url_for('market.browse.product.cart', slug=p.slug) }}" hx-post="{{ url_for('market.browse.product.cart', product_slug=p.slug) }}"
hx-target="#cart-mini" hx-target="#cart-mini"
hx-swap="outerHTML" hx-swap="outerHTML"
> >