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",
|
||||
"hardware_brand": "Samsung",
|
||||
"hardware_model": "Galaxy S",
|
||||
"os_name": "Android",
|
||||
"os_version": "2.3.6",
|
||||
"software_brand": "pretixdroid",
|
||||
"software_version": "4.0.0"
|
||||
}
|
||||
@@ -98,6 +100,8 @@ following endpoint:
|
||||
{
|
||||
"hardware_brand": "Samsung",
|
||||
"hardware_model": "Galaxy S",
|
||||
"os_name": "Android",
|
||||
"os_version": "2.3.6",
|
||||
"software_brand": "pretixdroid",
|
||||
"software_version": "4.1.0",
|
||||
"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
|
||||
hardware_brand string Device hardware manufacturer (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_version string Device software version (read-only)
|
||||
created datetime Creation time
|
||||
@@ -76,6 +78,8 @@ Device endpoints
|
||||
"security_profile": "full",
|
||||
"hardware_brand": "Zebra",
|
||||
"hardware_model": "TC25",
|
||||
"os_name": "Android",
|
||||
"os_version": "8.1.0",
|
||||
"software_brand": "pretixSCAN",
|
||||
"software_version": "1.5.1"
|
||||
}
|
||||
@@ -123,6 +127,8 @@ Device endpoints
|
||||
"security_profile": "full",
|
||||
"hardware_brand": "Zebra",
|
||||
"hardware_model": "TC25",
|
||||
"os_name": "Android",
|
||||
"os_version": "8.1.0",
|
||||
"software_brand": "pretixSCAN",
|
||||
"software_version": "1.5.1"
|
||||
}
|
||||
@@ -173,6 +179,8 @@ Device endpoints
|
||||
"initialized": null
|
||||
"hardware_brand": null,
|
||||
"hardware_model": null,
|
||||
"os_name": null,
|
||||
"os_version": null,
|
||||
"software_brand": null,
|
||||
"software_version": null
|
||||
}
|
||||
|
||||
@@ -70,6 +70,8 @@ The provider class
|
||||
|
||||
.. autoattribute:: settings_form_fields
|
||||
|
||||
.. autoattribute:: walletqueries
|
||||
|
||||
.. automethod:: settings_form_clean
|
||||
|
||||
.. 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
|
||||
# <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_last',
|
||||
'payment_term_expire_automatically',
|
||||
'payment_term_expire_delay_days',
|
||||
'payment_term_accept_late',
|
||||
'payment_explanation',
|
||||
'payment_pending_hidden',
|
||||
|
||||
@@ -251,6 +251,8 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
unique_serial = serializers.CharField(read_only=True)
|
||||
hardware_brand = 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_version = serializers.CharField(read_only=True)
|
||||
created = serializers.DateTimeField(read_only=True)
|
||||
@@ -263,7 +265,7 @@ class DeviceSerializer(serializers.ModelSerializer):
|
||||
fields = (
|
||||
'device_id', 'unique_serial', 'initialization_token', 'all_events', 'limit_events',
|
||||
'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)
|
||||
hardware_brand = 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_version = serializers.CharField(max_length=190)
|
||||
info = serializers.JSONField(required=False, allow_null=True)
|
||||
@@ -50,6 +52,8 @@ class InitializationRequestSerializer(serializers.Serializer):
|
||||
class UpdateRequestSerializer(serializers.Serializer):
|
||||
hardware_brand = 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_version = serializers.CharField(max_length=190)
|
||||
info = serializers.JSONField(required=False, allow_null=True)
|
||||
@@ -99,6 +103,8 @@ class InitializeView(APIView):
|
||||
device.initialized = now()
|
||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||
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_version = serializer.validated_data.get('software_version')
|
||||
device.info = serializer.validated_data.get('info')
|
||||
@@ -120,6 +126,8 @@ class UpdateView(APIView):
|
||||
device = request.auth
|
||||
device.hardware_brand = serializer.validated_data.get('hardware_brand')
|
||||
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_version = serializer.validated_data.get('software_version')
|
||||
device.info = serializer.validated_data.get('info')
|
||||
|
||||
@@ -26,7 +26,6 @@ from decimal import Decimal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
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)
|
||||
extension = os.path.basename(image_file.name).split('.')[-1]
|
||||
else:
|
||||
img = Image.open(image_file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||
img = Image.open(image_file)
|
||||
ftype = Image.MIME[img.format]
|
||||
extensions = {
|
||||
'GIF': 'gif', 'TIFF': 'tif', 'BMP': 'bmp', 'JPEG': 'jpg', 'PNG': 'png'
|
||||
|
||||
@@ -500,14 +500,14 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
||||
file = BytesIO(data['content'])
|
||||
|
||||
try:
|
||||
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||
image = Image.open(file)
|
||||
# verify() must be called immediately after the constructor.
|
||||
image.verify()
|
||||
|
||||
# We want to do more than just verify(), so we need to re-open the file
|
||||
if hasattr(file, 'seek'):
|
||||
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
|
||||
if image.width > 10_000 or image.height > 10_000:
|
||||
@@ -566,7 +566,7 @@ class PortraitImageField(SizeValidationMixin, ExtValidationMixin, forms.FileFiel
|
||||
return f
|
||||
|
||||
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)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -826,7 +826,11 @@ class BaseQuestionsForm(forms.Form):
|
||||
help_text=help_text,
|
||||
initial=initial.file if initial else None,
|
||||
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,
|
||||
)
|
||||
elif q.type == Question.TYPE_DATE:
|
||||
|
||||
@@ -249,7 +249,7 @@ class SecurityMiddleware(MiddlewareMixin):
|
||||
|
||||
h = {
|
||||
'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'"],
|
||||
'frame-src': ['{static}', 'https://checkout.stripe.com', 'https://js.stripe.com'],
|
||||
'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,
|
||||
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(
|
||||
max_length=190,
|
||||
null=True, blank=True
|
||||
|
||||
@@ -896,6 +896,28 @@ class Order(LockModel, LoggedModel):
|
||||
), tz)
|
||||
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]:
|
||||
error_messages = {
|
||||
'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
|
||||
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
|
||||
def file_name(self):
|
||||
|
||||
@@ -830,13 +830,12 @@ class QuestionColumn(ImportColumn):
|
||||
class CustomerColumn(ImportColumn):
|
||||
identifier = 'customer'
|
||||
verbose_name = gettext_lazy('Customer')
|
||||
default_value = None
|
||||
|
||||
def clean(self, value, previous_values):
|
||||
if value:
|
||||
try:
|
||||
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:
|
||||
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__)
|
||||
|
||||
|
||||
class WalletQueries:
|
||||
APPLEPAY = 'applepay'
|
||||
GOOGLEPAY = 'googlepay'
|
||||
|
||||
WALLETS = (
|
||||
(APPLEPAY, pgettext_lazy('payment', 'Apple Pay')),
|
||||
(GOOGLEPAY, pgettext_lazy('payment', 'Google Pay')),
|
||||
)
|
||||
|
||||
|
||||
class PaymentProviderForm(Form):
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
@@ -436,6 +446,19 @@ class BasePaymentProvider:
|
||||
d['_restrict_to_sales_channels']._as_type = list
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
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
|
||||
else:
|
||||
if etag:
|
||||
@@ -939,7 +939,7 @@ class Renderer:
|
||||
|
||||
# reportlab does not support unicode combination characters
|
||||
# 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
|
||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||
|
||||
@@ -1270,12 +1270,12 @@ def expire_orders(sender, **kwargs):
|
||||
Exists(
|
||||
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:
|
||||
if o.event_id != event_id:
|
||||
expire = o.event.settings.get('payment_term_expire_automatically', as_type=bool)
|
||||
event_id = o.event_id
|
||||
if expire:
|
||||
if expire and now() >= o.payment_term_expire_date:
|
||||
mark_order_expired(o)
|
||||
|
||||
|
||||
|
||||
@@ -893,6 +893,28 @@ DEFAULTS = {
|
||||
"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': {
|
||||
'default': 'False',
|
||||
'type': bool,
|
||||
@@ -2713,7 +2735,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
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 '
|
||||
'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_kwargs': dict(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
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 '
|
||||
'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_kwargs': dict(
|
||||
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,
|
||||
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 '
|
||||
@@ -2817,7 +2839,7 @@ Your {organizer} team""")) # noqa: W291
|
||||
'form_class': ExtFileField,
|
||||
'form_kwargs': dict(
|
||||
label=_('Logo image'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
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.')
|
||||
|
||||
@@ -48,6 +48,8 @@ from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.safestring import mark_safe
|
||||
from markdown import Extension
|
||||
from markdown.inlinepatterns import SubstituteTagInlineProcessor
|
||||
from markdown.postprocessors import Postprocessor
|
||||
from markdown.treeprocessors import UnescapeTreeprocessor
|
||||
from tlds import tld_set
|
||||
|
||||
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)))
|
||||
|
||||
DOT_ESCAPE = "|escaped-dot-sGnY9LMK|"
|
||||
|
||||
|
||||
def safelink_callback(attrs, new=False):
|
||||
"""
|
||||
@@ -185,6 +189,109 @@ class EmailNl2BrExtension(Extension):
|
||||
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):
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
@@ -192,18 +299,20 @@ def markdown_compile_email(source):
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
return linker.linkify(bleach.clean(
|
||||
markdown.markdown(
|
||||
source,
|
||||
extensions=[
|
||||
'markdown.extensions.sane_lists',
|
||||
EmailNl2BrExtension(),
|
||||
]
|
||||
),
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
))
|
||||
return markdown.markdown(
|
||||
source,
|
||||
extensions=[
|
||||
'markdown.extensions.sane_lists',
|
||||
EmailNl2BrExtension(),
|
||||
LinkifyAndCleanExtension(
|
||||
linker,
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
strip=False,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SnippetExtension(markdown.extensions.Extension):
|
||||
@@ -213,23 +322,24 @@ class SnippetExtension(markdown.extensions.Extension):
|
||||
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
|
||||
exts = [
|
||||
'markdown.extensions.sane_lists',
|
||||
'markdown.extensions.nl2br'
|
||||
'markdown.extensions.nl2br',
|
||||
LinkifyAndCleanExtension(
|
||||
linker,
|
||||
tags=tags,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
strip=snippet,
|
||||
)
|
||||
]
|
||||
if snippet:
|
||||
exts.append(SnippetExtension())
|
||||
return bleach.clean(
|
||||
markdown.markdown(
|
||||
source,
|
||||
extensions=exts
|
||||
),
|
||||
strip=snippet,
|
||||
tags=tags,
|
||||
attributes=ALLOWED_ATTRIBUTES,
|
||||
protocols=ALLOWED_PROTOCOLS,
|
||||
return markdown.markdown(
|
||||
source,
|
||||
extensions=exts
|
||||
)
|
||||
|
||||
|
||||
@@ -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]),
|
||||
parse_email=True
|
||||
)
|
||||
body_md = linker.linkify(markdown_compile(text))
|
||||
body_md = markdown_compile(text, linker)
|
||||
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]),
|
||||
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)
|
||||
|
||||
@@ -127,7 +127,7 @@ class ClearableBasenameFileInput(forms.ClearableFileInput):
|
||||
|
||||
@property
|
||||
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):
|
||||
if hasattr(self.file, 'display_name'):
|
||||
|
||||
@@ -749,6 +749,7 @@ class PaymentSettingsForm(EventSettingsValidationMixin, SettingsForm):
|
||||
'payment_term_minutes',
|
||||
'payment_term_last',
|
||||
'payment_term_expire_automatically',
|
||||
'payment_term_expire_delay_days',
|
||||
'payment_term_accept_late',
|
||||
'payment_pending_hidden',
|
||||
'payment_explanation',
|
||||
|
||||
@@ -416,7 +416,7 @@ class OrganizerSettingsForm(SettingsForm):
|
||||
|
||||
organizer_logo_image = ExtFileField(
|
||||
label=_('Header image'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_IMAGE,
|
||||
ext_whitelist=(".png", ".jpg", ".gif", ".jpeg"),
|
||||
max_size=settings.FILE_UPLOAD_MAX_SIZE_IMAGE,
|
||||
required=False,
|
||||
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(
|
||||
label=_('Favicon'),
|
||||
ext_whitelist=settings.FILE_UPLOAD_EXTENSIONS_FAVICON,
|
||||
ext_whitelist=(".ico", ".png", ".jpg", ".gif", ".jpeg"),
|
||||
required=False,
|
||||
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. '
|
||||
|
||||
@@ -65,6 +65,8 @@
|
||||
{% bootstrap_field form.payment_term_minutes layout="control" %}
|
||||
{% bootstrap_field form.payment_term_last 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_pending_hidden layout="control" %}
|
||||
</fieldset>
|
||||
|
||||
@@ -1918,7 +1918,7 @@ class OrderContactChange(OrderView):
|
||||
'pretix.event.order.contact.changed',
|
||||
data={
|
||||
'old_email': old_email,
|
||||
'new_email': self.form.cleaned_data['email'],
|
||||
'new_email': self.form.cleaned_data.get('email'),
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
@@ -1930,7 +1930,7 @@ class OrderContactChange(OrderView):
|
||||
'pretix.event.order.phone.changed',
|
||||
data={
|
||||
'old_phone': old_phone,
|
||||
'new_phone': self.form.cleaned_data['phone'],
|
||||
'new_phone': self.form.cleaned_data.get('phone'),
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import logging
|
||||
from io import BytesIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from PIL.Image import MAX_IMAGE_PIXELS, DecompressionBombError
|
||||
@@ -52,7 +51,7 @@ def validate_uploaded_file_for_valid_image(f):
|
||||
|
||||
try:
|
||||
try:
|
||||
image = Image.open(file, formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||
image = Image.open(file)
|
||||
# verify() must be called immediately after the constructor.
|
||||
image.verify()
|
||||
except DecompressionBombError:
|
||||
|
||||
@@ -21,8 +21,6 @@
|
||||
#
|
||||
from datetime import datetime
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
def monkeypatch_vobject_performance():
|
||||
"""
|
||||
@@ -54,19 +52,5 @@ def monkeypatch_vobject_performance():
|
||||
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():
|
||||
monkeypatch_vobject_performance()
|
||||
monkeypatch_pillow_safer()
|
||||
|
||||
@@ -20,9 +20,8 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from arabic_reshaper import ArabicReshaper
|
||||
from django.conf import settings
|
||||
from django.utils.functional import SimpleLazyObject
|
||||
from PIL import Image
|
||||
from PIL.Image import Resampling
|
||||
from reportlab.lib.utils import ImageReader
|
||||
|
||||
|
||||
@@ -34,7 +33,7 @@ class ThumbnailingImageReader(ImageReader):
|
||||
height = width * self._image.size[1] / self._image.size[0]
|
||||
self._image.thumbnail(
|
||||
size=(int(width * dpi / 72), int(height * dpi / 72)),
|
||||
resample=Image.Resampling.BICUBIC
|
||||
resample=Resampling.BICUBIC
|
||||
)
|
||||
self._data = None
|
||||
return width, height
|
||||
@@ -45,9 +44,6 @@ class ThumbnailingImageReader(ImageReader):
|
||||
# (smaller) size of the modified image.
|
||||
return None
|
||||
|
||||
def _read_image(self, fp):
|
||||
return Image.open(fp, formats=settings.PILLOW_FORMATS_IMAGE)
|
||||
|
||||
|
||||
reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
||||
'delete_harakat': True,
|
||||
|
||||
@@ -23,7 +23,6 @@ import hashlib
|
||||
import math
|
||||
from io import BytesIO
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files.storage import default_storage
|
||||
from PIL import Image, ImageOps, ImageSequence
|
||||
@@ -166,7 +165,7 @@ def resize_image(image, size):
|
||||
|
||||
def create_thumbnail(sourcename, size):
|
||||
source = default_storage.open(sourcename)
|
||||
image = Image.open(BytesIO(source.read()), formats=settings.PILLOW_FORMATS_QUESTIONS_IMAGE)
|
||||
image = Image.open(BytesIO(source.read()))
|
||||
try:
|
||||
image.load()
|
||||
except:
|
||||
|
||||
@@ -5,8 +5,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-06-27 14:53+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2023-06-29 05:00+0000\n"
|
||||
"Last-Translator: Moritz Lerch <dev@moritz-lerch.de>\n"
|
||||
"Language-Team: German <https://translate.pretix.eu/projects/pretix/pretix/de/"
|
||||
">\n"
|
||||
"Language: de\n"
|
||||
@@ -16352,7 +16352,7 @@ msgstr "Terminal-ID"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:96
|
||||
msgid "Card holder"
|
||||
msgstr "Karteninhaber"
|
||||
msgstr "Karteninhaber*in"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:100
|
||||
msgid "Card expiration"
|
||||
@@ -21206,7 +21206,7 @@ msgstr "Import durchführen"
|
||||
#: pretix/control/templates/pretixcontrol/orders/import_start.html:10
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:16
|
||||
msgid "Upload a new file"
|
||||
msgstr "Neuen Datei hochladen"
|
||||
msgstr "Neue Datei hochladen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/orders/import_start.html:16
|
||||
msgid ""
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-06-27 14:53+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2023-06-29 05:00+0000\n"
|
||||
"Last-Translator: Moritz Lerch <dev@moritz-lerch.de>\n"
|
||||
"Language-Team: German (informal) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/de_Informal/>\n"
|
||||
"Language: de_Informal\n"
|
||||
@@ -16324,7 +16324,7 @@ msgstr "Terminal-ID"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:96
|
||||
msgid "Card holder"
|
||||
msgstr "Karteninhaber"
|
||||
msgstr "Karteninhaber*in"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/boxoffice/payment.html:100
|
||||
msgid "Card expiration"
|
||||
@@ -21172,7 +21172,7 @@ msgstr "Import durchführen"
|
||||
#: pretix/control/templates/pretixcontrol/orders/import_start.html:10
|
||||
#: pretix/plugins/banktransfer/templates/pretixplugins/banktransfer/import_form.html:16
|
||||
msgid "Upload a new file"
|
||||
msgstr "Neuen Datei hochladen"
|
||||
msgstr "Neue Datei hochladen"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/orders/import_start.html:16
|
||||
msgid ""
|
||||
|
||||
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2023-06-27 12:51+0000\n"
|
||||
"PO-Revision-Date: 2023-06-27 07:40+0000\n"
|
||||
"Last-Translator: Raphael Michel <michel@rami.io>\n"
|
||||
"PO-Revision-Date: 2023-06-28 06:00+0000\n"
|
||||
"Last-Translator: Yucheng Lin <yuchenglinedu@gmail.com>\n"
|
||||
"Language-Team: Chinese (Traditional) <https://translate.pretix.eu/projects/"
|
||||
"pretix/pretix/zh_Hant/>\n"
|
||||
"Language: zh_Hant\n"
|
||||
@@ -522,24 +522,20 @@ msgid "Test-Mode of shop has been deactivated"
|
||||
msgstr "商店的測試模式已啟動"
|
||||
|
||||
#: pretix/api/webhooks.py:339
|
||||
#, fuzzy
|
||||
msgid "Waiting list entry added"
|
||||
msgstr "候補名單條目"
|
||||
msgstr "候補名單條目已添加"
|
||||
|
||||
#: pretix/api/webhooks.py:343
|
||||
#, fuzzy
|
||||
msgid "Waiting list entry changed"
|
||||
msgstr "候補名單條目"
|
||||
msgstr "候補名單條目已更改"
|
||||
|
||||
#: pretix/api/webhooks.py:347
|
||||
#, fuzzy
|
||||
msgid "Waiting list entry deleted"
|
||||
msgstr "候補名單條目"
|
||||
msgstr "候補名單條目已刪除"
|
||||
|
||||
#: pretix/api/webhooks.py:351
|
||||
#, fuzzy
|
||||
msgid "Waiting list entry received voucher"
|
||||
msgstr "候補名單條目"
|
||||
msgstr "候補名單條目收到憑證"
|
||||
|
||||
#: pretix/base/addressvalidation.py:100 pretix/base/addressvalidation.py:103
|
||||
#: pretix/base/addressvalidation.py:108 pretix/base/forms/questions.py:938
|
||||
@@ -5347,9 +5343,8 @@ msgid "Seat {number}"
|
||||
msgstr "座位 {number}"
|
||||
|
||||
#: pretix/base/models/tax.py:157
|
||||
#, fuzzy
|
||||
msgid "Your set of rules is not valid. Error message: {}"
|
||||
msgstr "你的樣式檔案並非有效樣式。錯誤訊息:{}"
|
||||
msgstr "你的條例規則並非有效。錯誤訊息:{}"
|
||||
|
||||
#: pretix/base/models/tax.py:168 pretix/base/models/tax.py:146
|
||||
msgid "Official name"
|
||||
@@ -7642,9 +7637,8 @@ msgid "Something happened in your event after the export, please try again."
|
||||
msgstr "匯出後你的活動中發生一些事情,請重試。"
|
||||
|
||||
#: pretix/base/services/shredder.py:177
|
||||
#, fuzzy
|
||||
msgid "Data shredding completed"
|
||||
msgstr "完成抓取。"
|
||||
msgstr "數據粉碎完成"
|
||||
|
||||
#: pretix/base/services/stats.py:210
|
||||
msgid "Uncategorized"
|
||||
@@ -10720,6 +10714,20 @@ msgid ""
|
||||
"\n"
|
||||
"Your pretix team\n"
|
||||
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
|
||||
msgid "Upload photo"
|
||||
@@ -13913,11 +13921,11 @@ msgstr "活動已被刪除。"
|
||||
|
||||
#: pretix/control/logdisplay.py:375
|
||||
msgid "A removal process for personal data has been started."
|
||||
msgstr ""
|
||||
msgstr "個人數據的刪除過程已啟動。"
|
||||
|
||||
#: pretix/control/logdisplay.py:376
|
||||
msgid "A removal process for personal data has been completed."
|
||||
msgstr ""
|
||||
msgstr "個人數據的刪除過程已完成。"
|
||||
|
||||
#: pretix/control/logdisplay.py:377 pretix/control/logdisplay.py:375
|
||||
msgid "The order details have been changed."
|
||||
@@ -21588,7 +21596,8 @@ msgstr "確認碼"
|
||||
msgid ""
|
||||
"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."
|
||||
msgstr ""
|
||||
msgstr "根據事件中的數據量,以下步驟可能需要一段時間才能完成。完成後,我們將通過電子"
|
||||
"郵件通知你。"
|
||||
|
||||
#: pretix/control/templates/pretixcontrol/shredder/index.html:11
|
||||
msgid ""
|
||||
|
||||
@@ -76,7 +76,11 @@ class BaseMailForm(FormPlaceholderMixin, forms.Form):
|
||||
attachment = CachedFileField(
|
||||
label=_("Attachment"),
|
||||
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 '
|
||||
'of no more than 2 MB in size.'),
|
||||
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.forms import SecretKeySettingsField
|
||||
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.services.mail import SendMailException
|
||||
from pretix.base.settings import SettingsSandbox
|
||||
@@ -219,6 +221,20 @@ class StripeSettingsHolder(BasePaymentProvider):
|
||||
]
|
||||
|
||||
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',
|
||||
forms.CharField(
|
||||
label=_('Statement descriptor postfix'),
|
||||
@@ -747,6 +763,15 @@ class StripeCC(StripeMethod):
|
||||
public_name = _('Credit card')
|
||||
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:
|
||||
account = get_stripe_account_key(self)
|
||||
if not RegisteredApplePayDomain.objects.filter(account=account, domain=request.host).exists():
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
{% load money %}
|
||||
{% load bootstrap3 %}
|
||||
{% load rich_text %}
|
||||
{% block custom_header %}
|
||||
{{ block.super }}
|
||||
{% include "pretixpresale/event/fragment_walletdetection_head.html" %}
|
||||
{% endblock %}
|
||||
{% block inner %}
|
||||
{% if current_payments %}
|
||||
<p>{% trans "You already selected the following payment methods:" %}</p>
|
||||
@@ -71,7 +75,8 @@
|
||||
{% if selected == p.provider.identifier %}checked="checked"{% endif %}
|
||||
id="input_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>
|
||||
</p>
|
||||
</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 money %}
|
||||
{% block title %}{% trans "Change payment method" %}{% endblock %}
|
||||
{% block custom_header %}
|
||||
{{ block.super }}
|
||||
{% include "pretixpresale/event/fragment_walletdetection_head.html" %}
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% blocktrans trimmed with code=order.code %}
|
||||
@@ -29,7 +33,8 @@
|
||||
<input type="radio" name="payment" value="{{ p.provider.identifier }}"
|
||||
data-parent="#payment_accordion"
|
||||
{% 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>
|
||||
</label>
|
||||
</h4>
|
||||
|
||||
@@ -350,7 +350,9 @@ def _detect_event(request, require_live=True, require_plugin=None):
|
||||
)
|
||||
pathparts = request.get_full_path().split('/')
|
||||
pathparts[1] = event.slug
|
||||
return redirect('/'.join(pathparts))
|
||||
r = redirect('/'.join(pathparts))
|
||||
r['Access-Control-Allow-Origin'] = '*'
|
||||
return r
|
||||
else:
|
||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||
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[1] = event.organizer.slug
|
||||
pathparts[2] = event.slug
|
||||
return redirect('/'.join(pathparts))
|
||||
r = redirect('/'.join(pathparts))
|
||||
r['Access-Control-Allow-Origin'] = '*'
|
||||
return r
|
||||
except Event.DoesNotExist:
|
||||
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.'))
|
||||
pathparts = request.get_full_path().split('/')
|
||||
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.'))
|
||||
|
||||
request._event_detected = True
|
||||
|
||||
@@ -212,11 +212,14 @@ def price_dict(item, price):
|
||||
}
|
||||
|
||||
|
||||
def get_picture(event, picture):
|
||||
try:
|
||||
thumb = get_thumbnail(picture.name, '60x60^').thumb.url
|
||||
except:
|
||||
logger.exception(f'Failed to create thumbnail of {picture.name}')
|
||||
def get_picture(event, picture, size=None):
|
||||
thumb = None
|
||||
if size:
|
||||
try:
|
||||
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)
|
||||
return urljoin(build_absolute_uri(event, 'presale:event.index'), thumb)
|
||||
|
||||
@@ -264,7 +267,8 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
{
|
||||
'id': item.pk,
|
||||
'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,
|
||||
'has_variations': item.has_variations,
|
||||
'require_voucher': item.require_voucher,
|
||||
|
||||
@@ -165,13 +165,13 @@ if SITE_URL.endswith('/'):
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [urlparse(SITE_URL).scheme + '://' + urlparse(SITE_URL).hostname]
|
||||
|
||||
TRUST_X_FORWARDED_FOR = config.getboolean('pretix', 'trust_x_forwarded_for', fallback=False)
|
||||
USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fallback=False)
|
||||
TRUST_X_FORWARDED_FOR = config.get('pretix', 'trust_x_forwarded_for', 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)
|
||||
|
||||
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')
|
||||
|
||||
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_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
|
||||
|
||||
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
|
||||
+ '<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">'
|
||||
+ '<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"'
|
||||
@@ -530,6 +530,12 @@ Vue.component('item', {
|
||||
methods: {
|
||||
expand: function () {
|
||||
this.expanded = !this.expanded;
|
||||
},
|
||||
lightbox: function () {
|
||||
this.$root.overlay.lightbox = {
|
||||
image: this.item.picture_fullsize,
|
||||
description: this.item.name,
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -784,12 +790,44 @@ var shared_alert_fragment = (
|
||||
+ '</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', {
|
||||
template: ('<div class="pretix-widget-overlay">'
|
||||
+ shared_iframe_fragment
|
||||
+ shared_alert_fragment
|
||||
+ shared_lightbox_fragment
|
||||
+ '</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: {
|
||||
frameClasses: function () {
|
||||
return {
|
||||
@@ -803,8 +841,27 @@ Vue.component('pretix-overlay', {
|
||||
'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: {
|
||||
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 () {
|
||||
this.$root.error_message = null;
|
||||
this.$root.error_url_after = null;
|
||||
@@ -1795,6 +1852,7 @@ var create_overlay = function (app) {
|
||||
error_url_after: null,
|
||||
error_url_after_new_tab: true,
|
||||
error_message: null,
|
||||
lightbox: null,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -179,3 +179,7 @@
|
||||
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 {
|
||||
/* in SVG */
|
||||
fill: $brand-primary;
|
||||
|
||||
@@ -94,6 +94,8 @@ def test_initialize_valid_token(client, new_device: Device):
|
||||
'hardware_brand': 'Samsung',
|
||||
'hardware_model': 'Galaxy S',
|
||||
'software_brand': 'pretixdroid',
|
||||
'os_name': 'Android',
|
||||
'os_version': '2.3.3',
|
||||
'software_version': '4.0.0'
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
@@ -105,6 +107,7 @@ def test_initialize_valid_token(client, new_device: Device):
|
||||
new_device.refresh_from_db()
|
||||
assert new_device.api_token
|
||||
assert new_device.initialized
|
||||
assert new_device.os_version == "2.3.3"
|
||||
|
||||
|
||||
@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', {
|
||||
'hardware_brand': 'Samsung',
|
||||
'hardware_model': 'Galaxy S',
|
||||
'os_name': 'Android',
|
||||
'os_version': '2.3.3',
|
||||
'software_brand': 'pretixdroid',
|
||||
'software_version': '5.0.0',
|
||||
'info': {
|
||||
@@ -151,9 +156,23 @@ def test_update_valid_fields(device_client, device: Device):
|
||||
assert resp.status_code == 200
|
||||
device.refresh_from_db()
|
||||
assert device.software_version == '5.0.0'
|
||||
assert device.os_version == '2.3.3'
|
||||
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
|
||||
def test_keyroll_required_auth(client, token_client, device: Device):
|
||||
resp = client.post('/api/v1/device/roll', {})
|
||||
|
||||
@@ -35,6 +35,8 @@ def device(organizer, event):
|
||||
unique_serial="UOS3GNZ27O39V3QS",
|
||||
initialization_token="frkso3m2w58zuw70",
|
||||
hardware_model="TC25",
|
||||
os_name="Android",
|
||||
os_version="8.1.0",
|
||||
software_brand="pretixSCAN",
|
||||
software_version="1.5.1",
|
||||
initialized=now(),
|
||||
@@ -58,6 +60,8 @@ TEST_DEV_RES = {
|
||||
"initialized": "2020-09-18T14:17:44.190021Z",
|
||||
"hardware_brand": "Zebra",
|
||||
"hardware_model": "TC25",
|
||||
"os_name": "Android",
|
||||
"os_version": "8.1.0",
|
||||
"software_brand": "pretixSCAN",
|
||||
"software_version": "1.5.1",
|
||||
"security_profile": "full"
|
||||
|
||||
@@ -20,7 +20,8 @@
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import zoneinfo
|
||||
from datetime import date, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
@@ -31,6 +32,7 @@ from django.test import TestCase
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django_countries.fields import Country
|
||||
from django_scopes import scope
|
||||
from freezegun import freeze_time
|
||||
from tests.testdummy.signals import FoobazSalesChannel
|
||||
|
||||
from pretix.base.decimal import round_decimal
|
||||
@@ -406,6 +408,56 @@ def test_expiring_auto_disabled(event):
|
||||
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
|
||||
def test_do_not_expire_if_approval_pending(event):
|
||||
o1 = Order.objects.create(
|
||||
|
||||
@@ -30,6 +30,8 @@ from pretix.base.templatetags.rich_text import (
|
||||
# Test link detection
|
||||
("google.com",
|
||||
'<a href="http://google.com" rel="noopener" target="_blank">google.com</a>'),
|
||||
# Test link escaping
|
||||
("google\\.com", 'google.com'),
|
||||
# Test abslink_callback
|
||||
("[Call](tel:+12345)",
|
||||
'<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):
|
||||
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,
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"picture": None,
|
||||
"picture_fullsize": None,
|
||||
"has_variations": 0,
|
||||
"allow_waitinglist": True,
|
||||
"mandatory_priced_addons": False,
|
||||
@@ -204,6 +205,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"max_price": "14.00",
|
||||
"price": None,
|
||||
"picture": None,
|
||||
"picture_fullsize": None,
|
||||
"has_variations": 4,
|
||||
"allow_waitinglist": True,
|
||||
"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",
|
||||
"includes_mixed_tax_rate": False},
|
||||
"picture": None,
|
||||
"picture_fullsize": None,
|
||||
"has_variations": 0,
|
||||
"allow_waitinglist": True,
|
||||
"mandatory_priced_addons": False,
|
||||
@@ -310,6 +313,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"max_price": "14.00",
|
||||
"price": None,
|
||||
"picture": None,
|
||||
"picture_fullsize": None,
|
||||
"has_variations": 4,
|
||||
"allow_waitinglist": True,
|
||||
"mandatory_priced_addons": False,
|
||||
@@ -371,6 +375,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
"max_price": None,
|
||||
"price": {"gross": "23.00", "net": "19.33", "tax": "3.67", "name": "", "rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"picture": None,
|
||||
"picture_fullsize": None,
|
||||
"has_variations": 0,
|
||||
"allow_waitinglist": True,
|
||||
"mandatory_priced_addons": False,
|
||||
@@ -425,6 +430,7 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
'id': self.shirt.pk,
|
||||
'name': 'T-Shirt',
|
||||
'picture': None,
|
||||
"picture_fullsize": None,
|
||||
'description': None,
|
||||
'has_variations': 2,
|
||||
"allow_waitinglist": True,
|
||||
|
||||
Reference in New Issue
Block a user