mirror of
https://github.com/pretix/pretix.git
synced 2026-03-03 10:52:26 +00:00
Compare commits
14 Commits
error-back
...
fix-mails-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c229e5ad5a | ||
|
|
769e1312d4 | ||
|
|
3d53c03906 | ||
|
|
59d1d2cb16 | ||
|
|
7e45837295 | ||
|
|
fd9ed15065 | ||
|
|
2df3d9206b | ||
|
|
fbd8bbbeaa | ||
|
|
1c305e4b30 | ||
|
|
ea114b4f64 | ||
|
|
0342613635 | ||
|
|
743c4b796b | ||
|
|
8a7f54795e | ||
|
|
cb464ad597 |
@@ -92,7 +92,7 @@ dependencies = [
|
|||||||
"redis==7.1.*",
|
"redis==7.1.*",
|
||||||
"reportlab==4.4.*",
|
"reportlab==4.4.*",
|
||||||
"requests==2.32.*",
|
"requests==2.32.*",
|
||||||
"sentry-sdk==2.52.*",
|
"sentry-sdk==2.53.*",
|
||||||
"sepaxml==2.7.*",
|
"sepaxml==2.7.*",
|
||||||
"stripe==7.9.*",
|
"stripe==7.9.*",
|
||||||
"text-unidecode==1.*",
|
"text-unidecode==1.*",
|
||||||
@@ -110,7 +110,7 @@ dev = [
|
|||||||
"aiohttp==3.13.*",
|
"aiohttp==3.13.*",
|
||||||
"coverage",
|
"coverage",
|
||||||
"coveralls",
|
"coveralls",
|
||||||
"fakeredis==2.33.*",
|
"fakeredis==2.34.*",
|
||||||
"flake8==7.3.*",
|
"flake8==7.3.*",
|
||||||
"freezegun",
|
"freezegun",
|
||||||
"isort==7.0.*",
|
"isort==7.0.*",
|
||||||
|
|||||||
@@ -188,11 +188,15 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
|||||||
clist = self.get_object()
|
clist = self.get_object()
|
||||||
if serializer.validated_data.get('nonce'):
|
if serializer.validated_data.get('nonce'):
|
||||||
if kwargs.get('position'):
|
if kwargs.get('position'):
|
||||||
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
|
prev = kwargs['position'].all_checkins.filter(
|
||||||
|
nonce=serializer.validated_data['nonce'],
|
||||||
|
successful=False
|
||||||
|
).first()
|
||||||
else:
|
else:
|
||||||
prev = clist.checkins.filter(
|
prev = clist.checkins.filter(
|
||||||
nonce=serializer.validated_data['nonce'],
|
nonce=serializer.validated_data['nonce'],
|
||||||
raw_barcode=serializer.validated_data['raw_barcode'],
|
raw_barcode=serializer.validated_data['raw_barcode'],
|
||||||
|
successful=False
|
||||||
).first()
|
).first()
|
||||||
if prev:
|
if prev:
|
||||||
# Ignore because nonce is already handled
|
# Ignore because nonce is already handled
|
||||||
|
|||||||
@@ -259,7 +259,14 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
action='pretix.giftcards.transaction.manual',
|
action='pretix.giftcards.transaction.manual',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data=merge_dicts(self.request.data, {'id': inst.pk, 'acceptor_id': self.request.organizer.id})
|
data=merge_dicts(
|
||||||
|
self.request.data,
|
||||||
|
{
|
||||||
|
'id': inst.pk,
|
||||||
|
'acceptor_id': self.request.organizer.id,
|
||||||
|
'acceptor_slug': self.request.organizer.slug
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic()
|
@transaction.atomic()
|
||||||
@@ -290,7 +297,11 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
action='pretix.giftcards.transaction.manual',
|
action='pretix.giftcards.transaction.manual',
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
auth=self.request.auth,
|
auth=self.request.auth,
|
||||||
data={'value': diff, 'acceptor_id': self.request.organizer.id}
|
data={
|
||||||
|
'value': diff,
|
||||||
|
'acceptor_id': self.request.organizer.id,
|
||||||
|
'acceptor_slug': self.request.organizer.slug
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return inst
|
return inst
|
||||||
@@ -320,7 +331,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
|||||||
data={
|
data={
|
||||||
'value': value,
|
'value': value,
|
||||||
'text': text,
|
'text': text,
|
||||||
'acceptor_id': self.request.organizer.id
|
'acceptor_id': self.request.organizer.id,
|
||||||
|
'acceptor_slug': self.request.organizer.slug
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
|
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
|
|||||||
'notification_id': logentry.pk,
|
'notification_id': logentry.pk,
|
||||||
'issuer_id': logentry.organizer_id,
|
'issuer_id': logentry.organizer_id,
|
||||||
'acceptor_id': logentry.parsed_data.get('acceptor_id'),
|
'acceptor_id': logentry.parsed_data.get('acceptor_id'),
|
||||||
|
'acceptor_slug': logentry.parsed_data.get('acceptor_slug'),
|
||||||
'giftcard': giftcard.pk,
|
'giftcard': giftcard.pk,
|
||||||
'action': logentry.action_type,
|
'action': logentry.action_type,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -651,6 +651,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
pgettext('address', 'State'),
|
pgettext('address', 'State'),
|
||||||
_('Voucher'),
|
_('Voucher'),
|
||||||
_('Voucher budget usage'),
|
_('Voucher budget usage'),
|
||||||
|
_('Voucher tag'),
|
||||||
_('Pseudonymization ID'),
|
_('Pseudonymization ID'),
|
||||||
_('Ticket secret'),
|
_('Ticket secret'),
|
||||||
_('Seat ID'),
|
_('Seat ID'),
|
||||||
@@ -769,6 +770,7 @@ class OrderListExporter(MultiSheetListExporter):
|
|||||||
op.state_for_address or '',
|
op.state_for_address or '',
|
||||||
op.voucher.code if op.voucher else '',
|
op.voucher.code if op.voucher else '',
|
||||||
op.voucher_budget_use if op.voucher_budget_use else '',
|
op.voucher_budget_use if op.voucher_budget_use else '',
|
||||||
|
op.voucher.tag if op.voucher else '',
|
||||||
op.pseudonymization_id,
|
op.pseudonymization_id,
|
||||||
op.secret,
|
op.secret,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
|
|||||||
|
|
||||||
class PriceModeColumn(ImportColumn):
|
class PriceModeColumn(ImportColumn):
|
||||||
identifier = 'price_mode'
|
identifier = 'price_mode'
|
||||||
verbose_name = gettext_lazy('Price mode')
|
verbose_name = gettext_lazy('Price effect')
|
||||||
default_value = None
|
default_value = None
|
||||||
initial = 'static:none'
|
initial = 'static:none'
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ class PriceModeColumn(ImportColumn):
|
|||||||
elif value in reverse:
|
elif value in reverse:
|
||||||
return reverse[value]
|
return reverse[value]
|
||||||
else:
|
else:
|
||||||
raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format(
|
raise ValidationError(_("Could not parse {value} as a price effect, use one of {options}.").format(
|
||||||
value=value, options=', '.join(d.keys())
|
value=value, options=', '.join(d.keys())
|
||||||
))
|
))
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ class ValueColumn(DecimalColumnMixin, ImportColumn):
|
|||||||
def clean(self, value, previous_values):
|
def clean(self, value, previous_values):
|
||||||
value = super().clean(value, previous_values)
|
value = super().clean(value, previous_values)
|
||||||
if value and previous_values.get("price_mode") == "none":
|
if value and previous_values.get("price_mode") == "none":
|
||||||
raise ValidationError(_("It is pointless to set a value without a price mode."))
|
raise ValidationError(_("It is pointless to set a value without a price effect."))
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def assign(self, value, obj: Voucher, **kwargs):
|
def assign(self, value, obj: Voucher, **kwargs):
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ class Voucher(LoggedModel):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
price_mode = models.CharField(
|
price_mode = models.CharField(
|
||||||
verbose_name=_("Price mode"),
|
verbose_name=_("Price effect"),
|
||||||
max_length=100,
|
max_length=100,
|
||||||
choices=PRICE_MODES,
|
choices=PRICE_MODES,
|
||||||
default='none'
|
default='none'
|
||||||
|
|||||||
@@ -1650,7 +1650,8 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
action='pretix.giftcards.transaction.payment',
|
action='pretix.giftcards.transaction.payment',
|
||||||
data={
|
data={
|
||||||
'value': trans.value,
|
'value': trans.value,
|
||||||
'acceptor_id': self.event.organizer.id
|
'acceptor_id': self.event.organizer.id,
|
||||||
|
'acceptor_slug': self.event.organizer.slug
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except PaymentException as e:
|
except PaymentException as e:
|
||||||
@@ -1682,6 +1683,7 @@ class GiftCardPayment(BasePaymentProvider):
|
|||||||
data={
|
data={
|
||||||
'value': refund.amount,
|
'value': refund.amount,
|
||||||
'acceptor_id': self.event.organizer.id,
|
'acceptor_id': self.event.organizer.id,
|
||||||
|
'acceptor_slug': self.event.organizer.slug,
|
||||||
'text': refund.comment,
|
'text': refund.comment,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -389,7 +389,7 @@ def mail_send_task(self, **kwargs) -> bool:
|
|||||||
# mail_send_task(self, *, outgoing_mail)
|
# mail_send_task(self, *, outgoing_mail)
|
||||||
with scopes_disabled():
|
with scopes_disabled():
|
||||||
mail_send(**kwargs)
|
mail_send(**kwargs)
|
||||||
return
|
return False
|
||||||
else:
|
else:
|
||||||
raise ValueError("Unknown arguments")
|
raise ValueError("Unknown arguments")
|
||||||
|
|
||||||
@@ -443,15 +443,24 @@ def mail_send_task(self, **kwargs) -> bool:
|
|||||||
content = ct.file.read()
|
content = ct.file.read()
|
||||||
args.append((name, content, ct.type))
|
args.append((name, content, ct.type))
|
||||||
attach_size += len(content)
|
attach_size += len(content)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
# This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out
|
# This sometimes fails e.g. with FileNotFoundError. We haven't been able to figure out
|
||||||
# why (probably some race condition with ticket cache invalidation?), so retry later.
|
# why (probably some race condition with ticket cache invalidation?), so retry later.
|
||||||
try:
|
try:
|
||||||
self.retry(max_retries=5, countdown=60)
|
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}, will retry')
|
||||||
|
retry_after = 60
|
||||||
|
outgoing_mail.error = "Tickets not ready"
|
||||||
|
outgoing_mail.error_detail = str(e)
|
||||||
|
outgoing_mail.sent = now()
|
||||||
|
outgoing_mail.status = OutgoingMail.STATUS_AWAITING_RETRY
|
||||||
|
outgoing_mail.retry_after = now() + timedelta(seconds=retry_after)
|
||||||
|
outgoing_mail.save(update_fields=["status", "error", "error_detail", "sent", "retry_after",
|
||||||
|
"actual_attachments"])
|
||||||
|
self.retry(max_retries=5, countdown=retry_after)
|
||||||
except MaxRetriesExceededError:
|
except MaxRetriesExceededError:
|
||||||
# Well then, something is really wrong, let's send it without attachment before we
|
# Well then, something is really wrong, let's send it without attachment before we
|
||||||
# don't send at all
|
# don't send at all
|
||||||
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}')
|
logger.exception(f'Too many retries attaching tickets to email {outgoing_mail.guid}, skip attachment')
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
|
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
|
||||||
|
|||||||
@@ -253,7 +253,8 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
|||||||
auth=auth,
|
auth=auth,
|
||||||
data={
|
data={
|
||||||
'value': position.price,
|
'value': position.price,
|
||||||
'acceptor_id': order.event.organizer.id
|
'acceptor_id': order.event.organizer.id,
|
||||||
|
'acceptor_slug': order.event.organizer.slug
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
@@ -563,6 +564,7 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
|||||||
data={
|
data={
|
||||||
'value': -position.price,
|
'value': -position.price,
|
||||||
'acceptor_id': order.event.organizer.id,
|
'acceptor_id': order.event.organizer.id,
|
||||||
|
'acceptor_slug': order.event.organizer.slug
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2457,7 +2459,8 @@ class OrderChangeManager:
|
|||||||
auth=self.auth,
|
auth=self.auth,
|
||||||
data={
|
data={
|
||||||
'value': -position.price,
|
'value': -position.price,
|
||||||
'acceptor_id': self.order.event.organizer.id
|
'acceptor_id': self.order.event.organizer.id,
|
||||||
|
'acceptor_slug': self.order.event.organizer.slug
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2483,7 +2486,8 @@ class OrderChangeManager:
|
|||||||
auth=self.auth,
|
auth=self.auth,
|
||||||
data={
|
data={
|
||||||
'value': -opa.position.price,
|
'value': -opa.position.price,
|
||||||
'acceptor_id': self.order.event.organizer.id
|
'acceptor_id': self.order.event.organizer.id,
|
||||||
|
'acceptor_slug': self.order.event.organizer.slug
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -3453,6 +3457,7 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
|
|||||||
data={
|
data={
|
||||||
'value': trans.value,
|
'value': trans.value,
|
||||||
'acceptor_id': order.event.organizer.id,
|
'acceptor_id': order.event.organizer.id,
|
||||||
|
'acceptor_slug': order.event.organizer.slug
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
any_giftcards = True
|
any_giftcards = True
|
||||||
|
|||||||
@@ -8,9 +8,6 @@
|
|||||||
<h1>{% trans "Not found" %}</h1>
|
<h1>{% trans "Not found" %}</h1>
|
||||||
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</p>
|
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</p>
|
||||||
<p>{{ exception }}</p>
|
<p>{{ exception }}</p>
|
||||||
<p class="links">
|
|
||||||
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
|
|
||||||
</p>
|
|
||||||
{% if request.user.is_staff and not staff_session %}
|
{% if request.user.is_staff and not staff_session %}
|
||||||
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
|
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -144,14 +144,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<p>
|
<p>
|
||||||
{% trans "If you lose access to your devices, you can use one of the following keys to log in. We recommend to store them in a safe place, e.g. printed out or in a password manager. Every token can be used at most once." %}
|
{% blocktrans trimmed %}
|
||||||
|
If you lose access to your devices, you can use one of your emergency tokens to log in.
|
||||||
|
We recommend to store them in a safe place, e.g. printed out or in a password manager.
|
||||||
|
Every token can be used at most once.
|
||||||
|
{% endblocktrans %}
|
||||||
</p>
|
</p>
|
||||||
<p>{% trans "Unused tokens:" %}</p>
|
{% if static_tokens_device %}
|
||||||
<ul>
|
<p>
|
||||||
{% for t in static_tokens %}
|
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
|
||||||
<li><code>{{ t.token }}</code></li>
|
You generated your emergency tokens on {{ generation_date_time }}.
|
||||||
{% endfor %}
|
{% endblocktrans %}
|
||||||
</ul>
|
</p>
|
||||||
|
{% else %}
|
||||||
|
<p>
|
||||||
|
{% trans "You don't have any emergency tokens yet." %}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
|
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
|
||||||
<span class="fa fa-refresh"></span>
|
<span class="fa fa-refresh"></span>
|
||||||
{% trans "Generate new emergency tokens" %}
|
{% trans "Generate new emergency tokens" %}
|
||||||
|
|||||||
@@ -1850,7 +1850,8 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
|||||||
data={
|
data={
|
||||||
'value': value,
|
'value': value,
|
||||||
'text': request.POST.get('text'),
|
'text': request.POST.get('text'),
|
||||||
'acceptor_id': self.request.organizer.id
|
'acceptor_id': self.request.organizer.id,
|
||||||
|
'acceptor_slug': self.request.organizer.slug
|
||||||
},
|
},
|
||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
)
|
)
|
||||||
@@ -1913,7 +1914,8 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
|||||||
user=self.request.user,
|
user=self.request.user,
|
||||||
data={
|
data={
|
||||||
'value': form.cleaned_data['value'],
|
'value': form.cleaned_data['value'],
|
||||||
'acceptor_id': self.request.organizer.id
|
'acceptor_id': self.request.organizer.id,
|
||||||
|
'acceptor_slug': self.request.organizer.slug
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
return redirect(reverse(
|
return redirect(reverse(
|
||||||
|
|||||||
@@ -49,12 +49,14 @@ from django.db import transaction
|
|||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.crypto import get_random_string
|
from django.utils.crypto import get_random_string
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from django.utils.html import format_html
|
from django.utils.html import format_html
|
||||||
from django.utils.http import url_has_allowed_host_and_scheme
|
from django.utils.http import url_has_allowed_host_and_scheme
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
from django.views.decorators.cache import never_cache
|
||||||
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||||
from django_otp.plugins.otp_static.models import StaticDevice
|
from django_otp.plugins.otp_static.models import StaticDevice
|
||||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||||
@@ -85,8 +87,9 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class RecentAuthenticationRequiredMixin:
|
class RecentAuthenticationRequiredMixin:
|
||||||
max_time = 3600
|
max_time = 900
|
||||||
|
|
||||||
|
@method_decorator(never_cache)
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
|
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
|
||||||
if tdelta > self.max_time:
|
if tdelta > self.max_time:
|
||||||
@@ -289,16 +292,13 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
|||||||
ctx = super().get_context_data()
|
ctx = super().get_context_data()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
ctx['static_tokens'] = StaticDevice.objects.get(user=self.request.user, name='emergency').token_set.all()
|
ctx['static_tokens_device'] = StaticDevice.objects.get(user=self.request.user, name='emergency')
|
||||||
except StaticDevice.MultipleObjectsReturned:
|
except StaticDevice.MultipleObjectsReturned:
|
||||||
ctx['static_tokens'] = StaticDevice.objects.filter(
|
ctx['static_tokens_device'] = StaticDevice.objects.filter(
|
||||||
user=self.request.user, name='emergency'
|
user=self.request.user, name='emergency'
|
||||||
).first().token_set.all()
|
).first()
|
||||||
except StaticDevice.DoesNotExist:
|
except StaticDevice.DoesNotExist:
|
||||||
d = StaticDevice.objects.create(user=self.request.user, name='emergency')
|
ctx['static_tokens_device'] = None
|
||||||
for i in range(10):
|
|
||||||
d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
|
|
||||||
ctx['static_tokens'] = d.token_set.all()
|
|
||||||
|
|
||||||
ctx['devices'] = []
|
ctx['devices'] = []
|
||||||
for dt in REAL_DEVICE_TYPES:
|
for dt in REAL_DEVICE_TYPES:
|
||||||
@@ -631,7 +631,8 @@ class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, Template
|
|||||||
self.request.user.update_session_token()
|
self.request.user.update_session_token()
|
||||||
update_session_auth_hash(self.request, self.request.user)
|
update_session_auth_hash(self.request, self.request.user)
|
||||||
messages.success(request, _('Your emergency codes have been newly generated. Remember to store them in a safe '
|
messages.success(request, _('Your emergency codes have been newly generated. Remember to store them in a safe '
|
||||||
'place in case you lose access to your devices.'))
|
'place in case you lose access to your devices. You will not be able to view them '
|
||||||
|
'again here.\n\nYour emergency codes:\n- ' + '\n- '.join(t.token for t in d.token_set.all())))
|
||||||
return redirect(reverse('control:user.settings.2fa'))
|
return redirect(reverse('control:user.settings.2fa'))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,10 @@
|
|||||||
<dt>{% trans "Reference code (important):" %}</dt><dd><b>{{ code }}</b></dd>
|
<dt>{% trans "Reference code (important):" %}</dt><dd><b>{{ code }}</b></dd>
|
||||||
<dt>{% trans "Amount:" %}</dt><dd>{{ amount|money:event.currency }}</dd>
|
<dt>{% trans "Amount:" %}</dt><dd>{{ amount|money:event.currency }}</dd>
|
||||||
{% if settings.bank_details_type == "sepa" %}
|
{% if settings.bank_details_type == "sepa" %}
|
||||||
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dt>
|
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dd>
|
||||||
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dt>
|
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dd>
|
||||||
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dt>
|
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dd>
|
||||||
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dt>
|
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</dl>
|
</dl>
|
||||||
{% if details %}
|
{% if details %}
|
||||||
@@ -38,4 +38,4 @@
|
|||||||
{% if payment_qr_codes %}
|
{% if payment_qr_codes %}
|
||||||
{% include "pretixpresale/event/payment_qr_codes.html" %}
|
{% include "pretixpresale/event/payment_qr_codes.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ logger = logging.getLogger('pretix.plugins.stripe')
|
|||||||
# - UPI: ✗
|
# - UPI: ✗
|
||||||
# - Netbanking: ✗
|
# - Netbanking: ✗
|
||||||
# - TWINT: ✓
|
# - TWINT: ✓
|
||||||
|
# - Wero: ✓ (No settings UI yet)
|
||||||
#
|
#
|
||||||
# Bank transfers
|
# Bank transfers
|
||||||
# - ACH Bank Transfer: ✗
|
# - ACH Bank Transfer: ✗
|
||||||
@@ -509,6 +510,15 @@ class StripeSettingsHolder(BasePaymentProvider):
|
|||||||
'before they work properly.'),
|
'before they work properly.'),
|
||||||
required=False,
|
required=False,
|
||||||
)),
|
)),
|
||||||
|
# Disabled for now, since still in closed Beta and only available to dedicated boarded accounts.
|
||||||
|
# ('method_wero',
|
||||||
|
# forms.BooleanField(
|
||||||
|
# label=_('Wero'),
|
||||||
|
# disabled=self.event.currency not in 'EUR',
|
||||||
|
# help_text=_('Some payment methods might need to be enabled in the settings of your Stripe account '
|
||||||
|
# 'before they work properly.'),
|
||||||
|
# required=False,
|
||||||
|
# )),
|
||||||
] + extra_fields + list(super().settings_form_fields.items()) + moto_settings
|
] + extra_fields + list(super().settings_form_fields.items()) + moto_settings
|
||||||
)
|
)
|
||||||
if not self.settings.connect_client_id or self.settings.secret_key:
|
if not self.settings.connect_client_id or self.settings.secret_key:
|
||||||
@@ -1946,3 +1956,15 @@ class StripeMobilePay(StripeRedirectMethod):
|
|||||||
"type": "mobilepay",
|
"type": "mobilepay",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class StripeWero(StripeRedirectMethod):
|
||||||
|
identifier = 'stripe_wero'
|
||||||
|
verbose_name = _('WERO via Stripe')
|
||||||
|
public_name = 'WERO'
|
||||||
|
method = 'wero'
|
||||||
|
confirmation_method = 'automatic'
|
||||||
|
explanation = _(
|
||||||
|
'This payment method is available to European online banking users, whose banking institutions support WERO '
|
||||||
|
'either through their native banking apps or through the WERO wallet app. Please have you app ready.'
|
||||||
|
)
|
||||||
|
|||||||
@@ -49,14 +49,14 @@ def register_payment_provider(sender, **kwargs):
|
|||||||
StripeMultibanco, StripePayByBank, StripePayPal, StripePromptPay,
|
StripeMultibanco, StripePayByBank, StripePayPal, StripePromptPay,
|
||||||
StripePrzelewy24, StripeRevolutPay, StripeSEPADirectDebit,
|
StripePrzelewy24, StripeRevolutPay, StripeSEPADirectDebit,
|
||||||
StripeSettingsHolder, StripeSofort, StripeSwish, StripeTwint,
|
StripeSettingsHolder, StripeSofort, StripeSwish, StripeTwint,
|
||||||
StripeWeChatPay,
|
StripeWeChatPay, StripeWero,
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
|
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
|
||||||
StripeSofort, StripeEPS, StripeMultibanco, StripePayByBank, StripePrzelewy24, StripePromptPay, StripeRevolutPay,
|
StripeSofort, StripeEPS, StripeMultibanco, StripePayByBank, StripePrzelewy24, StripePromptPay, StripeRevolutPay,
|
||||||
StripeWeChatPay, StripeSEPADirectDebit, StripeAffirm, StripeKlarna, StripePayPal, StripeSwish,
|
StripeWeChatPay, StripeSEPADirectDebit, StripeAffirm, StripeKlarna, StripePayPal, StripeSwish,
|
||||||
StripeTwint, StripeMobilePay
|
StripeTwint, StripeMobilePay, StripeWero
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1177,6 +1177,30 @@ def test_store_failed(token_client, organizer, clist, event, order):
|
|||||||
assert resp.status_code == 400
|
assert resp.status_code == 400
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_store_failed_after_success(token_client, organizer, clist, event, order):
|
||||||
|
with scopes_disabled():
|
||||||
|
p = order.positions.first()
|
||||||
|
p.all_checkins.create(
|
||||||
|
type=Checkin.TYPE_ENTRY,
|
||||||
|
nonce='foobar',
|
||||||
|
successful=True,
|
||||||
|
list=clist,
|
||||||
|
raw_barcode=p.secret
|
||||||
|
)
|
||||||
|
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/failed_checkins/'.format(
|
||||||
|
organizer.slug, event.slug, clist.pk,
|
||||||
|
), {
|
||||||
|
'raw_barcode': p.secret,
|
||||||
|
'nonce': 'foobar',
|
||||||
|
'position': p.pk,
|
||||||
|
'error_reason': 'unpaid'
|
||||||
|
}, format='json')
|
||||||
|
assert resp.status_code == 201
|
||||||
|
with scopes_disabled():
|
||||||
|
assert Checkin.all.filter(position=p).count() == 2
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_redeem_unknown(token_client, organizer, clist, event, order):
|
def test_redeem_unknown(token_client, organizer, clist, event, order):
|
||||||
resp = _redeem(token_client, organizer, clist, 'unknown_secret', {'force': True})
|
resp = _redeem(token_client, organizer, clist, 'unknown_secret', {'force': True})
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ def test_price_mode_validation(event, item, user):
|
|||||||
import_vouchers.apply(
|
import_vouchers.apply(
|
||||||
args=(event.pk, inputfile_factory().id, settings, 'en', user.pk)
|
args=(event.pk, inputfile_factory().id, settings, 'en', user.pk)
|
||||||
).get()
|
).get()
|
||||||
assert 'It is pointless to set a value without a price mode.' in str(excinfo.value)
|
assert 'It is pointless to set a value without a price effect.' in str(excinfo.value)
|
||||||
|
|
||||||
settings['price_mode'] = 'static:percent'
|
settings['price_mode'] = 'static:percent'
|
||||||
import_vouchers.apply(
|
import_vouchers.apply(
|
||||||
|
|||||||
@@ -339,13 +339,17 @@ class UserSettings2FATest(SoupTest):
|
|||||||
|
|
||||||
def test_gen_emergency(self):
|
def test_gen_emergency(self):
|
||||||
self.client.get('/control/settings/2fa/')
|
self.client.get('/control/settings/2fa/')
|
||||||
|
assert not StaticDevice.objects.filter(user=self.user, name='emergency').exists()
|
||||||
|
|
||||||
|
self.client.post('/control/settings/2fa/regenemergency')
|
||||||
d = StaticDevice.objects.get(user=self.user, name='emergency')
|
d = StaticDevice.objects.get(user=self.user, name='emergency')
|
||||||
assert d.token_set.count() == 10
|
assert d.token_set.count() == 10
|
||||||
old_tokens = set(t.token for t in d.token_set.all())
|
old_tokens = set(t.token for t in d.token_set.all())
|
||||||
|
|
||||||
self.client.post('/control/settings/2fa/regenemergency')
|
self.client.post('/control/settings/2fa/regenemergency')
|
||||||
new_tokens = set(t.token for t in d.token_set.all())
|
|
||||||
d = StaticDevice.objects.get(user=self.user, name='emergency')
|
d = StaticDevice.objects.get(user=self.user, name='emergency')
|
||||||
assert d.token_set.count() == 10
|
assert d.token_set.count() == 10
|
||||||
|
new_tokens = set(t.token for t in d.token_set.all())
|
||||||
assert old_tokens != new_tokens
|
assert old_tokens != new_tokens
|
||||||
|
|
||||||
def test_delete_u2f(self):
|
def test_delete_u2f(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user