Dialog for cart renewal, async task without page refresh (#5148)

* async_task: deduplicate response handling code

* extend cart without full page reload

* update dialog markup

* fix error response from CartExtend

* refactor asynctask, make sure waitingDialog.show() re-initializes dialog contents

* add cart expiry notification

* add aria references to other dialogs

* improve error handling

* fix error if max_extend=None

* different message for expiring soon and expired carts

* refactor dialog css

* add classes to further dialog elements

* switch extend-cart-dialog and loadingmodal to <dialog>

* Backport simple_block_tag from Django 5.2

* Use simple_block_tag for {% dialog %} tag

* add alertdialog role

* Update src/pretix/static/pretixbase/scss/_dialogs.scss

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* fix mobile dialog styles not being overwritten

* asynctask dialog: prevent close by escape on chrome

* remove dynamic aria-live from #cart-deadline

dynamic aria-live is generally not well supported and as we have the dialog now anyways, we can remove it

* move continue-button to right

* Update src/pretix/static/pretixpresale/js/ui/cart.js

Co-authored-by: Richard Schreiber <schreiber@rami.io>

* Fix CSS for old-style dialog

* fix heading display/level

* align dialogs at the top as they originally were

* fix </div> from merge-conflict

* fix missing grow for dialog-content

* improve cart-extend-button ui

* do not show cart-extend-dialog onload

* improve message if 0 minutes

* do not save messae in session if ajax_dont_redirect

* add ajax_dont_redirect to async_task_check_url

* improve draw_deadline to only update #cart-deadline if necessary

* add renew-confirmation-message

---------

Co-authored-by: Richard Schreiber <schreiber@rami.io>
Co-authored-by: Raphael Michel <michel@rami.io>
This commit is contained in:
luelista
2025-05-27 07:17:50 +02:00
committed by GitHub
parent fdbcffd5fd
commit 5962536a11
19 changed files with 621 additions and 556 deletions

View File

@@ -1655,7 +1655,7 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> None:
def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en', sales_channel='web', override_now_dt: datetime=None) -> dict:
"""
Resets the expiry time of a cart to the configured reservation time of this event.
Limited to 11x the reservation time.
@@ -1672,7 +1672,7 @@ def extend_cart_reservation(self, event: Event, cart_id: str=None, locale='en',
try:
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
cm.commit()
return cm.num_extended_positions
return {"success": cm.num_extended_positions, "expiry": cm._expiry, "max_expiry_extend": cm._max_expiry_extend}
except LockTimeoutException:
self.retry()
except (MaxRetriesExceededError, LockTimeoutException):

View File

@@ -23,38 +23,39 @@ from django import template
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from pretix.helpers.templatetags.simple_block_tag import (
register_simple_block_tag,
)
from django.utils.translation import gettext_lazy as _ # NOQA
register = template.Library()
@register.simple_tag
def dialog(html_id, label, description, *args, **kwargs):
@register_simple_block_tag(register)
def dialog(content, html_id, title, description, *args, **kwargs):
format_kwargs = {
"id": html_id,
"label": label,
"title": title,
"description": description,
"icon": format_html('<div class="modal-card-icon"><span class="fa fa-{}" aria-hidden="true"></span></div>', kwargs["icon"]) if "icon" in kwargs else "",
"alert": mark_safe('role="alertdialog"') if kwargs.get("alert", "False") != "False" else "",
"content": content,
}
result = """
<dialog {alert}
id="{id}"
aria-labelledby="{id}-label"
id="{id}" class="modal-card"
aria-labelledby="{id}-title"
aria-describedby="{id}-description">
<form method="dialog" class="modal-card form-horizontal">
<form method="dialog" class="modal-card-inner form-horizontal">
{icon}
<div class="modal-card-content">
<h2 id="{id}-label">{label}</h2>
<p id="{id}-description">{description}</p>
"""
return format_html(result, **format_kwargs)
@register.simple_tag
def enddialog(*args, **kwargs):
return mark_safe("""
<h2 id="{id}-title" class="modal-card-title h3">{title}</h2>
<p id="{id}-description" class="modal-card-description">{description}</p>
{content}
</div>
</form>
</dialog>
""")
"""
return format_html(result, **format_kwargs)

View File

@@ -68,7 +68,7 @@ class AsyncMixin:
def get_check_url(self, task_id, ajax):
return self.request.path + '?async_id=%s' % task_id + ('&ajax=1' if ajax else '')
def _ajax_response_data(self):
def _ajax_response_data(self, value):
return {}
def _return_ajax_result(self, res, timeout=.5):
@@ -85,7 +85,7 @@ class AsyncMixin:
logger.warning('Ignored ResponseError in AsyncResult.get()')
except ConnectionError:
# Redis probably just restarted, let's just report not ready and retry next time
data = self._ajax_response_data()
data = self._ajax_response_data(None)
data.update({
'async_id': res.id,
'ready': False
@@ -93,7 +93,7 @@ class AsyncMixin:
return data
state, info = res.state, res.info
data = self._ajax_response_data()
data = self._ajax_response_data(info)
data.update({
'async_id': res.id,
'ready': ready,
@@ -102,23 +102,21 @@ class AsyncMixin:
if ready:
if state == states.SUCCESS and not isinstance(info, Exception):
smes = self.get_success_message(info)
if smes:
if smes and 'ajax_dont_redirect' not in self.request.GET and 'ajax_dont_redirect' not in self.request.POST:
messages.success(self.request, smes)
# TODO: Do not store message if the ajax client states that it will not redirect
# but handle the message itself
data.update({
'redirect': self.get_success_url(info),
'success': True,
'message': str(self.get_success_message(info))
'message': str(smes)
})
else:
messages.error(self.request, self.get_error_message(info))
# TODO: Do not store message if the ajax client states that it will not redirect
# but handle the message itself
smes = self.get_error_message(info)
if smes and 'ajax_dont_redirect' not in self.request.GET and 'ajax_dont_redirect' not in self.request.POST:
messages.error(self.request, smes)
data.update({
'redirect': self.get_error_url(),
'success': False,
'message': str(self.get_error_message(info))
'message': str(smes)
})
elif state == 'PROGRESS':
data.update({