mirror of
https://github.com/pretix/pretix.git
synced 2026-09-15 16:34:42 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7da5d2e0e7 |
@@ -1,2 +0,0 @@
|
||||
# Format pre-vue code with eslint where possible (2026-09-10)
|
||||
d85e52c83ed3639e040372fd0052e842e4899d90
|
||||
@@ -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
@@ -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([
|
||||
|
||||
Generated
+4
-14
@@ -9,8 +9,7 @@
|
||||
"version": "1.0.0",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"dependencies": {
|
||||
"vue": "^3.5.30",
|
||||
"vue-slicksort": "^2.0.5"
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
@@ -4149,9 +4148,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/smol-toml": {
|
||||
"version": "1.7.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.7.1.tgz",
|
||||
"integrity": "sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==",
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz",
|
||||
"integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
@@ -4636,15 +4635,6 @@
|
||||
"vue-eslint-parser": "^10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vue-slicksort": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/vue-slicksort/-/vue-slicksort-2.0.5.tgz",
|
||||
"integrity": "sha512-fXz1YrNjhUbJK7o0tMk27mIr4pMAZYLSYvtmLazCtfpvz+zafPCn34ILDL8B7hT7WLVZKreYs6JVe5VWymqmzA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": ">=3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
+1
-2
@@ -27,8 +27,7 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.5.30",
|
||||
"vue-slicksort": "^2.0.5"
|
||||
"vue": "^3.5.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
+3
-3
@@ -37,7 +37,7 @@ dependencies = [
|
||||
"css-inline==0.21.*",
|
||||
"defusedcsv>=3.0.0",
|
||||
"dnspython==2.*",
|
||||
"Django[argon2]==5.2.*,>=5.2.17",
|
||||
"Django[argon2]==5.2.*",
|
||||
"django-bootstrap3==26.2",
|
||||
"django-compressor==4.6.0",
|
||||
"django-countries==9.0.*",
|
||||
@@ -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.*",
|
||||
@@ -84,7 +84,7 @@ dependencies = [
|
||||
"pycountry",
|
||||
"pycparser==3.0",
|
||||
"pycryptodome==3.23.*",
|
||||
"pypdf==6.11.*",
|
||||
"pypdf==6.5.*",
|
||||
"python-bidi==0.6.*", # Support for Arabic in reportlab
|
||||
"python-dateutil==2.9.*",
|
||||
"pytz",
|
||||
|
||||
@@ -979,7 +979,7 @@ class DeviceEventSettingsSerializer(EventSettingsSerializer):
|
||||
'reusable_media_type_nfc_mf0aes',
|
||||
'reusable_media_type_nfc_mf0aes_random_uid',
|
||||
'reusable_media_usage_enforced',
|
||||
'system_question_order', # TODO(questionnaires) - remove or replace
|
||||
'system_question_order',
|
||||
'tax_rule_payment',
|
||||
'tax_rule_cancellation',
|
||||
]
|
||||
|
||||
@@ -53,7 +53,6 @@ from pretix.base.models import (
|
||||
ItemVariation, ItemVariationMetaValue, Question, QuestionOption, Quota,
|
||||
SalesChannel,
|
||||
)
|
||||
from pretix.base.models.items import Questionnaire, QuestionnaireChild
|
||||
|
||||
|
||||
class InlineItemVariationSerializer(SalesChannelMigrationMixin, I18nAwareModelSerializer):
|
||||
@@ -543,7 +542,6 @@ class LegacyDependencyValueField(serializers.CharField):
|
||||
class QuestionSerializer(I18nAwareModelSerializer):
|
||||
options = InlineQuestionOptionSerializer(many=True, required=False)
|
||||
identifier = serializers.CharField(allow_null=True)
|
||||
internal_name = serializers.CharField(allow_null=True, source='question', read_only=True)
|
||||
dependency_value = LegacyDependencyValueField(source='dependency_values', required=False, allow_null=True)
|
||||
|
||||
class Meta:
|
||||
@@ -552,7 +550,7 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
'ask_during_checkin', 'show_during_checkin', 'identifier', 'dependency_question', 'dependency_values',
|
||||
'hidden', 'dependency_value', 'print_on_invoice', 'help_text', 'valid_number_min',
|
||||
'valid_number_max', 'valid_date_min', 'valid_date_max', 'valid_datetime_min', 'valid_datetime_max',
|
||||
'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait', 'internal_name',)
|
||||
'valid_string_length_max', 'valid_string_length_min', 'valid_file_portrait')
|
||||
|
||||
def validate_identifier(self, value):
|
||||
Question._clean_identifier(self.context['event'], value, self.instance)
|
||||
@@ -628,160 +626,6 @@ class QuestionSerializer(I18nAwareModelSerializer):
|
||||
return question
|
||||
|
||||
|
||||
class QuestionRefField(serializers.PrimaryKeyRelatedField):
|
||||
def to_representation(self, qc):
|
||||
if not qc:
|
||||
return None
|
||||
elif qc.system_datafield:
|
||||
return qc.system_datafield
|
||||
elif qc.user_datafield_id:
|
||||
return qc.user_datafield_id
|
||||
else:
|
||||
return None
|
||||
|
||||
def to_internal_value(self, data):
|
||||
if type(data) == int:
|
||||
return {'user_datafield': super().to_internal_value(data), 'system_datafield': None}
|
||||
elif type(data) == str or data is None:
|
||||
return {'user_datafield': None, 'system_datafield': data}
|
||||
else:
|
||||
self.fail('incorrect_type', data_type=type(data).__name__)
|
||||
|
||||
def use_pk_only_optimization(self):
|
||||
return self.source == '*'
|
||||
|
||||
|
||||
class InlineQuestionnaireChildSerializer(I18nAwareModelSerializer):
|
||||
question = QuestionRefField(source='*', queryset=Question.objects.none())
|
||||
dependency_question = QuestionRefField(allow_null=True, required=False, queryset=Question.objects.none())
|
||||
|
||||
class Meta:
|
||||
model = QuestionnaireChild
|
||||
fields = ('question', 'required', 'label', 'help_text', 'dependency_question', 'dependency_values')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["question"].queryset = self.context["event"].questions.all()
|
||||
self.fields["dependency_question"].queryset = self.context["event"].questions.all()
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
full_data.update(data)
|
||||
|
||||
if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
dep = full_data.get('dependency_question')
|
||||
if dep:
|
||||
if dep.ask_during_checkin:
|
||||
raise ValidationError(_('Question cannot depend on a question asked during check-in.'))
|
||||
|
||||
seen_ids = {self.instance.pk} if self.instance else set()
|
||||
while dep:
|
||||
if dep.pk in seen_ids:
|
||||
raise ValidationError(_('Circular dependency between questions detected.'))
|
||||
seen_ids.add(dep.pk)
|
||||
dep = dep.dependency_question
|
||||
|
||||
return data
|
||||
|
||||
def validate_dependency_question(self, value):
|
||||
if value:
|
||||
if value.type not in (Question.TYPE_CHOICE, Question.TYPE_BOOLEAN, Question.TYPE_CHOICE_MULTIPLE):
|
||||
raise ValidationError('Question dependencies can only be set to boolean or choice questions.')
|
||||
if value == self.instance:
|
||||
raise ValidationError('A question cannot depend on itself.')
|
||||
return value
|
||||
|
||||
|
||||
class QuestionnaireSerializer(I18nAwareModelSerializer):
|
||||
limit_sales_channels = serializers.SlugRelatedField(
|
||||
slug_field="identifier",
|
||||
queryset=SalesChannel.objects.none(),
|
||||
required=False,
|
||||
allow_empty=True,
|
||||
many=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Questionnaire
|
||||
fields = ('id', 'type', 'internal_name', 'items', 'position', 'all_sales_channels', 'limit_sales_channels', 'children')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.fields['children'] = InlineQuestionnaireChildSerializer(many=True, required=True, context=kwargs['context'], partial=False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def validate(self, data):
|
||||
data = super().validate(data)
|
||||
event = self.context['event']
|
||||
|
||||
#full_data = self.to_internal_value(self.to_representation(self.instance)) if self.instance else {}
|
||||
#full_data.update(data)
|
||||
|
||||
#if full_data.get('ask_during_checkin') and full_data.get('dependency_question'):
|
||||
# raise ValidationError('Dependencies are not supported during check-in.')
|
||||
|
||||
#if full_data.get('ask_during_checkin') and full_data.get('type') in Question.ASK_DURING_CHECKIN_UNSUPPORTED:
|
||||
# raise ValidationError(_('This type of question cannot be asked during check-in.'))
|
||||
|
||||
#if full_data.get('show_during_checkin') and full_data.get('type') in Question.SHOW_DURING_CHECKIN_UNSUPPORTED:
|
||||
# raise ValidationError(_('This type of question cannot be shown during check-in.'))
|
||||
|
||||
#Question.clean_items(event, full_data.get('items') or [])
|
||||
return data
|
||||
|
||||
def validate_children(self, value):
|
||||
prev_questions = {}
|
||||
for child in value:
|
||||
if child.get('dependency_question'):
|
||||
if (child['dependency_question']['user_datafield'] or child['dependency_question']['system_datafield']) not in prev_questions:
|
||||
raise ValidationError('A question can only depend on a previous question from the same questionnaire.')
|
||||
|
||||
if child['user_datafield']:
|
||||
prev_questions[child['user_datafield']] = child
|
||||
if child['system_datafield']:
|
||||
prev_questions[child['system_datafield']] = child
|
||||
return value
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
children_data = validated_data.pop('children') if 'children' in validated_data else []
|
||||
questionnaire = super().create(validated_data)
|
||||
self.set_children(questionnaire, children_data)
|
||||
return questionnaire
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
children_data = validated_data.pop('children', None)
|
||||
questionnaire = super().update(instance, validated_data)
|
||||
if children_data is not None:
|
||||
self.set_children(questionnaire, children_data)
|
||||
return questionnaire
|
||||
|
||||
def set_children(self, questionnaire, new_data):
|
||||
result = []
|
||||
child_serializer = self.fields['children'].child
|
||||
existing = questionnaire.children.all()
|
||||
for i, d in enumerate(new_data):
|
||||
d['questionnaire'] = questionnaire
|
||||
d['position'] = i + 1
|
||||
d.setdefault('required', False)
|
||||
d.setdefault('help_text', None)
|
||||
d.setdefault('dependency_question', None)
|
||||
d.setdefault('dependency_values', None)
|
||||
updatable = min(len(existing), len(new_data))
|
||||
for i in range(0, updatable):
|
||||
result.append(child_serializer.update(existing[i], new_data[i]))
|
||||
for i in range(updatable, len(new_data)):
|
||||
result.append(child_serializer.create(new_data[i]))
|
||||
for i in range(updatable, len(existing)):
|
||||
existing[i].delete()
|
||||
return result
|
||||
|
||||
|
||||
class QuotaSerializer(I18nAwareModelSerializer):
|
||||
available = serializers.BooleanField(read_only=True)
|
||||
available_number = serializers.IntegerField(read_only=True)
|
||||
|
||||
@@ -79,8 +79,7 @@ event_router.register(r'subevents', event.SubEventViewSet)
|
||||
event_router.register(r'clone', event.CloneEventViewSet)
|
||||
event_router.register(r'items', item.ItemViewSet)
|
||||
event_router.register(r'categories', item.ItemCategoryViewSet)
|
||||
event_router.register(r'datafields', item.QuestionViewSet)
|
||||
event_router.register(r'questionnaires', item.QuestionnaireViewSet)
|
||||
event_router.register(r'questions', item.QuestionViewSet)
|
||||
event_router.register(r'discounts', discount.DiscountViewSet)
|
||||
event_router.register(r'quotas', item.QuotaViewSet)
|
||||
event_router.register(r'vouchers', voucher.VoucherViewSet)
|
||||
|
||||
@@ -48,15 +48,13 @@ from pretix.api.pagination import TotalOrderingFilter
|
||||
from pretix.api.serializers.item import (
|
||||
ItemAddOnSerializer, ItemBundleSerializer, ItemCategorySerializer,
|
||||
ItemProgramTimeSerializer, ItemSerializer, ItemVariationSerializer,
|
||||
QuestionnaireSerializer, QuestionOptionSerializer, QuestionSerializer,
|
||||
QuotaSerializer,
|
||||
QuestionOptionSerializer, QuestionSerializer, QuotaSerializer,
|
||||
)
|
||||
from pretix.api.views import ConditionalListView
|
||||
from pretix.base.models import (
|
||||
CartPosition, Item, ItemAddOn, ItemBundle, ItemCategory, ItemProgramTime,
|
||||
ItemVariation, Question, QuestionOption, Quota,
|
||||
)
|
||||
from pretix.base.models.items import Questionnaire
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
from pretix.helpers.dicts import merge_dicts
|
||||
from pretix.helpers.i18n import i18ncomp
|
||||
@@ -568,51 +566,6 @@ class QuestionOptionViewSet(viewsets.ModelViewSet):
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class QuestionnaireViewSet(ConditionalListView, viewsets.ModelViewSet):
|
||||
serializer_class = QuestionnaireSerializer
|
||||
queryset = Questionnaire.objects.none()
|
||||
#filter_backends = (DjangoFilterBackend, TotalOrderingFilter)
|
||||
#filterset_class = QuestionFilter
|
||||
ordering_fields = ('id', 'position')
|
||||
ordering = ('position', 'id')
|
||||
permission = None
|
||||
write_permission = 'event.items:write'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questionnaires.prefetch_related('children').all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.questionnaire.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def get_serializer_context(self):
|
||||
ctx = super().get_serializer_context()
|
||||
ctx['event'] = self.request.event
|
||||
return ctx
|
||||
|
||||
def perform_update(self, serializer):
|
||||
serializer.save(event=self.request.event)
|
||||
serializer.instance.log_action(
|
||||
'pretix.event.questionnaire.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.log_action(
|
||||
'pretix.event.questionnaire.deleted',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
)
|
||||
super().perform_destroy(instance)
|
||||
|
||||
|
||||
class NumberInFilter(django_filters.BaseInFilter, django_filters.NumberFilter):
|
||||
pass
|
||||
|
||||
|
||||
+143
-118
@@ -36,6 +36,7 @@ import copy
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from io import BytesIO
|
||||
@@ -349,22 +350,16 @@ class WrappedPhonePrefixSelect(Select):
|
||||
return super().render(name, value or self.initial, *args, **kwargs)
|
||||
|
||||
def get_context(self, name, value, attrs):
|
||||
# self.choices is lazy evaluated, needs to be realized to be modifiable
|
||||
choices = list(self.choices)
|
||||
if value and choices[1][0] != value:
|
||||
matching_choices = len([1 for p, c in choices if p == value])
|
||||
if value and self.choices[1][0] != value:
|
||||
matching_choices = len([1 for p, c in self.choices if p == value])
|
||||
if matching_choices > 1:
|
||||
# Some countries share a phone prefix, for example +1 is used all over the Americas.
|
||||
# This causes a UX problem: If the default value or the existing data is +12125552368,
|
||||
# the widget will just show the first <option> entry with value="+1" as selected,
|
||||
# which alphabetically is America Samoa, although most numbers statistically are from
|
||||
# the US. As a workaround, we detect this case and add an additional choice value with
|
||||
# the US. As a workaround, we detect this case and add an aditional choice value with
|
||||
# just <option value="+1">+1</option> without an explicit country.
|
||||
self.choices = [
|
||||
choices[0],
|
||||
(value, value),
|
||||
*choices[1:],
|
||||
]
|
||||
self.choices.insert(1, (value, value))
|
||||
context = super().get_context(name, value, attrs)
|
||||
return context
|
||||
|
||||
@@ -642,22 +637,49 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
|
||||
FakeQuestion = namedtuple(
|
||||
'FakeQuestion', 'id question position required help_text container_type', defaults=('', Question.ContainerType.ORDERPOSITION)
|
||||
)
|
||||
|
||||
|
||||
def get_fake_attendee_questions(settings):
|
||||
fq = []
|
||||
sqo = settings.system_question_order
|
||||
|
||||
if settings.attendee_names_asked:
|
||||
fq.append(FakeQuestion('attendee_name_parts', _('Attendee name'), sqo.get('attendee_name_parts', 0), settings.attendee_names_required))
|
||||
|
||||
if settings.attendee_emails_asked:
|
||||
fq.append(FakeQuestion('attendee_email', _('Attendee email'), sqo.get('attendee_email', 0), settings.attendee_emails_required))
|
||||
|
||||
if settings.attendee_company_asked:
|
||||
fq.append(FakeQuestion('company', _('Company'), sqo.get('company', 0), settings.attendee_company_required))
|
||||
|
||||
if settings.attendee_addresses_asked:
|
||||
fq.append(FakeQuestion('street', _('Street'), sqo.get('street', 0), settings.attendee_addresses_required))
|
||||
fq.append(FakeQuestion('zipcode', _('ZIP code'), sqo.get('zipcode', 0), settings.attendee_addresses_required))
|
||||
fq.append(FakeQuestion('city', _('City'), sqo.get('city', 0), settings.attendee_addresses_required))
|
||||
fq.append(FakeQuestion('state', _('State'), sqo.get('country', 0), settings.attendee_addresses_required))
|
||||
fq.append(FakeQuestion('country', _('Country'), sqo.get('country', 0), settings.attendee_addresses_required))
|
||||
return fq
|
||||
|
||||
|
||||
class BaseQuestionsForm(forms.Form):
|
||||
"""
|
||||
This is the base form class responsible for asking order- or ticket-related questions.
|
||||
"""
|
||||
address_validation = False
|
||||
|
||||
def build_user_question_field(self, request, event, answerlist, qc, datafield):
|
||||
def build_user_question_field(self, request, event, answerlist, q):
|
||||
# Do we already have an answer? Provide it as the initial value
|
||||
answers = [a for a in answerlist if a.question_id == datafield.id]
|
||||
answers = [a for a in answerlist if a.question_id == q.id]
|
||||
if answers:
|
||||
initial = answers[0]
|
||||
else:
|
||||
initial = None
|
||||
tz = ZoneInfo(event.settings.timezone)
|
||||
required = qc.required and not self.all_optional
|
||||
if datafield.type == Question.TYPE_BOOLEAN:
|
||||
required = q.required and not self.all_optional
|
||||
if q.type == Question.TYPE_BOOLEAN:
|
||||
if required:
|
||||
# For some reason, django-bootstrap3 does not set the required attribute
|
||||
# itself.
|
||||
@@ -671,105 +693,107 @@ class BaseQuestionsForm(forms.Form):
|
||||
initialbool = False
|
||||
|
||||
field = forms.BooleanField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initialbool, widget=widget,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_NUMBER:
|
||||
elif q.type == Question.TYPE_NUMBER:
|
||||
field = forms.DecimalField(
|
||||
label=escape(qc.label), required=required,
|
||||
min_value=datafield.valid_number_min or Decimal('0.00'),
|
||||
max_value=datafield.valid_number_max,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
min_value=q.valid_number_min or Decimal('0.00'),
|
||||
max_value=q.valid_number_max,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_STRING:
|
||||
elif q.type == Question.TYPE_STRING:
|
||||
field = forms.CharField(
|
||||
label=escape(qc.label), required=required,
|
||||
min_length=datafield.valid_string_length_min,
|
||||
max_length=datafield.valid_string_length_max,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
min_length=q.valid_string_length_min,
|
||||
max_length=q.valid_string_length_max,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_TEXT:
|
||||
elif q.type == Question.TYPE_TEXT:
|
||||
field = forms.CharField(
|
||||
label=escape(qc.label), required=required,
|
||||
min_length=datafield.valid_string_length_min,
|
||||
max_length=datafield.valid_string_length_max,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
min_length=q.valid_string_length_min,
|
||||
max_length=q.valid_string_length_max,
|
||||
help_text=rich_text(q.help_text),
|
||||
widget=forms.Textarea,
|
||||
initial=initial.answer if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_COUNTRYCODE:
|
||||
elif q.type == Question.TYPE_COUNTRYCODE:
|
||||
field = CountryField(
|
||||
countries=CachedCountries,
|
||||
blank=True, null=True, blank_label=' ',
|
||||
).formfield(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
widget=forms.Select,
|
||||
empty_label=' ',
|
||||
initial=initial.answer if initial else (guess_country_from_request(request, event) if required else None),
|
||||
initial=initial.answer if initial else (
|
||||
guess_country_from_request(request, event) if required else None),
|
||||
)
|
||||
elif datafield.type == Question.TYPE_CHOICE:
|
||||
elif q.type == Question.TYPE_CHOICE:
|
||||
field = forms.ModelChoiceField(
|
||||
queryset=datafield.options,
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
queryset=q.options,
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
widget=forms.Select,
|
||||
to_field_name='identifier',
|
||||
empty_label='',
|
||||
initial=initial.options.first() if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
elif q.type == Question.TYPE_CHOICE_MULTIPLE:
|
||||
field = forms.ModelMultipleChoiceField(
|
||||
queryset=datafield.options,
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
queryset=q.options,
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
to_field_name='identifier',
|
||||
widget=QuestionCheckboxSelectMultiple,
|
||||
initial=initial.options.all() if initial else None,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_FILE:
|
||||
if datafield.valid_file_portrait:
|
||||
elif q.type == Question.TYPE_FILE:
|
||||
if q.valid_file_portrait:
|
||||
field = PortraitImageField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initial.file if initial else None,
|
||||
widget=PortraitImageWidget(answer=initial, request=request, attrs={'data-portrait-photo': 'true'}),
|
||||
widget=PortraitImageWidget(answer=initial, request=request,
|
||||
attrs={'data-portrait-photo': 'true'}),
|
||||
)
|
||||
else:
|
||||
field = ExtFileField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=initial.file if initial else None,
|
||||
widget=UploadedFileWidget(answer=initial, request=request),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_OTHER,
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
|
||||
)
|
||||
elif datafield.type == Question.TYPE_DATE:
|
||||
elif q.type == Question.TYPE_DATE:
|
||||
attrs = {}
|
||||
if datafield.valid_date_min:
|
||||
attrs['data-min'] = datafield.valid_date_min.isoformat()
|
||||
if datafield.valid_date_max:
|
||||
attrs['data-max'] = datafield.valid_date_max.isoformat()
|
||||
help_text = qc.help_text
|
||||
if q.valid_date_min:
|
||||
attrs['data-min'] = q.valid_date_min.isoformat()
|
||||
if q.valid_date_max:
|
||||
attrs['data-max'] = q.valid_date_max.isoformat()
|
||||
help_text = q.help_text
|
||||
if not help_text:
|
||||
if datafield.valid_date_min and datafield.valid_date_max:
|
||||
if q.valid_date_min and q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date between {min} and {max}.'),
|
||||
min=date_format(datafield.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(datafield.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_date_min:
|
||||
elif q.valid_date_min:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date no earlier than {min}.'),
|
||||
min=date_format(datafield.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_date_max:
|
||||
elif q.valid_date_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date no later than {max}.'),
|
||||
max=date_format(datafield.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
|
||||
)
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
@@ -779,16 +803,16 @@ class BaseQuestionsForm(forms.Form):
|
||||
else:
|
||||
_initial = None
|
||||
field = forms.DateField(
|
||||
label=escape(qc.label), required=required,
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(help_text),
|
||||
initial=_initial,
|
||||
widget=DatePickerWidget(attrs),
|
||||
)
|
||||
if datafield.valid_date_min:
|
||||
field.validators.append(MinDateValidator(datafield.valid_date_min))
|
||||
if datafield.valid_date_max:
|
||||
field.validators.append(MaxDateValidator(datafield.valid_date_max))
|
||||
elif datafield.type == Question.TYPE_TIME:
|
||||
if q.valid_date_min:
|
||||
field.validators.append(MinDateValidator(q.valid_date_min))
|
||||
if q.valid_date_max:
|
||||
field.validators.append(MaxDateValidator(q.valid_date_max))
|
||||
elif q.type == Question.TYPE_TIME:
|
||||
if initial and initial.answer:
|
||||
try:
|
||||
_initial = dateutil.parser.parse(initial.answer).time()
|
||||
@@ -797,29 +821,29 @@ class BaseQuestionsForm(forms.Form):
|
||||
else:
|
||||
_initial = None
|
||||
field = forms.TimeField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
initial=_initial,
|
||||
widget=TimePickerWidget(without_seconds=True),
|
||||
)
|
||||
elif datafield.type == Question.TYPE_DATETIME:
|
||||
help_text = qc.help_text
|
||||
elif q.type == Question.TYPE_DATETIME:
|
||||
help_text = q.help_text
|
||||
if not help_text:
|
||||
if datafield.valid_datetime_min and datafield.valid_datetime_max:
|
||||
if q.valid_datetime_min and q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time between {min} and {max}.'),
|
||||
min=date_format(datafield.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(datafield.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_datetime_min:
|
||||
elif q.valid_datetime_min:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time no earlier than {min}.'),
|
||||
min=date_format(datafield.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
elif datafield.valid_datetime_max:
|
||||
elif q.valid_datetime_max:
|
||||
help_text = format_lazy(
|
||||
_('Please enter a date and time no later than {max}.'),
|
||||
max=date_format(datafield.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
|
||||
)
|
||||
|
||||
if initial and initial.answer:
|
||||
@@ -831,20 +855,20 @@ class BaseQuestionsForm(forms.Form):
|
||||
_initial = None
|
||||
|
||||
field = SplitDateTimeField(
|
||||
label=escape(qc.label), required=required,
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(help_text),
|
||||
initial=_initial,
|
||||
widget=SplitDateTimePickerWidget(
|
||||
time_format=get_format_without_seconds('TIME_INPUT_FORMATS'),
|
||||
min_date=datafield.valid_datetime_min,
|
||||
max_date=datafield.valid_datetime_max
|
||||
min_date=q.valid_datetime_min,
|
||||
max_date=q.valid_datetime_max
|
||||
),
|
||||
)
|
||||
if datafield.valid_datetime_min:
|
||||
field.validators.append(MinDateTimeValidator(datafield.valid_datetime_min))
|
||||
if datafield.valid_datetime_max:
|
||||
field.validators.append(MaxDateTimeValidator(datafield.valid_datetime_max))
|
||||
elif datafield.type == Question.TYPE_PHONENUMBER:
|
||||
if q.valid_datetime_min:
|
||||
field.validators.append(MinDateTimeValidator(q.valid_datetime_min))
|
||||
if q.valid_datetime_max:
|
||||
field.validators.append(MaxDateTimeValidator(q.valid_datetime_max))
|
||||
elif q.type == Question.TYPE_PHONENUMBER:
|
||||
if initial:
|
||||
try:
|
||||
initial = PhoneNumber().from_string(initial.answer)
|
||||
@@ -857,27 +881,26 @@ class BaseQuestionsForm(forms.Form):
|
||||
initial = "+{}.".format(phone_prefix)
|
||||
|
||||
field = PhoneNumberField(
|
||||
label=escape(qc.label), required=required,
|
||||
help_text=rich_text(qc.help_text),
|
||||
label=escape(q.question), required=required,
|
||||
help_text=rich_text(q.help_text),
|
||||
# We now exploit an implementation detail in PhoneNumberPrefixWidget to allow us to pass just
|
||||
# a country code but no number as an initial value. It's a bit hacky, but should be stable for
|
||||
# the future.
|
||||
initial=initial,
|
||||
widget=WrappedPhoneNumberPrefixWidget()
|
||||
)
|
||||
field.datafield = datafield
|
||||
field.question = q
|
||||
if answers:
|
||||
# Cache the answer object for later use
|
||||
field.answer = answers[0]
|
||||
|
||||
if qc.dependency_question_id:
|
||||
field.widget.attrs['data-question-dependency'] = qc.dependency_question_id
|
||||
field.widget.attrs['data-question-dependency-values'] = escapejson_attr(json.dumps(qc.dependency_values))
|
||||
if datafield.type != 'M':
|
||||
field.widget.attrs['required'] = qc.required and not self.all_optional
|
||||
field._required = qc.required and not self.all_optional
|
||||
if q.dependency_question_id:
|
||||
field.widget.attrs['data-question-dependency'] = q.dependency_question_id
|
||||
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 = False
|
||||
|
||||
return field
|
||||
|
||||
def check_user_questions(self, d):
|
||||
@@ -941,7 +964,6 @@ class OrderLevelQuestionsForm(BaseQuestionsForm):
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# TODO(questionnaires) - switch olq's to questionnaires !
|
||||
questions = Question.objects.filter(
|
||||
event=event, container_type=Question.ContainerType.ORDER,
|
||||
ask_during_checkin=False, hidden=False,
|
||||
@@ -976,7 +998,6 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
orderpos = self.orderpos = kwargs.pop('orderpos', None)
|
||||
pos = cartpos or orderpos
|
||||
item = pos.item
|
||||
questionnaires = pos.item.relevant_questionnaires
|
||||
event = kwargs.pop('event')
|
||||
self.all_optional = kwargs.pop('all_optional', False)
|
||||
self.attendee_addresses_required = event.settings.attendee_addresses_required and not self.all_optional
|
||||
@@ -986,13 +1007,18 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
if cartpos and item.validity_mode == Item.VALIDITY_MODE_DYNAMIC and item.validity_dynamic_start_choice:
|
||||
self.fields['requested_valid_from'] = self.build_requested_valid_from_field(event, pos, item)
|
||||
|
||||
for questionnaire in questionnaires:
|
||||
for child in getattr(questionnaire, 'childlist', questionnaire.children.all()):
|
||||
if child.user_datafield:
|
||||
df = child.user_datafield
|
||||
self.fields['question_%s' % df.id] = self.build_user_question_field(request, event, pos.answerlist, child, df)
|
||||
elif child.system_datafield:
|
||||
self.fields[child.system_datafield] = self.build_system_question_field(request, event, pos, child)
|
||||
questions = []
|
||||
if item.ask_attendee_data:
|
||||
questions += get_fake_attendee_questions(event.settings)
|
||||
questions += pos.item.questions_to_ask
|
||||
|
||||
questions.sort(key=lambda q: q.position)
|
||||
|
||||
for q in questions:
|
||||
if isinstance(q, FakeQuestion):
|
||||
self.fields[q.id] = self.build_system_question_field(request, event, pos, q)
|
||||
else:
|
||||
self.fields['question_%s' % q.id] = self.build_user_question_field(request, event, pos.answerlist, q)
|
||||
|
||||
responses = question_form_fields.send(sender=event, position=pos)
|
||||
data = pos.meta_info_data
|
||||
@@ -1057,21 +1083,21 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
)
|
||||
|
||||
def build_system_question_field(self, request, event, pos, qc):
|
||||
field_name = qc.system_datafield
|
||||
field_name = qc.id
|
||||
if field_name == 'attendee_name_parts':
|
||||
return NamePartsFormField(
|
||||
max_length=255,
|
||||
required=qc.required and not self.all_optional,
|
||||
scheme=event.settings.name_scheme,
|
||||
titles=event.settings.name_scheme_titles,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=pos.attendee_name_parts,
|
||||
)
|
||||
if field_name == 'attendee_email':
|
||||
return forms.EmailField(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=pos.attendee_email,
|
||||
widget=forms.EmailInput(
|
||||
@@ -1083,7 +1109,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
if field_name == 'company':
|
||||
return forms.CharField(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
max_length=255,
|
||||
initial=pos.company,
|
||||
@@ -1092,7 +1118,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
if field_name == 'street':
|
||||
return forms.CharField(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
widget=forms.Textarea(attrs={
|
||||
'rows': 2,
|
||||
@@ -1105,7 +1131,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
return forms.CharField(
|
||||
required=False,
|
||||
max_length=30,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=pos.zipcode,
|
||||
widget=forms.TextInput(attrs={
|
||||
@@ -1115,7 +1141,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
if field_name == 'city':
|
||||
return forms.CharField(
|
||||
required=False,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
max_length=255,
|
||||
initial=pos.city,
|
||||
@@ -1129,7 +1155,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
countries=CachedCountries
|
||||
).formfield(
|
||||
required=qc.required and not self.all_optional,
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
initial=country,
|
||||
widget=forms.Select(attrs={
|
||||
@@ -1157,7 +1183,7 @@ class TicketLevelQuestionsForm(BaseQuestionsForm):
|
||||
del self.data[fprefix + 'state']
|
||||
|
||||
field = forms.ChoiceField(
|
||||
label=escape(qc.label),
|
||||
label=escape(qc.question),
|
||||
help_text=rich_text(qc.help_text),
|
||||
required=False,
|
||||
choices=c,
|
||||
@@ -1471,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,196 +0,0 @@
|
||||
# Generated by Django 4.2.29 on 2026-03-19 14:24
|
||||
import json
|
||||
from collections import namedtuple
|
||||
from itertools import chain, groupby
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import i18nfield.fields
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
import pretix.base.models.base
|
||||
import pretix.base.models.fields
|
||||
|
||||
|
||||
FakeQuestion = namedtuple(
|
||||
'FakeQuestion', 'id question position required'
|
||||
)
|
||||
|
||||
|
||||
def get_fake_questions(settings):
|
||||
def b(s):
|
||||
return s == 'True'
|
||||
fq = []
|
||||
sqo = json.loads(settings.get('system_question_order', '{}'))
|
||||
_ = LazyI18nString.from_gettext
|
||||
|
||||
if b(settings.get('attendee_names_asked', 'True')):
|
||||
fq.append(FakeQuestion('attendee_name_parts', _('Attendee name'), sqo.get('attendee_name_parts', 0), b(settings.get('attendee_names_required'))))
|
||||
|
||||
if b(settings.get('attendee_emails_asked')):
|
||||
fq.append(FakeQuestion('attendee_email', _('Attendee email'), sqo.get('attendee_email', 0), b(settings.get('attendee_emails_required'))))
|
||||
|
||||
if b(settings.get('attendee_company_asked')):
|
||||
fq.append(FakeQuestion('company', _('Company'), sqo.get('company', 0), b(settings.get('attendee_company_required'))))
|
||||
|
||||
if b(settings.get('attendee_addresses_asked')):
|
||||
fq.append(FakeQuestion('street', _('Street'), sqo.get('street', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('zipcode', _('ZIP code'), sqo.get('zipcode', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('city', _('City'), sqo.get('city', 0), b(settings.get('attendee_addresses_required'))))
|
||||
fq.append(FakeQuestion('country', _('Country'), sqo.get('country', 0), b(settings.get('attendee_addresses_required'))))
|
||||
return fq
|
||||
|
||||
|
||||
def migrate_questions_forward(apps, schema_editor):
|
||||
Event = apps.get_model("pretixbase", "Event")
|
||||
Item = apps.get_model("pretixbase", "Item")
|
||||
Question = apps.get_model("pretixbase", "Question")
|
||||
Questionnaire = apps.get_model("pretixbase", "Questionnaire")
|
||||
QuestionnaireChild = apps.get_model("pretixbase", "QuestionnaireChild")
|
||||
EventSettingsStore = apps.get_model('pretixbase', 'Event_SettingsStore')
|
||||
|
||||
def create_grouped_item_questionnaires(event, children, label_prefix, questionnaire_type):
|
||||
# group by item, creating a unique questionnaire per item
|
||||
item_questionnaires = (([t[3] for t in children], item_id) for item_id, children in
|
||||
groupby(children, key=lambda t: t[0]))
|
||||
|
||||
# group again, merging all questionnaires with identical children
|
||||
merged_questionnaires = groupby(sorted(item_questionnaires, key=lambda t: [q.id for q in t[0]]),
|
||||
key=lambda t: t[0])
|
||||
for children, iterator in merged_questionnaires:
|
||||
items = [item for _c, item in iterator]
|
||||
|
||||
# create questionnaires and children
|
||||
questionnaire = Questionnaire.objects.create(
|
||||
event=event, type=questionnaire_type, position=0, all_sales_channels=True,
|
||||
internal_name=label_prefix + ', '.join(str(iname or name) for (id, iname, name) in items)
|
||||
)
|
||||
questionnaire.items.set([id for (id, iname, name) in items])
|
||||
deps = {}
|
||||
for position, child in enumerate(children):
|
||||
if isinstance(child, FakeQuestion):
|
||||
QuestionnaireChild.objects.create(
|
||||
questionnaire=questionnaire,
|
||||
position=position + 1,
|
||||
system_datafield=child.id,
|
||||
required=child.required,
|
||||
label=child.question,
|
||||
)
|
||||
else:
|
||||
deps[child.id] = QuestionnaireChild.objects.create(
|
||||
questionnaire=questionnaire,
|
||||
position=position + 1,
|
||||
user_datafield=child,
|
||||
required=child.required,
|
||||
label=child.question,
|
||||
help_text=child.help_text,
|
||||
dependency_question=deps[child.dependency_question.id] if child.dependency_question else None,
|
||||
dependency_values=child.dependency_values,
|
||||
)
|
||||
|
||||
for event in Event.objects.iterator():
|
||||
# get relevant settings
|
||||
settings = {
|
||||
setting.key: setting.value for setting in EventSettingsStore.objects.filter(object_id=event.id, key__in=(
|
||||
'system_question_order', 'attendee_names_asked', 'attendee_names_required', 'attendee_emails_asked', 'attendee_emails_required',
|
||||
'attendee_company_asked', 'attendee_company_required', 'attendee_addresses_asked', 'attendee_addresses_required',
|
||||
))
|
||||
}
|
||||
|
||||
# get all ticket-level questions (user-defined and system provided), along with the products for which they're asked
|
||||
questions = event.questions.filter(container_type='P')
|
||||
children = sorted(chain((
|
||||
(item, q.position, 0, q)
|
||||
for q in get_fake_questions(settings)
|
||||
for item in event.items.filter(personalized=True).values_list('id', 'internal_name', 'name')
|
||||
), (
|
||||
(item, q.position, q.id, q)
|
||||
for q in questions.filter(hidden=False)
|
||||
for item in q.items.values_list('id', 'internal_name', 'name')
|
||||
)), key=lambda t: (t[0], t[1]))
|
||||
|
||||
create_grouped_item_questionnaires(event, children, '', 'PS')
|
||||
|
||||
children = sorted(chain((
|
||||
(item, q.position, q.id, q)
|
||||
for q in questions.filter(hidden=True)
|
||||
for item in q.items.values_list('id', 'internal_name', 'name')
|
||||
)), key=lambda t: (t[0], t[1]))
|
||||
|
||||
create_grouped_item_questionnaires(event, children, 'Hidden questions for ', 'PH')
|
||||
|
||||
# get all order-level questions
|
||||
questions = list(event.questions.filter(container_type='O', hidden=False).order_by('position'))
|
||||
if questions:
|
||||
# create questionnaires and children
|
||||
questionnaire = Questionnaire.objects.create(
|
||||
event=event, type='OS', position=0, all_sales_channels=True,
|
||||
internal_name='Per-order questions',
|
||||
)
|
||||
deps = {}
|
||||
for position, child in enumerate(questions):
|
||||
deps[child.id] = QuestionnaireChild.objects.create(
|
||||
questionnaire=questionnaire,
|
||||
position=position + 1,
|
||||
user_datafield=child,
|
||||
required=child.required,
|
||||
label=child.question,
|
||||
help_text=child.help_text,
|
||||
dependency_question=deps[child.dependency_question.id] if child.dependency_question else None,
|
||||
dependency_values=child.dependency_values,
|
||||
)
|
||||
|
||||
|
||||
def migrate_questions_backward(apps, schema_editor):
|
||||
pass # as long as we don't delete the old columns, this is a no op. after that, it gets complicated...
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0309_alter_questionanswer_unique_together_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Questionnaire',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('internal_name', models.CharField(max_length=255)),
|
||||
('type', models.CharField(max_length=5)),
|
||||
('position', models.PositiveIntegerField(default=0)),
|
||||
('all_sales_channels', models.BooleanField(default=True)),
|
||||
('event', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='questionnaires', to='pretixbase.event')),
|
||||
('items', models.ManyToManyField(related_name='questionnaires', to='pretixbase.item')),
|
||||
('limit_sales_channels', models.ManyToManyField(to='pretixbase.saleschannel')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='QuestionnaireChild',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False)),
|
||||
('position', models.PositiveIntegerField(default=0)),
|
||||
('system_datafield', models.CharField(max_length=25, null=True)),
|
||||
('required', models.BooleanField(default=False)),
|
||||
('label', i18nfield.fields.I18nTextField()),
|
||||
('help_text', i18nfield.fields.I18nTextField(null=True)),
|
||||
('dependency_values', pretix.base.models.fields.MultiStringField(default=[])),
|
||||
('dependency_question', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dependent_questions', to='pretixbase.questionnairechild')),
|
||||
('questionnaire', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='children', to='pretixbase.questionnaire')),
|
||||
('user_datafield', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='references', to='pretixbase.question')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=(models.Model, pretix.base.models.base.LoggingMixin),
|
||||
),
|
||||
migrations.RunPython(
|
||||
migrate_questions_forward,
|
||||
migrate_questions_backward,
|
||||
),
|
||||
# TODO(questionnaires) remove old columns from Question model
|
||||
]
|
||||
@@ -37,8 +37,8 @@ from .invoices import Invoice, InvoiceLine, invoice_filename
|
||||
from .items import (
|
||||
Item, ItemAddOn, ItemBundle, ItemCategory, ItemMetaProperty, ItemMetaValue,
|
||||
ItemProgramTime, ItemVariation, ItemVariationMetaValue, Question,
|
||||
Questionnaire, QuestionnaireChild, QuestionOption, Quota, SubEventItem,
|
||||
SubEventItemVariation, itempicture_upload_to,
|
||||
QuestionOption, Quota, SubEventItem, SubEventItemVariation,
|
||||
itempicture_upload_to,
|
||||
)
|
||||
from .log import LogEntry
|
||||
from .mail import OutgoingMail
|
||||
|
||||
+16
-119
@@ -1569,12 +1569,10 @@ class ItemBundle(models.Model):
|
||||
|
||||
class Question(LoggedModel):
|
||||
"""
|
||||
A question is a data field that can be used to extend an order or a ticket by custom
|
||||
information, e.g. "Attendee age". To be actually useful, questions need to be added to
|
||||
one or multiple Questionnaires. The answers may be found in QuestionAnswers, attached
|
||||
to Orders, OrderPositions or CartPositions.
|
||||
|
||||
A question can allow one of several input types, currently:
|
||||
A question is an input field that can be used to extend a ticket by custom information,
|
||||
e.g. "Attendee age". The answers are found next to the position. The answers may be found
|
||||
in QuestionAnswers, attached to OrderPositions/CartPositions. A question can allow one of
|
||||
several input types, currently:
|
||||
|
||||
* a number (``TYPE_NUMBER``)
|
||||
* a one-line string (``TYPE_STRING``)
|
||||
@@ -1594,7 +1592,7 @@ class Question(LoggedModel):
|
||||
:param required: Whether answering this question is required for submitting an order including
|
||||
items associated with this question.
|
||||
:type required: bool
|
||||
:param items: TO BE REMOVED
|
||||
:param items: A set of ``Items`` objects that this question should be applied to
|
||||
:param ask_during_checkin: Whether to ask this question during check-in instead of during check-out.
|
||||
:type ask_during_checkin: bool
|
||||
:param show_during_checkin: Whether to show the answer to this question during check-in.
|
||||
@@ -1653,7 +1651,6 @@ class Question(LoggedModel):
|
||||
default=ContainerType.ORDERPOSITION,
|
||||
)
|
||||
question = I18nTextField(
|
||||
# TODO(questionnaires) : to be renamed to 'internal_name'
|
||||
verbose_name=_("Question")
|
||||
)
|
||||
identifier = models.CharField(
|
||||
@@ -1669,7 +1666,6 @@ class Question(LoggedModel):
|
||||
],
|
||||
)
|
||||
help_text = I18nTextField(
|
||||
# TODO(questionnaires) : to be removed
|
||||
verbose_name=_("Help text"),
|
||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||
null=True, blank=True,
|
||||
@@ -1679,22 +1675,22 @@ class Question(LoggedModel):
|
||||
choices=TYPE_CHOICES,
|
||||
verbose_name=_("Question type")
|
||||
)
|
||||
required = models.BooleanField( # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Required question")
|
||||
)
|
||||
items = models.ManyToManyField( # TODO(questionnaires) : to be removed, -> Questionnaire
|
||||
items = models.ManyToManyField(
|
||||
Item,
|
||||
related_name='questions',
|
||||
verbose_name=_("Products"),
|
||||
blank=True,
|
||||
help_text=_('This question will be asked to buyers of the selected products')
|
||||
)
|
||||
position = models.PositiveIntegerField( # TODO(questionnaires) : to be removed, -> Questionnaire + QuestionnaireChild
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
ask_during_checkin = models.BooleanField( # TODO(questionnaires) : to be removed
|
||||
ask_during_checkin = models.BooleanField(
|
||||
verbose_name=_('Ask during check-in instead of in the ticket buying process'),
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
@@ -1704,7 +1700,7 @@ class Question(LoggedModel):
|
||||
help_text=_('Not supported by all check-in apps for all question types.'),
|
||||
default=False
|
||||
)
|
||||
hidden = models.BooleanField( # to be removed
|
||||
hidden = models.BooleanField(
|
||||
verbose_name=_('Hidden question'),
|
||||
help_text=_('This question will only show up in the backend.'),
|
||||
default=False
|
||||
@@ -1713,10 +1709,10 @@ class Question(LoggedModel):
|
||||
verbose_name=_('Print answer on invoices'),
|
||||
default=False
|
||||
)
|
||||
dependency_question = models.ForeignKey( # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
||||
dependency_question = models.ForeignKey(
|
||||
'Question', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_values = MultiStringField(default=[]) # TODO(questionnaires) : to be removed, -> QuestionnaireChild
|
||||
dependency_values = MultiStringField(default=[])
|
||||
valid_number_min = models.DecimalField(decimal_places=6, max_digits=30, null=True, blank=True,
|
||||
verbose_name=_('Minimum value'),
|
||||
help_text=_('Currently not supported in our apps and during check-in'))
|
||||
@@ -1755,9 +1751,9 @@ class Question(LoggedModel):
|
||||
objects = ScopedManager(organizer='event__organizer')
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Data field")
|
||||
verbose_name_plural = _("Data fields")
|
||||
ordering = ('question', 'id')
|
||||
verbose_name = _("Question")
|
||||
verbose_name_plural = _("Questions")
|
||||
ordering = ('position', 'id')
|
||||
unique_together = (('event', 'identifier'),)
|
||||
|
||||
def __str__(self):
|
||||
@@ -1908,7 +1904,7 @@ class Question(LoggedModel):
|
||||
return answer
|
||||
|
||||
@staticmethod
|
||||
def clean_items(event, items): # TODO(questionnaires) : remove method / move to qc
|
||||
def clean_items(event, items):
|
||||
for item in items:
|
||||
if event != item.event:
|
||||
raise ValidationError(_('One or more items do not belong to this event.'))
|
||||
@@ -1989,105 +1985,6 @@ class QuestionOption(models.Model):
|
||||
ordering = ('position', 'id')
|
||||
|
||||
|
||||
class Questionnaire(LoggedModel):
|
||||
TYPE_ORDER_SALE = "OS"
|
||||
TYPE_ORDER_POSITION_SALE = "PS"
|
||||
TYPE_ORDER_POSITION_ATTENDEE_ONLY = "PA"
|
||||
TYPE_ORDER_POSITION_CHECKIN = "PC"
|
||||
TYPE_ORDER_POSITION_HIDDEN = "PH"
|
||||
TYPE_CHOICES = (
|
||||
(TYPE_ORDER_SALE, _("Order-wide, before purchase")),
|
||||
(TYPE_ORDER_POSITION_SALE, _("Per product, before purchase")),
|
||||
(TYPE_ORDER_POSITION_ATTENDEE_ONLY, _("Per product, via attendee link")),
|
||||
(TYPE_ORDER_POSITION_CHECKIN, _("Per product, at check-in")),
|
||||
(TYPE_ORDER_POSITION_HIDDEN, _("Per product, hidden")),
|
||||
)
|
||||
event = models.ForeignKey(
|
||||
Event,
|
||||
related_name="questionnaires",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
internal_name = models.CharField(
|
||||
verbose_name=_("Internal name"),
|
||||
max_length=255,
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=5,
|
||||
choices=TYPE_CHOICES,
|
||||
verbose_name=_("Questionnaire type")
|
||||
)
|
||||
items = models.ManyToManyField(
|
||||
Item,
|
||||
related_name='questionnaires',
|
||||
verbose_name=_("Products"),
|
||||
blank=True,
|
||||
help_text=_('This questionnaire will be asked to buyers of the selected products')
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
all_sales_channels = models.BooleanField(
|
||||
verbose_name=_("Sell on all sales channels the product is sold on"),
|
||||
default=True,
|
||||
)
|
||||
limit_sales_channels = models.ManyToManyField(
|
||||
"SalesChannel",
|
||||
verbose_name=_("Restrict to specific sales channels"),
|
||||
help_text=_('The sales channel selection for the product as a whole takes precedence, so if a sales channel is '
|
||||
'selected here but not on product level, the variation will not be available.'),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
|
||||
class QuestionnaireChild(LoggedModel):
|
||||
SYSTEM_QUESTION_CHOICES = (
|
||||
('attendee_name_parts', _('Attendee name')),
|
||||
('attendee_email', _('Attendee email')),
|
||||
('company', _('Company')),
|
||||
('street', _('Street')),
|
||||
('zipcode', _('ZIP code')),
|
||||
('city', _('City')),
|
||||
('country', _('Country')),
|
||||
)
|
||||
questionnaire = models.ForeignKey(
|
||||
Questionnaire,
|
||||
related_name="children",
|
||||
on_delete=models.CASCADE
|
||||
)
|
||||
position = models.PositiveIntegerField(
|
||||
default=0,
|
||||
verbose_name=_("Position")
|
||||
)
|
||||
user_datafield = models.ForeignKey(
|
||||
Question,
|
||||
related_name="references",
|
||||
on_delete=models.CASCADE,
|
||||
null=True, blank=True,
|
||||
)
|
||||
system_datafield = models.CharField(
|
||||
max_length=25,
|
||||
choices=SYSTEM_QUESTION_CHOICES,
|
||||
null=True, blank=True,
|
||||
)
|
||||
required = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("Required question")
|
||||
)
|
||||
label = I18nTextField(
|
||||
verbose_name=_("Question")
|
||||
)
|
||||
help_text = I18nTextField(
|
||||
verbose_name=_("Help text"),
|
||||
help_text=_("If the question needs to be explained or clarified, do it here!"),
|
||||
null=True, blank=True,
|
||||
)
|
||||
dependency_question = models.ForeignKey(
|
||||
'QuestionnaireChild', null=True, blank=True, on_delete=models.SET_NULL, related_name='dependent_questions'
|
||||
)
|
||||
dependency_values = MultiStringField(default=[])
|
||||
|
||||
|
||||
class Quota(LoggedModel):
|
||||
"""
|
||||
A quota is a "pool of tickets". It is there to limit the number of items
|
||||
|
||||
@@ -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):
|
||||
@@ -1449,12 +1482,6 @@ class QuestionAnswer(models.Model):
|
||||
else:
|
||||
return self.answer
|
||||
|
||||
def to_dependency_values(self):
|
||||
if self.question.type in (Question.TYPE_CHOICE, Question.TYPE_CHOICE_MULTIPLE):
|
||||
return [o.identifier for o in self.options.all()]
|
||||
elif self.question.type in (Question.TYPE_BOOLEAN, Question.TYPE_COUNTRYCODE):
|
||||
return self.answer
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.orderposition and self.cartposition:
|
||||
raise ValueError('QuestionAnswer cannot be linked to an order and a cart position at the same time.')
|
||||
@@ -1599,80 +1626,53 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
|
||||
|
||||
def cache_answers(self, all=True):
|
||||
"""
|
||||
Creates a new property on the object:
|
||||
questions: a list of Question objects, extended by an 'answer' property
|
||||
Creates two properties on the object.
|
||||
(1) answ: a dictionary of question.id → answer string
|
||||
(2) questions: a list of Question objects, extended by an 'answer' property
|
||||
"""
|
||||
self.answ = {}
|
||||
for a in getattr(self, 'answerlist', self.answers.all()): # use prefetch_related cache from get_cart
|
||||
self.answ[a.question_id] = a
|
||||
|
||||
# We need to clone our question objects, otherwise we will override the cached
|
||||
# answers of other items in the same cart if the question objects have been
|
||||
# selected via prefetch_related
|
||||
if not all:
|
||||
if hasattr(self.item, 'relevant_questionnaires'):
|
||||
children = list(copy.copy(qc) for qq in self.item.relevant_questionnaires for qc in qq.childlist)
|
||||
if hasattr(self.item, 'questions_to_ask'):
|
||||
questions = list(copy.copy(q) for q in self.item.questions_to_ask)
|
||||
else:
|
||||
children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type='PS') for qc in qq.children.all())
|
||||
questions = list(copy.copy(q) for q in self.item.questions.filter(ask_during_checkin=False,
|
||||
hidden=False))
|
||||
else:
|
||||
children = list(copy.copy(qc) for qq in self.item.questionnaires.filter(type__startswith='P') for qc in qq.children.all())
|
||||
questions = list(copy.copy(q) for q in self.item.questions.all())
|
||||
|
||||
qc_cache = {
|
||||
q.pk: q for q in children
|
||||
question_cache = {
|
||||
q.pk: q for q in questions
|
||||
}
|
||||
|
||||
def qc_is_visible(parentid, qvals):
|
||||
if parentid not in qc_cache:
|
||||
def question_is_visible(parentid, qvals):
|
||||
if parentid not in question_cache:
|
||||
return False
|
||||
parentqc = qc_cache[parentid]
|
||||
if parentqc.dependency_question_id and not qc_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
|
||||
return False
|
||||
answer_values = self.get_dependency_answer_values(parentqc)
|
||||
return any(qval in answer_values for qval in qvals)
|
||||
if parentid not in self.answ:
|
||||
return False
|
||||
return (
|
||||
('True' in qvals and self.answ[parentid].answer == 'True')
|
||||
or ('False' in qvals and self.answ[parentid].answer == 'False')
|
||||
or (any(qval in [o.identifier for o in self.answ[parentid].options.all()] for qval in qvals))
|
||||
)
|
||||
|
||||
self.questions = []
|
||||
for qc in children:
|
||||
if qc.user_datafield_id and qc.user_datafield_id in self.answer_cache:
|
||||
qc.answer = self.answer_cache[qc.user_datafield_id]
|
||||
#qc.answer.question = qc # cache object
|
||||
elif qc.system_datafield:
|
||||
qc.answer = self.get_system_answer(qc.system_datafield)
|
||||
#qc.answer.question = qc # cache object
|
||||
for q in questions:
|
||||
if q.id in self.answ:
|
||||
q.answer = self.answ[q.id]
|
||||
q.answer.question = q # cache object
|
||||
else:
|
||||
qc.answer = ""
|
||||
if not qc.dependency_question_id or qc_is_visible(qc.dependency_question_id, qc.dependency_values):
|
||||
self.questions.append(qc)
|
||||
|
||||
@cached_property
|
||||
def answer_cache(self):
|
||||
return {
|
||||
aw.question_id: aw for aw in getattr(self, 'answerlist', self.answers.all())
|
||||
}
|
||||
|
||||
def get_dependency_answer_values(self, qc):
|
||||
if qc.user_datafield_id:
|
||||
if qc.user_datafield_id not in self.answer_cache:
|
||||
return None
|
||||
answer = self.answer_cache[qc.user_datafield_id]
|
||||
return answer.to_dependency_values()
|
||||
elif qc.system_datafield:
|
||||
return [self.get_system_answer(qc.system_datafield)]
|
||||
else:
|
||||
raise ValueError('Questionnaire child without datafield has no answer')
|
||||
|
||||
def get_system_answer(self, system_datafield_name):
|
||||
if system_datafield_name == 'attendee_name_parts':
|
||||
return self.attendee_name_parts
|
||||
elif system_datafield_name == 'attendee_email':
|
||||
return self.attendee_email
|
||||
elif system_datafield_name == 'street':
|
||||
return self.street
|
||||
elif system_datafield_name == 'zipcode':
|
||||
return self.zipcode
|
||||
elif system_datafield_name == 'city':
|
||||
return self.city
|
||||
elif system_datafield_name == 'state':
|
||||
return self.state
|
||||
elif system_datafield_name == 'country':
|
||||
return self.country
|
||||
else:
|
||||
raise ValueError('Unknown system question name')
|
||||
q.answer = ""
|
||||
if not q.dependency_question_id or question_is_visible(q.dependency_question_id, q.dependency_values):
|
||||
self.questions.append(q)
|
||||
|
||||
@property
|
||||
def net_price(self):
|
||||
|
||||
@@ -72,7 +72,7 @@ from pretix.helpers.countries import CachedCountries
|
||||
from pretix.helpers.format import format_map
|
||||
from pretix.helpers.money import DecimalTextInput
|
||||
from pretix.multidomain.urlreverse import eventreverse_absolute
|
||||
from pretix.presale.views import get_cart_positions
|
||||
from pretix.presale.views import get_cart
|
||||
from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1165,7 +1165,7 @@ class FreeOrderProvider(BasePaymentProvider):
|
||||
def is_allowed(self, request: HttpRequest, total: Decimal=None) -> bool:
|
||||
from .services.cart import get_fees
|
||||
|
||||
cart = get_cart_positions(request)
|
||||
cart = get_cart(request)
|
||||
|
||||
try:
|
||||
fees = get_fees(event=request.event, request=request,
|
||||
@@ -1433,7 +1433,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
for p in cs.get('payments', [])
|
||||
if p.get('info_data', {}).get('gift_card')
|
||||
]
|
||||
positions = get_cart_positions(request)
|
||||
positions = get_cart(request)
|
||||
testmode = self.event.testmode
|
||||
else:
|
||||
used_cards = []
|
||||
|
||||
+25
-16
@@ -1232,7 +1232,9 @@ class Renderer:
|
||||
|
||||
for i, page in enumerate(fg_pdf.pages):
|
||||
bg_page = self.bg_pdf.pages[i]
|
||||
_merge_with_correct_page_media_box(output, page, bg_page)
|
||||
_correct_page_media_box(bg_page)
|
||||
page.merge_page(bg_page, over=False)
|
||||
output.add_page(page)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
if float(self.bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
@@ -1297,32 +1299,39 @@ def merge_background(fg_pdf: PdfWriter, bg_pdf: PdfWriter, out_file, compress):
|
||||
bg_pdf.write(bg_filename)
|
||||
subprocess.run(pdftk_cmd, check=True, stdout=out_file)
|
||||
else:
|
||||
output = PdfWriter()
|
||||
for i, page in enumerate(fg_pdf.pages):
|
||||
bg_page = bg_pdf.pages[i]
|
||||
_merge_with_correct_page_media_box(output, page, bg_page)
|
||||
_correct_page_media_box(bg_page)
|
||||
page.merge_page(bg_page, over=False)
|
||||
|
||||
# pdf_header is a string like "%pdf-X.X"
|
||||
output.pdf_header = (
|
||||
bg_pdf.pdf_header
|
||||
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:])
|
||||
else fg_pdf.pdf_header
|
||||
)
|
||||
output.write(out_file)
|
||||
if float(bg_pdf.pdf_header[5:]) > float(fg_pdf.pdf_header[5:]):
|
||||
fg_pdf.pdf_header = bg_pdf.pdf_header
|
||||
|
||||
fg_pdf.write(out_file)
|
||||
|
||||
|
||||
def _merge_with_correct_page_media_box(output: pypdf.PdfWriter, fg_page: pypdf.PageObject, bg_page: pypdf.PageObject):
|
||||
if bg_page.rotation != 0:
|
||||
bg_page.transfer_rotation_to_content()
|
||||
media_box = bg_page.mediabox
|
||||
def _correct_page_media_box(page: pypdf.PageObject):
|
||||
if page.rotation != 0:
|
||||
page.transfer_rotation_to_content()
|
||||
media_box = page.mediabox
|
||||
trsf = pypdf.Transformation()
|
||||
if media_box.bottom != 0:
|
||||
trsf = trsf.translate(0, -media_box.bottom)
|
||||
if media_box.left != 0:
|
||||
trsf = trsf.translate(-media_box.left, 0)
|
||||
|
||||
fg_page = output.add_page(fg_page)
|
||||
fg_page.merge_transformed_page(bg_page, trsf, over=False, expand=False)
|
||||
page.add_transformation(trsf, False)
|
||||
for b in ["/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"]:
|
||||
if b in page:
|
||||
rr = pypdf.generic.RectangleObject(page[b])
|
||||
pt1 = trsf.apply_on(rr.lower_left)
|
||||
pt2 = trsf.apply_on(rr.upper_right)
|
||||
page[pypdf.generic.NameObject(b)] = pypdf.generic.RectangleObject((
|
||||
min(pt1[0], pt2[0]),
|
||||
min(pt1[1], pt2[1]),
|
||||
max(pt1[0], pt2[0]),
|
||||
max(pt1[1], pt2[1]),
|
||||
))
|
||||
|
||||
|
||||
@deconstructible
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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':
|
||||
|
||||
+12
-25
File diff suppressed because one or more lines are too long
@@ -27,7 +27,7 @@ from decimal import Decimal
|
||||
from django import forms
|
||||
from django.core.files.uploadedfile import UploadedFile
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Prefetch, Q, QuerySet
|
||||
from django.db.models import Prefetch, QuerySet
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import make_aware
|
||||
|
||||
@@ -37,7 +37,7 @@ from pretix.base.forms.questions import (
|
||||
)
|
||||
from pretix.base.models import (
|
||||
CartPosition, InvoiceAddress, OrderPosition, Question, QuestionAnswer,
|
||||
QuestionnaireChild, QuestionOption,
|
||||
QuestionOption,
|
||||
)
|
||||
from pretix.base.models.customers import AttendeeProfile
|
||||
from pretix.base.models.orders import CheckoutSession, Order
|
||||
@@ -348,37 +348,27 @@ class OrderQuestionsViewMixin(BaseQuestionsViewMixin):
|
||||
|
||||
@cached_property
|
||||
def positions(self):
|
||||
qqs = self.request.event.questionnaires.all()
|
||||
qqs = self.request.event.questions.all()
|
||||
if self.only_user_visible:
|
||||
qqs = qqs.filter(type='PS')
|
||||
else:
|
||||
qqs = qqs.filter(type__startswith='P')
|
||||
qqs = qqs.filter(
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=self.order.sales_channel.identifier)
|
||||
)
|
||||
qqs = qqs.filter(ask_during_checkin=False, hidden=False, container_type=Question.ContainerType.ORDERPOSITION)
|
||||
return list(self.order.positions.select_related(
|
||||
'item', 'variation'
|
||||
).prefetch_related(
|
||||
Prefetch('answers',
|
||||
QuestionAnswer.objects.prefetch_related('options'),
|
||||
to_attr='answerlist'),
|
||||
Prefetch('item__questionnaires',
|
||||
Prefetch('item__questions',
|
||||
qqs.prefetch_related(
|
||||
Prefetch('children', QuestionnaireChild.objects.prefetch_related(
|
||||
Prefetch('user_datafield', Question.objects.prefetch_related(
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
))
|
||||
),
|
||||
to_attr='childlist')
|
||||
),
|
||||
to_attr='relevant_questionnaires')
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
).select_related('dependency_question'),
|
||||
to_attr='questions_to_ask')
|
||||
))
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -26,7 +26,6 @@ from django.core import signing
|
||||
from django.http import HttpResponseBadRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.urls import reverse
|
||||
from django.utils.html import format_html
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -61,7 +60,6 @@ def redir_view(request):
|
||||
u = urllib.parse.urlparse(url)
|
||||
return render(request, 'pretixbase/redirect.html', {
|
||||
'hostname': u.hostname,
|
||||
'bold_hostname': format_html("<strong>{}</strong>", u.hostname),
|
||||
'url': url,
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -105,12 +105,12 @@ class GlobalSettingsForm(SettingsForm):
|
||||
domain=settings.SITE_URL
|
||||
)
|
||||
)),
|
||||
('widget_vue2_origins', forms.CharField(
|
||||
('widget_vite_origins', forms.CharField(
|
||||
widget=forms.Textarea(attrs={'rows': '3'}),
|
||||
required=False,
|
||||
# Not translated on purpose, this is a temporary feature and contains too many special case words
|
||||
label="Vue2 widget origins",
|
||||
help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the old vue2-based widget.",
|
||||
label="Vite widget origins",
|
||||
help_text="One origin per line (e.g. https://example.com). Requests from these origins will be served the new vite-based widget.",
|
||||
))
|
||||
])
|
||||
responses = register_global_settings.send(self)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -861,9 +861,6 @@ class OrganizerPluginStateLogEntryType(LogEntryType):
|
||||
'pretix.event.question.option.added': _('An answer option has been added to the question.'),
|
||||
'pretix.event.question.option.deleted': _('An answer option has been removed from the question.'),
|
||||
'pretix.event.question.option.changed': _('An answer option has been changed.'),
|
||||
'pretix.event.questionnaire.added': _('A questionnaire has been created.'),
|
||||
'pretix.event.questionnaire.deleted': _('A questionnaire has been deleted.'),
|
||||
'pretix.event.questionnaire.changed': _('A questionnaire has been changed.'),
|
||||
'pretix.event.permissions.added': _('A user has been added to the event team.'),
|
||||
'pretix.event.permissions.invited': _('A user has been invited to the event team.'),
|
||||
'pretix.event.permissions.changed': _('A user\'s permissions have been changed.'),
|
||||
|
||||
@@ -182,12 +182,12 @@ def get_event_navigation(request: HttpRequest):
|
||||
'active': 'event.items.categories' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Questionnaires'),
|
||||
'url': reverse('control:event.items.questionnaires', kwargs={
|
||||
'label': _('Questions'),
|
||||
'url': reverse('control:event.items.questions', kwargs={
|
||||
'event': request.event.slug,
|
||||
'organizer': request.event.organizer.slug,
|
||||
}),
|
||||
'active': 'event.items.questionnaires' in url.url_name or 'event.items.questions' in url.url_name,
|
||||
'active': 'event.items.questions' in url.url_name,
|
||||
},
|
||||
{
|
||||
'label': _('Discounts'),
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -12,120 +12,151 @@
|
||||
{% endblock %}
|
||||
{% block inside %}
|
||||
{% if question %}
|
||||
<h1>{% blocktrans with name=question.question %}Data field: {{ name }}{% endblocktrans %}</h1>
|
||||
<h1>{% blocktrans with name=question.question %}Question: {{ name }}{% endblocktrans %}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Data field" %}</h1>
|
||||
<h1>{% trans "Question" %}</h1>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.question layout="control" %}
|
||||
{% bootstrap_field form.type layout="control" %}
|
||||
<div id="valid-number">
|
||||
{% bootstrap_field form.valid_number_min layout="control" %}
|
||||
{% bootstrap_field form.valid_number_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-date">
|
||||
{% bootstrap_field form.valid_date_min layout="control" %}
|
||||
{% bootstrap_field form.valid_date_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-datetime">
|
||||
{% bootstrap_field form.valid_datetime_min layout="control" %}
|
||||
{% bootstrap_field form.valid_datetime_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-string">
|
||||
{% bootstrap_field form.valid_string_length_min layout="control" %}
|
||||
{% bootstrap_field form.valid_string_length_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-file">
|
||||
{% bootstrap_field form.valid_file_portrait layout="control" %}
|
||||
</div>
|
||||
<div id="answer-options">
|
||||
<h3>{% trans "Answer options" %}</h3>
|
||||
<noscript>
|
||||
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
|
||||
</noscript>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" data-formset-delete-confirm-text="{% trans "If you delete an answer option, you will no longer be able to see statistical data on customers who previously selected this option, and when such customers edit their answers, they need to select a different option." %}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% blocktrans trimmed with id=form.instance.identifier %}
|
||||
Answer option {{ id }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="tabbed-form">
|
||||
<fieldset>
|
||||
<legend>{% trans "General" %}</legend>
|
||||
{% bootstrap_field form.question layout="control" %}
|
||||
{% bootstrap_field form.type layout="control" %}
|
||||
{% if form.items %}
|
||||
{% bootstrap_field form.items layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.required layout="control" %}
|
||||
<div class="alert alert-info alert-required-boolean">
|
||||
{% blocktrans trimmed %}
|
||||
If you mark a Yes/No question as required, it means that the user has to select Yes and No is not
|
||||
accepted. If you want to allow both options, do not make this field required.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% trans "New answer option" %}
|
||||
</span>
|
||||
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
|
||||
<div id="valid-number">
|
||||
{% bootstrap_field form.valid_number_min layout="control" %}
|
||||
{% bootstrap_field form.valid_number_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-date">
|
||||
{% bootstrap_field form.valid_date_min layout="control" %}
|
||||
{% bootstrap_field form.valid_date_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-datetime">
|
||||
{% bootstrap_field form.valid_datetime_min layout="control" %}
|
||||
{% bootstrap_field form.valid_datetime_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-string">
|
||||
{% bootstrap_field form.valid_string_length_min layout="control" %}
|
||||
{% bootstrap_field form.valid_string_length_max layout="control" %}
|
||||
</div>
|
||||
<div id="valid-file">
|
||||
{% bootstrap_field form.valid_file_portrait layout="control" %}
|
||||
</div>
|
||||
<div id="answer-options">
|
||||
<h3>{% trans "Answer options" %}</h3>
|
||||
<noscript>
|
||||
<p>{% trans "Only applicable if you choose 'Choose one/multiple from a list' above." %}</p>
|
||||
</noscript>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}" data-formset-delete-confirm-text="{% trans "If you delete an answer option, you will no longer be able to see statistical data on customers who previously selected this option, and when such customers edit their answers, they need to select a different option." %}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% blocktrans trimmed with id=form.instance.identifier %}
|
||||
Answer option {{ id }}
|
||||
{% endblocktrans %}
|
||||
</span>
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new option" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
{% bootstrap_field formset.empty_form.ORDER form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="row question-option-row">
|
||||
<div class="col-xs-10">
|
||||
<span class="text-muted">
|
||||
{% trans "New answer option" %}
|
||||
</span>
|
||||
{% bootstrap_field formset.empty_form.answer layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-xs-2 text-right flip">
|
||||
<span> </span><br>
|
||||
<button type="button" class="btn btn-default" data-formset-move-up-button>
|
||||
<i class="fa fa-arrow-up"></i></button>
|
||||
<button type="button" class="btn btn-default" data-formset-move-down-button>
|
||||
<i class="fa fa-arrow-down"></i></button>
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new option" %}</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Advanced" %}</legend>
|
||||
{% bootstrap_field form.help_text layout="control" %}
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% if form.ask_during_checkin %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
{% endif %}
|
||||
{% if form.show_during_checkin %}
|
||||
{% bootstrap_field form.show_during_checkin layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.hidden layout="control" %}
|
||||
{% if form.print_on_invoice %}
|
||||
{% bootstrap_field form.print_on_invoice layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_dependency_question">
|
||||
{% trans "Question dependency" %}
|
||||
<br><span class="optional">{% trans "Optional" context "form" %}</span>
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
{% bootstrap_field form.dependency_question layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<script type="text/plain" id="dependency_value_val">{{ form.instance.dependency_values|escapejson_dumps }}</script>
|
||||
{% bootstrap_field form.dependency_values layout="inline" form_group_class="inner" %}
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</div>
|
||||
{% bootstrap_field form.identifier layout="control" %}
|
||||
{% if form.ask_during_checkin %}
|
||||
{% bootstrap_field form.ask_during_checkin layout="control" %}
|
||||
{% endif %}
|
||||
{% if form.show_during_checkin %}
|
||||
{% bootstrap_field form.show_during_checkin layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.hidden layout="control" %}
|
||||
{% if form.print_on_invoice %}
|
||||
{% bootstrap_field form.print_on_invoice layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="form-group submit-group">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load static %}
|
||||
{% load icon %}
|
||||
{% load compress %}
|
||||
{% load vite %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Questionnaires" %}
|
||||
{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>
|
||||
{% trans "Questionnaires" %}
|
||||
<a href="{% url "control:event.items.questions" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default pull-right">
|
||||
{% icon "wrench" %} {% trans "Manage data fields" %}
|
||||
</a>
|
||||
</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Questionaires allow your attendees to fill in additional data about their ticket. If you provide food, one
|
||||
example might be to ask your users about dietary requirements.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<p>
|
||||
TODO(questionnaires) : add more specific explanation of the questionnaire concept.
|
||||
</p>
|
||||
|
||||
{{ request.event.settings.locales|json_script:"event_locales" }}
|
||||
{{ questionnaire_type_choices|json_script:"questionnaire_type_choices" }}
|
||||
|
||||
{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=0 as datafield_edit_url %}
|
||||
{{ datafield_edit_url|json_script:"datafield_edit_url" }}
|
||||
|
||||
<div id="questionnaires-editor">
|
||||
<!-- Vue app mount point -->
|
||||
</div>
|
||||
|
||||
{% vite_hmr %}
|
||||
{% vite_asset "src/pretix/static/pretixcontrol/js/ui/questionnaires/index.ts" %}
|
||||
{% endblock %}
|
||||
@@ -1,8 +1,8 @@
|
||||
{% extends "pretixcontrol/items/base.html" %}
|
||||
{% load i18n %}
|
||||
{% block title %}{% trans "Data fields" %}{% endblock %}
|
||||
{% block title %}{% trans "Questions" %}{% endblock %}
|
||||
{% block inside %}
|
||||
<h1>{% trans "Data fields" %}</h1>
|
||||
<h1>{% trans "Questions" %}</h1>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
Questions allow your attendees to fill in additional data about their ticket. If you provide food, one
|
||||
@@ -14,19 +14,19 @@
|
||||
{% if request.event.settings.feature_flag_order_level_questions %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new per-ticket data field" %}
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new per-ticket question" %}
|
||||
</a>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=O" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new order-level data field" %}
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=O" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new order-level question" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<h2>{% trans "Per-ticket data fields" %}</h2>
|
||||
<p>{% trans "These data field can be used on individual tickets." %}</p>
|
||||
<h2>{% trans "Per-ticket questions" %}</h2>
|
||||
<p>{% trans "These questions are asked for every ticket, so possibly multiple times in the same order." %}</p>
|
||||
{% else %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<p>
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new data field" %}
|
||||
<a href="{% url "control:event.items.questions.add" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P" class="btn btn-default"><i class="fa fa-plus"></i> {% trans "Create a new question" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
@@ -35,27 +35,39 @@
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Internal name" %}</th>
|
||||
<th>{% trans "Question" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<th class="action-col-2"></th>
|
||||
{% endif %}
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody data-dnd-url="{% url "control:event.items.questions.reorder" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=P">
|
||||
{% for q in questions %}{% if q.container_type == "P" %}
|
||||
<tr>
|
||||
<tr data-dnd-id="{{ q.id }}">
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">
|
||||
{{ q.question }}
|
||||
</a>
|
||||
{% if q.pk %}
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}">
|
||||
{% endif %}
|
||||
{{ q.question }}
|
||||
{% if q.pk %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</strong><br>
|
||||
<small class="text-muted">{{ q.identifier }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ q.get_type_display }}
|
||||
{% if q.pk %}
|
||||
{{ q.get_type_display }}
|
||||
{% else %}
|
||||
{% trans "System question" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.required %}
|
||||
@@ -72,11 +84,35 @@
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Hidden question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.pk %}
|
||||
<ul>
|
||||
{% for item in q.items.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<small>{% trans "All personalized products" %}</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<td class="dnd-container">
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right flip">
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-bar-chart"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% if q.pk %}
|
||||
<a href="{% url "control:event.items.questions.show" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-bar-chart"></i></a>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.questions.edit" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.items.questions.delete" organizer=request.event.organizer.slug event=request.event.slug question=q.id %}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if 'event.settings.general:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.settings" organizer=request.event.organizer.slug event=request.event.slug %}#tab-0-2-open"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-wrench"></i></a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -87,31 +123,34 @@
|
||||
|
||||
{% if request.event.settings.feature_flag_order_level_questions %}
|
||||
<h2>
|
||||
{% trans "Per-order data fields" %}
|
||||
{% trans "Per-order questions" %}
|
||||
<small><span class="label label-info" title="
|
||||
{% trans "This functionality is in active development and expected to change significantly over the coming months." %}
|
||||
{% trans "In pretixPOS, per-order data fields are currently not supported and will not be displayed." %}
|
||||
{% trans "Per-order questions are currently not supported and will not be displayed in pretixPOS." %}
|
||||
" data-toggle="tooltip">
|
||||
<span class="fa fa-flask" aria-hidden="true"></span>
|
||||
{% trans "Experimental feature" %}
|
||||
</span></small>
|
||||
</h2>
|
||||
<p>{% trans "These data fields are asked once per order." %}</p>
|
||||
<p>{% trans "These questions are asked once per order." %}</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Internal name" %}</th>
|
||||
<th>{% trans "Question" %}</th>
|
||||
<th>{% trans "Type" %}</th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
<th class="iconcol"></th>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<th class="action-col-2"></th>
|
||||
{% endif %}
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody data-dnd-url="{% url "control:event.items.questions.reorder" organizer=request.event.organizer.slug event=request.event.slug %}?container_type=O">
|
||||
{% for q in questions %}{% if q.container_type == "O" %}
|
||||
<tr>
|
||||
<tr data-dnd-id="{{ q.id }}">
|
||||
<td>
|
||||
<strong>
|
||||
{{ q.question }}
|
||||
@@ -119,7 +158,11 @@
|
||||
<small class="text-muted">{{ q.identifier }}</small>
|
||||
</td>
|
||||
<td>
|
||||
{{ q.get_type_display }}
|
||||
{% if q.pk %}
|
||||
{{ q.get_type_display }}
|
||||
{% else %}
|
||||
{% trans "System question" %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if q.required %}
|
||||
@@ -136,6 +179,10 @@
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip" title="{% trans "Hidden question" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<td class="dnd-container">
|
||||
</td>
|
||||
{% endif %}
|
||||
<td class="text-right flip">
|
||||
{% if q.pk %}
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
|
||||
@@ -20,10 +20,8 @@
|
||||
{% endif %}
|
||||
{% elif question.type == "M" %}
|
||||
{{ answer.to_string_i18n|rich_text_snippet }}
|
||||
{% elif question.type %}
|
||||
{{ answer.to_string_i18n|rich_text_snippet }}
|
||||
{% else %}
|
||||
{{ answer|linebreaksbr }}
|
||||
{{ answer.to_string_i18n|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
|
||||
@@ -610,9 +610,60 @@
|
||||
{% endif %}
|
||||
{% if line.has_questions %}
|
||||
<dl>
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_names_asked %}
|
||||
<dt>{% trans "Attendee name" %}</dt>
|
||||
<dd>{% if line.attendee_name %}{{ line.attendee_name_all_components }}{% else %}
|
||||
<em>{% trans "not answered" %}</em>{% endif %}</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_emails_asked %}
|
||||
<dt>{% trans "Attendee email" %}</dt>
|
||||
<dd>
|
||||
{% if line.attendee_email %}
|
||||
{{ line.attendee_email }}
|
||||
{% if not line.addon_to %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
|
||||
class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_company_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee company" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if line.company %}{{ line.company }}{% else %}<em>{% trans "not answered" %}</em>{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% if line.item.ask_attendee_data and event.settings.attendee_addresses_asked %}
|
||||
<dt>
|
||||
{% trans "Attendee address" %}
|
||||
</dt>
|
||||
<dd>
|
||||
{% if line.street or line.zipcode or line.city or line.country %}
|
||||
{{ line.street|default_if_none:""|linebreaksbr }}<br>
|
||||
{{ line.zipcode|default_if_none:"" }} {{ line.city|default_if_none:"" }}<br>
|
||||
{% if line.state %}{{ line.state_for_address }}<br>{% endif %}
|
||||
{{ line.country.name|default_if_none:"" }}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
{% for q in line.questions %}
|
||||
<dt>
|
||||
{{ q.label }}
|
||||
{{ q.question }}
|
||||
{% if q.ask_during_checkin %}
|
||||
<span class="fa fa-qrcode text-muted"
|
||||
data-toggle="tooltip"
|
||||
@@ -621,49 +672,7 @@
|
||||
{% endif %}
|
||||
</dt>
|
||||
<dd>
|
||||
{#
|
||||
{% if q.answer %}
|
||||
{% if q.answer.file %}
|
||||
<span class="fa fa-file"></span>
|
||||
<a href="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}">
|
||||
{{ q.answer.file_name }}
|
||||
</a>
|
||||
<span class="label label-danger" data-toggle="tooltip"
|
||||
title="{% trans "This file has been uploaded by a user and could contain viruses or other malicious content." %}">
|
||||
{% trans "UNSAFE" %}
|
||||
</span>
|
||||
{% if q.answer.is_image %}
|
||||
<br>
|
||||
<a href="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}" data-lightbox="order"
|
||||
class="answer-thumb">
|
||||
<img src="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}">
|
||||
</a>
|
||||
{% endif %}
|
||||
{% elif q.type == "M" %}
|
||||
{{ q.answer.to_string_i18n|rich_text_snippet }}
|
||||
{% elif q.type %}
|
||||
{{ q.answer.to_string_i18n|linebreaksbr }}
|
||||
{% else %}<!-- TODO(questionnaires): proper separation of QuestionAnswer objects and system answers...... -->
|
||||
{{ q.answer|linebreaksbr }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<em>{% trans "not answered" %}</em>
|
||||
{% endif %}
|
||||
#}
|
||||
{% include "pretixcontrol/order/fragment_question_answer.html" with request=request question=q answer=q.answer %}
|
||||
{% if q.system_datafield == "attendee_email" and not line.addon_to %}
|
||||
<form class="form-inline helper-display-inline" method="post"
|
||||
action="{% url "control:event.order.resendlink" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}">
|
||||
{% csrf_token %}
|
||||
<a href="{% url "control:event.order.position.sendmail" event=request.event.slug organizer=request.event.organizer.slug code=order.code position=line.pk %}"
|
||||
class="btn btn-default btn-xs">
|
||||
<span class="fa fa-envelope-o"></span>
|
||||
</a>
|
||||
<button class="btn btn-default btn-xs">
|
||||
{% trans "Resend link" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% endfor %}
|
||||
{% for q in line.additional_fields %}
|
||||
|
||||
@@ -350,7 +350,6 @@ urlpatterns = [
|
||||
re_path(r'^questions/(?P<question>\d+)/change$', item.QuestionUpdate.as_view(),
|
||||
name='event.items.questions.edit'),
|
||||
re_path(r'^questions/add$', item.QuestionCreate.as_view(), name='event.items.questions.add'),
|
||||
re_path(r'^questionnaires/$', item.QuestionnairesEditor.as_view(), name='event.items.questionnaires'),
|
||||
re_path(r'^quotas/$', item.QuotaList.as_view(), name='event.items.quotas'),
|
||||
re_path(r'^quotas/bulk_action$', item.QuotaBulkAction.as_view(), name='event.items.quotas.bulkaction'),
|
||||
re_path(r'^quotas/bulk_edit$', item.QuotaBulkUpdateView.as_view(), name='event.items.quotas.bulkedit'),
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -163,9 +163,6 @@ class DiscountCreate(EventPermissionRequiredMixin, CreateView):
|
||||
i = modelcopy(self.copy_from)
|
||||
i.pk = None
|
||||
kwargs['instance'] = i
|
||||
kwargs["initial"]["limit_sales_channels"] = self.copy_from.limit_sales_channels.all()
|
||||
kwargs["initial"]["condition_limit_products"] = self.copy_from.condition_limit_products.all()
|
||||
kwargs["initial"]["benefit_limit_products"] = self.copy_from.benefit_limit_products.all()
|
||||
else:
|
||||
kwargs['instance'] = Discount(event=self.request.event)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext, gettext_lazy as _
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.views.generic import FormView, ListView, TemplateView, View
|
||||
from django.views.generic import FormView, ListView, View
|
||||
from django.views.generic.detail import DetailView, SingleObjectMixin
|
||||
from django_countries.fields import Country
|
||||
|
||||
@@ -65,6 +65,7 @@ from pretix.api.serializers.item import (
|
||||
ItemVariationSerializer,
|
||||
)
|
||||
from pretix.base.forms import I18nFormSet
|
||||
from pretix.base.forms.questions import get_fake_attendee_questions
|
||||
from pretix.base.models import (
|
||||
CartPosition, Item, ItemCategory, ItemProgramTime, ItemVariation, LogEntry,
|
||||
OrderPosition, Question, QuestionAnswer, QuestionOption, Quota,
|
||||
@@ -436,7 +437,66 @@ class QuestionList(ListView):
|
||||
template_name = 'pretixcontrol/items/questions.html'
|
||||
|
||||
def get_queryset(self):
|
||||
return self.request.event.questions
|
||||
return self.request.event.questions.prefetch_related('items')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
|
||||
questions = get_fake_attendee_questions(self.request.event.settings)
|
||||
|
||||
questions += list(ctx['questions'])
|
||||
questions.sort(key=lambda q: q.position)
|
||||
ctx['questions'] = questions
|
||||
return ctx
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
@event_permission_required("event.items:write")
|
||||
@require_http_methods(["POST"])
|
||||
def reorder_questions(request, organizer, event):
|
||||
try:
|
||||
ids = json.loads(request.body.decode('utf-8'))['ids']
|
||||
except (JSONDecodeError, KeyError, ValueError):
|
||||
return HttpResponseBadRequest("expected JSON: {ids:[]}")
|
||||
|
||||
qs = request.event.questions.filter(container_type=request.GET['container_type'])
|
||||
|
||||
# filter system_questions - normal questions are int/digit, system_questions strings
|
||||
custom_question_ids = [i for i in ids if i.isdigit()]
|
||||
input_questions = list(qs.filter(id__in=custom_question_ids))
|
||||
|
||||
if len(input_questions) != len(custom_question_ids):
|
||||
raise Http404(_("Some of the provided object ids are invalid."))
|
||||
|
||||
if len(input_questions) != qs.count():
|
||||
raise Http404(_("Not all objects have been selected."))
|
||||
|
||||
for q in input_questions:
|
||||
pos = ids.index(str(q.pk))
|
||||
if pos != q.position: # Save unneccessary UPDATE queries
|
||||
q.position = pos
|
||||
q.save(update_fields=['position'])
|
||||
q.log_action(
|
||||
'pretix.event.question.reordered', user=request.user, data={
|
||||
'position': pos,
|
||||
}
|
||||
)
|
||||
|
||||
if request.GET['container_type'] == Question.ContainerType.ORDERPOSITION:
|
||||
system_question_order = {}
|
||||
for s in ('attendee_name_parts', 'attendee_email', 'company', 'street', 'zipcode', 'city', 'country'):
|
||||
if s in ids:
|
||||
system_question_order[s] = ids.index(s)
|
||||
else:
|
||||
system_question_order[s] = -1
|
||||
request.event.settings.system_question_order = system_question_order
|
||||
request.event.log_action(
|
||||
'pretix.event.settings', user=request.user, data={
|
||||
'system_question_order': system_question_order,
|
||||
}
|
||||
)
|
||||
|
||||
return HttpResponse()
|
||||
|
||||
|
||||
class QuestionDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
@@ -706,11 +766,6 @@ class QuestionCreate(EventPermissionRequiredMixin, QuestionMixin, CreateView):
|
||||
return ret
|
||||
|
||||
|
||||
class QuestionnairesEditor(EventPermissionRequiredMixin, TemplateView):
|
||||
permission = 'can_change_items'
|
||||
template_name = 'pretixcontrol/items/questionnaires.html'
|
||||
|
||||
|
||||
class QuotaQueryMixin:
|
||||
|
||||
@cached_property
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -144,7 +144,7 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
|
||||
headers = [
|
||||
_('Voucher code'), _('Valid until'), _('Product'), _('Reserve quota'), _('Bypass quota'),
|
||||
_('Price effect'), _('Value'), _('Tag'), _('Redeemed'), _('Maximum usages'), _('Seat'),
|
||||
_('Comment'), _('Budget'), _('Budget used')
|
||||
_('Comment')
|
||||
]
|
||||
writer.writerow(headers)
|
||||
|
||||
@@ -170,9 +170,7 @@ class VoucherList(VoucherQueryMixin, PaginationMixin, EventPermissionRequiredMix
|
||||
str(v.redeemed),
|
||||
str(v.max_usages),
|
||||
str(v.seat) if v.seat else "",
|
||||
str(v.comment) if v.comment else "",
|
||||
str(v.budget) if v.budget is not None else "",
|
||||
str(v.budget_used) if v.budget is not None else "",
|
||||
str(v.comment) if v.comment else ""
|
||||
]
|
||||
writer.writerow(row)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -8,16 +8,16 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
|
||||
"PO-Revision-Date: 2026-08-24 14:30+0000\n"
|
||||
"Last-Translator: Albizuri <oier@puntu.eus>\n"
|
||||
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"eu/>\n"
|
||||
"Language-Team: Basque <https://translate.pretix.eu/projects/pretix/pretix/eu/"
|
||||
">\n"
|
||||
"Language: eu\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.9\n"
|
||||
"X-Generator: Weblate 2026.8.1\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -891,7 +891,7 @@ msgstr "Produktuaren data"
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order.html
|
||||
msgid "Order details"
|
||||
msgstr "Eskaeraren xehetasunak"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/datasync/sourcefields.py pretix/base/modelimport_orders.py
|
||||
#: pretix/control/forms/filter.py
|
||||
@@ -17296,7 +17296,7 @@ msgstr ""
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
msgid "The order details have been changed."
|
||||
msgstr "Eskaeraren xehetasunak aldatu dira."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/logdisplay.py
|
||||
msgid "The order has been marked as unpaid."
|
||||
@@ -23106,7 +23106,7 @@ msgstr ""
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#, python-format
|
||||
msgid "Order details: %(code)s"
|
||||
msgstr "Eskaeraren xehetasunak: %(code)s"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/index.html
|
||||
#: pretix/control/templates/pretixcontrol/orders/index.html
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-09-07 18:00+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-08-16 22:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 2026.9\n"
|
||||
"X-Generator: Weblate 2026.8.1\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -93,7 +93,7 @@ msgstr "ヘブライ語"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr "ハンガリー語"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
@@ -220,7 +220,7 @@ msgstr "ターゲットURL"
|
||||
#: pretix/api/models.py pretix/base/models/devices.py
|
||||
#: pretix/base/models/organizer.py
|
||||
msgid "All events (including newly created ones)"
|
||||
msgstr "すべてのイベント(今後作成されるものを含む)"
|
||||
msgstr "すべてのイベント(最近作成されたイベントを含む)"
|
||||
|
||||
#: pretix/api/models.py pretix/base/models/devices.py
|
||||
#: pretix/base/models/organizer.py
|
||||
@@ -428,8 +428,10 @@ msgid "You cannot exchange a medium for a medium."
|
||||
msgstr "メディアを別のメディアに変更することはできません。"
|
||||
|
||||
#: pretix/api/views/checkin.py
|
||||
#, fuzzy
|
||||
#| msgid "Product does not support medium exchange."
|
||||
msgid "You cannot simulate a medium exchange."
|
||||
msgstr "メディアの交換をシミュレートすることはできません。"
|
||||
msgstr "本製品は中程度の交換に対応していません。"
|
||||
|
||||
#: pretix/api/views/oauth.py pretix/control/logdisplay.py
|
||||
#, python-brace-format
|
||||
@@ -5689,7 +5691,7 @@ msgstr "国コード(ISO 3166-1 alpha-2)"
|
||||
|
||||
#: pretix/base/models/items.py
|
||||
msgid "Asked on"
|
||||
msgstr "質問日"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/models/items.py pretix/base/models/organizer.py
|
||||
msgid ""
|
||||
@@ -7338,9 +7340,12 @@ msgid "The payment for this invoice has already been received."
|
||||
msgstr "この請求書の支払いはすでに受領済みです。"
|
||||
|
||||
#: pretix/base/payment.py
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "This payment is already being processed and can not be canceled any more."
|
||||
msgid ""
|
||||
"This payment is already being processed and cannot be canceled any more."
|
||||
msgstr "この支払いはすでに処理中のため、キャンセルできません。"
|
||||
msgstr "この支払いは処理中のため、キャンセルできません。"
|
||||
|
||||
#: pretix/base/payment.py
|
||||
msgid "Automatic refunds are not supported by this payment provider."
|
||||
@@ -8100,12 +8105,16 @@ msgid "Presale end"
|
||||
msgstr "前売り終了"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order email"
|
||||
msgid "Order creation"
|
||||
msgstr "注文の作成"
|
||||
msgstr "注文者メール"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, fuzzy
|
||||
#| msgid "Order expired"
|
||||
msgid "Order expiry"
|
||||
msgstr "注文の失効"
|
||||
msgstr "注文の有効期限が切れました"
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
msgid "before"
|
||||
@@ -8134,22 +8143,22 @@ msgstr "未設定"
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr "相対的な日付は、「{}」に対して「before」で表すことはできません"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative date cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr "相対的な日付は「{}」に対して「after」として表すことはできません"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"before\" for \"{}\""
|
||||
msgstr "相対時間は「{}」に対して「before」で表すことはできません"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/reldate.py
|
||||
#, python-brace-format
|
||||
msgid "A relative time cannot be expressed as \"after\" for \"{}\""
|
||||
msgstr "相対時間は「{}」に対して「after」で表すことはできません"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/secrets.py
|
||||
msgid "Random (default, works with all pretix apps)"
|
||||
@@ -10058,12 +10067,12 @@ msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "No dates match your criteria."
|
||||
msgstr "条件に合致する日付はありません。"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
msgid "Text for empty date results"
|
||||
msgstr "空の日付結果のテキスト"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgctxt "subevents"
|
||||
@@ -10074,10 +10083,6 @@ msgid ""
|
||||
"touch with you to arrange further dates. We do not recommend more than one "
|
||||
"or two sentences."
|
||||
msgstr ""
|
||||
"このテキストは、カレンダーまたは日付リストが空の場合に表示されます。たとえば"
|
||||
"、月に日付が含まれていない場合や、ユーザーが選択したフィルターに結果が見つか"
|
||||
"らない場合です。これをご利用いただくことで、今後の日程調整のために連絡する方"
|
||||
"法を宣伝できます。1文または2文以上は推奨いたしません。"
|
||||
|
||||
#: pretix/base/settings.py
|
||||
msgid "Guidance text"
|
||||
@@ -13897,20 +13902,26 @@ msgid ""
|
||||
msgstr "ギフトカードの有効期限を発行年を含めた{}年に設定しました。"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment date"
|
||||
msgid "Payment term"
|
||||
msgstr "支払条件"
|
||||
msgstr "支払い日"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "same as above"
|
||||
msgstr "上記と同じ"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment term in days"
|
||||
msgid "different payment term in days"
|
||||
msgstr "日数で表す異なる支払条件"
|
||||
msgstr "支払い条件(日数)"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
#, fuzzy
|
||||
#| msgid "Payment term in minutes"
|
||||
msgid "different payment term in minutes"
|
||||
msgstr "分で表す異なる支払条件"
|
||||
msgstr "支払い期限(分単位)"
|
||||
|
||||
#: pretix/control/forms/event.py
|
||||
msgid "Prices including tax"
|
||||
@@ -22522,24 +22533,28 @@ msgstr ""
|
||||
"食事を提供する場合、ユーザーに食事制限について尋ねることができる一例です。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Create a new question"
|
||||
msgid "Create a new per-ticket question"
|
||||
msgstr "チケットごとに新しい質問を作成"
|
||||
msgstr "新しい質問を作成"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Create a new question"
|
||||
msgid "Create a new order-level question"
|
||||
msgstr "注文ごとに新しい質問を作成"
|
||||
msgstr "新しい質問を作成"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Delete question"
|
||||
msgid "Per-ticket questions"
|
||||
msgstr "チケットごとの質問"
|
||||
msgstr "質問を削除"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"These questions are asked for every ticket, so possibly multiple times in "
|
||||
"the same order."
|
||||
msgstr ""
|
||||
"これらの質問はすべてのチケットに対して尋ねられるため、同じ順序で複数回になる"
|
||||
"可能性があります。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid "Create a new question"
|
||||
@@ -22558,24 +22573,28 @@ msgid "All personalized products"
|
||||
msgstr "すべてのパーソナライズされた製品"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "Include questions"
|
||||
msgid "Per-order questions"
|
||||
msgstr "注文ごとの質問"
|
||||
msgstr "質問を含む"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"This functionality is in active development and expected to change "
|
||||
"significantly over the coming months."
|
||||
msgstr "この機能は現在開発が活発で、今後数か月で大幅に変化すると予想されています。"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
msgid ""
|
||||
"Per-order questions are currently not supported and will not be displayed in "
|
||||
"pretixPOS."
|
||||
msgstr "注文ごとの質問は現在サポートされておらず、pretixPOSでは表示されません。"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/questions.html
|
||||
#, fuzzy
|
||||
#| msgid "The question has been reordered."
|
||||
msgid "These questions are asked once per order."
|
||||
msgstr "これらの質問は、注文ごとに1回ずつ尋ねられます。"
|
||||
msgstr "質問の並び順が変更されました。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/items/quota.html
|
||||
#: pretix/control/templates/pretixcontrol/items/quota_edit.html
|
||||
@@ -23167,8 +23186,10 @@ msgstr "(任意)"
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_confirm.html
|
||||
#: pretix/presale/templates/pretixpresale/event/checkout_questions.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_modify.html
|
||||
#, fuzzy
|
||||
#| msgid "Additional information"
|
||||
msgid "Additional order information"
|
||||
msgstr "追加の注文情報"
|
||||
msgstr "追加情報"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/order/delete.html
|
||||
msgid "Delete order"
|
||||
@@ -24835,13 +24856,14 @@ msgid "Hardware model"
|
||||
msgstr "ハードウェアの機種"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
#, python-format
|
||||
#, fuzzy, python-format
|
||||
#| msgid "Begin: %(time)s"
|
||||
msgid "Last seen: %(time)s"
|
||||
msgstr "最後の閲覧: %(time)s"
|
||||
msgstr "開始: %(time)s"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "No recent contact"
|
||||
msgstr "最近の連絡なし"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/organizers/devices.html
|
||||
msgid "Not yet initialized"
|
||||
@@ -28198,14 +28220,19 @@ msgstr ""
|
||||
"に高いことを意味します。ドメインのDNS設定を更新すべきです。"
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
#, fuzzy
|
||||
#| msgid ""
|
||||
#| "We did not find DMARC record for your domain. This means that there is a "
|
||||
#| "very high chance most of the emails will be rejected or marked as spam. "
|
||||
#| "You should update the DNS settings of your domain."
|
||||
msgid ""
|
||||
"We did not find a DMARC record for your domain. This means that there is a "
|
||||
"very high chance most of the emails will be rejected or marked as spam. You "
|
||||
"should update the DNS settings of your domain."
|
||||
msgstr ""
|
||||
"あなたのドメインのDMARCレコードが見つかりませんでした。これは、ほとんどの"
|
||||
"メールが拒否されたかスパムとしてマークされた可能性が非常に高いことを意味しま"
|
||||
"す。ドメインのDNS設定を更新すべきです。"
|
||||
"お客様のドメインのDMARCレコードが見つかりませんでした。これは、ほとんどのメー"
|
||||
"ルが拒否されたりスパムとしてマークされたりする可能性が非常に高いことを意味し"
|
||||
"ます。ドメインのDNS設定を更新すべきです。"
|
||||
|
||||
#: pretix/control/views/mailsetup.py
|
||||
msgid "The verification code was incorrect, please try again."
|
||||
@@ -30879,7 +30906,7 @@ msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Allow further payments during compliance hold"
|
||||
msgstr "コンプライアンス保留の間、追加の支払いを許可する"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
@@ -30889,20 +30916,16 @@ msgid ""
|
||||
"attempts during that window. This might result in them being charged twice "
|
||||
"if the original payment is approved."
|
||||
msgstr ""
|
||||
"PayPalの不正防止は、個々の支払いの処理をかなりの期間ブロックする可能性があり"
|
||||
"ます。この期間中、支払いは「保留中」とマークされています。顧客がその期間中に"
|
||||
"別の支払い試行を開始できるように許可することができます。元の支払いが承認され"
|
||||
"た場合、二重に請求される可能性があります。"
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "Timeout further payment attempts"
|
||||
msgstr "追加の支払い試行がタイムアウト"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid ""
|
||||
"Time duration in minutes after which another payment attempt is possible, "
|
||||
"while the last payment is still under investigation."
|
||||
msgstr "最後の支払いがまだ調査中である間、別の支払い試行が可能となる時間(分)。"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/plugins/paypal2/payment.py
|
||||
msgid "-- Automatic --"
|
||||
@@ -31167,10 +31190,6 @@ msgid ""
|
||||
"twice in case PayPal allows your initial payment attempt. Please contact us "
|
||||
"to resolve this case."
|
||||
msgstr ""
|
||||
"支払いはPayPalで処理されています。通常より時間がかかります。PayPalが支払いを"
|
||||
"確認するまでお待ちいただくか、こちらまたは別の支払い方法で再度お支払いをお試"
|
||||
"しください。PayPalが最初の支払い試行を許可した場合、二重に請求される可能性が"
|
||||
"あります。この件を解決するために、弊社までご連絡ください。"
|
||||
|
||||
#: pretix/plugins/paypal2/views.py
|
||||
msgid ""
|
||||
|
||||
@@ -8,7 +8,7 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-09-02 08:35+0000\n"
|
||||
"PO-Revision-Date: 2026-03-30 21:00+0000\n"
|
||||
"Last-Translator: Renne Rocha <renne@rocha.dev.br>\n"
|
||||
"Language-Team: Portuguese (Brazil) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/pt_BR/>\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n > 1;\n"
|
||||
"X-Generator: Weblate 2026.8.1\n"
|
||||
"X-Generator: Weblate 5.16.2\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -93,7 +93,7 @@ msgstr "Hebraico"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Hungarian"
|
||||
msgstr "Húngaro"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Indonesian"
|
||||
@@ -153,7 +153,7 @@ msgstr "Espanhol (América latina)"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Thai"
|
||||
msgstr "Tailandês"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "Turkish"
|
||||
@@ -3022,12 +3022,11 @@ msgid ""
|
||||
"The field \"%(label)s\" may not contain special characters such as "
|
||||
"\"%(chars)s\"."
|
||||
msgstr ""
|
||||
"O campo \"%(label)s\" não pode conter caracteres especiais como %(chars)s\"."
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
#, python-format
|
||||
msgid "The field \"%(label)s\" may not contain an URL (%(url)s)."
|
||||
msgstr "O campo \"%(label)s\" não pode conter uma URL (%(url)s)."
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/forms/questions.py
|
||||
msgctxt "phonenumber"
|
||||
@@ -8174,7 +8173,7 @@ msgstr "Permitida"
|
||||
#: pretix/base/permissions.py
|
||||
msgctxt "permission_level"
|
||||
msgid "Access existing events"
|
||||
msgstr "Acessar eventos existentes"
|
||||
msgstr ""
|
||||
|
||||
#: pretix/base/permissions.py
|
||||
msgctxt "permission_level"
|
||||
@@ -13608,15 +13607,11 @@ msgstr "Por favor, continue em uma nova aba"
|
||||
#: pretix/base/templates/pretixbase/framebreak.html
|
||||
msgid "For security reasons, the following step is only possible in a new tab."
|
||||
msgstr ""
|
||||
"Por razões de segurança, o passo seguinte só é possível de ser feito em uma "
|
||||
"nova aba."
|
||||
|
||||
#: pretix/base/templates/pretixbase/framebreak.html
|
||||
msgid ""
|
||||
"If the new tab did not open automatically, please click the following button:"
|
||||
msgstr ""
|
||||
"Se uma nova aba não abrir automaticamente, por favor, clique no botão a "
|
||||
"seguir:"
|
||||
|
||||
#: pretix/base/templates/pretixbase/framebreak.html
|
||||
#: pretix/presale/templates/pretixpresale/event/cookies.html
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-08-24 15:35+0000\n"
|
||||
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
|
||||
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
|
||||
"PO-Revision-Date: 2026-08-09 06:00+0000\n"
|
||||
"Last-Translator: Julien <julien@circusiloveyou.com>\n"
|
||||
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix/"
|
||||
"sv/>\n"
|
||||
"Language: sv\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.9\n"
|
||||
"X-Generator: Weblate 2026.8.1\n"
|
||||
|
||||
#: pretix/_base_settings.py
|
||||
msgid "English"
|
||||
@@ -19170,9 +19170,8 @@ msgstr ""
|
||||
#: pretix/presale/templates/pretixpresale/event/order_pay.html
|
||||
#: pretix/presale/templates/pretixpresale/event/order_pay_change.html
|
||||
#: pretix/presale/templates/pretixpresale/event/position_change.html
|
||||
#, fuzzy
|
||||
msgid "Continue"
|
||||
msgstr "Fortsätt"
|
||||
msgstr "Fortsätta"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html
|
||||
msgid "Authorize an application"
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-07-06 15:52+0000\n"
|
||||
"PO-Revision-Date: 2026-09-04 14:29+0000\n"
|
||||
"Last-Translator: Linnea Thelander <linnea@coeo.events>\n"
|
||||
"PO-Revision-Date: 2026-08-09 06:00+0000\n"
|
||||
"Last-Translator: Julien <julien@circusiloveyou.com>\n"
|
||||
"Language-Team: Swedish <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/sv/>\n"
|
||||
"Language: sv\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 2026.9\n"
|
||||
"X-Generator: Weblate 2026.8.1\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js
|
||||
msgid "Marked as paid"
|
||||
@@ -133,9 +133,8 @@ msgstr "Mercado Pago"
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/i18n.ts
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js
|
||||
#, fuzzy
|
||||
msgid "Continue"
|
||||
msgstr "Fortsätt"
|
||||
msgstr "Fortsätta"
|
||||
|
||||
#: pretix/plugins/paypal2/static/pretixplugins/paypal2/pretix-paypal.js
|
||||
#: pretix/plugins/stripe/static/pretixplugins/stripe/pretix-stripe.js
|
||||
|
||||
@@ -55,6 +55,7 @@ from django.db.models.functions import Cast, Coalesce
|
||||
from django.utils.timezone import make_aware
|
||||
from django.utils.translation import gettext as _, gettext_lazy, pgettext_lazy
|
||||
from pypdf import PageObject, PdfReader, PdfWriter, Transformation
|
||||
from pypdf.generic import RectangleObject
|
||||
from reportlab.lib import pagesizes
|
||||
from reportlab.lib.units import inch, mm
|
||||
from reportlab.pdfgen import canvas
|
||||
@@ -237,8 +238,15 @@ def _render_nup_page(nup_pdf: PdfWriter, input_pages: PageObject, opt: dict) ->
|
||||
di = i % badges_per_page
|
||||
tx = opt['margins'][3] + (di % opt['cols']) * opt['offsets'][0]
|
||||
ty = opt['margins'][2] + (opt['rows'] - 1 - (di // opt['cols'])) * opt['offsets'][1]
|
||||
page.add_transformation(Transformation().translate(tx, ty))
|
||||
page.mediabox = RectangleObject((
|
||||
Decimal('%.5f' % (page.mediabox.left.as_numeric() + tx)),
|
||||
Decimal('%.5f' % (page.mediabox.bottom.as_numeric() + ty)),
|
||||
Decimal('%.5f' % (page.mediabox.right.as_numeric() + tx)),
|
||||
Decimal('%.5f' % (page.mediabox.top.as_numeric() + ty))
|
||||
))
|
||||
page.trimbox = page.cropbox = page.mediabox
|
||||
nup_page.merge_transformed_page(page, Transformation().translate(tx, ty))
|
||||
nup_page.merge_page(page)
|
||||
return nup_page
|
||||
|
||||
|
||||
|
||||
@@ -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'
|
||||
+ '¤cy=' + pretixpaypal.currency
|
||||
let sdk_url = 'https://www.paypal.com/sdk/js' +
|
||||
'?client-id=' + pretixpaypal.client_id +
|
||||
'&components=buttons,funding-eligibility' +
|
||||
'¤cy=' + 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();
|
||||
})();
|
||||
});
|
||||
|
||||
@@ -55,8 +55,6 @@ from django_countries.fields import Country
|
||||
from reportlab.lib import colors
|
||||
from reportlab.lib.enums import TA_CENTER
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.lib.utils import simpleSplit
|
||||
from reportlab.pdfbase.pdfmetrics import stringWidth
|
||||
from reportlab.pdfgen.canvas import Canvas
|
||||
from reportlab.platypus import PageBreak, Spacer, Table, TableStyle
|
||||
|
||||
@@ -228,20 +226,10 @@ class ReportlabExportMixin:
|
||||
def page_header(self, canvas, doc):
|
||||
from reportlab.lib.units import mm
|
||||
|
||||
font_name = 'OpenSans'
|
||||
font_size = 10
|
||||
left_string = self.get_left_header_string()
|
||||
right_string = self.get_right_header_string()
|
||||
right_width = stringWidth(right_string, font_name, font_size)
|
||||
max_left_width = self.pagesize[0] - doc.leftMargin - doc.rightMargin - right_width - 5 * mm
|
||||
left_string_lines = simpleSplit(left_string, font_name, font_size, max_left_width)
|
||||
if len(left_string_lines) > 1:
|
||||
left_string = left_string_lines[0] + " …"
|
||||
|
||||
canvas.setFont(font_name, font_size)
|
||||
canvas.drawString(doc.leftMargin, self.pagesize[1] - 15 * mm, left_string)
|
||||
canvas.setFont('OpenSans', 10)
|
||||
canvas.drawString(doc.leftMargin, self.pagesize[1] - 15 * mm, self.get_left_header_string())
|
||||
canvas.drawRightString(self.pagesize[0] - doc.rightMargin, self.pagesize[1] - 15 * mm,
|
||||
right_string)
|
||||
self.get_right_header_string())
|
||||
canvas.setStrokeColorRGB(0, 0, 0)
|
||||
canvas.line(doc.leftMargin, self.pagesize[1] - 17 * mm,
|
||||
self.pagesize[0] - doc.rightMargin, self.pagesize[1] - 17 * mm)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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')
|
||||
@@ -355,9 +354,9 @@ class OrderSendView(BaseSenderView):
|
||||
statusq |= Q(status=Order.STATUS_PENDING, require_approval=False, valid_if_pending=True)
|
||||
orders = qs.filter(statusq)
|
||||
|
||||
opq = OrderPosition.objects.with_scopes_disabled().filter(
|
||||
opq = OrderPosition.objects.filter(
|
||||
Q(item_id__in=[i.pk for i in form.cleaned_data.get('items')]) | Q(Exists(
|
||||
OrderPosition.objects.with_scopes_disabled().filter(
|
||||
OrderPosition.objects.filter(
|
||||
addon_to_id=OuterRef('pk'),
|
||||
item_id__in=[i.pk for i in form.cleaned_data.get('items')]
|
||||
)
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
@@ -98,9 +95,7 @@ from pretix.presale.signals import (
|
||||
question_form_fields_overrides,
|
||||
)
|
||||
from pretix.presale.utils import customer_login
|
||||
from pretix.presale.views import (
|
||||
CartMixin, get_cart_is_free, get_cart_positions,
|
||||
)
|
||||
from pretix.presale.views import CartMixin, get_cart, get_cart_is_free
|
||||
from pretix.presale.views.cart import (
|
||||
_items_from_post_data, cart_session, create_empty_cart_id,
|
||||
get_or_create_cart_id,
|
||||
@@ -499,7 +494,7 @@ class AddOnsStep(CartMixin, AsyncAction, TemplateFlowStep):
|
||||
self.request = request
|
||||
|
||||
# check whether addons are applicable
|
||||
if get_cart_positions(request).filter(item__addons__isnull=False).exists():
|
||||
if get_cart(request).filter(item__addons__isnull=False).exists():
|
||||
return True
|
||||
|
||||
# don't re-check whether cross-selling is applicable if we're already past the AddOnsStep
|
||||
@@ -875,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
|
||||
@@ -1092,18 +1065,26 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
return False
|
||||
|
||||
for cp in self._positions_for_questions:
|
||||
qc_cache = {
|
||||
qc.pk: qc for qq in cp.item.relevant_questionnaires for qc in qq.childlist
|
||||
answ = {
|
||||
aw.question_id: aw for aw in cp.answerlist
|
||||
}
|
||||
question_cache = {
|
||||
q.pk: q for q in cp.item.questions_to_ask
|
||||
}
|
||||
|
||||
def question_is_visible(parentid, qvals):
|
||||
if parentid not in qc_cache:
|
||||
if parentid not in question_cache:
|
||||
return False
|
||||
parentqc = qc_cache[parentid]
|
||||
if parentqc.dependency_question_id and not question_is_visible(parentqc.dependency_question_id, parentqc.dependency_values):
|
||||
parentq = question_cache[parentid]
|
||||
if parentq.dependency_question_id and not question_is_visible(parentq.dependency_question_id, parentq.dependency_values):
|
||||
return False
|
||||
answer_values = cp.get_dependency_answer_values(parentqc)
|
||||
return any(qval in answer_values for qval in qvals)
|
||||
if parentid not in answ:
|
||||
return False
|
||||
return (
|
||||
('True' in qvals and answ[parentid].answer == 'True')
|
||||
or ('False' in qvals and answ[parentid].answer == 'False')
|
||||
or (any(qval in [o.identifier for o in answ[parentid].options.all()] for qval in qvals))
|
||||
)
|
||||
|
||||
def question_is_required(q):
|
||||
return (
|
||||
@@ -1112,16 +1093,31 @@ class QuestionsStep(CartQuestionsViewMixin, CartMixin, TemplateFlowStep):
|
||||
)
|
||||
|
||||
if not self.all_optional:
|
||||
for qq in cp.item.relevant_questionnaires:
|
||||
for qc in qq.childlist:
|
||||
if qc.user_datafield_id and question_is_required(qc) and qc.user_datafield_id not in cp.answer_cache:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if qc.system_datafield and question_is_required(qc) and not cp.get_system_answer(qc.system_datafield):
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
for q in cp.item.questions_to_ask:
|
||||
if question_is_required(q) and q.id not in answ:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_names_required', as_type=bool) \
|
||||
and not cp.attendee_name_parts:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_emails_required', as_type=bool) \
|
||||
and cp.attendee_email is None:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_company_required', as_type=bool) \
|
||||
and cp.company is None:
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
if cp.item.ask_attendee_data and self.request.event.settings.get('attendee_addresses_required', as_type=bool) \
|
||||
and (cp.street is None and cp.city is None and cp.country is None):
|
||||
if warn:
|
||||
messages.warning(request, _('Please fill in answers to all required questions.'))
|
||||
return False
|
||||
|
||||
responses = question_form_fields.send(sender=self.request.event, position=cp)
|
||||
form_data = cp.meta_info_data.get('question_form_data', {})
|
||||
@@ -1348,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))
|
||||
@@ -1441,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.'))
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -204,7 +204,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% for q in line.questions %}
|
||||
<dt>{{ q.label }}</dt>
|
||||
<dt>{{ q.question }}</dt>
|
||||
<dd>
|
||||
{% include "pretixpresale/event/fragment_question_answer.html" with request=request question=q answer=q.answer %}
|
||||
</dd>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{% load escapejson %}
|
||||
|
||||
{% if payment_qr_codes %}
|
||||
<div class="tabcontainer col-md-6 col-sm-6 col-xs-12 text-center js-only blank-after">
|
||||
<div class="tabcontainer col-md-6 col-sm-6 hidden-xs text-center js-only blank-after">
|
||||
<div id="banktransfer_qrcodes_tabs_content" class="tabpanels blank-after">
|
||||
{% for code_info in payment_qr_codes %}
|
||||
<div id="banktransfer_qrcodes_{{ code_info.id }}"
|
||||
|
||||
@@ -41,7 +41,7 @@ from itertools import groupby
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib import messages
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Q, Sum
|
||||
from django.db.models import Exists, OuterRef, Prefetch, Sum
|
||||
from django.utils import translation
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.timezone import now
|
||||
@@ -54,7 +54,6 @@ from pretix.base.models import (
|
||||
CartPosition, Customer, InvoiceAddress, ItemAddOn, OrderFee, Question,
|
||||
QuestionAnswer, QuestionOption, TaxRule,
|
||||
)
|
||||
from pretix.base.models.items import QuestionnaireChild
|
||||
from pretix.base.models.orders import CheckoutSession
|
||||
from pretix.base.services.cart import get_fees
|
||||
from pretix.base.services.pricing import apply_rounding
|
||||
@@ -96,7 +95,7 @@ class CartMixin:
|
||||
"""
|
||||
A list of this users cart position
|
||||
"""
|
||||
return list(get_cart_positions(self.request))
|
||||
return list(get_cart(self.request))
|
||||
|
||||
@cached_property
|
||||
def cart_session(self):
|
||||
@@ -400,7 +399,7 @@ def cart_exists(request):
|
||||
return bool(request._cart_cache)
|
||||
|
||||
|
||||
def get_cart_positions(request):
|
||||
def get_cart(request):
|
||||
from pretix.presale.views.cart import get_or_create_cart_id
|
||||
|
||||
if not hasattr(request, '_cart_cache'):
|
||||
@@ -408,11 +407,8 @@ def get_cart_positions(request):
|
||||
if not cart_id:
|
||||
request._cart_cache = CartPosition.objects.none()
|
||||
else:
|
||||
qqs = request.event.questionnaires.all()
|
||||
qqs = qqs.filter(
|
||||
Q(all_sales_channels=True) | Q(limit_sales_channels__identifier=request.sales_channel.identifier),
|
||||
type='PS'
|
||||
)
|
||||
qqs = request.event.questions.all()
|
||||
qqs = qqs.filter(ask_during_checkin=False, hidden=False, container_type=Question.ContainerType.ORDERPOSITION)
|
||||
request._cart_cache = CartPosition.objects.filter(
|
||||
cart_id=cart_id, event=request.event
|
||||
).annotate(
|
||||
@@ -433,23 +429,18 @@ def get_cart_positions(request):
|
||||
Prefetch('answers',
|
||||
QuestionAnswer.objects.prefetch_related('options'),
|
||||
to_attr='answerlist'),
|
||||
Prefetch('item__questionnaires',
|
||||
Prefetch('item__questions',
|
||||
qqs.prefetch_related(
|
||||
Prefetch('children', QuestionnaireChild.objects.prefetch_related(
|
||||
Prefetch('user_datafield', Question.objects.prefetch_related(
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
))
|
||||
),
|
||||
to_attr='childlist')
|
||||
),
|
||||
to_attr='relevant_questionnaires')
|
||||
Prefetch('options', QuestionOption.objects.prefetch_related(Prefetch(
|
||||
# This prefetch statement is utter bullshit, but it actually prevents Django from doing
|
||||
# a lot of queries since ModelChoiceIterator stops trying to be clever once we have
|
||||
# a prefetch lookup on this query...
|
||||
'question',
|
||||
Question.objects.none(),
|
||||
to_attr='dummy'
|
||||
)))
|
||||
).select_related('dependency_question'),
|
||||
to_attr='questions_to_ask')
|
||||
)
|
||||
by_id = {cp.pk: cp for cp in request._cart_cache}
|
||||
for cp in request._cart_cache:
|
||||
@@ -459,8 +450,6 @@ def get_cart_positions(request):
|
||||
cp.addon_to = by_id[cp.addon_to_id]
|
||||
return request._cart_cache
|
||||
|
||||
get_cart = get_cart_positions # legacy compatibility
|
||||
|
||||
|
||||
def get_cart_total(request):
|
||||
"""
|
||||
@@ -512,7 +501,7 @@ def get_cart_is_free(request):
|
||||
|
||||
if not hasattr(request, '_cart_free_cache'):
|
||||
cs = cart_session(request)
|
||||
pos = get_cart_positions(request)
|
||||
pos = get_cart(request)
|
||||
ia = get_cart_invoice_address(request)
|
||||
try:
|
||||
fees = get_fees(event=request.event, request=request, invoice_address=ia,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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__)
|
||||
@@ -122,20 +122,19 @@ def widget_css_etag(request, version, **kwargs):
|
||||
return f'{_get_source_cache_key(version)}-{request.organizer.cache.get_or_set("css_version", default=lambda: int(time.time()))}'
|
||||
|
||||
|
||||
# use vite by default, serve old vue2-based widget only for widget_vue2_origins
|
||||
def _use_vite(request):
|
||||
if getattr(settings, 'PRETIX_WIDGET_VUE', False) or "legacy" in request.GET:
|
||||
return False
|
||||
if getattr(settings, 'PRETIX_WIDGET_VITE', False) or "beta" in request.GET:
|
||||
return True
|
||||
origin = request.META.get('HTTP_ORIGIN', '')
|
||||
gs = GlobalSettingsObject()
|
||||
vue_origins = gs.settings.get('widget_vue2_origins', as_type=str, default='')
|
||||
if vue_origins and not origin:
|
||||
vite_origins = gs.settings.get('widget_vite_origins', as_type=str, default='')
|
||||
if vite_origins and not origin:
|
||||
referer = request.META.get('HTTP_REFERER', '')
|
||||
origin = '/'.join(referer.split('/', 3)[:3])
|
||||
if origin and vue_origins:
|
||||
origins_list = [o.strip() for o in vue_origins.strip().splitlines() if o.strip()]
|
||||
return origin not in origins_list
|
||||
return True
|
||||
if origin and vite_origins:
|
||||
origins_list = [o.strip() for o in vite_origins.strip().splitlines() if o.strip()]
|
||||
return origin in origins_list
|
||||
return False
|
||||
|
||||
|
||||
def widget_js_etag(request, version, lang, **kwargs):
|
||||
@@ -758,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'] = [
|
||||
{
|
||||
|
||||
@@ -913,7 +913,7 @@ VITE_DEV_SERVER_PORT = 5173
|
||||
VITE_DEV_SERVER = f"http://localhost:{VITE_DEV_SERVER_PORT}"
|
||||
VITE_DEV_MODE = DEBUG
|
||||
VITE_IGNORE = False # Used to ignore `collectstatic`/`rebuild`
|
||||
PRETIX_WIDGET_VUE = os.environ.get('PRETIX_WIDGET_VUE', '') not in ('', '0')
|
||||
PRETIX_WIDGET_VITE = os.environ.get('PRETIX_WIDGET_VITE', '') not in ('', '0')
|
||||
|
||||
if DEBUG:
|
||||
# Reload if settings file changes
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]', '@').replaceAll('[dot]', '.')
|
||||
link.textContent = link.textContent.replace('[at]', '@').replaceAll('[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]', '.');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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,146 +0,0 @@
|
||||
<script lang="ts">
|
||||
import QuestionnaireElement from './QuestionnaireElement.vue';
|
||||
import * as api from './api';
|
||||
import { Questionnaire } from './model';
|
||||
import { i18n_any, QUESTION_TYPE, sort, localeComp, numericComp, groupBy, _ } from './helper';
|
||||
import {Ref, ref} from 'vue';
|
||||
import { SlickList, SlickItem } from 'vue-slicksort';
|
||||
|
||||
const items_list = await api.getItems();
|
||||
const categories_list = await api.getCategories();
|
||||
const categories = Object.fromEntries(categories_list.map(cat => [cat.id, cat]));
|
||||
categories['null'] = { position: -1, internal_name: _('Uncategorized') };
|
||||
sort(items_list, numericComp(item => categories[item.category]?.position), numericComp(item => item.position));
|
||||
console.log("items_list", items_list);
|
||||
const grouped_items = [...groupBy(items_list, item => categories[item.category])];
|
||||
console.log("grouped_items", grouped_items);
|
||||
|
||||
const all_questionnaires: (Omit<Questionnaire, 'id'> & { _new_id?: number, id?: number })[] = await api.getQuestionnaires();
|
||||
const order_questionnaires = ref(all_questionnaires.filter(q => q.type.startsWith('O')));
|
||||
const position_questionnaires = ref(all_questionnaires.filter(q => q.type.startsWith('P')));
|
||||
const datafields = ref(await api.getDatafields());
|
||||
|
||||
function saveQuestionnaire(questionnaire) {
|
||||
if (questionnaire.id) {
|
||||
api.updateQuestionnaire(questionnaire.id, questionnaire);
|
||||
} else {
|
||||
api.createQuestionnaire(questionnaire);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
QuestionnaireElement, SlickList, SlickItem,
|
||||
},
|
||||
methods: {
|
||||
i18n_any,
|
||||
addPositionQuestionnaire() {
|
||||
position_questionnaires.value.push({
|
||||
all_sales_channels: false, children: [], limit_sales_channels: [], position: 0,
|
||||
items: [], internal_name: "Unnamed questionnaire", type: "PS",
|
||||
_new_id: Date.now(),
|
||||
});
|
||||
},
|
||||
addOrderQuestionnaire() {
|
||||
order_questionnaires.value.push({
|
||||
all_sales_channels: false, children: [], limit_sales_channels: [], position: 0,
|
||||
items: [], internal_name: "Unnamed questionnaire", type: "OS",
|
||||
_new_id: Date.now(),
|
||||
});
|
||||
},
|
||||
saveData() {
|
||||
for (const questionnaire of order_questionnaires.value) {
|
||||
saveQuestionnaire(questionnaire);
|
||||
}
|
||||
for (const questionnaire of position_questionnaires.value) {
|
||||
saveQuestionnaire(questionnaire);
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
order_questionnaires,
|
||||
position_questionnaires,
|
||||
datafields,
|
||||
items: items_list,
|
||||
selected_product: ref(""),
|
||||
grouped_items,
|
||||
categories,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.hidden-questionnaire { opacity: 0.3; }
|
||||
|
||||
.questionnaires-list { margin-right: 180px; }
|
||||
.question-edit-buttons { float:right; }
|
||||
.question-edit-buttons div { position: absolute; margin-left: 10px; min-width: 100px; }
|
||||
.question-edit-buttons button { }
|
||||
.form-group { margin-bottom: 30px }
|
||||
|
||||
.questionnaires-editor.product-selected .panel .panel-heading {}
|
||||
|
||||
.questionnaires-editor.product-selected .panel { margin-bottom: 0; border: 0 none; border-bottom: 1px solid #ddd; box-shadow: none; border-radius: 0; }
|
||||
.questionnaires-editor.product-selected .panel .panel-heading { font-style: italic; background: white; border: 0 none; }
|
||||
|
||||
.filter-row { background: #f8e6ff; border: 1px solid #e3cbed; padding: 10px; }
|
||||
|
||||
.debuginfo { font-size: 70%; background: rgba(200, 200, 200, 0.5); }
|
||||
.dependency-info { position: absolute; }
|
||||
.dependency-info > span { }
|
||||
|
||||
.category-header { margin: 8px 0 -5px 0; font-weight: bold; color: #737373; }
|
||||
</style>
|
||||
<template>
|
||||
<div class="questionnaires-editor">
|
||||
<p class="filter-row">
|
||||
Order questionnaires
|
||||
</p>
|
||||
<div class="questionnaires-list">
|
||||
<SlickList axis="y" v-model:list="order_questionnaires" useDragHandle appendTo="#orderQuestionnaireListParent" id="orderQuestionnaireListParent">
|
||||
<SlickItem v-for="(questionnaire, index) in order_questionnaires" :key="questionnaire.id || questionnaire._new_id" :index="index">
|
||||
<QuestionnaireElement
|
||||
:questionnaire="questionnaire"
|
||||
:datafields="datafields"
|
||||
:grouped_items="null"
|
||||
:selected_product="null" />
|
||||
</SlickItem>
|
||||
</SlickList>
|
||||
</div>
|
||||
<p>
|
||||
<button class="btn btn-default" @click="addOrderQuestionnaire()"><i class="fa fa-plus"></i> Neuen Fragebogen erstellen</button>
|
||||
<button class="btn btn-default" @click="saveData()"><i class="fa fa-save"></i> Speichern</button>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div :class="`questionnaires-editor ${selected_product ? 'product-selected':''}`">
|
||||
<p class="filter-row">
|
||||
Questionnaires for product:
|
||||
<select v-model="selected_product">
|
||||
<option value="">(all)</option>
|
||||
<optgroup v-for="[category, items] in grouped_items" :label="category.internal_name || i18n_any(category.name)">
|
||||
<option v-for="item in items" :value="item.id">
|
||||
{{ item.internal_name || i18n_any(item.name) }}
|
||||
</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
</p>
|
||||
<div class="questionnaires-list">
|
||||
<SlickList axis="y" v-model:list="position_questionnaires" useDragHandle appendTo="#questionnaireListParent" id="questionnaireListParent">
|
||||
<SlickItem v-for="(questionnaire, index) in position_questionnaires" :key="questionnaire.id || questionnaire._new_id" :index="index">
|
||||
<QuestionnaireElement
|
||||
:questionnaire="questionnaire"
|
||||
:datafields="datafields"
|
||||
:grouped_items="grouped_items"
|
||||
:selected_product="selected_product" />
|
||||
</SlickItem>
|
||||
</SlickList>
|
||||
</div>
|
||||
<p>
|
||||
<button class="btn btn-default" @click="addPositionQuestionnaire()"><i class="fa fa-plus"></i> Neuen Fragebogen erstellen</button>
|
||||
<button class="btn btn-default" @click="saveData()"><i class="fa fa-save"></i> Speichern</button>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user