fix: dedupe flash messages — render-level + request.flash_once helper
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.
This commit is contained in:
parent
89156a5d6c
commit
7870c9425e
3 changed files with 57 additions and 27 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -5,12 +5,21 @@
|
|||
</noscript>
|
||||
|
||||
<div id="alerts">
|
||||
{%- 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 %}
|
||||
<div class="alert alert-{{level}}" onclick="this.style.display='none'" name="alert">
|
||||
<p class="alert-message">{{message|safe}}</p>
|
||||
<label class="close js-only" alt="dismiss" title="Mark as read">X</label>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue