forked from CGM_Public/pretix_original
Compare commits
21 Commits
v2023.6.2
...
walletdete
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
451ed221d9 | ||
|
|
b9af34f0fd | ||
|
|
fc15811b7f | ||
|
|
fdb2a20514 | ||
|
|
1b1c4358d3 | ||
|
|
3717c4b553 | ||
|
|
609f45d818 | ||
|
|
1d49c98cf2 | ||
|
|
586f42557f | ||
|
|
e3f219366d | ||
|
|
c571b269ff | ||
|
|
6d57501c5c | ||
|
|
5f3e039b2e | ||
|
|
8fa7aeef78 | ||
|
|
3b5baa7701 | ||
|
|
c6bb3e71bf | ||
|
|
104607d34e | ||
|
|
714ef0d3b6 | ||
|
|
db7c52ca93 | ||
|
|
fc94fbd9c8 | ||
|
|
61b3207ea2 |
@@ -32,6 +32,8 @@ as well as the type of underlying hardware. Example:
|
|||||||
"token": "kpp4jn8g2ynzonp6",
|
"token": "kpp4jn8g2ynzonp6",
|
||||||
"hardware_brand": "Samsung",
|
"hardware_brand": "Samsung",
|
||||||
"hardware_model": "Galaxy S",
|
"hardware_model": "Galaxy S",
|
||||||
|
"os_name": "Android",
|
||||||
|
"os_version": "2.3.6",
|
||||||
"software_brand": "pretixdroid",
|
"software_brand": "pretixdroid",
|
||||||
"software_version": "4.0.0"
|
"software_version": "4.0.0"
|
||||||
}
|
}
|
||||||
@@ -98,6 +100,8 @@ following endpoint:
|
|||||||
{
|
{
|
||||||
"hardware_brand": "Samsung",
|
"hardware_brand": "Samsung",
|
||||||
"hardware_model": "Galaxy S",
|
"hardware_model": "Galaxy S",
|
||||||
|
"os_name": "Android",
|
||||||
|
"os_version": "2.3.6",
|
||||||
"software_brand": "pretixdroid",
|
"software_brand": "pretixdroid",
|
||||||
"software_version": "4.1.0",
|
"software_version": "4.1.0",
|
||||||
"info": {"arbitrary": "data"}
|
"info": {"arbitrary": "data"}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ all_events boolean Whether this de
|
|||||||
limit_events list List of event slugs this device has access to
|
limit_events list List of event slugs this device has access to
|
||||||
hardware_brand string Device hardware manufacturer (read-only)
|
hardware_brand string Device hardware manufacturer (read-only)
|
||||||
hardware_model string Device hardware model (read-only)
|
hardware_model string Device hardware model (read-only)
|
||||||
|
os_name string Device operating system name (read-only)
|
||||||
|
os_version string Device operating system version (read-only)
|
||||||
software_brand string Device software product (read-only)
|
software_brand string Device software product (read-only)
|
||||||
software_version string Device software version (read-only)
|
software_version string Device software version (read-only)
|
||||||
created datetime Creation time
|
created datetime Creation time
|
||||||
@@ -76,6 +78,8 @@ Device endpoints
|
|||||||
"security_profile": "full",
|
"security_profile": "full",
|
||||||
"hardware_brand": "Zebra",
|
"hardware_brand": "Zebra",
|
||||||
"hardware_model": "TC25",
|
"hardware_model": "TC25",
|
||||||
|
"os_name": "Android",
|
||||||
|
"os_version": "8.1.0",
|
||||||
"software_brand": "pretixSCAN",
|
"software_brand": "pretixSCAN",
|
||||||
"software_version": "1.5.1"
|
"software_version": "1.5.1"
|
||||||
}
|
}
|
||||||
@@ -123,6 +127,8 @@ Device endpoints
|
|||||||
"security_profile": "full",
|
"security_profile": "full",
|
||||||
"hardware_brand": "Zebra",
|
"hardware_brand": "Zebra",
|
||||||
"hardware_model": "TC25",
|
"hardware_model": "TC25",
|
||||||
|
"os_name": "Android",
|
||||||
|
"os_version": "8.1.0",
|
||||||
"software_brand": "pretixSCAN",
|
"software_brand": "pretixSCAN",
|
||||||
"software_version": "1.5.1"
|
"software_version": "1.5.1"
|
||||||
}
|
}
|
||||||
@@ -173,6 +179,8 @@ Device endpoints
|
|||||||
"initialized": null
|
"initialized": null
|
||||||
"hardware_brand": null,
|
"hardware_brand": null,
|
||||||
"hardware_model": null,
|
"hardware_model": null,
|
||||||
|
"os_name": null,
|
||||||
|
"os_version": null,
|
||||||
"software_brand": null,
|
"software_brand": null,
|
||||||
"software_version": null
|
"software_version": null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ The provider class
|
|||||||
|
|
||||||
.. autoattribute:: settings_form_fields
|
.. autoattribute:: settings_form_fields
|
||||||
|
|
||||||
|
.. autoattribute:: walletqueries
|
||||||
|
|
||||||
.. automethod:: settings_form_clean
|
.. automethod:: settings_form_clean
|
||||||
|
|
||||||
.. automethod:: settings_content_render
|
.. automethod:: settings_content_render
|
||||||
|
|||||||
@@ -19,4 +19,4 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
__version__ = "2023.6.2"
|
__version__ = "2023.7.0.dev0"
|
||||||
|
|||||||
@@ -728,6 +728,7 @@ class EventSettingsSerializer(SettingsSerializer):
|
|||||||
'payment_term_minutes',
|
'payment_term_minutes',
|
||||||
'payment_term_last',
|
'payment_term_last',
|
||||||
'payment_term_expire_automatically',
|
'payment_term_expire_automatically',
|
||||||
|
'payment_term_expire_delay_days',
|
||||||
'payment_term_accept_late',
|
'payment_term_accept_late',
|
||||||
'payment_explanation',
|
'payment_explanation',
|
||||||
'payment_pending_hidden',
|
'payment_pending_hidden',
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
unique_serial = serializers.CharField(read_only=True)
|
unique_serial = serializers.CharField(read_only=True)
|
||||||
hardware_brand = serializers.CharField(read_only=True)
|
hardware_brand = serializers.CharField(read_only=True)
|
||||||
hardware_model = serializers.CharField(read_only=True)
|
hardware_model = serializers.CharField(read_only=True)
|
||||||
|
os_name = serializers.CharField(read_only=True)
|
||||||
|
os_version = serializers.CharField(read_only=True)
|
||||||
software_brand = serializers.CharField(read_only=True)
|
software_brand = serializers.CharField(read_only=True)
|
||||||
software_version = serializers.CharField(read_only=True)
|
software_version = serializers.CharField(read_only=True)
|
||||||
created = serializers.DateTimeField(read_only=True)
|
created = serializers.DateTimeField(read_only=True)
|
||||||
@@ -263,7 +265,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
|||||||
fields = (
|
fields = (
|
||||||
'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events',
|
'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events',
|
||||||
'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model',
|
'revoked', 'name', 'created', 'initialized', 'hardware_brand', 'hardware_model',
|
||||||
'software_brand', 'software_version', 'security_profile'
|
'os_name', 'os_version', 'software_brand', 'software_version', 'security_profile'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ class InitializationRequestSerializer(serializers.Serializer):
|
|||||||
token = serializers.CharField(max_length=190)
|
token = serializers.CharField(max_length=190)
|
||||||
hardware_brand = serializers.CharField(max_length=190)
|
hardware_brand = serializers.CharField(max_length=190)
|
||||||
hardware_model = serializers.CharField(max_length=190)
|
hardware_model = serializers.CharField(max_length=190)
|
||||||
|
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
|
||||||
|
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
|
||||||
software_brand = serializers.CharField(max_length=190)
|
software_brand = serializers.CharField(max_length=190)
|
||||||
software_version = serializers.CharField(max_length=190)
|
software_version = serializers.CharField(max_length=190)
|
||||||
info = serializers.JSONField(required=False, allow_null=True)
|
info = serializers.JSONField(required=False, allow_null=True)
|
||||||
@@ -50,6 +52,8 @@ class InitializationRequestSerializer(serializers.Serializer):
|
|||||||
class UpdateRequestSerializer(serializers.Serializer):
|
class UpdateRequestSerializer(serializers.Serializer):
|
||||||
hardware_brand = serializers.CharField(max_length=190)
|
hardware_brand = serializers.CharField(max_length=190)
|
||||||
hardware_model = serializers.CharField(max_length=190)
|
hardware_model = serializers.CharField(max_length=190)
|
||||||
|
os_name = serializers.CharField(max_length=190, required=False, allow_null=True)
|
||||||
|
os_version = serializers.CharField(max_length=190, required=False, allow_null=True)
|
||||||
software_brand = serializers.CharField(max_length=190)
|
software_brand = serializers.CharField(max_length=190)
|
||||||
software_version = serializers.CharField(max_length=190)
|
software_version = serializers.CharField(max_length=190)
|
||||||
info = serializers.JSONField(required=False, allow_null=True)
|
info = serializers.JSONField(required=False, allow_null=True)
|
||||||
@@ -99,6 +103,8 @@ class InitializeView(APIView):
|
|||||||
device.initialized = now()
|
device.initialized = now()
|
||||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
|
device.os_name = serializer.validated_data.get('os_name')
|
||||||
|
device.os_version = serializer.validated_data.get('os_version')
|
||||||
device.software_brand = serializer.validated_data.get('software_brand')
|
device.software_brand = serializer.validated_data.get('software_brand')
|
||||||
device.software_version = serializer.validated_data.get('software_version')
|
device.software_version = serializer.validated_data.get('software_version')
|
||||||
device.info = serializer.validated_data.get('info')
|
device.info = serializer.validated_data.get('info')
|
||||||
@@ -120,6 +126,8 @@ class UpdateView(APIView):
|
|||||||
device = request.auth
|
device = request.auth
|
||||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||||
device.hardware_model = serializer.validated_data.get('hardware_model')
|
device.hardware_model = serializer.validated_data.get('hardware_model')
|
||||||
|
device.os_name = serializer.validated_data.get('os_name')
|
||||||
|
device.os_version = serializer.validated_data.get('os_version')
|
||||||
device.software_brand = serializer.validated_data.get('software_brand')
|
device.software_brand = serializer.validated_data.get('software_brand')
|
||||||
device.software_version = serializer.validated_data.get('software_version')
|
device.software_version = serializer.validated_data.get('software_version')
|
||||||
device.info = serializer.validated_data.get('info')
|
device.info = serializer.validated_data.get('info')
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ from decimal import Decimal
|
|||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
import django_filters
|
import django_filters
|
||||||
from django.conf import settings
|
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import (
|
from django.db.models import (
|
||||||
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
|
Exists, F, OuterRef, Prefetch, Q, Subquery, prefetch_related_objects,
|
||||||
@@ -1191,7 +1190,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet):
|
|||||||
ftype, ignored = mimetypes.guess_type(image_file.name)
|
ftype, ignored = mimetypes.guess_type(image_file.name)
|
||||||
extension = os.path.basename(image_file.name).split('.')[-1]
|
extension = os.path.basename(image_file.name).split('.')[-1]
|
||||||
else:
|
else:
|
||||||
img = Image.open(image_file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
img = Image.open(image_file)
|
||||||
ftype = Image.MIME[img.format]
|
ftype = Image.MIME[img.format]
|
||||||
extensions = {
|
extensions = {
|
||||||
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'
|
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'
|
||||||
|
|||||||
@@ -500,14 +500,14 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
|||||||
file = BytesIO(data['content'])
|
file = BytesIO(data['content'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
image = Image.open(file)
|
||||||
# verify() must be called immediately after the constructor.
|
# verify() must be called immediately after the constructor.
|
||||||
image.verify()
|
image.verify()
|
||||||
|
|
||||||
# We want to do more than just verify(), so we need to re-open the file
|
# We want to do more than just verify(), so we need to re-open the file
|
||||||
if hasattr(file, 'seek'):
|
if hasattr(file, 'seek'):
|
||||||
file.seek(0)
|
file.seek(0)
|
||||||
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
image = Image.open(file)
|
||||||
|
|
||||||
# load() is a potential DoS vector (see Django bug #18520), so we verify the size first
|
# load() is a potential DoS vector (see Django bug #18520), so we verify the size first
|
||||||
if image.width > 10_000 or image.height > 10_000:
|
if image.width > 10_000 or image.height > 10_000:
|
||||||
@@ -566,7 +566,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
|||||||
return f
|
return f
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
kwargs.setdefault('ext_whitelist', settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE)
|
kwargs.setdefault('ext_whitelist', (".png", ".jpg", ".jpeg", ".jfif", ".tif", ".tiff", ".bmp"))
|
||||||
kwargs.setdefault('max_size', settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
kwargs.setdefault('max_size', settings.FILE_UPLOAD_MAX_SIZE_IMAGE)
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@@ -826,7 +826,11 @@ class BaseQuestionsForm(forms.Form):
|
|||||||
help_text=help_text,
|
help_text=help_text,
|
||||||
initial=initial.file if initial else None,
|
initial=initial.file if initial else None,
|
||||||
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
widget=UploadedFileWidget(position=pos, event=event, answer=initial),
|
||||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_OTHER,
|
ext_whitelist=(
|
||||||
|
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
||||||
|
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
||||||
|
".bmp", ".tif", ".tiff"
|
||||||
|
),
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_OTHER,
|
||||||
)
|
)
|
||||||
elif q.type == Question.TYPE_DATE:
|
elif q.type == Question.TYPE_DATE:
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
|||||||
|
|
||||||
h = {
|
h = {
|
||||||
'default-src': ["{static}"],
|
'default-src': ["{static}"],
|
||||||
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
'script-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com', 'https://pay.google.com'],
|
||||||
'object-src': ["'none'"],
|
'object-src': ["'none'"],
|
||||||
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||||
'style-src': ["{static}", "{media}"],
|
'style-src': ["{static}", "{media}"],
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 4.1.9 on 2023-06-26 10:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('pretixbase', '0242_auto_20230512_1008'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='os_name',
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='device',
|
||||||
|
name='os_version',
|
||||||
|
field=models.CharField(max_length=190, null=True),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -143,6 +143,14 @@ class Device(LoggedModel):
|
|||||||
max_length=190,
|
max_length=190,
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
)
|
)
|
||||||
|
os_name = models.CharField(
|
||||||
|
max_length=190,
|
||||||
|
null=True, blank=True
|
||||||
|
)
|
||||||
|
os_version = models.CharField(
|
||||||
|
max_length=190,
|
||||||
|
null=True, blank=True
|
||||||
|
)
|
||||||
software_brand = models.CharField(
|
software_brand = models.CharField(
|
||||||
max_length=190,
|
max_length=190,
|
||||||
null=True, blank=True
|
null=True, blank=True
|
||||||
|
|||||||
@@ -896,6 +896,28 @@ class Order(LockModel, LoggedModel):
|
|||||||
), tz)
|
), tz)
|
||||||
return term_last
|
return term_last
|
||||||
|
|
||||||
|
@property
|
||||||
|
def payment_term_expire_date(self):
|
||||||
|
delay = self.event.settings.get('payment_term_expire_delay_days', as_type=int)
|
||||||
|
if not delay: # performance saver + backwards compatibility
|
||||||
|
return self.expires
|
||||||
|
|
||||||
|
term_last = self.payment_term_last
|
||||||
|
if term_last and self.expires > term_last: # backwards compatibility
|
||||||
|
return self.expires
|
||||||
|
|
||||||
|
expires = self.expires.date() + timedelta(days=delay)
|
||||||
|
|
||||||
|
tz = ZoneInfo(self.event.settings.timezone)
|
||||||
|
expires = make_aware(datetime.combine(
|
||||||
|
expires,
|
||||||
|
time(hour=23, minute=59, second=59)
|
||||||
|
), tz)
|
||||||
|
if term_last:
|
||||||
|
return min(expires, term_last)
|
||||||
|
else:
|
||||||
|
return expires
|
||||||
|
|
||||||
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
|
def _can_be_paid(self, count_waitinglist=True, ignore_date=False, force=False) -> Union[bool, str]:
|
||||||
error_messages = {
|
error_messages = {
|
||||||
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
'late_lastdate': _("The payment can not be accepted as the last date of payments configured in the "
|
||||||
@@ -1219,7 +1241,7 @@ class QuestionAnswer(models.Model):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_image(self):
|
def is_image(self):
|
||||||
return any(self.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE)
|
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.png', '.gif', '.tiff', '.bmp', '.jpeg'))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file_name(self):
|
def file_name(self):
|
||||||
|
|||||||
@@ -830,13 +830,12 @@ class QuestionColumn(ImportColumn):
|
|||||||
class CustomerColumn(ImportColumn):
|
class CustomerColumn(ImportColumn):
|
||||||
identifier = 'customer'
|
identifier = 'customer'
|
||||||
verbose_name = gettext_lazy('Customer')
|
verbose_name = gettext_lazy('Customer')
|
||||||
default_value = None
|
|
||||||
|
|
||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
if value:
|
if value:
|
||||||
try:
|
try:
|
||||||
value = self.event.organizer.customers.get(
|
value = self.event.organizer.customers.get(
|
||||||
Q(identifier=value) | Q(email=value) | Q(external_identifier=value)
|
Q(identifier=value) | Q(email__iexact=value) | Q(external_identifier=value)
|
||||||
)
|
)
|
||||||
except Customer.MultipleObjectsReturned:
|
except Customer.MultipleObjectsReturned:
|
||||||
value = self.event.organizer.customers.get(
|
value = self.event.organizer.customers.get(
|
||||||
|
|||||||
@@ -78,6 +78,16 @@ from pretix.presale.views.cart import cart_session, get_or_create_cart_id
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class WalletQueries:
|
||||||
|
APPLEPAY = 'applepay'
|
||||||
|
GOOGLEPAY = 'googlepay'
|
||||||
|
|
||||||
|
WALLETS = (
|
||||||
|
(APPLEPAY, pgettext_lazy('payment', 'Apple Pay')),
|
||||||
|
(GOOGLEPAY, pgettext_lazy('payment', 'Google Pay')),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PaymentProviderForm(Form):
|
class PaymentProviderForm(Form):
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
@@ -436,6 +446,19 @@ class BasePaymentProvider:
|
|||||||
d['_restrict_to_sales_channels']._as_type = list
|
d['_restrict_to_sales_channels']._as_type = list
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
@property
|
||||||
|
def walletqueries(self):
|
||||||
|
"""
|
||||||
|
.. warning:: This property is considered **experimental**. It might change or get removed at any time without
|
||||||
|
prior notice.
|
||||||
|
|
||||||
|
A list of wallet payment methods that should be dynamically joined to the public name of the payment method,
|
||||||
|
if they are available to the user.
|
||||||
|
The detection is made on a best effort basis with no guarantees of correctness and actual availability.
|
||||||
|
Wallets that pretix can check for are exposed through ``pretix.base.payment.WalletQueries``.
|
||||||
|
"""
|
||||||
|
return []
|
||||||
|
|
||||||
def settings_form_clean(self, cleaned_data):
|
def settings_form_clean(self, cleaned_data):
|
||||||
"""
|
"""
|
||||||
Overriding this method allows you to inject custom validation into the settings form.
|
Overriding this method allows you to inject custom validation into the settings form.
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ def images_from_questions(sender, *args, **kwargs):
|
|||||||
else:
|
else:
|
||||||
a = op.answers.filter(question_id=question_id).first() or a
|
a = op.answers.filter(question_id=question_id).first() or a
|
||||||
|
|
||||||
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE):
|
if not a or not a.file or not any(a.file.name.lower().endswith(e) for e in (".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff")):
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
if etag:
|
if etag:
|
||||||
@@ -939,7 +939,7 @@ class Renderer:
|
|||||||
|
|
||||||
# reportlab does not support unicode combination characters
|
# reportlab does not support unicode combination characters
|
||||||
# It's important we do this before we use ArabicReshaper
|
# It's important we do this before we use ArabicReshaper
|
||||||
text = unicodedata.normalize("NFKC", text)
|
text = unicodedata.normalize("NFC", text)
|
||||||
|
|
||||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||||
|
|||||||
@@ -1270,12 +1270,12 @@ def expire_orders(sender, **kwargs):
|
|||||||
Exists(
|
Exists(
|
||||||
OrderFee.objects.filter(order_id=OuterRef('pk'), fee_type=OrderFee.FEE_TYPE_CANCELLATION)
|
OrderFee.objects.filter(order_id=OuterRef('pk'), fee_type=OrderFee.FEE_TYPE_CANCELLATION)
|
||||||
)
|
)
|
||||||
).select_related('event').order_by('event_id')
|
).prefetch_related('event').order_by('event_id')
|
||||||
for o in qs:
|
for o in qs:
|
||||||
if o.event_id != event_id:
|
if o.event_id != event_id:
|
||||||
expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool)
|
expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool)
|
||||||
event_id = o.event_id
|
event_id = o.event_id
|
||||||
if expire:
|
if expire and now() >= o.payment_term_expire_date:
|
||||||
mark_order_expired(o)
|
mark_order_expired(o)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -893,6 +893,28 @@ DEFAULTS = {
|
|||||||
"the pool and can be ordered by other people."),
|
"the pool and can be ordered by other people."),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
'payment_term_expire_delay_days': {
|
||||||
|
'default': '0',
|
||||||
|
'type': int,
|
||||||
|
'form_class': forms.IntegerField,
|
||||||
|
'serializer_class': serializers.IntegerField,
|
||||||
|
'form_kwargs': dict(
|
||||||
|
label=_('Expiration delay'),
|
||||||
|
help_text=_("The order will only actually expire this many days after the expiration date communicated "
|
||||||
|
"to the customer. However, this will not delay beyond the \"last date of payments\" "
|
||||||
|
"configured above, which is always enforced. The delay may also end on a weekend regardless "
|
||||||
|
"of the other settings above."),
|
||||||
|
# Every order in between the official expiry date and the delayed expiry date has a performance penalty
|
||||||
|
# for the cron job, so we limit this feature to 30 days to prevent arbitrary numbers of orders needing
|
||||||
|
# to be checked.
|
||||||
|
min_value=0,
|
||||||
|
max_value=30,
|
||||||
|
),
|
||||||
|
'serializer_kwargs': dict(
|
||||||
|
min_value=0,
|
||||||
|
max_value=30,
|
||||||
|
),
|
||||||
|
},
|
||||||
'payment_pending_hidden': {
|
'payment_pending_hidden': {
|
||||||
'default': 'False',
|
'default': 'False',
|
||||||
'type': bool,
|
'type': bool,
|
||||||
@@ -2713,7 +2735,7 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
'form_class': ExtFileField,
|
'form_class': ExtFileField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_('Header image'),
|
label=_('Header image'),
|
||||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
help_text=_('If you provide a logo image, we will by default not show your event name and date '
|
||||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||||
@@ -2756,7 +2778,7 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
'form_class': ExtFileField,
|
'form_class': ExtFileField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_('Header image'),
|
label=_('Header image'),
|
||||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||||
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
'in the page header. By default, we show your logo with a size of up to 1140x120 pixels. You '
|
||||||
@@ -2796,7 +2818,7 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
'form_class': ExtFileField,
|
'form_class': ExtFileField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_('Social media image'),
|
label=_('Social media image'),
|
||||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
help_text=_('This picture will be used as a preview if you post links to your ticket shop on social media. '
|
||||||
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
'Facebook advises to use a picture size of 1200 x 630 pixels, however some platforms like '
|
||||||
@@ -2817,7 +2839,7 @@ Your {organizer} team""")) # noqa: W291
|
|||||||
'form_class': ExtFileField,
|
'form_class': ExtFileField,
|
||||||
'form_kwargs': dict(
|
'form_kwargs': dict(
|
||||||
label=_('Logo image'),
|
label=_('Logo image'),
|
||||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||||
required=False,
|
required=False,
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
help_text=_('We will show your logo with a maximal height and width of 2.5 cm.')
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ from django.utils.http import url_has_allowed_host_and_scheme
|
|||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from markdown import Extension
|
from markdown import Extension
|
||||||
from markdown.inlinepatterns import SubstituteTagInlineProcessor
|
from markdown.inlinepatterns import SubstituteTagInlineProcessor
|
||||||
|
from markdown.postprocessors import Postprocessor
|
||||||
|
from markdown.treeprocessors import UnescapeTreeprocessor
|
||||||
from tlds import tld_set
|
from tlds import tld_set
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
@@ -108,6 +110,8 @@ URL_RE = SimpleLazyObject(lambda: build_url_re(tlds=sorted(tld_set, key=len, rev
|
|||||||
|
|
||||||
EMAIL_RE = SimpleLazyObject(lambda: build_email_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
EMAIL_RE = SimpleLazyObject(lambda: build_email_re(tlds=sorted(tld_set, key=len, reverse=True)))
|
||||||
|
|
||||||
|
DOT_ESCAPE = "|escaped-dot-sGnY9LMK|"
|
||||||
|
|
||||||
|
|
||||||
def safelink_callback(attrs, new=False):
|
def safelink_callback(attrs, new=False):
|
||||||
"""
|
"""
|
||||||
@@ -185,6 +189,109 @@ class EmailNl2BrExtension(Extension):
|
|||||||
md.inlinePatterns.register(br_tag, 'nl', 5)
|
md.inlinePatterns.register(br_tag, 'nl', 5)
|
||||||
|
|
||||||
|
|
||||||
|
class LinkifyPostprocessor(Postprocessor):
|
||||||
|
def __init__(self, linker):
|
||||||
|
self.linker = linker
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def run(self, text):
|
||||||
|
return self.linker.linkify(text)
|
||||||
|
|
||||||
|
|
||||||
|
class CleanPostprocessor(Postprocessor):
|
||||||
|
def __init__(self, tags, attributes, protocols, strip):
|
||||||
|
self.tags = tags
|
||||||
|
self.attributes = attributes
|
||||||
|
self.protocols = protocols
|
||||||
|
self.strip = strip
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def run(self, text):
|
||||||
|
return bleach.clean(
|
||||||
|
text,
|
||||||
|
tags=self.tags,
|
||||||
|
attributes=self.attributes,
|
||||||
|
protocols=self.protocols,
|
||||||
|
strip=self.strip
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUnescapeTreeprocessor(UnescapeTreeprocessor):
|
||||||
|
"""
|
||||||
|
This un-escapes everything except \\.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _unescape(self, m):
|
||||||
|
if m.group(1) == "46": # 46 is the ASCII position of .
|
||||||
|
return DOT_ESCAPE
|
||||||
|
return chr(int(m.group(1)))
|
||||||
|
|
||||||
|
|
||||||
|
class CustomUnescapePostprocessor(Postprocessor):
|
||||||
|
"""
|
||||||
|
Restore escaped .
|
||||||
|
"""
|
||||||
|
|
||||||
|
def run(self, text):
|
||||||
|
return text.replace(DOT_ESCAPE, ".")
|
||||||
|
|
||||||
|
|
||||||
|
class LinkifyAndCleanExtension(Extension):
|
||||||
|
r"""
|
||||||
|
We want to do:
|
||||||
|
|
||||||
|
input --> markdown --> bleach clean --> linkify --> output
|
||||||
|
|
||||||
|
Internally, the markdown library does:
|
||||||
|
|
||||||
|
source --> parse --> (tree|inline)processors --> serializing --> postprocessors
|
||||||
|
|
||||||
|
All escaped characters such as \. will be turned to something like <STX>46<ETX> in the processors
|
||||||
|
step and then will be converted to . back again in the last tree processor, before serialization.
|
||||||
|
Therefore, linkify does not see the escaped character anymore. This is annoying for the one case
|
||||||
|
where you want to type "rich_text.py" and *not* have it turned into a link, since you can't type
|
||||||
|
"rich_text\.py" either.
|
||||||
|
|
||||||
|
A simple solution would be to run linkify before markdown, but that may cause other issues when
|
||||||
|
linkify messes with the markdown syntax and it makes handling our attributes etc. harder.
|
||||||
|
|
||||||
|
So we do a weird hack where we modify the unescape processor to unescape everything EXCEPT for the
|
||||||
|
dot and then unescape that one manually after linkify. However, to make things even harder, the bleach
|
||||||
|
clean step removes any invisible characters, so we need to cheat a bit more.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, linker, tags, attributes, protocols, strip):
|
||||||
|
self.linker = linker
|
||||||
|
self.tags = tags
|
||||||
|
self.attributes = attributes
|
||||||
|
self.protocols = protocols
|
||||||
|
self.strip = strip
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def extendMarkdown(self, md):
|
||||||
|
md.treeprocessors.deregister('unescape')
|
||||||
|
md.treeprocessors.register(
|
||||||
|
CustomUnescapeTreeprocessor(md),
|
||||||
|
'unescape',
|
||||||
|
0
|
||||||
|
)
|
||||||
|
md.postprocessors.register(
|
||||||
|
CleanPostprocessor(self.tags, self.attributes, self.protocols, self.strip),
|
||||||
|
'clean',
|
||||||
|
2
|
||||||
|
)
|
||||||
|
md.postprocessors.register(
|
||||||
|
LinkifyPostprocessor(self.linker),
|
||||||
|
'linkify',
|
||||||
|
1
|
||||||
|
)
|
||||||
|
md.postprocessors.register(
|
||||||
|
CustomUnescapePostprocessor(self.linker),
|
||||||
|
'unescape_dot',
|
||||||
|
0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def markdown_compile_email(source):
|
def markdown_compile_email(source):
|
||||||
linker = bleach.Linker(
|
linker = bleach.Linker(
|
||||||
url_re=URL_RE,
|
url_re=URL_RE,
|
||||||
@@ -192,18 +299,20 @@ def markdown_compile_email(source):
|
|||||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||||
parse_email=True
|
parse_email=True
|
||||||
)
|
)
|
||||||
return linker.linkify(bleach.clean(
|
return markdown.markdown(
|
||||||
markdown.markdown(
|
source,
|
||||||
source,
|
extensions=[
|
||||||
extensions=[
|
'markdown.extensions.sane_lists',
|
||||||
'markdown.extensions.sane_lists',
|
EmailNl2BrExtension(),
|
||||||
EmailNl2BrExtension(),
|
LinkifyAndCleanExtension(
|
||||||
]
|
linker,
|
||||||
),
|
tags=ALLOWED_TAGS,
|
||||||
tags=ALLOWED_TAGS,
|
attributes=ALLOWED_ATTRIBUTES,
|
||||||
attributes=ALLOWED_ATTRIBUTES,
|
protocols=ALLOWED_PROTOCOLS,
|
||||||
protocols=ALLOWED_PROTOCOLS,
|
strip=False,
|
||||||
))
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SnippetExtension(markdown.extensions.Extension):
|
class SnippetExtension(markdown.extensions.Extension):
|
||||||
@@ -213,23 +322,24 @@ class SnippetExtension(markdown.extensions.Extension):
|
|||||||
md.parser.blockprocessors.deregister('quote')
|
md.parser.blockprocessors.deregister('quote')
|
||||||
|
|
||||||
|
|
||||||
def markdown_compile(source, snippet=False):
|
def markdown_compile(source, linker, snippet=False):
|
||||||
tags = ALLOWED_TAGS_SNIPPET if snippet else ALLOWED_TAGS
|
tags = ALLOWED_TAGS_SNIPPET if snippet else ALLOWED_TAGS
|
||||||
exts = [
|
exts = [
|
||||||
'markdown.extensions.sane_lists',
|
'markdown.extensions.sane_lists',
|
||||||
'markdown.extensions.nl2br'
|
'markdown.extensions.nl2br',
|
||||||
|
LinkifyAndCleanExtension(
|
||||||
|
linker,
|
||||||
|
tags=tags,
|
||||||
|
attributes=ALLOWED_ATTRIBUTES,
|
||||||
|
protocols=ALLOWED_PROTOCOLS,
|
||||||
|
strip=snippet,
|
||||||
|
)
|
||||||
]
|
]
|
||||||
if snippet:
|
if snippet:
|
||||||
exts.append(SnippetExtension())
|
exts.append(SnippetExtension())
|
||||||
return bleach.clean(
|
return markdown.markdown(
|
||||||
markdown.markdown(
|
source,
|
||||||
source,
|
extensions=exts
|
||||||
extensions=exts
|
|
||||||
),
|
|
||||||
strip=snippet,
|
|
||||||
tags=tags,
|
|
||||||
attributes=ALLOWED_ATTRIBUTES,
|
|
||||||
protocols=ALLOWED_PROTOCOLS,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -245,7 +355,7 @@ def rich_text(text: str, **kwargs):
|
|||||||
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
||||||
parse_email=True
|
parse_email=True
|
||||||
)
|
)
|
||||||
body_md = linker.linkify(markdown_compile(text))
|
body_md = markdown_compile(text, linker)
|
||||||
return mark_safe(body_md)
|
return mark_safe(body_md)
|
||||||
|
|
||||||
|
|
||||||
@@ -261,5 +371,5 @@ def rich_text_snippet(text: str, **kwargs):
|
|||||||
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
callbacks=DEFAULT_CALLBACKS + ([truelink_callback, safelink_callback] if kwargs.get('safelinks', True) else [truelink_callback, abslink_callback]),
|
||||||
parse_email=True
|
parse_email=True
|
||||||
)
|
)
|
||||||
body_md = linker.linkify(markdown_compile(text, snippet=True))
|
body_md = markdown_compile(text, linker, snippet=True)
|
||||||
return mark_safe(body_md)
|
return mark_safe(body_md)
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def is_img(self):
|
def is_img(self):
|
||||||
return any(self.file.name.lower().endswith(e) for e in settings.FILE_UPLOAD_EXTENSIONS_IMAGE)
|
return any(self.file.name.lower().endswith(e) for e in ('.jpg', '.jpeg', '.png', '.gif'))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if hasattr(self.file, 'display_name'):
|
if hasattr(self.file, 'display_name'):
|
||||||
|
|||||||
@@ -749,6 +749,7 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
|||||||
'payment_term_minutes',
|
'payment_term_minutes',
|
||||||
'payment_term_last',
|
'payment_term_last',
|
||||||
'payment_term_expire_automatically',
|
'payment_term_expire_automatically',
|
||||||
|
'payment_term_expire_delay_days',
|
||||||
'payment_term_accept_late',
|
'payment_term_accept_late',
|
||||||
'payment_pending_hidden',
|
'payment_pending_hidden',
|
||||||
'payment_explanation',
|
'payment_explanation',
|
||||||
|
|||||||
@@ -416,7 +416,7 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
|
|
||||||
organizer_logo_image = ExtFileField(
|
organizer_logo_image = ExtFileField(
|
||||||
label=_('Header image'),
|
label=_('Header image'),
|
||||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||||
required=False,
|
required=False,
|
||||||
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
help_text=_('If you provide a logo image, we will by default not show your organization name '
|
||||||
@@ -426,7 +426,7 @@ class OrganizerSettingsForm(SettingsForm):
|
|||||||
)
|
)
|
||||||
favicon = ExtFileField(
|
favicon = ExtFileField(
|
||||||
label=_('Favicon'),
|
label=_('Favicon'),
|
||||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
|
ext_whitelist=(".ico", ".png", ".jpg", ".gif", ".jpeg"),
|
||||||
required=False,
|
required=False,
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_FAVICON,
|
||||||
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
help_text=_('If you provide a favicon, we will show it instead of the default pretix icon. '
|
||||||
|
|||||||
@@ -65,6 +65,8 @@
|
|||||||
{% bootstrap_field form.payment_term_minutes layout="control" %}
|
{% bootstrap_field form.payment_term_minutes layout="control" %}
|
||||||
{% bootstrap_field form.payment_term_last layout="control" %}
|
{% bootstrap_field form.payment_term_last layout="control" %}
|
||||||
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
|
{% bootstrap_field form.payment_term_expire_automatically layout="control" %}
|
||||||
|
{% trans "days" context "unit" as days %}
|
||||||
|
{% bootstrap_field form.payment_term_expire_delay_days layout="control" addon_after=days %}
|
||||||
{% bootstrap_field form.payment_term_accept_late layout="control" %}
|
{% bootstrap_field form.payment_term_accept_late layout="control" %}
|
||||||
{% bootstrap_field form.payment_pending_hidden layout="control" %}
|
{% bootstrap_field form.payment_pending_hidden layout="control" %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|||||||
@@ -1918,7 +1918,7 @@ class OrderContactChange(OrderView):
|
|||||||
'pretix.event.order.contact.changed',
|
'pretix.event.order.contact.changed',
|
||||||
data={
|
data={
|
||||||
'old_email': old_email,
|
'old_email': old_email,
|
||||||
'new_email': self.form.cleaned_data['email'],
|
'new_email': self.form.cleaned_data.get('email'),
|
||||||
},
|
},
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
)
|
)
|
||||||
@@ -1930,7 +1930,7 @@ class OrderContactChange(OrderView):
|
|||||||
'pretix.event.order.phone.changed',
|
'pretix.event.order.phone.changed',
|
||||||
data={
|
data={
|
||||||
'old_phone': old_phone,
|
'old_phone': old_phone,
|
||||||
'new_phone': self.form.cleaned_data['phone'],
|
'new_phone': self.form.cleaned_data.get('phone'),
|
||||||
},
|
},
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@
|
|||||||
import logging
|
import logging
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from PIL.Image import MAX_IMAGE_PIXELS, DecompressionBombError
|
from PIL.Image import MAX_IMAGE_PIXELS, DecompressionBombError
|
||||||
@@ -52,7 +51,7 @@ def validate_uploaded_file_for_valid_image(f):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
image = Image.open(file)
|
||||||
# verify() must be called immediately after the constructor.
|
# verify() must be called immediately after the constructor.
|
||||||
image.verify()
|
image.verify()
|
||||||
except DecompressionBombError:
|
except DecompressionBombError:
|
||||||
|
|||||||
@@ -21,8 +21,6 @@
|
|||||||
#
|
#
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_vobject_performance():
|
def monkeypatch_vobject_performance():
|
||||||
"""
|
"""
|
||||||
@@ -54,19 +52,5 @@ def monkeypatch_vobject_performance():
|
|||||||
icalendar.tzinfo_eq = new_tzinfo_eq
|
icalendar.tzinfo_eq = new_tzinfo_eq
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_pillow_safer():
|
|
||||||
"""
|
|
||||||
Pillow supports many file formats, among them EPS. For EPS, Pillow loads GhostScript whenever GhostScript
|
|
||||||
is installed (cannot officially be disabled). However, GhostScript is known for regular security vulnerabilities.
|
|
||||||
We have no use of reading EPS files and usually prevent this by using `Image.open(…, formats=[…])` to disable EPS
|
|
||||||
support explicitly. However, we are worried about our dependencies like reportlab using `Image.open` without the
|
|
||||||
`formats=` parameter. Therefore, as a defense in depth approach, we monkeypatch EPS support away by modifying the
|
|
||||||
internal image format registry of Pillow.
|
|
||||||
"""
|
|
||||||
if "EPS" in Image.ID:
|
|
||||||
Image.ID.remove("EPS")
|
|
||||||
|
|
||||||
|
|
||||||
def monkeypatch_all_at_ready():
|
def monkeypatch_all_at_ready():
|
||||||
monkeypatch_vobject_performance()
|
monkeypatch_vobject_performance()
|
||||||
monkeypatch_pillow_safer()
|
|
||||||
|
|||||||
@@ -20,9 +20,8 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
from arabic_reshaper import ArabicReshaper
|
from arabic_reshaper import ArabicReshaper
|
||||||
from django.conf import settings
|
|
||||||
from django.utils.functional import SimpleLazyObject
|
from django.utils.functional import SimpleLazyObject
|
||||||
from PIL import Image
|
from PIL.Image import Resampling
|
||||||
from reportlab.lib.utils import ImageReader
|
from reportlab.lib.utils import ImageReader
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +33,7 @@ class ThumbnailingImageReader(ImageReader):
|
|||||||
height = width * self._image.size[1] / self._image.size[0]
|
height = width * self._image.size[1] / self._image.size[0]
|
||||||
self._image.thumbnail(
|
self._image.thumbnail(
|
||||||
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
||||||
resample=Image.Resampling.BICUBIC
|
resample=Resampling.BICUBIC
|
||||||
)
|
)
|
||||||
self._data = None
|
self._data = None
|
||||||
return width, height
|
return width, height
|
||||||
@@ -45,9 +44,6 @@ class ThumbnailingImageReader(ImageReader):
|
|||||||
# (smaller) size of the modified image.
|
# (smaller) size of the modified image.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _read_image(self, fp):
|
|
||||||
return Image.open(fp, formats=settings.PILLOW_FORMATS_IMAGE)
|
|
||||||
|
|
||||||
|
|
||||||
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
||||||
'delete_harakat': True,
|
'delete_harakat': True,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import hashlib
|
|||||||
import math
|
import math
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.conf import settings
|
|
||||||
from django.core.files.base import ContentFile
|
from django.core.files.base import ContentFile
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from PIL import Image, ImageOps, ImageSequence
|
from PIL import Image, ImageOps, ImageSequence
|
||||||
@@ -166,7 +165,7 @@ def resize_image(image, size):
|
|||||||
|
|
||||||
def create_thumbnail(sourcename, size):
|
def create_thumbnail(sourcename, size):
|
||||||
source = default_storage.open(sourcename)
|
source = default_storage.open(sourcename)
|
||||||
image = Image.open(BytesIO(source.read()), formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
image = Image.open(BytesIO(source.read()))
|
||||||
try:
|
try:
|
||||||
image.load()
|
image.load()
|
||||||
except:
|
except:
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ msgstr ""
|
|||||||
"Project-Id-Version: 1\n"
|
"Project-Id-Version: 1\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||||
"PO-Revision-Date: 2023-06-27 14:53+0000\n"
|
"PO-Revision-Date: 2023-06-29 05:00+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Moritz Lerch <dev@moritz-lerch.de>\n"
|
||||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
|
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
|
||||||
">\n"
|
">\n"
|
||||||
"Language: de\n"
|
"Language: de\n"
|
||||||
@@ -16352,7 +16352,7 @@ msgstr "Terminal-ID"
|
|||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:96
|
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:96
|
||||||
msgid "Card holder"
|
msgid "Card holder"
|
||||||
msgstr "Karteninhaber"
|
msgstr "Karteninhaber*in"
|
||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:100
|
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:100
|
||||||
msgid "Card expiration"
|
msgid "Card expiration"
|
||||||
@@ -21206,7 +21206,7 @@ msgstr "Import durchführen"
|
|||||||
#: pretix/control/templates/pretixcontrol/orders/import_start.html:10
|
#: pretix/control/templates/pretixcontrol/orders/import_start.html:10
|
||||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:16
|
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:16
|
||||||
msgid "Upload a new file"
|
msgid "Upload a new file"
|
||||||
msgstr "Neuen Datei hochladen"
|
msgstr "Neue Datei hochladen"
|
||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/orders/import_start.html:16
|
#: pretix/control/templates/pretixcontrol/orders/import_start.html:16
|
||||||
msgid ""
|
msgid ""
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ msgstr ""
|
|||||||
"Project-Id-Version: 1\n"
|
"Project-Id-Version: 1\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||||
"PO-Revision-Date: 2023-06-27 14:53+0000\n"
|
"PO-Revision-Date: 2023-06-29 05:00+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Moritz Lerch <dev@moritz-lerch.de>\n"
|
||||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||||
"pretix/pretix/de_Informal/>\n"
|
"pretix/pretix/de_Informal/>\n"
|
||||||
"Language: de_Informal\n"
|
"Language: de_Informal\n"
|
||||||
@@ -16324,7 +16324,7 @@ msgstr "Terminal-ID"
|
|||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:96
|
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:96
|
||||||
msgid "Card holder"
|
msgid "Card holder"
|
||||||
msgstr "Karteninhaber"
|
msgstr "Karteninhaber*in"
|
||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:100
|
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:100
|
||||||
msgid "Card expiration"
|
msgid "Card expiration"
|
||||||
@@ -21172,7 +21172,7 @@ msgstr "Import durchführen"
|
|||||||
#: pretix/control/templates/pretixcontrol/orders/import_start.html:10
|
#: pretix/control/templates/pretixcontrol/orders/import_start.html:10
|
||||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:16
|
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:16
|
||||||
msgid "Upload a new file"
|
msgid "Upload a new file"
|
||||||
msgstr "Neuen Datei hochladen"
|
msgstr "Neue Datei hochladen"
|
||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/orders/import_start.html:16
|
#: pretix/control/templates/pretixcontrol/orders/import_start.html:16
|
||||||
msgid ""
|
msgid ""
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ msgstr ""
|
|||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||||
"PO-Revision-Date: 2023-06-27 07:40+0000\n"
|
"PO-Revision-Date: 2023-06-28 06:00+0000\n"
|
||||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
"Last-Translator: Yucheng Lin <yuchenglinedu@gmail.com>\n"
|
||||||
"Language-Team: Chinese (Traditional) <https://translate.pretix.eu/projects/"
|
"Language-Team: Chinese (Traditional) <https://translate.pretix.eu/projects/"
|
||||||
"pretix/pretix/zh_Hant/>\n"
|
"pretix/pretix/zh_Hant/>\n"
|
||||||
"Language: zh_Hant\n"
|
"Language: zh_Hant\n"
|
||||||
@@ -522,24 +522,20 @@ msgid "Test-Mode of shop has been deactivated"
|
|||||||
msgstr "商店的測試模式已啟動"
|
msgstr "商店的測試模式已啟動"
|
||||||
|
|
||||||
#: pretix/api/webhooks.py:339
|
#: pretix/api/webhooks.py:339
|
||||||
#, fuzzy
|
|
||||||
msgid "Waiting list entry added"
|
msgid "Waiting list entry added"
|
||||||
msgstr "候補名單條目"
|
msgstr "候補名單條目已添加"
|
||||||
|
|
||||||
#: pretix/api/webhooks.py:343
|
#: pretix/api/webhooks.py:343
|
||||||
#, fuzzy
|
|
||||||
msgid "Waiting list entry changed"
|
msgid "Waiting list entry changed"
|
||||||
msgstr "候補名單條目"
|
msgstr "候補名單條目已更改"
|
||||||
|
|
||||||
#: pretix/api/webhooks.py:347
|
#: pretix/api/webhooks.py:347
|
||||||
#, fuzzy
|
|
||||||
msgid "Waiting list entry deleted"
|
msgid "Waiting list entry deleted"
|
||||||
msgstr "候補名單條目"
|
msgstr "候補名單條目已刪除"
|
||||||
|
|
||||||
#: pretix/api/webhooks.py:351
|
#: pretix/api/webhooks.py:351
|
||||||
#, fuzzy
|
|
||||||
msgid "Waiting list entry received voucher"
|
msgid "Waiting list entry received voucher"
|
||||||
msgstr "候補名單條目"
|
msgstr "候補名單條目收到憑證"
|
||||||
|
|
||||||
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
|
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
|
||||||
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:938
|
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:938
|
||||||
@@ -5347,9 +5343,8 @@ msgid "Seat {number}"
|
|||||||
msgstr "座位 {number}"
|
msgstr "座位 {number}"
|
||||||
|
|
||||||
#: pretix/base/models/tax.py:157
|
#: pretix/base/models/tax.py:157
|
||||||
#, fuzzy
|
|
||||||
msgid "Your set of rules is not valid. Error message: {}"
|
msgid "Your set of rules is not valid. Error message: {}"
|
||||||
msgstr "你的樣式檔案並非有效樣式。錯誤訊息:{}"
|
msgstr "你的條例規則並非有效。錯誤訊息:{}"
|
||||||
|
|
||||||
#: pretix/base/models/tax.py:168 pretix/base/models/tax.py:146
|
#: pretix/base/models/tax.py:168 pretix/base/models/tax.py:146
|
||||||
msgid "Official name"
|
msgid "Official name"
|
||||||
@@ -7642,9 +7637,8 @@ msgid "Something happened in your event after the export, please try again."
|
|||||||
msgstr "匯出後你的活動中發生一些事情,請重試。"
|
msgstr "匯出後你的活動中發生一些事情,請重試。"
|
||||||
|
|
||||||
#: pretix/base/services/shredder.py:177
|
#: pretix/base/services/shredder.py:177
|
||||||
#, fuzzy
|
|
||||||
msgid "Data shredding completed"
|
msgid "Data shredding completed"
|
||||||
msgstr "完成抓取。"
|
msgstr "數據粉碎完成"
|
||||||
|
|
||||||
#: pretix/base/services/stats.py:210
|
#: pretix/base/services/stats.py:210
|
||||||
msgid "Uncategorized"
|
msgid "Uncategorized"
|
||||||
@@ -10720,6 +10714,20 @@ msgid ""
|
|||||||
"\n"
|
"\n"
|
||||||
"Your pretix team\n"
|
"Your pretix team\n"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
"你好\n"
|
||||||
|
"\n"
|
||||||
|
"我們特此確認以下數據粉碎工作已完成:\n"
|
||||||
|
"\n"
|
||||||
|
"召集人:%(organizer)s\n"
|
||||||
|
"\n"
|
||||||
|
"事件: %(event)s\n"
|
||||||
|
"\n"
|
||||||
|
"數據選擇: %(shredders)s\n"
|
||||||
|
"開始時間:%(start_time)s(在此時間之後添加的新資料可能尚未刪除)\n"
|
||||||
|
"\n"
|
||||||
|
"敬上\n"
|
||||||
|
"\n"
|
||||||
|
"你的卓越團隊\n"
|
||||||
|
|
||||||
#: pretix/base/templates/pretixbase/forms/widgets/portrait_image.html:10
|
#: pretix/base/templates/pretixbase/forms/widgets/portrait_image.html:10
|
||||||
msgid "Upload photo"
|
msgid "Upload photo"
|
||||||
@@ -13913,11 +13921,11 @@ msgstr "活動已被刪除。"
|
|||||||
|
|
||||||
#: pretix/control/logdisplay.py:375
|
#: pretix/control/logdisplay.py:375
|
||||||
msgid "A removal process for personal data has been started."
|
msgid "A removal process for personal data has been started."
|
||||||
msgstr ""
|
msgstr "個人數據的刪除過程已啟動。"
|
||||||
|
|
||||||
#: pretix/control/logdisplay.py:376
|
#: pretix/control/logdisplay.py:376
|
||||||
msgid "A removal process for personal data has been completed."
|
msgid "A removal process for personal data has been completed."
|
||||||
msgstr ""
|
msgstr "個人數據的刪除過程已完成。"
|
||||||
|
|
||||||
#: pretix/control/logdisplay.py:377 pretix/control/logdisplay.py:375
|
#: pretix/control/logdisplay.py:377 pretix/control/logdisplay.py:375
|
||||||
msgid "The order details have been changed."
|
msgid "The order details have been changed."
|
||||||
@@ -21588,7 +21596,8 @@ msgstr "確認碼"
|
|||||||
msgid ""
|
msgid ""
|
||||||
"Depending on the amount of data in your event, the following step may take a "
|
"Depending on the amount of data in your event, the following step may take a "
|
||||||
"while to complete. We will inform you via email once it has been completed."
|
"while to complete. We will inform you via email once it has been completed."
|
||||||
msgstr ""
|
msgstr "根據事件中的數據量,以下步驟可能需要一段時間才能完成。完成後,我們將通過電子"
|
||||||
|
"郵件通知你。"
|
||||||
|
|
||||||
#: pretix/control/templates/pretixcontrol/shredder/index.html:11
|
#: pretix/control/templates/pretixcontrol/shredder/index.html:11
|
||||||
msgid ""
|
msgid ""
|
||||||
|
|||||||
@@ -76,7 +76,11 @@ class BaseMailForm(FormPlaceholderMixin, forms.Form):
|
|||||||
attachment = CachedFileField(
|
attachment = CachedFileField(
|
||||||
label=_("Attachment"),
|
label=_("Attachment"),
|
||||||
required=False,
|
required=False,
|
||||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT,
|
ext_whitelist=(
|
||||||
|
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
||||||
|
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
||||||
|
".bmp", ".tif", ".tiff"
|
||||||
|
),
|
||||||
help_text=_('Sending an attachment increases the chance of your email not arriving or being sorted into spam folders. We recommend only using PDFs '
|
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.'),
|
'of no more than 2 MB in size.'),
|
||||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT
|
max_size=settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT
|
||||||
|
|||||||
@@ -59,7 +59,9 @@ from pretix import __version__
|
|||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
from pretix.base.forms import SecretKeySettingsField
|
from pretix.base.forms import SecretKeySettingsField
|
||||||
from pretix.base.models import Event, OrderPayment, OrderRefund, Quota
|
from pretix.base.models import Event, OrderPayment, OrderRefund, Quota
|
||||||
from pretix.base.payment import BasePaymentProvider, PaymentException
|
from pretix.base.payment import (
|
||||||
|
BasePaymentProvider, PaymentException, WalletQueries,
|
||||||
|
)
|
||||||
from pretix.base.plugins import get_all_plugins
|
from pretix.base.plugins import get_all_plugins
|
||||||
from pretix.base.services.mail import SendMailException
|
from pretix.base.services.mail import SendMailException
|
||||||
from pretix.base.settings import SettingsSandbox
|
from pretix.base.settings import SettingsSandbox
|
||||||
@@ -219,6 +221,20 @@ class StripeSettingsHolder(BasePaymentProvider):
|
|||||||
]
|
]
|
||||||
|
|
||||||
extra_fields = [
|
extra_fields = [
|
||||||
|
('walletdetection',
|
||||||
|
forms.BooleanField(
|
||||||
|
label=mark_safe(
|
||||||
|
_('Check for Apple Pay/Google Pay') +
|
||||||
|
' ' +
|
||||||
|
'<span class="label label-info">{}</span>'.format(_('experimental'))
|
||||||
|
),
|
||||||
|
help_text=_("pretix will attempt to check if the customer's webbrowser supports wallet-based payment "
|
||||||
|
"methods like Apple Pay or Google Pay and display them prominently with the credit card"
|
||||||
|
"payment method. This detection does not take into consideration if Google Pay/Apple Pay "
|
||||||
|
"has been disabled in the Stripe Dashboard."),
|
||||||
|
initial=True,
|
||||||
|
required=False,
|
||||||
|
)),
|
||||||
('postfix',
|
('postfix',
|
||||||
forms.CharField(
|
forms.CharField(
|
||||||
label=_('Statement descriptor postfix'),
|
label=_('Statement descriptor postfix'),
|
||||||
@@ -747,6 +763,15 @@ class StripeCC(StripeMethod):
|
|||||||
public_name = _('Credit card')
|
public_name = _('Credit card')
|
||||||
method = 'cc'
|
method = 'cc'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def walletqueries(self):
|
||||||
|
# ToDo: Check against Stripe API, if ApplePay and GooglePay are even activated/available
|
||||||
|
# This is probably only really feasable once the Payment Methods Configuration API is out of beta
|
||||||
|
# https://stripe.com/docs/connect/payment-method-configurations
|
||||||
|
if self.settings.get("walletdetection", True, as_type=bool):
|
||||||
|
return [WalletQueries.APPLEPAY, WalletQueries.GOOGLEPAY]
|
||||||
|
return []
|
||||||
|
|
||||||
def payment_form_render(self, request, total) -> str:
|
def payment_form_render(self, request, total) -> str:
|
||||||
account = get_stripe_account_key(self)
|
account = get_stripe_account_key(self)
|
||||||
if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists():
|
if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists():
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
{% load money %}
|
{% load money %}
|
||||||
{% load bootstrap3 %}
|
{% load bootstrap3 %}
|
||||||
{% load rich_text %}
|
{% load rich_text %}
|
||||||
|
{% block custom_header %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% include "pretixpresale/event/fragment_walletdetection_head.html" %}
|
||||||
|
{% endblock %}
|
||||||
{% block inner %}
|
{% block inner %}
|
||||||
{% if current_payments %}
|
{% if current_payments %}
|
||||||
<p>{% trans "You already selected the following payment methods:" %}</p>
|
<p>{% trans "You already selected the following payment methods:" %}</p>
|
||||||
@@ -71,7 +75,8 @@
|
|||||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||||
id="input_payment_{{ p.provider.identifier }}"
|
id="input_payment_{{ p.provider.identifier }}"
|
||||||
aria-describedby="payment_{{ p.provider.identifier }}"
|
aria-describedby="payment_{{ p.provider.identifier }}"
|
||||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"/>
|
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"
|
||||||
|
data-wallets="{{ p.provider.walletqueries|join:"|" }}" />
|
||||||
<label for="input_payment_{{ p.provider.identifier }}"><strong>{{ p.provider.public_name }}</strong></label>
|
<label for="input_payment_{{ p.provider.identifier }}"><strong>{{ p.provider.public_name }}</strong></label>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{% load static %}
|
||||||
|
{% load compress %}
|
||||||
|
|
||||||
|
{% compress js %}
|
||||||
|
<script type="text/javascript" src="{% static "pretixpresale/js/walletdetection.js" %}"></script>
|
||||||
|
{% endcompress %}
|
||||||
@@ -3,6 +3,10 @@
|
|||||||
{% load eventurl %}
|
{% load eventurl %}
|
||||||
{% load money %}
|
{% load money %}
|
||||||
{% block title %}{% trans "Change payment method" %}{% endblock %}
|
{% block title %}{% trans "Change payment method" %}{% endblock %}
|
||||||
|
{% block custom_header %}
|
||||||
|
{{ block.super }}
|
||||||
|
{% include "pretixpresale/event/fragment_walletdetection_head.html" %}
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>
|
<h2>
|
||||||
{% blocktrans trimmed with code=order.code %}
|
{% blocktrans trimmed with code=order.code %}
|
||||||
@@ -29,7 +33,8 @@
|
|||||||
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
||||||
data-parent="#payment_accordion"
|
data-parent="#payment_accordion"
|
||||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||||
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}" />
|
data-toggle="radiocollapse" data-target="#payment_{{ p.provider.identifier }}"
|
||||||
|
data-wallets="{{ p.provider.walletqueries|join:"|" }}"/>
|
||||||
<strong>{{ p.provider.public_name }}</strong>
|
<strong>{{ p.provider.public_name }}</strong>
|
||||||
</label>
|
</label>
|
||||||
</h4>
|
</h4>
|
||||||
|
|||||||
@@ -350,7 +350,9 @@ def _detect_event(request, require_live=True, require_plugin=None):
|
|||||||
)
|
)
|
||||||
pathparts = request.get_full_path().split('/')
|
pathparts = request.get_full_path().split('/')
|
||||||
pathparts[1] = event.slug
|
pathparts[1] = event.slug
|
||||||
return redirect('/'.join(pathparts))
|
r = redirect('/'.join(pathparts))
|
||||||
|
r['Access-Control-Allow-Origin'] = '*'
|
||||||
|
return r
|
||||||
else:
|
else:
|
||||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||||
event = Event.objects.select_related('organizer').get(
|
event = Event.objects.select_related('organizer').get(
|
||||||
@@ -360,7 +362,9 @@ def _detect_event(request, require_live=True, require_plugin=None):
|
|||||||
pathparts = request.get_full_path().split('/')
|
pathparts = request.get_full_path().split('/')
|
||||||
pathparts[1] = event.organizer.slug
|
pathparts[1] = event.organizer.slug
|
||||||
pathparts[2] = event.slug
|
pathparts[2] = event.slug
|
||||||
return redirect('/'.join(pathparts))
|
r = redirect('/'.join(pathparts))
|
||||||
|
r['Access-Control-Allow-Origin'] = '*'
|
||||||
|
return r
|
||||||
except Event.DoesNotExist:
|
except Event.DoesNotExist:
|
||||||
raise Http404(_('The selected event was not found.'))
|
raise Http404(_('The selected event was not found.'))
|
||||||
raise Http404(_('The selected event was not found.'))
|
raise Http404(_('The selected event was not found.'))
|
||||||
@@ -374,7 +378,9 @@ def _detect_event(request, require_live=True, require_plugin=None):
|
|||||||
raise Http404(_('The selected organizer was not found.'))
|
raise Http404(_('The selected organizer was not found.'))
|
||||||
pathparts = request.get_full_path().split('/')
|
pathparts = request.get_full_path().split('/')
|
||||||
pathparts[1] = organizer.slug
|
pathparts[1] = organizer.slug
|
||||||
return redirect('/'.join(pathparts))
|
r = redirect('/'.join(pathparts))
|
||||||
|
r['Access-Control-Allow-Origin'] = '*'
|
||||||
|
return r
|
||||||
raise Http404(_('The selected organizer was not found.'))
|
raise Http404(_('The selected organizer was not found.'))
|
||||||
|
|
||||||
request._event_detected = True
|
request._event_detected = True
|
||||||
|
|||||||
@@ -212,11 +212,14 @@ def price_dict(item, price):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def get_picture(event, picture):
|
def get_picture(event, picture, size=None):
|
||||||
try:
|
thumb = None
|
||||||
thumb = get_thumbnail(picture.name, '60x60^').thumb.url
|
if size:
|
||||||
except:
|
try:
|
||||||
logger.exception(f'Failed to create thumbnail of {picture.name}')
|
thumb = get_thumbnail(picture.name, size).thumb.url
|
||||||
|
except:
|
||||||
|
logger.exception(f'Failed to create thumbnail of {picture.name}')
|
||||||
|
if not thumb:
|
||||||
thumb = default_storage.url(picture.name)
|
thumb = default_storage.url(picture.name)
|
||||||
return urljoin(build_absolute_uri(event, 'presale:event.index'), thumb)
|
return urljoin(build_absolute_uri(event, 'presale:event.index'), thumb)
|
||||||
|
|
||||||
@@ -264,7 +267,8 @@ class WidgetAPIProductList(EventListMixin, View):
|
|||||||
{
|
{
|
||||||
'id': item.pk,
|
'id': item.pk,
|
||||||
'name': str(item.name),
|
'name': str(item.name),
|
||||||
'picture': get_picture(self.request.event, item.picture) if item.picture else None,
|
'picture': get_picture(self.request.event, item.picture, '60x60^') if item.picture else None,
|
||||||
|
'picture_fullsize': get_picture(self.request.event, item.picture) if item.picture else None,
|
||||||
'description': str(rich_text(item.description, safelinks=False)) if item.description else None,
|
'description': str(rich_text(item.description, safelinks=False)) if item.description else None,
|
||||||
'has_variations': item.has_variations,
|
'has_variations': item.has_variations,
|
||||||
'require_voucher': item.require_voucher,
|
'require_voucher': item.require_voucher,
|
||||||
|
|||||||
@@ -165,13 +165,13 @@ if SITE_URL.endswith('/'):
|
|||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [urlparse(SITE_URL).scheme + '://' + urlparse(SITE_URL).hostname]
|
CSRF_TRUSTED_ORIGINS = [urlparse(SITE_URL).scheme + '://' + urlparse(SITE_URL).hostname]
|
||||||
|
|
||||||
TRUST_X_FORWARDED_FOR = config.getboolean('pretix', 'trust_x_forwarded_for', fallback=False)
|
TRUST_X_FORWARDED_FOR = config.get('pretix', 'trust_x_forwarded_for', fallback=False)
|
||||||
USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fallback=False)
|
USE_X_FORWARDED_HOST = config.get('pretix', 'trust_x_forwarded_host', fallback=False)
|
||||||
|
|
||||||
|
|
||||||
REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False)
|
REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False)
|
||||||
|
|
||||||
if config.getboolean('pretix', 'trust_x_forwarded_proto', fallback=False):
|
if config.get('pretix', 'trust_x_forwarded_proto', fallback=False):
|
||||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||||
|
|
||||||
PRETIX_PLUGINS_DEFAULT = config.get('pretix', 'plugins_default',
|
PRETIX_PLUGINS_DEFAULT = config.get('pretix', 'plugins_default',
|
||||||
@@ -684,22 +684,4 @@ FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT = 1024 * 1024 * config.getint("pretix_file
|
|||||||
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_email_auto_attachment", fallback=1)
|
FILE_UPLOAD_MAX_SIZE_EMAIL_AUTO_ATTACHMENT = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_email_auto_attachment", fallback=1)
|
||||||
FILE_UPLOAD_MAX_SIZE_OTHER = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_other", fallback=10)
|
FILE_UPLOAD_MAX_SIZE_OTHER = 1024 * 1024 * config.getint("pretix_file_upload", "max_size_other", fallback=10)
|
||||||
|
|
||||||
# Allowed file extensions for various places plus matching Pillow formats.
|
|
||||||
# Never allow EPS, it is full of dangerous bugs.
|
|
||||||
FILE_UPLOAD_EXTENSIONS_IMAGE = (".png", ".jpg", ".gif", ".jpeg")
|
|
||||||
PILLOW_FORMATS_IMAGE = ('PNG', 'GIF', 'JPEG')
|
|
||||||
|
|
||||||
FILE_UPLOAD_EXTENSIONS_FAVICON = (".ico", ".png", "jpg", ".gif", ".jpeg")
|
|
||||||
|
|
||||||
FILE_UPLOAD_EXTENSIONS_QUESTION_IMAGE = (".png", "jpg", ".gif", ".jpeg", ".bmp", ".tif", ".tiff", ".jfif")
|
|
||||||
PILLOW_FORMATS_QUESTIONS_IMAGE = ('PNG', 'GIF', 'JPEG', 'BMP', 'TIFF')
|
|
||||||
|
|
||||||
FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT = (
|
|
||||||
".png", ".jpg", ".gif", ".jpeg", ".pdf", ".txt", ".docx", ".gif", ".svg",
|
|
||||||
".pptx", ".ppt", ".doc", ".xlsx", ".xls", ".jfif", ".heic", ".heif", ".pages",
|
|
||||||
".bmp", ".tif", ".tiff"
|
|
||||||
)
|
|
||||||
FILE_UPLOAD_EXTENSIONS_OTHER = FILE_UPLOAD_EXTENSIONS_EMAIL_ATTACHMENT
|
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # sadly. we would prefer BigInt, and should use it for all new models but the migration will be hard
|
DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' # sadly. we would prefer BigInt, and should use it for all new models but the migration will be hard
|
||||||
|
|||||||
71
src/pretix/static/pretixpresale/js/walletdetection.js
Normal file
71
src/pretix/static/pretixpresale/js/walletdetection.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
var walletdetection = {
|
||||||
|
applepay: async function () {
|
||||||
|
// This is a weak check for Apple Pay - in order to do a proper check, we would need to also call
|
||||||
|
// canMakePaymentsWithActiveCard(merchantIdentifier)
|
||||||
|
|
||||||
|
return !!(window.ApplePaySession && window.ApplePaySession.canMakePayments());
|
||||||
|
},
|
||||||
|
googlepay: async function () {
|
||||||
|
// Checking for Google Pay is a little bit more involved, since it requires including the Google Pay JS SDK, and
|
||||||
|
// providing a lot of information.
|
||||||
|
// So for the time being, we only check if Google Pay is available in TEST-mode, which should hopefully give us a
|
||||||
|
// good enough idea if Google Pay could be present on this device; even though there are still a lot of other
|
||||||
|
// factors that could inhibit Google Pay from actually being offered to the customer.
|
||||||
|
|
||||||
|
return $.ajax({
|
||||||
|
url: 'https://pay.google.com/gp/p/js/pay.js',
|
||||||
|
dataType: 'script',
|
||||||
|
}).then(function() {
|
||||||
|
const paymentsClient = new google.payments.api.PaymentsClient({environment: 'TEST'});
|
||||||
|
return paymentsClient.isReadyToPay({
|
||||||
|
apiVersion: 2,
|
||||||
|
apiVersionMinor: 0,
|
||||||
|
allowedPaymentMethods: [{
|
||||||
|
type: 'CARD',
|
||||||
|
parameters: {
|
||||||
|
allowedAuthMethods: ["PAN_ONLY", "CRYPTOGRAM_3DS"],
|
||||||
|
allowedCardNetworks: ["AMEX", "DISCOVER", "INTERAC", "JCB", "MASTERCARD", "VISA"]
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
})
|
||||||
|
}).then(function (response) {
|
||||||
|
return !!response.result;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
name_map: {
|
||||||
|
applepay: gettext('Apple Pay'),
|
||||||
|
googlepay: gettext('Google Pay'),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(function () {
|
||||||
|
const wallets = $('[data-wallets]')
|
||||||
|
.map(function(index, pm) {
|
||||||
|
return pm.getAttribute("data-wallets").split("|");
|
||||||
|
})
|
||||||
|
.get()
|
||||||
|
.flat()
|
||||||
|
.filter(function(item, pos, self) {
|
||||||
|
// filter out empty or duplicate values
|
||||||
|
return item && self.indexOf(item) == pos;
|
||||||
|
});
|
||||||
|
|
||||||
|
wallets.forEach(function(wallet) {
|
||||||
|
const labels = $('[data-wallets*='+wallet+'] + label strong, [data-wallets*='+wallet+'] + strong')
|
||||||
|
.append('<span class="wallet wallet-loading"> <i aria-hidden="true" class="fa fa-cog fa-spin"></i></span>')
|
||||||
|
walletdetection[wallet]()
|
||||||
|
.then(function(result) {
|
||||||
|
const spans = labels.find(".wallet-loading:nth-of-type(1)");
|
||||||
|
if (result) {
|
||||||
|
spans.removeClass('wallet-loading').hide().text(', ' + walletdetection.name_map[wallet]).fadeIn(300);
|
||||||
|
} else {
|
||||||
|
spans.remove();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(result) {
|
||||||
|
labels.find(".wallet-loading:nth-of-type(1)").remove();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -450,7 +450,7 @@ Vue.component('item', {
|
|||||||
|
|
||||||
// Product description
|
// Product description
|
||||||
+ '<div class="pretix-widget-item-info-col">'
|
+ '<div class="pretix-widget-item-info-col">'
|
||||||
+ '<img :src="item.picture" v-if="item.picture" class="pretix-widget-item-picture">'
|
+ '<a :href="item.picture_fullsize" v-if="item.picture" class="pretix-widget-item-picture-link" @click.prevent.stop="lightbox"><img :src="item.picture" class="pretix-widget-item-picture"></a>'
|
||||||
+ '<div class="pretix-widget-item-title-and-description">'
|
+ '<div class="pretix-widget-item-title-and-description">'
|
||||||
+ '<a v-if="item.has_variations && show_toggle" class="pretix-widget-item-title" :href="\'#\' + item.id + \'-variants\'"'
|
+ '<a v-if="item.has_variations && show_toggle" class="pretix-widget-item-title" :href="\'#\' + item.id + \'-variants\'"'
|
||||||
+ ' @click.prevent.stop="expand" role="button" tabindex="0"'
|
+ ' @click.prevent.stop="expand" role="button" tabindex="0"'
|
||||||
@@ -530,6 +530,12 @@ Vue.component('item', {
|
|||||||
methods: {
|
methods: {
|
||||||
expand: function () {
|
expand: function () {
|
||||||
this.expanded = !this.expanded;
|
this.expanded = !this.expanded;
|
||||||
|
},
|
||||||
|
lightbox: function () {
|
||||||
|
this.$root.overlay.lightbox = {
|
||||||
|
image: this.item.picture_fullsize,
|
||||||
|
description: this.item.name,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
@@ -784,12 +790,44 @@ var shared_alert_fragment = (
|
|||||||
+ '</div>'
|
+ '</div>'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
var shared_lightbox_fragment = (
|
||||||
|
'<div :class="lightboxClasses" role="dialog" aria-modal="true" v-if="$root.lightbox" @click="lightboxClose">'
|
||||||
|
+ '<div class="pretix-widget-lightbox-loading" v-if="$root.lightbox?.loading">'
|
||||||
|
+ '<svg width="256" height="256" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path class="pretix-widget-primary-color" d="M1152 896q0-106-75-181t-181-75-181 75-75 181 75 181 181 75 181-75 75-181zm512-109v222q0 12-8 23t-20 13l-185 28q-19 54-39 91 35 50 107 138 10 12 10 25t-9 23q-27 37-99 108t-94 71q-12 0-26-9l-138-108q-44 23-91 38-16 136-29 186-7 28-36 28h-222q-14 0-24.5-8.5t-11.5-21.5l-28-184q-49-16-90-37l-141 107q-10 9-25 9-14 0-25-11-126-114-165-168-7-10-7-23 0-12 8-23 15-21 51-66.5t54-70.5q-27-50-41-99l-183-27q-13-2-21-12.5t-8-23.5v-222q0-12 8-23t19-13l186-28q14-46 39-92-40-57-107-138-10-12-10-24 0-10 9-23 26-36 98.5-107.5t94.5-71.5q13 0 26 10l138 107q44-23 91-38 16-136 29-186 7-28 36-28h222q14 0 24.5 8.5t11.5 21.5l28 184q49 16 90 37l142-107q9-9 24-9 13 0 25 10 129 119 165 170 7 8 7 22 0 12-8 23-15 21-51 66.5t-54 70.5q26 50 41 98l183 28q13 2 21 12.5t8 23.5z"/></svg>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '<div class="pretix-widget-lightbox-inner" @click.stop="">'
|
||||||
|
+ '<figure class="pretix-widget-lightbox-image">'
|
||||||
|
+ '<img :src="$root.lightbox.image" :alt="$root.lightbox.description" @load="lightboxLoaded" ref="lightboxImage">'
|
||||||
|
+ '<figcaption v-if="$root.lightbox.description">{{$root.lightbox.description}}</figcaption>'
|
||||||
|
+ '</figure>'
|
||||||
|
+ '<button type="button" class="pretix-widget-lightbox-close" @click="lightboxClose" aria-label="'+strings.close+'">'
|
||||||
|
+ '<svg height="16" viewBox="0 0 512 512" width="16" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M437.5,386.6L306.9,256l130.6-130.6c14.1-14.1,14.1-36.8,0-50.9c-14.1-14.1-36.8-14.1-50.9,0L256,205.1L125.4,74.5 c-14.1-14.1-36.8-14.1-50.9,0c-14.1,14.1-14.1,36.8,0,50.9L205.1,256L74.5,386.6c-14.1,14.1-14.1,36.8,0,50.9 c14.1,14.1,36.8,14.1,50.9,0L256,306.9l130.6,130.6c14.1,14.1,36.8,14.1,50.9,0C451.5,423.4,451.5,400.6,437.5,386.6z"/></svg>'
|
||||||
|
+ '</button>'
|
||||||
|
+ '</div>'
|
||||||
|
+ '</div>'
|
||||||
|
);
|
||||||
|
|
||||||
Vue.component('pretix-overlay', {
|
Vue.component('pretix-overlay', {
|
||||||
template: ('<div class="pretix-widget-overlay">'
|
template: ('<div class="pretix-widget-overlay">'
|
||||||
+ shared_iframe_fragment
|
+ shared_iframe_fragment
|
||||||
+ shared_alert_fragment
|
+ shared_alert_fragment
|
||||||
|
+ shared_lightbox_fragment
|
||||||
+ '</div>'
|
+ '</div>'
|
||||||
),
|
),
|
||||||
|
watch: {
|
||||||
|
'$root.lightbox': function (newValue, oldValue) {
|
||||||
|
if (newValue) {
|
||||||
|
if (newValue.image != oldValue?.image) {
|
||||||
|
this.$set(newValue, "loading", true);
|
||||||
|
}
|
||||||
|
if (!oldValue) {
|
||||||
|
window.addEventListener('keyup', this.lightboxCloseOnKeyup);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
window.removeEventListener('keyup', this.lightboxCloseOnKeyup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
frameClasses: function () {
|
frameClasses: function () {
|
||||||
return {
|
return {
|
||||||
@@ -803,8 +841,27 @@ Vue.component('pretix-overlay', {
|
|||||||
'pretix-widget-alert-shown': this.$root.error_message,
|
'pretix-widget-alert-shown': this.$root.error_message,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
lightboxClasses: function () {
|
||||||
|
return {
|
||||||
|
'pretix-widget-lightbox-holder': true,
|
||||||
|
'pretix-widget-lightbox-shown': this.$root.lightbox,
|
||||||
|
'pretix-widget-lightbox-isloading': this.$root.lightbox?.loading,
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
lightboxCloseOnKeyup: function (event) {
|
||||||
|
if (event.keyCode === 27) {
|
||||||
|
// abort on ESC-key
|
||||||
|
this.lightboxClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
lightboxClose: function () {
|
||||||
|
this.$root.lightbox = null;
|
||||||
|
},
|
||||||
|
lightboxLoaded: function () {
|
||||||
|
this.$root.lightbox.loading = false;
|
||||||
|
},
|
||||||
errorClose: function () {
|
errorClose: function () {
|
||||||
this.$root.error_message = null;
|
this.$root.error_message = null;
|
||||||
this.$root.error_url_after = null;
|
this.$root.error_url_after = null;
|
||||||
@@ -1795,6 +1852,7 @@ var create_overlay = function (app) {
|
|||||||
error_url_after: null,
|
error_url_after: null,
|
||||||
error_url_after_new_tab: true,
|
error_url_after_new_tab: true,
|
||||||
error_message: null,
|
error_message: null,
|
||||||
|
lightbox: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|||||||
@@ -179,3 +179,7 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wallet-loading + .wallet-loading {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
@@ -768,6 +768,102 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.pretix-widget-lightbox-holder {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
z-index: 16777271;
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s, visibility 0.5s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.pretix-widget-lightbox-loading svg {
|
||||||
|
margin: 40px;
|
||||||
|
-webkit-animation: pretix-widget-spin 6s linear infinite;
|
||||||
|
-moz-animation: pretix-widget-spin 6s linear infinite;
|
||||||
|
animation: pretix-widget-spin 6s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.pretix-widget-lightbox-shown {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.5s, visibility 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pretix-widget-lightbox-inner {
|
||||||
|
position: relative;
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px 5px 5px 5px;
|
||||||
|
-moz-border-radius: 5px 5px 5px 5px;
|
||||||
|
-webkit-border-radius: 5px 5px 5px 5px;
|
||||||
|
box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09);
|
||||||
|
-webkit-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09);
|
||||||
|
-moz-box-shadow: 0 4px 18px 0 rgba(0, 0, 0, 0.1), 0 6px 20px 0 rgba(0, 0, 0, 0.09);
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 10px;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 90%;
|
||||||
|
}
|
||||||
|
&.pretix-widget-lightbox-isloading .pretix-widget-lightbox-inner {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
padding: 0;
|
||||||
|
margin: -1px;
|
||||||
|
overflow: hidden;
|
||||||
|
clip: rect(0,0,0,0);
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pretix-widget-lightbox-image {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.pretix-widget-lightbox-image img {
|
||||||
|
max-width: 80vw;
|
||||||
|
max-height: 80vh;
|
||||||
|
object-fit: scale-down;
|
||||||
|
}
|
||||||
|
.pretix-widget-lightbox-image figcaption {
|
||||||
|
margin: 0.5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pretix-widget-lightbox-close {
|
||||||
|
position: absolute;
|
||||||
|
right: -12px;
|
||||||
|
top: -12px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
background: $brand-primary;
|
||||||
|
margin: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 12px;
|
||||||
|
-moz-border-radius: 12px;
|
||||||
|
-webkit-border-radius: 12px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: sans-serif;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 4px 0;
|
||||||
|
display: inline-block;
|
||||||
|
line-height: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pretix-widget-lightbox-close svg {
|
||||||
|
display: inline-block;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.pretix-widget-primary-color {
|
.pretix-widget-primary-color {
|
||||||
/* in SVG */
|
/* in SVG */
|
||||||
fill: $brand-primary;
|
fill: $brand-primary;
|
||||||
|
|||||||
@@ -94,6 +94,8 @@ def test_initialize_valid_token(client, new_device: Device):
|
|||||||
'hardware_brand': 'Samsung',
|
'hardware_brand': 'Samsung',
|
||||||
'hardware_model': 'Galaxy S',
|
'hardware_model': 'Galaxy S',
|
||||||
'software_brand': 'pretixdroid',
|
'software_brand': 'pretixdroid',
|
||||||
|
'os_name': 'Android',
|
||||||
|
'os_version': '2.3.3',
|
||||||
'software_version': '4.0.0'
|
'software_version': '4.0.0'
|
||||||
})
|
})
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
@@ -105,6 +107,7 @@ def test_initialize_valid_token(client, new_device: Device):
|
|||||||
new_device.refresh_from_db()
|
new_device.refresh_from_db()
|
||||||
assert new_device.api_token
|
assert new_device.api_token
|
||||||
assert new_device.initialized
|
assert new_device.initialized
|
||||||
|
assert new_device.os_version == "2.3.3"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
@@ -142,6 +145,8 @@ def test_update_valid_fields(device_client, device: Device):
|
|||||||
resp = device_client.post('/api/v1/device/update', {
|
resp = device_client.post('/api/v1/device/update', {
|
||||||
'hardware_brand': 'Samsung',
|
'hardware_brand': 'Samsung',
|
||||||
'hardware_model': 'Galaxy S',
|
'hardware_model': 'Galaxy S',
|
||||||
|
'os_name': 'Android',
|
||||||
|
'os_version': '2.3.3',
|
||||||
'software_brand': 'pretixdroid',
|
'software_brand': 'pretixdroid',
|
||||||
'software_version': '5.0.0',
|
'software_version': '5.0.0',
|
||||||
'info': {
|
'info': {
|
||||||
@@ -151,9 +156,23 @@ def test_update_valid_fields(device_client, device: Device):
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
device.refresh_from_db()
|
device.refresh_from_db()
|
||||||
assert device.software_version == '5.0.0'
|
assert device.software_version == '5.0.0'
|
||||||
|
assert device.os_version == '2.3.3'
|
||||||
assert device.info == {'foo': 'bar'}
|
assert device.info == {'foo': 'bar'}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_update_valid_without_optional_fields(device_client, device: Device):
|
||||||
|
resp = device_client.post('/api/v1/device/update', {
|
||||||
|
'hardware_brand': 'Samsung',
|
||||||
|
'hardware_model': 'Galaxy S',
|
||||||
|
'software_brand': 'pretixdroid',
|
||||||
|
'software_version': '5.0.0',
|
||||||
|
}, format='json')
|
||||||
|
assert resp.status_code == 200
|
||||||
|
device.refresh_from_db()
|
||||||
|
assert device.software_version == '5.0.0'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_keyroll_required_auth(client, token_client, device: Device):
|
def test_keyroll_required_auth(client, token_client, device: Device):
|
||||||
resp = client.post('/api/v1/device/roll', {})
|
resp = client.post('/api/v1/device/roll', {})
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ def device(organizer, event):
|
|||||||
unique_serial="UOS3GNZ27O39V3QS",
|
unique_serial="UOS3GNZ27O39V3QS",
|
||||||
initialization_token="frkso3m2w58zuw70",
|
initialization_token="frkso3m2w58zuw70",
|
||||||
hardware_model="TC25",
|
hardware_model="TC25",
|
||||||
|
os_name="Android",
|
||||||
|
os_version="8.1.0",
|
||||||
software_brand="pretixSCAN",
|
software_brand="pretixSCAN",
|
||||||
software_version="1.5.1",
|
software_version="1.5.1",
|
||||||
initialized=now(),
|
initialized=now(),
|
||||||
@@ -58,6 +60,8 @@ TEST_DEV_RES = {
|
|||||||
"initialized": "2020-09-18T14:17:44.190021Z",
|
"initialized": "2020-09-18T14:17:44.190021Z",
|
||||||
"hardware_brand": "Zebra",
|
"hardware_brand": "Zebra",
|
||||||
"hardware_model": "TC25",
|
"hardware_model": "TC25",
|
||||||
|
"os_name": "Android",
|
||||||
|
"os_version": "8.1.0",
|
||||||
"software_brand": "pretixSCAN",
|
"software_brand": "pretixSCAN",
|
||||||
"software_version": "1.5.1",
|
"software_version": "1.5.1",
|
||||||
"security_profile": "full"
|
"security_profile": "full"
|
||||||
|
|||||||
@@ -20,7 +20,8 @@
|
|||||||
# <https://www.gnu.org/licenses/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timedelta
|
import zoneinfo
|
||||||
|
from datetime import date, datetime, timedelta
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ from django.test import TestCase
|
|||||||
from django.utils.timezone import make_aware, now
|
from django.utils.timezone import make_aware, now
|
||||||
from django_countries.fields import Country
|
from django_countries.fields import Country
|
||||||
from django_scopes import scope
|
from django_scopes import scope
|
||||||
|
from freezegun import freeze_time
|
||||||
from tests.testdummy.signals import FoobazSalesChannel
|
from tests.testdummy.signals import FoobazSalesChannel
|
||||||
|
|
||||||
from pretix.base.decimal import round_decimal
|
from pretix.base.decimal import round_decimal
|
||||||
@@ -406,6 +408,56 @@ def test_expiring_auto_disabled(event):
|
|||||||
assert o2.status == Order.STATUS_PENDING
|
assert o2.status == Order.STATUS_PENDING
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_expiring_auto_delayed(event):
|
||||||
|
event.settings.set('payment_term_expire_delay_days', 3)
|
||||||
|
event.settings.set('payment_term_last', date(2023, 7, 2))
|
||||||
|
event.settings.set('timezone', 'Europe/Berlin')
|
||||||
|
o1 = Order.objects.create(
|
||||||
|
code='FOO', event=event, email='dummy@dummy.test',
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
datetime=datetime(2023, 6, 22, 12, 13, 14, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
|
||||||
|
expires=datetime(2023, 6, 30, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
|
||||||
|
total=0,
|
||||||
|
)
|
||||||
|
o2 = Order.objects.create(
|
||||||
|
code='FO2', event=event, email='dummy@dummy.test',
|
||||||
|
status=Order.STATUS_PENDING,
|
||||||
|
datetime=datetime(2023, 6, 22, 12, 13, 14, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
|
||||||
|
expires=datetime(2023, 6, 28, 23, 59, 59, tzinfo=zoneinfo.ZoneInfo("Europe/Berlin")),
|
||||||
|
total=0,
|
||||||
|
)
|
||||||
|
assert o1.payment_term_expire_date == o1.expires + timedelta(days=2) # limited by term_last
|
||||||
|
assert o2.payment_term_expire_date == o2.expires + timedelta(days=3)
|
||||||
|
with freeze_time("2023-06-29T00:01:00+02:00"):
|
||||||
|
expire_orders(None)
|
||||||
|
o1 = Order.objects.get(id=o1.id)
|
||||||
|
assert o1.status == Order.STATUS_PENDING
|
||||||
|
o2 = Order.objects.get(id=o2.id)
|
||||||
|
assert o2.status == Order.STATUS_PENDING
|
||||||
|
|
||||||
|
with freeze_time("2023-07-01T23:50:00+02:00"):
|
||||||
|
expire_orders(None)
|
||||||
|
o1 = Order.objects.get(id=o1.id)
|
||||||
|
assert o1.status == Order.STATUS_PENDING
|
||||||
|
o2 = Order.objects.get(id=o2.id)
|
||||||
|
assert o2.status == Order.STATUS_PENDING
|
||||||
|
|
||||||
|
with freeze_time("2023-07-02T00:01:00+02:00"):
|
||||||
|
expire_orders(None)
|
||||||
|
o1 = Order.objects.get(id=o1.id)
|
||||||
|
assert o1.status == Order.STATUS_PENDING
|
||||||
|
o2 = Order.objects.get(id=o2.id)
|
||||||
|
assert o2.status == Order.STATUS_EXPIRED
|
||||||
|
|
||||||
|
with freeze_time("2023-07-03T00:01:00+02:00"):
|
||||||
|
expire_orders(None)
|
||||||
|
o1 = Order.objects.get(id=o1.id)
|
||||||
|
assert o1.status == Order.STATUS_EXPIRED
|
||||||
|
o2 = Order.objects.get(id=o2.id)
|
||||||
|
assert o2.status == Order.STATUS_EXPIRED
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_do_not_expire_if_approval_pending(event):
|
def test_do_not_expire_if_approval_pending(event):
|
||||||
o1 = Order.objects.create(
|
o1 = Order.objects.create(
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ from pretix.base.templatetags.rich_text import (
|
|||||||
# Test link detection
|
# Test link detection
|
||||||
("google.com",
|
("google.com",
|
||||||
'<a href="http://google.com" rel="noopener" target="_blank">google.com</a>'),
|
'<a href="http://google.com" rel="noopener" target="_blank">google.com</a>'),
|
||||||
|
# Test link escaping
|
||||||
|
("google\\.com", 'google.com'),
|
||||||
# Test abslink_callback
|
# Test abslink_callback
|
||||||
("[Call](tel:+12345)",
|
("[Call](tel:+12345)",
|
||||||
'<a href="tel:+12345" rel="nofollow">Call</a>'),
|
'<a href="tel:+12345" rel="nofollow">Call</a>'),
|
||||||
@@ -79,3 +81,20 @@ def test_newline_handling(content, result):
|
|||||||
])
|
])
|
||||||
def test_newline_handling_email(content, result):
|
def test_newline_handling_email(content, result):
|
||||||
assert markdown_compile_email(content) == result
|
assert markdown_compile_email(content) == result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("content,result,result_snippet", [
|
||||||
|
# attributes
|
||||||
|
('<a onclick="javascript:foo()">foo</a>', '<p><a>foo</a></p>', '<a>foo</a>'),
|
||||||
|
('<strong color="red">foo</strong>',
|
||||||
|
'<p><strong>foo</strong></p>',
|
||||||
|
'<strong>foo</strong>'),
|
||||||
|
# protocols
|
||||||
|
('<a href="javascript:foo()">foo</a>', '<p><a>foo</a></p>', '<a>foo</a>'),
|
||||||
|
# tags
|
||||||
|
('<script>foo</script>', '<script>foo</script>', 'foo'),
|
||||||
|
])
|
||||||
|
def test_cleanup(content, result, result_snippet):
|
||||||
|
assert rich_text(content) == result
|
||||||
|
assert rich_text_snippet(content) == result_snippet
|
||||||
|
assert markdown_compile_email(content) == result
|
||||||
|
|||||||
@@ -185,6 +185,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
"max_price": None,
|
"max_price": None,
|
||||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||||
"picture": None,
|
"picture": None,
|
||||||
|
"picture_fullsize": None,
|
||||||
"has_variations": 0,
|
"has_variations": 0,
|
||||||
"allow_waitinglist": True,
|
"allow_waitinglist": True,
|
||||||
"mandatory_priced_addons": False,
|
"mandatory_priced_addons": False,
|
||||||
@@ -204,6 +205,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
"max_price": "14.00",
|
"max_price": "14.00",
|
||||||
"price": None,
|
"price": None,
|
||||||
"picture": None,
|
"picture": None,
|
||||||
|
"picture_fullsize": None,
|
||||||
"has_variations": 4,
|
"has_variations": 4,
|
||||||
"allow_waitinglist": True,
|
"allow_waitinglist": True,
|
||||||
"mandatory_priced_addons": False,
|
"mandatory_priced_addons": False,
|
||||||
@@ -265,6 +267,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
|
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00",
|
||||||
"includes_mixed_tax_rate": False},
|
"includes_mixed_tax_rate": False},
|
||||||
"picture": None,
|
"picture": None,
|
||||||
|
"picture_fullsize": None,
|
||||||
"has_variations": 0,
|
"has_variations": 0,
|
||||||
"allow_waitinglist": True,
|
"allow_waitinglist": True,
|
||||||
"mandatory_priced_addons": False,
|
"mandatory_priced_addons": False,
|
||||||
@@ -310,6 +313,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
"max_price": "14.00",
|
"max_price": "14.00",
|
||||||
"price": None,
|
"price": None,
|
||||||
"picture": None,
|
"picture": None,
|
||||||
|
"picture_fullsize": None,
|
||||||
"has_variations": 4,
|
"has_variations": 4,
|
||||||
"allow_waitinglist": True,
|
"allow_waitinglist": True,
|
||||||
"mandatory_priced_addons": False,
|
"mandatory_priced_addons": False,
|
||||||
@@ -371,6 +375,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
"max_price": None,
|
"max_price": None,
|
||||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||||
"picture": None,
|
"picture": None,
|
||||||
|
"picture_fullsize": None,
|
||||||
"has_variations": 0,
|
"has_variations": 0,
|
||||||
"allow_waitinglist": True,
|
"allow_waitinglist": True,
|
||||||
"mandatory_priced_addons": False,
|
"mandatory_priced_addons": False,
|
||||||
@@ -425,6 +430,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
|||||||
'id': self.shirt.pk,
|
'id': self.shirt.pk,
|
||||||
'name': 'T-Shirt',
|
'name': 'T-Shirt',
|
||||||
'picture': None,
|
'picture': None,
|
||||||
|
"picture_fullsize": None,
|
||||||
'description': None,
|
'description': None,
|
||||||
'has_variations': 2,
|
'has_variations': 2,
|
||||||
"allow_waitinglist": True,
|
"allow_waitinglist": True,
|
||||||
|
|||||||
Reference in New Issue
Block a user