Compare commits

..
Author SHA1 Message Date
Martin Gross 7da5d2e0e7 Vite: Add dev CORS 2026-09-01 10:23:16 +02:00
91 changed files with 10223 additions and 11008 deletions
+7 -9
View File
@@ -566,7 +566,7 @@ organizer level.
Content-Type: application/json
{
"region": "DE",
"imprint_url": "https://pretix.eu",
}
@@ -579,14 +579,12 @@ organizer level.
Content-Type: application/json
{
"region":
"imprint_url":
{
"value": "DE",
"label": "Region",
"value": "https://pretix.eu",
"label": "Imprint URL",
"readonly": false,
"help_text": "Will be used to determine date and time formatting as well as default country for customer
addresses and phone numbers. For formatting, this takes less priority than the language and
is therefore mostly relevant for languages used in different regions globally (like English)."
"help_text": "This should point e.g. to a part of your website that has your contact details and legal information."
}
},
@@ -622,7 +620,7 @@ organizer level.
Content-Type: application/json
{
"region": "DE"
"imprint_url": "https://example.org/imprint/"
}
**Example response**:
@@ -634,7 +632,7 @@ organizer level.
Content-Type: application/json
{
"region": "DE",
"imprint_url": "https://example.org/imprint/",
}
+1 -58
View File
@@ -8,64 +8,7 @@ import vuePug from 'eslint-plugin-vue-pug'
const ignores = globalIgnores([
'**/node_modules',
'**/dist',
// Vendored code
'src/pretix/static/leaflet',
'src/pretix/static/clipboard',
'src/pretix/static/cropper',
'src/pretix/static/lightbox',
'src/pretix/static/are-you-sure',
'src/pretix/static/vuejs',
'src/pretix/static/fontawesome',
'src/pretix/static/typeahead',
'src/pretix/static/moment',
'src/pretix/static/pdfjs',
'src/pretix/static/sortable',
'src/pretix/static/iframeresizer',
'src/pretix/static/bootstrap',
'src/pretix/static/d3',
'src/pretix/static/jsi18n',
'src/pretix/static/fabric',
'src/pretix/static/datetimepicker',
'src/pretix/static/charts',
'src/pretix/static/fileupload',
'src/pretix/static/seating',
'src/pretix/static/rest_framework',
'src/pretix/static/select2',
'src/pretix/static/schema',
'src/pretix/static/slider',
'src/pretix/static/jquery',
'src/pretix/static/colorpicker',
'src/pretix/static/rrule',
'src/pretix/static/pretixcontrol/js/jquery.qrcode.min.js',
'src/pretix/static/pretixpresale/js/widget/docready.js',
// Pre-vue JS code
'src/pretix/static/pretixbase/js/addressform.js',
'src/pretix/static/pretixbase/js/asynctask.js',
'src/pretix/static/pretixbase/js/details.js',
'src/pretix/static/pretixbase/js/gettextstub.js',
'src/pretix/static/pretixbase/js/i18nstring.js',
'src/pretix/static/pretixcontrol/js/menu.js',
'src/pretix/static/pretixcontrol/js/ui/editor.js',
'src/pretix/static/pretixcontrol/js/ui/geo.js',
'src/pretix/static/pretixcontrol/js/ui/main.js',
'src/pretix/static/pretixcontrol/js/ui/plugins.js',
'src/pretix/static/pretixcontrol/js/ui/subevent.js',
'src/pretix/static/pretixcontrol/js/ui/variations.js',
'src/pretix/static/pretixcontrol/js/ui/webauthn.js',
'src/pretix/static/pretixpresale/js/ui/cart.js',
'src/pretix/static/pretixpresale/js/ui/main.js',
'src/pretix/static/pretixpresale/js/ui/questions.js',
'src/pretix/static/pretixpresale/js/widget/floatformat.js',
'src/pretix/static/pretixpresale/js/widget/widget.js',
'src/pretix/plugins/banktransfer/static',
'src/pretix/plugins/paypal2/static',
'src/pretix/plugins/statistics/static',
'src/pretix/plugins/stripe/static',
// Plugin checkouts
'local',
// docs
'doc',
'**/dist'
])
export default defineConfig([
+1 -1
View File
@@ -56,7 +56,7 @@ dependencies = [
"django-querytagger==0.0.3",
"django-redis==7.0.*",
"django-scopes==2.1.*",
"django-statici18n==2.8.*",
"django-statici18n==2.7.*",
"djangorestframework==3.17.*",
"dnspython==2.8.*",
"drf_ujson2==1.7.*",
+2 -3
View File
@@ -899,7 +899,7 @@ class BaseQuestionsForm(forms.Form):
field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(q.dependency_values))
if q.type != 'M':
field.widget.attrs['required'] = q.required and not self.all_optional
field._required = q.required and not self.all_optional
field._required = q.required and not self.all_optional
field.required = False
return field
@@ -1497,8 +1497,7 @@ class BaseInvoiceAddressForm(forms.ModelForm):
pass # Skip re-validation if it is validated
elif self.validate_vat_id and vat_id_applicable:
try:
requester_id = self.request.event.settings.invoice_address_from_vat_id
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')), requester_id)
normalized_id = validate_vat_id(data.get('vat_id'), str(data.get('country')))
self.instance.vat_id_validated = bool(normalized_id)
self.instance.vat_id = data['vat_id'] = normalized_id
except VATIDFinalError as e:
+1 -3
View File
@@ -510,9 +510,7 @@ class Item(LoggedModel):
verbose_name=_("Free price input"),
help_text=_("If this option is active, your users can choose the price themselves. The price configured above "
"is then interpreted as the minimum price a user has to enter. You could use this e.g. to collect "
"additional donations for your event. We recommend against combining this feature with automatic "
"discounts since discounts are applied as the last step of price computation, which means that the "
"price entered by the customer will be modified again.")
"additional donations for your event.")
)
free_price_suggestion = models.DecimalField(
verbose_name=_("Suggested price"),
+40 -7
View File
@@ -626,14 +626,47 @@ class Order(LockModel, LoggedModel):
self.save(update_fields=['last_modified'])
def set_expires(self, now_dt=None, subevents=None):
from pretix.base.services.payment import compute_payment_deadline
now_dt = now_dt or now()
tz = ZoneInfo(self.event.settings.timezone)
self.expires = compute_payment_deadline(
event=self.event,
sales_channel=self.sales_channel,
now_dt=now_dt,
subevents=subevents,
)
sales_channel_suffix = "_" + self.sales_channel.identifier.replace(".", "_")
if not (mode := self.event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
mode = self.event.settings.get('payment_term_mode')
sales_channel_suffix = ""
if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(days=self.event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if self.event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
elif mode == 'minutes':
exp_by_date = now_dt.astimezone(tz) + timedelta(minutes=self.event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
else:
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
self.expires = exp_by_date
term_last = self.event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if self.event.has_subevents and subevents:
terms = [
term_last.datetime(se).date()
for se in subevents
]
if not terms:
return
term_last = min(terms)
else:
term_last = term_last.datetime(self.event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < self.expires:
self.expires = term_last
@cached_property
def tax_total(self):
-1
View File
@@ -1605,7 +1605,6 @@ def add_payment_to_cart_session(cart_session, provider, min_value: Decimal=None,
'max_value': str(max_value) if max_value is not None else None,
'info_data': info_data or {},
})
cart_session['payments_postpone'] = False
def add_payment_to_cart(request, provider, min_value: Decimal=None, max_value: Decimal=None, info_data: dict=None):
@@ -51,7 +51,6 @@ class CrossSellingService:
self.sales_channel = sales_channel
self.cartpositions = cartpositions
self.customer = customer
self._discount_cache = {}
def get_data(self):
if self.event.has_subevents:
@@ -178,7 +177,6 @@ class CrossSellingService:
)
if self.customer else None
),
_discount_cache=self._discount_cache,
)
new_items = list()
for item in items:
+3 -18
View File
@@ -961,7 +961,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: List[dict], address: InvoiceAddress,
meta_info: dict, event: Event, sales_channel: SalesChannel, require_approval=False):
meta_info: dict, event: Event, require_approval=False):
fees = []
# Pre-rounding, pre-fee total is used for fee calculation
total = sum([c.gross_price_before_rounding for c in positions])
@@ -1021,14 +1021,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
payments_assigned += to_pay
p['payment_amount'] = to_pay
allow_postponed_payment = (
require_approval or
(
sales_channel.identifier in event.settings.payment_choice_postpone_allowed_channels and not payment_requests
)
)
if total != payments_assigned and not allow_postponed_payment:
if total != payments_assigned and not require_approval:
raise OrderError(_("The selected payment methods do not cover the total balance."))
return fees
@@ -1050,15 +1043,7 @@ def _create_order(event: Event, *, email: str, positions: List[CartPosition], no
# Final calculation of fees, also performs final rounding
try:
fees = _apply_rounding_and_fees(
positions,
payment_requests,
address,
meta_info,
event,
sales_channel=sales_channel,
require_approval=require_approval
)
fees = _apply_rounding_and_fees(positions, payment_requests, address, meta_info, event, require_approval=require_approval)
except TaxRule.SaleNotAllowed:
raise OrderError(error_messages['country_blocked'])
-76
View File
@@ -1,76 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
from django.utils.timezone import make_aware, now
from pretix.base.models import Event, SalesChannel
from pretix.base.reldate import RelativeDateWrapper
def compute_payment_deadline(event: Event, sales_channel: SalesChannel, now_dt=None, subevents=None) -> datetime:
now_dt = now_dt or now()
tz = ZoneInfo(event.settings.timezone)
sales_channel_suffix = "_" + sales_channel.identifier.replace(".", "_")
if not (mode := event.settings.get(f'payment_term_mode{sales_channel_suffix}')):
mode = event.settings.get('payment_term_mode')
sales_channel_suffix = ""
if mode == 'days':
exp_by_date = now_dt.astimezone(tz) + timedelta(
days=event.settings.get(f'payment_term_days{sales_channel_suffix}', as_type=int))
exp_by_date = exp_by_date.astimezone(tz).replace(hour=23, minute=59, second=59, microsecond=0)
if event.settings.get('payment_term_weekdays'):
if exp_by_date.weekday() == 5:
exp_by_date += timedelta(days=2)
elif exp_by_date.weekday() == 6:
exp_by_date += timedelta(days=1)
elif mode == 'minutes':
exp_by_date = now_dt.astimezone(tz) + timedelta(
minutes=event.settings.get(f'payment_term_minutes{sales_channel_suffix}', as_type=int))
else:
raise ValueError("'payment_term_mode' has an invalid value '{}'.".format(mode))
expires = exp_by_date
term_last = event.settings.get('payment_term_last', as_type=RelativeDateWrapper)
if term_last:
if event.has_subevents and subevents:
terms = [
term_last.datetime(se).date()
for se in subevents
]
if not terms:
return expires
term_last = min(terms)
else:
term_last = term_last.datetime(event).date()
term_last = make_aware(datetime.combine(
term_last,
time(hour=23, minute=59, second=59)
), tz)
if term_last < expires:
return term_last
return expires
+2 -68
View File
@@ -343,66 +343,6 @@ def _validate_vat_id_EU(vat_id, country_code):
return vat_id
def _validate_vat_id_EU_fallback_germany(vat_id, country_code, requester_id):
# We can skip most static validation checks because _validate_vat_id_EU always runs before
vat_id = normalize_vat_id(vat_id, country_code)
# The VIES service of the European commission is overused and down due to rate limits A LOT. There is another
# API by German BZSt, but it only works if the requester is German and the requested is not.
# https://www.bzst.de/DE/Unternehmen/Identifikationsnummern/Umsatzsteuer-Identifikationsnummer/AuslaendischeUSt-IdNr/auslaendische_ust_idnr_node.html
try:
r = requests.post(
"https://api.evatr.vies.bzst.de/app/v1/abfrage",
json={
"anfragendeUstid": requester_id,
"angefragteUstid": vat_id,
},
timeout=10,
)
d = r.json()
if r.status_code == 200:
if d['status'] in ('evatr-0000', 'evatr-2008'):
# evatr-0000: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt gültig.
# evatr-2008: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt gültig.
# Für die qualifizierte Bestätigungsanfrage liegt einer Besonderheit vor.
# Für Rückfragen wenden Sie sich an das BZSt.
return vat_id
# evatr-2002: Die angefragte USt-IdNr. ist zum Anfragezeitpunkt nicht gültig.
# Sie ist erst gültig ab dem Datum im Feld gueltigAb.
# evatr-2006: Die angefragte Ust-IdNr. ist zum Anfragezeitpunkt nicht gültig.
# Sie war gültig im Zeitraum, der durch die Werte in den Feldern gueltigAb und gueltigBis beschrieben ist.
raise VATIDFinalError(error_messages['invalid'])
elif r.status_code == 400:
if d['status'] in ('evatr-0002', 'evatr-0004', 'evatr-0008'):
# evatr-0002: Mindestens eins der Pflichtfelder ist nicht besetzt.
# evatr-0004: Die anfragende DE Ust-IdNr. ist syntaktisch falsch. Sie passt nicht in das deutsche Erzeugungsschema.
# evatr-0008: Die maximale Anzahl von qualifizierten Bestätigungsabfragen für diese Session wurde erreicht.
# Bitte starten Sie erneut mit einer einfachen Bestätigungsabfrage.
raise VATIDTemporaryError(error_messages['unavailable'])
# evatr-0005: Die angegebene angefragte Ust-IdNr. ist syntaktisch falsch.
# evatr-0012: Die angefrage USt-IdNr. ist syntaktisch falsch. Sie passt nicht in das Erzeugungsschema.
# evatr-2003: Das angegebene Länderkennzeichen der angefragten USt-IdNr. ist nicht gültig.
raise VATIDFinalError(error_messages['invalid'])
elif r.status_code == 403:
# evatr-0006: Die anfragende DE USt-IdNr. ist nicht berechtigt eine DE Ust-IdNr. anzufragen.
# evatr-0007: Fehlerhafter Aufruf.
raise VATIDTemporaryError(error_messages['unavailable'])
elif r.status_code == 404:
if d['status'] in ('evatr-2005'):
# evatr-2005: Die angegebene eigene DE Ust-IdNr. ist zum Anfragezeitpunkt nicht gültig.
raise VATIDTemporaryError(error_messages['unavailable'])
# evatr-2001: Die angefragte USt-IdNr. ist zum Anfragezeitpunkt nicht vergeben.
raise VATIDFinalError(error_messages['invalid'])
else: # 500, 503
raise VATIDTemporaryError(error_messages['unavailable'])
except requests.RequestException:
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(error_messages['unavailable'])
except ValueError: # JSON parsing failed
logger.exception('VAT ID checking failed for country {}'.format(country_code))
raise VATIDTemporaryError(error_messages['unavailable'])
def _validate_vat_id_CH(vat_id, country_code):
if vat_id[:3] != 'CHE':
raise VATIDFinalError(error_messages['country_mismatch'])
@@ -454,18 +394,12 @@ def _validate_vat_id_CH(vat_id, country_code):
return vat_id
def validate_vat_id(vat_id, country_code, requester_id=None):
def validate_vat_id(vat_id, country_code):
if not vat_id:
return vat_id
country_code = str(country_code)
if is_eu_country(country_code):
try:
return _validate_vat_id_EU(vat_id, country_code)
except VATIDTemporaryError:
if requester_id and requester_id.startswith("DE") and not vat_id.startswith("DE"):
return _validate_vat_id_EU_fallback_germany(vat_id, country_code, requester_id)
else:
raise
return _validate_vat_id_EU(vat_id, country_code)
elif country_code == 'CH':
return _validate_vat_id_CH(vat_id, country_code)
elif country_code == 'NO':
+10 -23
View File
@@ -1159,19 +1159,6 @@ DEFAULTS = {
"configured above."),
)
},
'payment_choice_postpone_allowed_channels': {
'default': [],
'type': list,
'form_class': forms.MultipleChoiceField,
'form_kwargs': dict(
label=_('Allow postponed payment choice for sales channels'),
help_text=_("If postponed payment is allowed on a sales channel, customers can complete their order without "
"selecting a payment method. This is useful whenever orders are not created by the same "
"person who is making the payment."),
widget=forms.CheckboxSelectMultiple,
choices=[],
)
},
'presale_start_show_date': {
'default': 'True',
'type': bool,
@@ -1943,6 +1930,8 @@ DEFAULTS = {
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Hide all unavailable dates from calendar or list views"),
help_text=_("This option currently only affects the calendar of this event series, not the organizer-wide "
"calendar.")
)
},
'event_calendar_future_only': {
@@ -2314,27 +2303,25 @@ DEFAULTS = {
},
'contact_url': {
'default': None,
'type': LazyI18nString,
'form_class': I18nURLFormField,
'type': str,
'serializer_class': serializers.URLField,
'form_class': forms.URLField,
'form_kwargs': dict(
label=_("Contact URL"),
help_text=_("If you set this, the footer contact link will point here instead of using the email address above. "
"Please note that you still need to add a contact email address that will be shared with all emails you send."),
widget=I18nTextInput,
),
'serializer_class': I18nURLField,
"Please note that you still need to add a contact email address that will be shared with all emails you send.")
)
},
'imprint_url': {
'default': None,
'type': LazyI18nString,
'form_class': I18nURLFormField,
'type': str,
'form_class': forms.URLField,
'form_kwargs': dict(
label=_("Imprint URL"),
help_text=_("This should point e.g. to a part of your website that has your contact details and legal "
"information."),
widget=I18nTextInput,
),
'serializer_class': I18nURLField,
'serializer_class': serializers.URLField,
},
'privacy_url': {
'default': None,
+1 -12
View File
@@ -172,9 +172,7 @@ class CachedFileInput(forms.ClearableFileInput):
from ...base.models import CachedFile
v = super().value_from_datadict(data, files, name)
if v is None and data.get(name + '-cachedfile'): # An explicit "[x] clear" would be False, not None
v = CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
if not v.allowed_for_session(self.request):
v = None
return CachedFile.objects.filter(id=data[name + '-cachedfile']).first()
return v
def get_context(self, name, value, attrs):
@@ -246,11 +244,6 @@ class ExtFileField(ExtValidationMixin, SizeFileField):
class CachedFileField(ExtFileField):
widget = CachedFileInput
def __init__(self, *args, **kwargs):
self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs)
self.widget.request = self.request
def to_python(self, data):
from ...base.models import CachedFile
@@ -278,8 +271,6 @@ class CachedFileField(ExtFileField):
filename=data.name,
type=data.content_type,
)
if self.request:
cf.bind_to_session(self.request) # no salt because we want direct web access
cf.file.save(data.name, data.file)
cf.save()
data._uploaded_to = cf
@@ -303,8 +294,6 @@ class CachedFileField(ExtFileField):
filename=data.name,
type=data.content_type,
)
if self.request:
cf.bind_to_session(self.request) # no salt because we want direct web access
cf.file.save(data.name, data.file)
cf.save()
data._uploaded_to = cf
+1 -8
View File
@@ -855,21 +855,14 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
'payment_term_accept_late',
'payment_pending_hidden',
'payment_explanation',
'payment_choice_postpone_allowed_channels',
'tax_rule_payment',
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
channels = list(self.obj.organizer.sales_channels.all())
self.fields['payment_choice_postpone_allowed_channels'].choices = [
(c.identifier, c.label) for c in channels
if c.type_instance.payment_restrictions_supported
]
self.term_channel_fields = {}
for c in channels:
for c in self.obj.organizer.sales_channels.all():
if c.type_instance.payment_restrictions_supported and c.identifier != "web":
# At the moment, it seems sufficient to allow this for the same channel types as other payment settings
# We can always introduce more flags later if needed
-2
View File
@@ -87,7 +87,6 @@ class RRuleForm(forms.Form):
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('4', pgettext_lazy('rrule', 'fourth')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
@@ -135,7 +134,6 @@ class RRuleForm(forms.Form):
('1', pgettext_lazy('rrule', 'first')),
('2', pgettext_lazy('rrule', 'second')),
('3', pgettext_lazy('rrule', 'third')),
('4', pgettext_lazy('rrule', 'fourth')),
('-1', pgettext_lazy('rrule', 'last')),
],
required=False
@@ -109,7 +109,6 @@
{% bootstrap_form_errors form layout="control" %}
{% bootstrap_field form.tax_rule_payment layout="control" %}
{% bootstrap_field form.payment_explanation layout="control" %}
{% bootstrap_field form.payment_choice_postpone_allowed_channels layout="control" %}
</fieldset>
</div>
{% if "event.settings.payment:write" in request.eventpermset %}
+9 -12
View File
@@ -64,7 +64,6 @@ from pretix.base.forms.auth import (
)
from pretix.base.metrics import pretix_failed_logins, pretix_successful_logins
from pretix.base.models import TeamInvite, U2FDevice, User, WebAuthnDevice
from pretix.helpers import OF_SELF
from pretix.helpers.http import get_client_ip, redirect_to_url
from pretix.helpers.ratelimit import rate_limit, rate_limit_reset
from pretix.helpers.security import handle_login_source, session_login
@@ -396,17 +395,15 @@ class Recover(TemplateView):
def post(self, request, *args, **kwargs):
if self.form.is_valid():
with transaction.atomic():
# Check token in transaction to prevent race condition
try:
user = User.objects.select_for_update(of=OF_SELF).get(id=self.request.GET.get('id'), auth_backend='native')
except User.DoesNotExist:
return self.invalid('unknownuser')
if not default_token_generator.check_token(user, self.request.GET.get('token')):
return self.invalid('invalid')
user.set_password(self.form.cleaned_data['password'])
user.needs_password_change = False
user.save()
try:
user = User.objects.get(id=self.request.GET.get('id'), auth_backend='native')
except User.DoesNotExist:
return self.invalid('unknownuser')
if not default_token_generator.check_token(user, self.request.GET.get('token')):
return self.invalid('invalid')
user.set_password(self.form.cleaned_data['password'])
user.needs_password_change = False
user.save()
messages.success(request, _('You can now login using your new password.'))
user.log_action('pretix.control.auth.user.forgot_password.recovered')
+1 -2
View File
@@ -1646,8 +1646,7 @@ class OrderCheckVATID(OrderView):
return redirect(self.get_order_url())
try:
requester_id = self.request.event.settings.invoice_address_from_vat_id
normalized_id = validate_vat_id(ia.vat_id, str(ia.country), requester_id)
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
with transaction.atomic():
ia.vat_id_validated = True
ia.vat_id = normalized_id
-5
View File
@@ -27,7 +27,6 @@ from decimal import Decimal
from io import BytesIO
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.core.files import File
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
@@ -194,7 +193,6 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background_preview.pdf'
c.bind_to_session(request, "ticketoutput-pdf-background")
c.type = 'application/pdf'
c.save()
c.file.save('empty.pdf', ContentFile(buffer.read()))
@@ -220,7 +218,6 @@ class BaseEditorView(EventPermissionRequiredMixin, TemplateView):
c.expires = now() + timedelta(days=7)
c.date = now()
c.filename = 'background_preview.pdf'
c.bind_to_session(request, "ticketoutput-pdf-background")
c.type = 'application/pdf'
c.file = fileobj
c.save()
@@ -306,7 +303,5 @@ class FontsCSSView(TemplateView):
class PdfView(TemplateView):
def get(self, request, *args, **kwargs):
cf = get_object_or_404(CachedFile, id=kwargs.get("filename"), filename="background_preview.pdf")
if not cf.allowed_for_session(request, "ticketoutput-pdf-background"):
raise PermissionDenied()
resp = FileResponse(cf.file, filename=cf.filename, content_type='application/pdf')
return resp
+1 -1
View File
@@ -29,7 +29,7 @@ from django.urls import reverse
def build_absolute_uri(urlname, args=None, kwargs=None):
warnings.warn(
'Usage of build_absolute_uri is confusing since there are many functions with that name. '
'Replace this usage with mainreverse_absolute.',
'Replace this usage with ',
DeprecationWarning
)
return mainreverse_absolute(urlname, args, kwargs)
@@ -1,183 +1,183 @@
/* global gettext */
/*global $, gettext*/
var bankimport_transactionlist = {
_btn_click: function (e) {
console.log(e.delegateTarget)
let trans_id = parseInt($(e.delegateTarget).attr('name').split('_')[1])
let value = $(e.delegateTarget).val()
if (value === 'discard') {
bankimport_transactionlist.discard(trans_id)
} else if (value === 'accept') {
bankimport_transactionlist.accept(trans_id)
} else if (value === 'retry') {
bankimport_transactionlist.retry(trans_id)
} else if (value === 'assign') {
bankimport_transactionlist.assign(trans_id)
}
return false
},
_btn_click: function (e) {
console.log(e.delegateTarget);
var trans_id = parseInt($(e.delegateTarget).attr("name").split("_")[1]);
var value = $(e.delegateTarget).val();
if (value === "discard") {
bankimport_transactionlist.discard(trans_id);
} else if (value === "accept") {
bankimport_transactionlist.accept(trans_id);
} else if (value === "retry") {
bankimport_transactionlist.retry(trans_id);
} else if (value === "assign") {
bankimport_transactionlist.assign(trans_id);
}
return false;
},
_action: function (id, action, success) {
$('tr[data-id=' + id + '] button').prop('disabled', true)
let data = {
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()
}
data['action_' + id] = action
$.ajax({
method: 'POST',
url: $('.transaction-list').attr('data-url'),
data: data,
dataType: 'json',
success: function (data) {
if (data.status == 'ok') {
$('tr[data-id=' + id + ']').removeClass('has-error')
if (data.comment) {
bankimport_transactionlist.comment_reset_to_text(id, data.comment, data.plain)
}
success()
} else {
$('tr[data-id=' + id + '] button').prop('disabled', false)
$('tr[data-id=' + id + '] .help-block').remove()
$('tr[data-id=' + id + ']').addClass('has-error')
$('<p>').addClass('help-block').text(data.message).appendTo($('tr[data-id=' + id + '] td.actions'))
}
}
})
},
_action: function (id, action, success) {
$("tr[data-id=" + id + "] button").prop("disabled", true);
var data = {
"csrfmiddlewaretoken": $("[name=csrfmiddlewaretoken]").val()
};
data["action_" + id] = action;
$.ajax({
"method": "POST",
"url": $(".transaction-list").attr("data-url"),
"data": data,
"dataType": "json",
"success": function (data) {
if (data.status == "ok") {
$("tr[data-id=" + id + "]").removeClass("has-error");
if (data.comment) {
bankimport_transactionlist.comment_reset_to_text(id, data.comment, data.plain);
}
success();
} else {
$("tr[data-id=" + id + "] button").prop("disabled", false);
$("tr[data-id=" + id + "] .help-block").remove();
$("tr[data-id=" + id + "]").addClass("has-error");
$("<p>").addClass("help-block").text(data.message).appendTo($("tr[data-id=" + id + "] td.actions"));
}
}
});
},
discard: function (id) {
bankimport_transactionlist._action(id, 'discard', function () {
$('tr[data-id=' + id + '] td').remove()
})
},
discard: function (id) {
bankimport_transactionlist._action(id, "discard", function () {
$("tr[data-id=" + id + "] td").remove();
});
},
retry: function (id) {
bankimport_transactionlist._action(id, 'retry', function () {
$('tr[data-id=' + id + '] td.actions').html('').text(gettext('Marked as paid'))
})
},
retry: function (id) {
bankimport_transactionlist._action(id, "retry", function () {
$("tr[data-id=" + id + "] td.actions").html('').text(gettext("Marked as paid"));
});
},
accept: function (id) {
bankimport_transactionlist._action(id, 'accept', function () {
$('tr[data-id=' + id + '] td.actions').html('').text(gettext('Marked as paid'))
})
},
accept: function (id) {
bankimport_transactionlist._action(id, "accept", function () {
$("tr[data-id=" + id + "] td.actions").html('').text(gettext("Marked as paid"));
});
},
assign: function (id) {
bankimport_transactionlist._action(id, 'assign:' + $('tr[data-id=' + id + '] input.form-control:not(.tt-hint)').val(), function () {
$('tr[data-id=' + id + '] td.actions').html('').text(gettext('Marked as paid'))
})
},
assign: function (id) {
bankimport_transactionlist._action(id, "assign:" + $("tr[data-id=" + id + "] input.form-control:not(.tt-hint)").val(), function () {
$("tr[data-id=" + id + "] td.actions").html('').text(gettext("Marked as paid"));
});
},
comment_reset_to_text: function (id, text, plain) {
let $box = $('tr[data-id=' + id + '] .comment-box')
$box[0].dataset['plain'] = plain
$box.html('')
.append($('<strong>').text(gettext('Comment:')))
.append(' ')
.append($('<span>').addClass('comment').append(' ').append(text))
.append(' ')
.append($('<a>').addClass('comment-modify btn btn-default btn-xs')
.append('<span class=\'fa fa-edit\'></span>'))
},
comment_reset_to_text: function (id, text, plain) {
var $box = $("tr[data-id=" + id + "] .comment-box");
$box[0].dataset["plain"] = plain;
$box.html("")
.append($("<strong>").text(gettext("Comment:")))
.append(" ")
.append($("<span>").addClass("comment").append(" ").append(text))
.append(" ")
.append($("<a>").addClass("comment-modify btn btn-default btn-xs")
.append("<span class='fa fa-edit'></span>"));
},
comment_start_edit: function (e) {
let $box = $(e.target).closest('div')
let id = $box.closest('tr').attr('data-id')
let $inp = $('<textarea>').addClass('form-control')
let orig_rendered = $box.find('.comment')
let orig_text = $box[0].dataset.plain
$inp.val(orig_text)
comment_start_edit: function (e) {
var $box = $(e.target).closest("div");
var id = $box.closest("tr").attr("data-id");
var $inp = $("<textarea>").addClass("form-control");
var orig_rendered = $box.find(".comment");
var orig_text = $box[0].dataset.plain;
$inp.val(orig_text);
let $btngrp = $('<div>')
$btngrp.addClass('btn-group')
let $btn1 = $('<button>')
$btn1.attr('type', 'button').addClass('btn btn-default')
$btn1.append('<span class=\'fa fa-check\'></span>')
$btngrp.append($btn1)
let $btn2 = $('<button>')
$btn2.attr('type', 'button').addClass('btn btn-default')
$btn2.append('<span class=\'fa fa-close\'></span>')
$btngrp.append($btn2)
$box.html('').append($inp).append($btngrp)
$btn1.click(function () {
let text = $box.find('textarea').val()
$box.find('input, textarea, button').prop('disabled', true)
bankimport_transactionlist._action(id, 'comment:' + text, function () {
$('tr[data-id=' + id + '] button').prop('disabled', false)
})
})
$btn2.click(function () {
bankimport_transactionlist.comment_reset_to_text(id, orig_rendered, orig_text)
})
var $btngrp = $("<div>");
$btngrp.addClass("btn-group");
var $btn1 = $("<button>");
$btn1.attr("type", "button").addClass("btn btn-default");
$btn1.append("<span class='fa fa-check'></span>");
$btngrp.append($btn1);
var $btn2 = $("<button>");
$btn2.attr("type", "button").addClass("btn btn-default");
$btn2.append("<span class='fa fa-close'></span>");
$btngrp.append($btn2);
$box.html("").append($inp).append($btngrp);
$btn1.click(function () {
var text = $box.find("textarea").val();
$box.find("input, textarea, button").prop("disabled", true);
bankimport_transactionlist._action(id, "comment:" + text, function () {
$("tr[data-id=" + id + "] button").prop("disabled", false);
});
});
$btn2.click(function () {
bankimport_transactionlist.comment_reset_to_text(id, orig_rendered, orig_text);
});
e.preventDefault()
},
e.preventDefault();
},
typeahead_source: function () {
return new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: $('.transaction-list').attr('data-url'),
prepare: function (query, settings) {
settings.url = settings.url + '?query=' + encodeURIComponent(query)
return settings
},
transform: function (object) {
let results = object.results
let suggs = []
let reslen = results.length
for (let i = 0; i < reslen; i++) {
suggs.push(results[i])
}
return suggs
}
}
})
},
typeahead_source: function () {
return new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
remote: {
url: $(".transaction-list").attr("data-url"),
prepare: function (query, settings) {
settings.url = settings.url + '?query=' + encodeURIComponent(query);
return settings;
},
transform: function (object) {
var results = object.results;
var suggs = [];
var reslen = results.length;
for (var i = 0; i < reslen; i++) {
suggs.push(results[i]);
}
return suggs;
}
}
});
},
init: function () {
if ($('.transaction-list').length) {
$('.transaction-list button').click(bankimport_transactionlist._btn_click)
init: function () {
if ($(".transaction-list").length) {
$(".transaction-list button").click(bankimport_transactionlist._btn_click);
$('.transaction-list').on('click', '.comment-modify', bankimport_transactionlist.comment_start_edit)
$(".transaction-list").on("click", ".comment-modify", bankimport_transactionlist.comment_start_edit);
$('.transaction-list .form-control').typeahead(null, {
minLength: 2,
name: 'order-dataset',
source: bankimport_transactionlist.typeahead_source(),
display: function (obj) {
return obj.code
},
templates: {
suggestion: function (obj) {
return '<div>' + obj.code + ' (' + obj.total + ', ' + obj.status + ')</div>'
}
}
}).keypress(function (e) {
if (e.keyCode === 13) {
$(this).parent().parent().find('button[value=assign]').click()
}
})
}
$(".transaction-list .form-control").typeahead(null, {
minLength: 2,
name: 'order-dataset',
source: bankimport_transactionlist.typeahead_source(),
display: function (obj) {
return obj.code;
},
templates: {
suggestion: function (obj) {
return '<div>' + obj.code + ' (' + obj.total + ', ' + obj.status + ')</div>';
}
}
}).keypress(function (e) {
if (e.keyCode === 13) {
$(this).parent().parent().find("button[value=assign]").click();
}
});
}
if ($('[data-job-waiting]').length) {
window.setTimeout(bankimport_transactionlist.check_state, 750)
}
},
if ($("[data-job-waiting]").length) {
window.setTimeout(bankimport_transactionlist.check_state, 750);
}
},
check_state: function () {
$.getJSON($('[data-job-waiting-url]').attr('data-job-waiting-url'), function (data) {
if (data.state == 'running' || data.state == 'pending') {
window.setTimeout(bankimport_transactionlist.check_state, 750)
} else {
location.reload()
}
})
}
}
check_state: function () {
$.getJSON($("[data-job-waiting-url]").attr("data-job-waiting-url"), function (data) {
if (data.state == 'running' || data.state == 'pending') {
window.setTimeout(bankimport_transactionlist.check_state, 750);
} else {
location.reload();
}
});
}
};
$(function () {
bankimport_transactionlist.init()
})
bankimport_transactionlist.init();
});
@@ -1,350 +1,350 @@
/* global paypal_client_id, paypal_loadingmessage, gettext */
'use strict'
/*global $, paypal_client_id, paypal_loadingmessage, gettext */
'use strict';
var pretixpaypal = {
paypal: null,
client_id: null,
order_id: null,
payer_id: null,
merchant_id: null,
currency: null,
method: null,
additional_disabled_funding: null,
additional_enabled_funding: null,
debug_buyer_country: null,
continue_button: null,
paypage: false,
method_map: {
wallet: {
method: 'wallet',
funding_source: 'paypal',
// disable_funding: null,
// enable_funding: 'paylater',
early_auth: true,
},
apm: {
method: 'apm',
funding_source: null,
// disable_funding: null,
// enable_funding: null,
early_auth: false,
}
},
apm_map: {
paypal: gettext('PayPal'),
venmo: gettext('Venmo'),
applepay: gettext('Apple Pay'),
itau: gettext('Itaú'),
credit: gettext('PayPal Credit'),
card: gettext('Credit Card'),
paylater: gettext('PayPal Pay Later'),
ideal: gettext('iDEAL | Wero'),
sepa: gettext('SEPA Direct Debit'),
bancontact: gettext('Bancontact'),
giropay: gettext('giropay'),
sofort: gettext('SOFORT'),
eps: gettext('eps'),
mybank: gettext('MyBank'),
p24: gettext('Przelewy24'),
verkkopankki: gettext('Verkkopankki'),
payu: gettext('PayU'),
blik: gettext('BLIK'),
trustly: gettext('Trustly'),
zimpler: gettext('Zimpler'),
maxima: gettext('Maxima'),
oxxo: gettext('OXXO'),
boleto: gettext('Boleto'),
wechatpay: gettext('WeChat Pay'),
mercadopago: gettext('Mercado Pago')
},
readyToSubmitApproval: false,
paypal: null,
client_id: null,
order_id: null,
payer_id: null,
merchant_id: null,
currency: null,
method: null,
additional_disabled_funding: null,
additional_enabled_funding: null,
debug_buyer_country: null,
continue_button: null,
paypage: false,
method_map: {
wallet: {
method: 'wallet',
funding_source: 'paypal',
//disable_funding: null,
//enable_funding: 'paylater',
early_auth: true,
},
apm: {
method: 'apm',
funding_source: null,
//disable_funding: null,
//enable_funding: null,
early_auth: false,
}
},
apm_map: {
paypal: gettext('PayPal'),
venmo: gettext('Venmo'),
applepay: gettext('Apple Pay'),
itau: gettext('Itaú'),
credit: gettext('PayPal Credit'),
card: gettext('Credit Card'),
paylater: gettext('PayPal Pay Later'),
ideal: gettext('iDEAL | Wero'),
sepa: gettext('SEPA Direct Debit'),
bancontact: gettext('Bancontact'),
giropay: gettext('giropay'),
sofort: gettext('SOFORT'),
eps: gettext('eps'),
mybank: gettext('MyBank'),
p24: gettext('Przelewy24'),
verkkopankki: gettext('Verkkopankki'),
payu: gettext('PayU'),
blik: gettext('BLIK'),
trustly: gettext('Trustly'),
zimpler: gettext('Zimpler'),
maxima: gettext('Maxima'),
oxxo: gettext('OXXO'),
boleto: gettext('Boleto'),
wechatpay: gettext('WeChat Pay'),
mercadopago: gettext('Mercado Pago')
},
readyToSubmitApproval: false,
load: function () {
if (pretixpaypal.paypal === null) {
pretixpaypal.client_id = $.trim($('#paypal_client_id').html())
pretixpaypal.merchant_id = $.trim($('#paypal_merchant_id').html())
pretixpaypal.debug_buyer_country = $.trim($('#paypal_buyer_country').html())
pretixpaypal.continue_button = $('.checkout-button-row').closest('form').find('.checkout-button-row .btn-primary')
pretixpaypal.continue_button.closest('div').append('<div id="paypal-button-container"></div>')
pretixpaypal.additional_disabled_funding = $.trim($('#paypal_disable_funding').html())
pretixpaypal.additional_enabled_funding = $.trim($('#paypal_enable_funding').html())
pretixpaypal.paypage = Boolean($('#paypal-button-container').data('paypage'))
pretixpaypal.order_id = $.trim($('#paypal_oid').html())
pretixpaypal.currency = $('body').attr('data-currency')
pretixpaypal.locale = this.guessLocale()
}
load: function () {
if (pretixpaypal.paypal === null) {
pretixpaypal.client_id = $.trim($("#paypal_client_id").html());
pretixpaypal.merchant_id = $.trim($("#paypal_merchant_id").html());
pretixpaypal.debug_buyer_country = $.trim($("#paypal_buyer_country").html());
pretixpaypal.continue_button = $('.checkout-button-row').closest("form").find(".checkout-button-row .btn-primary");
pretixpaypal.continue_button.closest('div').append('<div id="paypal-button-container"></div>');
pretixpaypal.additional_disabled_funding = $.trim($("#paypal_disable_funding").html());
pretixpaypal.additional_enabled_funding = $.trim($("#paypal_enable_funding").html());
pretixpaypal.paypage = Boolean($('#paypal-button-container').data('paypage'));
pretixpaypal.order_id = $.trim($("#paypal_oid").html());
pretixpaypal.currency = $("body").attr("data-currency");
pretixpaypal.locale = this.guessLocale();
}
$('input[name=payment][value^=\'paypal\']').change(function () {
if (pretixpaypal.paypal !== null) {
pretixpaypal.renderButton($(this).val())
} else {
pretixpaypal.continue_button.prop('disabled', true)
}
})
$("input[name=payment][value^='paypal']").change(function () {
if (pretixpaypal.paypal !== null) {
pretixpaypal.renderButton($(this).val());
} else {
pretixpaypal.continue_button.prop("disabled", true);
}
});
$('input[name=payment]').not('[value^=\'paypal\']').change(function () {
pretixpaypal.restore()
})
$("input[name=payment]").not("[value^='paypal']").change(function () {
pretixpaypal.restore();
});
// If paypal is pre-selected, we must disable the continue button and handle it after SDK is loaded
if ($('input[name=payment][value^=\'paypal\']').is(':checked')) {
pretixpaypal.continue_button.prop('disabled', true)
}
// If paypal is pre-selected, we must disable the continue button and handle it after SDK is loaded
if ($("input[name=payment][value^='paypal']").is(':checked')) {
pretixpaypal.continue_button.prop("disabled", true);
}
// We are setting the cogwheel already here, as the renderAPM() method might take some time to get loaded.
const apmtextselector = $('input[name=payment][value=paypal_apm]').closest('label').find('.accordion-label-text')
apmtextselector.append(' <span aria-hidden="true" class="fa fa-cog fa-spin"></span>')
// We are setting the cogwheel already here, as the renderAPM() method might take some time to get loaded.
const apmtextselector = $("input[name=payment][value=paypal_apm]").closest("label").find(".accordion-label-text");
apmtextselector.append(' <span aria-hidden="true" class="fa fa-cog fa-spin"></span>');
let sdk_url = 'https://www.paypal.com/sdk/js'
+ '?client-id=' + pretixpaypal.client_id
+ '&components=buttons,funding-eligibility'
+ '&currency=' + pretixpaypal.currency
let sdk_url = 'https://www.paypal.com/sdk/js' +
'?client-id=' + pretixpaypal.client_id +
'&components=buttons,funding-eligibility' +
'&currency=' + pretixpaypal.currency;
if (pretixpaypal.locale) {
sdk_url += '&locale=' + pretixpaypal.locale
}
if (pretixpaypal.locale) {
sdk_url += '&locale=' + pretixpaypal.locale;
}
if (pretixpaypal.merchant_id) {
sdk_url += '&merchant-id=' + pretixpaypal.merchant_id
}
if (pretixpaypal.merchant_id) {
sdk_url += '&merchant-id=' + pretixpaypal.merchant_id;
}
if (pretixpaypal.additional_disabled_funding) {
sdk_url += '&disable-funding=' + [pretixpaypal.additional_disabled_funding].filter(Boolean).join(',')
}
if (pretixpaypal.additional_disabled_funding) {
sdk_url += '&disable-funding=' + [pretixpaypal.additional_disabled_funding].filter(Boolean).join(',');
}
if (pretixpaypal.additional_enabled_funding) {
sdk_url += '&enable-funding=' + [pretixpaypal.additional_enabled_funding].filter(Boolean).join(',')
}
if (pretixpaypal.additional_enabled_funding) {
sdk_url += '&enable-funding=' + [pretixpaypal.additional_enabled_funding].filter(Boolean).join(',');
}
if (pretixpaypal.debug_buyer_country) {
sdk_url += '&buyer-country=' + pretixpaypal.debug_buyer_country
}
if (pretixpaypal.debug_buyer_country) {
sdk_url += '&buyer-country=' + pretixpaypal.debug_buyer_country;
}
let ppscript = document.createElement('script')
let ready = false
let head = document.getElementsByTagName('head')[0]
ppscript.setAttribute('src', sdk_url)
ppscript.setAttribute('data-csp-nonce', $.trim($('#csp_nonce').html()))
ppscript.setAttribute('data-page-type', 'checkout')
ppscript.setAttribute('data-partner-attribution-id', 'ramiioGmbH_Cart_PPCP')
document.head.appendChild(ppscript)
let ppscript = document.createElement('script');
let ready = false;
let head = document.getElementsByTagName("head")[0];
ppscript.setAttribute('src', sdk_url);
ppscript.setAttribute('data-csp-nonce', $.trim($("#csp_nonce").html()));
ppscript.setAttribute('data-page-type', 'checkout');
ppscript.setAttribute('data-partner-attribution-id', 'ramiioGmbH_Cart_PPCP');
document.head.appendChild(ppscript);
ppscript.onload = ppscript.onreadystatechange = function () {
if (!ready && (!this.readyState || this.readyState === 'loaded' || this.readyState === 'complete')) {
ready = true
ppscript.onload = ppscript.onreadystatechange = function () {
if (!ready && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete")) {
ready = true;
pretixpaypal.paypal = paypal
pretixpaypal.paypal = paypal;
// Handle memory leak in IE
ppscript.onload = ppscript.onreadystatechange = null
if (head && ppscript.parentNode) {
head.removeChild(ppscript)
}
}
}
// Handle memory leak in IE
ppscript.onload = ppscript.onreadystatechange = null;
if (head && ppscript.parentNode) {
head.removeChild(ppscript);
}
}
};
document.addEventListener('visibilitychange', this.onApproveSubmit)
},
document.addEventListener("visibilitychange", this.onApproveSubmit);
},
ready: function () {
if ($('input[name=payment][value=paypal_apm]').length > 0) {
pretixpaypal.renderAPMs()
}
ready: function () {
if ($("input[name=payment][value=paypal_apm]").length > 0) {
pretixpaypal.renderAPMs();
}
if ($('input[name=payment][value^=\'paypal\']').is(':checked')) {
pretixpaypal.renderButton($('input[name=payment][value^=\'paypal\']:checked').val())
} else if ($('.payment-redo-form').length) {
pretixpaypal.renderButton($('input[name=payment][value^=\'paypal\']').val())
} else if ($('#paypal-button-container').data('paypage')) {
pretixpaypal.renderButton('paypal_apm')
}
},
if ($("input[name=payment][value^='paypal']").is(':checked')) {
pretixpaypal.renderButton($("input[name=payment][value^='paypal']:checked").val());
} else if ($(".payment-redo-form").length) {
pretixpaypal.renderButton($("input[name=payment][value^='paypal']").val());
} else if ($('#paypal-button-container').data('paypage')) {
pretixpaypal.renderButton('paypal_apm');
}
},
restore: function () {
// if PayPal has not been initialized, there shouldn't be anything to cleanup
if (pretixpaypal.paypal !== null) {
$('#paypal-button-container').empty()
pretixpaypal.continue_button.text(gettext('Continue'))
pretixpaypal.continue_button.show()
}
pretixpaypal.continue_button.prop('disabled', false)
},
restore: function () {
// if PayPal has not been initialized, there shouldn't be anything to cleanup
if (pretixpaypal.paypal !== null) {
$('#paypal-button-container').empty()
pretixpaypal.continue_button.text(gettext('Continue'));
pretixpaypal.continue_button.show();
}
pretixpaypal.continue_button.prop("disabled", false);
},
renderButton: function (method) {
if (method === 'paypal') {
method = 'wallet'
} else {
method = method.split('paypal_').at(-1)
}
pretixpaypal.method = pretixpaypal.method_map[method]
renderButton: function (method) {
if (method === 'paypal') {
method = "wallet"
} else {
method = method.split('paypal_').at(-1)
}
pretixpaypal.method = pretixpaypal.method_map[method];
if (pretixpaypal.method.method === 'apm' && !pretixpaypal.paypage) {
pretixpaypal.restore()
return
}
if (pretixpaypal.method.method === 'apm' && !pretixpaypal.paypage) {
pretixpaypal.restore();
return;
}
$('#paypal-button-container').empty()
$('#paypal-card-container').empty()
$('#paypal-button-container').empty()
$('#paypal-card-container').empty()
let button = pretixpaypal.paypal.Buttons({
fundingSource: pretixpaypal.method.funding_source,
style: {
layout: pretixpaypal.method.early_auth ? 'horizontal' : 'vertical',
// color: 'white',
shape: 'rect',
label: 'pay',
tagline: false
},
createOrder: function (data, actions) {
if (pretixpaypal.order_id) {
return pretixpaypal.order_id
}
let button = pretixpaypal.paypal.Buttons({
fundingSource: pretixpaypal.method.funding_source,
style: {
layout: pretixpaypal.method.early_auth ? 'horizontal' : 'vertical',
//color: 'white',
shape: 'rect',
label: 'pay',
tagline: false
},
createOrder: function (data, actions) {
if (pretixpaypal.order_id) {
return pretixpaypal.order_id;
}
// On the paypal:pay view, we already pregenerated the OID.
// Since this view is also only used for APMs, we only need the XHR-calls for the Smart Payment Buttons.
if (pretixpaypal.paypage) {
return $('#payment_paypal_' + pretixpaypal.method.method + '_oid')
} else {
var xhrurl = $('#payment_paypal_' + pretixpaypal.method.method + '_xhr').val()
}
// On the paypal:pay view, we already pregenerated the OID.
// Since this view is also only used for APMs, we only need the XHR-calls for the Smart Payment Buttons.
if (pretixpaypal.paypage) {
return $("#payment_paypal_" + pretixpaypal.method.method + "_oid");
} else {
var xhrurl = $("#payment_paypal_" + pretixpaypal.method.method + "_xhr").val();
}
return fetch(xhrurl, {
method: 'POST'
}).then(function (res) {
return res.json()
}).then(function (data) {
if ('id' in data) {
return data.id
} else {
// Refreshing the page to surface the request-error message
location.reload()
}
})
},
onApprove: function (data, actions) {
waitingDialog.show(gettext('Confirming your payment …'))
pretixpaypal.order_id = data.orderID
pretixpaypal.payer_id = data.payerID
return fetch(xhrurl, {
method: 'POST'
}).then(function (res) {
return res.json();
}).then(function (data) {
if ('id' in data) {
return data.id;
} else {
// Refreshing the page to surface the request-error message
location.reload();
}
});
},
onApprove: function (data, actions) {
waitingDialog.show(gettext("Confirming your payment …"));
pretixpaypal.order_id = data.orderID;
pretixpaypal.payer_id = data.payerID;
let method = pretixpaypal.paypage ? 'wallet' : pretixpaypal.method.method
let selectorstub = '#payment_paypal_' + method
// Insert the tokens into the form, so it gets submitted to the server
$(selectorstub + '_oid').val(pretixpaypal.order_id)
$(selectorstub + '_payer').val(pretixpaypal.payer_id)
let method = pretixpaypal.paypage ? "wallet" : pretixpaypal.method.method;
let selectorstub = "#payment_paypal_" + method;
// Insert the tokens into the form, so it gets submitted to the server
$(selectorstub + "_oid").val(pretixpaypal.order_id);
$(selectorstub + "_payer").val(pretixpaypal.payer_id);
// We are moving the submission to a separate function, which is also an EventListener, since
// SFSafariView refuses to submit a form that is not visible. Unfortunately, that is exactly the case
// when the ticket shop is used on iOS within an SFSafariView and the PayPal payment popup has not
// closed itself quickly enough.
pretixpaypal.readyToSubmitApproval = true
pretixpaypal.onApproveSubmit()
// We are moving the submission to a separate function, which is also an EventListener, since
// SFSafariView refuses to submit a form that is not visible. Unfortunately, that is exactly the case
// when the ticket shop is used on iOS within an SFSafariView and the PayPal payment popup has not
// closed itself quickly enough.
pretixpaypal.readyToSubmitApproval = true;
pretixpaypal.onApproveSubmit();
// billingToken: null
// facilitatorAccessToken: "A21AAL_fEu0gDD-sIXyOy65a6MjgSJJrhmxuPcxxUGnL5gW2DzTxiiAksfoC4x8hD-BjeY1LsFVKl7ceuO7UR1a9pQr8Q_AVw"
// orderID: "7RF70259NY7589848"
// payerID: "8M3BU92Z97VXA"
// paymentID: null
},
})
// billingToken: null
// facilitatorAccessToken: "A21AAL_fEu0gDD-sIXyOy65a6MjgSJJrhmxuPcxxUGnL5gW2DzTxiiAksfoC4x8hD-BjeY1LsFVKl7ceuO7UR1a9pQr8Q_AVw"
// orderID: "7RF70259NY7589848"
// payerID: "8M3BU92Z97VXA"
// paymentID: null
},
});
if (button.isEligible()) {
button.render('#paypal-button-container')
pretixpaypal.continue_button.hide()
} else {
pretixpaypal.continue_button.text(gettext('Payment method unavailable'))
pretixpaypal.continue_button.show()
}
},
if (button.isEligible()) {
button.render('#paypal-button-container');
pretixpaypal.continue_button.hide();
} else {
pretixpaypal.continue_button.text(gettext('Payment method unavailable'));
pretixpaypal.continue_button.show();
}
},
onApproveSubmit: function () {
if (document.visibilityState === 'visible' && pretixpaypal.readyToSubmitApproval === true) {
let method = pretixpaypal.paypage ? 'wallet' : pretixpaypal.method.method
let selectorstub = '#payment_paypal_' + method
let $form = $(selectorstub + '_oid').closest('form')
onApproveSubmit: function() {
if (document.visibilityState === "visible" && pretixpaypal.readyToSubmitApproval === true) {
let method = pretixpaypal.paypage ? "wallet" : pretixpaypal.method.method;
let selectorstub = "#payment_paypal_" + method;
var $form = $(selectorstub + "_oid").closest("form");
$form.get(0).submit()
}
},
$form.get(0).submit();
}
},
renderAPMs: function () {
pretixpaypal.restore()
let inputselector = $('input[name=payment][value=paypal_apm]')
let textselector = inputselector.closest('label').find('.accordion-label-text')
let eligibles = []
renderAPMs: function () {
pretixpaypal.restore();
let inputselector = $("input[name=payment][value=paypal_apm]");
let textselector = inputselector.closest("label").find('.accordion-label-text');
let eligibles = [];
pretixpaypal.paypal.getFundingSources().forEach(function (fundingSource) {
// Let's always skip PayPal, since it's always a dedicated funding source
if (fundingSource === 'paypal') {
return
}
pretixpaypal.paypal.getFundingSources().forEach(function (fundingSource) {
// Let's always skip PayPal, since it's always a dedicated funding source
if (fundingSource === 'paypal') {
return;
}
// This could also be paypal.Marks() - but they only expose images instead of cleartext...
let button = pretixpaypal.paypal.Buttons({
fundingSource: fundingSource
})
// This could also be paypal.Marks() - but they only expose images instead of cleartext...
let button = pretixpaypal.paypal.Buttons({
fundingSource: fundingSource
});
if (button.isEligible()) {
eligibles.push(gettext(pretixpaypal.apm_map[fundingSource] || fundingSource))
}
})
if (button.isEligible()) {
eligibles.push(gettext(pretixpaypal.apm_map[fundingSource] || fundingSource));
}
});
inputselector.attr('title', eligibles.join(', '))
textselector.fadeOut(300, function () {
textselector.text(eligibles.join(', '))
textselector.fadeIn(300)
})
},
inputselector.attr('title', eligibles.join(', '));
textselector.fadeOut(300, function () {
textselector.text(eligibles.join(', '));
textselector.fadeIn(300);
});
},
guessLocale: function () {
// This is a horrible hackjob and does not at all take into consideration the actual locale.
// Instead, we only look at the language that the shop is currently being displayed in and make
// that into a locale.
let allowed_locales = [
'en_US',
'ar_DZ',
'fr_FR',
'es_ES',
'zh_CN',
'de_DE',
'nl_NL',
'pt_PT',
'cs_CZ',
'da_DK',
'fi_FI',
'el_GR',
'hu_HU',
'id_ID',
'he_IL',
'it_IT',
'ja_JP',
'ru_RU',
'no_NO',
'pl_PL',
'sk_SK',
'sv_SE',
'th_TH',
'tr_TR',
]
let lang = $('body').attr('data-locale').split('-')[0]
return allowed_locales.find(element => element.startsWith(lang))
}
}
guessLocale: function() {
// This is a horrible hackjob and does not at all take into consideration the actual locale.
// Instead, we only look at the language that the shop is currently being displayed in and make
// that into a locale.
let allowed_locales = [
'en_US',
'ar_DZ',
'fr_FR',
'es_ES',
'zh_CN',
'de_DE',
'nl_NL',
'pt_PT',
'cs_CZ',
'da_DK',
'fi_FI',
'el_GR',
'hu_HU',
'id_ID',
'he_IL',
'it_IT',
'ja_JP',
'ru_RU',
'no_NO',
'pl_PL',
'sk_SK',
'sv_SE',
'th_TH',
'tr_TR',
]
let lang = $("body").attr("data-locale").split('-')[0];
return allowed_locales.find(element => element.startsWith(lang));
}
};
$(function () {
// This script is always loaded if paypal is enabled as a payment method, regardless of
// whether it is available (it could e.g. be hidden or limited to certain countries).
// We do not want to unnecessarily load the sdk.
// If no paypal/paypal_apm payment option is present and we are not on
// the (APM) PayView, then we do not need the SDK.
if (!$('input[name=payment][value^=\'paypal\']').length && !$('#paypal-button-container').data('paypage')) {
return
}
// This script is always loaded if paypal is enabled as a payment method, regardless of
// whether it is available (it could e.g. be hidden or limited to certain countries).
// We do not want to unnecessarily load the sdk.
// If no paypal/paypal_apm payment option is present and we are not on
// the (APM) PayView, then we do not need the SDK.
if (!$("input[name=payment][value^='paypal']").length && !$('#paypal-button-container').data('paypage')) {
return
}
pretixpaypal.load();
pretixpaypal.load();
(async () => {
while (!pretixpaypal.paypal)
await new Promise(resolve => setTimeout(resolve, 1000))
pretixpaypal.ready()
})()
})
(async() => {
while(!pretixpaypal.paypal)
await new Promise(resolve => setTimeout(resolve, 1000));
pretixpaypal.ready();
})();
});
+8 -11
View File
@@ -56,11 +56,18 @@ from pretix.base.services.placeholders import FormPlaceholderMixin # noqa
class BaseMailForm(FormPlaceholderMixin, forms.Form):
subject = forms.CharField(label=_("Subject"))
message = forms.CharField(label=_("Message"))
attachment = CachedFileField(
label=_("Attachment"),
required=False,
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
help_text=_('Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
'of no more than 2 MB in size.'),
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT
)
def __init__(self, *args, **kwargs):
event = self.event = kwargs.pop('event')
context_parameters = kwargs.pop('context_parameters')
request = kwargs.pop('request')
super().__init__(*args, **kwargs)
self.fields['subject'] = I18nFormField(
label=_('Subject'),
@@ -72,16 +79,6 @@ class BaseMailForm(FormPlaceholderMixin, forms.Form):
widget=I18nMarkdownTextarea, required=True,
locales=event.settings.get('locales'),
)
self.fields['attachment'] = CachedFileField(
label=_("Attachment"),
required=False,
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
help_text=_(
'Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
'of no more than 2 MB in size.'),
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT,
request=request,
)
self._set_field_placeholders('subject', context_parameters, rich=False)
self._set_field_placeholders('message', context_parameters, rich=True)
+16 -24
View File
@@ -157,7 +157,6 @@ class BaseSenderView(EventPermissionRequiredMixin, FormView):
kwargs = super().get_form_kwargs()
kwargs['event'] = self.request.event
kwargs['context_parameters'] = self.context_parameters
kwargs['request'] = self.request
if 'from_log' in self.request.GET:
try:
from_log_id = self.request.GET.get('from_log')
@@ -367,43 +366,36 @@ class OrderSendView(BaseSenderView):
)
if form.cleaned_data.get('filter_checkins'):
ci_filter = Q(pk__in=[]) # return nothing
ql = []
if form.cleaned_data.get('not_checked_in'):
consider_tickets_used_lists = list(self.request.event.checkin_lists.filter(consider_tickets_used=True).values_list("id", flat=True))
opq = opq.alias(
any_checkins=Exists(
Checkin.objects.with_scopes_disabled().filter(
position_id=OuterRef('pk'),
list_id__in=consider_tickets_used_lists,
)
) | Exists(
Checkin.objects.with_scopes_disabled().filter(
position__addon_to_id=OuterRef('pk'),
list_id__in=consider_tickets_used_lists,
Checkin.all.filter(
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
successful=True,
list__consider_tickets_used=True,
)
)
)
ci_filter |= Q(any_checkins=False)
ql.append(Q(any_checkins=False))
if form.cleaned_data.get('checkin_lists'):
opq = opq.alias(
matching_checkins=Exists(
Checkin.objects.with_scopes_disabled().filter(
position_id=OuterRef('pk'),
list_id__in=[i.pk for i in form.cleaned_data.get('checkin_lists', [])],
)
) | Exists(
Checkin.objects.with_scopes_disabled().filter(
position__addon_to_id=OuterRef('pk'),
Checkin.all.filter(
Q(position_id=OuterRef('pk')) | Q(position__addon_to_id=OuterRef('pk')),
list_id__in=[i.pk for i in form.cleaned_data.get('checkin_lists', [])],
successful=True
)
)
)
ci_filter |= Q(matching_checkins=True)
opq = opq.filter(ci_filter)
ql.append(Q(matching_checkins=True))
if len(ql) == 2:
opq = opq.filter(ql[0] | ql[1])
elif ql:
opq = opq.filter(ql[0])
else:
opq = opq.none()
if form.cleaned_data.get('subevent'):
opq = opq.filter(subevent=form.cleaned_data.get('subevent'))
@@ -1,81 +1,81 @@
/* globals Morris, django */
function gettext (msgid) {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid)
}
return msgid
/*globals $, Morris, gettext, django*/
function gettext(msgid) {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid);
}
return msgid;
}
$(function () {
$('.chart').css('height', '250px')
new Morris.Area({
element: 'obd_chart',
data: JSON.parse($('#obd-data').html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
})
new Morris.Area({
element: 'abd_chart',
data: JSON.parse($('#abd-data').html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
})
new Morris.Area({
element: 'abt_chart',
data: JSON.parse($('#abt-data').html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
})
new Morris.Area({
element: 'rev_chart',
data: JSON.parse($('#rev-data').html()),
xkey: 'date',
ykeys: ['revenue'],
labels: [gettext('Total revenue')],
smooth: false,
resize: true,
lineColors: ['#3b1c4a'],
fillOpacity: 0.3,
preUnits: $.trim($('#currency').html()) + ' '
})
new Morris.Bar({
element: 'obp_chart',
data: JSON.parse($('#obp-data').html()),
xkey: 'item_short',
ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')],
barColors: ['#3b1c4a', '#50a167'],
hoverCallback: function (index, options, content, row) {
console.log(content)
let $c = $('<div>' + content + '</div>')
let $label = $c.find('.morris-hover-row-label')
$label.text(row.item)
let newc = $label.get(0).outerHTML
$c.find('.morris-hover-point').each(function (i, r) {
if ($.trim($(r).text().split('\n')[2]) !== '0') {
newc += r.outerHTML
}
})
return newc
},
resize: true,
xLabelAngle: 30
})
})
$(".chart").css("height", "250px");
new Morris.Area({
element: 'obd_chart',
data: JSON.parse($("#obd-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'abd_chart',
data: JSON.parse($("#abd-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'abt_chart',
data: JSON.parse($("#abt-data").html()),
xkey: 'date',
ykeys: ['ordered', 'paid'],
labels: [gettext('Attendees (ordered)'), gettext('Attendees (paid)')],
lineColors: ['#3b1c4a', '#50a167'],
smooth: false,
resize: true,
fillOpacity: 0.3,
behaveLikeLine: true
});
new Morris.Area({
element: 'rev_chart',
data: JSON.parse($("#rev-data").html()),
xkey: 'date',
ykeys: ['revenue'],
labels: [gettext('Total revenue')],
smooth: false,
resize: true,
lineColors: ['#3b1c4a'],
fillOpacity: 0.3,
preUnits: $.trim($("#currency").html()) + ' '
});
new Morris.Bar({
element: 'obp_chart',
data: JSON.parse($("#obp-data").html()),
xkey: 'item_short',
ykeys: ['ordered', 'paid'],
labels: [gettext('Placed orders'), gettext('Paid orders')],
barColors: ['#3b1c4a', '#50a167'],
hoverCallback: function (index, options, content, row) {
console.log(content);
var $c = $("<div>" + content + "</div>");
var $label = $c.find(".morris-hover-row-label");
$label.text(row.item);
var newc = $label.get(0).outerHTML;
$c.find('.morris-hover-point').each(function (i, r) {
if ($.trim($(r).text().split("\n")[2]) !== "0") {
newc += r.outerHTML;
}
});
return newc;
},
resize: true,
xLabelAngle: 30
});
});
@@ -1,435 +1,435 @@
/* global stripe_pubkey, stripe_loadingmessage, gettext */
'use strict'
/*global $, stripe_pubkey, stripe_loadingmessage, gettext */
'use strict';
var pretixstripe = {
stripe: null,
elements: null,
card: null,
sepa: null,
affirm: null,
klarna: null,
paymentRequest: null,
paymentRequestButton: null,
stripe: null,
elements: null,
card: null,
sepa: null,
affirm: null,
klarna: null,
paymentRequest: null,
paymentRequestButton: null,
pm_request: function (method, element, kwargs = {}) {
waitingDialog.show(gettext('Contacting Stripe …'))
$('.stripe-errors').hide()
'pm_request': function (method, element, kwargs = {}) {
waitingDialog.show(gettext("Contacting Stripe …"));
$(".stripe-errors").hide();
pretixstripe.stripe.createPaymentMethod(method, element, kwargs).then(function (result) {
waitingDialog.hide()
if (result.error) {
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>' + result.error.message + '</div>')
$('.stripe-errors').slideDown()
} else {
let $form = $('#stripe_' + method + '_payment_method_id').closest('form')
// Insert the token into the form so it gets submitted to the server
$('#stripe_' + method + '_payment_method_id').val(result.paymentMethod.id)
if (method === 'card') {
$('#stripe_card_brand').val(result.paymentMethod.card.brand)
$('#stripe_card_last4').val(result.paymentMethod.card.last4)
}
if (method === 'sepa_debit') {
$('#stripe_sepa_debit_last4').val(result.paymentMethod.sepa_debit.last4)
}
// and submit
$form.get(0).submit()
}
}).catch((e) => {
waitingDialog.hide()
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + e + '</div>')
$('.stripe-errors').slideDown()
})
},
load: function () {
if (pretixstripe.stripe !== null) {
return
}
$('.stripe-container').closest('form').find('.checkout-button-row .btn-primary').prop('disabled', true)
$.ajax(
{
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($('#stripe_connectedAccountId').html())) {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), {
stripeAccount: $.trim($('#stripe_connectedAccountId').html()),
locale: $.trim($('body').attr('data-locale'))
})
} else {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), {
locale: $.trim($('body').attr('data-locale'))
})
}
pretixstripe.elements = pretixstripe.stripe.elements()
if ($.trim($('#stripe_merchantcountry').html()) !== '') {
try {
pretixstripe.paymentRequest = pretixstripe.stripe.paymentRequest({
country: $('#stripe_merchantcountry').html(),
currency: $('#stripe_card_currency').val().toLowerCase(),
total: {
label: gettext('Total'),
amount: parseInt($('#stripe_card_total').val())
},
displayItems: [],
requestPayerName: false,
requestPayerEmail: false,
requestPayerPhone: false,
requestShipping: false,
})
pretixstripe.stripe.createPaymentMethod(method, element, kwargs).then(function (result) {
waitingDialog.hide();
if (result.error) {
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>" + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
var $form = $("#stripe_" + method + "_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
$("#stripe_" + method + "_payment_method_id").val(result.paymentMethod.id);
if (method === 'card') {
$("#stripe_card_brand").val(result.paymentMethod.card.brand);
$("#stripe_card_last4").val(result.paymentMethod.card.last4);
}
if (method === 'sepa_debit') {
$("#stripe_sepa_debit_last4").val(result.paymentMethod.sepa_debit.last4);
}
// and submit
$form.get(0).submit();
}
}).catch((e) => {
waitingDialog.hide();
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + e + "</div>");
$(".stripe-errors").slideDown();
});
},
'load': function () {
if (pretixstripe.stripe !== null) {
return;
}
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", true);
$.ajax(
{
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($("#stripe_connectedAccountId").html())) {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
stripeAccount: $.trim($("#stripe_connectedAccountId").html()),
locale: $.trim($("body").attr("data-locale"))
});
} else {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
locale: $.trim($("body").attr("data-locale"))
});
}
pretixstripe.elements = pretixstripe.stripe.elements();
if ($.trim($("#stripe_merchantcountry").html()) !== "") {
try {
pretixstripe.paymentRequest = pretixstripe.stripe.paymentRequest({
country: $("#stripe_merchantcountry").html(),
currency: $("#stripe_card_currency").val().toLowerCase(),
total: {
label: gettext('Total'),
amount: parseInt($("#stripe_card_total").val())
},
displayItems: [],
requestPayerName: false,
requestPayerEmail: false,
requestPayerPhone: false,
requestShipping: false,
});
pretixstripe.paymentRequest.on('paymentmethod', function (ev) {
ev.complete('success')
pretixstripe.paymentRequest.on('paymentmethod', function (ev) {
ev.complete('success');
let $form = $('#stripe_card_payment_method_id').closest('form')
// Insert the token into the form so it gets submitted to the server
$('#stripe_card_payment_method_id').val(ev.paymentMethod.id)
$('#stripe_card_brand').val(ev.paymentMethod.card.brand)
$('#stripe_card_last4').val(ev.paymentMethod.card.last4)
// and submit
$form.get(0).submit()
})
} catch (e) {
pretixstripe.paymentRequest = null
}
} else {
pretixstripe.paymentRequest = null
}
if ($('#stripe-card').length) {
pretixstripe.card = pretixstripe.elements.create('card', {
style: {
base: {
fontFamily: '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
fontSize: '14px',
color: '#555555',
lineHeight: '1.42857',
border: '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
invalid: {
color: 'red',
},
},
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
})
pretixstripe.card.mount('#stripe-card')
pretixstripe.card.on('ready', function () {
$('.stripe-container').closest('form').find('.checkout-button-row .btn-primary').prop('disabled', false)
})
}
if ($('#stripe-sepa').length) {
pretixstripe.sepa = pretixstripe.elements.create('iban', {
style: {
base: {
fontFamily: '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
fontSize: '14px',
color: '#555555',
lineHeight: '1.42857',
border: '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
invalid: {
color: 'red',
},
},
supportedCountries: ['SEPA'],
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
})
pretixstripe.sepa.on('change', function (event) {
// List of IBAN-countries, that require the country as well as line1-property according to
// https://stripe.com/docs/payments/sepa-debit/accept-a-payment?platform=web&ui=element#web-submit-payment
if (['AD', 'PF', 'TF', 'GI', 'GB', 'GG', 'VA', 'IM', 'JE', 'MC', 'NC', 'BL', 'PM', 'SM', 'CH', 'WF'].indexOf(event.country) > 0) {
$('#stripe_sepa_debit_country').prop('checked', true)
$('#stripe_sepa_debit_country').change()
} else {
$('#stripe_sepa_debit_country').prop('checked', false)
$('#stripe_sepa_debit_country').change()
}
if (event.bankName) {
$('#stripe_sepa_debit_bank').val(event.bankName)
}
})
pretixstripe.sepa.mount('#stripe-sepa')
pretixstripe.sepa.on('ready', function () {
$('.stripe-container').closest('form').find('.checkout-button-row .btn-primary').prop('disabled', false)
})
}
if ($('#stripe-affirm').length) {
pretixstripe.affirm = pretixstripe.elements.create('affirmMessage', {
amount: parseInt($('#stripe_affirm_total').val()),
currency: $('#stripe_affirm_currency').val(),
})
var $form = $("#stripe_card_payment_method_id").closest("form");
// Insert the token into the form so it gets submitted to the server
$("#stripe_card_payment_method_id").val(ev.paymentMethod.id);
$("#stripe_card_brand").val(ev.paymentMethod.card.brand);
$("#stripe_card_last4").val(ev.paymentMethod.card.last4);
// and submit
$form.get(0).submit();
});
} catch (e) {
pretixstripe.paymentRequest = null;
}
} else {
pretixstripe.paymentRequest = null;
}
if ($("#stripe-card").length) {
pretixstripe.card = pretixstripe.elements.create('card', {
'style': {
'base': {
'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
'fontSize': '14px',
'color': '#555555',
'lineHeight': '1.42857',
'border': '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
'invalid': {
'color': 'red',
},
},
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
});
pretixstripe.card.mount("#stripe-card");
pretixstripe.card.on('ready', function () {
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
});
}
if ($("#stripe-sepa").length) {
pretixstripe.sepa = pretixstripe.elements.create('iban', {
'style': {
'base': {
'fontFamily': '"Open Sans","OpenSans","Helvetica Neue",Helvetica,Arial,sans-serif',
'fontSize': '14px',
'color': '#555555',
'lineHeight': '1.42857',
'border': '1px solid #ccc',
'::placeholder': {
color: 'rgba(0,0,0,0.4)',
},
},
'invalid': {
'color': 'red',
},
},
supportedCountries: ['SEPA'],
classes: {
focus: 'is-focused',
invalid: 'has-error',
}
});
pretixstripe.sepa.on('change', function (event) {
// List of IBAN-countries, that require the country as well as line1-property according to
// https://stripe.com/docs/payments/sepa-debit/accept-a-payment?platform=web&ui=element#web-submit-payment
if (['AD', 'PF', 'TF', 'GI', 'GB', 'GG', 'VA', 'IM', 'JE', 'MC', 'NC', 'BL', 'PM', 'SM', 'CH', 'WF'].indexOf(event.country) > 0) {
$("#stripe_sepa_debit_country").prop('checked', true);
$("#stripe_sepa_debit_country").change();
} else {
$("#stripe_sepa_debit_country").prop('checked', false);
$("#stripe_sepa_debit_country").change();
}
if (event.bankName) {
$("#stripe_sepa_debit_bank").val(event.bankName);
}
});
pretixstripe.sepa.mount("#stripe-sepa");
pretixstripe.sepa.on('ready', function () {
$('.stripe-container').closest("form").find(".checkout-button-row .btn-primary").prop("disabled", false);
});
}
if ($("#stripe-affirm").length) {
pretixstripe.affirm = pretixstripe.elements.create('affirmMessage', {
'amount': parseInt($("#stripe_affirm_total").val()),
'currency': $("#stripe_affirm_currency").val(),
});
pretixstripe.affirm.mount('#stripe-affirm')
}
if ($('#stripe-klarna').length) {
try {
pretixstripe.klarna = pretixstripe.elements.create('paymentMethodMessaging', {
amount: parseInt($('#stripe_klarna_total').val()),
currency: $('#stripe_klarna_currency').val(),
countryCode: $('#stripe_klarna_country').val(),
paymentMethodTypes: ['klarna'],
})
pretixstripe.affirm.mount('#stripe-affirm');
}
if ($("#stripe-klarna").length) {
try {
pretixstripe.klarna = pretixstripe.elements.create('paymentMethodMessaging', {
'amount': parseInt($("#stripe_klarna_total").val()),
'currency': $("#stripe_klarna_currency").val(),
'countryCode': $("#stripe_klarna_country").val(),
'paymentMethodTypes': ['klarna'],
});
pretixstripe.klarna.mount('#stripe-klarna')
} catch (e) {
console.error(e)
$('#stripe-klarna').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + e + '</div>')
}
}
if ($('#stripe-payment-request-button').length && pretixstripe.paymentRequest != null) {
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
paymentRequest: pretixstripe.paymentRequest,
})
pretixstripe.klarna.mount('#stripe-klarna');
} catch (e) {
console.error(e);
$("#stripe-klarna").html("<div class='alert alert-danger'>Technical error, please contact support: " + e + "</div>");
}
}
if ($("#stripe-payment-request-button").length && pretixstripe.paymentRequest != null) {
pretixstripe.paymentRequestButton = pretixstripe.elements.create('paymentRequestButton', {
paymentRequest: pretixstripe.paymentRequest,
});
pretixstripe.paymentRequest.canMakePayment().then(function (result) {
if (result) {
pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button')
$('#stripe-card-elements .stripe-or').removeClass('hidden')
$('#stripe-payment-request-button').parent().removeClass('hidden')
} else {
$('#stripe-payment-request-button').hide()
document.getElementById('stripe-payment-request-button').style.display = 'none'
}
})
}
}
}
)
},
withStripe: function (callback) {
$.ajax({
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($('#stripe_connectedAccountId').html())) {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), {
stripeAccount: $.trim($('#stripe_connectedAccountId').html()),
locale: $.trim($('body').attr('data-locale'))
})
} else {
pretixstripe.stripe = Stripe($.trim($('#stripe_pubkey').html()), {
locale: $.trim($('body').attr('data-locale'))
})
}
callback()
}
})
},
handleAlipayAction: function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmAlipayPayment(
payment_intent_client_secret,
{
return_url: window.location.href
}
).then(function (result) {
if (result.error) {
waitingDialog.hide()
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + result.error.message + '</div>')
$('.stripe-errors').slideDown()
} else {
waitingDialog.show(gettext('Confirming your payment …'))
}
})
})
},
handleWechatAction: function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmWechatPayPayment(
payment_intent_client_secret,
{
payment_method_options: {
wechat_pay: {
client: 'web',
},
},
}
).then(function (result) {
if (result.error) {
waitingDialog.hide()
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + result.error.message + '</div>')
$('.stripe-errors').slideDown()
} else {
waitingDialog.show(gettext('Confirming your payment …'))
location.reload()
}
})
})
},
handleCardAction: function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.handleCardAction(
payment_intent_client_secret
).then(function (result) {
if (result.error) {
waitingDialog.hide()
$('.stripe-errors').stop().hide().removeClass('sr-only')
$('.stripe-errors').html('<div class=\'alert alert-danger\'>Technical error, please contact support: ' + result.error.message + '</div>')
$('.stripe-errors').slideDown()
} else {
waitingDialog.show(gettext('Confirming your payment …'))
location.reload()
}
})
})
},
handlePaymentRedirectAction: function (payment_intent_next_action_redirect_url) {
waitingDialog.show(gettext('Contacting your bank …'))
pretixstripe.paymentRequest.canMakePayment().then(function (result) {
if (result) {
pretixstripe.paymentRequestButton.mount('#stripe-payment-request-button');
$('#stripe-card-elements .stripe-or').removeClass("hidden");
$('#stripe-payment-request-button').parent().removeClass("hidden");
} else {
$('#stripe-payment-request-button').hide();
document.getElementById('stripe-payment-request-button').style.display = 'none';
}
});
}
}
}
);
},
'withStripe': function (callback) {
$.ajax({
url: 'https://js.stripe.com/v3/',
dataType: 'script',
success: function () {
if ($.trim($("#stripe_connectedAccountId").html())) {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
stripeAccount: $.trim($("#stripe_connectedAccountId").html()),
locale: $.trim($("body").attr("data-locale"))
});
} else {
pretixstripe.stripe = Stripe($.trim($("#stripe_pubkey").html()), {
locale: $.trim($("body").attr("data-locale"))
});
}
callback();
}
});
},
'handleAlipayAction': function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmAlipayPayment(
payment_intent_client_secret,
{
return_url: window.location.href
}
).then(function (result) {
if (result.error) {
waitingDialog.hide();
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
waitingDialog.show(gettext("Confirming your payment …"));
}
});
});
},
'handleWechatAction': function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.confirmWechatPayPayment(
payment_intent_client_secret,
{
payment_method_options: {
wechat_pay: {
client: 'web',
},
},
}
).then(function (result) {
if (result.error) {
waitingDialog.hide();
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
waitingDialog.show(gettext("Confirming your payment …"));
location.reload();
}
});
});
},
'handleCardAction': function (payment_intent_client_secret) {
pretixstripe.withStripe(function () {
pretixstripe.stripe.handleCardAction(
payment_intent_client_secret
).then(function (result) {
if (result.error) {
waitingDialog.hide();
$(".stripe-errors").stop().hide().removeClass("sr-only");
$(".stripe-errors").html("<div class='alert alert-danger'>Technical error, please contact support: " + result.error.message + "</div>");
$(".stripe-errors").slideDown();
} else {
waitingDialog.show(gettext("Confirming your payment …"));
location.reload();
}
});
});
},
'handlePaymentRedirectAction': function (payment_intent_next_action_redirect_url) {
waitingDialog.show(gettext("Contacting your bank …"));
let payment_intent_redirect_action_handling = $.trim($('#stripe_payment_intent_redirect_action_handling').html())
if (payment_intent_redirect_action_handling === 'iframe') {
let iframe = document.createElement('iframe')
iframe.src = payment_intent_next_action_redirect_url
iframe.className = 'embed-responsive-item'
$('#scacontainer').append(iframe)
$('#scacontainer iframe').on('load', function () {
waitingDialog.hide()
})
} else if (payment_intent_redirect_action_handling === 'redirect') {
window.location.href = payment_intent_next_action_redirect_url
}
}
}
let payment_intent_redirect_action_handling = $.trim($("#stripe_payment_intent_redirect_action_handling").html());
if (payment_intent_redirect_action_handling === 'iframe') {
let iframe = document.createElement('iframe');
iframe.src = payment_intent_next_action_redirect_url;
iframe.className = 'embed-responsive-item';
$('#scacontainer').append(iframe);
$('#scacontainer iframe').on("load", function () {
waitingDialog.hide();
});
} else if (payment_intent_redirect_action_handling === 'redirect') {
window.location.href = payment_intent_next_action_redirect_url;
}
}
};
$(function () {
if ($('#stripe_payment_intent_SCA_status').length) {
let payment_intent_redirect_action_handling = $.trim($('#stripe_payment_intent_redirect_action_handling').html())
let order_status = $.trim($('#order_status').html())
let order_url = $.trim($('#order_url').html())
if ($("#stripe_payment_intent_SCA_status").length) {
let payment_intent_redirect_action_handling = $.trim($("#stripe_payment_intent_redirect_action_handling").html());
let order_status = $.trim($("#order_status").html());
let order_url = $.trim($("#order_url").html())
if (payment_intent_redirect_action_handling === 'iframe') {
window.parent.postMessage('3DS-authentication-complete.' + order_status, '*')
return
} else if (payment_intent_redirect_action_handling === 'redirect') {
waitingDialog.show(gettext('Confirming your payment …'))
if (payment_intent_redirect_action_handling === 'iframe') {
window.parent.postMessage('3DS-authentication-complete.' + order_status, '*');
return;
} else if (payment_intent_redirect_action_handling === 'redirect') {
waitingDialog.show(gettext("Confirming your payment …"));
if (order_status === 'p') {
window.location.href = order_url + '?paid=yes'
} else {
window.location.href = order_url
}
}
} else if ($('#stripe_payment_intent_next_action_redirect_url').length) {
let payment_intent_next_action_redirect_url = JSON.parse($('#stripe_payment_intent_next_action_redirect_url').html())
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url)
} else if ($.trim($('#stripe_payment_intent_action_type').html()) === 'promptpay_display_qr_code') {
waitingDialog.hide()
} else if ($.trim($('#stripe_payment_intent_action_type').html()) === 'wechat_pay_display_qr_code') {
let payment_intent_client_secret = $.trim($('#stripe_payment_intent_client_secret').html())
pretixstripe.handleWechatAction(payment_intent_client_secret)
} else if ($.trim($('#stripe_payment_intent_action_type').html()) === 'alipay_handle_redirect') {
let payment_intent_client_secret = $.trim($('#stripe_payment_intent_client_secret').html())
pretixstripe.handleAlipayAction(payment_intent_client_secret)
} else if ($('#stripe_payment_intent_client_secret').length) {
let payment_intent_client_secret = $.trim($('#stripe_payment_intent_client_secret').html())
pretixstripe.handleCardAction(payment_intent_client_secret)
}
if (order_status === 'p') {
window.location.href = order_url + '?paid=yes';
} else {
window.location.href = order_url;
}
}
} else if ($("#stripe_payment_intent_next_action_redirect_url").length) {
let payment_intent_next_action_redirect_url = JSON.parse($("#stripe_payment_intent_next_action_redirect_url").html());
pretixstripe.handlePaymentRedirectAction(payment_intent_next_action_redirect_url);
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "promptpay_display_qr_code") {
waitingDialog.hide();
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "wechat_pay_display_qr_code") {
let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleWechatAction(payment_intent_client_secret);
} else if ($.trim($("#stripe_payment_intent_action_type").html()) === "alipay_handle_redirect") {
let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleAlipayAction(payment_intent_client_secret);
} else if ($("#stripe_payment_intent_client_secret").length) {
let payment_intent_client_secret = $.trim($("#stripe_payment_intent_client_secret").html());
pretixstripe.handleCardAction(payment_intent_client_secret);
}
$(window).on('message onmessage', function (e) {
if (typeof e.originalEvent.data === 'string' && e.originalEvent.data.startsWith('3DS-authentication-complete.')) {
waitingDialog.show(gettext('Confirming your payment …'))
$('#scacontainer').hide()
$('#continuebutton').removeClass('hidden')
$(window).on("message onmessage", function (e) {
if (typeof e.originalEvent.data === "string" && e.originalEvent.data.startsWith('3DS-authentication-complete.')) {
waitingDialog.show(gettext("Confirming your payment …"));
$('#scacontainer').hide();
$('#continuebutton').removeClass('hidden');
if (e.originalEvent.data.split('.')[1] == 'p') {
window.location.href = $('#continuebutton').attr('href') + '?paid=yes'
} else {
window.location.href = $('#continuebutton').attr('href')
}
}
})
if (e.originalEvent.data.split('.')[1] == 'p') {
window.location.href = $('#continuebutton').attr('href') + '?paid=yes';
} else {
window.location.href = $('#continuebutton').attr('href');
}
}
});
if (!$('.stripe-container').length)
return
if (!$(".stripe-container").length)
return;
if (
$('input[name=payment][value=stripe]').is(':checked')
|| $('input[name=payment][value=stripe_sepa_debit]').is(':checked')
|| $('input[name=payment][value=stripe_affirm]').is(':checked')
|| $('input[name=payment][value=stripe_klarna]').is(':checked')
|| $('.payment-redo-form').length) {
pretixstripe.load()
} else {
$('input[name=payment]').change(function () {
if (['stripe', 'stripe_sepa_debit', 'stripe_affirm', 'stripe_klarna'].indexOf($(this).val()) > -1) {
pretixstripe.load()
}
})
}
if (
$("input[name=payment][value=stripe]").is(':checked')
|| $("input[name=payment][value=stripe_sepa_debit]").is(':checked')
|| $("input[name=payment][value=stripe_affirm]").is(':checked')
|| $("input[name=payment][value=stripe_klarna]").is(':checked')
|| $(".payment-redo-form").length) {
pretixstripe.load();
} else {
$("input[name=payment]").change(function () {
if (['stripe', 'stripe_sepa_debit', 'stripe_affirm', 'stripe_klarna'].indexOf($(this).val()) > -1) {
pretixstripe.load();
}
})
}
$('#stripe_other_card').click(
function (e) {
$('#stripe_card_payment_method_id').val('')
$('#stripe-current-card').slideUp()
$('#stripe-card-elements').slideDown()
$("#stripe_other_card").click(
function (e) {
$("#stripe_card_payment_method_id").val("");
$("#stripe-current-card").slideUp();
$("#stripe-card-elements").slideDown();
e.preventDefault()
return false
}
)
e.preventDefault();
return false;
}
);
if ($('#stripe-current-card').length) {
$('#stripe-card-elements').hide()
}
if ($("#stripe-current-card").length) {
$("#stripe-card-elements").hide();
}
$('#stripe_other_account').click(
function (e) {
$('#stripe_sepa_debit_payment_method_id').val('')
$('#stripe-current-account').slideUp()
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').slideDown()
$("#stripe_other_account").click(
function (e) {
$("#stripe_sepa_debit_payment_method_id").val("");
$("#stripe-current-account").slideUp();
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').slideDown();
e.preventDefault()
return false
}
)
e.preventDefault();
return false;
}
);
if ($('#stripe-current-account').length) {
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').hide()
}
if ($("#stripe-current-account").length) {
// We're using a css-selector here instead of the id-selector,
// as we're hiding Stripe Elements *and* Django form fields
$('.stripe-sepa_debit-form').hide();
}
$('.stripe-container').closest('form').submit(
function () {
if ($('input[name=card_new]').length && !$('input[name=card_new]').prop('checked')) {
return null
}
if (($('input[name=payment][value=stripe]').prop('checked') || $('input[name=payment][type=radio]').length === 0)
&& $('#stripe_card_payment_method_id').val() == '') {
pretixstripe.pm_request('card', pretixstripe.card)
return false
}
$('.stripe-container').closest("form").submit(
function () {
if ($("input[name=card_new]").length && !$("input[name=card_new]").prop('checked')) {
return null;
}
if (($("input[name=payment][value=stripe]").prop('checked') || $("input[name=payment][type=radio]").length === 0)
&& $("#stripe_card_payment_method_id").val() == "") {
pretixstripe.pm_request('card', pretixstripe.card);
return false;
}
if (($('input[name=payment][value=stripe_sepa_debit]').prop('checked')) && $('#stripe_sepa_debit_payment_method_id').val() == '') {
pretixstripe.pm_request('sepa_debit', pretixstripe.sepa, {
billing_details: {
name: $('#id_payment_stripe_sepa_debit-accountname').val(),
email: $('#stripe_sepa_debit_email').val(),
address: {
line1: $('#id_payment_stripe_sepa_debit-line1').val(),
postal_code: $('#id_payment_stripe_sepa_debit-postal_code').val(),
city: $('#id_payment_stripe_sepa_debit-city').val(),
country: $('#id_payment_stripe_sepa_debit-country').val(),
}
}
})
return false
}
}
)
})
if (($("input[name=payment][value=stripe_sepa_debit]").prop('checked')) && $("#stripe_sepa_debit_payment_method_id").val() == "") {
pretixstripe.pm_request('sepa_debit', pretixstripe.sepa, {
billing_details: {
name: $("#id_payment_stripe_sepa_debit-accountname").val(),
email: $("#stripe_sepa_debit_email").val(),
address: {
line1: $("#id_payment_stripe_sepa_debit-line1").val(),
postal_code: $("#id_payment_stripe_sepa_debit-postal_code").val(),
city: $("#id_payment_stripe_sepa_debit-city").val(),
country: $("#id_payment_stripe_sepa_debit-country").val(),
}
}
});
return false;
}
}
);
});
+3 -54
View File
@@ -35,7 +35,6 @@ import copy
import inspect
import uuid
from collections import defaultdict
from datetime import time
from decimal import Decimal
from django import forms
@@ -53,7 +52,6 @@ from django.shortcuts import redirect
from django.utils import translation
from django.utils.functional import cached_property
from django.utils.html import conditional_escape
from django.utils.timezone import now
from django.utils.translation import (
get_language, gettext_lazy as _, pgettext_lazy,
)
@@ -73,7 +71,6 @@ from pretix.base.services.cart import (
from pretix.base.services.cross_selling import CrossSellingService
from pretix.base.services.memberships import validate_memberships_in_order
from pretix.base.services.orders import perform_order
from pretix.base.services.payment import compute_payment_deadline
from pretix.base.services.pricing import get_price
from pretix.base.services.tasks import EventTask
from pretix.base.settings import PERSON_NAME_SCHEMES
@@ -873,28 +870,6 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
'attendee_name_parts': d
})
wd = self.cart_session.get('widget_data', {})
if wd.get('attendee-fix', '') == 'true':
for k, v in wd.items():
if v and k.startswith('attendee-name'):
o.append({
'attendee_name_parts': {
'disabled': True,
}
})
elif v and k.startswith('email'):
o.append({
'attendee_email': {
'disabled': True,
}
})
elif v and k.startswith('question-'):
o.append({
k[9:].upper(): {
'disabled': True,
}
})
return o
@cached_property
@@ -1369,11 +1344,6 @@ class PaymentStep(CartMixin, TemplateFlowStep):
self.request = request
self.request.pci_dss_payment_page = True
if "postpone" in request.POST and self._allow_postpone:
self.cart_session['payments_postpone'] = True
self.cart_session['payments'] = []
return redirect_to_url(self.get_next_url(request))
if "remove_payment" in request.POST:
self._remove_payment(request.POST["remove_payment"])
return redirect_to_url(self.get_step_url(request))
@@ -1462,41 +1432,20 @@ class PaymentStep(CartMixin, TemplateFlowStep):
ctx['providers'] = self.provider_forms
ctx['show_fees'] = any(p['fee'] for p in self.provider_forms)
if 'payment' in self.request.POST:
ctx['selected'] = self.request.POST['payment']
elif self.cart_session.get('payments_postpone') and self._allow_postpone:
ctx['selected'] = ''
elif len(self.provider_forms) == 1:
if len(self.provider_forms) == 1:
ctx['selected'] = self.provider_forms[0]['provider'].identifier
elif 'payment' in self.request.POST:
ctx['selected'] = self.request.POST['payment']
elif self.single_use_payment:
ctx['selected'] = self.single_use_payment['provider']
else:
ctx['selected'] = ''
ctx['allow_postpone'] = self._allow_postpone
if self._allow_postpone:
now_dt = now()
ctx['payment_deadline'] = compute_payment_deadline(
event=self.request.event,
sales_channel=self.request.sales_channel,
subevents={p.subevent for p in ctx['cart']['raw']},
now_dt=now_dt,
)
if ctx['payment_deadline'].time() != time(hour=23, minute=59, second=59):
ctx['payment_deadline_minutes'] = int((ctx['payment_deadline'] - now_dt).total_seconds() // 60)
return ctx
@cached_property
def _allow_postpone(self):
return self.request.sales_channel.identifier in self.request.event.settings.payment_choice_postpone_allowed_channels
def _is_allowed(self, prov, request):
return prov.is_allowed(request, total=self._total_order_value)
def is_completed(self, request, warn=False):
if self.cart_session.get('payments_postpone') and self._allow_postpone:
return True
if not self.cart_session.get('payments'):
if warn:
messages.error(request, _('Please select a payment method to proceed.'))
+32 -124
View File
@@ -21,8 +21,7 @@
#
import sys
from datetime import datetime
from decimal import Decimal
from typing import List, Optional, Union
from typing import Optional
from django.conf import settings
from django.db.models import (
@@ -30,13 +29,11 @@ from django.db.models import (
)
from django.db.models.lookups import Exact
from pretix.base.decimal import round_decimal
from pretix.base.models import (
Discount, Event, Item, ItemVariation, Quota, SalesChannel,
SeatCategoryMapping, SubEvent, Voucher,
ItemVariation, Quota, SalesChannel, SeatCategoryMapping,
)
from pretix.base.models.items import (
ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
Item, ItemAddOn, ItemBundle, SubEventItem, SubEventItemVariation,
)
from pretix.base.services.quotas import QuotaAvailability
from pretix.base.timemachine import time_machine_now
@@ -57,39 +54,10 @@ def item_group_by_category(items):
)
def _single_item_discounts(event: Event, sales_channel: Union[str, SalesChannel],
subevent: SubEvent=None, voucher: Voucher=None, is_addons=False) -> List[Discount]:
discount_qs = event.discounts.filter(
Q(available_from__isnull=True) | Q(available_from__lte=time_machine_now()),
Q(available_until__isnull=True) | Q(available_until__gte=time_machine_now()),
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=sales_channel),
active=True,
# Only discounts that can be applied before we know the full cart
benefit_same_products=True,
benefit_only_apply_to_cheapest_n_matches__isnull=True,
condition_min_value=Decimal("0.00"),
condition_min_count=1,
).prefetch_related('condition_limit_products').order_by('position', 'pk')
if subevent:
discount_qs = discount_qs.filter(
Q(subevent_date_from__isnull=True) | Q(subevent_date_from__lte=subevent.date_from),
Q(subevent_date_until__isnull=True) | Q(subevent_date_until__gte=subevent.date_from),
)
if is_addons:
discount_qs = discount_qs.filter(condition_apply_to_addons=True, benefit_apply_to_addons=True)
if voucher and voucher.price_mode != "none":
discount_qs = discount_qs.filter(condition_ignore_voucher_discounted=False, benefit_ignore_voucher_discounted=False)
return list(discount_qs)
def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, voucher=None, require_seat=0, base_qs=None,
allow_addons=False, allow_cross_sell=False,
quota_cache=None, filter_items=None, filter_categories=None, memberships=None,
ignore_hide_sold_out_for_item_ids=None, _discount_cache=None):
ignore_hide_sold_out_for_item_ids=None):
base_qs_set = base_qs is not None
base_qs = base_qs if base_qs is not None else event.items
@@ -222,23 +190,6 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
if filter_categories:
items = items.filter(category_id__in=[a for a in filter_categories if a.isdigit()])
# We pre-computate discounts that do not rely on specific combinations in the cart.
# This is not the same order of operations that is applied in the cart, so there could be some differences
# when it comes to tax rate handling, but we don't have that information in the product list anyway, so
# that is acceptable.
cache_key = (event, channel, voucher, subevent, allow_addons)
if _discount_cache is None:
_discount_cache = {}
if cache_key not in _discount_cache:
_discount_cache[cache_key] = list(_single_item_discounts(
event=event,
sales_channel=channel,
voucher=voucher,
subevent=subevent,
is_addons=allow_addons,
))
discounts = _discount_cache[cache_key]
display_add_to_cart = False
quota_cache_key = f'item_quota_cache:{subevent.id if subevent else 0}:{channel.identifier}:{bool(require_seat)}'
quota_cache = quota_cache or event.cache.get(quota_cache_key) or {}
@@ -321,10 +272,6 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
if resp:
item.description += ("<br/>" if item.description else "") + resp
matching_discounts = [
d for d in discounts if d.condition_all_products or item in d.condition_limit_products.all()
]
if not item.has_variations:
item._remove = False
if not bool(item._subevent_quotas):
@@ -352,51 +299,30 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
max_per_order
)
configured_price = item_price_override.get(item.pk, item.default_price)
original_price = item_price_override.get(item.pk, item.default_price)
voucher_reduced = False
if voucher:
price = voucher.calculate_price(configured_price)
voucher_reduced = price < configured_price
price = voucher.calculate_price(original_price)
voucher_reduced = price < original_price
include_bundled = not voucher.all_bundles_included
else:
price = configured_price
price = original_price
include_bundled = True
if matching_discounts and not item.free_price:
# Discounts and free prices are a non-recommended combination that behaves unintuitively, so we can
# accept it not being handled here.
discount = matching_discounts[0] # First matching discount rule always wins
# First handle taxes because discount is always computed on gross
taxed_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
price_without_bundles = taxed_price.gross - sum(b.count * b.designated_price for b in item.bundles.all())
discounted_price = round_decimal(
taxed_price.gross - price_without_bundles * discount.benefit_discount_matching_percent / Decimal('100.00'),
event.currency,
)
item.display_price = item.tax(discounted_price, currency=event.currency, include_bundled=include_bundled,
base_price_is='gross')
else:
discount = None
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
item.display_price = item.tax(price, currency=event.currency, include_bundled=include_bundled)
if item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
item.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency, include_bundled=include_bundled)
else:
item.suggested_price = item.display_price
if voucher_reduced or (discount and not item.original_price):
# If a voucher is used, we use the non-voucher price as the "original price", even if a different
# original price is set, to highlight the voucher's impact. We also use the configured price as
# original price if a discount is applied and no explicit original price is set
item.original_price = item.tax(configured_price, currency=event.currency, include_bundled=True)
elif item.original_price:
item.original_price = item.tax(
item.original_price, currency=event.currency, include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross' # backwards-compat
)
if price != original_price:
item.original_price = item.tax(original_price, currency=event.currency, include_bundled=True)
else:
item.original_price = None
item.original_price = (
item.tax(item.original_price, currency=event.currency, include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
if item.original_price else None
)
if not display_add_to_cart:
display_add_to_cart = not item.requires_seat and item.order_max > 0
else:
@@ -426,53 +352,35 @@ def prepare_item_list_for_shop(event, *, channel: SalesChannel, subevent=None, v
max_per_order
)
configured_price = var_price_override.get(var.pk, var.price)
original_price = var_price_override.get(var.pk, var.price)
voucher_reduced = False
if voucher:
price = voucher.calculate_price(configured_price)
voucher_reduced = price < configured_price
price = voucher.calculate_price(original_price)
voucher_reduced = price < original_price
include_bundled = not voucher.all_bundles_included
else:
price = configured_price
price = original_price
include_bundled = True
if matching_discounts and not item.free_price:
# Discounts and free prices are a non-recommended combination that behaves unintuitively, so we can
# accept it not being handled here.
discount = matching_discounts[0] # First matching discount rule always wins
# First handle taxes because discount is always computed on gross
taxed_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
price_without_bundles = taxed_price.gross - sum(b.count * b.designated_price for b in item.bundles.all())
discounted_price = round_decimal(
taxed_price.gross - price_without_bundles * discount.benefit_discount_matching_percent / Decimal('100.00'),
event.currency,
)
var.display_price = var.tax(discounted_price, currency=event.currency,
include_bundled=include_bundled,
base_price_is='gross')
else:
discount = None
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
var.display_price = var.tax(price, currency=event.currency, include_bundled=include_bundled)
if item.free_price and var.free_price_suggestion is not None and not voucher_reduced:
var.suggested_price = var.tax(max(price, var.free_price_suggestion), currency=event.currency,
include_bundled=include_bundled)
var.suggested_price = item.tax(max(price, var.free_price_suggestion), currency=event.currency,
include_bundled=include_bundled)
elif item.free_price and item.free_price_suggestion is not None and not voucher_reduced:
var.suggested_price = var.tax(max(price, item.free_price_suggestion), currency=event.currency,
include_bundled=include_bundled)
var.suggested_price = item.tax(max(price, item.free_price_suggestion), currency=event.currency,
include_bundled=include_bundled)
else:
var.suggested_price = var.display_price
if voucher_reduced or (discount and not item.original_price and not var.original_price):
var.original_price = var.tax(configured_price, currency=event.currency, include_bundled=True)
elif item.original_price or var.original_price:
var.original_price = var.tax(
var.original_price or item.original_price, currency=event.currency,
include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross' # backwards-compat
)
if price != original_price:
var.original_price = var.tax(original_price, currency=event.currency, include_bundled=True)
else:
var.original_price = None
var.original_price = (
var.tax(var.original_price or item.original_price, currency=event.currency,
include_bundled=True,
base_price_is='net' if event.settings.display_net_prices else 'gross') # backwards-compat
) if var.original_price or item.original_price else None
var.current_unavailability_reason = _get_variant_unavailability_reason(var, has_voucher=voucher, subevent=subevent)
@@ -128,35 +128,6 @@
{% endif %}
</div>
{% endif %}
{% if allow_postpone %}
<div class="panel panel-default">
<div class="panel-body row">
<div class="col-md-9 col-xs-12">
{% trans "Not sure yet? You can complete your order first and then select a payment method later." %}
<br>
<span class="text-muted">
{% if current_payments %}
{% trans "To do so, please first remove the payment methods you already selected above." %}
{% elif payment_deadline_minutes %}
{% blocktrans trimmed with minutes=payment_deadline_minutes %}
Your payment needs to be completed within {{ minutes }} minutes.
{% endblocktrans %}
{% else %}
{% blocktrans trimmed with deadline=payment_deadline|date:"SHORT_DATE_FORMAT" %}
Your payment needs to be completed by {{ deadline }}.
{% endblocktrans %}
{% endif %}
</span>
</div>
<div class="col-md-3 col-xs-12 text-right flip">
<button name="postpone" value="on" class="btn btn-primary"
{% if current_payments %}disabled{% endif %}>
{% trans "Proceed without selection" %}
</button>
</div>
</div>
</div>
{% endif %}
<div class="row checkout-button-row">
<div class="col-md-4 col-sm-6">
<a class="btn btn-block btn-default btn-lg"
@@ -23,11 +23,7 @@
{% endblocktrans %} ::
{% endif %}
{% elif subevent %}
{% if subevent.name|upper != event.name|upper %}
{# The |upper is a trick to force LazyI18nString→str conversion before comparison #}
{{ subevent.name }} ::
{% endif %}
{{ subevent.get_date_range_display_with_times }} ::
{{ subevent.get_date_range_display }} ::
{% endif %}
{% endblock %}
-6
View File
@@ -54,7 +54,6 @@ from pretix.base.models import Customer, InvoiceAddress, Order, OrderPosition
from pretix.base.services.mail import mail
from pretix.base.settings import PERSON_NAME_SCHEMES
from pretix.base.signals import customer_created, customer_signed_in
from pretix.helpers import OF_SELF
from pretix.helpers.compat import CompatDeleteView
from pretix.helpers.http import redirect_to_url
from pretix.multidomain.models import KnownDomain
@@ -281,11 +280,6 @@ class SetPasswordView(FormView):
def form_valid(self, form):
with transaction.atomic():
# Re-check token in transaction to prevent race condition
self.customer = Customer.objects.select_for_update(of=OF_SELF).get(pk=self.customer.pk)
if not TokenGenerator().check_token(self.customer, self.request.GET.get('token', '')):
return HttpResponseRedirect(self.get_success_url())
self.customer.set_password(form.cleaned_data['password'])
self.customer.is_verified = True
self.customer.save()
+7 -5
View File
@@ -79,7 +79,7 @@ from pretix.presale.signals import seatingframe_html_head
from pretix.presale.views.organizer import (
EventListMixin, add_subevents_for_days, days_for_template,
filter_qs_by_attr, filter_subevents_with_plugins, has_before_after,
should_hide_subevent, weeks_for_template,
weeks_for_template,
)
from . import (
@@ -443,10 +443,12 @@ class EventIndex(EventViewMixin, EventListMixin, CartMixin, TemplateView):
)
)
subevents = filter_subevents_with_plugins(list(subevents), self.request.sales_channel)
context['subevent_list'] = [
se for se in subevents
if not should_hide_subevent(self.request.event.settings, se, voucher)
]
context['subevent_list'] = subevents
if self.request.event.settings.event_list_available_only and not voucher:
context['subevent_list'] = [
se for se in subevents
if not se.presale_has_ended and (se.best_availability_state is None or se.best_availability_state >= Quota.AVAILABILITY_RESERVED)
]
context['visible_events'] = len(subevents) > 0
return context
+13 -28
View File
@@ -601,32 +601,6 @@ def filter_subevents_with_plugins(subevents, sales_channel=None):
return subevents
def should_hide_subevent(settings, subevent, voucher=None):
hide = False
if settings.event_list_available_only:
hide = (
# Presale is over → the subevent is not available → hide
subevent.presale_has_ended or
# Not a single product is available on this sales channel → hide
# Note that means there could be products which are ignored for calendar availability (Quota.ignore_for_event_availability)
# or products only visible with a voucher. However, for customers with these scenarios, the event_list_available_only
# makes only very little sense as it would never do anything, so the flag can just be removed -- or the products should
# be made visible so people know why there are no products. In case a voucher is already entered on the calendar view,
# this is already respected and subevents are shown correctly.
subevent.best_availability_state is None or
(
# Sold out → hide, unless we have a voucher active that can bypass all quotas
(not voucher or not voucher.allow_ignore_quota) and
subevent.best_availability_state < Quota.AVAILABILITY_RESERVED
)
)
if settings.event_calendar_future_only:
if (subevent.date_to or subevent.date_from) < time_machine_now():
hide = True
return hide
def add_subevents_for_days(qs, before, after, ebd, timezones, sales_channel, event=None, cart_namespace=None,
voucher=None):
qs = qs.filter(active=True, is_public=True).filter(
@@ -666,8 +640,19 @@ def add_subevents_for_days(qs, before, after, ebd, timezones, sales_channel, eve
kwargs['cart_namespace'] = cart_namespace
s = event.settings if event else se.event.settings
if should_hide_subevent(s, se, voucher):
continue
if s.event_list_available_only:
hide = se.presale_has_ended or (
(not voucher or not voucher.allow_ignore_quota) and
se.best_availability_state is not None and
se.best_availability_state < Quota.AVAILABILITY_RESERVED
)
if hide:
continue
if s.event_calendar_future_only:
if (se.date_to or se.date_from) < time_machine_now():
continue
timezones.add(s.timezone)
tz = ZoneInfo(s.timezone)
+9 -5
View File
@@ -75,7 +75,7 @@ from pretix.presale.views.cart import get_or_create_cart_id
from pretix.presale.views.organizer import (
EventListMixin, add_events_for_days, add_subevents_for_days,
days_for_template, filter_qs_by_attr, filter_subevents_with_plugins,
should_hide_subevent, weeks_for_template,
weeks_for_template,
)
logger = logging.getLogger(__name__)
@@ -757,10 +757,14 @@ class WidgetAPIProductList(EventListMixin, View):
evs = evs[:limit]
tz = request.event.timezone
evs = [
se for se in evs
if not should_hide_subevent(self.request.event.settings, se)
]
if self.request.event.settings.event_list_available_only:
evs = [
se for se in evs
if not se.presale_has_ended and (
se.best_availability_state is not None and
se.best_availability_state >= Quota.AVAILABILITY_RESERVED
)
]
data['events'] = [
{
+171 -169
View File
@@ -1,193 +1,195 @@
$(function () {
'use strict'
"use strict";
// Responses are expected to only depend on the GET parameters passed, so we can have a little client-side cache
// to prevent fetching the same thing many times.
let responseCache = {}
// Responses are expected to only depend on the GET parameters passed, so we can have a little client-side cache
// to prevent fetching the same thing many times.
var responseCache = {};
const cleanName = (name) => {
// Remove form prefix
name = name.split('-').pop()
// Remove settings prefix
name = name.replace(/^invoice_address_from_/, '')
return name
}
const cleanName = (name) => {
// Remove form prefix
name = name.split("-").pop();
// Remove settings prefix
name = name.replace(/^invoice_address_from_/, "");
return name
}
$('[data-address-information-url]').each(function () {
let xhr
const form = $(this)
const dependencies = $(this).find('[data-trigger-address-info]')
const loader = $('<span class=\'fa fa-cog fa-spin\'></span>').hide().prependTo(dependencies.closest('.form-group').find('label').first())
const baseUrl = this.getAttribute('data-address-information-url')
const isAnyRequired = dependencies.toArray().some(function (e) { return $(e).closest('.form-group').is('.required') })
$("[data-address-information-url]").each(function () {
let xhr;
const form = $(this);
const dependencies = $(this).find("[data-trigger-address-info]");
const loader = $("<span class='fa fa-cog fa-spin'></span>").hide().prependTo(dependencies.closest(".form-group").find("label").first())
const baseUrl = this.getAttribute('data-address-information-url')
const isAnyRequired = dependencies.toArray().some(function (e) { return $(e).closest(".form-group").is(".required") });
const dependents = {
city: form.find('input[name$=city]'),
zipcode: form.find('input[name$=zipcode]'),
street: form.find('textarea[name$=street]'),
state: form.find('select[name$=state]'),
vat_id: form.find('input[name$=vat_id]'),
}
const dependents = {
'city': form.find("input[name$=city]"),
'zipcode': form.find("input[name$=zipcode]"),
'street': form.find("textarea[name$=street]"),
'state': form.find("select[name$=state]"),
'vat_id': form.find("input[name$=vat_id]"),
};
form.find('select[name*=transmission_], textarea[name*=transmission_], input[name*=transmission_]').each(function () {
dependents[cleanName($(this).attr('name'))] = $(this)
})
form.find("select[name*=transmission_], textarea[name*=transmission_], input[name*=transmission_]").each(function () {
dependents[cleanName($(this).attr("name"))] = $(this)
})
const dependentsDisabled = []
for (let k in dependents) {
if (dependents[k].prop('disabled')) {
dependentsDisabled.push(k)
}
}
const dependentsDisabled = [];
for (var k in dependents) {
if (dependents[k].prop("disabled")) {
dependentsDisabled.push(k);
}
}
if (!Object.values(dependents).some((el) => el.length)) {
// No address fields found, do not create request
return
}
if (!Object.values(dependents).some((el) => el.length)) {
// No address fields found, do not create request
return;
}
const update_form = function (data) {
let selected_state = dependents.state.prop('data-selected-value')
if (selected_state) dependents.state.prop('data-selected-value', '')
dependents.state.find('option:not([value=\'\'])').remove()
$.each(data.data, function (k, s) {
let o = $('<option>').attr('value', s.code).text(s.name)
if (selected_state === s.code) o.prop('selected', true)
dependents.state.append(o)
})
const update_form = function (data) {
var selected_state = dependents.state.prop("data-selected-value");
if (selected_state) dependents.state.prop("data-selected-value", "");
dependents.state.find("option:not([value=''])").remove();
$.each(data.data, function (k, s) {
var o = $("<option>").attr("value", s.code).text(s.name);
if (selected_state === s.code) o.prop("selected", true);
dependents.state.append(o);
});
if (dependents.transmission_type) {
let selected_transmission_type = dependents.transmission_type.prop('data-selected-value')
if (selected_transmission_type) dependents.transmission_type.prop('data-selected-value', '')
dependents.transmission_type.find('option:not([value=\'\']):not([value=\'-\'])').remove()
if (dependents.transmission_type) {
var selected_transmission_type = dependents.transmission_type.prop("data-selected-value");
if (selected_transmission_type) dependents.transmission_type.prop("data-selected-value", "");
dependents.transmission_type.find("option:not([value='']):not([value='-'])").remove();
if (!data.transmission_type.visible) {
selected_transmission_type = 'email'
}
if (!data.transmission_type.visible) {
selected_transmission_type = "email";
}
$.each(data.transmission_types, function (k, s) {
let o = $('<option>').attr('value', s.code).text(s.name)
if (selected_transmission_type === s.code) {
o.prop('selected', true)
}
dependents.transmission_type.append(o)
})
}
$.each(data.transmission_types, function (k, s) {
var o = $("<option>").attr("value", s.code).text(s.name);
if (selected_transmission_type === s.code) {
o.prop("selected", true);
}
dependents.transmission_type.append(o);
});
for (var k in dependents) {
const options = data[k],
dependent = dependents[k]
let visible = 'visible' in options ? options.visible : true
}
if (dependent.is('[data-display-dependency]')) {
const dependency = $(dependent.attr('data-display-dependency'))
visible = visible && (
(dependency.attr('type') === 'checkbox' || dependency.attr('type') === 'radio') ? dependency.prop('checked') : !!dependency.val()
)
}
for (var k in dependents) {
const options = data[k],
dependent = dependents[k];
let visible = 'visible' in options ? options.visible : true;
if ('label' in options) {
dependent.closest('.form-group').find('.control-label').text(options.label)
}
if ('helptext_visible' in options) {
dependent.closest('.form-group').find('.help-block').toggle(options.helptext_visible)
}
if (dependent.is("[data-display-dependency]")) {
const dependency = $(dependent.attr("data-display-dependency"));
visible = visible && (
(dependency.attr("type") === 'checkbox' || dependency.attr("type") === 'radio') ? dependency.prop('checked') : !!dependency.val()
);
}
const required = 'required' in options && visible && (
(options.required === 'if_any' && isAnyRequired)
|| (options.required === true)
)
dependent.closest('.form-group').toggle(visible).toggleClass('required', required)
dependent.prop('required', required)
if ('label' in options) {
dependent.closest(".form-group").find(".control-label").text(options.label);
}
if ('helptext_visible' in options) {
dependent.closest(".form-group").find(".help-block").toggle(options.helptext_visible);
}
const label = dependent.closest('.form-group').find('label')
const labelRequired = label.find('.label-required')
if (!required) {
labelRequired.remove()
} else if (!labelRequired.length) {
label.append('<i class="label-required">' + gettext('required') + '</i>')
}
}
for (var k in dependents) dependents[k].prop('disabled', dependentsDisabled.includes(k))
loader.hide()
}
const required = 'required' in options && visible && (
(options.required === 'if_any' && isAnyRequired) ||
(options.required === true)
);
dependent.closest(".form-group").toggle(visible).toggleClass('required', required);
dependent.prop("required", required);
const update = function (ev) {
dependents.state.prop('data-selected-value', dependents.state.val())
if (dependents.transmission_type) {
dependents.transmission_type.prop('data-selected-value', dependents.transmission_type.val())
}
const label = dependent.closest(".form-group").find("label");
const labelRequired = label.find(".label-required");
if (!required) {
labelRequired.remove();
} else if (!labelRequired.length) {
label.append('<i class="label-required">' + gettext('required') + '</i>')
}
}
for (var k in dependents) dependents[k].prop("disabled", dependentsDisabled.includes(k));
loader.hide();
}
for (let k in dependents) dependents[k].prop('disabled', true)
loader.show()
let url = new URL(baseUrl, location.href)
// Address depends on all annotated fields
form.find('[data-trigger-address-info]').each(function () {
// Remove prefix of the form to get actual field name
if (($(this).attr('type') === 'radio' || $(this).attr('type') === 'checkbox') && !$(this).prop('checked')) {
return
}
url.searchParams.append(cleanName($(this).attr('name')), $(this).val())
})
if (dependents.transmission_type) {
url.searchParams.append('transmission_type_required', !dependents.transmission_type.find('option[value=\'-\']').length)
}
const update = function (ev) {
dependents.state.prop("data-selected-value", dependents.state.val());
if (dependents.transmission_type) {
dependents.transmission_type.prop("data-selected-value", dependents.transmission_type.val());
}
if (xhr && url in responseCache) {
if (responseCache[url] == xhr) {
// already requested this, but XHR is still running and will resolve promise
// only re-resolve promise for JSON-data in responseCache[url]
return
} else {
// abort current xhr as it is not the one we want
// aborting deletes responseCache[url] but async
xhr.abort()
}
}
for (var k in dependents) dependents[k].prop("disabled", true);
loader.show();
var url = new URL(baseUrl, location.href);
// Address depends on all annotated fields
form.find("[data-trigger-address-info]").each(function () {
// Remove prefix of the form to get actual field name
if (($(this).attr("type") === "radio" || $(this).attr("type") === "checkbox") && !$(this).prop("checked")) {
return
}
url.searchParams.append(cleanName($(this).attr("name")), $(this).val());
})
if (dependents.transmission_type) {
url.searchParams.append("transmission_type_required", !dependents.transmission_type.find("option[value='-']").length);
}
if (!(url in responseCache)) {
responseCache[url] = xhr = $.ajax({
dataType: 'json',
url: url,
timeout: 3000,
})
}
if (xhr && url in responseCache) {
if (responseCache[url] == xhr) {
// already requested this, but XHR is still running and will resolve promise
// only re-resolve promise for JSON-data in responseCache[url]
return;
} else {
// abort current xhr as it is not the one we want
// aborting deletes responseCache[url] but async
xhr.abort();
}
}
Promise.resolve(responseCache[url]).then(function (data) {
responseCache[url] = data
update_form(data)
}).catch(function () {
delete responseCache[url]
// In case of errors, show everything and require nothing, we can still handle errors in backend
for (let k in dependents) {
const dependent = dependents[k],
visible = true,
required = false
if (!(url in responseCache)) {
responseCache[url] = xhr = $.ajax({
dataType: "json",
url: url,
timeout: 3000,
});
}
dependent.closest('.form-group').toggle(visible).toggleClass('required', required)
dependent.prop('required', required).prop('disabled', dependentsDisabled.includes(k))
}
}).finally(function () {
loader.hide()
})
}
update()
dependencies.on('change', update)
Promise.resolve(responseCache[url]).then(function (data) {
responseCache[url] = data;
update_form(data);
}).catch(function () {
delete responseCache[url];
// In case of errors, show everything and require nothing, we can still handle errors in backend
for (var k in dependents) {
const dependent = dependents[k],
visible = true,
required = false;
if (dependents.vat_id && dependents.transmission_type && dependents.transmission_peppol_participant_id) {
// In Belgium, the VAT ID is built from "BE" + the company ID. The Peppol ID also needs to be built
// from the company ID with ID scheme 0208. We can save users some knowing and typing by filling this in!
if (!dependents.transmission_peppol_participant_id.val()) {
const fill_peppol_id = function () {
const vatId = dependents.vat_id.val()
if (vatId && vatId.startsWith('BE') && dependents.transmission_type.val() === 'peppol') {
dependents.transmission_peppol_participant_id.val('0208:' + vatId.substring(2).replaceAll('.', ''))
}
}
dependents.vat_id.add(dependents.transmission_type).on('change', fill_peppol_id)
dependents.transmission_peppol_participant_id.one('change', () => {
dependents.vat_id.add(dependents.transmission_type).unbind('change', fill_peppol_id)
})
}
}
})
})
dependent.closest(".form-group").toggle(visible).toggleClass('required', required);
dependent.prop("required", required).prop("disabled", dependentsDisabled.includes(k));
}
}).finally(function () {
loader.hide();
});
};
update();
dependencies.on("change", update);
if (dependents.vat_id && dependents.transmission_type && dependents.transmission_peppol_participant_id) {
// In Belgium, the VAT ID is built from "BE" + the company ID. The Peppol ID also needs to be built
// from the company ID with ID scheme 0208. We can save users some knowing and typing by filling this in!
if (!dependents.transmission_peppol_participant_id.val()) {
const fill_peppol_id = function () {
const vatId = dependents.vat_id.val();
if (vatId && vatId.startsWith("BE") && dependents.transmission_type.val() === "peppol") {
dependents.transmission_peppol_participant_id.val("0208:" + vatId.substring(2).replaceAll(".", ""))
}
}
dependents.vat_id.add(dependents.transmission_type).on("change", fill_peppol_id);
dependents.transmission_peppol_participant_id.one("change", () => {
dependents.vat_id.add(dependents.transmission_type).unbind("change", fill_peppol_id)
});
}
}
});
});
@@ -1,10 +1,10 @@
let check = function () {
$.getJSON(location.href + '&ajax=1', function (data, _status) {
if (data.redirect) {
location.href = data.redirect
} else {
window.setTimeout(check, 500)
}
})
var check = function () {
$.getJSON(location.href + '&ajax=1', function (data, status) {
if (data.redirect) {
location.href = data.redirect;
} else {
window.setTimeout(check, 500);
}
});
}
window.setTimeout(check, 500)
window.setTimeout(check, 500);
+330 -328
View File
@@ -1,357 +1,359 @@
/* global gettext */
let async_task_id = null
let async_task_timeout = null
let async_task_check_url = null
let async_task_old_url = null
let async_task_is_download = false
let async_task_is_long = false
let async_task_dont_redirect = false
/*global $, gettext */
var async_task_id = null;
var async_task_timeout = null;
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;
let async_task_status_messages = {
// These are functions in order to be lazily evaluated after the gettext file is loaded
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.'
)
var async_task_status_messages = {
// These are functions in order to be lazily evaluated after the gettext file is loaded
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";
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_schedule_check (context, timeout) {
'use strict'
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) {
$(window).one("pageshow", function (e) {
// hide waitingDialog when using browser's history back
waitingDialog.hide();
});
if (async_task_is_download && window.self !== window.top) {
// if in an iframe, force to download an async_task_is_download
// e.g. pretix-reseller embeds order-page in iframe, which would cause ticket-PDFs to be displayed inline
var a = document.createElement("a");
a.href = data.redirect;
a.download = "";
a.target = "_blank";
a.click();
} else {
location.href = data.redirect;
}
}
$(this).trigger('pretix:async-task-success', data);
}
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) {
$(window).one('pageshow', function (e) {
// hide waitingDialog when using browser's history back
waitingDialog.hide()
})
if (async_task_is_download && window.self !== window.top) {
// if in an iframe, force to download an async_task_is_download
// e.g. pretix-reseller embeds order-page in iframe, which would cause ticket-PDFs to be displayed inline
let a = document.createElement('a')
a.href = data.redirect
a.download = ''
a.target = '_blank'
a.click()
} else {
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) {
async_task_on_success.call(this, data);
return;
}
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_check_callback (data, textStatus, jqXHR) {
'use strict'
if (data.ready && data.redirect) {
async_task_on_success.call(this, data)
return
}
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) {
waitingDialog.setStatus(async_task_status_messages.long_task_started());
} else {
waitingDialog.setStatus(async_task_status_messages.long_task_pending());
}
} else {
waitingDialog.setStatus(async_task_status_messages.short_task());
}
}
function async_task_update_status (data) {
if (async_task_is_long) {
if (data.started) {
waitingDialog.setStatus(async_task_status_messages.long_task_started())
} else {
waitingDialog.setStatus(async_task_status_messages.long_task_pending())
}
} else {
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_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);
var c = respdom.filter('.container');
if (jqXHR.status === 401 && jqXHR.getResponseHeader("X-Login-Url")) {
window.location = jqXHR.getResponseHeader("X-Login-Url") + "?next=" + encodeURIComponent(location.pathname + location.search + location.hash);
return;
}
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);
async_task_replace_page("body", jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body")
));
} else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay
$("body").data('ajaxing', false);
waitingDialog.hide();
if (location.href.indexOf("async_id") !== -1) {
history.replaceState({}, "pretix", async_task_old_url);
}
ajaxErrDialog.show(c.first().html());
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
$("body").data('ajaxing', false);
waitingDialog.hide();
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
} else {
// 500 can be an application error or overload in some cases :(
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);
}
}
}
function async_task_check_error (jqXHR, textStatus, errorThrown) {
'use strict'
let respdom = $(jqXHR.responseText)
let c = respdom.filter('.container')
if (jqXHR.status === 401 && jqXHR.getResponseHeader('X-Login-Url')) {
window.location = jqXHR.getResponseHeader('X-Login-Url') + '?next=' + encodeURIComponent(location.pathname + location.search + location.hash)
return
}
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)
async_task_replace_page('body', jqXHR.responseText.substring(
jqXHR.responseText.indexOf('<body'),
jqXHR.responseText.indexOf('</body')
))
} else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay
$('body').data('ajaxing', false)
waitingDialog.hide()
if (location.href.indexOf('async_id') !== -1) {
history.replaceState({}, 'pretix', async_task_old_url)
}
ajaxErrDialog.show(c.first().html())
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
$('body').data('ajaxing', false)
waitingDialog.hide()
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status))
} else {
// 500 can be an application error or overload in some cases :(
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)
}
}
function async_task_callback(data, jqXHR, status) {
"use strict";
$("body").data('ajaxing', false);
if (data.redirect) {
async_task_on_success.call(this, data);
return;
}
var check_url = new URL(data.check_url, window.location);
if (async_task_dont_redirect) {
check_url.searchParams.set('ajax_dont_redirect', '1');
}
async_task_id = data.async_id;
async_task_check_url = check_url.toString();
async_task_schedule_check(this, 100);
async_task_update_status(data);
if (location.href.indexOf("async_id") === -1) {
history.pushState({}, "Waiting", async_task_check_url.replace(/ajax=1/, ''));
}
}
function async_task_callback (data, jqXHR, status) {
'use strict'
$('body').data('ajaxing', false)
if (data.redirect) {
async_task_on_success.call(this, data)
return
}
let check_url = new URL(data.check_url, window.location)
if (async_task_dont_redirect) {
check_url.searchParams.set('ajax_dont_redirect', '1')
}
async_task_id = data.async_id
async_task_check_url = check_url.toString()
async_task_schedule_check(this, 100)
function async_task_error(jqXHR, textStatus, errorThrown) {
"use strict";
$("body").data('ajaxing', false);
if (jqXHR.status === 401 && jqXHR.getResponseHeader("X-Login-Url")) {
window.location = jqXHR.getResponseHeader("X-Login-Url") + "?next=" + encodeURIComponent(location.pathname + location.search + location.hash);
return;
}
waitingDialog.hide();
if (textStatus === "timeout") {
alert(gettext("The request took too long. Please try again."));
} 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'))) {
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
// This is a failed form validation, let's just use it
async_task_replace_page("#page-wrapper", respdom.find("#page-wrapper").html());
} else {
async_task_replace_page("body", jqXHR.responseText.substring(
jqXHR.responseText.indexOf("<body"),
jqXHR.responseText.indexOf("</body")
));
document.dispatchEvent(new Event("pretix:async-task-error"))
async_task_update_status(data)
}
if (location.href.indexOf('async_id') === -1) {
history.pushState({}, 'Waiting', async_task_check_url.replace(/ajax=1/, ''))
}
}
function async_task_error (jqXHR, textStatus, errorThrown) {
'use strict'
$('body').data('ajaxing', false)
if (jqXHR.status === 401 && jqXHR.getResponseHeader('X-Login-Url')) {
window.location = jqXHR.getResponseHeader('X-Login-Url') + '?next=' + encodeURIComponent(location.pathname + location.search + location.hash)
return
}
waitingDialog.hide()
if (textStatus === 'timeout') {
alert(gettext('The request took too long. Please try again.'))
} else if (jqXHR.responseText.indexOf('<html') > 0) {
let respdom = $(jqXHR.responseText)
let c = respdom.filter('.container')
if (respdom.filter('form') && (respdom.filter('.has-error') || respdom.filter('.alert-danger'))) {
if (respdom.filter('#page-wrapper') && $('#page-wrapper').length) {
// This is a failed form validation, let's just use it
async_task_replace_page('#page-wrapper', respdom.find('#page-wrapper').html())
} else {
async_task_replace_page('body', jqXHR.responseText.substring(
jqXHR.responseText.indexOf('<body'),
jqXHR.responseText.indexOf('</body')
))
document.dispatchEvent(new Event('pretix:async-task-error'))
}
} else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay
ajaxErrDialog.show(c.first().html())
} else {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status))
}
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status))
} else {
alert(gettext('We currently cannot reach the server. Please try again. '
+ 'Error code: {code}').replace(/\{code\}/, jqXHR.status))
}
}
} else if (c.length > 0) {
// This is some kind of 500/404/403 page, show it in an overlay
ajaxErrDialog.show(c.first().html());
} else {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
}
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
} else {
alert(gettext('We currently cannot reach the server. Please try again. ' +
'Error code: {code}').replace(/\{code\}/, jqXHR.status));
}
}
}
$(function () {
'use strict'
$('body').on('submit', 'form[data-asynctask]', function (e) {
// Not supported on IE, may lead to wrong results, but we don't support IE in the backend anymore
let submitter = e.originalEvent ? e.originalEvent.submitter : null
"use strict";
$("body").on('submit', 'form[data-asynctask]', function (e) {
// Not supported on IE, may lead to wrong results, but we don't support IE in the backend anymore
var submitter = e.originalEvent ? e.originalEvent.submitter : null;
if (submitter && submitter.hasAttribute('data-no-asynctask')) {
return
}
if (submitter && submitter.hasAttribute("data-no-asynctask")) {
return;
}
e.preventDefault()
$(this).removeClass('dirty') // Avoid problems with are-you-sure.js
if ($('body').data('ajaxing')) {
return
}
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)
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.'
)
)
e.preventDefault();
$(this).removeClass("dirty"); // Avoid problems with are-you-sure.js
if ($("body").data('ajaxing')) {
return;
}
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);
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.'
)
);
let action = this.action
let formData = new FormData(this)
formData.append('ajax', '1')
if (async_task_dont_redirect) {
formData.append('ajax_dont_redirect', '1')
}
if (submitter && submitter.name) {
formData.append(submitter.name, submitter.value)
}
if (submitter && submitter.getAttribute('formaction')) {
action = submitter.getAttribute('formaction')
}
$.ajax(
{
type: 'POST',
url: action,
data: formData,
processData: false,
contentType: false,
success: async_task_callback,
error: async_task_error,
context: this,
dataType: 'json',
timeout: 60000,
}
)
})
var action = this.action;
var formData = new FormData(this);
formData.append('ajax', '1');
if (async_task_dont_redirect) {
formData.append('ajax_dont_redirect', '1');
}
if (submitter && submitter.name) {
formData.append(submitter.name, submitter.value);
}
if (submitter && submitter.getAttribute("formaction")) {
action = submitter.getAttribute("formaction");
}
$.ajax(
{
'type': 'POST',
'url': action,
'data': formData,
processData: false,
contentType: false,
'success': async_task_callback,
'error': async_task_error,
'context': this,
'dataType': 'json',
'timeout': 60000,
}
);
});
window.addEventListener('pageshow', function (evt) {
// In Safari, if you submit an async task, then get redirected, then go back,
// Safari won't reload the HTML from disk cache but instead reuse the DOM of the
// previous request, thus not clearing the "loading" state.
if (evt.persisted && $('body').hasClass('loading')) {
setTimeout(function () {
window.location.reload()
}, 10)
}
}, false)
window.addEventListener("pageshow", function (evt) {
// In Safari, if you submit an async task, then get redirected, then go back,
// Safari won't reload the HTML from disk cache but instead reuse the DOM of the
// previous request, thus not clearing the "loading" state.
if (evt.persisted && $("body").hasClass("loading")) {
setTimeout(function () {
window.location.reload();
}, 10);
}
}, false);
$('#ajaxerr').on('click', '.ajaxerr-close', ajaxErrDialog.hide)
$('#loadingmodal').on('cancel', function () {
return false
})
$('#loadingmodal').prop('closedBy', 'none')
})
$("#ajaxerr").on("click", ".ajaxerr-close", ajaxErrDialog.hide);
$("#loadingmodal").on("cancel", function() {
return false;
});
$("#loadingmodal").prop("closedBy", "none");
});
var waitingDialog = {
show: function (title, text, status) {
'use strict'
this.setTitle(title)
this.setText(text)
this.setStatus(status || gettext('If this takes longer than a few minutes, please contact us.'))
this.setProgress(null)
this.setSteps(null)
document.getElementById('loadingmodal').showModal()
},
hide: function () {
'use strict'
document.getElementById('loadingmodal').close()
},
setTitle: function (title) {
$('#loadingmodal .modal-card-title').text(title)
},
setStatus: function (statusText) {
$('#loadingmodal p.status').text(statusText)
},
setText: function (text) {
if (text)
$('#loadingmodal .modal-card-description').text(text).show()
else
$('#loadingmodal .modal-card-description').hide()
},
setProgress: function (percentage) {
if (typeof percentage === 'number') {
$('#loadingmodal .progress').show()
$('#loadingmodal .progress .progress-bar').css('width', percentage + '%')
} else {
$('#loadingmodal .progress').hide()
}
},
setSteps: function (steps) {
let $steps = $('#loadingmodal .steps')
if (steps) {
$steps.html('').show()
for (let 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.hide()
}
}
}
show: function (title, text, status) {
"use strict";
this.setTitle(title);
this.setText(text);
this.setStatus(status || gettext('If this takes longer than a few minutes, please contact us.'));
this.setProgress(null);
this.setSteps(null);
document.getElementById("loadingmodal").showModal();
},
hide: function () {
"use strict";
document.getElementById("loadingmodal").close();
},
setTitle: function(title) {
$("#loadingmodal .modal-card-title").text(title);
},
setStatus: function(statusText) {
$("#loadingmodal p.status").text(statusText);
},
setText: function(text) {
if (text)
$("#loadingmodal .modal-card-description").text(text).show();
else
$("#loadingmodal .modal-card-description").hide();
},
setProgress: function(percentage) {
if (typeof percentage === 'number') {
$("#loadingmodal .progress").show();
$("#loadingmodal .progress .progress-bar").css("width", percentage + "%");
} else {
$("#loadingmodal .progress").hide();
}
},
setSteps: function(steps) {
var $steps = $("#loadingmodal .steps");
if (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.hide();
}
}
};
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 has-modal-dialog')
$('#ajaxerr').prop('hidden', false)
},
hide: function () {
'use strict'
$('body').removeClass('ajaxerr has-modal-dialog')
$('#ajaxerr').prop('hidden', true)
},
}
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 has-modal-dialog");
$("#ajaxerr").prop("hidden", false);
},
hide: function () {
"use strict";
$("body").removeClass("ajaxerr has-modal-dialog");
$("#ajaxerr").prop("hidden", true);
},
};
@@ -1,7 +1,7 @@
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('a[href^="mailto:"]').forEach(function (link) {
// Replace [at] with @ and the [dot] with . in both the href and the displayed text (if needed)
link.href = link.href.replace('[at]', '@').replace('[dot]', '.')
link.textContent = link.textContent.replace('[at]', '@').replace('[dot]', '.')
})
})
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a[href^="mailto:"]').forEach(function(link) {
// Replace [at] with @ and the [dot] with . in both the href and the displayed text (if needed)
link.href = link.href.replace('[at]', '@').replace('[dot]', '.');
link.textContent = link.textContent.replace('[at]', '@').replace('[dot]', '.');
});
});
+140 -137
View File
@@ -1,149 +1,152 @@
/*global $ */
setup_collapsible_details = function (el) {
el.find('.sneak-peek-trigger').each(function () {
let trigger = this
let button = this.querySelector('button')
let content = document.getElementById(button.getAttribute('aria-controls'))
if (content.scrollHeight < 200) {
trigger.remove()
content.classList.remove('sneak-peek-content')
return
}
content.setAttribute('aria-hidden', 'true')
content.setAttribute('inert', true)
button.setAttribute('aria-expanded', 'false')
button.addEventListener('click', function (e) {
button.setAttribute('aria-expanded', 'true')
content.setAttribute('aria-hidden', 'false')
content.removeAttribute('inert')
content.addEventListener('transitionend', function () {
content.classList.remove('sneak-peek-content')
content.style.removeProperty('height')
// we need to keep the trigger/button in the DOM to not irritate screenreaders toggling visibility
trigger.classList.add('sr-only')
}, { once: true })
content.style.height = content.scrollHeight + 'px'
el.find('.sneak-peek-trigger').each(function() {
var trigger = this;
var button = this.querySelector('button');
var content = document.getElementById(button.getAttribute('aria-controls'));
if (content.scrollHeight < 200) {
trigger.remove();
content.classList.remove('sneak-peek-content');
return;
}
content.setAttribute('aria-hidden', 'true');
content.setAttribute('inert', true);
button.setAttribute('aria-expanded', 'false');
button.addEventListener('click', function (e) {
button.setAttribute('aria-expanded', 'true');
content.setAttribute('aria-hidden', 'false');
content.removeAttribute('inert');
button.addEventListener('click', function (e) {
// this will be called by screenreader users if they kept focus on the button after expanding
// we need to keep the trigger/button in the DOM to not irritate screenreaders toggling visibility
let expanded = button.getAttribute('aria-expanded') == 'true'
button.setAttribute('aria-expanded', !expanded)
content.setAttribute('aria-hidden', expanded)
})
button.addEventListener('blur', function (e) {
// if content is visible and the user leaves the button, we can safely remove the trigger/button
if (button.getAttribute('aria-expanded') == 'true') {
trigger.remove()
}
})
}, { once: true })
content.addEventListener('transitionend', function() {
content.classList.remove('sneak-peek-content');
content.style.removeProperty('height');
// we need to keep the trigger/button in the DOM to not irritate screenreaders toggling visibility
trigger.classList.add('sr-only');
}, {once: true});
content.style.height = content.scrollHeight + 'px';
let container = this.closest('details.sneak-peek-container')
if (container) {
function removeSneekPeakWhenClosed (e) {
if (e.newState == 'closed') {
container.removeEventListener('toggle', removeSneekPeakWhenClosed)
trigger.remove()
content.removeAttribute('aria-hidden')
content.removeAttribute('inert')
content.classList.remove('sneak-peek-content')
}
}
container.addEventListener('toggle', removeSneekPeakWhenClosed)
}
})
button.addEventListener('click', function (e) {
// this will be called by screenreader users if they kept focus on the button after expanding
// we need to keep the trigger/button in the DOM to not irritate screenreaders toggling visibility
var expanded = button.getAttribute('aria-expanded') == 'true';
button.setAttribute('aria-expanded', !expanded);
content.setAttribute('aria-hidden', expanded);
});
button.addEventListener('blur', function (e) {
// if content is visible and the user leaves the button, we can safely remove the trigger/button
if (button.getAttribute('aria-expanded') == 'true') {
trigger.remove();
}
});
}, { once: true });
let isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]'
el.find('details summary').click(function (e) {
if (this.tagName !== 'A' && $(e.target).closest('a').length > 0) {
return true
}
let $details = $(this).closest('details')
let isOpen = $details.prop('open')
let $detailsNotSummary = $details.children(':not(summary)')
if ($detailsNotSummary.is(':animated')) {
e.preventDefault()
return false
}
if (isOpen) {
$details.removeClass('details-open')
$detailsNotSummary.stop().show().slideUp(500, function () {
$details.prop('open', false)
})
} else {
$detailsNotSummary.stop().hide()
$details.prop('open', true)
$details.addClass('details-open')
$detailsNotSummary.slideDown()
}
e.preventDefault()
return false
}).keyup(function (event) {
if (32 == event.keyCode || (13 == event.keyCode && !isOpera)) {
// Space or Enter is pressed — trigger the `click` event on the `summary` element
// Opera already seems to trigger the `click` event when Enter is pressed
event.preventDefault()
$(this).click()
}
})
var container = this.closest('details.sneak-peek-container');
if (container) {
function removeSneekPeakWhenClosed(e) {
if (e.newState == "closed") {
container.removeEventListener("toggle", removeSneekPeakWhenClosed);
trigger.remove();
content.removeAttribute('aria-hidden');
content.removeAttribute('inert');
content.classList.remove('sneak-peek-content');
}
}
container.addEventListener("toggle", removeSneekPeakWhenClosed);
}
});
$('details').each(function () {
let $details = $(this),
$detailsSummary = $('summary', $details).first(),
$detailsNotSummary = $details.children(':not(summary)')
$details.prop('open', typeof $details.attr('open') == 'string')
if (!$details.prop('open')) {
if ($details.find('.has-error, .alert-danger').length) {
$details.addClass('details-open')
$details.prop('open', true)
} else {
$detailsNotSummary.hide()
}
} else {
$details.addClass('details-open')
}
$detailsSummary.attr({
role: 'button',
'aria-controls': $details.attr('id')
}).prop('tabIndex', 0).bind('selectstart dragstart mousedown', function () {
return false
})
})
var isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
el.find("details summary").click(function (e) {
if (this.tagName !== "A" && $(e.target).closest("a").length > 0) {
return true;
}
var $details = $(this).closest("details");
var isOpen = $details.prop("open");
var $detailsNotSummary = $details.children(':not(summary)');
if ($detailsNotSummary.is(':animated')) {
e.preventDefault();
return false;
}
if (isOpen) {
$details.removeClass("details-open");
$detailsNotSummary.stop().show().slideUp(500, function () {
$details.prop("open", false);
});
} else {
$detailsNotSummary.stop().hide();
$details.prop("open", true);
$details.addClass("details-open");
$detailsNotSummary.slideDown();
}
e.preventDefault();
return false;
}).keyup(function (event) {
if (32 == event.keyCode || (13 == event.keyCode && !isOpera)) {
// Space or Enter is pressed — trigger the `click` event on the `summary` element
// Opera already seems to trigger the `click` event when Enter is pressed
event.preventDefault();
$(this).click();
}
});
el.find('article button[data-toggle=variations]').click(function (e) {
let $button = $(this)
let $details = $button.closest('article')
let $detailsNotSummary = $button.attr('aria-controls') ? $('#' + $button.attr('aria-controls')) : $('.variations', $details)
let isOpen = !$detailsNotSummary.prop('hidden')
if ($detailsNotSummary.is(':animated')) {
e.preventDefault()
return false
}
$('details').each(function () {
var $details = $(this),
$detailsSummary = $('summary', $details).first(),
$detailsNotSummary = $details.children(':not(summary)');
$details.prop('open', typeof $details.attr('open') == 'string');
if (!$details.prop('open')) {
if ($details.find(".has-error, .alert-danger").length) {
$details.addClass("details-open");
$details.prop('open', true);
} else {
$detailsNotSummary.hide();
}
} else {
$details.addClass("details-open");
}
$detailsSummary.attr({
'role': 'button',
'aria-controls': $details.attr('id')
}).prop('tabIndex', 0).bind('selectstart dragstart mousedown', function () {
return false;
});
});
let altLabel = $button.attr('data-label-alt')
$button.attr('data-label-alt', $button.text().trim())
$button.find('span').text(altLabel)
$button.attr('aria-expanded', !isOpen)
el.find("article button[data-toggle=variations]").click(function (e) {
var $button = $(this);
var $details = $button.closest("article");
var $detailsNotSummary = $button.attr("aria-controls") ? $('#' + $button.attr("aria-controls")) : $(".variations", $details);
var isOpen = !$detailsNotSummary.prop("hidden");
if ($detailsNotSummary.is(':animated')) {
e.preventDefault();
return false;
}
if (isOpen) {
$details.removeClass('details-open')
$detailsNotSummary.stop().show().slideUp(500, function () {
$detailsNotSummary.prop('hidden', true)
})
} else {
$detailsNotSummary.prop('hidden', false).stop().hide()
$details.addClass('details-open')
$detailsNotSummary.slideDown()
}
e.preventDefault()
return false
})
el.find('.variations-collapsed').prop('hidden', true)
}
var altLabel = $button.attr("data-label-alt");
$button.attr("data-label-alt", $button.text().trim());
$button.find("span").text(altLabel);
$button.attr("aria-expanded", !isOpen);
if (isOpen) {
$details.removeClass("details-open");
$detailsNotSummary.stop().show().slideUp(500, function () {
$detailsNotSummary.prop("hidden", true);
});
} else {
$detailsNotSummary.prop("hidden", false).stop().hide();
$details.addClass("details-open");
$detailsNotSummary.slideDown();
}
e.preventDefault();
return false;
});
el.find(".variations-collapsed").prop("hidden", true);
};
$(function () {
'use strict'
"use strict";
setup_collapsible_details($('body'))
})
setup_collapsible_details($("body"));
});
+10 -10
View File
@@ -1,11 +1,11 @@
['DOMContentLoaded', 'pretix:async-task-error'].forEach(function (ev) {
document.addEventListener(ev, function () {
document.querySelectorAll('#goback, #reload').forEach(function (element) {
const regularLoad = ev === 'DOMContentLoaded' && element.id === 'goback'
element.addEventListener('click', regularLoad
? () => window.history.back()
: () => window.location.reload()
)
})
})
})
document.addEventListener(ev, function () {
document.querySelectorAll('#goback, #reload').forEach(function (element) {
const regularLoad = ev === 'DOMContentLoaded' && element.id === 'goback';
element.addEventListener('click', regularLoad
? () => window.history.back()
: () => window.location.reload()
);
});
});
});
@@ -1,3 +1,3 @@
// Attempt to auto-open page in new tab. Will be ignored by most browser's popup blockers anyways, though.
let url = JSON.parse(document.getElementById('framebreak-url').innerText)
var url = JSON.parse(document.getElementById('framebreak-url').innerText)
window.open(url)
+22 -21
View File
@@ -1,29 +1,30 @@
// The actual gettext implementation is loaded asynchronously with the translation
function gettext (msgid) {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid)
}
return msgid
function gettext(msgid) {
if (typeof django !== 'undefined' && typeof django.gettext !== 'undefined') {
return django.gettext(msgid);
}
return msgid;
}
function ngettext (singular, plural, count) {
if (typeof django !== 'undefined' && typeof django.ngettext !== 'undefined') {
return django.ngettext(singular, plural, count)
}
return plural
function ngettext(singular, plural, count) {
if (typeof django !== 'undefined' && typeof django.ngettext !== 'undefined') {
return django.ngettext(singular, plural, count);
}
return plural;
}
function pgettext (context, msgid) {
if (typeof django !== 'undefined' && typeof django.pgettext !== 'undefined') {
return django.pgettext(context, msgid)
}
return msgid
function pgettext(context, msgid) {
if (typeof django !== 'undefined' && typeof django.pgettext !== 'undefined') {
return django.pgettext(context, msgid);
}
return msgid;
}
function interpolate (fmt, object, named) {
if (named) {
return fmt.replace(/%\(\w+\)s/g, function (match) { return String(obj[match.slice(2, -2)]) })
} else {
return fmt.replace(/%s/g, function (match) { return String(obj.shift()) })
}
function interpolate(fmt, object, named) {
if (named) {
return fmt.replace(/%\(\w+\)s/g, function(match){return String(obj[match.slice(2,-2)])});
} else {
return fmt.replace(/%s/g, function(match){return String(obj.shift())});
}
}
+20 -19
View File
@@ -1,24 +1,25 @@
function i18nstring_localize (o) {
let locale = document.body.attributes['data-pretixlocale'].value
let short_locale = locale.split('-')[0]
if (o[locale])
return o[locale]
function i18nstring_localize(o) {
var locale = document.body.attributes['data-pretixlocale'].value
var short_locale = locale.split('-')[0]
if (o[locale])
return o[locale]
if (o[short_locale])
return o[short_locale]
if (o[short_locale])
return o[short_locale]
for (let k of Object.keys(o)) {
if (k.split('-')[0] === short_locale && o[k]) {
return o[k]
}
}
for (k of Object.keys(o)) {
if (k.split('-')[0] === short_locale && o[k]) {
return o[k]
}
}
if (o['en'])
return o['en']
if (o['en'])
return o['en']
for (let k of Object.keys(o)) {
if (o[k]) {
return o[k]
}
}
for (k of Object.keys(o)) {
if (o[k]) {
return o[k]
}
}
}
+1 -1
View File
@@ -32,7 +32,7 @@ $(document).on('pretix:bind-forms', () => {
}
if (dirty) {
beforeAfterSelect.dispatchEvent(new Event('change', { bubbles: true }))
beforeAfterSelect.dispatchEvent(new Event('change', {bubbles: true}))
}
}
referenceSelect.addEventListener('change', updateBeforeOption)
@@ -1,8 +1,8 @@
const intId = window.setInterval(function () {
$.get(location.href + '?ajax=1', function (data, _status) {
if (data === '1') {
window.clearInterval(intId)
location.reload()
}
})
}, 500)
var intId = window.setInterval(function () {
$.get(location.href + '?ajax=1', function (data, status) {
if (data === "1") {
window.clearInterval(intId);
location.reload();
}
});
}, 500);
@@ -170,14 +170,13 @@ body.has-modal-dialog .container, body.has-modal-dialog #wrapper {
#lightbox-dialog {
width: fit-content;
max-width: 80%;
min-width: calc(min(24em, 90%));
min-width: 24em;
.modal-card-content {
padding: 2.5em;
}
img {
max-width: 100%;
max-height: calc(100dvh - 60px - 5em - 5em);
}
button {
+17 -17
View File
@@ -1,21 +1,21 @@
let hiddenfield = document.querySelector('input[name=origin][type=hidden]')
var hiddenfield = document.querySelector("input[name=origin][type=hidden]");
if (hiddenfield) {
hiddenfield.value = window.location.origin
hiddenfield.value = window.location.origin
}
async function runCheck () {
if (document.getElementById('good_origin')) {
if (document.getElementById('good_origin').innerText.split('').reverse().join('') !== window.location.origin) {
const _response = await fetch(document.getElementById('bad_origin_report_url').innerText.split('').reverse().join(''), {
method: 'POST',
mode: 'cors',
referrerPolicy: 'unsafe-url',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'origin=' + window.location.origin,
})
}
}
async function runCheck() {
if (document.getElementById("good_origin")) {
if (document.getElementById("good_origin").innerText.split('').reverse().join('') !== window.location.origin) {
const response = await fetch(document.getElementById("bad_origin_report_url").innerText.split('').reverse().join(''), {
method: "POST",
mode: "cors",
referrerPolicy: "unsafe-url",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: "origin=" + window.location.origin,
});
}
}
}
runCheck()
runCheck();
+95 -95
View File
@@ -1,118 +1,118 @@
let lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
var lookup = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
;(function (exports) {
'use strict'
'use strict'
let Arr = (typeof Uint8Array !== 'undefined')
? Uint8Array
: Array
var Arr = (typeof Uint8Array !== 'undefined')
? Uint8Array
: Array
let PLUS = '+'.charCodeAt(0)
let SLASH = '/'.charCodeAt(0)
let NUMBER = '0'.charCodeAt(0)
let LOWER = 'a'.charCodeAt(0)
let UPPER = 'A'.charCodeAt(0)
let PLUS_URL_SAFE = '-'.charCodeAt(0)
let SLASH_URL_SAFE = '_'.charCodeAt(0)
var PLUS = '+'.charCodeAt(0)
var SLASH = '/'.charCodeAt(0)
var NUMBER = '0'.charCodeAt(0)
var LOWER = 'a'.charCodeAt(0)
var UPPER = 'A'.charCodeAt(0)
var PLUS_URL_SAFE = '-'.charCodeAt(0)
var SLASH_URL_SAFE = '_'.charCodeAt(0)
function decode (elt) {
let code = elt.charCodeAt(0)
if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
if (code < NUMBER) return -1 // no match
if (code < NUMBER + 10) return code - NUMBER + 26 + 26
if (code < UPPER + 26) return code - UPPER
if (code < LOWER + 26) return code - LOWER + 26
}
function decode (elt) {
var code = elt.charCodeAt(0)
if (code === PLUS || code === PLUS_URL_SAFE) return 62 // '+'
if (code === SLASH || code === SLASH_URL_SAFE) return 63 // '/'
if (code < NUMBER) return -1 // no match
if (code < NUMBER + 10) return code - NUMBER + 26 + 26
if (code < UPPER + 26) return code - UPPER
if (code < LOWER + 26) return code - LOWER + 26
}
function b64ToByteArray (b64) {
let i, j, l, tmp, placeHolders, arr
function b64ToByteArray (b64) {
var i, j, l, tmp, placeHolders, arr
if (b64.length % 4 > 0) {
throw new Error('Invalid string. Length must be a multiple of 4')
}
if (b64.length % 4 > 0) {
throw new Error('Invalid string. Length must be a multiple of 4')
}
// the number of equal signs (place holders)
// if there are two placeholders, than the two characters before it
// represent one byte
// if there is only one, then the three characters before it represent 2 bytes
// this is just a cheap hack to not do indexOf twice
let len = b64.length
placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0
// the number of equal signs (place holders)
// if there are two placeholders, than the two characters before it
// represent one byte
// if there is only one, then the three characters before it represent 2 bytes
// this is just a cheap hack to not do indexOf twice
var len = b64.length
placeHolders = b64.charAt(len - 2) === '=' ? 2 : b64.charAt(len - 1) === '=' ? 1 : 0
// base64 is 4/3 + up to two characters of the original data
arr = new Arr(b64.length * 3 / 4 - placeHolders)
// base64 is 4/3 + up to two characters of the original data
arr = new Arr(b64.length * 3 / 4 - placeHolders)
// if there are placeholders, only get up to the last complete 4 chars
l = placeHolders > 0 ? b64.length - 4 : b64.length
// if there are placeholders, only get up to the last complete 4 chars
l = placeHolders > 0 ? b64.length - 4 : b64.length
let L = 0
var L = 0
function push (v) {
arr[L++] = v
}
function push (v) {
arr[L++] = v
}
for (i = 0, j = 0; i < l; i += 4, j += 3) {
tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
push((tmp & 0xFF0000) >> 16)
push((tmp & 0xFF00) >> 8)
push(tmp & 0xFF)
}
for (i = 0, j = 0; i < l; i += 4, j += 3) {
tmp = (decode(b64.charAt(i)) << 18) | (decode(b64.charAt(i + 1)) << 12) | (decode(b64.charAt(i + 2)) << 6) | decode(b64.charAt(i + 3))
push((tmp & 0xFF0000) >> 16)
push((tmp & 0xFF00) >> 8)
push(tmp & 0xFF)
}
if (placeHolders === 2) {
tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
push(tmp & 0xFF)
} else if (placeHolders === 1) {
tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
push((tmp >> 8) & 0xFF)
push(tmp & 0xFF)
}
if (placeHolders === 2) {
tmp = (decode(b64.charAt(i)) << 2) | (decode(b64.charAt(i + 1)) >> 4)
push(tmp & 0xFF)
} else if (placeHolders === 1) {
tmp = (decode(b64.charAt(i)) << 10) | (decode(b64.charAt(i + 1)) << 4) | (decode(b64.charAt(i + 2)) >> 2)
push((tmp >> 8) & 0xFF)
push(tmp & 0xFF)
}
return arr
}
return arr
}
function uint8ToBase64 (uint8) {
let i
let extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
let output = ''
let temp, length
function uint8ToBase64 (uint8) {
var i
var extraBytes = uint8.length % 3 // if we have 1 byte left, pad 2 bytes
var output = ''
var temp, length
function encode (num) {
return lookup.charAt(num)
}
function encode (num) {
return lookup.charAt(num)
}
function tripletToBase64 (num) {
return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
}
function tripletToBase64 (num) {
return encode(num >> 18 & 0x3F) + encode(num >> 12 & 0x3F) + encode(num >> 6 & 0x3F) + encode(num & 0x3F)
}
// go through the array every three bytes, we'll deal with trailing stuff later
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
output += tripletToBase64(temp)
}
// go through the array every three bytes, we'll deal with trailing stuff later
for (i = 0, length = uint8.length - extraBytes; i < length; i += 3) {
temp = (uint8[i] << 16) + (uint8[i + 1] << 8) + (uint8[i + 2])
output += tripletToBase64(temp)
}
// pad the end with zeros, but make sure to not forget the extra bytes
switch (extraBytes) {
case 1:
temp = uint8[uint8.length - 1]
output += encode(temp >> 2)
output += encode((temp << 4) & 0x3F)
output += '=='
break
case 2:
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
output += encode(temp >> 10)
output += encode((temp >> 4) & 0x3F)
output += encode((temp << 2) & 0x3F)
output += '='
break
default:
break
}
// pad the end with zeros, but make sure to not forget the extra bytes
switch (extraBytes) {
case 1:
temp = uint8[uint8.length - 1]
output += encode(temp >> 2)
output += encode((temp << 4) & 0x3F)
output += '=='
break
case 2:
temp = (uint8[uint8.length - 2] << 8) + (uint8[uint8.length - 1])
output += encode(temp >> 10)
output += encode((temp >> 4) & 0x3F)
output += encode((temp << 2) & 0x3F)
output += '='
break
default:
break
}
return output
}
return output
}
exports.toByteArray = b64ToByteArray
exports.fromByteArray = uint8ToBase64
exports.toByteArray = b64ToByteArray
exports.fromByteArray = uint8ToBase64
}(typeof exports === 'undefined' ? (this.base64js = {}) : exports))
+28 -28
View File
@@ -1,33 +1,33 @@
/* global gettext */
$(function () {
$('.btn-clipboard').tooltip({
trigger: 'click',
placement: 'bottom'
})
$(function() {
$('.btn-clipboard').tooltip({
trigger: 'click',
placement: 'bottom'
});
function setTooltip (btn, message) {
$(btn).tooltip('hide')
.attr('data-original-title', message)
.tooltip('show')
}
function setTooltip(btn, message) {
$(btn).tooltip('hide')
.attr('data-original-title', message)
.tooltip('show');
}
function hideTooltip (btn) {
setTimeout(function () {
$(btn).tooltip('hide')
}, 1000)
}
function hideTooltip(btn) {
setTimeout(function() {
$(btn).tooltip('hide');
}, 1000);
}
let clipboard = new Clipboard('.btn-clipboard')
var clipboard = new Clipboard('.btn-clipboard');
clipboard.on('success', function (e) {
if (e.text.length > 0) {
setTooltip(e.trigger, gettext('Copied!'))
hideTooltip(e.trigger)
}
})
clipboard.on('success', function(e) {
if (e.text.length > 0) {
setTooltip(e.trigger, gettext('Copied!'));
hideTooltip(e.trigger);
}
});
clipboard.on('error', function(e) {
setTooltip(e.trigger, gettext('Press Ctrl-C to copy!'));
hideTooltip(e.trigger);
});
});
clipboard.on('error', function (e) {
setTooltip(e.trigger, gettext('Press Ctrl-C to copy!'))
hideTooltip(e.trigger)
})
})
+65 -61
View File
@@ -8,75 +8,79 @@
* Under MIT License
* Modified by Raphael Michel
*/
;(function ($, window, document, undefined) {
let pluginName = 'metisMenu',
defaults = {
toggle: true,
}
;(function($, window, document, undefined) {
function Plugin (element, options) {
this.element = $(element)
this.settings = $.extend({}, defaults, options)
this._defaults = defaults
this._name = pluginName
this.init()
}
var pluginName = "metisMenu",
defaults = {
toggle: true,
};
Plugin.prototype = {
init: function () {
let $this = this.element,
$toggle = this.settings.toggle,
obj = this
function Plugin(element, options) {
this.element = $(element);
this.settings = $.extend({}, defaults, options);
this._defaults = defaults;
this._name = pluginName;
this.init();
}
if (this.isIE() <= 9) {
$this.find('li.active').has('ul').children('ul').collapse('show')
$this.find('li').not('.active').has('ul').children('ul').collapse('hide')
} else {
$this.find('li.active').has('ul').children('ul').addClass('collapse in')
$this.find('li').not('.active').has('ul').children('ul').addClass('collapse')
}
Plugin.prototype = {
init: function() {
$this.find('li').has('ul').children('a.arrow').on('click' + '.' + pluginName, function (e) {
e.preventDefault()
$(this).blur()
var $this = this.element,
$toggle = this.settings.toggle,
obj = this;
$(this).parent('li').toggleClass('active').children('ul').collapse('toggle')
if (this.isIE() <= 9) {
$this.find("li.active").has("ul").children("ul").collapse("show");
$this.find("li").not(".active").has("ul").children("ul").collapse("hide");
} else {
$this.find("li.active").has("ul").children("ul").addClass("collapse in");
$this.find("li").not(".active").has("ul").children("ul").addClass("collapse");
}
if ($toggle) {
$(this).parent('li').siblings().removeClass('active').children('ul.in').collapse('hide')
}
})
},
$this.find("li").has("ul").children("a.arrow").on("click" + "." + pluginName, function(e) {
e.preventDefault();
$(this).blur();
isIE: function () { // https://gist.github.com/padolsey/527683
let undef,
v = 3,
div = document.createElement('div'),
all = div.getElementsByTagName('i')
$(this).parent("li").toggleClass("active").children("ul").collapse("toggle");
while (
div.innerHTML = '<!--[if gt IE ' + (++v) + ']><i></i><![endif]-->',
all[0]
) {
return v > 4 ? v : undef
}
},
if ($toggle) {
$(this).parent("li").siblings().removeClass("active").children("ul.in").collapse("hide");
}
remove: function () {
this.element.off('.' + pluginName)
this.element.removeData(pluginName)
}
});
},
}
isIE: function() { //https://gist.github.com/padolsey/527683
var undef,
v = 3,
div = document.createElement("div"),
all = div.getElementsByTagName("i");
$.fn[pluginName] = function (options) {
this.each(function () {
let el = $(this)
if (el.data(pluginName)) {
el.data(pluginName).remove()
}
el.data(pluginName, new Plugin(this, options))
})
return this
}
})(jQuery, window, document)
while (
div.innerHTML = "<!--[if gt IE " + (++v) + "]><i></i><![endif]-->",
all[0]
) {
return v > 4 ? v : undef;
}
},
remove: function() {
this.element.off("." + pluginName);
this.element.removeData(pluginName);
}
};
$.fn[pluginName] = function(options) {
this.each(function () {
var el = $(this);
if (el.data(pluginName)) {
el.data(pluginName).remove();
}
el.data(pluginName, new Plugin(this, options));
});
return this;
};
})(jQuery, window, document);
@@ -1,34 +1,35 @@
/*global $ */
/*
Based on https://github.com/BlackrockDigital/startbootstrap-sb-admin-2
Copyright 2013-2016 Blackrock Digital LLC
MIT License
Modified by Raphael Michel
*/
// Loads the correct sidebar on window load,
// collapses the sidebar on window resize.
//Loads the correct sidebar on window load,
//collapses the sidebar on window resize.
// Sets the min-height of #page-wrapper to window size
$(window).bind('load resize', function () {
'use strict'
let topOffset = 50,
width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width
if (width < 768) {
$('div.navbar-collapse').addClass('collapse')
topOffset = 100 // 2-row-menu
} else {
$('div.navbar-collapse').removeClass('collapse')
}
$(window).bind("load resize", function () {
'use strict';
var topOffset = 50,
width = (this.window.innerWidth > 0) ? this.window.innerWidth : this.screen.width;
if (width < 768) {
$('div.navbar-collapse').addClass('collapse');
topOffset = 100; // 2-row-menu
} else {
$('div.navbar-collapse').removeClass('collapse');
}
let height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1
height = height - topOffset
if (height < 1) height = 1
if (height > topOffset) {
$('#page-wrapper').css('min-height', (height) + 'px')
}
})
var height = ((this.window.innerHeight > 0) ? this.window.innerHeight : this.screen.height) - 1;
height = height - topOffset;
if (height < 1) height = 1;
if (height > topOffset) {
$("#page-wrapper").css("min-height", (height) + "px");
}
});
$(function () {
'use strict'
$('ul.nav ul.nav-second-level a.active').parent().parent().addClass('in').parent().addClass('active')
$('#side-menu').metisMenu({
toggle: false,
})
})
'use strict';
$('ul.nav ul.nav-second-level a.active').parent().parent().addClass('in').parent().addClass('active');
$('#side-menu').metisMenu({
'toggle': false,
});
});
@@ -1 +1 @@
document.forms[0].submit()
document.forms[0].submit();
@@ -1,25 +1,26 @@
/* global add_log_expand_handlers */
/*global $,gettext*/
$(function () {
if ($('div[data-lazy-id]').length == 0) {
return
}
$.getJSON('widgets.json' + ($('select[name=\'subevent\']').val() ? '?subevent=' + $('select[name=\'subevent\']').val() : ''), function (data) {
$.each(data.widgets, function (_k, v) {
$('[data-lazy-id=' + v.lazy + ']').removeClass('widget-lazy-loading')
$('[data-lazy-id=' + v.lazy + '] .widget').html(v.content)
})
})
})
if ($("div[data-lazy-id]").length == 0) {
return;
}
$.getJSON("widgets.json" + ($("select[name='subevent']").val() ? "?subevent=" + $("select[name='subevent']").val() : ""), function (data) {
$.each(data.widgets, function (k, v) {
$("[data-lazy-id=" + v.lazy + "]").removeClass("widget-lazy-loading");
$("[data-lazy-id=" + v.lazy + "] .widget").html(v.content);
});
});
});
$(function () {
if ($('#logs_target').length == 0) {
return
}
$.get('dashboard/partials/logs', function (data) {
$('#logs_target').html(data)
add_log_expand_handlers($('#logs_target'))
})
$.get('dashboard/partials/warnings', function (data) {
$('#warnings_loading').remove()
$('#warnings_target').html(data)
})
})
if ($("#logs_target").length == 0) {
return;
}
$.get("dashboard/partials/logs", function (data) {
$("#logs_target").html(data)
add_log_expand_handlers($("#logs_target"))
});
$.get("dashboard/partials/warnings", function (data) {
$("#warnings_loading").remove()
$("#warnings_target").html(data)
});
});
@@ -1,12 +1,14 @@
/*globals $, Morris, gettext, RRule, RRuleSet*/
$(function () {
let update = function () {
$.getJSON(location.href + '?ajax=true', {}, function (data) {
if (data.initialized) {
location.reload()
} else {
window.setTimeout(update, 500)
}
})
}
window.setTimeout(update, 500)
})
var update = function () {
$.getJSON(location.href + '?ajax=true', {}, function (data) {
if (data.initialized) {
location.reload();
} else {
window.setTimeout(update, 500);
}
});
};
window.setTimeout(update, 500);
});
@@ -1,95 +1,91 @@
/* global Sortable */
/*global $, Sortable*/
$(function () {
const allContainers = $('[data-dnd-url]')
function updateAllSortButtonStates () {
allContainers.each(function () {
updateSortButtonState($(this))
})
}
function updateSortButtonState (container) {
let disabledUp = container.find('.sortable-up:disabled'),
firstUp = container.find('>tr[data-dnd-id] .sortable-up').first()
if (disabledUp.length && disabledUp.get(0) !== firstUp.get(0)) {
disabledUp.prop('disabled', false)
firstUp.prop('disabled', true)
}
const allContainers = $("[data-dnd-url]");
function updateAllSortButtonStates() {
allContainers.each(function() { updateSortButtonState($(this)); });
}
function updateSortButtonState(container) {
var disabledUp = container.find(".sortable-up:disabled"),
firstUp = container.find(">tr[data-dnd-id] .sortable-up").first();
if (disabledUp.length && disabledUp.get(0) !== firstUp.get(0)) {
disabledUp.prop("disabled", false);
firstUp.prop("disabled", true);
}
let disabledDown = container.find('.sortable-down:disabled'),
lastDown = container.find('>tr[data-dnd-id] .sortable-down').last()
if (disabledDown.length && disabledDown.get(0) !== lastDown.get(0)) {
disabledDown.prop('disabled', false)
lastDown.prop('disabled', true)
}
}
var disabledDown = container.find(".sortable-down:disabled"),
lastDown = container.find(">tr[data-dnd-id] .sortable-down").last();
if (disabledDown.length && disabledDown.get(0) !== lastDown.get(0)) {
disabledDown.prop("disabled", false);
lastDown.prop("disabled", true);
}
}
let didSort = false, lastClick = 0
allContainers.each(function () {
const container = $(this),
url = container.data('dnd-url'),
handle = $('<span class="btn btn-default btn-sm dnd-sort-handle"><i class="fa fa-arrows"></i></span>')
let didSort = false, lastClick = 0;
allContainers.each(function(){
const container = $(this),
url = container.data("dnd-url"),
handle = $('<span class="btn btn-default btn-sm dnd-sort-handle"><i class="fa fa-arrows"></i></span>');
container.find('.dnd-container').append(handle)
if (!sessionStorage.dndShowMoveButtons) {
container.find('.sortable-up, .sortable-down').addClass('sr-only').on('click', function () {
sessionStorage.dndShowMoveButtons = 'true'
})
}
if (container.find('[data-dnd-id]').length < 2 && !container.data('dnd-group')) {
handle.addClass('disabled')
return
}
function maybeShowSortButtons () {
if (Date.now() - lastClick < 3000) {
$('[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down').removeClass('sr-only')
updateAllSortButtonStates()
}
lastClick = Date.now()
}
container.find('.dnd-sort-handle').on('mouseup', maybeShowSortButtons)
const group = container.data('dnd-group')
const containers = group ? container.parent().find('[data-dnd-group="' + group + '"]') : container
Sortable.create(container.get(0), {
filter: '.sortable-disabled',
handle: '.dnd-sort-handle',
group: group,
onMove: function (evt) {
return evt.related.className.indexOf('sortable-disabled') === -1
},
onStart: function () {
containers.addClass('sortable-dragarea')
container.parent().addClass('sortable-sorting')
didSort = false
},
onEnd: function () {
containers.removeClass('sortable-dragarea')
container.parent().removeClass('sortable-sorting')
if (!didSort) {
maybeShowSortButtons()
} else {
$('[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down').addClass('sr-only')
delete sessionStorage.dndShowMoveButtons
}
},
onSort: function (evt) {
if (evt.target !== evt.to) return
didSort = true
container.find(".dnd-container").append(handle);
if (!sessionStorage.dndShowMoveButtons) {
container.find(".sortable-up, .sortable-down").addClass("sr-only").on("click", function () {
sessionStorage.dndShowMoveButtons = 'true';
});
}
if (container.find("[data-dnd-id]").length < 2 && !container.data("dnd-group")) {
handle.addClass("disabled");
return;
}
function maybeShowSortButtons() {
if (Date.now() - lastClick < 3000) {
$("[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down").removeClass("sr-only");
updateAllSortButtonStates();
}
lastClick = Date.now();
}
container.find(".dnd-sort-handle").on("mouseup", maybeShowSortButtons);
const group = container.data("dnd-group");
const containers = group ? container.parent().find('[data-dnd-group="' + group + '"]') : container;
Sortable.create(container.get(0), {
filter: ".sortable-disabled",
handle: ".dnd-sort-handle",
group: group,
onMove: function (evt) {
return evt.related.className.indexOf('sortable-disabled') === -1;
},
onStart: function (evt) {
containers.addClass("sortable-dragarea");
container.parent().addClass("sortable-sorting");
didSort = false;
},
onEnd: function (evt) {
containers.removeClass("sortable-dragarea");
container.parent().removeClass("sortable-sorting");
if (!didSort) {
maybeShowSortButtons();
} else {
$("[data-dnd-url] .sortable-up, [data-dnd-url] .sortable-down").addClass("sr-only");
delete sessionStorage.dndShowMoveButtons;
}
},
onSort: function (evt){
if (evt.target !== evt.to) return;
didSort = true;
const ids = container.find('[data-dnd-id]').toArray().map(function (e) {
return e.dataset.dndId
})
$.ajax(
{
type: 'POST',
url: url,
headers: { 'X-CSRFToken': $('input[name=csrfmiddlewaretoken]').val() },
data: JSON.stringify({
ids: ids
}),
contentType: 'application/json',
timeout: 30000
}
)
}
})
})
})
const ids = container.find("[data-dnd-id]").toArray().map(function (e) { return e.dataset.dndId; });
$.ajax(
{
'type': 'POST',
'url': url,
'headers': {'X-CSRFToken': $("input[name=csrfmiddlewaretoken]").val()},
'data': JSON.stringify({
ids: ids
}),
'contentType': "application/json",
'timeout': 30000
}
);
}
});
});
});
File diff suppressed because it is too large Load Diff
@@ -1,3 +1,3 @@
$(function () {
$('input, select, textarea').not(':disabled').focus()
})
$("input, select, textarea").not(":disabled").focus();
});
+182 -179
View File
@@ -1,198 +1,201 @@
/* globals $ */
/*globals $*/
$(document).on('pretix:bind-forms', function () {
function cleanup (l) {
return $.trim(l.replace(/\n/g, ', '))
}
function combine ($sel) {
let parts = [
$sel.filter('[name*=street]').val(),
$sel.filter('[name*=zipcode]').val(),
$sel.filter('[name*=city]').val(),
$sel.filter('[name*=state]').val(),
$sel.filter('[name*=country]').find('option:selected').text(),
$sel.filter('[name*=location]').val(),
]
let res = ''
for (let val of parts) {
if (val) {
if (res) {
res += ', '
}
res += val
}
}
return cleanup(res)
}
$('.geodata-section').each(function () {
// Geocoding
// detach notifications and append them to first label (should be from location)
let $notifications = $('.geodata-autoupdate', this).detach().appendTo($('label', this).first())
let $lat = $('input[name$=geo_lat]', this).first()
let $lon = $('input[name$=geo_lon]', this).first()
let lat
let lon
let $updateButton = $('[data-action=update]', this)
$(document).on("pretix:bind-forms", function () {
function cleanup(l) {
return $.trim(l.replace(/\n/g, ", "));
}
function combine($sel) {
var parts = [
$sel.filter("[name*=street]").val(),
$sel.filter("[name*=zipcode]").val(),
$sel.filter("[name*=city]").val(),
$sel.filter("[name*=state]").val(),
$sel.filter("[name*=country]").find("option:selected").text(),
$sel.filter("[name*=location]").val(),
]
var res = "";
for (var val of parts) {
if (val) {
if (res) {
res += ", "
}
res += val
}
}
return cleanup(res)
}
$(".geodata-section").each(function () {
// Geocoding
// detach notifications and append them to first label (should be from location)
var $notifications = $(".geodata-autoupdate", this).detach().appendTo($("label", this).first());
var $lat = $("input[name$=geo_lat]", this).first();
var $lon = $("input[name$=geo_lon]", this).first();
var lat;
var lon;
var $updateButton = $("[data-action=update]", this);
let $location
// The .geodata-section is expected to include either...
// ... an English "location" field
if ($('textarea[lang=en], input[lang=en]', this).length) {
$location = $('textarea[lang=en], input[lang=en], select', this).not('[name*=geo_]')
}
var $location;
// The .geodata-section is expected to include either...
// ... an English "location" field
if ($("textarea[lang=en], input[lang=en]", this).length) {
$location = $("textarea[lang=en], input[lang=en], select", this).not("[name*=geo_]");
}
// ... a "location" field in any other language
if (!$location || !$location.length) {
let lang = $('textarea, input[type=text]', this).not('[name*=geo_]').first().attr('lang')
if (lang) {
$location = $('textarea[lang=' + lang + '], input[lang=' + lang + '], select', this)
}
}
// ... a "location" field in any other language
if (!$location || !$location.length) {
var lang = $("textarea, input[type=text]", this).not("[name*=geo_]").first().attr("lang");
if (lang) {
$location = $("textarea[lang=" + lang + "], input[lang=" + lang + "], select", this);
}
}
// ... or a set of fields like a full address form
if (!$location || !$location.length) {
$location = $('textarea, input, select', this).not('[name*=geo_]')
}
// ... or a set of fields like a full address form
if (!$location || !$location.length) {
$location = $("textarea, input, select", this).not("[name*=geo_]");
}
if (!$lat.length || !$lon.length || !$location.length) {
return
}
if (!$lat.length || !$lon.length || !$location.length) {
return;
}
let debounceLoad, debounceLatLonChange, delayUpdateDismissal
let touched = $lat.val() !== ''
let xhr
let lastLocation = combine($location)
var debounceLoad, debounceLatLonChange, delayUpdateDismissal;
var touched = $lat.val() !== "";
var xhr;
var lastLocation = combine($location);
function load () {
window.clearTimeout(debounceLoad)
if (xhr) {
xhr.abort()
xhr = null
}
function load() {
window.clearTimeout(debounceLoad);
if (xhr) {
xhr.abort();
xhr = null;
}
let q = combine($location)
if (q === '' || q === lastLocation) return
var q = combine($location);
if (q === "" || q === lastLocation) return;
lastLocation = q
$notifications.attr('data-notify', 'loading')
lastLocation = q;
$notifications.attr("data-notify", "loading");
xhr = $.getJSON('/control/geocode/?q=' + encodeURIComponent(q), function (res) {
if (!res.results || !res.results.length) {
$notifications.attr('data-notify', 'error')
return
}
xhr = $.getJSON('/control/geocode/?q=' + encodeURIComponent(q), function (res) {
if (!res.results || !res.results.length) {
$notifications.attr("data-notify", "error");
return;
}
lat = res.results[0].lat
lon = res.results[0].lon
if ($lat.val() == lat && $lon.val() == lon) {
$notifications.attr('data-notify', '')
} else if (touched) {
$notifications.attr('data-notify', 'confirm')
} else {
$notifications.attr('data-notify', '')
$lat.val(lat)
$lon.val(lon)
center(13)
}
})
}
lat = res.results[0].lat;
lon = res.results[0].lon;
if ($lat.val() == lat && $lon.val() == lon) {
$notifications.attr("data-notify", "");
}
else if (touched) {
$notifications.attr("data-notify", "confirm");
}
else {
$notifications.attr("data-notify", "");
$lat.val(lat);
$lon.val(lon);
center(13);
}
})
}
$lat.add($lon).change(function () {
if (this.value !== '') touched = true
center(13)
}).keyup(function () {
window.clearTimeout(debounceLatLonChange)
debounceLatLonChange = window.setTimeout(center, 300)
})
$lat.add($lon).change(function () {
if (this.value !== "") touched = true;
center(13);
}).keyup(function () {
window.clearTimeout(debounceLatLonChange);
debounceLatLonChange = window.setTimeout(center, 300);
});
$location.change(load)
$location.keyup(function () {
window.clearTimeout(debounceLoad)
debounceLoad = window.setTimeout(load, 1000)
if ($notifications.attr('data-notify') == 'confirm' && lastLocation !== cleanup(this.value)) $notifications.attr('data-notify', '')
})
$location.change(load);
$location.keyup(function () {
window.clearTimeout(debounceLoad);
debounceLoad = window.setTimeout(load, 1000);
if ($notifications.attr("data-notify") == "confirm" && lastLocation !== cleanup(this.value)) $notifications.attr("data-notify", "");
});
$updateButton.click(function () {
$lat.val(lat)
$lon.val(lon).trigger('change')// change-event is needed by bulk-edit
touched = false
center(13)
$notifications.attr('data-notify', 'updated')
delayUpdateDismissal = window.setTimeout(function () {
if ($notifications.attr('data-notify') == 'updated') $notifications.attr('data-notify', '')
}, 2500)
})
$updateButton.click(function() {
$lat.val(lat);
$lon.val(lon).trigger("change");// change-event is needed by bulk-edit
touched = false;
center(13);
$notifications.attr("data-notify", "updated");
delayUpdateDismissal = window.setTimeout(function() {
if ($notifications.attr("data-notify") == "updated") $notifications.attr("data-notify", "");
}, 2500);
});
// Map
let $grp = $('.geodata-group', this)
let tiles = $grp.attr('data-tiles')
let attrib = $grp.attr('data-attrib')
if (tiles) {
let $map = $('<div>')
$grp.append($('<div>').addClass('col-md-9 col-md-offset-3').append($map))
let map = L.map($map.get(0))
L.tileLayer(tiles, {
attribution: attrib,
maxZoom: 18,
}).addTo(map)
// Map
var $grp = $(".geodata-group", this);
var tiles = $grp.attr("data-tiles");
var attrib = $grp.attr("data-attrib");
if (tiles) {
var $map = $("<div>");
$grp.append($("<div>").addClass("col-md-9 col-md-offset-3").append($map));
var map = L.map($map.get(0));
L.tileLayer(tiles, {
attribution: attrib,
maxZoom: 18,
}).addTo(map);
function getpoint () {
if ($lat.val() !== '' && $lon.val() !== '') {
let p = [parseFloat($lat.val().replace(',', '.')), parseFloat($lon.val().replace(',', '.'))]
// Clip to valid ranges. Very invalid lon/lat values can even lead to browser crashes in leaflet apparently
if (p[0] < -90) p[0] = -90
if (p[0] > 90) p[0] = 90
if (p[1] < -180) p[1] = -180
if (p[1] > 180) p[1] = 180
return p
} else {
return [0.0, 0.0]
}
}
function getpoint() {
if ($lat.val() !== "" && $lon.val() !== "") {
var p = [parseFloat($lat.val().replace(",", ".")), parseFloat($lon.val().replace(",", "."))];
// Clip to valid ranges. Very invalid lon/lat values can even lead to browser crashes in leaflet apparently
if (p[0] < -90) p[0] = -90
if (p[0] > 90) p[0] = 90
if (p[1] < -180) p[1] = -180
if (p[1] > 180) p[1] = 180
return p
} else {
return [0.0, 0.0];
}
}
let marker = L.marker(getpoint(), {
draggable: 'true',
icon: L.icon({
iconUrl: $grp.attr('data-icon'),
shadowUrl: $grp.attr('data-shadow'),
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41]
})
})
marker.addTo(map)
marker.on('dragend', function (event) {
let position = marker.getLatLng()
marker.setLatLng(position, {
draggable: 'true'
}).bindPopup(position).update()
$lat.val(position.lat.toFixed(7))
$lon.val(position.lng.toFixed(7))
touched = true
center(null)
})
var marker = L.marker(getpoint(), {
draggable: 'true',
icon: L.icon({
iconUrl: $grp.attr("data-icon"),
shadowUrl: $grp.attr("data-shadow"),
iconSize: [25, 41],
iconAnchor: [12, 41],
popupAnchor: [1, -34],
tooltipAnchor: [16, -28],
shadowSize: [41, 41]
})
});
marker.addTo(map);
marker.on("dragend", function (event) {
var position = marker.getLatLng();
marker.setLatLng(position, {
draggable: 'true'
}).bindPopup(position).update();
$lat.val(position.lat.toFixed(7));
$lon.val(position.lng.toFixed(7));
touched = true;
center(null);
});
function center (zoom) {
if ($lat.val() !== '' && $lon.val() !== '') {
if (zoom) {
map.setView(getpoint(), zoom)
} else {
map.panTo(getpoint())
}
marker.setLatLng(getpoint(), {
draggable: 'true'
}).bindPopup(getpoint()).update()
} else {
map.fitWorld()
}
}
function center(zoom) {
if ($lat.val() !== "" && $lon.val() !== "") {
if (zoom) {
map.setView(getpoint(), zoom);
} else {
map.panTo(getpoint());
}
marker.setLatLng(getpoint(), {
draggable: 'true'
}).bindPopup(getpoint()).update();
} else {
map.fitWorld();
}
}
center(13)
} else {
function center (zoom) {
}
}
})
})
center(13);
} else {
function center(zoom) {
}
}
});
});
@@ -1,39 +1,39 @@
$(function () {
hideDeselected(false)
hideDeselected(false);
function hideDeselected (animate) {
let v = $('input[name=\'quota_option\']:checked').val(),
fn = animate ? 'slideDown' : 'show'
if (v === 'existing') {
hideAll(animate)
$('#existing-quota-group').children()[fn]()
} else if (v === 'new') {
hideAll(animate)
if ($('#id_quota_add_new_name').val() === '') {
$('#id_quota_add_new_name').val($('input[name^=name_]').first().val())
}
$('#new-quota-group').children()[fn]()
} else {
hideAll(animate)
}
}
function hideDeselected(animate) {
var v = $("input[name='quota_option']:checked").val(),
fn = animate ? 'slideDown' : 'show';
if (v === "existing") {
hideAll(animate);
$("#existing-quota-group").children()[fn]();
} else if (v === "new") {
hideAll(animate);
if ($("#id_quota_add_new_name").val() === "") {
$("#id_quota_add_new_name").val($("input[name^=name_]").first().val());
}
$("#new-quota-group").children()[fn]();
} else {
hideAll(animate);
}
}
function hideAll (animate) {
let fn = animate ? 'slideUp' : 'hide'
$('#new-quota-group').children()[fn]()
$('#existing-quota-group').children()[fn]()
}
function hideAll(animate) {
var fn = animate ? 'slideUp' : 'hide';
$("#new-quota-group").children()[fn]();
$("#existing-quota-group").children()[fn]();
}
$('input[name=\'quota_option\']').on('change',
function () {
hideDeselected(true)
}
)
$("input[name='quota_option']").on('change',
function () {
hideDeselected(true);
}
);
function toggleblock () {
$('#new-quota-group').closest('fieldset').toggle(!$('[name=has_variations][value=on]').prop('checked'))
}
function toggleblock() {
$("#new-quota-group").closest('fieldset').toggle(!$("[name=has_variations][value=on]").prop('checked'));
}
$('[name=has_variations]').change(toggleblock)
toggleblock()
})
$("[name=has_variations]").change(toggleblock);
toggleblock();
});
+65 -65
View File
@@ -1,75 +1,75 @@
/* global gettext */
function preview_task_callback (data, _jqXHR, _status) {
'use strict'
if (data.item) {
$('#' + data.item + '_panel').data('ajaxing', false)
for (let m in data.msgs) {
let target = $('div[for=' + data.item + '][lang=' + m + ']')
if (target.length === 1) {
target.html(data.msgs[m])
target.find('.placeholder').tooltip()
}
}
}
function preview_task_callback(data, jqXHR, status) {
"use strict";
if (data.item) {
$('#' + data.item + '_panel').data('ajaxing', false);
for (var m in data.msgs){
var target = $('div[for=' + data.item + '][lang=' + m +']');
if (target.length === 1){
target.html(data.msgs[m]);
target.find('.placeholder').tooltip();
}
}
}
}
function preview_task_error (item) {
'use strict'
return function (jqXHR, textStatus, _errorThrown) {
$('#' + item + '_panel').data('ajaxing', false)
$('#' + item + '_preview div').text(gettext('An error has occurred.'))
if (textStatus === 'timeout') {
alert(gettext('The request took too long. Please try again.'))
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status))
} else {
alert(gettext('We currently cannot reach the server. Please try again. '
+ 'Error code: {code}').replace(/\{code\}/, jqXHR.status))
}
}
}
function preview_task_error(item) {
"use strict";
return function(jqXHR, textStatus, errorThrown) {
$('#' + item + '_panel').data('ajaxing', false);
$('#' + item + '_preview div').text(gettext('An error has occurred.'));
if (textStatus === "timeout") {
alert(gettext("The request took too long. Please try again."));
} else {
if (jqXHR.status >= 400 && jqXHR.status < 500) {
alert(gettext('An error of type {code} occurred.').replace(/\{code\}/, jqXHR.status));
} else {
alert(gettext('We currently cannot reach the server. Please try again. ' +
'Error code: {code}').replace(/\{code\}/, jqXHR.status));
}
}
}
}
function mail_preview_setup ($el) {
$el.find('.mail-preview .placeholder').tooltip()
$el.find('a[type=preview]').on('click', function () {
let itemName = $(this).closest('.preview-panel').attr('for')
if ($('#' + itemName + '_panel').data('ajaxing') || $(this).parent('.active').length !== 0) {
return
}
function mail_preview_setup($el) {
$el.find('.mail-preview .placeholder').tooltip();
$el.find('a[type=preview]').on('click', function () {
var itemName = $(this).closest('.preview-panel').attr('for');
if ($('#' + itemName + '_panel').data('ajaxing') || $(this).parent('.active').length !== 0) {
return;
}
// gathering data
let parentForm = $(this).closest('form')
let previewUrl = $(parentForm).attr('mail-preview-url')
let token = $(parentForm).find('input[name=csrfmiddlewaretoken]').val()
let dataString = 'item=' + itemName + '&csrfmiddlewaretoken=' + token
$('#' + itemName + '_edit textarea, #' + itemName + '_edit input').each(function () {
dataString += '&' + $(this).serialize()
})
// gathering data
var parentForm = $(this).closest('form');
var previewUrl = $(parentForm).attr('mail-preview-url');
var token = $(parentForm).find('input[name=csrfmiddlewaretoken]').val();
var dataString = 'item=' + itemName + '&csrfmiddlewaretoken=' + token;
$('#' + itemName + '_edit textarea, #' + itemName + '_edit input').each(function () {
dataString += '&' + $(this).serialize();
});
// prepare for ajax
$('#' + itemName + '_panel').data('ajaxing', true)
$('#' + itemName + '_preview div').text(gettext('Generating messages …'))
// prepare for ajax
$('#' + itemName + '_panel').data('ajaxing', true);
$('#' + itemName + '_preview div').text(gettext('Generating messages …'));
$.ajax(
{
type: 'POST',
url: previewUrl,
data: dataString,
success: preview_task_callback,
error: preview_task_error(itemName),
dataType: 'json',
timeout: 60000,
}
)
})
$.ajax(
{
'type': 'POST',
'url': previewUrl,
'data': dataString,
'success': preview_task_callback,
'error': preview_task_error(itemName),
'dataType': 'json',
'timeout': 60000,
}
);
});
}
$(function () {
'use strict'
mail_preview_setup($('body'))
$(document).on('pretix:bind-forms', function () {
mail_preview_setup($('body'))
})
})
"use strict";
mail_preview_setup($("body"));
$(document).on("pretix:bind-forms", function () {
mail_preview_setup($("body"));
});
});
File diff suppressed because it is too large Load Diff
@@ -1,62 +1,59 @@
/* global gettext */
/*global $, gettext*/
$(function () {
if (!$('.form-order-change').length) {
return
}
$('.form-order-change').each(function () {
let url = $(this).attr('data-pricecalc-endpoint')
let $itemvar = $(this).find('[name*=itemvar]')
let $subevent = $(this).find('[name*=subevent]')
let $tax_rule = $(this).find('[name*=tax_rule]')
let $price = $(this).find('[name*=price]')
let update_price = function () {
console.log(url)
let itemvar = $itemvar.val()
let item
let variation = null
if (itemvar.indexOf('-')) {
item = parseInt(itemvar.split('-')[0])
variation = parseInt(itemvar.split('-')[1])
} else {
item = parseInt(itemvar)
}
$price.closest('.field-container').append('<small class="loading-indicator"><span class="fa fa-cog fa-spin"></span> '
+ gettext('Calculating default price…') + '</small>')
$.ajax(
{
type: 'POST',
url: url,
headers: { 'X-CSRFToken': $('input[name=csrfmiddlewaretoken]').val() },
data: JSON.stringify({
item: item,
variation: variation,
subevent: $subevent.val(),
tax_rule: $tax_rule.val(),
locale: $('body').attr('data-pretixlocale'),
}),
contentType: 'application/json',
success: function (data) {
$price.val(data.gross_formatted)
$tax_rule.val(data.tax_rule)
$price.closest('.field-container').find('.loading-indicator').remove()
},
// 'error': …
context: this,
dataType: 'json',
timeout: 30000
}
)
}
$itemvar.on('change', function () {
$tax_rule.val(null)
update_price()
})
$tax_rule.on('change', update_price)
$subevent.on('change', update_price).on('change', function () {
let seat = $(this).closest('.form-order-change').find('[id$=seat]')
if (seat.length) {
seat.prop('required', !!$subevent.val())
}
})
})
})
if (!$(".form-order-change").length) {
return;
}
$(".form-order-change").each(function () {
var url = $(this).attr("data-pricecalc-endpoint");
var $itemvar = $(this).find("[name*=itemvar]");
var $subevent = $(this).find("[name*=subevent]");
var $tax_rule = $(this).find("[name*=tax_rule]");
var $price = $(this).find("[name*=price]");
var update_price = function () {
console.log(url);
var itemvar = $itemvar.val();
var item = null;
var variation = null;
if (itemvar.indexOf("-")) {
item = parseInt(itemvar.split("-")[0]);
variation = parseInt(itemvar.split("-")[1]);
} else {
item = parseInt(itemvar);
}
$price.closest(".field-container").append("<small class=\"loading-indicator\"><span class=\"fa fa-cog fa-spin\"></span> " +
gettext("Calculating default price…") + "</small>");
$.ajax(
{
'type': 'POST',
'url': url,
'headers': {'X-CSRFToken': $("input[name=csrfmiddlewaretoken]").val()},
'data': JSON.stringify({
'item': item,
'variation': variation,
'subevent': $subevent.val(),
'tax_rule': $tax_rule.val(),
'locale': $("body").attr("data-pretixlocale"),
}),
'contentType': "application/json",
'success': function (data) {
$price.val(data.gross_formatted);
$tax_rule.val(data.tax_rule);
$price.closest(".field-container").find(".loading-indicator").remove();
},
// 'error': …
'context': this,
'dataType': 'json',
'timeout': 30000
}
);
};
$itemvar.on("change", function () { $tax_rule.val(null); update_price() });
$tax_rule.on("change", update_price);
$subevent.on("change", update_price).on("change", function () {
var seat = $(this).closest(".form-order-change").find("[id$=seat]");
if (seat.length) {
seat.prop("required", !!$subevent.val());
}
});
});
});
@@ -1,43 +1,43 @@
function is_sandbox_supported () {
const iframe = document.createElement('iframe')
return 'sandbox' in iframe
function is_sandbox_supported() {
const iframe = document.createElement('iframe');
return 'sandbox' in iframe;
}
function safe_render (url, parent) {
// Estimate the height that prevents the user from having to scroll on two levels to see the full email
const height = (
Math.max(400, window.innerHeight - parent.parent().get(0).getBoundingClientRect().top - document.querySelector('footer').getBoundingClientRect().height - 20)
) + 'px'
function safe_render(url, parent) {
// Estimate the height that prevents the user from having to scroll on two levels to see the full email
const height = (
Math.max(400, window.innerHeight - parent.parent().get(0).getBoundingClientRect().top - document.querySelector("footer").getBoundingClientRect().height - 20)
) + "px";
const iframe = (
// Per the HTML spec, a data: URL in an iframe is treated as its own origin:
// https://github.com/whatwg/html/pull/1756
// It is unclear, if Firefox complies, and the behaviour around data URLs is quite wild:
// https://github.com/whatwg/html/issues/12091
// Together with the sandbox attribute disallowing all JavaScript, and the fact
// that we sanitize the HTML before we even save it to the database, this should
// still be the safest way to render HTML in the context of our backend.
$('<iframe>')
.height(height)
.attr('class', 'html-email')
.attr('src', url)
.attr('sandbox', 'allow-popups allow-popups-to-escape-sandbox')
.attr('csp', 'script-src \'none\'; font-src \'none\'; connect-src \'none\'; form-action \'none\'; style-src \'unsafe-inline\'') // respected only by chrome
.prop('credentialless', true) // respected only by chrome
)
const iframe = (
// Per the HTML spec, a data: URL in an iframe is treated as its own origin:
// https://github.com/whatwg/html/pull/1756
// It is unclear, if Firefox complies, and the behaviour around data URLs is quite wild:
// https://github.com/whatwg/html/issues/12091
// Together with the sandbox attribute disallowing all JavaScript, and the fact
// that we sanitize the HTML before we even save it to the database, this should
// still be the safest way to render HTML in the context of our backend.
$("<iframe>")
.height(height)
.attr("class", "html-email")
.attr("src", url)
.attr("sandbox", "allow-popups allow-popups-to-escape-sandbox")
.attr("csp", "script-src 'none'; font-src 'none'; connect-src 'none'; form-action 'none'; style-src 'unsafe-inline'") // respected only by chrome
.prop("credentialless", true) // respected only by chrome
);
console.log(parent, iframe)
parent.append(iframe)
console.log(parent, iframe);
parent.append(iframe);
}
$(function () {
const script_element = $('#mail_body_html')
if (!script_element.length) return
if (!is_sandbox_supported()) {
// Browser is too old for <iframe sandbox>
$(script_element.parent()).text('Please switch to a modern browser to view HTML content safely.')
return
}
const script_element = $("#mail_body_html");
if (!script_element.length) return;
if (!is_sandbox_supported()) {
// Browser is too old for <iframe sandbox>
$(script_element.parent()).text("Please switch to a modern browser to view HTML content safely.");
return;
}
safe_render(JSON.parse(script_element.html()), script_element.parent())
})
safe_render(JSON.parse(script_element.html()), script_element.parent());
});
@@ -1,89 +1,84 @@
/* global gettext */
$(function () {
let plugins = $('.plugin-container').toArray().map(function (el) {
return {
sortName: el.getAttribute('data-plugin-name').toLowerCase().replace(/pretix /g, ''),
name: el.getAttribute('data-plugin-name').toLowerCase(),
module: el.getAttribute('data-plugin-module').toLowerCase(),
description: $(el).find('.plugin-description').text().toLowerCase(),
html: el.outerHTML,
category: $(el).closest('[data-plugin-category]').attr('data-plugin-category'),
categoryLabel: $(el).closest('[data-plugin-category]').attr('data-plugin-category-label'),
active: !!$(el).has('[data-is-active]').length,
}
})
function SearchMatcher (term, fields) {
this.searchFor = term.toLowerCase().split(/\s+/)
this.fields = fields
}
function inStringRanked (haystack, needle) {
let pos = -1, rank = 0
do {
pos = haystack.indexOf(needle, pos + 1)
if (pos !== -1) rank = 10
if (pos === 0 || haystack.charCodeAt(pos - 1) <= 47)
return 15 // string start or word start (=char before match is special char)
} while (pos !== -1)
return rank
}
SearchMatcher.prototype.isMatch = function (obj) {
let rank = 0
for (let j = 0; j < this.searchFor.length; j++) {
let searchFor = this.searchFor[j]
for (let i = this.fields.length - 1; i >= 0; i--) {
let result = inStringRanked(obj[this.fields[i]], searchFor)
if (result) {
rank += (i + 1) * result
break
}
}
}
return rank
}
function strcmp (a, b) {
return a > b ? 1 : a < b ? -1 : 0
}
let $results_box = $('#plugin_search_results')
let $plugin_tabs = $('#plugin_tabs')
let $results = $('#plugin_search_results .plugin-list')
function search () {
$results.html('')
let value = $('#plugin_search_input').val()
let only_active = $('input[name=plugin_state_filter][value=active]').prop('checked')
if (!value && !only_active) {
$results_box.hide()
$plugin_tabs.show()
return
}
$results_box.show()
$plugin_tabs.hide()
let matcher = new SearchMatcher(value, ['description', 'module', 'name'])
let matches = []
for (const plugin of plugins) {
if (only_active && !plugin.active)
continue
let rank = matcher.isMatch(plugin)
if (!rank)
continue
matches.push([rank, plugin])
}
matches.sort(function (a, b) { return (b[0] - a[0]) || strcmp(a[1].sortName, b[1].sortName) })
$results.append(matches.map(function (res) { return $(res[1].html).prepend('<span class="pull-right">' + res[1].categoryLabel + '</span>') }))
$results.find('.panel-body, .panel, .featured-plugin, .btn-lg').removeClass('panel-body panel featured-plugin btn-lg')
if (matches.length === 0) {
$results.append(gettext('No results'))
}
}
$('#plugin_search_input').on('input', search)
$('input[name=plugin_state_filter]').on('change', search)
$results_box.find('button.close').on('click', function () {
$('input[name=plugin_state_filter][value=all]').prop('checked', true).trigger('click')
$('#plugin_search_input').val('').trigger('input')
})
if (location.search) {
var search = new URLSearchParams(location.search)
if (search.has('q')) {
$('#plugin_search_input').val(search.get('q')).trigger('input')
}
}
$(function() {
var plugins = $(".plugin-container").toArray().map(function(el) {
return {
sortName: el.getAttribute('data-plugin-name').toLowerCase().replace(/pretix /g, ''),
name: el.getAttribute('data-plugin-name').toLowerCase(),
module: el.getAttribute('data-plugin-module').toLowerCase(),
description: $(el).find('.plugin-description').text().toLowerCase(),
html: el.outerHTML,
category: $(el).closest('[data-plugin-category]').attr('data-plugin-category'),
categoryLabel: $(el).closest('[data-plugin-category]').attr('data-plugin-category-label'),
active: !!$(el).has('[data-is-active]').length,
}
});
function SearchMatcher(term, fields) {
this.searchFor = term.toLowerCase().split(/\s+/);
this.fields = fields;
}
function inStringRanked(haystack, needle) {
let pos = -1, rank = 0;
do {
pos = haystack.indexOf(needle, pos + 1);
if (pos !== -1) rank = 10;
if (pos === 0 || haystack.charCodeAt(pos - 1) <= 47)
return 15; // string start or word start (=char before match is special char)
} while (pos !== -1);
return rank;
}
SearchMatcher.prototype.isMatch = function(obj) {
let rank = 0;
for(let j = 0; j < this.searchFor.length; j++) {
var searchFor = this.searchFor[j];
for(let i = this.fields.length - 1; i >= 0; i--) {
var result = inStringRanked(obj[this.fields[i]], searchFor);
if (result) {
rank += (i + 1) * result;
break;
}
}
}
return rank;
}
function strcmp(a, b) {
return a > b ? 1 : a < b ? -1 : 0;
}
var $results_box = $("#plugin_search_results");
var $plugin_tabs = $("#plugin_tabs");
var $results = $("#plugin_search_results .plugin-list");
function search() {
$results.html("");
var value = $("#plugin_search_input").val();
var only_active = $("input[name=plugin_state_filter][value=active]").prop("checked");
if (!value && !only_active) {
$results_box.hide(); $plugin_tabs.show();
return;
}
$results_box.show(); $plugin_tabs.hide();
var matcher = new SearchMatcher(value, ["description", "module", "name"]);
var matches = [];
for(const plugin of plugins) {
if (only_active && !plugin.active) continue;
var rank = matcher.isMatch(plugin);
if (!rank) continue;
matches.push([rank, plugin]);
}
matches.sort(function (a,b) { return (b[0]-a[0]) || strcmp(a[1].sortName, b[1].sortName); })
$results.append(matches.map(function(res) { return $(res[1].html).prepend('<span class="pull-right">' + res[1].categoryLabel + '</span>'); }))
$results.find(".panel-body, .panel, .featured-plugin, .btn-lg").removeClass("panel-body panel featured-plugin btn-lg");
if (matches.length === 0) {
$results.append(gettext("No results"));
}
}
$("#plugin_search_input").on("input", search);
$("input[name=plugin_state_filter]").on("change", search);
$results_box.find("button.close").on("click", function() {
$("input[name=plugin_state_filter][value=all]").prop("checked", true).trigger("click");
$("#plugin_search_input").val("").trigger("input");
});
if (location.search) {
var search = new URLSearchParams(location.search);
if (search.has('q')) {
$("#plugin_search_input").val(search.get("q")).trigger("input");
}
}
})
+137 -137
View File
@@ -1,156 +1,156 @@
/* global Morris, gettext, apiGET, i18nToString */
/*global $, Morris, gettext*/
$(function () {
// Question view
if (!$('#question_chart').length) {
return
}
// Question view
if (!$("#question_chart").length) {
return;
}
$('.chart').css('height', '250px')
let data_type = $('#question_chart').attr('data-type'),
data = JSON.parse($('#question-chart-data').text() || '[]'),
others_sum = 0,
max_num = 8
$(".chart").css("height", "250px");
var data_type = $("#question_chart").attr("data-type"),
data = JSON.parse($("#question-chart-data").text() || "[]"),
others_sum = 0,
max_num = 8;
data = data.map(function (d) {
return {
value: d.count,
label: d.answer.length > 20 ? d.answer.substring(0, 20) + '…' : d.answer,
}
})
data = data.map(function (d) {
return {
'value': d.count,
'label': d.answer.length > 20 ? d.answer.substring(0, 20) + '…' : d.answer,
}
});
if (data_type == 'N') {
// Sort
data.sort(function (a, b) {
if (parseFloat(a.label) > parseFloat(b.label)) {
return 1
} else if (parseFloat(a.label) < parseFloat(b.label)) {
return -1
} else {
return 0
}
})
max_num = 20
}
if (data_type == 'N') {
// Sort
data.sort(function (a, b) {
if (parseFloat(a.label) > parseFloat(b.label)) {
return 1;
} else if (parseFloat(a.label) < parseFloat(b.label)) {
return -1;
} else {
return 0;
}
});
max_num = 20;
}
// Limit shown options
if (data.length > max_num) {
for (let i = max_num; i < data.length; i++) {
others_sum += data[i].value
}
data = data.slice(0, max_num)
data.push({ value: others_sum, label: gettext('Others') })
}
// Limit shown options
if (data.length > max_num) {
for (var i = max_num; i < data.length; i++) {
others_sum += data[i].value;
}
data = data.slice(0, max_num);
data.push({'value': others_sum, 'label': gettext('Others')});
}
if (data_type === 'B') {
let colors
if (data[0].answer_bool) {
colors = ['#50A167', '#C44F4F']
} else {
colors = ['#C44F4F', '#50A167']
}
new Morris.Donut({
element: 'question_chart',
data: data,
resize: true,
colors: colors
})
} else if (data_type === 'C') {
new Morris.Donut({
element: 'question_chart',
data: data,
resize: true,
colors: [
'#7F4A91',
'#50A167',
'#FFB419',
'#5F9CD4',
'#C44F4F',
'#83FFFA',
'#FF6C38',
'#1f5b8e',
'#2d683c',
]
})
} else { // M, N, S, T
new Morris.Bar({
element: 'question_chart',
data: data,
resize: true,
xkey: 'label',
ykeys: ['value'],
labels: [gettext('Count')]
})
}
if (data_type === 'B') {
var colors;
if (data[0].answer_bool) {
colors = ['#50A167', '#C44F4F'];
} else {
colors = ['#C44F4F', '#50A167'];
}
new Morris.Donut({
element: 'question_chart',
data: data,
resize: true,
colors: colors
});
} else if (data_type === 'C') {
new Morris.Donut({
element: 'question_chart',
data: data,
resize: true,
colors: [
'#7F4A91',
'#50A167',
'#FFB419',
'#5F9CD4',
'#C44F4F',
'#83FFFA',
'#FF6C38',
'#1f5b8e',
'#2d683c',
]
});
} else { // M, N, S, T
new Morris.Bar({
element: 'question_chart',
data: data,
resize: true,
xkey: 'label',
ykeys: ['value'],
labels: [gettext('Count')]
});
}
// N, S, T
})
// N, S, T
});
$(function () {
// Question editor
// Question editor
if (!$('#answer-options').length) {
return
}
if (!$("#answer-options").length) {
return;
}
// Question editor
$('#id_type').change(question_page_toggle_view)
$('#id_required').change(question_page_toggle_view)
question_page_toggle_view()
// Question editor
$("#id_type").change(question_page_toggle_view);
$("#id_required").change(question_page_toggle_view);
question_page_toggle_view();
function question_page_toggle_view () {
let show = $('#id_type').val() == 'C' || $('#id_type').val() == 'M'
$('#answer-options').toggle(show)
function question_page_toggle_view() {
var show = $("#id_type").val() == "C" || $("#id_type").val() == "M";
$("#answer-options").toggle(show);
$('#valid-date').toggle($('#id_type').val() == 'D')
$('#valid-datetime').toggle($('#id_type').val() == 'W')
$('#valid-string').toggle($('#id_type').val() == 'T' || $('#id_type').val() == 'S')
$('#valid-number').toggle($('#id_type').val() == 'N')
$('#valid-file').toggle($('#id_type').val() == 'F')
$("#valid-date").toggle($("#id_type").val() == "D");
$("#valid-datetime").toggle($("#id_type").val() == "W");
$("#valid-string").toggle($("#id_type").val() == "T" || $("#id_type").val() == "S");
$("#valid-number").toggle($("#id_type").val() == "N");
$("#valid-file").toggle($("#id_type").val() == "F");
show = $('#id_type').val() == 'B' && $('#id_required').prop('checked')
$('.alert-required-boolean').toggle(show)
}
show = $("#id_type").val() == "B" && $("#id_required").prop("checked");
$(".alert-required-boolean").toggle(show);
}
let $val = $('#id_dependency_values')
let $dq = $('#id_dependency_question')
let oldval = JSON.parse($('#dependency_value_val').text())
function update_dependency_options () {
$val.parent().find('.loading-indicator').remove()
$('#id_dependency_values option').remove()
$('#id_dependency_values').prop('required', false)
var $val = $("#id_dependency_values");
var $dq = $("#id_dependency_question");
var oldval = JSON.parse($("#dependency_value_val").text());
function update_dependency_options() {
$val.parent().find(".loading-indicator").remove();
$("#id_dependency_values option").remove();
$("#id_dependency_values").prop("required", false);
let val = $dq.children('option:selected').val()
if (!val) {
$('#id_dependency_values').show()
$val.show()
return
}
var val = $dq.children("option:selected").val();
if (!val) {
$("#id_dependency_values").show();
$val.show();
return;
}
$('#id_dependency_values').prop('required', true)
$val.hide()
$val.parent().append('<div class="help-block loading-indicator"><span class="fa'
+ ' fa-cog fa-spin"></span></div>')
$("#id_dependency_values").prop("required", true);
$val.hide();
$val.parent().append("<div class=\"help-block loading-indicator\"><span class=\"fa" +
" fa-cog fa-spin\"></span></div>");
// the container_type parameter is undocumented. this API is going to change in a later release.
apiGET('/api/v1/organizers/' + $('body').attr('data-organizer') + '/events/' + $('body').attr('data-event') + '/questions/' + val + '/?container_type=' + encodeURIComponent($dq.data('container-type')), function (data) {
if (data.type === 'B') {
$val.append($('<option>').attr('value', 'True').text(gettext('Yes')))
$val.append($('<option>').attr('value', 'False').text(gettext('No')))
} else {
for (let i = 0; i < data.options.length; i++) {
let opt = data.options[i]
let $opt = $('<option>').attr('value', opt.identifier).text(i18nToString(opt.answer))
$val.append($opt)
}
}
if (oldval) {
$val.val(oldval)
}
$val.parent().find('.loading-indicator').remove()
$val.show()
})
}
// the container_type parameter is undocumented. this API is going to change in a later release.
apiGET('/api/v1/organizers/' + $("body").attr("data-organizer") + '/events/' + $("body").attr("data-event") + '/questions/' + val + '/?container_type=' + encodeURIComponent($dq.data('container-type')), function (data) {
if (data.type === "B") {
$val.append($("<option>").attr("value", "True").text(gettext("Yes")));
$val.append($("<option>").attr("value", "False").text(gettext("No")));
} else {
for (var i = 0; i < data.options.length; i++) {
var opt = data.options[i];
var $opt = $("<option>").attr("value", opt.identifier).text(i18nToString(opt.answer));
$val.append($opt);
}
}
if (oldval) {
$val.val(oldval);
}
$val.parent().find(".loading-indicator").remove();
$val.show();
});
}
update_dependency_options()
$dq.change(update_dependency_options)
})
update_dependency_options();
$dq.change(update_dependency_options);
});
@@ -1,50 +1,50 @@
$(function () {
'use strict'
"use strict";
let ticket_type_quota_calculation = function () {
let sum = 0
$('#ticket-type-formset div[data-formset-form]').each(function () {
if (!$(this).find('input[name$=DELETE]').prop('checked')) {
let val = $(this).find('input[name$=quota]').val()
if (val === '') {
sum = '∞'
} else if (sum !== '∞') {
sum += parseInt(val)
}
}
})
$('#total-capacity').text(sum)
}
var ticket_type_quota_calculation = function () {
var sum = 0;
$("#ticket-type-formset div[data-formset-form]").each(function () {
if (!$(this).find("input[name$=DELETE]").prop("checked")) {
var val = $(this).find("input[name$=quota]").val();
if (val === "") {
sum = "∞";
} else if (sum !== "∞") {
sum += parseInt(val);
}
}
});
$("#total-capacity").text(sum);
};
let toggle_payment = function () {
let any = false
$('#ticket-type-formset div[data-formset-form]').each(function () {
if (!$(this).find('input[name$=DELETE]').prop('checked')) {
let val = $(this).find('input[name$=default_price]').val()
if (/.*[1-9].*/.test(val)) {
any = true
}
}
})
if ($('#quick-setup-step-payment:visible').length && !any) {
$('#quick-setup-step-payment').stop().slideUp()
} else if (!$('#quick-setup-step-payment:visible').length && any) {
$('#quick-setup-step-payment').stop().slideDown()
}
}
var toggle_payment = function () {
var any = false;
$("#ticket-type-formset div[data-formset-form]").each(function () {
if (!$(this).find("input[name$=DELETE]").prop("checked")) {
var val = $(this).find("input[name$=default_price]").val();
if (/.*[1-9].*/.test(val)) {
any = true;
}
}
});
if ($("#quick-setup-step-payment:visible").length && !any) {
$("#quick-setup-step-payment").stop().slideUp();
} else if (!$("#quick-setup-step-payment:visible").length && any) {
$("#quick-setup-step-payment").stop().slideDown();
}
};
$('#ticket-type-formset').bind('formAdded', ticket_type_quota_calculation)
$('#ticket-type-formset').on('change keyup keydown keypress', 'input', function () {
ticket_type_quota_calculation()
toggle_payment()
})
ticket_type_quota_calculation()
toggle_payment()
$("#ticket-type-formset").bind("formAdded", ticket_type_quota_calculation);
$("#ticket-type-formset").on("change keyup keydown keypress", "input", function () {
ticket_type_quota_calculation();
toggle_payment();
});
ticket_type_quota_calculation();
toggle_payment();
$('#total-capacity-edit').click(function () {
$('#id_total_quota').val(parseInt($('#total-capacity').text()))
$('#total-capacity').hide()
$('#id_total_quota').closest('div').removeClass('sr-only')
$('#total-capacity-edit').hide()
})
})
$("#total-capacity-edit").click(function () {
$("#id_total_quota").val(parseInt($("#total-capacity").text()));
$("#total-capacity").hide();
$("#id_total_quota").closest("div").removeClass("sr-only");
$("#total-capacity-edit").hide();
});
});
+37 -37
View File
@@ -1,43 +1,43 @@
/* globals Morris */
/*globals $, Morris, gettext*/
$(function () {
if (!$('#quota-stats').length) {
return
}
if (!$("#quota-stats").length) {
return;
}
$('.chart').css('height', '250px')
new Morris.Donut({
element: 'quota_chart',
data: JSON.parse($('#quota-chart-data').html()),
resize: true,
colors: [
'#0044CC', // paid
'#0088CC', // pending
'#BD362F', // vouchers
'#F89406', // carts
'#51A351' // available
]
})
})
$(".chart").css("height", "250px");
new Morris.Donut({
element: 'quota_chart',
data: JSON.parse($("#quota-chart-data").html()),
resize: true,
colors: [
'#0044CC', // paid
'#0088CC', // pending
'#BD362F', // vouchers
'#F89406', // carts
'#51A351' // available
]
});
});
$(function () {
if (!$('input[name=itemvars]').length) {
return
}
let autofill = ($('#id_name').val() === '')
if (!$("input[name=itemvars]").length) {
return;
}
var autofill = ($("#id_name").val() === "");
$('#id_name').on('change keyup keydown keypress', function () {
autofill = false
})
$("#id_name").on("change keyup keydown keypress", function () {
autofill = false;
})
function do_autofill () {
if (autofill) {
let names = []
$('input[name=itemvars]:checked').each(function () {
names.push($.trim($(this).closest('label').text()))
})
$('#id_name').val(names.join(', '))
}
}
$('input[name=itemvars]').change(do_autofill)
do_autofill()
})
function do_autofill() {
if (autofill) {
var names = [];
$("input[name=itemvars]:checked").each(function () {
names.push($.trim($(this).closest("label").text()))
});
$("#id_name").val(names.join(', '));
}
}
$("input[name=itemvars]").change(do_autofill);
do_autofill();
});
+17 -15
View File
@@ -1,19 +1,21 @@
function rrule_form_toggles ($form) {
let freq = $form.find('select[name*=freq]').val()
$form.find('.repeat-yearly').toggle(freq === 'yearly')
$form.find('.repeat-monthly').toggle(freq === 'monthly')
$form.find('.repeat-weekly').toggle(freq === 'weekly')
/*globals $, Morris, gettext, RRule, RRuleSet*/
function rrule_form_toggles($form) {
var freq = $form.find("select[name*=freq]").val();
$form.find(".repeat-yearly").toggle(freq === "yearly");
$form.find(".repeat-monthly").toggle(freq === "monthly");
$form.find(".repeat-weekly").toggle(freq === "weekly");
}
function rrule_bind_form ($form) {
$form.find('select[name*=freq]').change(function () {
rrule_form_toggles($form)
})
rrule_form_toggles($form)
function rrule_bind_form($form) {
$form.find("select[name*=freq]").change(function () {
rrule_form_toggles($form);
});
rrule_form_toggles($form);
}
$(document).on('pretix:bind-forms', function () {
$('.rrule-form').each(function () {
rrule_bind_form($(this))
})
})
$(document).on("pretix:bind-forms", function () {
$(".rrule-form").each(function () {
rrule_bind_form($(this));
});
});
+204 -203
View File
@@ -1,225 +1,226 @@
/* globals ngettext, rrule */
/*globals $, Morris, gettext, RRule, RRuleSet*/
$(document).on('pretix:bind-forms', function () {
if (!$('div[data-formset-prefix=checkinlist_set]').length) {
return
}
$(document).on("pretix:bind-forms", function () {
if (!$("div[data-formset-prefix=checkinlist_set]").length) {
return;
}
function parse_weekday (wd) {
map = {
MO: 0,
TU: 1,
WE: 2,
TH: 3,
FR: 4,
SA: 5,
SU: 6
}
if (wd.indexOf(',') > 0) {
let wds = []
$.each(wd.split(','), function (k, v) {
wds.push(map[v])
})
return wds
} else {
return map[wd]
}
}
function parse_weekday(wd) {
map = {
'MO': 0,
'TU': 1,
'WE': 2,
'TH': 3,
'FR': 4,
'SA': 5,
'SU': 6
}
if (wd.indexOf(",") > 0) {
var wds = [];
$.each(wd.split(","), function (k, v) {
wds.push(map[v]);
});
return wds;
} else {
return map[wd];
}
}
// RRule editor
function rrule_preview () {
let ruleset = new rrule.RRuleSet()
// RRule editor
function rrule_preview() {
var ruleset = new rrule.RRuleSet();
$('.rrule-form').each(function () {
if ($(this).find('input[name$=DELETE]').prop('checked')) {
return
}
$(".rrule-form").each(function () {
if ($(this).find("input[name$=DELETE]").prop("checked")) {
return;
}
let rule_args = {}
let $form = $(this)
let freq = $form.find('select[name*=freq]').val()
if (!$form.find('input[name*=dtstart]').data('DateTimePicker')) {
// uninitialized
return
}
let dtstart = $form.find('input[name*=dtstart]').data('DateTimePicker').date()
dtstart = dtstart.add(dtstart.utcOffset(), 'm').add(12, 'h').utcOffset(0)
rule_args.dtstart = dtstart.toDate()
rule_args.interval = parseInt($form.find('input[name*=interval]').val()) || 1
var rule_args = {};
var $form = $(this);
var freq = $form.find("select[name*=freq]").val();
if (!$form.find("input[name*=dtstart]").data("DateTimePicker")) {
// uninitialized
return;
}
var dtstart = $form.find("input[name*=dtstart]").data("DateTimePicker").date();
dtstart = dtstart.add(dtstart.utcOffset(), 'm').add(12, 'h').utcOffset(0);
rule_args.dtstart = dtstart.toDate();
rule_args.interval = parseInt($form.find("input[name*=interval]").val()) || 1;
if (freq === 'yearly') {
rule_args.freq = rrule.RRule.YEARLY
if (freq === 'yearly') {
rule_args.freq = rrule.RRule.YEARLY;
var same = $form.find('input[name*=yearly_same]:checked').val()
if (same === 'off') {
rule_args.bysetpos = parseInt($form.find('select[name*=yearly_bysetpos]').val())
rule_args.byweekday = parse_weekday($form.find('select[name*=yearly_byweekday]').val())
rule_args.bymonth = parseInt($form.find('select[name*=yearly_bymonth]').val())
}
} else if (freq === 'monthly') {
rule_args.freq = rrule.RRule.MONTHLY
var same = $form.find("input[name*=yearly_same]:checked").val();
if (same === "off") {
rule_args.bysetpos = parseInt($form.find("select[name*=yearly_bysetpos]").val());
rule_args.byweekday = parse_weekday($form.find("select[name*=yearly_byweekday]").val());
rule_args.bymonth = parseInt($form.find("select[name*=yearly_bymonth]").val());
}
} else if (freq === 'monthly') {
rule_args.freq = rrule.RRule.MONTHLY;
var same = $form.find('input[name*=monthly_same]:checked').val()
if (same === 'off') {
rule_args.bysetpos = parseInt($form.find('select[name*=monthly_bysetpos]').val())
rule_args.byweekday = parse_weekday($form.find('select[name*=monthly_byweekday]').val())
}
} else if (freq === 'weekly') {
rule_args.freq = rrule.RRule.WEEKLY
var same = $form.find("input[name*=monthly_same]:checked").val();
if (same === "off") {
rule_args.bysetpos = parseInt($form.find("select[name*=monthly_bysetpos]").val());
rule_args.byweekday = parse_weekday($form.find("select[name*=monthly_byweekday]").val());
}
} else if (freq === 'weekly') {
rule_args.freq = rrule.RRule.WEEKLY;
let days = []
$form.find('input[name*=weekly_byweekday]:checked').each(function () {
days.push(parse_weekday($(this).val()))
})
if (days.length !== 0) {
rule_args.byweekday = days
}
} else if (freq === 'daily') {
rule_args.freq = rrule.RRule.DAILY
}
var days = [];
$form.find("input[name*=weekly_byweekday]:checked").each(function () {
days.push(parse_weekday($(this).val()));
});
if (days.length !== 0) {
rule_args.byweekday = days;
}
} else if (freq === 'daily') {
rule_args.freq = rrule.RRule.DAILY;
}
let end = $form.find('input[name*=end]:checked').val()
if (end === 'count') {
rule_args.count = Math.max(parseInt($form.find('input[name*=count]').val()) || 1, 1)
} else {
let date = $form.find('input[name*=until]').data('DateTimePicker').date()
if (date !== null) {
// rrule.until is non-inclusive, whereas in pretix-backend "until" is inclusive => add 1 day
// date is a Moment-object. Moment.add() mutates, but is save to do here
date.add(1, 'days')
rule_args.until = date
}
}
var end = $form.find("input[name*=end]:checked").val();
if (end === "count") {
rule_args.count = Math.max(parseInt($form.find("input[name*=count]").val()) || 1, 1);
} else {
var date = $form.find("input[name*=until]").data("DateTimePicker").date();
if (date !== null) {
// rrule.until is non-inclusive, whereas in pretix-backend "until" is inclusive => add 1 day
// date is a Moment-object. Moment.add() mutates, but is save to do here
date.add(1, 'days');
rule_args.until = date;
}
}
if ($form.find('input[name*=exclude]').prop('checked')) {
ruleset.exrule(new rrule.RRule(rule_args))
$form.closest('.panel').addClass('panel-danger').removeClass('panel-default')
} else {
ruleset.rrule(new rrule.RRule(rule_args))
$form.closest('.panel').addClass('panel-default').removeClass('panel-danger')
}
})
if ($form.find("input[name*=exclude]").prop("checked")) {
ruleset.exrule(new rrule.RRule(rule_args));
$form.closest(".panel").addClass("panel-danger").removeClass("panel-default");
} else {
ruleset.rrule(new rrule.RRule(rule_args));
$form.closest(".panel").addClass("panel-default").removeClass("panel-danger");
}
});
let all_dates = ruleset.all()
let format = $('body').attr('data-longdateformat') + ' (dddd)'
$('#rrule-preview').html('')
if (all_dates.length > 20) {
$('#rrule-preview').html('')
all_dates.slice(0, 10).forEach(function (element) {
$('#rrule-preview').append($('<li>').text(moment(element).utc().format(format)))
})
$('#rrule-preview').append($('<li>').text(ngettext(
'(one more date)',
'({num} more dates)',
all_dates.length - 20
).replace(/\{num\}/g, all_dates.length - 20)))
all_dates.slice(-10).forEach(function (element) {
$('#rrule-preview').append($('<li>').text(moment(element).utc().format(format)))
})
} else {
all_dates.forEach(function (element) {
$('#rrule-preview').append($('<li>').text(moment(element).utc().format(format)))
})
}
}
$('#rrule-formset').on('change keydown keyup keypress dp.change', 'input, select', function () {
rrule_preview()
})
rrule_preview()
var all_dates = ruleset.all();
var format = $("body").attr("data-longdateformat") + " (dddd)";
$("#rrule-preview").html("");
if (all_dates.length > 20) {
$("#rrule-preview").html("");
all_dates.slice(0, 10).forEach(function(element) {
$("#rrule-preview").append($("<li>").text(moment(element).utc().format(format)));
});
$("#rrule-preview").append($("<li>").text(ngettext(
"(one more date)",
"({num} more dates)",
all_dates.length - 20
).replace(/\{num\}/g, all_dates.length - 20)));
all_dates.slice(-10).forEach(function(element) {
$("#rrule-preview").append($("<li>").text(moment(element).utc().format(format)));
});
} else {
all_dates.forEach(function(element) {
$("#rrule-preview").append($("<li>").text(moment(element).utc().format(format)));
});
}
}
$("#rrule-formset").on("change keydown keyup keypress dp.change", "input, select", function () {
rrule_preview();
});
rrule_preview();
$('#rrule-formset').on('formAdded', 'div', function (event) { rrule_bind_form($(event.target)) })
$("#rrule-formset").on("formAdded", "div", function (event) {rrule_bind_form($(event.target)); });
// Timeslot editor
$('#subevent_add_many_slots_go').on('click', function () {
$('#time-formset [data-formset-form]').each(function () {
let tf = $(this).find('[name$=time_from]').val()
if (!tf) {
$(this).remove()
}
})
// Timeslot editor
$("#subevent_add_many_slots_go").on("click", function () {
$("#time-formset [data-formset-form]").each(function () {
var tf = $(this).find("[name$=time_from]").val()
if (!tf) {
$(this).remove();
}
})
let first = $('#subevent_add_many_slots_first').data('DateTimePicker').date()
let end = $('#subevent_add_many_slots_end').data('DateTimePicker').date()
let length_m = parseFloat($('#subevent_add_many_slots_length').val()) || 0
let break_m = parseFloat($('#subevent_add_many_slots_break').val()) || 0
if (!first || !end || !length_m) {
console.log('invalid', first, end, length_m)
return
}
var first = $("#subevent_add_many_slots_first").data('DateTimePicker').date();
var end = $("#subevent_add_many_slots_end").data('DateTimePicker').date();
var length_m = parseFloat($("#subevent_add_many_slots_length").val()) || 0;
var break_m = parseFloat($("#subevent_add_many_slots_break").val()) || 0;
if (!first || !end || !length_m) {
console.log("invalid", first, end, length_m)
return
}
function closure ($form, time) {
return function () {
console.log('setting value', time)
$form.find('[name$=time_from]').data('DateTimePicker').date(time)
time.add(length_m, 'minutes')
$form.find('[name$=time_to]').data('DateTimePicker').date(time)
}
}
function closure($form, time) {
return function () {
console.log("setting value", time)
$form.find("[name$=time_from]").data('DateTimePicker').date(time);
time.add(length_m, 'minutes');
$form.find("[name$=time_to]").data('DateTimePicker').date(time);
}
}
let pointer = first.clone()
while (pointer.isBefore(end)) {
let $form = $('#time-formset').formset('getOrCreate').addForm()
$form.attr('data-formset-created-at-runtime', 'false') // prevents animation
let time = pointer.clone()
window.setTimeout(closure($form, time), 1)
// jquery.formset.js only calls trigger("formAdded") after a setTimeout of 0,
// but we need to run after that to make sure the date pickers are initialized
pointer.add(break_m + length_m, 'minutes')
}
$('#subevent_add_many_slots').addClass('hidden')
$('#subevent_add_many_slots_start').removeClass('hidden')
})
$('#subevent_add_many_slots_start').on('click', function () {
$('#subevent_add_many_slots').removeClass('hidden')
$(this).addClass('hidden')
})
var pointer = first.clone();
while (pointer.isBefore(end)) {
var $form = $("#time-formset").formset("getOrCreate").addForm();
$form.attr("data-formset-created-at-runtime", "false"); // prevents animation
var time = pointer.clone();
window.setTimeout(closure($form, time), 1);
// jquery.formset.js only calls trigger("formAdded") after a setTimeout of 0,
// but we need to run after that to make sure the date pickers are initialized
pointer.add(break_m + length_m, 'minutes');
}
$("#subevent_add_many_slots").addClass("hidden");
$("#subevent_add_many_slots_start").removeClass("hidden");
// Hide config for products that are not for sale
function quota_form_handlers (el) {
// searchable_selection = True
el.find('[id^="id_quotas-"]').on('select2:select select2:unselect', () => {
update_item_visibility()
})
// searchable_selection = False
el.find('input[id^="id_quotas-"][id*=itemvars_]').on('change', () => {
update_item_visibility()
})
}
function update_item_visibility () {
const itemvars = []
});
$("#subevent_add_many_slots_start").on("click", function () {
$("#subevent_add_many_slots").removeClass("hidden");
$(this).addClass("hidden");
});
// searchable_selection = True
$('select[id^=id_quotas-][id$=-itemvars]').filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]')
}).each((_, e) => itemvars.push(...$(e).val()))
// searchable_selection = False
$('input[id^=id_quotas-][id*=itemvars_]:checked').filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]')
}).each((_, e) => itemvars.push($(e).val()))
// Hide config for products that are not for sale
function quota_form_handlers(el) {
// searchable_selection = True
el.find('[id^="id_quotas-"]').on("select2:select select2:unselect", () => {
update_item_visibility();
});
// searchable_selection = False
el.find('input[id^="id_quotas-"][id*=itemvars_]').on("change", () => {
update_item_visibility();
});
}
function update_item_visibility() {
const itemvars = [];
$('div[data-itemvar]').each(function (idx, e) {
const el = $(e)
el.prop('hidden', !itemvars.includes(el.attr('data-itemvar')) && !el.find('.has-error, .alert-danger').length)
})
}
// searchable_selection = True
$("select[id^=id_quotas-][id$=-itemvars]").filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]');
}).each((_, e) => itemvars.push(...$(e).val()));
// searchable_selection = False
$("input[id^=id_quotas-][id*=itemvars_]:checked").filter((idx, el) => {
return !$(el).closest('[data-formset-form]').is('[data-formset-form-deleted]');
}).each((_, e) => itemvars.push($(e).val()));
$('[data-formset-prefix="quotas"]').on('formDeleted', 'div', () => {
update_item_visibility()
}).on('formAdded', 'div', (event) => {
quota_form_handlers($(event.target))
update_item_visibility()
})
quota_form_handlers($('body'))
update_item_visibility()
$("div[data-itemvar]").each(function (idx, e) {
const el = $(e);
el.prop("hidden", !itemvars.includes(el.attr("data-itemvar")) && !el.find(".has-error, .alert-danger").length);
});
}
// Auto-set name of check-in list
let $namef = $('input[id^=id_name]').first()
let lastValue = $namef.val()
$namef.change(function () {
let field = $('div[data-formset-prefix=checkinlist_set] input[id$=name]').first()
if (field.val() === lastValue) {
lastValue = $(this).val()
field.val(lastValue)
}
})
})
$('[data-formset-prefix="quotas"]').on("formDeleted", "div", () => {
update_item_visibility();
}).on("formAdded", "div", (event) => {
quota_form_handlers($(event.target));
update_item_visibility();
})
quota_form_handlers($("body"));
update_item_visibility();
// Auto-set name of check-in list
var $namef = $("input[id^=id_name]").first();
var lastValue = $namef.val();
$namef.change(function () {
var field = $("div[data-formset-prefix=checkinlist_set] input[id$=name]").first();
if (field.val() === lastValue) {
lastValue = $(this).val();
field.val(lastValue);
}
});
});
+55 -53
View File
@@ -1,54 +1,56 @@
$(function () {
let j = 0
$('.tabbed-form').each(function () {
let $form = $(this)
let $tabs = $('<ul>').addClass('nav nav-tabs').insertBefore($form)
$form.addClass('tab-content')
/*globals $*/
let i = 0
let preselect = null
let validity_error = false
$form.find('fieldset').each(function () {
let $fieldset = $(this)
let tid = $fieldset.attr('id')
if (!tid) tid = 'tab-' + j + '-' + i
let $tabli = $('<li>').appendTo($tabs)
let $tablink = $('<a>').attr('role', 'tab')
.attr('data-toggle', 'tab')
.attr('href', '#' + tid)
.text($fieldset.find('legend').text())
.appendTo($tabli)
if ($fieldset.find('.has-error, .alert-danger:not(.dynamic)').length > 0) {
$tablink.append(' ')
$tablink.append($('<span>').addClass('fa fa-warning text-danger'))
if (preselect === null) {
preselect = i
}
}
$fieldset.find('input, select, textarea').on('invalid', function () {
if ($tablink.find('.fa-warning').length === 0) {
$tablink.append(' ')
$tablink.append($('<span>').addClass('fa fa-warning text-danger'))
if (!validity_error) {
validity_error = true
$tablink.click()
}
}
})
$fieldset.find('legend').remove()
$fieldset.addClass('tab-pane').attr('id', tid)
if (location.hash && ($fieldset.find(location.hash).length || location.hash === '#' + tid + '-open') && preselect === null) {
preselect = i
}
i++
})
$tabs.find('a').get(preselect != null ? preselect : 0).click()
$tabs.find('a').on('shown.bs.tab', function (e) {
history.replaceState(null, null, e.target.getAttribute('href') + '-open')
})
$form.closest('form').on('submit', function () {
validity_error = false
})
j++
})
})
$(function () {
var j = 0;
$(".tabbed-form").each(function () {
var $form = $(this);
var $tabs = $("<ul>").addClass("nav nav-tabs").insertBefore($form);
$form.addClass("tab-content");
var i = 0;
var preselect = null;
var validity_error = false;
$form.find("fieldset").each(function () {
var $fieldset = $(this);
var tid = $fieldset.attr("id");
if (!tid) tid = "tab-" + j + "-" + i;
var $tabli = $("<li>").appendTo($tabs);
var $tablink = $("<a>").attr("role", "tab")
.attr("data-toggle", "tab")
.attr("href", "#" + tid)
.text($fieldset.find("legend").text())
.appendTo($tabli);
if ($fieldset.find(".has-error, .alert-danger:not(.dynamic)").length > 0) {
$tablink.append(" ");
$tablink.append($("<span>").addClass("fa fa-warning text-danger"));
if (preselect === null) {
preselect = i;
}
}
$fieldset.find("input, select, textarea").on("invalid", function () {
if ($tablink.find(".fa-warning").length === 0) {
$tablink.append(" ");
$tablink.append($("<span>").addClass("fa fa-warning text-danger"));
if (!validity_error) {
validity_error = true;
$tablink.click();
}
}
});
$fieldset.find("legend").remove();
$fieldset.addClass("tab-pane").attr("id", tid);
if (location.hash && ($fieldset.find(location.hash).length || location.hash === "#" + tid + "-open") && preselect === null) {
preselect = i;
}
i++;
});
$tabs.find("a").get(preselect != null ? preselect : 0).click();
$tabs.find("a").on('shown.bs.tab', function (e) {
history.replaceState(null, null, e.target.getAttribute("href") + "-open");
});
$form.closest("form").on("submit", function () {
validity_error = false;
});
j++;
});
});
+158 -157
View File
@@ -1,163 +1,164 @@
/*global $,u2f */
$(function () {
$('.context-selector.dropdown').on('shown.bs.collapse shown.bs.dropdown', function () {
$(this).parent().find('input').val('').trigger('forceRunQuery').focus()
})
$('.dropdown-menu .form-box input').click(function (e) {
e.stopPropagation()
})
$('.context-selector.dropdown').on('shown.bs.collapse shown.bs.dropdown', function () {
$(this).parent().find("input").val("").trigger('forceRunQuery').focus();
});
$('.dropdown-menu .form-box input').click(function (e) {
e.stopPropagation();
});
$('[data-event-typeahead]').each(function () {
let $container = $(this)
let $query = $(this).find('[data-typeahead-query]').length ? $(this).find('[data-typeahead-query]') : $($(this).attr('data-typeahead-field'))
$container.find('li:not(.query-holder)').remove()
let lastQuery = null
let runQueryTimeout = null
let loadIndicatorTimeout = null
let focusOutTimeout = null
function showLoadIndicator () {
$container.find('li:not(.query-holder)').remove()
$container.append('<li class=\'loading\'><span class=\'fa fa-4x fa-cog fa-spin\'></span></li>')
$container.toggleClass('focused', $query.is(':focus') && $container.children().length > 0)
}
function runQuery () {
let thisQuery = $query.val()
if (thisQuery === lastQuery) return
lastQuery = $query.val()
$("[data-event-typeahead]").each(function () {
var $container = $(this);
var $query = $(this).find('[data-typeahead-query]').length ? $(this).find('[data-typeahead-query]') : $($(this).attr("data-typeahead-field"));
$container.find("li:not(.query-holder)").remove();
var lastQuery = null;
var runQueryTimeout = null;
var loadIndicatorTimeout = null;
var focusOutTimeout = null;
function showLoadIndicator() {
$container.find("li:not(.query-holder)").remove();
$container.append("<li class='loading'><span class='fa fa-4x fa-cog fa-spin'></span></li>");
$container.toggleClass('focused', $query.is(":focus") && $container.children().length > 0);
}
function runQuery() {
var thisQuery = $query.val();
if (thisQuery === lastQuery) return;
lastQuery = $query.val();
window.clearTimeout(loadIndicatorTimeout)
loadIndicatorTimeout = window.setTimeout(showLoadIndicator, 80)
window.clearTimeout(loadIndicatorTimeout)
loadIndicatorTimeout = window.setTimeout(showLoadIndicator, 80)
$.getJSON(
$container.attr('data-source') + '?query=' + encodeURIComponent($query.val()) + (typeof $container.attr('data-organizer') !== 'undefined' ? '&organizer=' + $container.attr('data-organizer') : ''),
function (data) {
if (thisQuery !== lastQuery) {
// Lost race condition
return
}
window.clearTimeout(loadIndicatorTimeout)
$container.find('li:not(.query-holder)').remove()
$.each(data.results, function (_i, res) {
let $linkContent = $('<div>')
if (res.type === 'organizer') {
$linkContent.append(
$('<span>').addClass('event-name-full').append(
$('<span>').addClass('fa fa-users fa-fw')
).append(' ').append($('<div>').text(res.name).html())
)
} else if (res.type === 'order' || res.type === 'voucher') {
$linkContent.append(
$('<span>').addClass('event-name-full').append($('<div>').text(res.title).html())
).append(
$('<span>').addClass('event-organizer').append(
$('<span>').addClass('fa fa-calendar fa-fw')
).append(' ').append($('<div>').text(res.event).html())
)
} else if (res.type === 'user') {
$linkContent.append(
$('<span>').addClass('event-name-full').append(
$('<span>').addClass('fa fa-user fa-fw')
).append(' ').append($('<div>').text(res.name).html())
)
} else {
$linkContent.append(
$('<span>').addClass('event-name-full').append($('<div>').text(res.name).html())
).append(
$('<span>').addClass('event-organizer').append(
$('<span>').addClass('fa fa-users fa-fw')
).append(' ').append($('<div>').text(res.organizer).html())
).append(
$('<span>').addClass('event-daterange').append(
$('<span>').addClass('fa fa-calendar fa-fw')
).append(' ').append(res.date_range)
)
}
$.getJSON(
$container.attr("data-source") + "?query=" + encodeURIComponent($query.val()) + (typeof $container.attr("data-organizer") !== "undefined" ? "&organizer=" + $container.attr("data-organizer") : ""),
function (data) {
if (thisQuery !== lastQuery) {
// Lost race condition
return;
}
window.clearTimeout(loadIndicatorTimeout);
$container.find("li:not(.query-holder)").remove();
$.each(data.results, function (i, res) {
let $linkContent = $("<div>");
if (res.type === "organizer") {
$linkContent.append(
$("<span>").addClass("event-name-full").append(
$("<span>").addClass("fa fa-users fa-fw")
).append(" ").append($("<div>").text(res.name).html())
)
} else if (res.type === "order" || res.type === "voucher") {
$linkContent.append(
$("<span>").addClass("event-name-full").append($("<div>").text(res.title).html())
).append(
$("<span>").addClass("event-organizer").append(
$("<span>").addClass("fa fa-calendar fa-fw")
).append(" ").append($("<div>").text(res.event).html())
)
} else if (res.type === "user") {
$linkContent.append(
$("<span>").addClass("event-name-full").append(
$("<span>").addClass("fa fa-user fa-fw")
).append(" ").append($("<div>").text(res.name).html())
)
} else {
$linkContent.append(
$("<span>").addClass("event-name-full").append($("<div>").text(res.name).html())
).append(
$("<span>").addClass("event-organizer").append(
$("<span>").addClass("fa fa-users fa-fw")
).append(" ").append($("<div>").text(res.organizer).html())
).append(
$("<span>").addClass("event-daterange").append(
$("<span>").addClass("fa fa-calendar fa-fw")
).append(" ").append(res.date_range)
)
}
$container.append(
$('<li>').append(
$('<a>').attr('href', res.url).append(
$linkContent
)
)
)
})
$container.toggleClass('focused', $query.is(':focus') && $container.children().length > 0)
}
)
}
$query.on('forceRunQuery', function () {
runQuery()
})
$query.on('input', function () {
if ($container.attr('data-typeahead-field') && $query.val() === '') {
$container.removeClass('focused')
$container.find('li:not(.query-holder)').remove()
lastQuery = null
return
}
window.clearTimeout(runQueryTimeout)
runQueryTimeout = window.setTimeout(runQuery, 250)
})
$query.on('keydown', function (event) {
let $selected = $container.find('.active')
if (event.which === 13) { // enter
let $link = $selected.find('a')
if ($link.length) {
location.href = $link.attr('href')
}
event.preventDefault()
event.stopPropagation()
}
})
$container.add($query).on('keydown', function (event) {
if (event.which === 27) { // escape
$container.removeClass('focused')
}
}).on('focusin', function () {
window.clearTimeout(focusOutTimeout)
$(document.body).one('focusout', function () {
focusOutTimeout = window.setTimeout(function () {
$container.removeClass('focused')
}, 100)
})
})
$query.on('keyup', function (event) {
let $first = $container.find('li:not(.query-holder)').first()
let $last = $container.find('li:not(.query-holder)').last()
let $selected = $container.find('.active')
$container.append(
$("<li>").append(
$("<a>").attr("href", res.url).append(
$linkContent
)
)
);
});
$container.toggleClass('focused', $query.is(":focus") && $container.children().length > 0);
}
);
}
$query.on("forceRunQuery", function () {
runQuery();
});
$query.on("input", function () {
if ($container.attr("data-typeahead-field") && $query.val() === "") {
$container.removeClass('focused');
$container.find("li:not(.query-holder)").remove();
lastQuery = null;
return;
}
window.clearTimeout(runQueryTimeout)
runQueryTimeout = window.setTimeout(runQuery, 250)
});
$query.on("keydown", function (event) {
var $selected = $container.find(".active");
if (event.which === 13) { // enter
var $link = $selected.find("a");
if ($link.length) {
location.href = $link.attr("href");
}
event.preventDefault();
event.stopPropagation();
}
});
$container.add($query).on("keydown", function (event) {
if (event.which === 27) { // escape
$container.removeClass('focused');
}
}).on("focusin", function (event) {
window.clearTimeout(focusOutTimeout);
$(document.body).one("focusout", function (event) {
focusOutTimeout = window.setTimeout(function () {
$container.removeClass('focused');
}, 100);
})
});
$query.on("keyup", function (event) {
var $first = $container.find("li:not(.query-holder)").first();
var $last = $container.find("li:not(.query-holder)").last();
var $selected = $container.find(".active");
if (event.which === 13) { // enter
event.preventDefault()
event.stopPropagation()
return true
} else if (event.which === 40) { // down
let $next
if ($selected.length === 0) {
$next = $first
} else {
$next = $selected.next()
}
if ($next.length === 0) {
$next = $first
}
$selected.removeClass('active')
$next.addClass('active')
event.preventDefault()
event.stopPropagation()
return true
} else if (event.which === 38) { // up
if ($selected.length === 0) {
$selected = $first
}
let $prev = $selected.prev()
if ($prev.length === 0 || $prev.find('input').length > 0) {
$prev = $last
}
$selected.removeClass('active')
$prev.addClass('active')
event.preventDefault()
event.stopPropagation()
return true
}
})
})
})
if (event.which === 13) { // enter
event.preventDefault();
event.stopPropagation();
return true;
} else if (event.which === 40) { // down
var $next;
if ($selected.length === 0) {
$next = $first;
} else {
$next = $selected.next();
}
if ($next.length === 0) {
$next = $first;
}
$selected.removeClass("active");
$next.addClass("active");
event.preventDefault();
event.stopPropagation();
return true;
} else if (event.which === 38) { // up
if ($selected.length === 0) {
$selected = $first;
}
var $prev = $selected.prev();
if ($prev.length === 0 || $prev.find("input").length > 0) {
$prev = $last;
}
$selected.removeClass("active");
$prev.addClass("active");
event.preventDefault();
event.stopPropagation();
return true;
}
});
});
});
@@ -1,78 +1,78 @@
/* global i18nToString, formatPrice */
/*global $, Morris, gettext, formatPrice*/
$(function () {
// Question view
if (!$('#item_variations').length) {
return
}
// Question view
if (!$("#item_variations").length) {
return;
}
function update_variation_summary ($el) {
let var_names = Object.fromEntries(
$el
.find('input[name*=-value_]')
.filter(function () {
return !!this.value
})
.map(function () {
return [[this.getAttribute('lang'), this.value]]
})
.get()
)
let var_name = i18nToString(var_names)
let price = $el.find('input[name*=-default_price]').val()
if (price) {
let currency = $el.find('[name*=-default_price] + .input-group-addon').text()
price = formatPrice(price, currency)
}
function update_variation_summary($el) {
var var_names = Object.fromEntries(
$el
.find("input[name*=-value_]")
.filter(function () {
return !!this.value;
})
.map(function () {
return [[this.getAttribute("lang"), this.value]];
})
.get()
);
var var_name = i18nToString(var_names);
var price = $el.find("input[name*=-default_price]").val();
if (price) {
var currency = $el.find("[name*=-default_price] + .input-group-addon").text();
price = formatPrice(price, currency);
}
$el.find('.variation-name').text(var_name)
$el.find('.variation-price').text(price)
$el.find('.variation-timeframe').toggleClass('variation-icon-hidden', !(
!!$el.find('input[name$=-available_from_0]').val()
|| !!$el.find('input[name$=-available_until_0]').val()
))
$el.find('.variation-name').toggleClass('variation-disabled', !(
$el.find('input[name$=-active]').prop('checked')
))
$el.find('.variation-voucher').toggleClass('variation-icon-hidden', !(
$el.find('input[name$=-hide_without_voucher]').prop('checked')
))
$el.find('.variation-membership').toggleClass('variation-icon-hidden', !(
$el.find('input[name$=-require_membership]').prop('checked')
))
$el.find('.variation-warning').toggleClass('hidden', !(
$el.find('.alert-warning').length
))
$el.find('.variation-error').toggleClass('hidden', !(
$el.find('.alert-danger, .has-error').length
))
$el.find('input[name$=-limit_sales_channels]').each(function () {
$el.find('.variation-channel-' + $(this).val()).toggleClass('variation-icon-hidden', !(
(
$(this).closest('[data-formset-form]').find('input[name$=-all_sales_channels]').prop('checked')
|| $(this).prop('checked')
) && (
$('input[name=all_sales_channels]').prop('checked')
|| $('input[name=limit_sales_channels][value=' + $(this).val() + ']').prop('checked')
)
))
})
}
$el.find(".variation-name").text(var_name);
$el.find(".variation-price").text(price);
$el.find(".variation-timeframe").toggleClass("variation-icon-hidden", !(
!!$el.find("input[name$=-available_from_0]").val() ||
!!$el.find("input[name$=-available_until_0]").val()
));
$el.find(".variation-name").toggleClass("variation-disabled", !(
!!$el.find("input[name$=-active]").prop("checked")
));
$el.find(".variation-voucher").toggleClass("variation-icon-hidden", !(
!!$el.find("input[name$=-hide_without_voucher]").prop("checked")
));
$el.find(".variation-membership").toggleClass("variation-icon-hidden", !(
!!$el.find("input[name$=-require_membership]").prop("checked")
));
$el.find(".variation-warning").toggleClass("hidden", !(
$el.find(".alert-warning").length
));
$el.find(".variation-error").toggleClass("hidden", !(
$el.find(".alert-danger, .has-error").length
));
$el.find("input[name$=-limit_sales_channels]").each(function () {
$el.find(".variation-channel-" + $(this).val()).toggleClass("variation-icon-hidden", !(
(
$(this).closest("[data-formset-form]").find("input[name$=-all_sales_channels]").prop("checked") ||
$(this).prop("checked")
) && (
$("input[name=all_sales_channels]").prop("checked") ||
$("input[name=limit_sales_channels][value=" + $(this).val() + "]").prop("checked")
)
));
})
}
$('#item_variations [data-formset-form]').each(function () {
let $el = $(this)
update_variation_summary($el)
$(this).on('change dp.change', 'input', function () { update_variation_summary($el) })
})
$('input[name=limit_sales_channels] input[name=all_sales_channels]').on('change', function () {
$('#item_variations [data-formset-form]').each(function () {
update_variation_summary($(this))
})
})
$('#item_variations').on('formAdded', 'details', function (event) {
let $el = $(event.target)
update_variation_summary($el)
$(this).on('change dp.change', 'input', function () { update_variation_summary($el) })
setup_collapsible_details($('#item_variations'))
form_handlers($(event.target))
})
})
$("#item_variations [data-formset-form]").each(function () {
var $el = $(this);
update_variation_summary($el);
$(this).on("change dp.change", "input", function () {update_variation_summary($el)});
});
$("input[name=limit_sales_channels] input[name=all_sales_channels]").on("change", function() {
$("#item_variations [data-formset-form]").each(function () {
update_variation_summary($(this));
});
});
$("#item_variations").on("formAdded", "details", function (event) {
var $el = $(event.target);
update_variation_summary($el);
$(this).on("change dp.change", "input", function () {update_variation_summary($el)});
setup_collapsible_details($("#item_variations"));
form_handlers($(event.target));
});
});
+145 -142
View File
@@ -1,32 +1,32 @@
/* global base64js */
/*global $,u2f */
function b64enc (buf) {
return base64js.fromByteArray(buf)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '')
function b64enc(buf) {
return base64js.fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
}
function b64RawEnc (buf) {
return base64js.fromByteArray(buf)
.replace(/\+/g, '-')
.replace(/\//g, '_')
function b64RawEnc(buf) {
return base64js.fromByteArray(buf)
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function hexEncode (buf) {
return Array.from(buf)
.map(function (x) {
return ('0' + x.toString(16)).substr(-2)
})
.join('')
function hexEncode(buf) {
return Array.from(buf)
.map(function(x) {
return ("0" + x.toString(16)).substr(-2);
})
.join("");
}
async function fetch_json (url, options) {
const response = await fetch(url, options)
const body = await response.json()
if (body.fail)
throw body.fail
return body
async function fetch_json(url, options) {
const response = await fetch(url, options);
const body = await response.json();
if (body.fail)
throw body.fail;
return body;
}
/**
@@ -35,26 +35,27 @@ async function fetch_json (url, options) {
* @param {Object} credentialCreateOptionsFromServer
*/
const transformCredentialCreateOptions = function (credentialCreateOptionsFromServer) {
let { challenge, user, excludeCredentials } = credentialCreateOptionsFromServer
user.id = user.id.replace(/_/g, '/').replace(/-/g, '+')
user.id = Uint8Array.from(atob(user.id), c => c.charCodeAt(0))
let {challenge, user, excludeCredentials} = credentialCreateOptionsFromServer;
user.id = user.id.replace(/\_/g, "/").replace(/\-/g, "+");
user.id = Uint8Array.from(atob(user.id), c => c.charCodeAt(0));
challenge = challenge.replace(/_/g, '/').replace(/-/g, '+')
challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0))
challenge = challenge.replace(/\_/g, "/").replace(/\-/g, "+");
challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
excludeCredentials = excludeCredentials.map(credentialDescriptor => {
let { id } = credentialDescriptor
id = id.replace(/_/g, '/').replace(/-/g, '+')
id = Uint8Array.from(atob(id), c => c.charCodeAt(0))
return Object.assign({}, credentialDescriptor, { id })
})
excludeCredentials = excludeCredentials.map(credentialDescriptor => {
let {id} = credentialDescriptor;
id = id.replace(/\_/g, "/").replace(/\-/g, "+");
id = Uint8Array.from(atob(id), c => c.charCodeAt(0));
return Object.assign({}, credentialDescriptor, {id});
});
const transformedCredentialCreateOptions = Object.assign(
{}, credentialCreateOptionsFromServer,
{ challenge, user, excludeCredentials })
const transformedCredentialCreateOptions = Object.assign(
{}, credentialCreateOptionsFromServer,
{challenge, user, excludeCredentials});
return transformedCredentialCreateOptions;
};
return transformedCredentialCreateOptions
}
/**
* Transforms the binary data in the credential into base64 strings
@@ -62,132 +63,134 @@ const transformCredentialCreateOptions = function (credentialCreateOptionsFromSe
* @param {PublicKeyCredential} newAssertion
*/
const transformNewAssertionForServer = (newAssertion) => {
const attObj = new Uint8Array(newAssertion.response.attestationObject)
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON)
const rawId = new Uint8Array(newAssertion.rawId)
const transports = newAssertion.response.getTransports()
const authenticatorAttachment = newAssertion.authenticatorAttachment
const attObj = new Uint8Array(newAssertion.response.attestationObject);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const transports = newAssertion.response.getTransports();
const authenticatorAttachment = newAssertion.authenticatorAttachment;
const registrationClientExtensions = newAssertion.getClientExtensionResults()
const registrationClientExtensions = newAssertion.getClientExtensionResults();
return {
id: newAssertion.id,
rawId: b64enc(rawId),
response: {
attestationObject: b64enc(attObj),
clientDataJSON: b64enc(clientDataJSON),
transports: transports,
},
type: newAssertion.type,
clientExtensionResults: JSON.stringify(registrationClientExtensions),
authenticatorAttachment: authenticatorAttachment,
};
};
return {
id: newAssertion.id,
rawId: b64enc(rawId),
response: {
attestationObject: b64enc(attObj),
clientDataJSON: b64enc(clientDataJSON),
transports: transports,
},
type: newAssertion.type,
clientExtensionResults: JSON.stringify(registrationClientExtensions),
authenticatorAttachment: authenticatorAttachment,
}
}
const transformCredentialRequestOptions = (credentialRequestOptionsFromServer) => {
let { challenge, allowCredentials } = credentialRequestOptionsFromServer
let {challenge, allowCredentials} = credentialRequestOptionsFromServer;
challenge = challenge.replace(/_/g, '/').replace(/-/g, '+')
challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0))
challenge = challenge.replace(/\_/g, "/").replace(/\-/g, "+");
challenge = Uint8Array.from(atob(challenge), c => c.charCodeAt(0));
allowCredentials = allowCredentials.map(credentialDescriptor => {
let { id } = credentialDescriptor
id = id.replace(/_/g, '/').replace(/-/g, '+')
id = Uint8Array.from(atob(id), c => c.charCodeAt(0))
return Object.assign({}, credentialDescriptor, { id })
})
allowCredentials = allowCredentials.map(credentialDescriptor => {
let {id} = credentialDescriptor;
id = id.replace(/\_/g, "/").replace(/\-/g, "+");
id = Uint8Array.from(atob(id), c => c.charCodeAt(0));
return Object.assign({}, credentialDescriptor, {id});
});
const transformedCredentialRequestOptions = Object.assign(
{},
credentialRequestOptionsFromServer,
{ challenge, allowCredentials })
const transformedCredentialRequestOptions = Object.assign(
{},
credentialRequestOptionsFromServer,
{challenge, allowCredentials});
return transformedCredentialRequestOptions
}
return transformedCredentialRequestOptions;
};
/**
* Encodes the binary data in the assertion into strings for posting to the server.
* @param {PublicKeyCredential} newAssertion
*/
const transformAssertionForServer = (newAssertion) => {
const authData = new Uint8Array(newAssertion.response.authenticatorData)
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON)
const rawId = new Uint8Array(newAssertion.rawId)
const sig = new Uint8Array(newAssertion.response.signature)
const userHandle = new Uint8Array(newAssertion.response.userHandle)
const assertionClientExtensions = newAssertion.getClientExtensionResults()
const authenticatorAttachment = newAssertion.authenticatorAttachment
const authData = new Uint8Array(newAssertion.response.authenticatorData);
const clientDataJSON = new Uint8Array(newAssertion.response.clientDataJSON);
const rawId = new Uint8Array(newAssertion.rawId);
const sig = new Uint8Array(newAssertion.response.signature);
const userHandle = new Uint8Array(newAssertion.response.userHandle);
const assertionClientExtensions = newAssertion.getClientExtensionResults();
const authenticatorAttachment = newAssertion.authenticatorAttachment;
return {
id: newAssertion.id,
rawId: b64enc(rawId),
type: newAssertion.type,
response: {
authenticatorData: b64RawEnc(authData),
clientDataJSON: b64RawEnc(clientDataJSON),
signature: b64RawEnc(sig),
userHandle: b64RawEnc(userHandle),
},
authenticatorAttachment: authenticatorAttachment,
clientExtensionResults: JSON.stringify(assertionClientExtensions)
}
}
return {
id: newAssertion.id,
rawId: b64enc(rawId),
type: newAssertion.type,
response: {
authenticatorData: b64RawEnc(authData),
clientDataJSON: b64RawEnc(clientDataJSON),
signature: b64RawEnc(sig),
userHandle: b64RawEnc(userHandle),
},
authenticatorAttachment: authenticatorAttachment,
clientExtensionResults: JSON.stringify(assertionClientExtensions)
};
};
const startRegister = async () => {
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(JSON.parse($('#webauthn-enroll').text()))
const startRegister = async (e) => {
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(JSON.parse($("#webauthn-enroll").text()));
// request the authenticator(s) to create a new credential keypair.
let credential
try {
credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreateOptions
})
} catch (err) {
$('#webauthn-error').removeClass('hidden')
return console.error('Error creating credential:', err)
}
// request the authenticator(s) to create a new credential keypair.
let credential;
try {
credential = await navigator.credentials.create({
publicKey: publicKeyCredentialCreateOptions
});
} catch (err) {
$("#webauthn-error").removeClass("hidden");
return console.error("Error creating credential:", err);
}
// we now have a new credential! We now need to encode the byte arrays
// in the credential into strings, for posting to our server.
const newAssertionForServer = transformNewAssertionForServer(credential)
// we now have a new credential! We now need to encode the byte arrays
// in the credential into strings, for posting to our server.
const newAssertionForServer = transformNewAssertionForServer(credential);
$('#webauthn-response').val(JSON.stringify(newAssertionForServer))
$('#webauthn-form').submit()
}
$("#webauthn-response").val(JSON.stringify(newAssertionForServer));
$("#webauthn-form").submit();
};
const startLogin = async () => {
const transformedCredentialRequestOptions = transformCredentialRequestOptions(JSON.parse($('#webauthn-login').text()))
console.log(transformedCredentialRequestOptions)
// request the authenticator to create an assertion signature using the
// credential private key
let assertion
try {
assertion = await navigator.credentials.get({
publicKey: transformedCredentialRequestOptions,
})
} catch (err) {
$('#webauthn-error').removeClass('hidden')
return console.error('Error when creating credential:', err)
}
const startLogin = async (e) => {
const transformedCredentialRequestOptions = transformCredentialRequestOptions(JSON.parse($("#webauthn-login").text()));
console.log(transformedCredentialRequestOptions);
// we now have an authentication assertion! encode the byte arrays contained
// in the assertion data as strings for posting to the server
const transformedAssertionForServer = transformAssertionForServer(assertion)
// request the authenticator to create an assertion signature using the
// credential private key
let assertion;
try {
assertion = await navigator.credentials.get({
publicKey: transformedCredentialRequestOptions,
});
} catch (err) {
$("#webauthn-error").removeClass("hidden");
return console.error("Error when creating credential:", err);
}
// post the assertion to the server for verification.
$('input, select, textarea').prop('required', false)
$('#webauthn-response, #id_password').val(JSON.stringify(transformedAssertionForServer))
$('#webauthn-form').submit()
}
// we now have an authentication assertion! encode the byte arrays contained
// in the assertion data as strings for posting to the server
const transformedAssertionForServer = transformAssertionForServer(assertion);
// post the assertion to the server for verification.
$("input, select, textarea").prop("required", false);
$("#webauthn-response, #id_password").val(JSON.stringify(transformedAssertionForServer));
$("#webauthn-form").submit();
};
$(function () {
$('#webauthn-progress').hide()
if ($('#webauthn-enroll').length) {
$('#webauthn-progress').show()
startRegister()
} else if ($('#webauthn-login').length) {
$('#webauthn-progress').show()
startLogin()
}
})
$("#webauthn-progress").hide();
if ($("#webauthn-enroll").length) {
$("#webauthn-progress").show();
startRegister();
} else if ($("#webauthn-login").length) {
$("#webauthn-progress").show();
startLogin();
}
});
@@ -1,15 +1,15 @@
document.addEventListener('DOMContentLoaded', () => {
const COOKIE_NAME = '__Host-pretix_csrftoken'
const RELOAD_FLAG = 'csrfReloadPerformed'
document.addEventListener("DOMContentLoaded", () => {
const COOKIE_NAME = "__Host-pretix_csrftoken";
const RELOAD_FLAG = "csrfReloadPerformed";
const hasCookie = document.cookie
.split('; ')
.some((c) => c.startsWith(COOKIE_NAME + '='))
const hasCookie = document.cookie
.split("; ")
.some((c) => c.startsWith(COOKIE_NAME + "="));
if (!hasCookie && !sessionStorage.getItem(RELOAD_FLAG)) {
sessionStorage.setItem(RELOAD_FLAG, '1')
location.reload()
} else if (hasCookie && sessionStorage.getItem(RELOAD_FLAG)) {
sessionStorage.removeItem(RELOAD_FLAG)
}
})
if (!hasCookie && !sessionStorage.getItem(RELOAD_FLAG)) {
sessionStorage.setItem(RELOAD_FLAG, "1");
location.reload();
} else if (hasCookie && sessionStorage.getItem(RELOAD_FLAG)) {
sessionStorage.removeItem(RELOAD_FLAG);
}
});
@@ -1,8 +1,10 @@
/*global $ */
$(function () {
window.addEventListener('message', (event) => {
if (event.data && event.data.__process === 'popup_close') {
window.close()
}
})
window.opener.postMessage(JSON.parse($('#postmessage').text()), $('#origin').text())
})
window.addEventListener("message", (event) => {
if (event.data && event.data.__process === "popup_close") {
window.close()
}
});
window.opener.postMessage(JSON.parse($("#postmessage").text()), $("#origin").text())
})
+173 -173
View File
@@ -1,191 +1,191 @@
/* global gettext,ngettext,moment,django */
/*global $,gettext,ngettext */
var cart = {
_deadline: null,
_deadline_timeout: null,
_deadline_call: 0,
_time_offset: 0,
_prev_diff_minutes: 0,
_deadline: null,
_deadline_timeout: null,
_deadline_call: 0,
_time_offset: 0,
_prev_diff_minutes: 0,
_get_now: function () {
return moment().add(cart._time_offset, 'ms')
},
_get_now: function () {
return moment().add(cart._time_offset, 'ms');
},
_calc_offset: function () {
if (typeof window.performance === 'undefined') {
return
}
let perf = window.performance.timing
let server_time = Math.round(parseFloat($('body').attr('data-now')) * 1000)
// We use requestStart as we don't know how latency is distributed and we rather want to err on the safe side
let client_time = perf.requestStart
cart._time_offset = server_time - client_time
},
_calc_offset: function () {
if (typeof window.performance === "undefined") {
return;
}
var perf = window.performance.timing;
var server_time = Math.round(parseFloat($("body").attr("data-now")) * 1000);
// We use requestStart as we don't know how latency is distributed and we rather want to err on the safe side
var client_time = perf.requestStart;
cart._time_offset = server_time - client_time;
},
show_expiry_notification: function () {
document.getElementById('dialog-cart-extend').showModal()
cart._expiry_notified = true
},
show_expiry_notification: function () {
document.getElementById("dialog-cart-extend").showModal();
cart._expiry_notified = true;
},
draw_deadline: function () {
function pad (n, width, z) {
z = z || '0'
n = n + ''
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n
}
draw_deadline: function () {
function pad(n, width, z) {
z = z || '0';
n = n + '';
return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n;
}
cart._deadline_call++
if ((typeof django === 'undefined' || typeof django.gettext === 'undefined') && cart._deadline_call < 5) {
// Language files are not loaded yet, don't run during the first seconds
return
}
let now = cart._get_now()
let diff_total_seconds = cart._deadline.diff(now) / 1000
let diff_minutes = Math.floor(diff_total_seconds / 60)
let diff_seconds = Math.floor(diff_total_seconds % 60)
cart._deadline_call++;
if ((typeof django === 'undefined' || typeof django.gettext === 'undefined') && cart._deadline_call < 5) {
// Language files are not loaded yet, don't run during the first seconds
return;
}
var now = cart._get_now();
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 < 0) {
$('#cart-deadline').text(gettext('The items in your cart are no longer reserved for you. You can still complete your order as long as theyre available.'))
$('#cart-deadline-short').text(
gettext('Cart expired')
)
if (!cart._deadline_timeout) {
// no timeout => first time draw_deadline is invoked, but cart already expired => do not show dialog
cart._expiry_notified = true
}
} else {
if (diff_minutes !== cart._prev_diff_minutes) {
if (diff_minutes == 0) {
$('#cart-deadline').text(gettext('Your cart is about to expire.'))
} else {
$('#cart-deadline').text(
ngettext(
'The items in your cart are reserved for you for one minute.',
'The items in your cart are reserved for you for {num} minutes.',
diff_minutes
).replace(/\{num\}/g, diff_minutes)
)
}
cart._prev_diff_minutes = diff_minutes
}
if (diff_minutes < 0) {
$("#cart-deadline").text(gettext("The items in your cart are no longer reserved for you. You can still complete your order as long as theyre available."));
$("#cart-deadline-short").text(
gettext("Cart expired")
);
if (!cart._deadline_timeout) {
// no timeout => first time draw_deadline is invoked, but cart already expired => do not show dialog
cart._expiry_notified = true;
}
} else {
if (diff_minutes !== cart._prev_diff_minutes) {
if (diff_minutes == 0) {
$("#cart-deadline").text(gettext("Your cart is about to expire."))
} else {
$("#cart-deadline").text(
ngettext(
"The items in your cart are reserved for you for one minute.",
"The items in your cart are reserved for you for {num} minutes.",
diff_minutes
).replace(/\{num\}/g, diff_minutes)
);
}
cart._prev_diff_minutes = diff_minutes;
}
$('#cart-deadline-short').text(
pad(diff_minutes.toString(), 2) + ':' + pad(diff_seconds.toString(), 2)
)
$("#cart-deadline-short").text(
pad(diff_minutes.toString(), 2) + ':' + pad(diff_seconds.toString(), 2)
);
cart._deadline_timeout = window.setTimeout(cart.draw_deadline, 500)
}
let already_expired = diff_total_seconds <= 0
let 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()
$('#dialog-cart-extend-title').text(already_expired
? gettext('Your cart has expired.')
: gettext('Your cart is about to expire.'))
$('#dialog-cart-extend-description').text(already_expired
? gettext('The items in your cart are no longer reserved for you. You can still complete your order as long as they\'re available.')
: gettext('Do you want to renew the reservation period?'))
$('#dialog-cart-extend .modal-card-confirm button').text(already_expired
? gettext('Continue')
: gettext('Renew reservation'))
}
},
cart._deadline_timeout = window.setTimeout(cart.draw_deadline, 500);
}
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();
$("#dialog-cart-extend-title").text(already_expired
? gettext("Your cart has expired.")
: gettext("Your cart is about to expire."));
$("#dialog-cart-extend-description").text(already_expired
? gettext("The items in your cart are no longer reserved for you. You can still complete your order as long as they're available.")
: gettext("Do you want to renew the reservation period?"));
$("#dialog-cart-extend .modal-card-confirm button").text(already_expired
? gettext("Continue")
: gettext("Renew reservation"));
}
},
init: function () {
'use strict'
cart._calc_offset()
cart.set_deadline(
$('#cart-deadline').attr('data-expires'),
$('#cart-deadline').attr('data-max-expiry-extend')
)
},
init: function () {
"use strict";
cart._calc_offset();
cart.set_deadline(
$("#cart-deadline").attr("data-expires"),
$("#cart-deadline").attr("data-max-expiry-extend")
);
},
set_deadline: function (expiry, max_extend, renewed_message) {
'use strict'
cart._expiry_notified = false
cart._deadline = moment(expiry)
if (cart._deadline_timeout) {
window.clearTimeout(cart._deadline_timeout)
}
cart._deadline_timeout = null
cart._max_extend = moment(max_extend)
cart.draw_deadline()
}
}
set_deadline: function (expiry, max_extend, renewed_message) {
"use strict";
cart._expiry_notified = false;
cart._deadline = moment(expiry);
if (cart._deadline_timeout) {
window.clearTimeout(cart._deadline_timeout);
}
cart._deadline_timeout = null;
cart._max_extend = moment(max_extend);
cart.draw_deadline();
}
};
$(function () {
'use strict'
"use strict";
if ($('#cart-deadline').length) {
cart.init()
$('#cart-extend-confirmation-button').hide().on('blur', function () {
$(this).hide()
})
}
if ($("#cart-deadline").length) {
cart.init();
$("#cart-extend-confirmation-button").hide().on("blur", function() {
$(this).hide();
});
}
$('#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)
}
})
// renew-button in cart-panel is clicked, show inline dialog
$('#cart-extend-button').on('click', function () {
$('#cart-extend-form').one('pretix:async-task-success', function (_e, data) {
if (data.success) {
document.getElementById('cart-extend-confirmation-dialog').show()
}
})
})
$('#cart-extend-confirmation-dialog').on('keydown', function (e) {
if (e.key === 'Escape') {
this.close()
}
})
$("#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);
}
});
// renew-button in cart-panel is clicked, show inline dialog
$("#cart-extend-button").on("click", function() {
$("#cart-extend-form").one("pretix:async-task-success", function(e, data) {
if (data.success) {
document.getElementById("cart-extend-confirmation-dialog").show();
}
});
});
$("#cart-extend-confirmation-dialog").on("keydown", function (e) {
if(e.key === "Escape") {
this.close();
}
});
// renew-button in modal dialog is clicked, show modal dialog
$('#dialog-cart-extend form').submit(function () {
$('#cart-extend-form').one('pretix:async-task-success', function (_e, data) {
if (data.success) {
$('#dialog-cart-extended-title').text(data.message)
$('#dialog-cart-extended-description').text($('#cart-deadline').text())
document.getElementById('dialog-cart-extended').showModal()
}
}).submit()
})
// renew-button in modal dialog is clicked, show modal dialog
$("#dialog-cart-extend form").submit(function() {
$("#cart-extend-form").one("pretix:async-task-success", function(e, data) {
if (data.success) {
$("#dialog-cart-extended-title").text(data.message);
$("#dialog-cart-extended-description").text($("#cart-deadline").text());
document.getElementById("dialog-cart-extended").showModal();
}
}).submit();
});
$('.toggle-container').each(function () {
let summary = $('.toggle-summary', this)
let content = $('> :not(.toggle-summary)', this)
let toggle = summary.find('.toggle').on('click', function () {
this.ariaExpanded = !this.ariaExpanded
if (this.classList.contains('toggle-remove')) summary.attr('hidden', true)
content.show().find(':input:visible').first().focus()
})
if (toggle.attr('aria-expanded')) {
content.hide()
}
})
$(".toggle-container").each(function() {
var summary = $(".toggle-summary", this);
var content = $("> :not(.toggle-summary)", this);
var toggle = summary.find(".toggle").on("click", function(e) {
this.ariaExpanded = !this.ariaExpanded;
if (this.classList.contains("toggle-remove")) summary.attr("hidden", true);
content.show().find(":input:visible").first().focus();
});
if (toggle.attr("aria-expanded")) {
content.hide();
}
});
$('.cart-icon-details.collapse-lines').each(function () {
let $content = $(this).find('.content')
let original_html = $content.html()
let br_exp = /<br\s*\/?>/i
$content.text(original_html.split(br_exp).join(', '))
if ($content.get(0).scrollWidth > $content.get(0).offsetWidth) {
let $handler = $('<button>')
.text($(this).attr('data-expand-text'))
.addClass('btn btn-link collapse-handler')
.attr('type', 'button')
.attr('aria-controls', $content.attr('id'))
.attr('aria-expanded', 'false')
$handler.on('click', function () {
$content.html(original_html).removeClass('content')
$handler.attr('aria-expanded', 'true').attr('aria-hidden', 'true')
$handler.hide()
})
$(this).append($handler)
}
})
})
$(".cart-icon-details.collapse-lines").each(function () {
var $content = $(this).find(".content");
var original_html = $content.html();
var br_exp = /<br\s*\/?>/i;
$content.text(original_html.split(br_exp).join(', '));
if ($content.get(0).scrollWidth > $content.get(0).offsetWidth) {
var $handler = $("<button>")
.text($(this).attr("data-expand-text"))
.addClass("btn btn-link collapse-handler")
.attr("type", "button")
.attr("aria-controls", $content.attr('id'))
.attr("aria-expanded", "false");
$handler.on("click", function (ev) {
$content.html(original_html).removeClass("content");
$handler.attr("aria-expanded", "true").attr("aria-hidden", "true");
$handler.hide();
});
$(this).append($handler);
}
})
});
@@ -1,143 +1,145 @@
/*global $ */
$(function () {
window.pretix = window.pretix || {}
window.pretix = window.pretix || {};
let storage_key = $('#cookie-consent-storage-key').text()
let widget_consent = $('#cookie-consent-from-widget').text()
let consent_checkboxes = $('#cookie-consent-details input[type=checkbox][name]')
let consent_modal = document.getElementById('cookie-consent-modal')
var storage_key = $("#cookie-consent-storage-key").text();
var widget_consent = $("#cookie-consent-from-widget").text();
var consent_checkboxes = $("#cookie-consent-details input[type=checkbox][name]");
var consent_modal = document.getElementById("cookie-consent-modal");
function update_consent (consent, sessionOnly) {
if (storage_key && window.sessionStorage && sessionOnly) {
if (!window.localStorage[storage_key] || window.localStorage[storage_key] !== JSON.stringify(consent)) {
// No need to write to sessionStorage if the value is identical to the one in localStorage
window.sessionStorage[storage_key] = JSON.stringify(consent)
}
} else if (storage_key && window.localStorage) {
window.localStorage[storage_key] = JSON.stringify(consent)
// When saving permanent storage, clear session storage
window.sessionStorage.removeItem(storage_key)
}
window.pretix.cookie_consent = consent
function update_consent(consent, sessionOnly) {
if (storage_key && window.sessionStorage && sessionOnly) {
if (!window.localStorage[storage_key] || window.localStorage[storage_key] !== JSON.stringify(consent)) {
// No need to write to sessionStorage if the value is identical to the one in localStorage
window.sessionStorage[storage_key] = JSON.stringify(consent);
}
} else if (storage_key && window.localStorage) {
window.localStorage[storage_key] = JSON.stringify(consent);
// When saving permanent storage, clear session storage
window.sessionStorage.removeItem(storage_key);
}
window.pretix.cookie_consent = consent;
// Event() is not supported by IE11, see ployfill here:
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#polyfill
let e = document.createEvent('CustomEvent')
e.initCustomEvent('pretix:cookie-consent:change', true, true, consent)
document.dispatchEvent(e)
}
// Event() is not supported by IE11, see ployfill here:
// https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent#polyfill
var e = document.createEvent('CustomEvent');
e.initCustomEvent('pretix:cookie-consent:change', true, true, consent);
document.dispatchEvent(e)
}
if (!storage_key) {
// We are not on a page where the consent should run, fire the change event with empty consent but don't
// actually store anything.
update_consent(null, false)
return
}
if (!storage_key) {
// We are not on a page where the consent should run, fire the change event with empty consent but don't
// actually store anything.
update_consent(null, false);
return;
}
if (!window.localStorage) {
// Consent not supported. Even IE8 supports it, so we're on a weird embedded device.
// Let's just say we don't consent then.
update_consent({}, false)
return
}
if (!window.localStorage) {
// Consent not supported. Even IE8 supports it, so we're on a weird embedded device.
// Let's just say we don't consent then.
update_consent({}, false)
return;
}
let storage_val, consent_source, save_for_session_only
if (window.sessionStorage[storage_key]) {
// A manual input was given inside a widget. This is the user's last explicit choice and takes precedence
// as long as they are in the widget.
storage_val = JSON.parse(window.sessionStorage[storage_key])
consent_source = 'sessionStorage'
save_for_session_only = true
} else if (widget_consent) {
// An input was given through the widget. This takes precedence over localStorage as we need to assume the
// widget embedder is doing a correct job. If the user never visited the page without the widget, we also
// use it to prefill local storage to save the user from seeing more cookie banners. (This will stop working
// when browsers partition local storage of iframes, anyway.) If the user does have visited the page without
// the widget before and has a consent setting in localStorage, we respect the widget consent *only* within
// the widget -- hence, we save it into sessionStorage. We need to save it into sessionStorage because the
// widget_data value itself will not "survive" the entire lifetime of the tab, i.e. it is no longer present
// after the order was confirmed.
widget_consent = JSON.parse(widget_consent)
storage_val = {}
consent_checkboxes.each(function () {
this.checked = storage_val[this.name] = widget_consent.indexOf(this.name) > -1
})
consent_source = 'widget'
save_for_session_only = !!window.localStorage[storage_key]
} else if (window.localStorage[storage_key]) {
// The user made a specific selection, let's use that.
storage_val = JSON.parse(window.localStorage[storage_key]) || {}
consent_source = 'localStorage'
save_for_session_only = false
} else {
// No consent given, dialog will be shown.
storage_val = {}
consent_source = 'new'
save_for_session_only = false
}
var storage_val, consent_source, save_for_session_only;
if (window.sessionStorage[storage_key]) {
// A manual input was given inside a widget. This is the user's last explicit choice and takes precedence
// as long as they are in the widget.
storage_val = JSON.parse(window.sessionStorage[storage_key]);
consent_source = 'sessionStorage';
save_for_session_only = true;
} else if (widget_consent) {
// An input was given through the widget. This takes precedence over localStorage as we need to assume the
// widget embedder is doing a correct job. If the user never visited the page without the widget, we also
// use it to prefill local storage to save the user from seeing more cookie banners. (This will stop working
// when browsers partition local storage of iframes, anyway.) If the user does have visited the page without
// the widget before and has a consent setting in localStorage, we respect the widget consent *only* within
// the widget -- hence, we save it into sessionStorage. We need to save it into sessionStorage because the
// widget_data value itself will not "survive" the entire lifetime of the tab, i.e. it is no longer present
// after the order was confirmed.
widget_consent = JSON.parse(widget_consent);
storage_val = {};
consent_checkboxes.each(function () {
this.checked = storage_val[this.name] = widget_consent.indexOf(this.name) > -1;
});
consent_source = 'widget';
save_for_session_only = !!window.localStorage[storage_key];
} else if (window.localStorage[storage_key]) {
// The user made a specific selection, let's use that.
storage_val = JSON.parse(window.localStorage[storage_key]) || {};
consent_source = 'localStorage';
save_for_session_only = false;
} else {
// No consent given, dialog will be shown.
storage_val = {};
consent_source = 'new';
save_for_session_only = false;
}
let show_dialog = false
consent_checkboxes.each(function () {
if (typeof storage_val[this.name] === 'undefined') {
// A new cookie type has been added that we haven't asked for yet
if (consent_source === 'widget') {
// Trust the widget, keep it as "no consent"
} else {
show_dialog = true
}
} else if (storage_val[this.name]) {
this.checked = true
}
})
var show_dialog = false;
consent_checkboxes.each(function () {
if (typeof storage_val[this.name] === "undefined") {
// A new cookie type has been added that we haven't asked for yet
if (consent_source === "widget") {
// Trust the widget, keep it as "no consent"
} else {
show_dialog = true;
}
} else if (storage_val[this.name]) {
this.checked = true;
}
})
update_consent(storage_val, save_for_session_only)
update_consent(storage_val, save_for_session_only);
if (!consent_modal) {
// Cookie consent is active, but no provider defined
return
}
if (!consent_modal) {
// Cookie consent is active, but no provider defined
return;
}
function _set_button_text () {
let btn = $('#cookie-consent-button-no')
btn.text(
consent_checkboxes.filter(':checked').length
? btn.attr('data-detail-text')
: btn.attr('data-summary-text')
)
}
function _set_button_text () {
var btn = $("#cookie-consent-button-no");
btn.text(
consent_checkboxes.filter(":checked").length ?
btn.attr("data-detail-text") :
btn.attr("data-summary-text")
);
}
if (consent_checkboxes.filter(':checked').length) {
$('#cookie-consent-details').prop('open', true).find('> *:not(summary)').show()
}
if (consent_checkboxes.filter(":checked").length) {
$("#cookie-consent-details").prop("open", true).find("> *:not(summary)").show();
}
_set_button_text()
if (show_dialog) {
consent_modal.showModal()
consent_modal.addEventListener('cancel', function () {
// Dialog was initially shown, interpret Escape as „do not consent to new providers“
let consent = {}
consent_checkboxes.each(function () {
consent[this.name] = storage_val[this.name] || false
})
update_consent(consent, false)
}, { once: true })
}
_set_button_text();
if (show_dialog) {
consent_modal.showModal();
consent_modal.addEventListener("cancel", function() {
// Dialog was initially shown, interpret Escape as „do not consent to new providers“
var consent = {};
consent_checkboxes.each(function () {
consent[this.name] = storage_val[this.name] || false;
});
update_consent(consent, false);
}, {once : true});
}
consent_modal.addEventListener('close', function () {
if (!consent_modal.returnValue) { // ESC, do not save
return
}
let consent = {}
let consent_all = consent_modal.returnValue == 'yes'
consent_checkboxes.each(function () {
consent[this.name] = this.checked = consent_all || this.checked
})
if (consent_all) _set_button_text()
update_consent(consent, false)
})
consent_checkboxes.on('change', _set_button_text)
$('#cookie-consent-reopen').on('click', function (e) {
consent_modal.showModal()
e.preventDefault()
return true
})
})
consent_modal.addEventListener("close", function () {
if (!consent_modal.returnValue) {// ESC, do not save
return;
}
var consent = {};
var consent_all = consent_modal.returnValue == "yes";
consent_checkboxes.each(function () {
consent[this.name] = this.checked = consent_all || this.checked;
});
if (consent_all) _set_button_text();
update_consent(consent, false);
});
consent_checkboxes.on("change", _set_button_text);
$("#cookie-consent-reopen").on("click", function (e) {
consent_modal.showModal()
e.preventDefault()
return true
})
});
+16 -16
View File
@@ -1,18 +1,18 @@
let inIframe = function () {
try {
return window.self !== window.top
} catch (_e) {
return true
}
}
var inIframe = function () {
try {
return window.self !== window.top;
} catch (e) {
return true;
}
};
if (inIframe()) {
document.documentElement.classList.add('in-iframe')
try {
window.parent.postMessage({
type: 'pretix:widget:title',
title: document.title,
}, '*')
} catch (e) {
console.error('Could not post message to parent.', e)
}
document.documentElement.classList.add('in-iframe');
try {
window.parent.postMessage({
type: "pretix:widget:title",
title: document.title,
}, "*");
} catch (e) {
console.error("Could not post message to parent.", e);
}
}
File diff suppressed because it is too large Load Diff
+434 -426
View File
@@ -1,132 +1,134 @@
function questions_toggle_dependent (ev) {
function q_should_be_shown ($el) {
if (!$el.attr('data-question-dependency')) {
return true
}
/*global $ */
let dependency_name = $el.attr('name').split('_')[0] + '_' + $el.attr('data-question-dependency')
let dependency_values = JSON.parse($el.attr('data-question-dependency-values'))
let $dependency_el
function questions_toggle_dependent(ev) {
function q_should_be_shown($el) {
if (!$el.attr('data-question-dependency')) {
return true;
}
if ($('select[name=' + dependency_name + ']').length) {
// dependency is type C
$dependency_el = $('select[name=' + dependency_name + ']')
if (!$dependency_el.closest('.form-group').hasClass('dependency-hidden')) { // do not show things that depend on hidden things
return q_should_be_shown($dependency_el) && $.inArray($dependency_el.val(), dependency_values) > -1
}
} else if ($('input[type=checkbox][name=' + dependency_name + ']').length) {
// dependency type is B or M
if ($.inArray('True', dependency_values) > -1 || $.inArray('False', dependency_values) > -1) {
$dependency_el = $('input[name=' + dependency_name + ']')
if (!$dependency_el.closest('.form-group').hasClass('dependency-hidden')) { // do not show things that depend on hidden things
return q_should_be_shown($dependency_el) && (
($.inArray('True', dependency_values) > -1 && $dependency_el.prop('checked'))
|| ($.inArray('False', dependency_values) > -1 && !$dependency_el.prop('checked'))
)
}
} else {
let filter = ''
for (let i = 0; i < dependency_values.length; i++) {
if (filter) filter += ', '
filter += 'input[value=' + dependency_values[i] + '][name=' + dependency_name + ']:checked'
}
$dependency_el = $('input[value=' + dependency_values[0] + '][name=' + dependency_name + ']')
if (!$dependency_el.closest('.form-group').hasClass('dependency-hidden')) { // do not show things that depend on hidden things
return q_should_be_shown($dependency_el) && $(filter).length
}
}
}
}
var dependency_name = $el.attr("name").split("_")[0] + "_" + $el.attr("data-question-dependency");
var dependency_values = JSON.parse($el.attr("data-question-dependency-values"));
var $dependency_el;
$('[data-question-dependency]').each(function () {
let $dependent = $(this).closest('.form-group')
let is_shown = !$dependent.hasClass('dependency-hidden')
let should_be_shown = q_should_be_shown($(this))
if ($("select[name=" + dependency_name + "]").length) {
// dependency is type C
$dependency_el = $("select[name=" + dependency_name + "]");
if (!$dependency_el.closest(".form-group").hasClass("dependency-hidden")) { // do not show things that depend on hidden things
return q_should_be_shown($dependency_el) && $.inArray($dependency_el.val(), dependency_values) > -1;
}
} else if ($("input[type=checkbox][name=" + dependency_name + "]").length) {
// dependency type is B or M
if ($.inArray("True", dependency_values) > -1 || $.inArray("False", dependency_values) > -1) {
$dependency_el = $("input[name=" + dependency_name + "]");
if (!$dependency_el.closest(".form-group").hasClass("dependency-hidden")) { // do not show things that depend on hidden things
return q_should_be_shown($dependency_el) && (
($.inArray("True", dependency_values) > -1 && $dependency_el.prop('checked'))
|| ($.inArray("False", dependency_values) > -1 && !$dependency_el.prop('checked'))
);
}
} else {
var filter = "";
for (var i = 0; i < dependency_values.length; i++) {
if (filter) filter += ", ";
filter += "input[value=" + dependency_values[i] + "][name=" + dependency_name + "]:checked";
}
$dependency_el = $("input[value=" + dependency_values[0] + "][name=" + dependency_name + "]");
if (!$dependency_el.closest(".form-group").hasClass("dependency-hidden")) { // do not show things that depend on hidden things
return q_should_be_shown($dependency_el) && $(filter).length;
}
}
}
}
if (should_be_shown && !is_shown) {
$dependent.stop().removeClass('dependency-hidden')
if (!ev) {
$dependent.show()
} else {
$dependent.slideDown()
}
$dependent.find('input.required-hidden, select.required-hidden, textarea.required-hidden').each(function () {
$(this).prop('required', true).removeClass('required-hidden')
})
} else if (!should_be_shown && is_shown) {
if ($dependent.hasClass('has-error') || $dependent.find('.has-error').length) {
// Do not hide things with invalid validation
return
}
$dependent.stop().addClass('dependency-hidden')
if (!ev) {
$dependent.hide()
} else {
$dependent.slideUp()
}
$dependent.find('input[required], select[required], textarea[required]').each(function () {
$(this).prop('required', false).addClass('required-hidden')
})
}
})
$("[data-question-dependency]").each(function () {
var $dependent = $(this).closest(".form-group");
var is_shown = !$dependent.hasClass("dependency-hidden");
var should_be_shown = q_should_be_shown($(this));
if (should_be_shown && !is_shown) {
$dependent.stop().removeClass("dependency-hidden");
if (!ev) {
$dependent.show();
} else {
$dependent.slideDown();
}
$dependent.find("input.required-hidden, select.required-hidden, textarea.required-hidden").each(function () {
$(this).prop("required", true).removeClass("required-hidden");
});
} else if (!should_be_shown && is_shown) {
if ($dependent.hasClass("has-error") || $dependent.find(".has-error").length) {
// Do not hide things with invalid validation
return;
}
$dependent.stop().addClass("dependency-hidden");
if (!ev) {
$dependent.hide();
} else {
$dependent.slideUp();
}
$dependent.find("input[required], select[required], textarea[required]").each(function () {
$(this).prop("required", false).addClass("required-hidden");
});
}
});
}
function questions_init_photos (el) {
if (!FileReader) {
// No browser support
return
}
function questions_init_photos(el) {
if (!FileReader) {
// No browser support
return
}
el.find('input[data-portrait-photo]').each(function () {
let $inp = $(this)
let $container = $inp.parent().parent()
el.find("input[data-portrait-photo]").each(function () {
var $inp = $(this)
var $container = $inp.parent().parent()
$inp.prop('required', false)
$container.find('.photo-input').addClass('hidden')
$container.find('.photo-buttons').removeClass('hidden')
$inp.prop("required", false)
$container.find(".photo-input").addClass("hidden")
$container.find(".photo-buttons").removeClass("hidden")
$container.find('button[data-action=upload]').click(function () {
$inp.click()
})
$container.find("button[data-action=upload]").click(function () {
$inp.click();
})
var cropper = new Cropper($container.find('.photo-preview img').get(0), {
aspectRatio: 3 / 4,
viewMode: 1,
zoomable: false,
crop: function (event) {
$container.find('input[type=hidden]').val(JSON.stringify(cropper.getData(true)))
},
})
/* This rule is very important, please don't ignore this */
var cropper = new Cropper($container.find(".photo-preview img").get(0), {
aspectRatio: 3 / 4,
viewMode: 1,
zoomable: false,
crop: function (event) {
$container.find("input[type=hidden]").val(JSON.stringify(cropper.getData(true)));
},
});
/* This rule is very important, please don't ignore this */
$inp.on('change', function () {
if (!$inp.get(0).files[0]) return
$container.find('button[data-action=upload]').append('<span class=\'fa fa-spin fa-cog\'></span>')
let fr = new FileReader()
fr.onload = function () {
cropper.replace(fr.result)
$container.find('.photo-preview').removeClass('hidden')
$container.find('button[data-action=upload] .fa-spin').remove()
}
fr.readAsDataURL($inp.get(0).files[0])
})
})
$inp.on("change", function () {
if (!$inp.get(0).files[0]) return
$container.find("button[data-action=upload]").append("<span class='fa fa-spin fa-cog'></span>")
var fr = new FileReader()
fr.onload = function () {
cropper.replace(fr.result)
$container.find(".photo-preview").removeClass("hidden")
$container.find("button[data-action=upload] .fa-spin").remove()
}
fr.readAsDataURL($inp.get(0).files[0])
})
});
}
function questions_init_profiles (el) {
/*
function questions_init_profiles(el) {
/*
Auto-fill answers with profiles and addresses from customer account.
There are two types of profiles:
1. profiles for answers and
1. profiles for answers and
2. profiles for invoice addresses
Both are handled the same way.
Each form section/fieldset has its own auto-fill and save to profile
inputs. Each fieldset can define its own profiles by providing the
Each form section/fieldset has its own auto-fill and save to profile
inputs. Each fieldset can define its own profiles by providing the
HTML-attribute data-profiles-id, which defaults to "profiles_json".
Currently only the invoice address fieldset uses this to load a
Currently only the invoice address fieldset uses this to load a
different set of profiles.
For each section each profiles answers are matched to inputs inside
@@ -139,343 +141,349 @@ function questions_init_profiles (el) {
in the original profile description, strikethrough which answer
will be overwritten, followed by the new answer
add new answers with a + in front
change <select> to a list of radio-buttons for multiline-display
change <select> to a list of radio-buttons for multiline-display
of profiles?
*/
let profilesById = {}
function getProfilesById (id) {
if (!(id in profilesById)) {
let element = document.getElementById(id)
profilesById[id] = (!element || !element.textContent) ? [] : JSON.parse(element.textContent)
}
return profilesById[id]
}
var profilesById = {};
function getProfilesById(id) {
if (!(id in profilesById)) {
var element = document.getElementById(id);
profilesById[id] = (!element || !element.textContent) ? [] : JSON.parse(element.textContent);
}
return profilesById[id];
}
function matchProfilesToInputs (profiles, scope) {
let filtered = []
let data
let matched_field
let addSpecialKey
// special fields are used for substition with human readable or pre-formatted values
let addSpecialFieldMap = {
country: '_country_for_address',
state: '_state_for_address',
name_parts_0: '_name',
attendee_name_parts_0: '_attendee_name',
}
for (let p of profiles) {
data = {}
for (let key of Object.keys(p)) {
if (key.startsWith('_') || p[key] === null) {
continue
}
matched_field = getMatchingInput(key, p[key], scope)
if (matched_field) {
// TODO: only add if no other answer matches same fields?
data[key] = {
value: (typeof p[key] == 'string') ? p[key] : p[key]['value'],
field: matched_field
}
if (p[key]['label']) data[key]['label'] = p[key]['label']
if (p[key]['type']) data[key]['type'] = p[key]['type']
if (addSpecialKey = addSpecialFieldMap[key]) {
data[addSpecialKey] = p[addSpecialKey]
}
}
}
if (Object.keys(data).length) filtered.push(data)
};
return filtered
}
// For auto-fill with few inputs it could happen that multiple profiles
// only match with the same fields that have the same values. It makes
// no sense to show multiple profiles if all fill the same value(s).
// Therefore filter profiles to unique ones.
function uniqueProfiles (profiles) {
let uniques = []
let matchIndex
for (var p of profiles) {
matchIndex = uniques.findIndex(function (element, index, array) {
return _profilesAreEqual(element, p)
})
if (matchIndex === -1) uniques.push(p)
}
return uniques
}
function _profilesAreEqual (a, b) {
let keysA = Object.keys(a)
let keysB = Object.keys(b)
if (keysA.length !== keysB.length) return false
keysA.sort()
keysB.sort()
if (!keysA.every((val, index) => val === keysB[index])) return false
if (!keysA.every((key, index) => a[key].value === b[key].value)) return false
return true
}
function matchProfilesToInputs(profiles, scope) {
var filtered = [];
var data;
var matched_field;
var addSpecialKey;
// special fields are used for substition with human readable or pre-formatted values
var addSpecialFieldMap = {
"country": "_country_for_address",
"state": "_state_for_address",
"name_parts_0": "_name",
"attendee_name_parts_0": "_attendee_name",
}
for (var p of profiles) {
data = {};
for (var key of Object.keys(p)) {
if (key.startsWith("_") || p[key] === null) {
continue;
}
matched_field = getMatchingInput(key, p[key], scope);
if (matched_field) {
// TODO: only add if no other answer matches same fields?
data[key] = {
"value": (typeof p[key] == "string") ? p[key] : p[key]["value"],
"field": matched_field
};
if (p[key]["label"]) data[key]["label"] = p[key]["label"];
if (p[key]["type"]) data[key]["type"] = p[key]["type"];
if (addSpecialKey = addSpecialFieldMap[key]) {
data[addSpecialKey] = p[addSpecialKey];
}
}
}
if (Object.keys(data).length) filtered.push(data);
};
return filtered;
}
// For auto-fill with few inputs it could happen that multiple profiles
// only match with the same fields that have the same values. It makes
// no sense to show multiple profiles if all fill the same value(s).
// Therefore filter profiles to unique ones.
function uniqueProfiles(profiles) {
var uniques = [];
var matchIndex;
for (var p of profiles) {
matchIndex = uniques.findIndex(function(element, index, array) {
return _profilesAreEqual(element, p);
});
if (matchIndex === -1) uniques.push(p);
}
return uniques;
}
function _profilesAreEqual(a, b) {
var keysA = Object.keys(a);
var keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
keysA.sort();
keysB.sort();
if (!keysA.every((val, index) => val === keysB[index])) return false;
if (!keysA.every((key, index) => a[key].value === b[key].value)) return false;
return true;
}
function _getInputForLabel (label) {
if (!label) return null
let input
if (label.getAttribute('for')) {
input = document.getElementById(label.getAttribute('for'))
if (input) return input
}
// for grouped inputs like phone number the "label" is more a fieldset/legend
return label.closest('.form-group').querySelectorAll('select, input, textarea')
}
function getMatchingInput (key, answer, scope) {
let $label
// _0 and _1 are e.g. for phone-fields. name-fields have their parts/keys already split
let $fields = $('[name$="' + key + '"], [name$="' + key + '_0"], [name$="' + key + '_1"]', scope).not(':disabled')
if ($fields.length) return $fields
if (!answer) return null
function _getInputForLabel(label) {
if (!label) return null;
var input;
if (label.getAttribute("for")) {
input = document.getElementById(label.getAttribute("for"));
if (input) return input;
}
// for grouped inputs like phone number the "label" is more a fieldset/legend
return label.closest(".form-group").querySelectorAll("select, input, textarea");
}
function getMatchingInput(key, answer, scope) {
var $label;
// _0 and _1 are e.g. for phone-fields. name-fields have their parts/keys already split
var $fields = $('[name$="' + key + '"], [name$="' + key + '_0"], [name$="' + key + '_1"]', scope).not(":disabled");
if ($fields.length) return $fields;
if (!answer) return null;
if (answer.identifier) {
$label = $('[data-identifier="' + answer.identifier + '"]', scope)
var input = _getInputForLabel($label.get(0))
if (input) return $(input)
}
for (let label of scope.getElementsByTagName('label')) {
if (label.textContent === answer.label) {
var input = _getInputForLabel(label)
if (input) return $(input)
break
}
}
return null
}
if (answer.identifier) {
$label = $('[data-identifier="' + answer.identifier + '"]', scope);
var input = _getInputForLabel($label.get(0));
if (input) return $(input);
}
for (var label of scope.getElementsByTagName("label")) {
if (label.textContent === answer.label) {
var input = _getInputForLabel(label);
if (input) return $(input);
break;
}
}
return null;
}
function formatAnswerHumanReadable (answer) {
if (typeof answer == 'string') return answer
if (typeof answer == 'number') return answer.toString()
if (!answer && answer !== false) return ''
let value = answer.value
if ('type' in answer) {
if (answer.type === 'TEL') {
// TODO: format phone number with locale or use pre-formatted like with names?
return value
}
if (answer.type === 'W') {
return moment(value).format(document.body.getAttribute('data-datetimeformat'))
}
if (answer.type === 'D') {
return moment(value).format(document.body.getAttribute('data-dateformat'))
}
if (answer.type === 'H') {
let format = document.body.getAttribute('data-timeformat')
return moment(value, 'HH:mm:ss').format(format)
}
if (answer.type === 'B') {
return value ? gettext('Yes') : gettext('No')
}
}
if (typeof value == 'string') return value
if (!value) return ''
return Object.values(value).join(', ')
}
function formatAnswerHumanReadable(answer) {
if (typeof answer == "string") return answer;
if (typeof answer == "number") return answer.toString();
if (!answer && answer !== false) return "";
var value = answer.value;
if ("type" in answer) {
if (answer.type === "TEL") {
// TODO: format phone number with locale or use pre-formatted like with names?
return value;
}
if (answer.type === "W") {
return moment(value).format(document.body.getAttribute("data-datetimeformat"));
}
if (answer.type === "D") {
return moment(value).format(document.body.getAttribute("data-dateformat"));
}
if (answer.type === "H") {
var format = document.body.getAttribute("data-timeformat");
return moment(value, "HH:mm:ss").format(format);
}
if (answer.type === "B") {
return value ? gettext("Yes") : gettext("No");
}
}
if (typeof value == "string") return value;
if (!value) return "";
return Object.values(value).join(", ");
}
// TODO: add as few info as possible to make a distinction between available profiles?
function labelForProfile (p, profiles, scope = null) {
let parts = describeProfile(p)
let label = parts.join(', ')
if (label.length > 74) {
let len = label.lastIndexOf(' ', 74)
label = label.substr(0, Math.max(len, 48)) + ' …'
}
return label
}
function getAnswer (a) {
if (typeof a == 'string') return a
return a && 'value' in a ? a['value'] : ''
}
function describeProfile (p) {
if (!p) return []
let lines = [
getAnswer(p['company']),
p['_name'],
[p['_attendee_name'], getAnswer(p['attendee_email'])].filter(v => v).join(', '),
[
getAnswer(p['street']),
[getAnswer(p['zipcode']), getAnswer(p['city']), p['_state_for_address']].filter(v => v).join(' '),
p['_country_for_address']
].filter(v => v).join(', ')
]
lines = lines.filter(line => line && line.trim())
// TODO: add as few info as possible to make a distinction between available profiles?
function labelForProfile(p, profiles, scope = null) {
var parts = describeProfile(p);
var label = parts.join(", ");
if (label.length > 74) {
var len = label.lastIndexOf(' ', 74);
label = label.substr(0, Math.max(len, 48)) + " …";
}
return label;
}
function getAnswer(a) {
if (typeof a == "string") return a;
return a && "value" in a ? a["value"] : "";
}
function describeProfile(p) {
if (!p) return [];
var lines = [
getAnswer(p["company"]),
p["_name"],
[p["_attendee_name"], getAnswer(p["attendee_email"])].filter(v => v).join(", "),
[
getAnswer(p["street"]),
[getAnswer(p["zipcode"]), getAnswer(p["city"]), p["_state_for_address"]].filter(v => v).join(" "),
p["_country_for_address"]
].filter(v => v).join(", ")
];
lines = lines.filter(line => line && line.trim());
let answer
let label
for (let key of Object.keys(p)) {
if (!key.startsWith('question_')) continue
answer = p[key]
label = answer['label'] || ''
lines.push(label + ('!?.:'.split('').indexOf(label.slice(-1)) > -1 ? ' ' : ': ') + formatAnswerHumanReadable(answer))
}
return lines
}
function escapeHTML (t) {
return $('<div>').text(t).get(0).innerHTML
}
function describeProfileHTML (p) {
return describeProfile(p).map(escapeHTML).join('<br>')
}
var answer;
var label;
for (var key of Object.keys(p)) {
if (!key.startsWith("question_")) continue;
answer = p[key];
label = answer["label"] || "";
lines.push(label + ("!?.:".split("").indexOf(label.slice(-1)) > -1 ? " " : ": ") + formatAnswerHumanReadable(answer))
}
return lines;
}
function escapeHTML(t) {
return $("<div>").text(t).get(0).innerHTML;
}
function describeProfileHTML(p) {
return describeProfile(p).map(escapeHTML).join("<br>");
}
function _updateDescription (select, profile, $help) {
// show additional description if different from option-text
let label = select.options[select.selectedIndex].textContent
let lines = describeProfile(profile).map(escapeHTML)
if (!lines.length || label === lines.join(', ')) {
$help.slideUp(function () {
$help.html('')
})
} else {
$help.html(lines.join('<br>')).slideDown()
}
}
function setupSaveToProfile (scope, profiles) {
let $select = $('[name$="saved_id"]', scope)
let $selectContainer = $select.closest('.form-group').addClass('profile-save-id')
let $checkbox = $('[name$="save"]', scope)
let $checkboxContainer = $checkbox.closest('.form-group').addClass('profile-save')
let $help = $selectContainer.find('.help-block')
function _updateDescription(select, profile, $help) {
// show additional description if different from option-text
var label = select.options[select.selectedIndex].textContent;
var lines = describeProfile(profile).map(escapeHTML);
if (!lines.length || label === lines.join(", ")) {
$help.slideUp(function() {
$help.html("");
});
}
else {
$help.html(lines.join("<br>")).slideDown();
}
}
let $container = $('<div class=\'profile-save-container js-do-not-copy-answers\'></div>')
$selectContainer.after($container)
$container.append($checkboxContainer)
$container.append($selectContainer)
function setupSaveToProfile(scope, profiles) {
var $select = $('[name$="saved_id"]', scope);
var $selectContainer = $select.closest(".form-group").addClass("profile-save-id");
var $checkbox = $('[name$="save"]', scope);
var $checkboxContainer = $checkbox.closest(".form-group").addClass("profile-save");
var $help = $selectContainer.find(".help-block");
if (!profiles || !profiles.length) {
$selectContainer.hide()
return
}
var $container = $("<div class='profile-save-container js-do-not-copy-answers'></div>");
$selectContainer.after($container);
$container.append($checkboxContainer);
$container.append($selectContainer);
if (!profiles || !profiles.length) {
$selectContainer.hide();
return;
}
$checkbox.change(function () {
if (this.checked) $selectContainer.slideDown()
else $selectContainer.slideUp()
})
$checkbox.change(function() {
if (this.checked) $selectContainer.slideDown();
else $selectContainer.slideUp();
});
for (let p of profiles) {
$select.append($('<option>').attr('value', p._pk).text(labelForProfile(p, profiles)))
}
$select.append('<option value="" disabled></option>')
$select.append($select.find('option').first())
$select.get(0).selectedIndex = 0
$select.change(function () {
_updateDescription(this, profiles[this.selectedIndex], $help)
}).trigger('change')
$checkbox.trigger('change')
}
for (var p of profiles) {
$select.append($('<option>').attr('value', p._pk).text(labelForProfile(p, profiles)));
}
$select.append('<option value="" disabled></option>');
$select.append($select.find("option").first());
$select.get(0).selectedIndex = 0;
$select.change(function() {
_updateDescription(this, profiles[this.selectedIndex], $help);
}).trigger("change");
$checkbox.trigger("change");
}
// setup auto-fill for each scope/fieldset
// match profiles answers to inputs in scope
// if none match, do not show auto-fill
// if one matches, only show button to auto-fill
// else show select with profiles and button to auto-fill
function setupAutoFill (scope, profiles) {
let matchedProfiles = uniqueProfiles(matchProfilesToInputs(profiles, scope))
if (!matchedProfiles.length) {
$(scope).addClass('profile-none-matched')
return
}
// setup auto-fill for each scope/fieldset
// match profiles answers to inputs in scope
// if none match, do not show auto-fill
// if one matches, only show button to auto-fill
// else show select with profiles and button to auto-fill
function setupAutoFill(scope, profiles) {
var matchedProfiles = uniqueProfiles(matchProfilesToInputs(profiles, scope));
if (!matchedProfiles.length) {
$(scope).addClass("profile-none-matched");
return;
}
let selectedProfile = matchedProfiles[0]
let $select = $('.profile-select', scope)
let $button = $('.profile-apply', scope)
let $help = $('.profile-desc', scope)
var selectedProfile = matchedProfiles[0];
var $select = $(".profile-select", scope);
var $button = $(".profile-apply", scope);
var $help = $(".profile-desc", scope);
if (matchedProfiles.length === 1) {
$('.profile-select-control', scope).hide().parent().addClass('form-control-text')
$help.html(describeProfileHTML(selectedProfile)).addClass('single-profile-desc').after($button)
} else {
let i = 0
for (p of matchedProfiles) {
$select.append($('<option>').text(labelForProfile(p, matchedProfiles, scope)).attr('value', i))
i++
}
$select.change(function () {
selectedProfile = matchedProfiles[this.value]
_updateDescription(this, selectedProfile, $help)
}).trigger('change')
}
// Add-Ons sit on same level as their parent product scope
// Therefore use .prevUntil("legend") as an Add-On is
// offset by a <legend>
// if no <legend> is present e.g. on invoice-address the
// containing <summary> would be selected, which is not what we want
$(scope).prevUntil('legend').not('summary').addClass('profile-pre-select')
if (matchedProfiles.length === 1) {
$(".profile-select-control", scope).hide().parent().addClass("form-control-text");
$help.html(describeProfileHTML(selectedProfile)).addClass("single-profile-desc").after($button);
}
else {
var i = 0;
for (p of matchedProfiles) {
$select.append($("<option>").text(labelForProfile(p, matchedProfiles, scope)).attr("value", i));
i++;
}
$select.change(function() {
selectedProfile = matchedProfiles[this.value];
_updateDescription(this, selectedProfile, $help);
}).trigger("change");
}
// Add-Ons sit on same level as their parent product scope
// Therefore use .prevUntil("legend") as an Add-On is
// offset by a <legend>
// if no <legend> is present e.g. on invoice-address the
// containing <summary> would be selected, which is not what we want
$(scope).prevUntil("legend").not("summary").addClass("profile-pre-select");
$button.click(function () {
Object.keys(selectedProfile).forEach(function (key) {
let answer = selectedProfile[key].value
let $field = selectedProfile[key].field
if (!$field || !$field.length) return
$button.click(function() {
Object.keys(selectedProfile).forEach(function(key) {
var answer = selectedProfile[key].value;
var $field = selectedProfile[key].field;
if (!$field || !$field.length) return;
if ($field.attr("type") === "checkbox") {
if (answer === true || answer === false) {
// boolean
$field.prop("checked", answer).trigger("change");
}
else if (typeof answer !== 'string') {
answer = Object.keys(answer);
$field.each(function() {
var checked = answer.indexOf(this.value) > -1;
if (checked !== this.checked) {
this.checked = checked;
$(this).trigger("change");
}
});
}
} else if ($field.attr("type") === "radio") {
$field.filter('[value="' + answer + '"]').prop("checked", true).trigger("change");
} else if ($field.length > 1) {
// multiple matching fields, could be phone number or datetime
var $field_0 = $field.filter('[name$="_0"]');
var $field_1 = $field.filter('[name$="_1"]');
if (answer.substr(0, 1) === "+") {
var prefix = "";
var options = $field_0.get(0).options;
for (var i = 0; i < options.length; i++) {
var v = options[i].value;
if (v && answer.substr(0, v.length) === v) {
prefix = v;
break;
}
}
var number = answer.substr(prefix.length);
$field_0.val(prefix).trigger("change");
$field_1.val(number).trigger("change");
}
else if ($field_0.hasClass("datepickerfield")) {
$field_0.data('DateTimePicker').date(moment(answer));
$field_1.data('DateTimePicker').date(moment(answer));
}
} else if ($field.is("select")) {
if (answer && typeof answer !== 'string') {
answer = Object.keys(answer);
}
// save answer as data-attribute so if external event changes select-element/options it can select correct entries
// currently used when country => state changes
$field.prop("data-selected-value", answer);
$field.find("option").each(function() {
this.selected = this.value === answer || (answer && answer.indexOf && answer.indexOf(this.value) > -1);
});
$field.trigger("change");
} else {
if ($field.hasClass("datepickerfield")) {
$field.data('DateTimePicker').date(moment(answer));
}
else {
$field.val(answer).trigger("change");
}
}
});
})
}
if ($field.attr('type') === 'checkbox') {
if (answer === true || answer === false) {
// boolean
$field.prop('checked', answer).trigger('change')
} else if (typeof answer !== 'string') {
answer = Object.keys(answer)
$field.each(function () {
let checked = answer.indexOf(this.value) > -1
if (checked !== this.checked) {
this.checked = checked
$(this).trigger('change')
}
})
}
} else if ($field.attr('type') === 'radio') {
$field.filter('[value="' + answer + '"]').prop('checked', true).trigger('change')
} else if ($field.length > 1) {
// multiple matching fields, could be phone number or datetime
let $field_0 = $field.filter('[name$="_0"]')
let $field_1 = $field.filter('[name$="_1"]')
if (answer.substr(0, 1) === '+') {
let prefix = ''
let options = $field_0.get(0).options
for (let i = 0; i < options.length; i++) {
let v = options[i].value
if (v && answer.substr(0, v.length) === v) {
prefix = v
break
}
}
let number = answer.substr(prefix.length)
$field_0.val(prefix).trigger('change')
$field_1.val(number).trigger('change')
} else if ($field_0.hasClass('datepickerfield')) {
$field_0.data('DateTimePicker').date(moment(answer))
$field_1.data('DateTimePicker').date(moment(answer))
}
} else if ($field.is('select')) {
if (answer && typeof answer !== 'string') {
answer = Object.keys(answer)
}
// save answer as data-attribute so if external event changes select-element/options it can select correct entries
// currently used when country => state changes
$field.prop('data-selected-value', answer)
$field.find('option').each(function () {
this.selected = this.value === answer || (answer && answer.indexOf && answer.indexOf(this.value) > -1)
})
$field.trigger('change')
} else {
if ($field.hasClass('datepickerfield')) {
$field.data('DateTimePicker').date(moment(answer))
} else {
$field.val(answer).trigger('change')
}
}
})
})
}
// each fieldset is its own scope for auto-fill and save
el.find(".profile-scope").each(function () {
var profiles = getProfilesById(this.getAttribute("data-profiles-id") || "profiles_json");
// each fieldset is its own scope for auto-fill and save
el.find('.profile-scope').each(function () {
let profiles = getProfilesById(this.getAttribute('data-profiles-id') || 'profiles_json')
setupSaveToProfile(this, profiles);
setupAutoFill(this, profiles);
setupSaveToProfile(this, profiles)
setupAutoFill(this, profiles)
this.classList.add('profile-select-initialized')
})
this.classList.add("profile-select-initialized");
});
}
+45 -43
View File
@@ -1,51 +1,53 @@
/*global $ */
$(function () {
let popup_window = null
let popup_check_interval = null
var popup_window = null
var popup_check_interval = null
$('a[data-open-in-popup-window]').on('click', function (e) {
e.preventDefault()
$("a[data-open-in-popup-window]").on("click", function (e) {
e.preventDefault()
$('#popupmodal a').attr('href', this.href)
$("#popupmodal a").attr("href", this.href)
let url = this.href
if (url.includes('?')) {
url += '&popup_origin=' + window.location.origin
} else {
url += '?popup_origin=' + window.location.origin
}
popup_window = window.open(
url,
'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 has-modal-dialog')
$('#popupmodal').removeAttr('hidden')
var url = this.href
if (url.includes("?")) {
url += "&popup_origin=" + window.location.origin
} else {
url += "?popup_origin=" + window.location.origin
}
popup_window = window.open(
url,
"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 has-modal-dialog")
$("#popupmodal").removeAttr("hidden");
popup_check_interval = window.setInterval(function () {
if (popup_window.closed) {
$('body').removeClass('has-popup has-modal-dialog')
$('#popupmodal').attr('hidden', true)
window.clearInterval(popup_check_interval)
}
}, 250)
popup_check_interval = window.setInterval(function () {
if (popup_window.closed) {
$("body").removeClass("has-popup has-modal-dialog")
$("#popupmodal").attr("hidden", true);
window.clearInterval(popup_check_interval)
}
}, 250)
return false
})
return false
});
window.addEventListener('message', function (event) {
if (event.source !== popup_window)
return
if (event.data && event.data.__process === 'customer_sso_popup') {
if (event.data.status === 'ok') {
$('#customer_login .alert.alert-danger').addClass('hidden')
$('#login_sso_data').val(event.data.value)
$('#login_sso_data').closest('form').get(0).submit()
} else {
$('#customer_login .alert.alert-danger').html(event.data.value)
$('#customer_login .alert.alert-danger').removeClass('hidden')
}
event.source.postMessage({ __process: 'popup_close' }, '*')
}
console.log(event)
}, false)
window.addEventListener("message", function (event) {
if (event.source !== popup_window)
return
if (event.data && event.data.__process === "customer_sso_popup") {
if (event.data.status === "ok") {
$("#customer_login .alert.alert-danger").addClass("hidden");
$("#login_sso_data").val(event.data.value)
$("#login_sso_data").closest("form").get(0).submit()
} else {
$("#customer_login .alert.alert-danger").html(event.data.value);
$("#customer_login .alert.alert-danger").removeClass("hidden");
}
event.source.postMessage({'__process': 'popup_close'}, "*")
}
console.log(event)
}, false);
})
@@ -1,72 +1,71 @@
/* global google, gettext */
'use strict'
'use strict';
let walletdetection = {
applepay: async function () {
// This is a weak check for Apple Pay - in order to do a proper check, we would need to also call
// canMakePaymentsWithActiveCard(merchantIdentifier)
var walletdetection = {
applepay: async function () {
// This is a weak check for Apple Pay - in order to do a proper check, we would need to also call
// canMakePaymentsWithActiveCard(merchantIdentifier)
return !!(window.ApplePaySession && window.ApplePaySession.canMakePayments())
},
googlepay: async function () {
// Checking for Google Pay is a little bit more involved, since it requires including the Google Pay JS SDK, and
// providing a lot of information.
// So for the time being, we only check if Google Pay is available in TEST-mode, which should hopefully give us a
// good enough idea if Google Pay could be present on this device; even though there are still a lot of other
// factors that could inhibit Google Pay from actually being offered to the customer.
return !!(window.ApplePaySession && window.ApplePaySession.canMakePayments());
},
googlepay: async function () {
// Checking for Google Pay is a little bit more involved, since it requires including the Google Pay JS SDK, and
// providing a lot of information.
// So for the time being, we only check if Google Pay is available in TEST-mode, which should hopefully give us a
// good enough idea if Google Pay could be present on this device; even though there are still a lot of other
// factors that could inhibit Google Pay from actually being offered to the customer.
return $.ajax({
url: 'https://pay.google.com/gp/p/js/pay.js',
dataType: 'script',
}).then(function () {
const paymentsClient = new google.payments.api.PaymentsClient({ environment: 'TEST' })
return paymentsClient.isReadyToPay({
apiVersion: 2,
apiVersionMinor: 0,
allowedPaymentMethods: [{
type: 'CARD',
parameters: {
allowedAuthMethods: ['PAN_ONLY', 'CRYPTOGRAM_3DS'],
allowedCardNetworks: ['AMEX', 'DISCOVER', 'INTERAC', 'JCB', 'MASTERCARD', 'VISA']
}
}],
})
}).then(function (response) {
return !!response.result
})
},
name_map: {
applepay: gettext('Apple Pay'),
googlepay: gettext('Google Pay'),
}
return $.ajax({
url: 'https://pay.google.com/gp/p/js/pay.js',
dataType: 'script',
}).then(function() {
const paymentsClient = new google.payments.api.PaymentsClient({environment: 'TEST'});
return paymentsClient.isReadyToPay({
apiVersion: 2,
apiVersionMinor: 0,
allowedPaymentMethods: [{
type: 'CARD',
parameters: {
allowedAuthMethods: ["PAN_ONLY", "CRYPTOGRAM_3DS"],
allowedCardNetworks: ["AMEX", "DISCOVER", "INTERAC", "JCB", "MASTERCARD", "VISA"]
}
}],
})
}).then(function (response) {
return !!response.result;
});
},
name_map: {
applepay: gettext('Apple Pay'),
googlepay: gettext('Google Pay'),
}
}
$(function () {
const wallets = $('[data-wallets]')
.map(function (_index, pm) {
return pm.getAttribute('data-wallets').split('|')
})
.get()
.flat()
.filter(function (item, pos, self) {
// filter out empty or duplicate values
return item && self.indexOf(item) == pos
})
const wallets = $('[data-wallets]')
.map(function(index, pm) {
return pm.getAttribute("data-wallets").split("|");
})
.get()
.flat()
.filter(function(item, pos, self) {
// filter out empty or duplicate values
return item && self.indexOf(item) == pos;
});
wallets.forEach(function (wallet) {
const labels = $('[data-wallets*=' + wallet + '] + .accordion-label-text')
.append('<span class="wallet wallet-loading" data-wallet="' + wallet + '"> <span aria-hidden="true" class="fa fa-cog fa-spin"></span></span>')
walletdetection[wallet]()
.then(function (result) {
const spans = labels.find('.wallet-loading[data-wallet=' + wallet + ']')
if (result) {
spans.removeClass('wallet-loading').hide().text(', ' + walletdetection.name_map[wallet]).fadeIn(300)
} else {
spans.remove()
}
})
.catch(function () {
labels.find('.wallet-loading[data-wallet=' + wallet + ']').remove()
})
})
})
wallets.forEach(function(wallet) {
const labels = $('[data-wallets*='+wallet+'] + .accordion-label-text')
.append('<span class="wallet wallet-loading" data-wallet="'+wallet+'"> <span aria-hidden="true" class="fa fa-cog fa-spin"></span></span>')
walletdetection[wallet]()
.then(function(result) {
const spans = labels.find(".wallet-loading[data-wallet=" + wallet + "]");
if (result) {
spans.removeClass('wallet-loading').hide().text(', ' + walletdetection.name_map[wallet]).fadeIn(300);
} else {
spans.remove();
}
})
.catch(function(result) {
labels.find(".wallet-loading[data-wallet=" + wallet + "]").remove();
})
});
});
@@ -2,80 +2,80 @@
// by John Friend, https://github.com/jfriend00/docReady
// MIT License
(function (funcName, baseObj) {
'use strict'
// The public function name defaults to window.docReady
// but you can modify the last line of this function to pass in a different object or method name
// if you want to put them in a different namespace and those will be used instead of
// window.docReady(...)
funcName = funcName || 'docReady'
baseObj = baseObj || window
let readyList = []
let readyFired = false
let readyEventHandlersInstalled = false
(function(funcName, baseObj) {
"use strict";
// The public function name defaults to window.docReady
// but you can modify the last line of this function to pass in a different object or method name
// if you want to put them in a different namespace and those will be used instead of
// window.docReady(...)
funcName = funcName || "docReady";
baseObj = baseObj || window;
var readyList = [];
var readyFired = false;
var readyEventHandlersInstalled = false;
// call this when the document is ready
// this function protects itself against being called more than once
function ready () {
if (!readyFired) {
// this must be set to true before we start calling callbacks
readyFired = true
for (let i = 0; i < readyList.length; i++) {
// if a callback here happens to add new ready handlers,
// the docReady() function will see that it already fired
// and will schedule the callback to run right after
// this event loop finishes so all handlers will still execute
// in order and no new ones will be added to the readyList
// while we are processing the list
readyList[i].fn.call(window, readyList[i].ctx)
}
// allow any closures held by these functions to free
readyList = []
}
}
// call this when the document is ready
// this function protects itself against being called more than once
function ready() {
if (!readyFired) {
// this must be set to true before we start calling callbacks
readyFired = true;
for (var i = 0; i < readyList.length; i++) {
// if a callback here happens to add new ready handlers,
// the docReady() function will see that it already fired
// and will schedule the callback to run right after
// this event loop finishes so all handlers will still execute
// in order and no new ones will be added to the readyList
// while we are processing the list
readyList[i].fn.call(window, readyList[i].ctx);
}
// allow any closures held by these functions to free
readyList = [];
}
}
function readyStateChange () {
if (document.readyState === 'complete') {
ready()
}
}
function readyStateChange() {
if ( document.readyState === "complete" ) {
ready();
}
}
// This is the one public interface
// docReady(fn, context);
// the context argument is optional - if present, it will be passed
// as an argument to the callback
baseObj[funcName] = function (callback, context) {
if (typeof callback !== 'function') {
throw new TypeError('callback for docReady(fn) must be a function')
}
// if ready has already fired, then just schedule the callback
// to fire asynchronously, but right away
if (readyFired) {
setTimeout(function () { callback(context) }, 1)
return
} else {
// add the function and context to the list
readyList.push({ fn: callback, ctx: context })
}
// if document already ready to go, schedule the ready function to run
// IE only safe when readyState is "complete", others safe when readyState is "interactive"
if (document.readyState === 'complete' || (!document.attachEvent && document.readyState === 'interactive')) {
setTimeout(ready, 1)
} else if (!readyEventHandlersInstalled) {
// otherwise if we don't have event handlers installed, install them
if (document.addEventListener) {
// first choice is DOMContentLoaded event
document.addEventListener('DOMContentLoaded', ready, false)
// backup is window load event
window.addEventListener('load', ready, false)
} else {
// must be IE
document.attachEvent('onreadystatechange', readyStateChange)
window.attachEvent('onload', ready)
}
readyEventHandlersInstalled = true
}
}
})('docReady', window)
// This is the one public interface
// docReady(fn, context);
// the context argument is optional - if present, it will be passed
// as an argument to the callback
baseObj[funcName] = function(callback, context) {
if (typeof callback !== "function") {
throw new TypeError("callback for docReady(fn) must be a function");
}
// if ready has already fired, then just schedule the callback
// to fire asynchronously, but right away
if (readyFired) {
setTimeout(function() {callback(context);}, 1);
return;
} else {
// add the function and context to the list
readyList.push({fn: callback, ctx: context});
}
// if document already ready to go, schedule the ready function to run
// IE only safe when readyState is "complete", others safe when readyState is "interactive"
if (document.readyState === "complete" || (!document.attachEvent && document.readyState === "interactive")) {
setTimeout(ready, 1);
} else if (!readyEventHandlersInstalled) {
// otherwise if we don't have event handlers installed, install them
if (document.addEventListener) {
// first choice is DOMContentLoaded event
document.addEventListener("DOMContentLoaded", ready, false);
// backup is window load event
window.addEventListener("load", ready, false);
} else {
// must be IE
document.attachEvent("onreadystatechange", readyStateChange);
window.attachEvent("onload", ready);
}
readyEventHandlersInstalled = true;
}
}
})("docReady", window);
// modify this previous line to pass in your own method name
// and object for the method to be attached to
@@ -1,34 +1,36 @@
/* global django */
let roundTo = function (n, digits) {
if (digits === undefined) {
digits = 0
}
/*global django*/
var roundTo = function (n, digits) {
if (digits === undefined) {
digits = 0;
}
let multiplicator = Math.pow(10, digits)
n = parseFloat((n * multiplicator).toFixed(11))
return Math.round(n) / multiplicator
}
var multiplicator = Math.pow(10, digits);
n = parseFloat((n * multiplicator).toFixed(11));
return Math.round(n) / multiplicator;
};
let floatformat = function (val, places) {
'use strict'
if (places === undefined) {
places = 2
}
if (typeof val === 'string') {
val = parseFloat(val)
}
let parts = roundTo(val, places).toFixed(places).split('.')
if (places === 0) {
return parts[0]
}
parts[0] = parts[0].replace(new RegExp('\\B(?=(\\d{' + django.get_format('NUMBER_GROUPING') + '})+(?!\\d))', 'g'), django.get_format('THOUSAND_SEPARATOR'))
return parts[0] + django.get_format('DECIMAL_SEPARATOR') + parts[1]
}
let autofloatformat = function (val, places) {
'use strict'
if (val == roundTo(val, 0)) {
places = 0
}
return floatformat(val, places)
}
var floatformat = function (val, places) {
"use strict";
if (places === undefined) {
places = 2;
}
if (typeof val === "string") {
val = parseFloat(val);
}
var parts = roundTo(val, places).toFixed(places).split(".");
if (places === 0) {
return parts[0];
}
parts[0] = parts[0].replace(new RegExp("\\B(?=(\\d{" + django.get_format("NUMBER_GROUPING") + "})+(?!\\d))", "g"), django.get_format("THOUSAND_SEPARATOR"));
return parts[0] + django.get_format("DECIMAL_SEPARATOR") + parts[1];
};
var autofloatformat = function (val, places) {
"use strict";
if (val == roundTo(val, 0)) {
places = 0;
}
return floatformat(val, places);
};
File diff suppressed because it is too large Load Diff
@@ -86,8 +86,7 @@ const taxline = computed(() => {
}
})
// rate can either be "0.00" or "0" => parseFloat to check
const showTaxline = computed(() => Number.parseFloat(props.price.rate) && Number.parseFloat(props.price.gross))
const showTaxline = computed(() => props.price.rate !== '0' && props.price.gross !== '0.00')
</script>
<template lang="pug">
.pretix-widget-pricebox
+7 -21
View File
@@ -1434,12 +1434,8 @@ def test_get_event_settings(token_client, organizer, event):
'/api/v1/organizers/{}/events/{}/settings/'.format(organizer.slug, event.slug),
)
assert resp.status_code == 200
assert resp.data['imprint_url'] == {
"en": "https://example.org",
}
assert resp.data['contact_url'] == {
"en": "https://example.org/contact",
}
assert resp.data['imprint_url'] == "https://example.org"
assert resp.data['contact_url'] == "https://example.org/contact"
assert resp.data['seating_allow_blocked_seats_for_channel'] == []
resp = token_client.get(
@@ -1447,9 +1443,7 @@ def test_get_event_settings(token_client, organizer, event):
)
assert resp.status_code == 200
assert resp.data['imprint_url'] == {
"value": {
"en": "https://example.org",
},
"value": "https://example.org",
"label": "Imprint URL",
"help_text": "This should point e.g. to a part of your website that has your contact details and legal "
"information.",
@@ -1484,12 +1478,8 @@ def test_patch_event_settings(token_client, organizer, event, team):
format='json'
)
assert resp.status_code == 200
assert resp.data['contact_url'] == {
"en": "https://example.com/contact",
}
assert resp.data['imprint_url'] == {
"en": "https://example.com",
}
assert resp.data['contact_url'] == "https://example.com/contact"
assert resp.data['imprint_url'] == "https://example.com"
assert resp.data['seating_allow_blocked_seats_for_channel'] == ['web']
assert not resp.data['reusable_media_active']
event.settings.flush()
@@ -1552,12 +1542,8 @@ def test_patch_event_settings(token_client, organizer, event, team):
format='json'
)
assert resp.status_code == 200
assert resp.data['contact_url'] == {
"en": "https://example.org/contact",
}
assert resp.data['imprint_url'] == {
"en": "https://example.org",
}
assert resp.data['contact_url'] == "https://example.org/contact"
assert resp.data['imprint_url'] == "https://example.org"
event.settings.flush()
assert event.settings.contact_url == 'https://example.org/contact'
assert event.settings.imprint_url == 'https://example.org'
+2 -5
View File
@@ -25,7 +25,6 @@ from datetime import datetime
import pytest
from django.core.files.base import ContentFile
from django_scopes import scopes_disabled
from i18nfield.strings import LazyI18nString
from tests.const import SAMPLE_PNG
TEST_ORGANIZER_RES = {
@@ -166,11 +165,9 @@ def test_patch_settings(token_client, organizer):
format='json'
)
assert resp.status_code == 200
assert resp.data['contact_url'] == {
'en': 'https://example.org/contact',
}
assert resp.data['contact_url'] == 'https://example.org/contact'
organizer.settings.flush()
assert organizer.settings.contact_url == LazyI18nString('https://example.org/contact')
assert organizer.settings.contact_url == 'https://example.org/contact'
resp = token_client.patch(
'/api/v1/organizers/{}/settings/'.format(organizer.slug),
+33 -33
View File
@@ -749,22 +749,22 @@ def test_query_count_many_items(event, itemcount):
''',
recommendations=''' Price Discounted Price Max Count Prefix
''',
expect_num_queries=8,
)
check_cart_behaviour(
event,
cart_contents=''' Price Discounted Subev
Ticket 1 42.00 42.00 0
Ticket 1 42.00 42.00 0
Ticket 1 42.00 42.00 0
Ticket 1 42.00 42.00 0
Ticket 1 42.00 42.00 0
''',
recommendations=''' Price Discounted Price Max Count Prefix
Tickets Ticket 2 42.00 0.00 1 -
''',
expect_num_queries=9,
)
check_cart_behaviour(
event,
cart_contents=''' Price Discounted Subev
Ticket 1 42.00 42.00 0
Ticket 1 42.00 42.00 0
Ticket 1 42.00 42.00 0
Ticket 1 42.00 42.00 0
Ticket 1 42.00 42.00 0
''',
recommendations=''' Price Discounted Price Max Count Prefix
Tickets Ticket 2 42.00 0.00 1 -
''',
expect_num_queries=10,
)
check_cart_behaviour(
event,
cart_contents=''' Price Discounted Subev
@@ -779,7 +779,7 @@ def test_query_count_many_items(event, itemcount):
recommendations=''' Price Discounted Price Max Count Prefix
Tickets Ticket 2 42.00 0.00 1 -
''',
expect_num_queries=10,
expect_num_queries=9,
)
@@ -803,22 +803,22 @@ def test_query_count_many_categories_and_discounts(event, catcount):
''',
recommendations=''' Price Discounted Price Max Count Prefix
''',
expect_num_queries=8,
)
check_cart_behaviour(
event,
cart_contents=''' Price Discounted Subev
Ticket 1-A 42.00 42.00 0
Ticket 1-A 42.00 42.00 0
Ticket 1-A 42.00 42.00 0
Ticket 1-A 42.00 42.00 0
Ticket 1-A 42.00 42.00 0
''',
recommendations=''' Price Discounted Price Max Count Prefix
Category 1 Ticket 1-B 42.00 0.00 1 -
''',
expect_num_queries=9,
)
check_cart_behaviour(
event,
cart_contents=''' Price Discounted Subev
Ticket 1-A 42.00 42.00 0
Ticket 1-A 42.00 42.00 0
Ticket 1-A 42.00 42.00 0
Ticket 1-A 42.00 42.00 0
Ticket 1-A 42.00 42.00 0
''',
recommendations=''' Price Discounted Price Max Count Prefix
Category 1 Ticket 1-B 42.00 0.00 1 -
''',
expect_num_queries=10,
)
check_cart_behaviour(
event,
cart_contents=''' Price Discounted Subev
@@ -833,7 +833,7 @@ def test_query_count_many_categories_and_discounts(event, catcount):
recommendations=''' Price Discounted Price Max Count Prefix
Category 1 Ticket 1-B 42.00 0.00 1 -
''',
expect_num_queries=10,
expect_num_queries=9,
)
@@ -857,7 +857,7 @@ def test_query_count_many_cartpos(event, catcount):
''',
recommendations=''' Price Discounted Price Max Count Prefix
''',
expect_num_queries=9,
expect_num_queries=8,
)
check_cart_behaviour(
event,
@@ -871,7 +871,7 @@ def test_query_count_many_cartpos(event, catcount):
recommendations=''' Price Discounted Price Max Count Prefix
Category 1 Ticket 1-B 42.00 0.00 1 -
''',
expect_num_queries=10,
expect_num_queries=9,
)
check_cart_behaviour(
event,
@@ -893,5 +893,5 @@ def test_query_count_many_cartpos(event, catcount):
Category 1 Ticket 1-B 42.00 0.00 1 -
Category 2 Ticket 2-B 42.00 0.00 1 -
''',
expect_num_queries=14,
expect_num_queries=13,
)
-91
View File
@@ -2372,97 +2372,6 @@ class CheckoutTestCase(BaseCheckoutTestCase, TimemachineTestMixin, TestCase):
assert p2.fee.value == Decimal("0.46")
assert o.total == Decimal("25.76")
def test_payment_postpone_not_allowed(self):
self.event.settings.set('payment_banktransfer__enabled', True)
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'postpone': 'on',
}, follow=False)
assert 'Please select' in response.content.decode()
def test_payment_postpone_allowed(self):
self.event.settings.set('payment_banktransfer__enabled', True)
self.event.settings.payment_choice_postpone_allowed_channels = ['web']
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'postpone': 'on',
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
target_status_code=200)
response = self.client.post('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select(".thank-you")), 1)
with scopes_disabled():
o = Order.objects.last()
assert not o.payments.exists()
def test_payment_postpone_cleared_on_selection(self):
self.event.settings.set('payment_banktransfer__enabled', True)
self.event.settings.payment_choice_postpone_allowed_channels = ['web']
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'postpone': 'on',
}, follow=False)
self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
target_status_code=200)
assert self.client.session['carts'][self.session_key].get('payments_postpone')
# The only available provider must not be preselected while the choice is postponed
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('input[name="payment"]')), 1)
self.assertEqual(len(doc.select('input[name="payment"][checked]')), 0)
# Selecting a payment method takes the order out of the postponed state again
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'payment': 'banktransfer',
}, follow=False)
self.assertRedirects(response, '/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug),
target_status_code=200)
assert not self.client.session['carts'][self.session_key].get('payments_postpone')
def test_payment_postpone_disabled_with_partial_payment(self):
self.event.settings.set('payment_banktransfer__enabled', True)
self.event.settings.payment_choice_postpone_allowed_channels = ['web']
gc = self.orga.issued_gift_cards.create(currency="EUR")
gc.transactions.create(value=20, acceptor=self.orga)
with scopes_disabled():
CartPosition.objects.create(
event=self.event, cart_id=self.session_key, item=self.ticket,
price=23, expires=now() + timedelta(minutes=10)
)
response = self.client.get('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), follow=True)
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('button[name="postpone"]')), 1)
self.assertEqual(len(doc.select('button[name="postpone"][disabled]')), 0)
# Apply a gift card that only covers part of the total
response = self.client.post('/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug), {
'payment': 'giftcard',
'payment_giftcard-code': gc.secret,
}, follow=True)
self.assertRedirects(response, '/%s/%s/checkout/payment/' % (self.orga.slug, self.event.slug),
target_status_code=200)
# Postponing would silently drop the gift card, so it is no longer offered
doc = BeautifulSoup(response.content.decode(), "lxml")
self.assertEqual(len(doc.select('button[name="postpone"][disabled]')), 1)
def test_premature_confirm(self):
response = self.client.get('/%s/%s/checkout/confirm/' % (self.orga.slug, self.event.slug), follow=True)
self.assertRedirects(response, '/%s/%s/?require_cookie=true' % (self.orga.slug, self.event.slug),
-283
View File
@@ -1,283 +0,0 @@
#
# This file is part of pretix (Community Edition).
#
# Copyright (C) 2014-2020 Raphael Michel and contributors
# Copyright (C) 2020-today pretix GmbH and contributors
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
# Public License as published by the Free Software Foundation in version 3 of the License.
#
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
# this file, see <https://pretix.eu/about/en/license>.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from datetime import UTC, datetime, timedelta
from decimal import Decimal
import pytest
from django.utils.timezone import now
from django_scopes import scope
from pretix.base.models import Event, Organizer
from pretix.presale.productlist import prepare_item_list_for_shop
# Tests for prepare_item_list_for_shop are really incomplete since historically, most features are
# tested on the test_event or test_widget layer. We'll slowly add new tests here to test closer to the
# source.
@pytest.fixture
def event():
o = Organizer.objects.create(name='MRMCD', slug='mrmcd')
e = Event.objects.create(
organizer=o, name='MRMCD2015', slug='2015',
date_from=now(), live=True
)
with scope(organizer=o):
yield e
@pytest.fixture
def quota(event):
return event.quotas.create(name="Tickets", size=500)
@pytest.fixture
def item(event, quota):
i = event.items.create(name="Ticket", default_price=Decimal("42.00"))
quota.items.add(i)
return i
@pytest.fixture
def variation(event, quota):
i = event.items.create(name="Ticket with variants", default_price=Decimal("99.00"))
v = i.variations.create(value="Default", default_price=Decimal("42.00"))
quota.items.add(i)
quota.variations.add(v)
return v
@pytest.fixture
def discount(event):
return event.discounts.create(
internal_name="Early-Bird-Discount",
all_sales_channels=True,
available_from=now() - timedelta(days=2),
available_until=now() + timedelta(days=2),
condition_all_products=True,
condition_min_count=1,
benefit_discount_matching_percent=Decimal("10.00"),
)
@pytest.fixture
def channel(event):
return event.organizer.sales_channels.get(identifier="web")
@pytest.mark.django_db
def test_default_price(event, item, variation, channel):
items, _ = prepare_item_list_for_shop(event, channel=channel)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("42")
assert items[1].available_variations[0].display_price.gross == Decimal("42")
def _test_no_discount(event, channel):
items, _ = prepare_item_list_for_shop(event, channel=channel)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("42")
assert items[1].available_variations[0].display_price.gross == Decimal("42")
@pytest.mark.django_db
def test_discount_applied(event, item, variation, channel, discount):
items, _ = prepare_item_list_for_shop(event, channel=channel)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("37.80")
assert items[0].original_price.gross == Decimal("42.00")
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
assert items[1].available_variations[0].original_price.gross == Decimal("42.00")
@pytest.mark.django_db
def test_discount_for_groups_ignored(event, item, variation, channel, discount):
discount.condition_min_count = 2
discount.save()
_test_no_discount(event, channel)
@pytest.mark.django_db
def test_discount_original_price_kept(event, item, variation, channel, discount):
item.original_price = Decimal("46.00")
item.save()
variation.original_price = Decimal("46.00")
variation.save()
items, _ = prepare_item_list_for_shop(event, channel=channel)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("37.80")
assert items[0].original_price.gross == Decimal("46.00")
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
assert items[1].available_variations[0].original_price.gross == Decimal("46.00")
@pytest.mark.django_db
def test_discount_out_of_timeframe(event, item, variation, channel, discount):
discount.available_from = now() + timedelta(days=2)
discount.save()
_test_no_discount(event, channel)
@pytest.mark.django_db
def test_discount_wrong_channel(event, item, variation, channel, discount):
discount.all_sales_channels = False
discount.save()
_test_no_discount(event, channel)
@pytest.mark.django_db
def test_discount_benefits_other_products_ignored(event, item, variation, channel, discount):
discount.benefit_same_products = False
discount.save()
_test_no_discount(event, channel)
@pytest.mark.django_db
def test_discounts_for_addons(event, item, variation, channel, discount):
discount.condition_apply_to_addons = False
discount.save()
items, _ = prepare_item_list_for_shop(event, channel=channel, allow_addons=True)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("42.00")
assert items[1].available_variations[0].display_price.gross == Decimal("42.00")
discount.condition_apply_to_addons = True
discount.save()
items, _ = prepare_item_list_for_shop(event, channel=channel, allow_addons=True)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("37.80")
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
@pytest.mark.django_db
def test_discounts_with_voucher(event, item, variation, channel, discount):
voucher = event.vouchers.create(code="FOO", price_mode="subtract", value=Decimal("10.00"))
voucher2 = event.vouchers.create(code="BAR")
discount.condition_ignore_voucher_discounted = True
discount.save()
item.original_price = Decimal("46.00")
item.save()
variation.item.original_price = Decimal("46.00")
variation.item.save()
items, _ = prepare_item_list_for_shop(event, channel=channel, voucher=voucher)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("32.00")
assert items[0].original_price.gross == Decimal("42.00")
assert items[1].available_variations[0].display_price.gross == Decimal("32.00")
assert items[1].available_variations[0].original_price.gross == Decimal("42.00")
items, _ = prepare_item_list_for_shop(event, channel=channel, voucher=voucher2)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("37.80")
assert items[0].original_price.gross == Decimal("46.00")
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
assert items[1].available_variations[0].original_price.gross == Decimal("46.00")
discount.condition_ignore_voucher_discounted = False
discount.save()
items, _ = prepare_item_list_for_shop(event, channel=channel, voucher=voucher)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("28.80")
assert items[0].original_price.gross == Decimal("42.00")
assert items[1].available_variations[0].display_price.gross == Decimal("28.80")
assert items[1].available_variations[0].original_price.gross == Decimal("42.00")
@pytest.mark.django_db
def test_discounts_for_subevent_timeframe(event, quota, item, variation, channel, discount):
event.has_subevents = True
event.save()
se = event.subevents.create(
name="Foobar", date_from=datetime(2028, 12, 27, 10, 0, 0, tzinfo=UTC)
)
quota.subevent = se
quota.save()
discount.subevent_date_from = se.date_from + timedelta(days=1)
discount.save()
items, _ = prepare_item_list_for_shop(event, channel=channel, subevent=se)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("42.00")
assert items[1].available_variations[0].display_price.gross == Decimal("42.00")
discount.subevent_date_from = se.date_from - timedelta(days=1)
discount.save()
items, _ = prepare_item_list_for_shop(event, channel=channel, subevent=se)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("37.80")
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
@pytest.mark.django_db
def test_discounts_for_products(event, quota, item, variation, channel, discount):
discount.condition_all_products = False
discount.save()
items, _ = prepare_item_list_for_shop(event, channel=channel)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("42.00")
assert items[1].available_variations[0].display_price.gross == Decimal("42.00")
discount.condition_limit_products.add(item)
discount.condition_limit_products.add(variation.item)
discount.save()
items, _ = prepare_item_list_for_shop(event, channel=channel)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("37.80")
assert items[1].available_variations[0].display_price.gross == Decimal("37.80")
@pytest.mark.django_db
def test_discounts_for_products_tax_additive(event, quota, item, variation, channel, discount):
tr = event.tax_rules.create(rate=Decimal("19.00"), price_includes_tax=False)
item.tax_rule = tr
item.save()
variation.item.tax_rule = tr
variation.item.save()
items, _ = prepare_item_list_for_shop(event, channel=channel)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("44.98")
assert items[0].original_price.gross == Decimal("49.98")
assert items[0].original_price.net == Decimal("42.00")
assert items[1].available_variations[0].display_price.gross == Decimal("44.98")
assert items[1].available_variations[0].original_price.gross == Decimal("49.98")
assert items[1].available_variations[0].original_price.net == Decimal("42.00")
@pytest.mark.django_db
def test_discounts_for_products_tax_additive_bundle_included(event, quota, item, variation, channel, discount):
tr = event.tax_rules.create(rate=Decimal("19.00"), price_includes_tax=False)
b = event.items.create(name="Bundled product", default_price=Decimal("10.00"), tax_rule=tr, require_bundling=True)
quota.items.add(b)
item.tax_rule = tr
item.save()
variation.item.tax_rule = tr
variation.item.save()
item.bundles.create(bundled_item=b, count=2, designated_price=Decimal("5.00"))
variation.item.bundles.create(bundled_item=b, count=2, designated_price=Decimal("5.00"))
items, _ = prepare_item_list_for_shop(event, channel=channel)
assert len(items) == 2
assert items[0].display_price.gross == Decimal("45.98")
assert items[0].original_price.gross == Decimal("49.98")
assert items[0].original_price.net == Decimal("42.00")
assert items[1].available_variations[0].display_price.gross == Decimal("45.98")
assert items[1].available_variations[0].original_price.gross == Decimal("49.98")
assert items[1].available_variations[0].original_price.net == Decimal("42.00")