mirror of
https://github.com/pretix/pretix.git
synced 2025-12-05 21:32:28 +00:00
Compare commits
30 Commits
datasync
...
cart-renew
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffebfce2b3 | ||
|
|
62ba861cd2 | ||
|
|
c880e427c5 | ||
|
|
f702d44e19 | ||
|
|
2d8910e797 | ||
|
|
43a96ed3fa | ||
|
|
5df9a3fa8c | ||
|
|
fd002e0db0 | ||
|
|
6efa9f3f55 | ||
|
|
dbc9a72c90 | ||
|
|
3e6373485a | ||
|
|
f26c7984dc | ||
|
|
34440dcbdd | ||
|
|
4be74b7955 | ||
|
|
587b13807d | ||
|
|
4aac1df4dc | ||
|
|
ba2bd9bf54 | ||
|
|
169355cd43 | ||
|
|
cf4babd400 | ||
|
|
496be053ef | ||
|
|
c54be016de | ||
|
|
245c4fc996 | ||
|
|
a69927da84 | ||
|
|
d07026c4f6 | ||
|
|
54a657c8c9 | ||
|
|
87d9f278fb | ||
|
|
c84d1706b4 | ||
|
|
29205c490d | ||
|
|
b045274d8c | ||
|
|
9729496415 |
18
src/pretix/base/migrations/0280_cartposition_max_extend.py
Normal file
18
src/pretix/base/migrations/0280_cartposition_max_extend.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 4.2.20 on 2025-05-14 14:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0279_discount_event_date_from_discount_event_date_until'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cartposition',
|
||||
name='max_extend',
|
||||
field=models.DateTimeField(null=True),
|
||||
),
|
||||
]
|
||||
@@ -3098,7 +3098,10 @@ class CartPosition(AbstractPosition):
|
||||
verbose_name=_("Expiration date"),
|
||||
db_index=True
|
||||
)
|
||||
|
||||
max_extend = models.DateTimeField(
|
||||
verbose_name=_("Limit for extending expiration date"),
|
||||
null=True
|
||||
)
|
||||
tax_rate = models.DecimalField(
|
||||
max_digits=7, decimal_places=2, default=Decimal('0.00'),
|
||||
verbose_name=_('Tax rate')
|
||||
|
||||
@@ -45,6 +45,7 @@ from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import DatabaseError, transaction
|
||||
from django.db.models import Count, Exists, IntegerField, OuterRef, Q, Value
|
||||
from django.db.models.aggregates import Min
|
||||
from django.dispatch import receiver
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import (
|
||||
@@ -275,7 +276,10 @@ class CartManager:
|
||||
}
|
||||
|
||||
def __init__(self, event: Event, cart_id: str, sales_channel: SalesChannel,
|
||||
invoice_address: InvoiceAddress=None, widget_data=None, expiry=None):
|
||||
invoice_address: InvoiceAddress=None, widget_data=None, reservation_time: timedelta=None):
|
||||
"""
|
||||
Creates a new CartManager for an event.
|
||||
"""
|
||||
self.event = event
|
||||
self.cart_id = cart_id
|
||||
self.real_now_dt = now()
|
||||
@@ -286,11 +290,17 @@ class CartManager:
|
||||
self._subevents_cache = {}
|
||||
self._variations_cache = {}
|
||||
self._seated_cache = {}
|
||||
self._expiry = None
|
||||
self._explicit_expiry = expiry
|
||||
self.invoice_address = invoice_address
|
||||
self._widget_data = widget_data or {}
|
||||
self._sales_channel = sales_channel
|
||||
self.num_extended_positions = 0
|
||||
|
||||
if reservation_time:
|
||||
self._reservation_time = reservation_time
|
||||
else:
|
||||
self._reservation_time = timedelta(minutes=self.event.settings.get('reservation_time', as_type=int))
|
||||
self._expiry = self.real_now_dt + self._reservation_time
|
||||
self._max_expiry_extend = self.real_now_dt + (self._reservation_time * 11)
|
||||
|
||||
@property
|
||||
def positions(self):
|
||||
@@ -305,14 +315,6 @@ class CartManager:
|
||||
self._seated_cache[item, subevent] = item.seat_category_mappings.filter(subevent=subevent).exists()
|
||||
return self._seated_cache[item, subevent]
|
||||
|
||||
def _calculate_expiry(self):
|
||||
if self._explicit_expiry:
|
||||
self._expiry = self._explicit_expiry
|
||||
else:
|
||||
self._expiry = self.real_now_dt + timedelta(
|
||||
minutes=self.event.settings.get('reservation_time', as_type=int)
|
||||
)
|
||||
|
||||
def _check_presale_dates(self):
|
||||
if self.event.presale_start and time_machine_now(self.real_now_dt) < self.event.presale_start:
|
||||
raise CartError(error_messages['not_started'])
|
||||
@@ -329,9 +331,27 @@ class CartManager:
|
||||
raise CartError(error_messages['payment_ended'])
|
||||
|
||||
def _extend_expiry_of_valid_existing_positions(self):
|
||||
# real_now_dt is initialized at CartManager instantiation, so it's slightly in the past. Add a small
|
||||
# delta to reduce risk of extending already expired CartPositions.
|
||||
padded_now_dt = self.real_now_dt + timedelta(seconds=5)
|
||||
|
||||
# Make sure we do not extend past the max_extend timestamp, allowing users to extend their valid positions up
|
||||
# to 11 times the reservation time. If we add new positions to the cart while valid positions exist, the new
|
||||
# positions' reservation will also be limited to max_extend of the oldest position.
|
||||
# Only after all positions expire, an ExtendOperation may reset max_extend to another 11x reservation_time.
|
||||
max_extend_existing = self.positions.filter(expires__gt=padded_now_dt).aggregate(m=Min('max_extend'))['m']
|
||||
if max_extend_existing:
|
||||
self._expiry = min(self._expiry, max_extend_existing)
|
||||
self._max_expiry_extend = max_extend_existing
|
||||
|
||||
# Extend this user's cart session to ensure all items in the cart expire at the same time
|
||||
# We can extend the reservation of items which are not yet expired without risk
|
||||
self.positions.filter(expires__gt=self.real_now_dt).update(expires=self._expiry)
|
||||
if self._expiry > padded_now_dt:
|
||||
self.num_extended_positions += self.positions.filter(
|
||||
expires__gt=padded_now_dt, expires__lt=self._expiry,
|
||||
).update(
|
||||
expires=self._expiry,
|
||||
)
|
||||
|
||||
def _delete_out_of_timeframe(self):
|
||||
err = None
|
||||
@@ -1246,6 +1266,7 @@ class CartManager:
|
||||
item=op.item,
|
||||
variation=op.variation,
|
||||
expires=self._expiry,
|
||||
max_extend=self._max_expiry_extend,
|
||||
cart_id=self.cart_id,
|
||||
voucher=op.voucher,
|
||||
addon_to=op.addon_to if op.addon_to else None,
|
||||
@@ -1294,7 +1315,9 @@ class CartManager:
|
||||
event=self.event,
|
||||
item=b.item,
|
||||
variation=b.variation,
|
||||
expires=self._expiry, cart_id=self.cart_id,
|
||||
expires=self._expiry,
|
||||
max_extend=self._max_expiry_extend,
|
||||
cart_id=self.cart_id,
|
||||
voucher=None,
|
||||
addon_to=cp,
|
||||
subevent=b.subevent,
|
||||
@@ -1321,12 +1344,14 @@ class CartManager:
|
||||
op.position.delete()
|
||||
elif available_count == 1:
|
||||
op.position.expires = self._expiry
|
||||
op.position.max_extend = self._max_expiry_extend
|
||||
op.position.listed_price = op.listed_price
|
||||
op.position.price_after_voucher = op.price_after_voucher
|
||||
# op.position.price will be updated by recompute_final_prices_and_taxes()
|
||||
if op.position.pk not in deleted_positions:
|
||||
try:
|
||||
op.position.save(force_update=True, update_fields=['expires', 'listed_price', 'price_after_voucher'])
|
||||
op.position.save(force_update=True, update_fields=['expires', 'max_extend', 'listed_price', 'price_after_voucher'])
|
||||
self.num_extended_positions += 1
|
||||
except DatabaseError:
|
||||
# Best effort... The position might have been deleted in the meantime!
|
||||
pass
|
||||
@@ -1416,14 +1441,11 @@ class CartManager:
|
||||
def commit(self):
|
||||
self._check_presale_dates()
|
||||
self._check_max_cart_size()
|
||||
self._calculate_expiry()
|
||||
|
||||
err = self._delete_out_of_timeframe()
|
||||
err = self.extend_expired_positions() or err
|
||||
err = err or self._check_min_per_voucher()
|
||||
|
||||
self.real_now_dt = now()
|
||||
|
||||
self._extend_expiry_of_valid_existing_positions()
|
||||
err = self._perform_operations() or err
|
||||
self.recompute_final_prices_and_taxes()
|
||||
@@ -1632,6 +1654,31 @@ def clear_cart(self, event: Event, cart_id: str=None, locale='en', sales_channel
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
@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) -> dict:
|
||||
"""
|
||||
Resets the expiry time of a cart to the configured reservation time of this event.
|
||||
Limited to 11x the reservation time.
|
||||
|
||||
:param event: The event ID in question
|
||||
:param cart_id: The cart ID of the cart to modify
|
||||
"""
|
||||
with language(locale), time_machine_now_assigned(override_now_dt):
|
||||
try:
|
||||
sales_channel = event.organizer.sales_channels.get(identifier=sales_channel)
|
||||
except SalesChannel.DoesNotExist:
|
||||
raise CartError("Invalid sales channel.")
|
||||
try:
|
||||
try:
|
||||
cm = CartManager(event=event, cart_id=cart_id, sales_channel=sales_channel)
|
||||
cm.commit()
|
||||
return {"success": cm.num_extended_positions, "expiry": cm._expiry, "max_expiry_extend": cm._max_expiry_extend}
|
||||
except LockTimeoutException:
|
||||
self.retry()
|
||||
except (MaxRetriesExceededError, LockTimeoutException):
|
||||
raise CartError(error_messages['busy'])
|
||||
|
||||
|
||||
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(CartError,))
|
||||
def set_cart_addons(self, event: Event, addons: List[dict], add_to_cart_items: List[dict], cart_id: str=None, locale='en',
|
||||
invoice_address: int=None, sales_channel='web', override_now_dt: datetime=None) -> None:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/clipboard.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/menu.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/sb-admin-2.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/dialogs.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/main.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/quota.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/rrule.js" %}"></script>
|
||||
@@ -54,7 +56,6 @@
|
||||
<script type="text/javascript" src="{% static "leaflet/leaflet.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/geo.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/asynctask.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "sortable/Sortable.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "colorpicker/bootstrap-colorpicker.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "fileupload/jquery.ui.widget.js" %}"></script>
|
||||
@@ -463,25 +464,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ajaxerr">
|
||||
</div>
|
||||
<div id="loadingmodal">
|
||||
<div class="modal-card">
|
||||
<div class="modal-card-icon">
|
||||
<i class="fa fa-cog big-rotating-icon"></i>
|
||||
</div>
|
||||
<div class="modal-card-content">
|
||||
<h3></h3>
|
||||
<p class="text"></p>
|
||||
<p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p>
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success">
|
||||
</div>
|
||||
</div>
|
||||
<div class="steps">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="ajaxerr" class="modal-wrapper" hidden>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -11,31 +11,31 @@
|
||||
<h2>{% trans "Review order" %}</h2>
|
||||
{% include "pretixpresale/event/fragment_checkoutflow.html" %}
|
||||
<p>{% trans "Please review the details below and confirm your order." %}</p>
|
||||
<div class="panel panel-primary cart">
|
||||
<div class="panel-heading panel-heading-flex">
|
||||
<h3 class="panel-title">
|
||||
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
|
||||
{% trans "Your cart" %}
|
||||
<a href="{% eventurl request.event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}" class="h6">
|
||||
<span class="fa fa-edit" aria-hidden="true"></span>{% trans "Add or remove tickets" %}
|
||||
</a>
|
||||
</h3>
|
||||
<span class="panel-heading-flex-gap"></span>
|
||||
<strong class="helper-display-block" id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
|
||||
{% else %}
|
||||
{% trans "Cart expired" %}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=False %}
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" data-asynctask
|
||||
data-asynctask-headline="{% trans "Please hang tight, we're finalizing your order!" %}">
|
||||
{% csrf_token %}
|
||||
<div class="panel panel-primary cart">
|
||||
<div class="panel-heading panel-heading-flex">
|
||||
<h3 class="panel-title">
|
||||
<i class="fa fa-shopping-cart" aria-hidden="true"></i>
|
||||
{% trans "Your cart" %}
|
||||
<a href="{% eventurl request.event "presale:event.index" cart_namespace=cart_namespace|default_if_none:"" %}" class="h6">
|
||||
<span class="fa fa-edit" aria-hidden="true"></span>{% trans "Add or remove tickets" %}
|
||||
</a>
|
||||
</h3>
|
||||
<span class="panel-heading-flex-gap"></span>
|
||||
<strong class="helper-display-block" id="cart-deadline-short" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{{ cart.minutes_left|stringformat:"02d" }}:{{ cart.seconds_left|stringformat:"02d" }}
|
||||
{% else %}
|
||||
{% trans "Cart expired" %}
|
||||
{% endif %}
|
||||
</strong>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
{% include "pretixpresale/event/fragment_cart.html" with cart=cart event=request.event editable=False %}
|
||||
</div>
|
||||
</div>
|
||||
{% if payments %}
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
|
||||
@@ -492,15 +492,21 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% if not cart.is_ordered %}
|
||||
<p class="text-muted" id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{% blocktrans trimmed with minutes=cart.minutes_left %}
|
||||
The items in your cart are reserved for you for {{ minutes }} minutes.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "The items in your cart are no longer reserved for you. You can still complete your order as long as they’re available." %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<form class="text-muted" id="cart-extend-form" data-asynctask data-asynctask-no-redirect
|
||||
method="post" action="{% eventurl request.event "presale:event.cart.extend" cart_namespace=cart_namespace %}">
|
||||
{% csrf_token %}
|
||||
<span id="cart-deadline" data-expires="{{ cart.first_expiry|date:"Y-m-d H:i:sO" }}" data-max-expiry-extend="{{ cart.max_expiry_extend|date:"Y-m-d H:i:sO" }}">
|
||||
{% if cart.minutes_left > 0 or cart.seconds_left > 0 %}
|
||||
{% blocktrans trimmed with minutes=cart.minutes_left %}
|
||||
The items in your cart are reserved for you for {{ minutes }} minutes.
|
||||
{% endblocktrans %}
|
||||
{% else %}
|
||||
{% trans "The items in your cart are no longer reserved for you. You can still complete your order as long as they’re available." %}
|
||||
{% endif %}
|
||||
</span>
|
||||
<button class="btn btn-link" type="submit" id="cart-extend-button">
|
||||
<i class="fa fa-refresh" aria-hidden="true"></i> {% trans "Extend" %}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="sr-only" id="cart-description">{% trans "Overview of your ordered products." %}</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<script type="text/javascript" src="{% static "slider/bootstrap-slider.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "cropper/cropper.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/details.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixbase/js/dialogs.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/jquery.qrcode.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/widget/floatformat.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixpresale/js/ui/questions.js" %}"></script>
|
||||
|
||||
@@ -2,16 +2,17 @@
|
||||
{% load rich_text %}
|
||||
{% load safelink %}
|
||||
{% load escapejson %}
|
||||
<div id="ajaxerr">
|
||||
<div id="ajaxerr" class="modal-wrapper" hidden>
|
||||
</div>
|
||||
<div id="popupmodal" hidden aria-live="polite">
|
||||
<div id="popupmodal" class="modal-wrapper" hidden aria-live="polite" role="dialog"
|
||||
aria-labelledby="popupmodal-title">
|
||||
<div class="modal-card">
|
||||
<div class="modal-card-icon">
|
||||
<i class="fa fa-window-restore big-icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="modal-card-content">
|
||||
<div>
|
||||
<h3>
|
||||
<h3 id="popupmodal-title">
|
||||
{% trans "We've started the requested process in a new window." %}
|
||||
</h3>
|
||||
<p class="text">
|
||||
@@ -30,27 +31,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="loadingmodal" hidden aria-live="polite">
|
||||
<div class="modal-card">
|
||||
<div class="modal-card-icon">
|
||||
<i class="fa fa-cog big-rotating-icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="modal-card-content">
|
||||
<h3 id="loadingmodal-label"></h3>
|
||||
<div id="loadingmodal-description">
|
||||
<p class="text"></p>
|
||||
<p class="status">{% trans "If this takes longer than a few minutes, please contact us." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if request.organizer and request.organizer.settings.cookie_consent %}
|
||||
<script type="text/plain" id="cookie-consent-storage-key">cookie-consent-{{ request.organizer.slug }}</script>
|
||||
{% if cookie_consent_from_widget %}
|
||||
{{ cookie_consent_from_widget|json_script:"cookie-consent-from-widget" }}
|
||||
{% endif %}
|
||||
{% if cookie_providers %}
|
||||
<div id="cookie-consent-modal" aria-live="polite">
|
||||
<div id="cookie-consent-modal" class="modal-wrapper" hidden aria-live="polite" role="dialog"
|
||||
aria-labelledby="cookie-consent-modal-label">
|
||||
<div class="modal-card">
|
||||
<div class="modal-card-content">
|
||||
<h3 id="cookie-consent-modal-label"></h3>
|
||||
|
||||
@@ -56,6 +56,7 @@ frame_wrapped_urls = [
|
||||
re_path(r'^cart/remove$', pretix.presale.views.cart.CartRemove.as_view(), name='event.cart.remove'),
|
||||
re_path(r'^cart/voucher$', pretix.presale.views.cart.CartApplyVoucher.as_view(), name='event.cart.voucher'),
|
||||
re_path(r'^cart/clear$', pretix.presale.views.cart.CartClear.as_view(), name='event.cart.clear'),
|
||||
re_path(r'^cart/extend$', pretix.presale.views.cart.CartExtendReservation.as_view(), name='event.cart.extend'),
|
||||
re_path(r'^cart/answer/(?P<answer>[^/]+)/$',
|
||||
pretix.presale.views.cart.AnswerDownload.as_view(),
|
||||
name='event.cart.download.answer'),
|
||||
|
||||
@@ -229,11 +229,13 @@ class CartMixin:
|
||||
|
||||
try:
|
||||
first_expiry = min(p.expires for p in positions) if positions else now()
|
||||
max_expiry_extend = min((p.max_extend for p in positions if p.max_extend), default=None)
|
||||
total_seconds_left = max(first_expiry - now(), timedelta()).total_seconds()
|
||||
minutes_left = int(total_seconds_left // 60)
|
||||
seconds_left = int(total_seconds_left % 60)
|
||||
except AttributeError:
|
||||
first_expiry = None
|
||||
max_expiry_extend = None
|
||||
minutes_left = None
|
||||
seconds_left = None
|
||||
|
||||
@@ -250,6 +252,7 @@ class CartMixin:
|
||||
'minutes_left': minutes_left,
|
||||
'seconds_left': seconds_left,
|
||||
'first_expiry': first_expiry,
|
||||
'max_expiry_extend': max_expiry_extend,
|
||||
'is_ordered': bool(order),
|
||||
'itemcount': sum(c.count for c in positions if not c.addon_to),
|
||||
'current_selected_payments': [p for p in self.current_selected_payments(total) if p.get('multi_use_supported')]
|
||||
|
||||
@@ -62,7 +62,7 @@ from pretix.base.models import (
|
||||
)
|
||||
from pretix.base.services.cart import (
|
||||
CartError, add_items_to_cart, apply_voucher, clear_cart, error_messages,
|
||||
remove_cart_position,
|
||||
extend_cart_reservation, remove_cart_position,
|
||||
)
|
||||
from pretix.base.timemachine import time_machine_now
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
@@ -537,6 +537,26 @@ class CartClear(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
request.sales_channel.identifier, time_machine_now(default=None))
|
||||
|
||||
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
class CartExtendReservation(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
task = extend_cart_reservation
|
||||
known_errortypes = ['CartError']
|
||||
|
||||
def _ajax_response_data(self, value):
|
||||
if isinstance(value, dict):
|
||||
return value
|
||||
else:
|
||||
return {}
|
||||
|
||||
def get_success_message(self, value):
|
||||
if value['success'] > 0:
|
||||
return _('Your cart timeout was extended.')
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
return self.do(self.request.event.id, get_or_create_cart_id(self.request), translation.get_language(),
|
||||
request.sales_channel.identifier, time_machine_now(default=None))
|
||||
|
||||
|
||||
@method_decorator(allow_cors_if_namespaced, 'dispatch')
|
||||
@method_decorator(allow_frame_if_namespaced, 'dispatch')
|
||||
@method_decorator(iframe_entry_view_wrapper, 'dispatch')
|
||||
@@ -547,7 +567,7 @@ class CartAdd(EventViewMixin, CartActionMixin, AsyncAction, View):
|
||||
def get_success_message(self, value):
|
||||
return _('The products have been successfully added to your cart.')
|
||||
|
||||
def _ajax_response_data(self):
|
||||
def _ajax_response_data(self, value):
|
||||
cart_id = get_or_create_cart_id(self.request)
|
||||
return {
|
||||
'cart_id': cart_id,
|
||||
|
||||
@@ -5,74 +5,94 @@ var async_task_check_url = null;
|
||||
var async_task_old_url = null;
|
||||
var async_task_is_download = false;
|
||||
var async_task_is_long = false;
|
||||
var async_task_dont_redirect = false;
|
||||
|
||||
function async_task_check() {
|
||||
var async_task_status_messages = {
|
||||
long_task_started: gettext(
|
||||
'Your request is currently being processed. Depending on the size of your event, this might take up to ' +
|
||||
'a few minutes.'
|
||||
),
|
||||
long_task_pending: gettext(
|
||||
'Your request has been queued on the server and will soon be ' +
|
||||
'processed.'
|
||||
),
|
||||
short_task: gettext(
|
||||
'Your request arrived on the server but we still wait for it to be ' +
|
||||
'processed. If this takes longer than two minutes, please contact us or go ' +
|
||||
'back in your browser and try again.'
|
||||
)
|
||||
};
|
||||
|
||||
function async_task_schedule_check(context, timeout) {
|
||||
"use strict";
|
||||
$.ajax(
|
||||
{
|
||||
'type': 'GET',
|
||||
'url': async_task_check_url,
|
||||
'success': async_task_check_callback,
|
||||
'error': async_task_check_error,
|
||||
'context': this,
|
||||
'dataType': 'json'
|
||||
async_task_timeout = window.setTimeout(function() {
|
||||
$.ajax(
|
||||
{
|
||||
'type': 'GET',
|
||||
'url': async_task_check_url,
|
||||
'success': async_task_check_callback,
|
||||
'error': async_task_check_error,
|
||||
'context': context,
|
||||
'dataType': 'json'
|
||||
}
|
||||
);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
function async_task_on_success(data) {
|
||||
"use strict";
|
||||
if ((async_task_is_download && data.success) || async_task_dont_redirect) {
|
||||
waitingDialog.hide();
|
||||
if (location.href.indexOf("async_id") !== -1) {
|
||||
history.replaceState({}, "pretix", async_task_old_url);
|
||||
}
|
||||
);
|
||||
}
|
||||
if (!async_task_dont_redirect)
|
||||
location.href = data.redirect;
|
||||
$(this).trigger('pretix:async-task-success', data);
|
||||
}
|
||||
|
||||
function async_task_check_callback(data, textStatus, jqXHR) {
|
||||
"use strict";
|
||||
if (data.ready && data.redirect) {
|
||||
if (async_task_is_download && data.success) {
|
||||
waitingDialog.hide();
|
||||
if (location.href.indexOf("async_id") !== -1) {
|
||||
history.replaceState({}, "pretix", async_task_old_url);
|
||||
}
|
||||
}
|
||||
location.href = data.redirect;
|
||||
async_task_on_success.call(this, data);
|
||||
return;
|
||||
} else if (typeof data.percentage === "number") {
|
||||
$("#loadingmodal .progress").show();
|
||||
$("#loadingmodal .progress .progress-bar").css("width", data.percentage + "%");
|
||||
if (typeof data.steps === "object" && Array.isArray(data.steps)) {
|
||||
var $steps = $("#loadingmodal .steps");
|
||||
$steps.html("").show()
|
||||
for (var step of data.steps) {
|
||||
$steps.append(
|
||||
$("<span>").addClass("fa fa-fw")
|
||||
.toggleClass("fa-check text-success", step.done)
|
||||
.toggleClass("fa-cog fa-spin text-muted", !step.done)
|
||||
).append(
|
||||
$("<span>").text(step.label)
|
||||
).append(
|
||||
$("<br>")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
async_task_timeout = window.setTimeout(async_task_check, 250);
|
||||
|
||||
if (typeof data.percentage === "number") {
|
||||
waitingDialog.setProgress(data.percentage);
|
||||
}
|
||||
if (typeof data.steps === "object" && Array.isArray(data.steps)) {
|
||||
waitingDialog.setSteps(data.steps);
|
||||
}
|
||||
async_task_schedule_check(this, 250);
|
||||
|
||||
async_task_update_status(data);
|
||||
}
|
||||
|
||||
function async_task_update_status(data) {
|
||||
if (async_task_is_long) {
|
||||
if (data.started) {
|
||||
$("#loadingmodal p.status").text(gettext(
|
||||
'Your request is currently being processed. Depending on the size of your event, this might take up to ' +
|
||||
'a few minutes.'
|
||||
));
|
||||
waitingDialog.setStatus(async_task_status_messages.long_task_started);
|
||||
} else {
|
||||
$("#loadingmodal p.status").text(gettext(
|
||||
'Your request has been queued on the server and will soon be ' +
|
||||
'processed.'
|
||||
));
|
||||
waitingDialog.setStatus(async_task_status_messages.long_task_pending);
|
||||
}
|
||||
} else {
|
||||
$("#loadingmodal p.status").text(gettext(
|
||||
'Your request arrived on the server but we still wait for it to be ' +
|
||||
'processed. If this takes longer than two minutes, please contact us or go ' +
|
||||
'back in your browser and try again.'
|
||||
));
|
||||
waitingDialog.setStatus(async_task_status_messages.short_task);
|
||||
}
|
||||
}
|
||||
|
||||
function async_task_replace_page(target, new_html) {
|
||||
"use strict";
|
||||
waitingDialog.hide();
|
||||
$(target).html(new_html);
|
||||
setup_basics($(target));
|
||||
form_handlers($(target));
|
||||
setup_collapsible_details($(target));
|
||||
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
|
||||
$(document).trigger("pretix:bind-forms");
|
||||
}
|
||||
|
||||
function async_task_check_error(jqXHR, textStatus, errorThrown) {
|
||||
"use strict";
|
||||
var respdom = $(jqXHR.responseText);
|
||||
@@ -80,16 +100,10 @@ function async_task_check_error(jqXHR, textStatus, errorThrown) {
|
||||
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
|
||||
// This is a failed form validation, let's just use it
|
||||
$("body").data('ajaxing', false);
|
||||
waitingDialog.hide();
|
||||
$("body").html(jqXHR.responseText.substring(
|
||||
async_task_replace_page("body", jqXHR.responseText.substring(
|
||||
jqXHR.responseText.indexOf("<body"),
|
||||
jqXHR.responseText.indexOf("</body")
|
||||
));
|
||||
setup_basics($("body"));
|
||||
form_handlers($("body"));
|
||||
setup_collapsible_details($("body"));
|
||||
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
|
||||
$(document).trigger("pretix:bind-forms");
|
||||
} else if (c.length > 0) {
|
||||
// This is some kind of 500/404/403 page, show it in an overlay
|
||||
$("body").data('ajaxing', false);
|
||||
@@ -105,9 +119,9 @@ function async_task_check_error(jqXHR, textStatus, errorThrown) {
|
||||
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
|
||||
} else {
|
||||
// 500 can be an application error or overload in some cases :(
|
||||
$("#loadingmodal p.status").text(gettext('We currently cannot reach the server, but we keep trying.' +
|
||||
' Last error code: {code}').replace(/\{code\}/, jqXHR.status));
|
||||
async_task_timeout = window.setTimeout(async_task_check, 5000);
|
||||
waitingDialog.setStatus(gettext('We currently cannot reach the server, but we keep trying.' +
|
||||
' Last error code: {code}').replace(/\{code\}/, jqXHR.status));
|
||||
async_task_schedule_check(this, 5000);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,38 +130,15 @@ function async_task_callback(data, jqXHR, status) {
|
||||
"use strict";
|
||||
$("body").data('ajaxing', false);
|
||||
if (data.redirect) {
|
||||
if (async_task_is_download && data.success) {
|
||||
waitingDialog.hide();
|
||||
if (location.href.indexOf("async_id") !== -1) {
|
||||
history.replaceState({}, "pretix", async_task_old_url);
|
||||
}
|
||||
}
|
||||
location.href = data.redirect;
|
||||
async_task_on_success.call(this, data);
|
||||
return;
|
||||
}
|
||||
async_task_id = data.async_id;
|
||||
async_task_check_url = data.check_url;
|
||||
async_task_timeout = window.setTimeout(async_task_check, 100);
|
||||
async_task_schedule_check(this, 100);
|
||||
|
||||
async_task_update_status(data);
|
||||
|
||||
if (async_task_is_long) {
|
||||
if (data.started) {
|
||||
$("#loadingmodal p.status").text(gettext(
|
||||
'Your request is currently being processed. Depending on the size of your event, this might take up to ' +
|
||||
'a few minutes.'
|
||||
));
|
||||
} else {
|
||||
$("#loadingmodal p.status").text(gettext(
|
||||
'Your request has been queued on the server and will soon be ' +
|
||||
'processed.'
|
||||
));
|
||||
}
|
||||
} else {
|
||||
$("#loadingmodal p.status").text(gettext(
|
||||
'Your request arrived on the server but we still wait for it to be ' +
|
||||
'processed. If this takes longer than two minutes, please contact us or go ' +
|
||||
'back in your browser and try again.'
|
||||
));
|
||||
}
|
||||
if (location.href.indexOf("async_id") === -1) {
|
||||
history.pushState({}, "Waiting", async_task_check_url.replace(/ajax=1/, ''));
|
||||
}
|
||||
@@ -156,48 +147,34 @@ function async_task_callback(data, jqXHR, status) {
|
||||
function async_task_error(jqXHR, textStatus, errorThrown) {
|
||||
"use strict";
|
||||
$("body").data('ajaxing', false);
|
||||
waitingDialog.hide();
|
||||
if (textStatus === "timeout") {
|
||||
alert(gettext("The request took too long. Please try again."));
|
||||
waitingDialog.hide();
|
||||
} else if (jqXHR.responseText.indexOf('<html') > 0) {
|
||||
var respdom = $(jqXHR.responseText);
|
||||
var c = respdom.filter('.container');
|
||||
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
|
||||
// This is a failed form validation, let's just use it
|
||||
waitingDialog.hide();
|
||||
|
||||
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
|
||||
$("#page-wrapper").html(respdom.find("#page-wrapper").html());
|
||||
setup_basics($("#page-wrapper"));
|
||||
form_handlers($("#page-wrapper"));
|
||||
setup_collapsible_details($("#page-wrapper"));
|
||||
$(document).trigger("pretix:bind-forms");
|
||||
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
|
||||
async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html());
|
||||
} else {
|
||||
$("body").html(jqXHR.responseText.substring(
|
||||
async_task_replace_page("body", jqXHR.responseText.substring(
|
||||
jqXHR.responseText.indexOf("<body"),
|
||||
jqXHR.responseText.indexOf("</body")
|
||||
));
|
||||
setup_basics($("body"));
|
||||
form_handlers($("body"));
|
||||
setup_collapsible_details($("body"));
|
||||
$(document).trigger("pretix:bind-forms");
|
||||
window.setTimeout(function () { $(window).scrollTop(0) }, 200)
|
||||
}
|
||||
|
||||
} else if (c.length > 0) {
|
||||
waitingDialog.hide();
|
||||
// This is some kind of 500/404/403 page, show it in an overlay
|
||||
ajaxErrDialog.show(c.first().html());
|
||||
} else {
|
||||
waitingDialog.hide();
|
||||
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
|
||||
}
|
||||
} else {
|
||||
if (jqXHR.status >= 400 && jqXHR.status < 500) {
|
||||
waitingDialog.hide();
|
||||
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
|
||||
} else {
|
||||
waitingDialog.hide();
|
||||
alert(gettext('We currently cannot reach the server. Please try again. ' +
|
||||
'Error code: {code}').replace(/\{code\}/, jqXHR.status));
|
||||
}
|
||||
@@ -221,24 +198,19 @@ $(function () {
|
||||
}
|
||||
async_task_id = null;
|
||||
async_task_is_download = $(this).is("[data-asynctask-download]");
|
||||
async_task_dont_redirect = $(this).is("[data-asynctask-no-redirect]");
|
||||
async_task_is_long = $(this).is("[data-asynctask-long]");
|
||||
async_task_old_url = location.href;
|
||||
$("body").data('ajaxing', true);
|
||||
if ($(this).is("[data-asynctask-headline]")) {
|
||||
waitingDialog.show($(this).attr("data-asynctask-headline"));
|
||||
} else {
|
||||
waitingDialog.show(gettext('We are processing your request …'));
|
||||
}
|
||||
if ($(this).is("[data-asynctask-text]")) {
|
||||
$("#loadingmodal p.text").text($(this).attr("data-asynctask-text")).show();
|
||||
} else {
|
||||
$("#loadingmodal p.text").hide();
|
||||
}
|
||||
$("#loadingmodal p.status").text(gettext(
|
||||
'We are currently sending your request to the server. If this takes longer ' +
|
||||
'than one minute, please check your internet connection and then reload ' +
|
||||
'this page and try again.'
|
||||
));
|
||||
waitingDialog.show(
|
||||
$(this).attr("data-asynctask-headline") || gettext('We are processing your request …'),
|
||||
$(this).attr("data-asynctask-text") || '',
|
||||
gettext(
|
||||
'We are currently sending your request to the server. If this takes longer ' +
|
||||
'than one minute, please check your internet connection and then reload ' +
|
||||
'this page and try again.'
|
||||
)
|
||||
);
|
||||
|
||||
var action = this.action;
|
||||
var formData = new FormData(this);
|
||||
@@ -275,34 +247,72 @@ $(function () {
|
||||
}, 10);
|
||||
}
|
||||
}, false);
|
||||
|
||||
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
|
||||
});
|
||||
|
||||
var waitingDialog = {
|
||||
show: function (message) {
|
||||
"use strict";
|
||||
$("#loadingmodal h3").html(message);
|
||||
$("#loadingmodal .progress").hide();
|
||||
$("#loadingmodal .steps").hide();
|
||||
$("body").addClass("loading");
|
||||
$("#loadingmodal").removeAttr("hidden");
|
||||
},
|
||||
hide: function () {
|
||||
"use strict";
|
||||
$("body").removeClass("loading");
|
||||
$("#loadingmodal").attr("hidden", true);
|
||||
function AsyncStatusDialog(options) {
|
||||
ModalDialog.call(this, Object.assign({
|
||||
content: [
|
||||
this.statusEl = EL('p', {}),
|
||||
this.progressEl = EL('div', {class: 'progress'}, EL('div', {class:'progress-bar progress-bar-success', hidden: ''})),
|
||||
this.stepsEl = EL('div', {class: 'steps', hidden: ''}, ''),
|
||||
]
|
||||
}, options));
|
||||
}
|
||||
AsyncStatusDialog.prototype = Object.create(ModalDialog.prototype);
|
||||
AsyncStatusDialog.prototype.show = function (title, text, status) {
|
||||
ModalDialog.prototype.show.call(this);
|
||||
this.setTitle(title);
|
||||
this.setDescription(text);
|
||||
this.setStatus(status || gettext('If this takes longer than a few minutes, please contact us.'));
|
||||
this.setProgress(null);
|
||||
this.setSteps(null);
|
||||
}
|
||||
AsyncStatusDialog.prototype.setStatus = function (text) {
|
||||
this.statusEl.innerText = text;
|
||||
}
|
||||
AsyncStatusDialog.prototype.setProgress = function (percent) {
|
||||
$(this.progressEl).toggle(typeof percent === 'number').find('.progress-bar').css('width', percent + '%');
|
||||
}
|
||||
AsyncStatusDialog.prototype.setSteps = function (steps) {
|
||||
var $steps = $(this.stepsEl);
|
||||
if (typeof steps === "object" && Array.isArray(steps)) {
|
||||
$steps.html("").show();
|
||||
for (var step of steps) {
|
||||
$steps.append(
|
||||
$("<span>").addClass("fa fa-fw")
|
||||
.toggleClass("fa-check text-success", step.done)
|
||||
.toggleClass("fa-cog fa-spin text-muted", !step.done)
|
||||
).append(
|
||||
$("<span>").text(step.label)
|
||||
).append(
|
||||
$("<br>")
|
||||
)
|
||||
}
|
||||
} else {
|
||||
$steps.html("").hide();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
var waitingDialog;
|
||||
$(function() {
|
||||
waitingDialog = new AsyncStatusDialog({
|
||||
icon: 'cog', rotatingIcon: true,
|
||||
});
|
||||
});
|
||||
|
||||
var ajaxErrDialog = {
|
||||
show: function (c) {
|
||||
"use strict";
|
||||
$("#ajaxerr").html(c);
|
||||
$("#ajaxerr").html(c).show();
|
||||
$("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>"
|
||||
+ gettext("Close message") + "</a>");
|
||||
$("body").addClass("ajaxerr");
|
||||
+ gettext("Close message") + "</a>");
|
||||
ModalDialog.updateBodyClass();
|
||||
},
|
||||
hide: function () {
|
||||
"use strict";
|
||||
$("body").removeClass("ajaxerr");
|
||||
$("#ajaxerr").hide();
|
||||
ModalDialog.updateBodyClass();
|
||||
}
|
||||
};
|
||||
|
||||
76
src/pretix/static/pretixbase/js/dialogs.js
Normal file
76
src/pretix/static/pretixbase/js/dialogs.js
Normal file
@@ -0,0 +1,76 @@
|
||||
/*global $,gettext,ngettext */
|
||||
function EL(tagName, attrs) {
|
||||
var el = document.createElement(tagName);
|
||||
if (attrs) for(var key in attrs)
|
||||
if (key === 'style' && typeof attrs[key] === 'object') Object.assign(el.style, attrs[key]);
|
||||
else if (key === 'innerHTML') el.innerHTML = attrs[key];
|
||||
else if (key === 'appendTo' && (attrs.appendTo instanceof HTMLElement || attrs.appendTo instanceof ShadowRoot)) attrs.appendTo.append(el);
|
||||
else if (key === 'prependTo' && (attrs.prependTo instanceof HTMLElement || attrs.prependTo instanceof ShadowRoot)) attrs.prependTo.prepend(el);
|
||||
else if (key === 'insertBefore' && attrs.insertBefore instanceof HTMLElement) attrs.insertBefore.before(el);
|
||||
else if (key === 'insertAfter' && attrs.insertAfter instanceof HTMLElement) attrs.insertAfter.after(el);
|
||||
else if (key.startsWith("on")) el.addEventListener(key.substring(2), attrs[key], false);
|
||||
else if (key.startsWith(":")) el[key.substring(1)] = attrs[key];
|
||||
else if (key === 'checked' && 'checked' in el) el.checked = attrs.checked;
|
||||
else if (key === 'selected' && 'selected' in el) el.selected = attrs.selected;
|
||||
else if (key === 'multiple' && 'multiple' in el) el.multiple = attrs.multiple;
|
||||
else el.setAttribute(key, attrs[key]);
|
||||
|
||||
if (arguments[2] instanceof Array)
|
||||
var args = arguments[2], i = 0;
|
||||
else
|
||||
var args = arguments, i = 2;
|
||||
for(;i<args.length;i++){
|
||||
if (args[i] instanceof HTMLElement) el.appendChild(args[i]);
|
||||
else if (args[i]) el.appendChild(document.createTextNode(""+args[i]));
|
||||
}
|
||||
return el;
|
||||
}
|
||||
|
||||
function ModalDialog(options) {
|
||||
this.id = 'modal-dlg-' + (++ModalDialog._next_dialog_id);
|
||||
this.options = options;
|
||||
this.dialogEl = EL('dialog', {class: 'modal-card', id: this.id,
|
||||
'aria-live': 'polite', 'aria-labelledby': this.id + '-title', 'aria-describedby': this.id + '-desc',
|
||||
appendTo: document.body, onclose: this._onClose.bind(this)},
|
||||
(options.icon)
|
||||
? EL('div', {class: 'modal-card-icon'},
|
||||
EL('i', {'aria-hidden': 'true', class: 'fa fa-' + options.icon + ' ' + (options.rotatingIcon ? 'big-rotating-icon' : 'big-icon')}))
|
||||
: undefined,
|
||||
EL('div', {class: 'modal-card-content'},
|
||||
this.titleEl = EL('h3', {id: this.id + '-title'}, options.title || ''),
|
||||
this.descEl = EL('p', {id: this.id + '-desc'}, options.description || ''),
|
||||
this.contentEl = EL('div', {}, options.content || '')));
|
||||
}
|
||||
|
||||
ModalDialog._next_dialog_id = 1;
|
||||
ModalDialog.updateBodyClass = function() {
|
||||
if ($("dialog[open], .modal-wrapper:not([hidden])").length)
|
||||
$(document.body).addClass('has-modal-dialog');
|
||||
else
|
||||
$(document.body).removeClass('has-modal-dialog');
|
||||
}
|
||||
|
||||
ModalDialog.prototype.show = function() {
|
||||
this.dialogEl.showModal();
|
||||
ModalDialog.updateBodyClass();
|
||||
}
|
||||
ModalDialog.prototype.hide = function() {
|
||||
this.dialogEl.close();
|
||||
}
|
||||
ModalDialog.prototype.isOpen = function() {
|
||||
return this.dialogEl.open;
|
||||
}
|
||||
ModalDialog.prototype._onClose = function() {
|
||||
if (this.options.removeOnClose) this.dialogEl.remove();
|
||||
ModalDialog.updateBodyClass();
|
||||
}
|
||||
ModalDialog.prototype.setTitle = function(text) {
|
||||
this.titleEl.innerText = text;
|
||||
}
|
||||
ModalDialog.prototype.setDescription = function(text) {
|
||||
this.descEl.innerText = text;
|
||||
this.descEl.style.display = text ? '' : 'none';
|
||||
}
|
||||
ModalDialog.prototype.setContent = function(text) {
|
||||
this.contentEl.innerText = text;
|
||||
}
|
||||
102
src/pretix/static/pretixbase/scss/dialogs.scss
Normal file
102
src/pretix/static/pretixbase/scss/dialogs.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
body.has-modal-dialog .container, body.has-modal-dialog #wrapper {
|
||||
-webkit-filter: blur(2px);
|
||||
-moz-filter: blur(2px);
|
||||
-ms-filter: blur(2px);
|
||||
-o-filter: blur(2px);
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.big-rotating-icon {
|
||||
-webkit-animation: fa-spin 8s infinite linear;
|
||||
animation: fa-spin 8s infinite linear;
|
||||
font-size: 120px;
|
||||
color: $brand-primary;
|
||||
}
|
||||
dialog.modal-card::backdrop {
|
||||
background: rgba(255, 255, 255, .7);
|
||||
}
|
||||
.modal-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(255, 255, 255, .7);
|
||||
z-index: 900000;
|
||||
padding: 10px;
|
||||
}
|
||||
.modal-card {
|
||||
margin: 50px auto 0;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
background: white;
|
||||
border-radius: $border-radius-large;
|
||||
box-shadow: 0 7px 14px 0 rgba(78, 50, 92, 0.1),0 3px 6px 0 rgba(0,0,0,.07);
|
||||
padding: 20px;
|
||||
min-height: 160px;
|
||||
border: 0;
|
||||
|
||||
.modal-card-icon {
|
||||
float: left;
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
.big-icon {
|
||||
margin-top: 10px;
|
||||
font-size: 100px;
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
.modal-card-content {
|
||||
margin-left: 160px;
|
||||
text-align: left;
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#cookie-consent-modal {
|
||||
background: rgba(255, 255, 255, .5);
|
||||
.modal-card-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
details {
|
||||
& > summary {
|
||||
list-style: none;
|
||||
}
|
||||
& > summary::-webkit-details-marker,
|
||||
& > summary::marker {
|
||||
display: none;
|
||||
}
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.modal-card {
|
||||
margin: 25px auto 0;
|
||||
max-height: calc(100vh - 50px - 20px);
|
||||
.modal-card-icon {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
.modal-card-content {
|
||||
text-align: center;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ajaxerr {
|
||||
background: rgba(236, 236, 236, .9);
|
||||
|
||||
.big-icon {
|
||||
margin-top: 50px;
|
||||
font-size: 200px;
|
||||
color: $brand-primary;
|
||||
}
|
||||
}
|
||||
@@ -51,32 +51,6 @@ function formatPrice(price, currency, locale) {
|
||||
}
|
||||
}
|
||||
|
||||
var waitingDialog = {
|
||||
show: function (message) {
|
||||
"use strict";
|
||||
$("#loadingmodal").find("h1").html(message);
|
||||
$("body").addClass("loading");
|
||||
},
|
||||
hide: function () {
|
||||
"use strict";
|
||||
$("body").removeClass("loading");
|
||||
}
|
||||
};
|
||||
|
||||
var ajaxErrDialog = {
|
||||
show: function (c) {
|
||||
"use strict";
|
||||
$("#ajaxerr").html(c);
|
||||
$("#ajaxerr .links").html("<a class='btn btn-default ajaxerr-close'>"
|
||||
+ gettext("Close message") + "</a>");
|
||||
$("body").addClass("ajaxerr");
|
||||
},
|
||||
hide: function () {
|
||||
"use strict";
|
||||
$("body").removeClass("ajaxerr");
|
||||
}
|
||||
};
|
||||
|
||||
var apiGET = function (url, callback) {
|
||||
$.getJSON(url, function (data) {
|
||||
callback(data);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
@import "../../select2/select2.scss";
|
||||
@import "../../select2/select2_bootstrap.scss";
|
||||
@import "../../colorpicker/bootstrap-colorpicker.scss";
|
||||
@import "../../pretixbase/scss/dialogs.scss";
|
||||
|
||||
/* See https://github.com/pretix/pretix/pull/761 */
|
||||
.bootstrap-datetimepicker-widget table td span {
|
||||
@@ -234,99 +235,6 @@ p.bigger {
|
||||
}
|
||||
}
|
||||
|
||||
body.loading #wrapper {
|
||||
-webkit-filter: blur(2px);
|
||||
-moz-filter: blur(2px);
|
||||
-ms-filter: blur(2px);
|
||||
-o-filter: blur(2px);
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
#loadingmodal, #ajaxerr {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(255, 255, 255, .7);
|
||||
opacity: 0;
|
||||
z-index: 900000;
|
||||
visibility: hidden;
|
||||
padding: 10px;
|
||||
|
||||
.big-icon {
|
||||
margin-top: 50px;
|
||||
font-size: 200px;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
margin: 50px auto 0;
|
||||
top: 50px;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
background: white;
|
||||
border-radius: $border-radius-large;
|
||||
box-shadow: 0 7px 14px 0 rgba(78, 50, 92, 0.1),0 3px 6px 0 rgba(0,0,0,.07);
|
||||
padding: 20px;
|
||||
min-height: 160px;
|
||||
|
||||
.modal-card-icon {
|
||||
float: left;
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
|
||||
.big-rotating-icon {
|
||||
font-size: 120px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.modal-card-content {
|
||||
margin-left: 160px;
|
||||
text-align: left;
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
#loadingmodal, #ajaxerr {
|
||||
.modal-card {
|
||||
.modal-card-icon {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
.modal-card-content {
|
||||
text-align: center;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ajaxerr {
|
||||
background: rgba(236, 236, 236, .9);
|
||||
}
|
||||
|
||||
.loading #loadingmodal, .ajaxerr #ajaxerr {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity .5s ease-in-out;
|
||||
-moz-transition: opacity .5s ease-in-out;
|
||||
-webkit-transition: opacity .5s ease-in-out;
|
||||
}
|
||||
|
||||
.big-rotating-icon {
|
||||
margin-top: 50px;
|
||||
-webkit-animation: fa-spin 8s infinite linear;
|
||||
animation: fa-spin 8s infinite linear;
|
||||
font-size: 200px;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.user-admin-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
@@ -20,6 +20,27 @@ var cart = {
|
||||
cart._time_offset = server_time - client_time;
|
||||
},
|
||||
|
||||
show_expiry_notification: function () {
|
||||
if (cart._notify_dialog) return;
|
||||
|
||||
const btn_clicked = function() {
|
||||
cart._notify_dialog.hide();
|
||||
cart._notify_dialog = null;
|
||||
$("#cart-extend-form").submit();
|
||||
};
|
||||
cart._notify_dialog = new ModalDialog({
|
||||
icon: 'clock-o',
|
||||
title: gettext("Please let us know if you're still there"),
|
||||
description: undefined,
|
||||
content: EL('p', {},
|
||||
EL('button', {class: 'btn btn-primary btn-lg', onclick: btn_clicked, autofocus: ''}, gettext('Continue'))
|
||||
),
|
||||
removeOnClose: true,
|
||||
});
|
||||
cart._notify_dialog.show();
|
||||
cart._expiry_notified = true;
|
||||
},
|
||||
|
||||
draw_deadline: function () {
|
||||
function pad(n, width, z) {
|
||||
z = z || '0';
|
||||
@@ -33,8 +54,9 @@ var cart = {
|
||||
return;
|
||||
}
|
||||
var now = cart._get_now();
|
||||
var diff_minutes = Math.floor(cart._deadline.diff(now) / 1000 / 60);
|
||||
var diff_seconds = Math.floor(cart._deadline.diff(now) / 1000 % 60);
|
||||
var diff_total_seconds = cart._deadline.diff(now) / 1000;
|
||||
var diff_minutes = Math.floor(diff_total_seconds / 60);
|
||||
var diff_seconds = Math.floor(diff_total_seconds % 60);
|
||||
|
||||
if (diff_minutes < 2 || diff_minutes == 5) $("#cart-deadline").get(0).setAttribute("aria-live", "polite");
|
||||
else $("#cart-deadline").get(0).removeAttribute("aria-live");
|
||||
@@ -45,6 +67,7 @@ var cart = {
|
||||
gettext("Cart expired")
|
||||
);
|
||||
window.clearInterval(cart._deadline_interval);
|
||||
cart._deadline_interval = null;
|
||||
} else {
|
||||
$("#cart-deadline").text(ngettext(
|
||||
"The items in your cart are reserved for you for one minute.",
|
||||
@@ -55,13 +78,33 @@ var cart = {
|
||||
pad(diff_minutes.toString(), 2) + ':' + pad(diff_seconds.toString(), 2)
|
||||
);
|
||||
}
|
||||
var already_expired = diff_total_seconds <= 0;
|
||||
var can_extend_cart = diff_minutes < 3 && (already_expired || cart._deadline < cart._max_extend);
|
||||
$("#cart-extend-button").toggle(can_extend_cart);
|
||||
if (can_extend_cart && diff_total_seconds < 45) {
|
||||
if (!cart._expiry_notified) cart.show_expiry_notification();
|
||||
if (cart._notify_dialog) cart._notify_dialog.setDescription(already_expired
|
||||
? gettext("Your cart has expired. If you want to continue, please click here:")
|
||||
: gettext("Your cart is about to expire. If you want to continue, please click here:"));
|
||||
}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
"use strict";
|
||||
cart._deadline = moment($("#cart-deadline").attr("data-expires"));
|
||||
cart._deadline_interval = window.setInterval(cart.draw_deadline, 500);
|
||||
cart._calc_offset();
|
||||
cart.set_deadline(
|
||||
$("#cart-deadline").attr("data-expires"),
|
||||
$("#cart-deadline").attr("data-max-expiry-extend")
|
||||
);
|
||||
},
|
||||
|
||||
set_deadline: function (expiry, max_extend) {
|
||||
"use strict";
|
||||
cart._expiry_notified = false;
|
||||
cart._deadline = moment(expiry);
|
||||
cart._max_extend = moment(max_extend);
|
||||
if (!cart._deadline_interval)
|
||||
cart._deadline_interval = window.setInterval(cart.draw_deadline, 500);
|
||||
cart.draw_deadline();
|
||||
}
|
||||
};
|
||||
@@ -73,6 +116,13 @@ $(function () {
|
||||
cart.init();
|
||||
}
|
||||
|
||||
$("#cart-extend-form").on("pretix:async-task-success", function(e, data) {
|
||||
if (data.success)
|
||||
cart.set_deadline(data.expiry, data.max_expiry_extend);
|
||||
else
|
||||
alert(data.message);
|
||||
});
|
||||
|
||||
$(".toggle-container").each(function() {
|
||||
var summary = $(".toggle-summary", this);
|
||||
var content = $("> :not(.toggle-summary)", this);
|
||||
|
||||
@@ -409,8 +409,6 @@ $(function () {
|
||||
$("#voucher-toggle").slideUp();
|
||||
});
|
||||
|
||||
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
|
||||
|
||||
// Handlers for "Copy answers from above" buttons
|
||||
$(".js-copy-answers").click(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -4,8 +4,6 @@ $(function () {
|
||||
var popup_window = null
|
||||
var popup_check_interval = null
|
||||
|
||||
$("#popupmodal").removeAttr("hidden");
|
||||
|
||||
$("a[data-open-in-popup-window]").on("click", function (e) {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -22,11 +20,13 @@ $(function () {
|
||||
"presale-popup",
|
||||
"scrollbars=yes,resizable=yes,status=yes,location=yes,toolbar=no,menubar=no,width=940,height=620,left=50,top=50"
|
||||
)
|
||||
$("body").addClass("has-popup")
|
||||
$("body").addClass("has-popup has-modal-dialog")
|
||||
$("#popupmodal").removeAttr("hidden");
|
||||
|
||||
popup_check_interval = window.setInterval(function () {
|
||||
if (popup_window.closed) {
|
||||
$("body").removeClass("has-popup")
|
||||
$("body").removeClass("has-popup has-modal-dialog")
|
||||
$("#popupmodal").attr("hidden", true);
|
||||
window.clearInterval(popup_check_interval)
|
||||
}
|
||||
}, 250)
|
||||
|
||||
@@ -57,6 +57,7 @@ $headings-small-color: $text-muted;
|
||||
@import "_calendar.scss";
|
||||
@import "_checkout.scss";
|
||||
@import "../../pretixbase/scss/webfont.scss";
|
||||
@import "../../pretixbase/scss/dialogs.scss";
|
||||
|
||||
html {
|
||||
font-size: 1em;
|
||||
@@ -260,120 +261,6 @@ a:hover .panel-primary > .panel-heading {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
body.loading .container {
|
||||
-webkit-filter: blur(2px);
|
||||
-moz-filter: blur(2px);
|
||||
-ms-filter: blur(2px);
|
||||
-o-filter: blur(2px);
|
||||
filter: blur(2px);
|
||||
}
|
||||
|
||||
.big-rotating-icon {
|
||||
-webkit-animation: fa-spin 8s infinite linear;
|
||||
animation: fa-spin 8s infinite linear;
|
||||
font-size: 120px;
|
||||
color: $brand-primary;
|
||||
}
|
||||
#loadingmodal, #ajaxerr, #cookie-consent-modal, #popupmodal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(255, 255, 255, .7);
|
||||
opacity: 0;
|
||||
z-index: 900000;
|
||||
visibility: hidden;
|
||||
padding: 10px;
|
||||
|
||||
.big-icon {
|
||||
margin-top: 50px;
|
||||
font-size: 200px;
|
||||
color: $brand-primary;
|
||||
}
|
||||
&#popupmodal .big-icon {
|
||||
margin-top: 10px;
|
||||
font-size: 100px;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.modal-card {
|
||||
margin: 50px auto 0;
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
max-height: calc(100vh - 100px);
|
||||
overflow-y: auto;
|
||||
background: white;
|
||||
border-radius: $border-radius-large;
|
||||
box-shadow: 0 7px 14px 0 rgba(78, 50, 92, 0.1),0 3px 6px 0 rgba(0,0,0,.07);
|
||||
padding: 20px;
|
||||
min-height: 160px;
|
||||
|
||||
.modal-card-icon {
|
||||
float: left;
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
}
|
||||
.modal-card-content {
|
||||
margin-left: 160px;
|
||||
text-align: left;
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&#cookie-consent-modal {
|
||||
background: rgba(255, 255, 255, .5);
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
display: none;
|
||||
.modal-card-content {
|
||||
margin-left: 0;
|
||||
}
|
||||
details {
|
||||
& > summary {
|
||||
list-style: none;
|
||||
}
|
||||
& > summary::-webkit-details-marker,
|
||||
& > summary::marker {
|
||||
display: none;
|
||||
}
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
#loadingmodal, #ajaxerr, #cookie-consent-modal, #popupmodal {
|
||||
.modal-card {
|
||||
margin: 25px auto 0;
|
||||
max-height: calc(100vh - 50px - 20px);
|
||||
.modal-card-icon {
|
||||
float: none;
|
||||
width: 100%;
|
||||
}
|
||||
.modal-card-content {
|
||||
text-align: center;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ajaxerr {
|
||||
background: rgba(236, 236, 236, .9);
|
||||
}
|
||||
|
||||
.loading #loadingmodal, .ajaxerr #ajaxerr, .has-popup #popupmodal {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity .5s ease-in-out;
|
||||
-moz-transition: opacity .5s ease-in-out;
|
||||
-webkit-transition: opacity .5s ease-in-out;
|
||||
}
|
||||
|
||||
.typo-alert span[data-typosuggest] {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user