mirror of
https://github.com/pretix/pretix.git
synced 2026-07-31 09:15:08 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e009498386 |
+3
-3
@@ -92,7 +92,7 @@ dependencies = [
|
||||
"redis==7.1.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.53.*",
|
||||
"sentry-sdk==2.52.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
@@ -110,10 +110,10 @@ dev = [
|
||||
"aiohttp==3.13.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.34.*",
|
||||
"fakeredis==2.33.*",
|
||||
"flake8==7.3.*",
|
||||
"freezegun",
|
||||
"isort==8.0.*",
|
||||
"isort==7.0.*",
|
||||
"pep8-naming==0.15.*",
|
||||
"potypo",
|
||||
"pytest-asyncio>=0.24",
|
||||
|
||||
@@ -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__ = "2026.3.0.dev0"
|
||||
__version__ = "2026.2.0.dev0"
|
||||
|
||||
@@ -188,15 +188,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
|
||||
clist = self.get_object()
|
||||
if serializer.validated_data.get('nonce'):
|
||||
if kwargs.get('position'):
|
||||
prev = kwargs['position'].all_checkins.filter(
|
||||
nonce=serializer.validated_data['nonce'],
|
||||
successful=False
|
||||
).first()
|
||||
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
|
||||
else:
|
||||
prev = clist.checkins.filter(
|
||||
nonce=serializer.validated_data['nonce'],
|
||||
raw_barcode=serializer.validated_data['raw_barcode'],
|
||||
successful=False
|
||||
).first()
|
||||
if prev:
|
||||
# Ignore because nonce is already handled
|
||||
|
||||
@@ -259,14 +259,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=merge_dicts(
|
||||
self.request.data,
|
||||
{
|
||||
'id': inst.pk,
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
}
|
||||
)
|
||||
data=merge_dicts(self.request.data, {'id': inst.pk, 'acceptor_id': self.request.organizer.id})
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
@@ -297,11 +290,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
action='pretix.giftcards.transaction.manual',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'value': diff,
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
}
|
||||
data={'value': diff, 'acceptor_id': self.request.organizer.id}
|
||||
)
|
||||
|
||||
return inst
|
||||
@@ -331,8 +320,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
data={
|
||||
'value': value,
|
||||
'text': text,
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
'acceptor_id': self.request.organizer.id
|
||||
}
|
||||
)
|
||||
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
|
||||
|
||||
@@ -183,7 +183,6 @@ class ParametrizedGiftcardWebhookEvent(ParametrizedWebhookEvent):
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'issuer_id': logentry.organizer_id,
|
||||
'issuer_slug': logentry.organizer.slug,
|
||||
'giftcard': giftcard.pk,
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
@@ -198,9 +197,7 @@ class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
|
||||
return {
|
||||
'notification_id': logentry.pk,
|
||||
'issuer_id': logentry.organizer_id,
|
||||
'issuer_slug': logentry.organizer.slug,
|
||||
'acceptor_id': logentry.parsed_data.get('acceptor_id'),
|
||||
'acceptor_slug': logentry.parsed_data.get('acceptor_slug'),
|
||||
'giftcard': giftcard.pk,
|
||||
'action': logentry.action_type,
|
||||
}
|
||||
@@ -475,7 +472,7 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
),
|
||||
ParametrizedGiftcardTransactionWebhookEvent(
|
||||
'pretix.giftcards.transaction.*',
|
||||
_('Gift card used in transaction'),
|
||||
_('Gift card used in transcation'),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ from pretix.base.templatetags.rich_text import (
|
||||
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
|
||||
markdown_compile_email, truelink_callback,
|
||||
)
|
||||
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
@@ -141,7 +141,6 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
return markdown_compile_email(plaintext, context=context)
|
||||
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
|
||||
apply_format_map = not isinstance(plain_body, FormattedString)
|
||||
body_md = self.compile_markdown(plain_body, context)
|
||||
if context:
|
||||
linker = bleach.Linker(
|
||||
@@ -150,13 +149,12 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
if apply_format_map:
|
||||
body_md = format_map(
|
||||
body_md,
|
||||
context=context,
|
||||
mode=SafeFormatter.MODE_RICH_TO_HTML,
|
||||
linkifier=linker
|
||||
)
|
||||
body_md = format_map(
|
||||
body_md,
|
||||
context=context,
|
||||
mode=SafeFormatter.MODE_RICH_TO_HTML,
|
||||
linkifier=linker
|
||||
)
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
|
||||
@@ -651,7 +651,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
pgettext('address', 'State'),
|
||||
_('Voucher'),
|
||||
_('Voucher budget usage'),
|
||||
_('Voucher tag'),
|
||||
_('Pseudonymization ID'),
|
||||
_('Ticket secret'),
|
||||
_('Seat ID'),
|
||||
@@ -770,7 +769,6 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
op.state_for_address or '',
|
||||
op.voucher.code if op.voucher else '',
|
||||
op.voucher_budget_use if op.voucher_budget_use else '',
|
||||
op.voucher.tag if op.voucher else '',
|
||||
op.pseudonymization_id,
|
||||
op.secret,
|
||||
]
|
||||
|
||||
@@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
|
||||
|
||||
class PriceModeColumn(ImportColumn):
|
||||
identifier = 'price_mode'
|
||||
verbose_name = gettext_lazy('Price effect')
|
||||
verbose_name = gettext_lazy('Price mode')
|
||||
default_value = None
|
||||
initial = 'static:none'
|
||||
|
||||
@@ -147,7 +147,7 @@ class PriceModeColumn(ImportColumn):
|
||||
elif value in reverse:
|
||||
return reverse[value]
|
||||
else:
|
||||
raise ValidationError(_("Could not parse {value} as a price effect, use one of {options}.").format(
|
||||
raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format(
|
||||
value=value, options=', '.join(d.keys())
|
||||
))
|
||||
|
||||
@@ -162,7 +162,7 @@ class ValueColumn(DecimalColumnMixin, ImportColumn):
|
||||
def clean(self, value, previous_values):
|
||||
value = super().clean(value, previous_values)
|
||||
if value and previous_values.get("price_mode") == "none":
|
||||
raise ValidationError(_("It is pointless to set a value without a price effect."))
|
||||
raise ValidationError(_("It is pointless to set a value without a price mode."))
|
||||
return value
|
||||
|
||||
def assign(self, value, obj: Voucher, **kwargs):
|
||||
|
||||
@@ -87,7 +87,7 @@ from pretix.base.timemachine import time_machine_now
|
||||
|
||||
from ...helpers import OF_SELF
|
||||
from ...helpers.countries import CachedCountries, FastCountryField
|
||||
from ...helpers.format import FormattedString, format_map
|
||||
from ...helpers.format import format_map
|
||||
from ...helpers.names import build_name
|
||||
from ...testutils.middleware import debugflags_var
|
||||
from ._transactions import (
|
||||
@@ -1178,8 +1178,7 @@ class Order(LockModel, LoggedModel):
|
||||
recipient = position.attendee_email
|
||||
|
||||
email_content = render_mail(template, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.locale, self, headers=headers, sender=sender,
|
||||
@@ -2908,8 +2907,7 @@ class OrderPosition(AbstractPosition):
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
recipient = self.attendee_email
|
||||
email_content = render_mail(template, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
|
||||
|
||||
@@ -239,7 +239,7 @@ class Voucher(LoggedModel):
|
||||
)
|
||||
)
|
||||
price_mode = models.CharField(
|
||||
verbose_name=_("Price effect"),
|
||||
verbose_name=_("Price mode"),
|
||||
max_length=100,
|
||||
choices=PRICE_MODES,
|
||||
default='none'
|
||||
|
||||
@@ -1650,8 +1650,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
action='pretix.giftcards.transaction.payment',
|
||||
data={
|
||||
'value': trans.value,
|
||||
'acceptor_id': self.event.organizer.id,
|
||||
'acceptor_slug': self.event.organizer.slug
|
||||
'acceptor_id': self.event.organizer.id
|
||||
}
|
||||
)
|
||||
except PaymentException as e:
|
||||
@@ -1683,7 +1682,6 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
data={
|
||||
'value': refund.amount,
|
||||
'acceptor_id': self.event.organizer.id,
|
||||
'acceptor_slug': self.event.organizer.slug,
|
||||
'text': refund.comment,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -81,9 +81,7 @@ from pretix.base.signals import (
|
||||
)
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.format import (
|
||||
FormattedString, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||
)
|
||||
from pretix.helpers.format import SafeFormatter, format_map
|
||||
from pretix.helpers.hierarkey import clean_filename
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
from pretix.presale.ical import get_private_icals
|
||||
@@ -220,9 +218,6 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
if email == INVALID_ADDRESS:
|
||||
return
|
||||
|
||||
if isinstance(template, FormattedString):
|
||||
raise TypeError("Cannot pass an already formatted body template")
|
||||
|
||||
if no_order_links and not plain_text_only:
|
||||
raise ValueError('If you set no_order_links, you also need to set plain_text_only.')
|
||||
|
||||
@@ -256,28 +251,23 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
if event and attach_tickets and not event.settings.mail_attach_tickets:
|
||||
attach_tickets = False
|
||||
|
||||
with language(locale), override(timezone):
|
||||
with language(locale):
|
||||
if isinstance(context, dict) and order:
|
||||
_autoextend_context(context, order)
|
||||
|
||||
# Build raw content
|
||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
||||
body_plain = render_mail(template, context, placeholder_mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
if settings_holder:
|
||||
signature = str(settings_holder.settings.get('mail_text_signature'))
|
||||
else:
|
||||
signature = ""
|
||||
|
||||
# Build full plain-text body
|
||||
if not isinstance(content_plain, FormattedString):
|
||||
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
else:
|
||||
body_plain = content_plain
|
||||
body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links)
|
||||
body_plain = format_map(body_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
|
||||
# Build subject
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
|
||||
subject = str(subject).format_map(TolerantDict(context))
|
||||
subject = raw_subject = subject.replace('\n', ' ').replace('\r', '')[:900]
|
||||
if settings_holder:
|
||||
subject = prefix_subject(settings_holder, subject)
|
||||
@@ -296,24 +286,26 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La
|
||||
else:
|
||||
renderer = ClassicMailRenderer(None, organizer)
|
||||
|
||||
try:
|
||||
if 'context' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
|
||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without context argument because context argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without position argument because position argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||
except:
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
with override(timezone):
|
||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
||||
try:
|
||||
if 'context' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position, context)
|
||||
elif 'position' in inspect.signature(renderer.render).parameters:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without context argument because context argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order, position)
|
||||
else:
|
||||
# Backwards compatibility
|
||||
warnings.warn('Email renderer called without position argument because position argument is not '
|
||||
'supported.',
|
||||
DeprecationWarning)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||
except:
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
|
||||
m = OutgoingMail.objects.create(
|
||||
organizer=organizer,
|
||||
@@ -389,7 +381,7 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
# mail_send_task(self, *, outgoing_mail)
|
||||
with scopes_disabled():
|
||||
mail_send(**kwargs)
|
||||
return False
|
||||
return
|
||||
else:
|
||||
raise ValueError("Unknown arguments")
|
||||
|
||||
@@ -443,24 +435,15 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
content = ct.file.read()
|
||||
args.append((name, content, ct.type))
|
||||
attach_size += len(content)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
# 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.
|
||||
try:
|
||||
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)
|
||||
self.retry(max_retries=5, countdown=60)
|
||||
except MaxRetriesExceededError:
|
||||
# Well then, something is really wrong, let's send it without attachment before we
|
||||
# don't send at all
|
||||
logger.exception(f'Too many retries attaching tickets to email {outgoing_mail.guid}, skip attachment')
|
||||
logger.exception(f'Could not attach tickets to email {outgoing_mail.guid}')
|
||||
pass
|
||||
|
||||
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
|
||||
@@ -807,12 +790,7 @@ def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter
|
||||
body = format_map(body, context, mode=placeholder_mode)
|
||||
else:
|
||||
tpl = get_template(template)
|
||||
context = {
|
||||
# Known bug, should behave differently for plain and HTML but we'll fix after security release
|
||||
k: v.html if isinstance(v, PlainHtmlAlternativeString) else v
|
||||
for k, v in context.items()
|
||||
}
|
||||
body = FormattedString(tpl.render(context))
|
||||
body = tpl.render(context)
|
||||
return body
|
||||
|
||||
|
||||
@@ -958,7 +936,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
if signature:
|
||||
signature = format_map(signature, {"event": event.name if event else ''})
|
||||
signature = signature.format(event=event.name if event else '')
|
||||
body_plain += signature
|
||||
body_plain += "\r\n\r\n-- \r\n"
|
||||
|
||||
@@ -970,7 +948,7 @@ def _wrap_plain_body(content_plain, signature, event, order, position, no_order_
|
||||
body_plain += _(
|
||||
"You can view your order details at the following URL:\n{orderurl}."
|
||||
).replace("\n", "\r\n").format(
|
||||
orderurl=build_absolute_uri(
|
||||
event=event.name, orderurl=build_absolute_uri(
|
||||
order.event, 'presale:event.order.position', kwargs={
|
||||
'order': order.code,
|
||||
'secret': position.web_secret,
|
||||
|
||||
@@ -253,8 +253,7 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
|
||||
auth=auth,
|
||||
data={
|
||||
'value': position.price,
|
||||
'acceptor_id': order.event.organizer.id,
|
||||
'acceptor_slug': order.event.organizer.slug
|
||||
'acceptor_id': order.event.organizer.id
|
||||
}
|
||||
)
|
||||
break
|
||||
@@ -564,7 +563,6 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
|
||||
data={
|
||||
'value': -position.price,
|
||||
'acceptor_id': order.event.organizer.id,
|
||||
'acceptor_slug': order.event.organizer.slug
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1799,6 +1797,8 @@ class OrderChangeManager:
|
||||
tax_rule = tax_rules.get(pos.pk, pos.tax_rule)
|
||||
if not tax_rule:
|
||||
continue
|
||||
if not pos.price:
|
||||
continue
|
||||
|
||||
try:
|
||||
new_rate = tax_rule.tax_rate_for(ia)
|
||||
@@ -1815,9 +1815,7 @@ class OrderChangeManager:
|
||||
override_tax_rate=new_rate, override_tax_code=new_code)
|
||||
self._totaldiff_guesstimate += new_tax.gross - pos.price
|
||||
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
|
||||
if pos.price:
|
||||
# We do not consider the invoice dirty if only 0€-valued taxes are changed
|
||||
self._invoice_dirty = True
|
||||
self._invoice_dirty = True
|
||||
|
||||
def cancel_fee(self, fee: OrderFee):
|
||||
self._totaldiff_guesstimate -= fee.value
|
||||
@@ -2459,8 +2457,7 @@ class OrderChangeManager:
|
||||
auth=self.auth,
|
||||
data={
|
||||
'value': -position.price,
|
||||
'acceptor_id': self.order.event.organizer.id,
|
||||
'acceptor_slug': self.order.event.organizer.slug
|
||||
'acceptor_id': self.order.event.organizer.id
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2486,8 +2483,7 @@ class OrderChangeManager:
|
||||
auth=self.auth,
|
||||
data={
|
||||
'value': -opa.position.price,
|
||||
'acceptor_id': self.order.event.organizer.id,
|
||||
'acceptor_slug': self.order.event.organizer.slug
|
||||
'acceptor_id': self.order.event.organizer.id
|
||||
}
|
||||
)
|
||||
|
||||
@@ -3457,7 +3453,6 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
|
||||
data={
|
||||
'value': trans.value,
|
||||
'acceptor_id': order.event.organizer.id,
|
||||
'acceptor_slug': order.event.organizer.slug
|
||||
}
|
||||
)
|
||||
any_giftcards = True
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
<h1>{% trans "Not found" %}</h1>
|
||||
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</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 %}
|
||||
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
|
||||
<p>
|
||||
|
||||
@@ -19,44 +19,17 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django import forms
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes.forms import SafeModelChoiceField
|
||||
from phonenumber_field.formfields import PhoneNumberField
|
||||
|
||||
from pretix.base.forms import I18nModelForm
|
||||
from pretix.base.forms.questions import (
|
||||
NamePartsFormField, WrappedPhoneNumberPrefixWidget,
|
||||
)
|
||||
from pretix.base.models import WaitingListEntry
|
||||
from pretix.control.forms.widgets import Select2
|
||||
|
||||
|
||||
class WaitingListEntryEditForm(I18nModelForm):
|
||||
itemvar = forms.ChoiceField(
|
||||
error_messages={
|
||||
'invalid_choice': _("Select a valid choice.")
|
||||
}
|
||||
)
|
||||
class WaitingListEntryTransferForm(I18nModelForm):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.get('instance', None)
|
||||
initial = kwargs.get('initial', {})
|
||||
|
||||
choices = []
|
||||
if self.instance and self.instance.pk and 'itemvar' not in initial:
|
||||
if self.instance.variation is not None:
|
||||
initial['itemvar'] = f'{self.instance.item.pk}-{self.instance.variation.pk}'
|
||||
if self.instance.variation.active is False:
|
||||
choices.append((initial['itemvar'], str(self.instance.variation)))
|
||||
else:
|
||||
initial['itemvar'] = self.instance.item.pk
|
||||
if self.instance.item.active is False:
|
||||
choices.append((initial['itemvar'], str(self.instance)))
|
||||
|
||||
kwargs['initial'] = initial
|
||||
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if self.event.has_subevents:
|
||||
@@ -72,73 +45,12 @@ class WaitingListEntryEditForm(I18nModelForm):
|
||||
}
|
||||
)
|
||||
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
|
||||
else:
|
||||
del self.fields['subevent']
|
||||
|
||||
if self.event.settings.waiting_list_names_asked:
|
||||
self.fields['name_parts'] = NamePartsFormField(
|
||||
max_length=255,
|
||||
required=self.event.settings.waiting_list_names_required,
|
||||
scheme=self.event.organizer.settings.name_scheme,
|
||||
titles=self.event.organizer.settings.name_scheme_titles,
|
||||
label=_('Name'),
|
||||
)
|
||||
else:
|
||||
del self.fields['name_parts']
|
||||
|
||||
if not self.event.settings.waiting_list_phones_asked:
|
||||
del self.fields['phone']
|
||||
|
||||
items = self.event.items.filter(active=True).prefetch_related(
|
||||
'variations'
|
||||
)
|
||||
|
||||
for item in items:
|
||||
if len(item.variations.all()) > 0:
|
||||
for variation in item.variations.all():
|
||||
if variation.active:
|
||||
choices.append(
|
||||
('{}-{}'.format(item.pk, variation.pk), '{} - {}'.format(str(item), str(variation)))
|
||||
)
|
||||
else:
|
||||
choices.append(('{}'.format(item.pk), str(item)))
|
||||
|
||||
self.fields['itemvar'].label = _("Product")
|
||||
self.fields['itemvar'].help_text = _("Only includes active products.")
|
||||
self.fields['itemvar'].required = True
|
||||
self.fields['itemvar'].choices = choices
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
|
||||
if self.instance.voucher is not None:
|
||||
raise forms.ValidationError(_('A voucher for this waiting list entry was already sent out.'))
|
||||
|
||||
itemvar = cleaned_data.get('itemvar')
|
||||
if itemvar:
|
||||
self.instance.item = self.event.items.get(pk=itemvar.split('-')[0])
|
||||
if '-' in itemvar:
|
||||
self.instance.variation = self.instance.item.variations.get(pk=itemvar.split('-')[1])
|
||||
|
||||
if ((self.instance.item and not self.instance.item.active) or
|
||||
(self.instance.variation and not self.instance.variation.active)):
|
||||
self.add_error('itemvar', _('The selected product is not active.'))
|
||||
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = WaitingListEntry
|
||||
fields = [
|
||||
'email',
|
||||
'name_parts',
|
||||
'phone',
|
||||
'subevent',
|
||||
]
|
||||
field_classes = {
|
||||
'subevent': SafeModelChoiceField,
|
||||
'email': forms.EmailField,
|
||||
'phone': PhoneNumberField,
|
||||
}
|
||||
widgets = {
|
||||
'phone': WrappedPhoneNumberPrefixWidget,
|
||||
}
|
||||
|
||||
@@ -19,14 +19,6 @@
|
||||
</ul>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% if possible_cookie_problem %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
It looks like your browser is not accepting our cookie and you need to log in repeatedly. Please
|
||||
check if your browser is set to block cookies, or delete all existing cookies and retry.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
|
||||
@@ -144,23 +144,14 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p>
|
||||
{% 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 %}
|
||||
{% 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." %}
|
||||
</p>
|
||||
{% if static_tokens_device %}
|
||||
<p>
|
||||
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
|
||||
You generated your emergency tokens on {{ generation_date_time }}.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% else %}
|
||||
<p>
|
||||
{% trans "You don't have any emergency tokens yet." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
<p>{% trans "Unused tokens:" %}</p>
|
||||
<ul>
|
||||
{% for t in static_tokens %}
|
||||
<li><code>{{ t.token }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
|
||||
<span class="fa fa-refresh"></span>
|
||||
{% trans "Generate new emergency tokens" %}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Edit entry" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Edit entry" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.subevent %}
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
{% bootstrap_field form.email layout="control" %}
|
||||
|
||||
{% if form.name_parts %}
|
||||
{% bootstrap_field form.name_parts layout="control" %}
|
||||
{% endif %}
|
||||
|
||||
{% if form.phone %}
|
||||
{% bootstrap_field form.phone layout="control" %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.itemvar layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -124,7 +124,6 @@
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input name="search" type="text" placeholder="{% trans "Search" %}" class="form-control" value="{{ request.GET.search }}">
|
||||
{% if request.event.has_subevents %}
|
||||
<select name="subevent" class="form-control">
|
||||
<option value="">{% trans "All dates" context "subevent" %}</option>
|
||||
@@ -268,13 +267,13 @@
|
||||
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
|
||||
<span class="fa fa-thumbs-down"></span>
|
||||
</button>
|
||||
|
||||
<a href="{% url "control:event.orders.waitinglist.edit" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
|
||||
class="btn btn-default btn-sm" title="{% trans "Edit entry" %}"
|
||||
data-toggle="tooltip">
|
||||
<i class="fa fa-edit" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
{% if request.event.has_subevents %}
|
||||
<a href="{% url "control:event.orders.waitinglist.transfer" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
|
||||
class="btn btn-default btn-sm" title="{% trans "Transfer to other date" context "subevent" %}"
|
||||
data-toggle="tooltip">
|
||||
<i class="fa fa-calendar" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% else %}
|
||||
<button class="btn btn-default btn-sm disabled">
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Transfer entry" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>{% trans "Transfer entry" %}</h1>
|
||||
<form action="" method="post" class="form-horizontal">
|
||||
{% csrf_token %}
|
||||
<p>{% blocktrans trimmed context "subevent" %}
|
||||
Please select the date to which the following waiting list entry should be
|
||||
transferred: <strong>{{ entry }}</strong>?
|
||||
{% endblocktrans %}</p>
|
||||
{% bootstrap_field form.subevent layout="control" %}
|
||||
<div class="form-group submit-group">
|
||||
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Transfer" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -480,8 +480,8 @@ urlpatterns = [
|
||||
re_path(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
|
||||
re_path(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
|
||||
name='event.orders.waitinglist.delete'),
|
||||
re_path(r'^waitinglist/(?P<entry>\d+)/edit$', waitinglist.EntryEdit.as_view(),
|
||||
name='event.orders.waitinglist.edit'),
|
||||
re_path(r'^waitinglist/(?P<entry>\d+)/transfer$', waitinglist.EntryTransfer.as_view(),
|
||||
name='event.orders.waitinglist.transfer'),
|
||||
re_path(r'^checkins/$', checkin.CheckinListView.as_view(), name='event.orders.checkins'),
|
||||
re_path(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
|
||||
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),
|
||||
|
||||
@@ -149,8 +149,6 @@ def login(request):
|
||||
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
|
||||
else:
|
||||
form = LoginForm(backend=backend, request=request)
|
||||
# Detect redirection loop (usually means cookie not accepted)
|
||||
ctx['possible_cookie_problem'] = request.path in request.headers.get("Referer", "")
|
||||
ctx['form'] = form
|
||||
ctx['can_register'] = settings.PRETIX_REGISTRATION
|
||||
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET
|
||||
|
||||
@@ -870,15 +870,11 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
msgs[self.supported_locale[idx]] = format_html(
|
||||
'<div class="alert alert-danger">{}</div>',
|
||||
PlaceholderValidator.error_message
|
||||
)
|
||||
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||
PlaceholderValidator.error_message)
|
||||
except KeyError as e:
|
||||
msgs[self.supported_locale[idx]] = format_html(
|
||||
'<div class="alert alert-danger">{}</div>',
|
||||
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]}
|
||||
)
|
||||
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
|
||||
|
||||
return JsonResponse({
|
||||
'item': preview_item,
|
||||
|
||||
@@ -1850,8 +1850,7 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
data={
|
||||
'value': value,
|
||||
'text': request.POST.get('text'),
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
'acceptor_id': self.request.organizer.id
|
||||
},
|
||||
user=self.request.user,
|
||||
)
|
||||
@@ -1914,8 +1913,7 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
user=self.request.user,
|
||||
data={
|
||||
'value': form.cleaned_data['value'],
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
'acceptor_id': self.request.organizer.id
|
||||
}
|
||||
)
|
||||
return redirect(reverse(
|
||||
|
||||
@@ -49,14 +49,12 @@ from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
from django.urls import reverse
|
||||
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.html import format_html
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.decorators.cache import never_cache
|
||||
from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
@@ -87,9 +85,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecentAuthenticationRequiredMixin:
|
||||
max_time = 900
|
||||
max_time = 3600
|
||||
|
||||
@method_decorator(never_cache)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
|
||||
if tdelta > self.max_time:
|
||||
@@ -292,13 +289,16 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
ctx = super().get_context_data()
|
||||
|
||||
try:
|
||||
ctx['static_tokens_device'] = StaticDevice.objects.get(user=self.request.user, name='emergency')
|
||||
ctx['static_tokens'] = StaticDevice.objects.get(user=self.request.user, name='emergency').token_set.all()
|
||||
except StaticDevice.MultipleObjectsReturned:
|
||||
ctx['static_tokens_device'] = StaticDevice.objects.filter(
|
||||
ctx['static_tokens'] = StaticDevice.objects.filter(
|
||||
user=self.request.user, name='emergency'
|
||||
).first()
|
||||
).first().token_set.all()
|
||||
except StaticDevice.DoesNotExist:
|
||||
ctx['static_tokens_device'] = None
|
||||
d = StaticDevice.objects.create(user=self.request.user, name='emergency')
|
||||
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'] = []
|
||||
for dt in REAL_DEVICE_TYPES:
|
||||
@@ -630,14 +630,8 @@ class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, Template
|
||||
])
|
||||
self.request.user.update_session_token()
|
||||
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 '
|
||||
'place in case you lose access to your devices. You will not be able to view them '
|
||||
'again here.\n\nYour emergency codes:\n{tokens}').format(
|
||||
tokens='- ' + '\n- '.join(t.token for t in d.token_set.all())
|
||||
)
|
||||
)
|
||||
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.'))
|
||||
return redirect(reverse('control:user.settings.2fa'))
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ from pretix.base.models import Item, LogEntry, Quota, WaitingListEntry
|
||||
from pretix.base.models.waitinglist import WaitingListException
|
||||
from pretix.base.services.waitinglist import assign_automatically
|
||||
from pretix.base.views.tasks import AsyncAction
|
||||
from pretix.control.forms.waitinglist import WaitingListEntryEditForm
|
||||
from pretix.control.forms.waitinglist import WaitingListEntryTransferForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import PaginationMixin
|
||||
|
||||
@@ -138,17 +138,6 @@ class WaitingListQuerySetMixin:
|
||||
elif force_filtered and '__ALL' not in self.request_data:
|
||||
qs = qs.none()
|
||||
|
||||
if self.request_data.get("search", "") != "":
|
||||
s = self.request_data.get("search", "")
|
||||
search_q = Q(email__icontains=s)
|
||||
|
||||
if self.request.event.settings.waiting_list_names_asked:
|
||||
search_q = search_q | Q(name_cached__icontains=s)
|
||||
if self.request.event.settings.waiting_list_phones_asked:
|
||||
search_q = search_q | Q(phone__icontains=s)
|
||||
|
||||
qs = qs.filter(search_q)
|
||||
|
||||
return qs
|
||||
|
||||
|
||||
@@ -249,7 +238,7 @@ class WaitingListView(EventPermissionRequiredMixin, WaitingListQuerySetMixin, Pa
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['items'] = Item.objects.filter(event=self.request.event)
|
||||
ctx['filtered'] = any(param in self.request.GET for param in ("status", "item", "search"))
|
||||
ctx['filtered'] = ("status" in self.request.GET or "item" in self.request.GET)
|
||||
|
||||
itemvar_cache = {}
|
||||
quota_cache = {}
|
||||
@@ -401,20 +390,25 @@ class EntryDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
})
|
||||
|
||||
|
||||
class EntryEdit(EventPermissionRequiredMixin, UpdateView):
|
||||
class EntryTransfer(EventPermissionRequiredMixin, UpdateView):
|
||||
model = WaitingListEntry
|
||||
template_name = 'pretixcontrol/waitinglist/edit.html'
|
||||
template_name = 'pretixcontrol/waitinglist/transfer.html'
|
||||
permission = 'can_change_orders'
|
||||
form_class = WaitingListEntryEditForm
|
||||
form_class = WaitingListEntryTransferForm
|
||||
context_object_name = 'entry'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not self.request.event.has_subevents:
|
||||
raise Http404(_("This is not an event series."))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_object(self, queryset=None) -> WaitingListEntry:
|
||||
return get_object_or_404(WaitingListEntry, pk=self.kwargs['entry'], event=self.request.event, voucher__isnull=True)
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, _('The waitinglist entry has been transferred.'))
|
||||
if form.has_changed():
|
||||
messages.success(self.request, _('The waitinglist entry has been changed.'))
|
||||
self.object.log_action(
|
||||
'pretix.event.orders.waitinglist.changed', user=self.request.user, data={
|
||||
k: form.cleaned_data.get(k) for k in form.changed_data
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
import logging
|
||||
from string import Formatter
|
||||
|
||||
from django.core.exceptions import SuspiciousOperation
|
||||
from django.utils.html import conditional_escape
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -38,17 +37,6 @@ class PlainHtmlAlternativeString:
|
||||
return f"PlainHtmlAlternativeString('{self.plain}', '{self.html}')"
|
||||
|
||||
|
||||
class FormattedString(str):
|
||||
"""
|
||||
A str subclass that has been specifically marked as "already formatted" for email rendering
|
||||
purposes to avoid duplicate formatting.
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def __str__(self):
|
||||
return self
|
||||
|
||||
|
||||
class SafeFormatter(Formatter):
|
||||
"""
|
||||
Customized version of ``str.format`` that (a) behaves just like ``str.format_map`` and
|
||||
@@ -89,19 +77,8 @@ class SafeFormatter(Formatter):
|
||||
# Ignore format_spec
|
||||
return super().format_field(self._prepare_value(value), '')
|
||||
|
||||
def convert_field(self, value, conversion):
|
||||
# Ignore any conversions
|
||||
if conversion is None:
|
||||
return value
|
||||
else:
|
||||
return str(value)
|
||||
|
||||
|
||||
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None) -> FormattedString:
|
||||
if isinstance(template, FormattedString):
|
||||
raise SuspiciousOperation("Calling format_map() on an already formatted string is likely unsafe.")
|
||||
def format_map(template, context, raise_on_missing=False, mode=SafeFormatter.MODE_RICH_TO_PLAIN, linkifier=None):
|
||||
if not isinstance(template, str):
|
||||
template = str(template)
|
||||
return FormattedString(
|
||||
SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
|
||||
)
|
||||
return SafeFormatter(context, raise_on_missing, mode=mode, linkifier=linkifier).format(template)
|
||||
|
||||
+2262
-2556
File diff suppressed because it is too large
Load Diff
+2270
-2735
File diff suppressed because it is too large
Load Diff
+2262
-2556
File diff suppressed because it is too large
Load Diff
+2273
-2657
File diff suppressed because it is too large
Load Diff
+2260
-2684
File diff suppressed because it is too large
Load Diff
+2262
-2576
File diff suppressed because it is too large
Load Diff
+2278
-2666
File diff suppressed because it is too large
Load Diff
+2268
-2661
File diff suppressed because it is too large
Load Diff
@@ -265,7 +265,6 @@ Objekt-IDs
|
||||
Offline-Scan
|
||||
OK
|
||||
Online-Banking
|
||||
Online-Banking-Nutzer
|
||||
Onlinebanking
|
||||
Onlinebanking-Zugangsdaten
|
||||
Open
|
||||
@@ -540,8 +539,6 @@ WeChat-Zahlung
|
||||
Weiterleitungs-URIs
|
||||
Weiterleitungs-URL
|
||||
Weiterleitungs-URLs
|
||||
WERO
|
||||
WERO-App
|
||||
WhatsApp
|
||||
Widget
|
||||
Widget-Code
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -265,7 +265,6 @@ Objekt-IDs
|
||||
Offline-Scan
|
||||
OK
|
||||
Online-Banking
|
||||
Online-Banking-Nutzer
|
||||
Onlinebanking
|
||||
Onlinebanking-Zugangsdaten
|
||||
Open
|
||||
@@ -540,8 +539,6 @@ WeChat-Zahlung
|
||||
Weiterleitungs-URIs
|
||||
Weiterleitungs-URL
|
||||
Weiterleitungs-URLs
|
||||
WERO
|
||||
WERO-App
|
||||
WhatsApp
|
||||
Widget
|
||||
Widget-Code
|
||||
|
||||
+2262
-2556
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
|
||||
"POT-Creation-Date: 2026-01-26 13:20+0000\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
|
||||
+2282
-2702
File diff suppressed because it is too large
Load Diff
+2262
-2556
File diff suppressed because it is too large
Load Diff
+2270
-2682
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2262
-2558
File diff suppressed because it is too large
Load Diff
+2256
-2643
File diff suppressed because it is too large
Load Diff
+2258
-2684
File diff suppressed because it is too large
Load Diff
+2264
-2573
File diff suppressed because it is too large
Load Diff
+2272
-2687
File diff suppressed because it is too large
Load Diff
+2283
-2708
File diff suppressed because it is too large
Load Diff
+2256
-2721
File diff suppressed because it is too large
Load Diff
+2254
-2662
File diff suppressed because it is too large
Load Diff
+2263
-2631
File diff suppressed because it is too large
Load Diff
+2259
-2716
File diff suppressed because it is too large
Load Diff
+2257
-2629
File diff suppressed because it is too large
Load Diff
+2485
-2903
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,8 @@ msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
|
||||
"PO-Revision-Date: 2026-02-23 10:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
|
||||
"PO-Revision-Date: 2026-02-06 07:41+0000\n"
|
||||
"Last-Translator: Ryo Tagami <rtagami@airstrip.jp>\n"
|
||||
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix-"
|
||||
"js/ja/>\n"
|
||||
"Language: ja\n"
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=1; plural=0;\n"
|
||||
"X-Generator: Weblate 5.16\n"
|
||||
"X-Generator: Weblate 5.15.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -256,7 +256,7 @@ msgstr "承認保留中"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:48
|
||||
msgid "Redeemed"
|
||||
msgstr "引き換え済み"
|
||||
msgstr "使用済"
|
||||
|
||||
#: pretix/plugins/webcheckin/static/pretixplugins/webcheckin/main.js:49
|
||||
msgid "Cancel"
|
||||
@@ -456,11 +456,11 @@ msgstr "="
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:99
|
||||
msgid "Product"
|
||||
msgstr "製品"
|
||||
msgstr "商品"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:103
|
||||
msgid "Product variation"
|
||||
msgstr "製品バリエーション"
|
||||
msgstr "商品バリエーション"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/checkinrules.js:107
|
||||
msgid "Gate"
|
||||
@@ -726,8 +726,8 @@ msgid ""
|
||||
"The items in your cart are no longer reserved for you. You can still "
|
||||
"complete your order as long as they’re available."
|
||||
msgstr ""
|
||||
"カート内のアイテムの予約が解除されました。在庫がある限り、引き続き注文を完了"
|
||||
"できます。"
|
||||
"カート内の商品の確保期限が切れました。在庫があれば、このまま注文を完了するこ"
|
||||
"とができます。"
|
||||
|
||||
#: pretix/static/pretixpresale/js/ui/cart.js:49
|
||||
msgid "Cart expired"
|
||||
|
||||
+2257
-2639
File diff suppressed because it is too large
Load Diff
+2252
-2624
File diff suppressed because it is too large
Load Diff
+2262
-2558
File diff suppressed because it is too large
Load Diff
+2260
-2648
File diff suppressed because it is too large
Load Diff
+2262
-2556
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2304
-2719
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,7 @@ msgstr ""
|
||||
"Project-Id-Version: 1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
|
||||
"PO-Revision-Date: 2026-02-19 22:00+0000\n"
|
||||
"PO-Revision-Date: 2026-02-05 23:00+0000\n"
|
||||
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
|
||||
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix-js/"
|
||||
"nl/>\n"
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"Plural-Forms: nplurals=2; plural=n != 1;\n"
|
||||
"X-Generator: Weblate 5.16\n"
|
||||
"X-Generator: Weblate 5.15.2\n"
|
||||
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
|
||||
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
|
||||
@@ -674,7 +674,7 @@ msgstr "Zoekopdracht"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:461
|
||||
msgid "All"
|
||||
msgstr "Allemaal"
|
||||
msgstr "Alle"
|
||||
|
||||
#: pretix/static/pretixcontrol/js/ui/main.js:462
|
||||
msgid "None"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2261
-2727
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2266
-2599
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2273
-2732
File diff suppressed because it is too large
Load Diff
+2265
-2648
File diff suppressed because it is too large
Load Diff
+2263
-2570
File diff suppressed because it is too large
Load Diff
+2258
-2704
File diff suppressed because it is too large
Load Diff
+2262
-2623
File diff suppressed because it is too large
Load Diff
+2266
-2570
File diff suppressed because it is too large
Load Diff
+2261
-2723
File diff suppressed because it is too large
Load Diff
+2936
-3242
File diff suppressed because it is too large
Load Diff
+2281
-2696
File diff suppressed because it is too large
Load Diff
+2261
-2704
File diff suppressed because it is too large
Load Diff
+2256
-2688
File diff suppressed because it is too large
Load Diff
+2262
-2556
File diff suppressed because it is too large
Load Diff
@@ -187,7 +187,6 @@ webhooks
|
||||
webserver
|
||||
Wechat
|
||||
WeChat
|
||||
WERO
|
||||
WhatsApp
|
||||
whitespace
|
||||
xlsx
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -21,10 +21,10 @@
|
||||
<dt>{% trans "Reference code (important):" %}</dt><dd><b>{{ code }}</b></dd>
|
||||
<dt>{% trans "Amount:" %}</dt><dd>{{ amount|money:event.currency }}</dd>
|
||||
{% if settings.bank_details_type == "sepa" %}
|
||||
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dd>
|
||||
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dd>
|
||||
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dd>
|
||||
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dd>
|
||||
<dt>{% trans "Account holder" %}:</dt><dd>{{ settings.bank_details_sepa_name }}</dt>
|
||||
<dt>{% trans "IBAN" %}:</dt><dd>{{ settings.bank_details_sepa_iban|ibanformat }}</dt>
|
||||
<dt>{% trans "BIC" %}:</dt><dd>{{ settings.bank_details_sepa_bic }}</dt>
|
||||
<dt>{% trans "Bank" %}:</dt><dd>{{ settings.bank_details_sepa_bank }}</dt>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% if details %}
|
||||
@@ -38,4 +38,4 @@
|
||||
{% if payment_qr_codes %}
|
||||
{% include "pretixpresale/event/payment_qr_codes.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -786,12 +786,7 @@ class PaypalMethod(BasePaymentProvider):
|
||||
else:
|
||||
pp_captured_order = response.result
|
||||
|
||||
payment.refresh_from_db()
|
||||
|
||||
any_captures = False
|
||||
all_captures_completed = True
|
||||
for purchaseunit in pp_captured_order.purchase_units:
|
||||
if hasattr(purchaseunit, 'payments'):
|
||||
for purchaseunit in pp_captured_order.purchase_units:
|
||||
for capture in purchaseunit.payments.captures:
|
||||
try:
|
||||
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=capture.id)
|
||||
@@ -799,16 +794,14 @@ class PaypalMethod(BasePaymentProvider):
|
||||
pass
|
||||
|
||||
if capture.status != 'COMPLETED':
|
||||
all_captures_completed = False
|
||||
else:
|
||||
any_captures = True
|
||||
if not (any_captures and all_captures_completed):
|
||||
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
|
||||
'soon as the payment completed.'))
|
||||
payment.info = json.dumps(pp_captured_order.dict())
|
||||
payment.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment.save()
|
||||
return
|
||||
messages.warning(request, _('PayPal has not yet approved the payment. We will inform you as '
|
||||
'soon as the payment completed.'))
|
||||
payment.info = json.dumps(pp_captured_order.dict())
|
||||
payment.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment.save()
|
||||
return
|
||||
|
||||
payment.refresh_from_db()
|
||||
|
||||
if pp_captured_order.status != 'COMPLETED':
|
||||
payment.fail(info=pp_captured_order.dict())
|
||||
|
||||
@@ -118,7 +118,6 @@ logger = logging.getLogger('pretix.plugins.stripe')
|
||||
# - UPI: ✗
|
||||
# - Netbanking: ✗
|
||||
# - TWINT: ✓
|
||||
# - Wero: ✓ (No settings UI yet)
|
||||
#
|
||||
# Bank transfers
|
||||
# - ACH Bank Transfer: ✗
|
||||
@@ -510,15 +509,6 @@ class StripeSettingsHolder(BasePaymentProvider):
|
||||
'before they work properly.'),
|
||||
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
|
||||
)
|
||||
if not self.settings.connect_client_id or self.settings.secret_key:
|
||||
@@ -1956,15 +1946,3 @@ class StripeMobilePay(StripeRedirectMethod):
|
||||
"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,
|
||||
StripePrzelewy24, StripeRevolutPay, StripeSEPADirectDebit,
|
||||
StripeSettingsHolder, StripeSofort, StripeSwish, StripeTwint,
|
||||
StripeWeChatPay, StripeWero,
|
||||
StripeWeChatPay,
|
||||
)
|
||||
|
||||
return [
|
||||
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
|
||||
StripeSofort, StripeEPS, StripeMultibanco, StripePayByBank, StripePrzelewy24, StripePromptPay, StripeRevolutPay,
|
||||
StripeWeChatPay, StripeSEPADirectDebit, StripeAffirm, StripeKlarna, StripePayPal, StripeSwish,
|
||||
StripeTwint, StripeMobilePay, StripeWero
|
||||
StripeTwint, StripeMobilePay
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<script type="text/plain" id="stripe_payment_intent_action_type">{{ payment_intent_action_type }}</script>
|
||||
<script type="text/plain" id="stripe_payment_intent_client_secret">{{ payment_intent_client_secret }}</script>
|
||||
{% if payment_intent_next_action_redirect_url %}
|
||||
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url|safe }}</script>
|
||||
<script type="text/plain" id="stripe_payment_intent_next_action_redirect_url">{{ payment_intent_next_action_redirect_url }}</script>
|
||||
{% endif %}
|
||||
{% if payment_intent_redirect_action_handling %}
|
||||
<script type="text/plain" id="stripe_payment_intent_redirect_action_handling">{{ payment_intent_redirect_action_handling }}</script>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
{% extends "pretixpresale/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block title %}{% trans "Resend order link" %}{% endblock %}
|
||||
{% block title %}{% trans "Resend order links" %}{% endblock %}
|
||||
{% block custom_header %}
|
||||
{{ block.super }}
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<h2>
|
||||
{% trans "Resend order link" %}
|
||||
{% trans "Resend order links" %}
|
||||
</h2>
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
|
||||
@@ -393,6 +393,7 @@ class TokenView(View):
|
||||
|
||||
if grant.code_challenge_method == "S256":
|
||||
expected_challenge = base64.urlsafe_b64encode(hashlib.sha256(request.POST["code_verifier"].encode()).digest()).decode().rstrip("=")
|
||||
print(grant.code_challenge, expected_challenge)
|
||||
if expected_challenge != grant.code_challenge:
|
||||
return JsonResponse({
|
||||
"error": "invalid_grant",
|
||||
|
||||
@@ -1510,10 +1510,7 @@ class OrderChangeMixin:
|
||||
'max_count': iao.max_count,
|
||||
'iao': iao,
|
||||
'items': [i for i in items if not i.require_voucher],
|
||||
'items_missing': {
|
||||
k: v for k, v in current_addon_products_missing.items()
|
||||
if v and k[0].category_id == iao.addon_category_id
|
||||
},
|
||||
'items_missing': {k: v for k, v in current_addon_products_missing.items() if v},
|
||||
})
|
||||
|
||||
return positions
|
||||
|
||||
@@ -211,10 +211,6 @@ USE_X_FORWARDED_HOST = config.getboolean('pretix', 'trust_x_forwarded_host', fal
|
||||
|
||||
|
||||
REQUEST_ID_HEADER = config.get('pretix', 'request_id_header', fallback=False)
|
||||
if REQUEST_ID_HEADER in config.cp.BOOLEAN_STATES:
|
||||
raise ImproperlyConfigured(
|
||||
"request_id_header should be set to a header name, not a boolean value."
|
||||
)
|
||||
|
||||
if config.getboolean('pretix', 'trust_x_forwarded_proto', fallback=False):
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
@@ -252,7 +252,7 @@ Vue.component('availbox', {
|
||||
variation: Object
|
||||
},
|
||||
mounted: function() {
|
||||
if (!this.$root.cart_exists && this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
|
||||
if (this.$root.itemnum === 1 && (!this.$root.categories[0].items[0].has_variations || this.$root.categories[0].items[0].variations.length < 2) && !this.$root.has_seating_plan ? 1 : 0) {
|
||||
this.$refs.quantity.value = 1;
|
||||
if (this.order_max === 1) {
|
||||
this.$refs.quantity.checked = true;
|
||||
|
||||
@@ -1177,30 +1177,6 @@ def test_store_failed(token_client, organizer, clist, event, order):
|
||||
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
|
||||
def test_redeem_unknown(token_client, organizer, clist, event, order):
|
||||
resp = _redeem(token_client, organizer, clist, 'unknown_secret', {'force': True})
|
||||
|
||||
+22
-207
@@ -32,26 +32,21 @@
|
||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations under the License.
|
||||
|
||||
import datetime
|
||||
import os
|
||||
import re
|
||||
from decimal import Decimal
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.core import mail as djmail
|
||||
from django.test import override_settings
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_scopes import scope, scopes_disabled
|
||||
from django_scopes import scope
|
||||
from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.models import (
|
||||
Event, InvoiceAddress, Order, Organizer, OutgoingMail, User,
|
||||
)
|
||||
from pretix.base.models import Event, Organizer, OutgoingMail, User
|
||||
from pretix.base.services.mail import mail, mail_send_task
|
||||
|
||||
|
||||
@@ -73,45 +68,6 @@ def env():
|
||||
yield event, user, o
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@scopes_disabled()
|
||||
def item(env):
|
||||
return env[0].items.create(name="Budget Ticket", default_price=23)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@scopes_disabled()
|
||||
def order(env, item):
|
||||
event, _, _ = env
|
||||
o = Order.objects.create(
|
||||
code="FOO",
|
||||
event=event,
|
||||
email="dummy@dummy.test",
|
||||
status=Order.STATUS_PENDING,
|
||||
secret="k24fiuwvu8kxz3y1",
|
||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||
datetime=datetime.datetime(2017, 12, 1, 10, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
expires=datetime.datetime(2017, 12, 10, 10, 0, 0, tzinfo=datetime.timezone.utc),
|
||||
total=23,
|
||||
locale="en",
|
||||
)
|
||||
o.positions.create(
|
||||
order=o,
|
||||
item=item,
|
||||
variation=None,
|
||||
price=Decimal("23"),
|
||||
attendee_email="peter@example.org",
|
||||
attendee_name_parts={"given_name": "Peter", "family_name": "Miller"},
|
||||
secret="z3fsn8jyufm5kpk768q69gkbyr5f4h6w",
|
||||
pseudonymization_id="ABCDEFGHKL",
|
||||
)
|
||||
InvoiceAddress.objects.create(
|
||||
order=o,
|
||||
name_parts={"given_name": "Peter", "family_name": "Miller"},
|
||||
)
|
||||
return o
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_send_mail_with_prefix(env):
|
||||
djmail.outbox = []
|
||||
@@ -323,7 +279,7 @@ def _extract_html(mail):
|
||||
def test_placeholder_html_rendering_from_template(env):
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "<strong>event & co. kg</strong> {currency}"
|
||||
event.name = "<strong>event & co. kg</strong>"
|
||||
event.save()
|
||||
mail('dummy@dummy.dummy', '{event} Test subject', 'mailtest.txt', get_email_context(
|
||||
event=event,
|
||||
@@ -332,26 +288,25 @@ def test_placeholder_html_rendering_from_template(env):
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
# Known bug for now: These should not have HTML for the plain body, but we'll fix this safter the security release
|
||||
assert escape('Event name: <strong>event & co. kg</strong> {currency}') in djmail.outbox[0].body
|
||||
assert '<strong>IBAN</strong>: 123<br>\n<strong>BIC</strong>: 456' in djmail.outbox[0].body
|
||||
assert '**Meta**: <em>Beep</em>' in djmail.outbox[0].body
|
||||
assert escape('Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)') in djmail.outbox[0].body
|
||||
# todo: assert '<' not in djmail.outbox[0].body
|
||||
# todo: assert '&' not in djmail.outbox[0].body
|
||||
assert 'Unevaluated placeholder: {currency}' in djmail.outbox[0].body
|
||||
assert 'EUR' not in djmail.outbox[0].body
|
||||
assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body
|
||||
assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
|
||||
assert '**Meta**: *Beep*' in djmail.outbox[0].body
|
||||
assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body
|
||||
assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body
|
||||
assert '<' not in djmail.outbox[0].body
|
||||
assert '&' not in djmail.outbox[0].body
|
||||
html = _extract_html(djmail.outbox[0])
|
||||
|
||||
assert '<strong>event' not in html
|
||||
assert 'Event name: <strong>event & co. kg</strong> {currency}' in html
|
||||
assert 'Event name: <strong>event & co. kg</strong>' in html
|
||||
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
|
||||
assert '<strong>Meta</strong>: <em>Beep</em>' in html
|
||||
assert 'Unevaluated placeholder: {currency}' in html
|
||||
assert 'EUR' not in html
|
||||
assert re.search(
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
|
||||
r'<strong>event & co. kg</strong> {currency}</a>',
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
html
|
||||
)
|
||||
|
||||
@@ -369,7 +324,7 @@ def test_placeholder_html_rendering_from_string(env):
|
||||
})
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "<strong>event & co. kg</strong> {currency}"
|
||||
event.name = "<strong>event & co. kg</strong>"
|
||||
event.save()
|
||||
ctx = get_email_context(
|
||||
event=event,
|
||||
@@ -380,9 +335,9 @@ def test_placeholder_html_rendering_from_string(env):
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
assert 'Event name: <strong>event & co. kg</strong> {currency}' in djmail.outbox[0].body
|
||||
assert 'Event website: [<strong>event & co. kg</strong> {currency}](https://example.org/dummy)' in djmail.outbox[0].body
|
||||
assert 'Other website: [<strong>event & co. kg</strong> {currency}](https://example.com)' in djmail.outbox[0].body
|
||||
assert 'Event name: <strong>event & co. kg</strong>' in djmail.outbox[0].body
|
||||
assert 'Event website: [<strong>event & co. kg</strong>](https://example.org/dummy)' in djmail.outbox[0].body
|
||||
assert 'Other website: [<strong>event & co. kg</strong>](https://example.com)' in djmail.outbox[0].body
|
||||
assert '**IBAN**: 123 \n**BIC**: 456' in djmail.outbox[0].body
|
||||
assert '**Meta**: *Beep*' in djmail.outbox[0].body
|
||||
assert 'URL: https://google.com' in djmail.outbox[0].body
|
||||
@@ -397,13 +352,11 @@ def test_placeholder_html_rendering_from_string(env):
|
||||
assert '<strong>IBAN</strong>: 123<br/>\n<strong>BIC</strong>: 456' in html
|
||||
assert '<strong>Meta</strong>: <em>Beep</em>' in html
|
||||
assert re.search(
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank">'
|
||||
r'<strong>event & co. kg</strong> {currency}</a>',
|
||||
r'Event website: <a href="https://example.org/dummy" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank">'
|
||||
r'<strong>event & co. kg</strong> {currency}</a>',
|
||||
r'Other website: <a href="https://example.com" rel="noopener" style="[^"]+" target="_blank"><strong>event & co. kg</strong></a>',
|
||||
html
|
||||
)
|
||||
assert re.search(
|
||||
@@ -424,141 +377,3 @@ def test_placeholder_html_rendering_from_string(env):
|
||||
r'style="[^"]+" target="_blank">Link & Text</a>',
|
||||
html
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_nested_placeholder_inclusion_full_process(env, order):
|
||||
# Test that it is not possible to sneak in a placeholder like {url_cancel} inside a user-controlled
|
||||
# placeholder value like {invoice_company}
|
||||
event, user, organizer = env
|
||||
position = order.positions.get()
|
||||
order.invoice_address.company = "{url_cancel} Corp"
|
||||
order.invoice_address.save()
|
||||
event.settings.mail_text_resend_link = LazyI18nString({"en": "Ticket for {invoice_company}"})
|
||||
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": "Ticket for {invoice_company}"})
|
||||
|
||||
djmail.outbox = []
|
||||
position.resend_link()
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [position.attendee_email]
|
||||
assert "Ticket for {url_cancel} Corp" == djmail.outbox[0].subject
|
||||
assert "/cancel" not in djmail.outbox[0].body
|
||||
assert "/order" not in djmail.outbox[0].body
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain):
|
||||
assert "Ticket for {url_cancel} Corp" in part
|
||||
assert "/order/" not in part
|
||||
assert "/cancel" not in part
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_nested_placeholder_inclusion_mail_service(env):
|
||||
# test that it is not possible to have placeholders within the values of placeholders when
|
||||
# the mail() function is called directly
|
||||
template = LazyI18nString("Event name: {event}")
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "event & {currency} co. kg"
|
||||
event.slug = "event-co-ag-slug"
|
||||
event.save()
|
||||
|
||||
mail(
|
||||
"dummy@dummy.dummy",
|
||||
"{event} Test subject",
|
||||
template,
|
||||
get_email_context(
|
||||
event=event,
|
||||
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
|
||||
),
|
||||
event,
|
||||
)
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain, djmail.outbox[0].subject):
|
||||
assert "event & {currency} co. kg" in part or "event & {currency} co. kg" in part
|
||||
assert "EUR" not in part
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("tpl", [
|
||||
"Event: {event.__class__}",
|
||||
"Event: {{event.__class__}}",
|
||||
"Event: {{{event.__class__}}}",
|
||||
])
|
||||
def test_variable_inclusion_from_string_full_process(env, tpl, order):
|
||||
# Test that it is not possible to use placeholders that leak system information in templates
|
||||
# when run through system processes
|
||||
event, user, organizer = env
|
||||
event.name = "event & co. kg"
|
||||
event.save()
|
||||
position = order.positions.get()
|
||||
event.settings.mail_text_resend_link = LazyI18nString({"en": tpl})
|
||||
event.settings.mail_subject_resend_link_attendee = LazyI18nString({"en": tpl})
|
||||
|
||||
position.resend_link()
|
||||
assert len(djmail.outbox) == 1
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain, djmail.outbox[0].subject):
|
||||
assert "{event.__class__}" in part
|
||||
assert "LazyI18nString" not in part
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("tpl", [
|
||||
"Event: {event.__class__}",
|
||||
"Event: {{event.__class__}}",
|
||||
"Event: {{{event.__class__}}}",
|
||||
])
|
||||
def test_variable_inclusion_from_string_mail_service(env, tpl):
|
||||
# Test that it is not possible to use placeholders that leak system information in templates
|
||||
# when run through mail() directly
|
||||
event, user, organizer = env
|
||||
event.name = "event & co. kg"
|
||||
event.save()
|
||||
|
||||
djmail.outbox = []
|
||||
mail(
|
||||
"dummy@dummy.dummy",
|
||||
tpl,
|
||||
LazyI18nString(tpl),
|
||||
get_email_context(
|
||||
event=event,
|
||||
payment_info="**IBAN**: 123 \n**BIC**: 456\n" + tpl,
|
||||
),
|
||||
event,
|
||||
)
|
||||
assert len(djmail.outbox) == 1
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain, djmail.outbox[0].subject):
|
||||
assert "{event.__class__}" in part
|
||||
assert "LazyI18nString" not in part
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escaped_braces_mail_services(env):
|
||||
# Test that braces can be escaped by doubling
|
||||
template = LazyI18nString("Event name: -{{currency}}-")
|
||||
djmail.outbox = []
|
||||
event, user, organizer = env
|
||||
event.name = "event & co. kg"
|
||||
event.save()
|
||||
|
||||
mail(
|
||||
"dummy@dummy.dummy",
|
||||
"-{{currency}}- Test subject",
|
||||
template,
|
||||
get_email_context(
|
||||
event=event,
|
||||
payment_info="**IBAN**: 123 \n**BIC**: 456 {event}",
|
||||
),
|
||||
event,
|
||||
)
|
||||
|
||||
assert len(djmail.outbox) == 1
|
||||
assert djmail.outbox[0].to == [user.email]
|
||||
html, plain = _extract_html(djmail.outbox[0]), djmail.outbox[0].body
|
||||
for part in (html, plain, djmail.outbox[0].subject):
|
||||
assert "EUR" not in part
|
||||
assert "-{currency}-" in part
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user