From 7870c9425e5f579946cd10538663af0417bea5ff Mon Sep 17 00:00:00 2001 From: "russell@unturf.com" Date: Wed, 13 May 2026 20:00:54 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20dedupe=20flash=20messages=20=E2=80=94=20?= =?UTF-8?q?render-level=20+=20request.flash=5Fonce=20helper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Crypto status-poll endpoints flashed "Payment received! Waiting for confirmations…" on every refresh while a quote was in the received-but-not-confirmed state. The flash queue is a list, so identical alerts piled up — fox's screenshot showed the same line repeated 18+ times stacked down the page. Two-layer fix: 1. Render-level dedupe in templates/snippets/flash-alerts.j2. Pop the queue once, walk it, render each unique (message, level) pair only the first time it's seen. Safe regardless of how the queue was populated — covers any other view that might flash duplicates without the helper. 2. New request.flash_once(message, level) helper (request_methods.py) that peeks the queue and skips the append if (message, level) is already there. Switch the two crypto status-poll handlers (views/crypto.py — Monero block ~820, Dogecoin block ~910) to use it. Both endpoints are hit on every poll, so the queue-level dedupe matters even with render-level dedupe (other consumers of the queue would still see duplicates, and the queue would balloon). The flash queue itself stays a list (order-preserving) — set would lose insertion order, which matters when multiple distinct alerts need to render top-to-bottom. The peek-then-skip pattern keeps the list semantics while preventing dup growth. --- make_post_sell/request_methods.py | 17 ++++++ .../templates/snippets/flash-alerts.j2 | 15 ++++-- make_post_sell/views/crypto.py | 52 ++++++++++--------- 3 files changed, 57 insertions(+), 27 deletions(-) diff --git a/make_post_sell/request_methods.py b/make_post_sell/request_methods.py index ec4e5dc..557187c 100644 --- a/make_post_sell/request_methods.py +++ b/make_post_sell/request_methods.py @@ -506,3 +506,20 @@ def includeme(config): config.add_request_method( add_has_doge_refund_address, "has_doge_refund_address", reify=True ) + + def flash_once(request, message, level="info", queue=""): + """request.session.flash() that skips the append if (message, + level) is already in the queue. Use for messages emitted on + polling endpoints / redirect loops where the same status + otherwise piles up dozens of copies in the flash queue. + Pyramid's flash queue is a list; we peek first and bail on + duplicates. + """ + for queued in request.session.peek_flash(queue): + # queued is the (message, level) tuple as previously + # flashed (or just the message if level wasn't set). + if queued == (message, level): + return + request.session.flash((message, level), queue=queue) + + config.add_request_method(flash_once, "flash_once") diff --git a/make_post_sell/templates/snippets/flash-alerts.j2 b/make_post_sell/templates/snippets/flash-alerts.j2 index b141dec..070578e 100644 --- a/make_post_sell/templates/snippets/flash-alerts.j2 +++ b/make_post_sell/templates/snippets/flash-alerts.j2 @@ -5,12 +5,21 @@
-{%- if request.session.peek_flash() -%} -{%- for message, level in request.session.pop_flash() %} +{#- Dedupe identical (message, level) pairs queued during this session. + Redirect loops (e.g. the crypto-payment status poll flashing + "Payment received! Waiting for confirmations..." on every refresh) + could pile up dozens of copies of the same alert. Pop everything off + the queue once, then render each unique pair only the first time + it's seen. -#} +{%- set _queued = request.session.pop_flash() -%} +{%- set _seen = namespace(items=[]) -%} +{%- for message, level in _queued %} +{%- if (message, level) not in _seen.items %} +{%- if _seen.items.append((message, level)) %}{% endif %}

{{message|safe}}

+{%- endif -%} {%- endfor %} -{%- endif %}
diff --git a/make_post_sell/views/crypto.py b/make_post_sell/views/crypto.py index 607204d..ba30df2 100644 --- a/make_post_sell/views/crypto.py +++ b/make_post_sell/views/crypto.py @@ -815,11 +815,14 @@ def crypto_xmr_status(request): if not crypto_payment: return HTTPBadRequest("payment not found") - # Flash messages for status (without storing in session) + # Flash messages for status. flash_once dedupes at the queue + # level — this endpoint is hit by the front-end status poll on + # every refresh, so a plain flash() would stack the same alert + # dozens of times. current_status = crypto_payment.status if current_status == "received": - request.session.flash( - ("Payment received! Waiting for confirmations...", "info") + request.flash_once( + "Payment received! Waiting for confirmations...", "info" ) elif current_status == "confirmed": # Check if it's a digital product by looking at line items @@ -831,27 +834,27 @@ def crypto_xmr_status(request): break if has_digital: - request.session.flash( - ("Purchase completed! Download your files now!", "success") + request.flash_once( + "Purchase completed! Download your files now!", "success" ) else: - request.session.flash(("Purchase completed!", "success")) + request.flash_once("Purchase completed!", "success") elif current_status == "confirmed-overpay": - request.session.flash( - ("Purchase completed! Overpayment will be refunded.", "success") + request.flash_once( + "Purchase completed! Overpayment will be refunded.", "success" ) elif current_status == "expired": - request.session.flash(("Payment quote expired.", "warning")) + request.flash_once("Payment quote expired.", "warning") elif current_status in [ "expired-refunded", "underpaid-refunded", "out-of-stock-refunded", ]: - request.session.flash( - ("Refund initiated. Please allow time for processing.", "info") + request.flash_once( + "Refund initiated. Please allow time for processing.", "info" ) elif current_status == "cancelled": - request.session.flash(("Payment cancelled.", "info")) + request.flash_once("Payment cancelled.", "info") payload = { "payment_id": str(crypto_payment.id), @@ -904,11 +907,12 @@ def crypto_doge_status(request): if not crypto_payment: return HTTPBadRequest("payment not found") - # Flash messages for status (without storing in session) + # Flash messages for status. flash_once dedupes the queue so the + # poll endpoint doesn't pile up identical alerts on every refresh. current_status = crypto_payment.status if current_status == "received": - request.session.flash( - ("Payment received! Waiting for confirmations...", "info") + request.flash_once( + "Payment received! Waiting for confirmations...", "info" ) elif current_status == "confirmed": # Check if it's a digital product by looking at line items @@ -920,27 +924,27 @@ def crypto_doge_status(request): break if has_digital: - request.session.flash( - ("Purchase completed! Download your files now!", "success") + request.flash_once( + "Purchase completed! Download your files now!", "success" ) else: - request.session.flash(("Purchase completed!", "success")) + request.flash_once("Purchase completed!", "success") elif current_status == "confirmed-overpay": - request.session.flash( - ("Purchase completed! Overpayment will be refunded.", "success") + request.flash_once( + "Purchase completed! Overpayment will be refunded.", "success" ) elif current_status == "expired": - request.session.flash(("Payment quote expired.", "warning")) + request.flash_once("Payment quote expired.", "warning") elif current_status in [ "expired-refunded", "underpaid-refunded", "out-of-stock-refunded", ]: - request.session.flash( - ("Refund initiated. Please allow time for processing.", "info") + request.flash_once( + "Refund initiated. Please allow time for processing.", "info" ) elif current_status == "cancelled": - request.session.flash(("Payment cancelled.", "info")) + request.flash_once("Payment cancelled.", "info") payload = { "payment_id": str(crypto_payment.id),