mirror of
https://github.com/pretix/pretix.git
synced 2026-07-21 07:41:40 +00:00
Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c3c4ca63b | ||
|
|
cb9c4466f9 | ||
|
|
3398cda74b | ||
|
|
e5c8f19984 | ||
|
|
5027f6dd59 | ||
|
|
787db18d72 | ||
|
|
aadce7be00 | ||
|
|
26f296bc11 | ||
|
|
6ae80cdd4b | ||
|
|
cb3956c994 | ||
|
|
b9f350bf3a | ||
|
|
ab447bb85f | ||
|
|
bf33a42ae8 | ||
|
|
081f975ff9 | ||
|
|
eab7d81a51 | ||
|
|
b2dce51a24 | ||
|
|
5bd660a913 | ||
|
|
8e9cdd7548 | ||
|
|
d6592cbb93 | ||
|
|
0e3ccae5d4 | ||
|
|
034b46d218 | ||
|
|
a3f120198d | ||
|
|
fa5f3bb15a | ||
|
|
5120b312b6 | ||
|
|
09064844b2 | ||
|
|
1a60b3a712 | ||
|
|
6216f0d7df | ||
|
|
380b55e699 | ||
|
|
6e67bb5045 | ||
|
|
1463ee9227 | ||
|
|
3b49e77722 | ||
|
|
ceed07af94 | ||
|
|
802c03f8f3 | ||
|
|
9962d8a3be | ||
|
|
028a41f3e4 | ||
|
|
6d8a9854f9 | ||
|
|
861e14bb16 | ||
|
|
7a080c0820 | ||
|
|
2dbdb91066 | ||
|
|
b8efb8f61d | ||
|
|
5f0cc4cc59 | ||
|
|
d3bb1f3190 | ||
|
|
69a215feff | ||
|
|
435dd5ebaf | ||
|
|
015d74f7ae | ||
|
|
5c9a069d77 | ||
|
|
5866cf94ee | ||
|
|
fa15ba4435 | ||
|
|
e982f04d59 | ||
|
|
ced00266dc | ||
|
|
b534c125db | ||
|
|
769e1312d4 | ||
|
|
3d53c03906 | ||
|
|
59d1d2cb16 | ||
|
|
7e45837295 | ||
|
|
fd9ed15065 | ||
|
|
2df3d9206b | ||
|
|
fbd8bbbeaa | ||
|
|
1c305e4b30 | ||
|
|
ea114b4f64 | ||
|
|
0342613635 | ||
|
|
743c4b796b | ||
|
|
8a7f54795e | ||
|
|
cb464ad597 | ||
|
|
119cc50897 | ||
|
|
61f9cf13b4 |
+3
-3
@@ -92,7 +92,7 @@ dependencies = [
|
||||
"redis==7.1.*",
|
||||
"reportlab==4.4.*",
|
||||
"requests==2.32.*",
|
||||
"sentry-sdk==2.52.*",
|
||||
"sentry-sdk==2.53.*",
|
||||
"sepaxml==2.7.*",
|
||||
"stripe==7.9.*",
|
||||
"text-unidecode==1.*",
|
||||
@@ -110,10 +110,10 @@ dev = [
|
||||
"aiohttp==3.13.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.33.*",
|
||||
"fakeredis==2.34.*",
|
||||
"flake8==7.3.*",
|
||||
"freezegun",
|
||||
"isort==7.0.*",
|
||||
"isort==8.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.2.0.dev0"
|
||||
__version__ = "2026.3.0.dev0"
|
||||
|
||||
@@ -188,11 +188,15 @@ 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']).first()
|
||||
prev = kwargs['position'].all_checkins.filter(
|
||||
nonce=serializer.validated_data['nonce'],
|
||||
successful=False
|
||||
).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,7 +259,14 @@ 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})
|
||||
data=merge_dicts(
|
||||
self.request.data,
|
||||
{
|
||||
'id': inst.pk,
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
@transaction.atomic()
|
||||
@@ -290,7 +297,11 @@ 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}
|
||||
data={
|
||||
'value': diff,
|
||||
'acceptor_id': self.request.organizer.id,
|
||||
'acceptor_slug': self.request.organizer.slug
|
||||
}
|
||||
)
|
||||
|
||||
return inst
|
||||
@@ -320,7 +331,8 @@ class GiftCardViewSet(viewsets.ModelViewSet):
|
||||
data={
|
||||
'value': value,
|
||||
'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)
|
||||
|
||||
@@ -183,6 +183,7 @@ 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,
|
||||
}
|
||||
@@ -197,7 +198,9 @@ 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,
|
||||
}
|
||||
@@ -472,7 +475,7 @@ def register_default_webhook_events(sender, **kwargs):
|
||||
),
|
||||
ParametrizedGiftcardTransactionWebhookEvent(
|
||||
'pretix.giftcards.transaction.*',
|
||||
_('Gift card used in transcation'),
|
||||
_('Gift card used in transaction'),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+18
-31
@@ -22,7 +22,7 @@
|
||||
import logging
|
||||
from itertools import groupby
|
||||
from smtplib import SMTPResponseException
|
||||
from typing import TypeVar, Union
|
||||
from typing import TypeVar
|
||||
|
||||
import bleach
|
||||
import css_inline
|
||||
@@ -31,7 +31,6 @@ from django.core.mail.backends.smtp import EmailBackend
|
||||
from django.db.models import Count
|
||||
from django.dispatch import receiver
|
||||
from django.template.loader import get_template
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.translation import get_language, gettext_lazy as _
|
||||
|
||||
from pretix.base.models import Event
|
||||
@@ -40,9 +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, PlainHtmlAlternativeString, SafeFormatter, format_map,
|
||||
)
|
||||
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
|
||||
|
||||
from pretix.base.services.placeholders import ( # noqa
|
||||
get_available_placeholders, PlaceholderContext
|
||||
@@ -86,8 +83,8 @@ class BaseHTMLMailRenderer:
|
||||
def __str__(self):
|
||||
return self.identifier
|
||||
|
||||
def render(self, content: Union[str, FormattedString, PlainHtmlAlternativeString], plain_signature: str,
|
||||
subject: str, order=None, position=None, context=None) -> str:
|
||||
def render(self, plain_body: str, plain_signature: str, subject: str, order=None,
|
||||
position=None, context=None) -> str:
|
||||
"""
|
||||
This method should generate the HTML part of the email.
|
||||
|
||||
@@ -143,37 +140,27 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
|
||||
def compile_markdown(self, plaintext, context=None):
|
||||
return markdown_compile_email(plaintext, context=context)
|
||||
|
||||
def render(self, content: Union[str, FormattedString, PlainHtmlAlternativeString], plain_signature: str,
|
||||
subject: str, order=None, position=None, context=None) -> str:
|
||||
if isinstance(content, FormattedString):
|
||||
# Raw string that is already formatted but not markdown-rendered
|
||||
body_content_html = self.compile_markdown(content, context)
|
||||
|
||||
elif isinstance(content, PlainHtmlAlternativeString):
|
||||
# HTML already rendered by Django templates
|
||||
body_content_html = content.html
|
||||
|
||||
else:
|
||||
# Raw string that is not yet formatted or markdown-rendered
|
||||
body_content_html = self.compile_markdown(content, context)
|
||||
if context:
|
||||
linker = bleach.Linker(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
|
||||
parse_email=True
|
||||
)
|
||||
body_content_html = format_map(
|
||||
body_content_html,
|
||||
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(
|
||||
url_re=URL_RE,
|
||||
email_re=EMAIL_RE,
|
||||
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
|
||||
)
|
||||
|
||||
htmlctx = {
|
||||
'site': settings.PRETIX_INSTANCE_NAME,
|
||||
'site_url': settings.SITE_URL,
|
||||
'body': mark_safe(body_content_html),
|
||||
'body': body_md,
|
||||
'subject': str(subject),
|
||||
'color': settings.PRETIX_PRIMARY_COLOR,
|
||||
'rtl': get_language() in settings.LANGUAGES_RTL or get_language().split('-')[0] in settings.LANGUAGES_RTL,
|
||||
|
||||
@@ -651,6 +651,7 @@ class OrderListExporter(MultiSheetListExporter):
|
||||
pgettext('address', 'State'),
|
||||
_('Voucher'),
|
||||
_('Voucher budget usage'),
|
||||
_('Voucher tag'),
|
||||
_('Pseudonymization ID'),
|
||||
_('Ticket secret'),
|
||||
_('Seat ID'),
|
||||
@@ -769,6 +770,7 @@ 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,
|
||||
]
|
||||
|
||||
@@ -33,7 +33,8 @@ from pretix.base.invoicing.transmission import (
|
||||
transmission_types,
|
||||
)
|
||||
from pretix.base.models import Invoice, InvoiceAddress
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
|
||||
@transmission_types.new()
|
||||
@@ -133,7 +134,9 @@ class EmailTransmissionProvider(TransmissionProvider):
|
||||
subject = invoice.order.event.settings.get('mail_subject_order_invoice', as_type=LazyI18nString)
|
||||
|
||||
# Do not set to completed because that is done by the email sending task
|
||||
outgoing_mail = mail(
|
||||
subject = format_map(subject, context)
|
||||
email_content = render_mail(template, context)
|
||||
mail(
|
||||
[recipient],
|
||||
subject,
|
||||
template,
|
||||
@@ -148,10 +151,19 @@ class EmailTransmissionProvider(TransmissionProvider):
|
||||
plain_text_only=True,
|
||||
no_order_links=True,
|
||||
)
|
||||
if outgoing_mail:
|
||||
invoice.order.log_action(
|
||||
'pretix.event.order.email.invoice',
|
||||
user=None,
|
||||
auth=None,
|
||||
data=outgoing_mail.log_data()
|
||||
)
|
||||
invoice.order.log_action(
|
||||
'pretix.event.order.email.invoice',
|
||||
user=None,
|
||||
auth=None,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': None,
|
||||
'recipient': recipient,
|
||||
'invoices': [invoice.pk],
|
||||
'attach_tickets': False,
|
||||
'attach_ical': False,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
|
||||
|
||||
class PriceModeColumn(ImportColumn):
|
||||
identifier = 'price_mode'
|
||||
verbose_name = gettext_lazy('Price mode')
|
||||
verbose_name = gettext_lazy('Price effect')
|
||||
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 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())
|
||||
))
|
||||
|
||||
@@ -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 mode."))
|
||||
raise ValidationError(_("It is pointless to set a value without a price effect."))
|
||||
return value
|
||||
|
||||
def assign(self, value, obj: Voucher, **kwargs):
|
||||
|
||||
@@ -220,20 +220,3 @@ class OutgoingMail(models.Model):
|
||||
error_log_action_type = 'pretix.email.error'
|
||||
log_target = None
|
||||
return log_target, error_log_action_type
|
||||
|
||||
def log_data(self):
|
||||
return {
|
||||
"subject": self.subject,
|
||||
"message": self.body_plain,
|
||||
"to": self.to,
|
||||
"cc": self.cc,
|
||||
"bcc": self.bcc,
|
||||
|
||||
"invoices": [i.pk for i in self.should_attach_invoices.all()],
|
||||
"attach_tickets": self.should_attach_tickets,
|
||||
"attach_ical": self.should_attach_ical,
|
||||
"attach_other_files": self.should_attach_other_files,
|
||||
"attach_cached_files": [cf.filename for cf in self.should_attach_cached_files.all()],
|
||||
|
||||
"position": self.orderposition.positionid if self.orderposition else None,
|
||||
}
|
||||
|
||||
@@ -87,6 +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.names import build_name
|
||||
from ...testutils.middleware import debugflags_var
|
||||
from ._transactions import (
|
||||
@@ -1166,7 +1167,7 @@ class Order(LockModel, LoggedModel):
|
||||
only be attached for this position and child positions, the link will only point to the
|
||||
position and the attendee email will be used if available.
|
||||
"""
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
|
||||
if not self.email and not (position and position.attendee_email):
|
||||
return
|
||||
@@ -1176,20 +1177,32 @@ class Order(LockModel, LoggedModel):
|
||||
if position and position.attendee_email:
|
||||
recipient = position.attendee_email
|
||||
|
||||
outgoing_mail = mail(
|
||||
email_content = render_mail(template, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.locale, self, headers=headers, sender=sender,
|
||||
invoices=invoices, attach_tickets=attach_tickets,
|
||||
position=position, auto_email=auto_email, attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files, attach_cached_files=attach_cached_files,
|
||||
)
|
||||
if outgoing_mail:
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'position': position.positionid if position else None,
|
||||
'recipient': recipient,
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
|
||||
def resend_link(self, user=None, auth=None):
|
||||
with language(self.locale, self.event.settings.region):
|
||||
@@ -2887,14 +2900,17 @@ class OrderPosition(AbstractPosition):
|
||||
:param attach_tickets: Attach tickets of this order, if they are existing and ready to download
|
||||
:param attach_ical: Attach relevant ICS files
|
||||
"""
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
|
||||
if not self.attendee_email:
|
||||
return
|
||||
|
||||
with language(self.order.locale, self.order.event.settings.region):
|
||||
recipient = self.attendee_email
|
||||
outgoing_mail = mail(
|
||||
email_content = render_mail(template, context)
|
||||
if not isinstance(subject, FormattedString):
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
|
||||
position=self,
|
||||
@@ -2903,13 +2919,21 @@ class OrderPosition(AbstractPosition):
|
||||
attach_ical=attach_ical,
|
||||
attach_other_files=attach_other_files,
|
||||
)
|
||||
if outgoing_mail:
|
||||
self.order.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
self.order.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'invoices': [i.pk for i in invoices] if invoices else [],
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [],
|
||||
}
|
||||
)
|
||||
|
||||
def resend_link(self, user=None, auth=None):
|
||||
|
||||
|
||||
@@ -239,7 +239,7 @@ class Voucher(LoggedModel):
|
||||
)
|
||||
)
|
||||
price_mode = models.CharField(
|
||||
verbose_name=_("Price mode"),
|
||||
verbose_name=_("Price effect"),
|
||||
max_length=100,
|
||||
choices=PRICE_MODES,
|
||||
default='none'
|
||||
|
||||
@@ -34,9 +34,10 @@ from phonenumber_field.modelfields import PhoneNumberField
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import User, Voucher
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.mail import mail, render_mail
|
||||
from pretix.helpers import OF_SELF
|
||||
|
||||
from ...helpers.format import format_map
|
||||
from ...helpers.names import build_name
|
||||
from .base import LoggedModel
|
||||
from .event import Event, SubEvent
|
||||
@@ -271,7 +272,9 @@ class WaitingListEntry(LoggedModel):
|
||||
with language(self.locale, self.event.settings.region):
|
||||
recipient = self.email
|
||||
|
||||
outgoing_mail = mail(
|
||||
email_content = render_mail(template, context)
|
||||
subject = format_map(subject, context)
|
||||
mail(
|
||||
recipient, subject, template, context,
|
||||
self.event,
|
||||
self.locale,
|
||||
@@ -281,13 +284,18 @@ class WaitingListEntry(LoggedModel):
|
||||
attach_other_files=attach_other_files,
|
||||
attach_cached_files=attach_cached_files,
|
||||
)
|
||||
if outgoing_mail:
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
self.log_action(
|
||||
log_entry_type,
|
||||
user=user,
|
||||
auth=auth,
|
||||
data={
|
||||
'subject': subject,
|
||||
'message': email_content,
|
||||
'recipient': recipient,
|
||||
'attach_other_files': attach_other_files,
|
||||
'attach_cached_files': [cf.filename for cf in attach_cached_files] if attach_cached_files else [],
|
||||
}
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def clean_itemvar(event, item, variation):
|
||||
|
||||
@@ -1295,7 +1295,6 @@ class ManualPayment(BasePaymentProvider):
|
||||
|
||||
def format_map(self, order, payment):
|
||||
return {
|
||||
# Possible placeholder injection, we should make sure to never include user-controlled variables here
|
||||
'order': order.code,
|
||||
'amount': payment.amount,
|
||||
'currency': self.event.currency,
|
||||
@@ -1651,7 +1650,8 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
action='pretix.giftcards.transaction.payment',
|
||||
data={
|
||||
'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:
|
||||
@@ -1683,6 +1683,7 @@ class GiftCardPayment(BasePaymentProvider):
|
||||
data={
|
||||
'value': refund.amount,
|
||||
'acceptor_id': self.event.organizer.id,
|
||||
'acceptor_slug': self.event.organizer.slug,
|
||||
'text': refund.comment,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ from pretix.base.services.tax import split_fee_for_taxes
|
||||
from pretix.base.templatetags.money import money_filter
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers import OF_SELF
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,7 +55,7 @@ def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: Lazy
|
||||
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
|
||||
mail(
|
||||
wle.email,
|
||||
str(subject),
|
||||
format_map(subject, email_context),
|
||||
message,
|
||||
email_context,
|
||||
wle.event,
|
||||
@@ -72,8 +73,9 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
|
||||
email_context = get_email_context(event_or_subevent=subevent or order.event, refund_amount=refund_amount,
|
||||
order=order, position_or_address=ia, event=order.event)
|
||||
real_subject = format_map(subject, email_context)
|
||||
order.send_mail(
|
||||
subject, message, email_context,
|
||||
real_subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
user,
|
||||
)
|
||||
@@ -83,13 +85,14 @@ def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, s
|
||||
continue
|
||||
|
||||
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
|
||||
real_subject = format_map(subject, email_context)
|
||||
email_context = get_email_context(event_or_subevent=p.subevent or order.event,
|
||||
event=order.event,
|
||||
refund_amount=refund_amount,
|
||||
position_or_address=p,
|
||||
order=order, position=p)
|
||||
order.send_mail(
|
||||
subject, message, email_context,
|
||||
real_subject, message, email_context,
|
||||
'pretix.event.order.email.event_canceled',
|
||||
position=p,
|
||||
user=user
|
||||
|
||||
@@ -58,7 +58,6 @@ from django.core.mail.message import SafeMIMEText
|
||||
from django.db import connection, transaction
|
||||
from django.db.models import Q
|
||||
from django.dispatch import receiver
|
||||
from django.template import Context
|
||||
from django.template.loader import get_template
|
||||
from django.utils.html import escape
|
||||
from django.utils.timezone import now, override
|
||||
@@ -150,13 +149,13 @@ def prefix_subject(settings_holder, subject, highlight=False):
|
||||
return subject
|
||||
|
||||
|
||||
def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString], template: Union[str, LazyI18nString],
|
||||
def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString],
|
||||
context: Dict[str, Any] = None, event: Event = None, locale: str = None, order: Order = None,
|
||||
position: OrderPosition = None, *, headers: dict = None, sender: str = None, organizer: Organizer = None,
|
||||
customer: Customer = None, invoices: Sequence = None, attach_tickets=False, auto_email=True, user=None,
|
||||
attach_ical=False, attach_cached_files: Sequence = None, attach_other_files: list=None,
|
||||
plain_text_only=False, no_order_links=False, cc: Sequence[str]=None, bcc: Sequence[str]=None,
|
||||
sensitive: bool=False) -> Optional[OutgoingMail]:
|
||||
sensitive: bool=False):
|
||||
"""
|
||||
Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation.
|
||||
|
||||
@@ -262,22 +261,17 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString],
|
||||
_autoextend_context(context, order)
|
||||
|
||||
# Build raw content
|
||||
content = render_mail(template, context, placeholder_mode=None)
|
||||
content_plain = render_mail(template, context, placeholder_mode=None)
|
||||
if settings_holder:
|
||||
signature = str(settings_holder.settings.get('mail_text_signature'))
|
||||
else:
|
||||
signature = ""
|
||||
|
||||
# Build full plain-text body
|
||||
if isinstance(content, FormattedString):
|
||||
# Already formatted by render_mail() from format_values
|
||||
body_plain = content
|
||||
elif isinstance(content, PlainHtmlAlternativeString):
|
||||
# Already formatted by render_mail() form a django template
|
||||
body_plain = content.plain
|
||||
if not isinstance(content_plain, FormattedString):
|
||||
body_plain = format_map(content_plain, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
else:
|
||||
# Not yet formatted
|
||||
body_plain = format_map(content, context, mode=SafeFormatter.MODE_RICH_TO_PLAIN)
|
||||
body_plain = content_plain
|
||||
body_plain = _wrap_plain_body(body_plain, signature, event, order, position, no_order_links)
|
||||
|
||||
# Build subject
|
||||
@@ -304,19 +298,19 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString],
|
||||
|
||||
try:
|
||||
if 'context' in inspect.signature(renderer.render).parameters:
|
||||
body_html = renderer.render(content, signature, raw_subject, order, position, context)
|
||||
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, signature, raw_subject, order, position)
|
||||
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, signature, raw_subject, order)
|
||||
body_html = renderer.render(content_plain, signature, raw_subject, order)
|
||||
except:
|
||||
logger.exception('Could not render HTML body')
|
||||
body_html = None
|
||||
@@ -341,26 +335,14 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString],
|
||||
should_attach_other_files=attach_other_files or [],
|
||||
sensitive=sensitive,
|
||||
)
|
||||
m._prefetched_objects_cache = {}
|
||||
if invoices and not position:
|
||||
m.should_attach_invoices.add(*invoices)
|
||||
# Hack: For logging, we'll later make a `should_attach_invoices.all()` call. We can prevent a useless
|
||||
# DB query by filling the cache
|
||||
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = invoices
|
||||
else:
|
||||
m._prefetched_objects_cache[m.should_attach_invoices.prefetch_cache_name] = Invoice.objects.none()
|
||||
if attach_cached_files:
|
||||
cf_list = []
|
||||
for cf in attach_cached_files:
|
||||
if not isinstance(cf, CachedFile):
|
||||
cf = CachedFile.objects.get(pk=cf)
|
||||
m.should_attach_cached_files.add(cf)
|
||||
cf_list.append(cf)
|
||||
# Hack: For logging, we'll later make a `should_attach_cached_files.all()` call. We can prevent a useless
|
||||
# DB query by filling the cache
|
||||
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = cf_list
|
||||
else:
|
||||
m._prefetched_objects_cache[m.should_attach_cached_files.prefetch_cache_name] = CachedFile.objects.none()
|
||||
m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf))
|
||||
else:
|
||||
m.should_attach_cached_files.add(cf)
|
||||
|
||||
send_task = mail_send_task.si(
|
||||
outgoing_mail=m.id
|
||||
@@ -382,8 +364,6 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString],
|
||||
lambda: chain(*task_chain).apply_async()
|
||||
)
|
||||
|
||||
return m
|
||||
|
||||
|
||||
class CustomEmail(EmailMultiAlternatives):
|
||||
def _create_mime_attachment(self, content, mimetype):
|
||||
@@ -409,7 +389,7 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
# mail_send_task(self, *, outgoing_mail)
|
||||
with scopes_disabled():
|
||||
mail_send(**kwargs)
|
||||
return
|
||||
return False
|
||||
else:
|
||||
raise ValueError("Unknown arguments")
|
||||
|
||||
@@ -463,15 +443,24 @@ def mail_send_task(self, **kwargs) -> bool:
|
||||
content = ct.file.read()
|
||||
args.append((name, content, ct.type))
|
||||
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
|
||||
# why (probably some race condition with ticket cache invalidation?), so retry later.
|
||||
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:
|
||||
# Well then, something is really wrong, let's send it without attachment before we
|
||||
# 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
|
||||
|
||||
if attach_size * 1.37 < settings.FILE_UPLOAD_MAX_SIZE_EMAIL_ATTACHMENT - 1024 * 1024:
|
||||
@@ -816,22 +805,15 @@ def render_mail(template, context, placeholder_mode: Optional[int]=SafeFormatter
|
||||
body = str(template)
|
||||
if context and placeholder_mode:
|
||||
body = format_map(body, context, mode=placeholder_mode)
|
||||
return body
|
||||
else:
|
||||
tpl = get_template(template)
|
||||
|
||||
plain_context = Context({
|
||||
k: v.plain if isinstance(v, PlainHtmlAlternativeString) else v
|
||||
for k, v in context.items()
|
||||
} | {"to_html": False}, autoescape=False)
|
||||
html_context = Context({
|
||||
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()
|
||||
} | {"to_html": True}, autoescape=True)
|
||||
return PlainHtmlAlternativeString(
|
||||
plain=tpl.template.render(plain_context),
|
||||
html=tpl.template.render(html_context),
|
||||
)
|
||||
}
|
||||
body = FormattedString(tpl.render(context))
|
||||
return body
|
||||
|
||||
|
||||
def replace_images_with_cid_paths(body_html):
|
||||
|
||||
@@ -253,7 +253,8 @@ 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_id': order.event.organizer.id,
|
||||
'acceptor_slug': order.event.organizer.slug
|
||||
}
|
||||
)
|
||||
break
|
||||
@@ -563,6 +564,7 @@ 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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -2457,7 +2459,8 @@ class OrderChangeManager:
|
||||
auth=self.auth,
|
||||
data={
|
||||
'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,
|
||||
data={
|
||||
'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={
|
||||
'value': trans.value,
|
||||
'acceptor_id': order.event.organizer.id,
|
||||
'acceptor_slug': order.event.organizer.slug
|
||||
}
|
||||
)
|
||||
any_giftcards = True
|
||||
|
||||
@@ -39,7 +39,7 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
||||
with language(event.settings.locale):
|
||||
email_context = get_email_context(event=event, name=r.get('name') or '',
|
||||
voucher_list=[v.code for v in voucher_list])
|
||||
outgoing_mail = mail(
|
||||
mail(
|
||||
r['email'],
|
||||
subject,
|
||||
LazyI18nString(message),
|
||||
@@ -60,8 +60,8 @@ def vouchers_send(event: Event, vouchers: list, subject: str, message: str, reci
|
||||
data={
|
||||
'recipient': r['email'],
|
||||
'name': r.get('name'),
|
||||
'subject': outgoing_mail.subject,
|
||||
'message': outgoing_mail.body_plain,
|
||||
'subject': subject,
|
||||
'message': message,
|
||||
},
|
||||
save=False
|
||||
))
|
||||
|
||||
@@ -363,7 +363,7 @@ class EmailAddressShredder(BaseDataShredder):
|
||||
le.save(update_fields=['data', 'shredded'])
|
||||
else:
|
||||
shred_log_fields(le, banlist=[
|
||||
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email', 'bcc', 'cc',
|
||||
'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email'
|
||||
])
|
||||
|
||||
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
<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,17 +19,44 @@
|
||||
# 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 WaitingListEntryTransferForm(I18nModelForm):
|
||||
class WaitingListEntryEditForm(I18nModelForm):
|
||||
itemvar = forms.ChoiceField(
|
||||
error_messages={
|
||||
'invalid_choice': _("Select a valid choice.")
|
||||
}
|
||||
)
|
||||
|
||||
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:
|
||||
@@ -45,12 +72,73 @@ class WaitingListEntryTransferForm(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,6 +19,14 @@
|
||||
</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">
|
||||
|
||||
@@ -24,9 +24,7 @@
|
||||
{% if log.display %}
|
||||
<br/><span class="fa fa-fw fa-comment-o"></span> {{ log.display }}
|
||||
{% endif %}
|
||||
{% if log.parsed_data.to %}
|
||||
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.to|join:", " }}
|
||||
{% elif log.parsed_data.recipient %} {# legacy #}
|
||||
{% if log.parsed_data.recipient %}
|
||||
<br/><span class="fa fa-fw fa-envelope-o"></span> {{ log.parsed_data.recipient }}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
@@ -144,14 +144,23 @@
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<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>{% trans "Unused tokens:" %}</p>
|
||||
<ul>
|
||||
{% for t in static_tokens %}
|
||||
<li><code>{{ t.token }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% 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 %}
|
||||
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
|
||||
<span class="fa fa-refresh"></span>
|
||||
{% trans "Generate new emergency tokens" %}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{% 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,6 +124,7 @@
|
||||
</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>
|
||||
@@ -267,13 +268,13 @@
|
||||
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
|
||||
<span class="fa fa-thumbs-down"></span>
|
||||
</button>
|
||||
{% 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.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>
|
||||
|
||||
<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">
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
{% 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+)/transfer$', waitinglist.EntryTransfer.as_view(),
|
||||
name='event.orders.waitinglist.transfer'),
|
||||
re_path(r'^waitinglist/(?P<entry>\d+)/edit$', waitinglist.EntryEdit.as_view(),
|
||||
name='event.orders.waitinglist.edit'),
|
||||
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,6 +149,8 @@ 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,11 +870,15 @@ class MailSettingsPreview(EventPermissionRequiredMixin, View):
|
||||
)
|
||||
|
||||
except ValueError:
|
||||
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||
PlaceholderValidator.error_message)
|
||||
msgs[self.supported_locale[idx]] = format_html(
|
||||
'<div class="alert alert-danger">{}</div>',
|
||||
PlaceholderValidator.error_message
|
||||
)
|
||||
except KeyError as e:
|
||||
msgs[self.supported_locale[idx]] = '<div class="alert alert-danger">{}</div>'.format(
|
||||
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]})
|
||||
msgs[self.supported_locale[idx]] = format_html(
|
||||
'<div class="alert alert-danger">{}</div>',
|
||||
_('Invalid placeholder: {%(value)s}') % {'value': e.args[0]}
|
||||
)
|
||||
|
||||
return JsonResponse({
|
||||
'item': preview_item,
|
||||
|
||||
@@ -2413,9 +2413,9 @@ class OrderSendMail(EventPermissionRequiredMixin, OrderViewMixin, FormView):
|
||||
with language(order.locale, self.request.event.settings.region):
|
||||
email_context = get_email_context(event=order.event, order=order)
|
||||
email_template = LazyI18nString(form.cleaned_data['message'])
|
||||
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
|
||||
email_content = render_mail(email_template, email_context)
|
||||
if self.request.POST.get('action') == 'preview':
|
||||
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
|
||||
email_content = render_mail(email_template, email_context)
|
||||
self.preview_output = {
|
||||
'subject': mark_safe(_('Subject: {subject}').format(
|
||||
subject=prefix_subject(order.event, escape(email_subject), highlight=True)
|
||||
@@ -2477,9 +2477,9 @@ class OrderPositionSendMail(OrderSendMail):
|
||||
with language(position.order.locale, self.request.event.settings.region):
|
||||
email_context = get_email_context(event=position.order.event, order=position.order, position=position)
|
||||
email_template = LazyI18nString(form.cleaned_data['message'])
|
||||
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
|
||||
email_content = render_mail(email_template, email_context)
|
||||
if self.request.POST.get('action') == 'preview':
|
||||
email_subject = format_map(str(form.cleaned_data['subject']), email_context)
|
||||
email_content = render_mail(email_template, email_context)
|
||||
self.preview_output = {
|
||||
'subject': mark_safe(_('Subject: {subject}').format(
|
||||
subject=prefix_subject(position.order.event, escape(email_subject), highlight=True))
|
||||
|
||||
@@ -1850,7 +1850,8 @@ class GiftCardDetailView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
data={
|
||||
'value': value,
|
||||
'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,
|
||||
)
|
||||
@@ -1913,7 +1914,8 @@ class GiftCardCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMi
|
||||
user=self.request.user,
|
||||
data={
|
||||
'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(
|
||||
|
||||
@@ -49,12 +49,14 @@ 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
|
||||
@@ -85,8 +87,9 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecentAuthenticationRequiredMixin:
|
||||
max_time = 3600
|
||||
max_time = 900
|
||||
|
||||
@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:
|
||||
@@ -289,16 +292,13 @@ class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
|
||||
ctx = super().get_context_data()
|
||||
|
||||
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:
|
||||
ctx['static_tokens'] = StaticDevice.objects.filter(
|
||||
ctx['static_tokens_device'] = StaticDevice.objects.filter(
|
||||
user=self.request.user, name='emergency'
|
||||
).first().token_set.all()
|
||||
).first()
|
||||
except StaticDevice.DoesNotExist:
|
||||
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['static_tokens_device'] = None
|
||||
|
||||
ctx['devices'] = []
|
||||
for dt in REAL_DEVICE_TYPES:
|
||||
@@ -630,8 +630,14 @@ 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.'))
|
||||
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())
|
||||
)
|
||||
)
|
||||
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 WaitingListEntryTransferForm
|
||||
from pretix.control.forms.waitinglist import WaitingListEntryEditForm
|
||||
from pretix.control.permissions import EventPermissionRequiredMixin
|
||||
from pretix.control.views import PaginationMixin
|
||||
|
||||
@@ -138,6 +138,17 @@ 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
|
||||
|
||||
|
||||
@@ -238,7 +249,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'] = ("status" in self.request.GET or "item" in self.request.GET)
|
||||
ctx['filtered'] = any(param in self.request.GET for param in ("status", "item", "search"))
|
||||
|
||||
itemvar_cache = {}
|
||||
quota_cache = {}
|
||||
@@ -390,25 +401,20 @@ class EntryDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
})
|
||||
|
||||
|
||||
class EntryTransfer(EventPermissionRequiredMixin, UpdateView):
|
||||
class EntryEdit(EventPermissionRequiredMixin, UpdateView):
|
||||
model = WaitingListEntry
|
||||
template_name = 'pretixcontrol/waitinglist/transfer.html'
|
||||
template_name = 'pretixcontrol/waitinglist/edit.html'
|
||||
permission = 'can_change_orders'
|
||||
form_class = WaitingListEntryTransferForm
|
||||
form_class = WaitingListEntryEditForm
|
||||
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
|
||||
|
||||
+2561
-2267
File diff suppressed because it is too large
Load Diff
+2735
-2270
File diff suppressed because it is too large
Load Diff
+2561
-2267
File diff suppressed because it is too large
Load Diff
+2661
-2277
File diff suppressed because it is too large
Load Diff
+2684
-2260
File diff suppressed because it is too large
Load Diff
+2581
-2267
File diff suppressed because it is too large
Load Diff
+2667
-2279
File diff suppressed because it is too large
Load Diff
+2661
-2268
File diff suppressed because it is too large
Load Diff
@@ -265,6 +265,7 @@ Objekt-IDs
|
||||
Offline-Scan
|
||||
OK
|
||||
Online-Banking
|
||||
Online-Banking-Nutzer
|
||||
Onlinebanking
|
||||
Onlinebanking-Zugangsdaten
|
||||
Open
|
||||
@@ -539,6 +540,8 @@ 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,6 +265,7 @@ Objekt-IDs
|
||||
Offline-Scan
|
||||
OK
|
||||
Online-Banking
|
||||
Online-Banking-Nutzer
|
||||
Onlinebanking
|
||||
Onlinebanking-Zugangsdaten
|
||||
Open
|
||||
@@ -539,6 +540,8 @@ WeChat-Zahlung
|
||||
Weiterleitungs-URIs
|
||||
Weiterleitungs-URL
|
||||
Weiterleitungs-URLs
|
||||
WERO
|
||||
WERO-App
|
||||
WhatsApp
|
||||
Widget
|
||||
Widget-Code
|
||||
|
||||
+2561
-2267
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-01-26 13:20+0000\n"
|
||||
"POT-Creation-Date: 2026-02-24 11:50+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"
|
||||
|
||||
+2702
-2282
File diff suppressed because it is too large
Load Diff
+2561
-2267
File diff suppressed because it is too large
Load Diff
+2682
-2270
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2563
-2267
File diff suppressed because it is too large
Load Diff
+2643
-2256
File diff suppressed because it is too large
Load Diff
+2684
-2258
File diff suppressed because it is too large
Load Diff
+2578
-2269
File diff suppressed because it is too large
Load Diff
+2687
-2272
File diff suppressed because it is too large
Load Diff
+2704
-2279
File diff suppressed because it is too large
Load Diff
+2721
-2256
File diff suppressed because it is too large
Load Diff
+2662
-2254
File diff suppressed because it is too large
Load Diff
+2654
-2286
File diff suppressed because it is too large
Load Diff
+2716
-2259
File diff suppressed because it is too large
Load Diff
+2630
-2258
File diff suppressed because it is too large
Load Diff
+2895
-2477
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-12 20:00+0000\n"
|
||||
"Last-Translator: Yasunobu YesNo Kawaguchi <kawaguti@gmail.com>\n"
|
||||
"PO-Revision-Date: 2026-02-23 10:00+0000\n"
|
||||
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\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.15.2\n"
|
||||
"X-Generator: Weblate 5.16\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"
|
||||
|
||||
+2639
-2257
File diff suppressed because it is too large
Load Diff
+2648
-2276
File diff suppressed because it is too large
Load Diff
+2563
-2267
File diff suppressed because it is too large
Load Diff
+2648
-2260
File diff suppressed because it is too large
Load Diff
+2561
-2267
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2717
-2302
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-05 23:00+0000\n"
|
||||
"PO-Revision-Date: 2026-02-19 22: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.15.2\n"
|
||||
"X-Generator: Weblate 5.16\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 "Alle"
|
||||
msgstr "Allemaal"
|
||||
|
||||
#: 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
+2727
-2261
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+2580
-2247
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
+2732
-2273
File diff suppressed because it is too large
Load Diff
+2648
-2265
File diff suppressed because it is too large
Load Diff
+2551
-2244
File diff suppressed because it is too large
Load Diff
+2704
-2258
File diff suppressed because it is too large
Load Diff
+2623
-2262
File diff suppressed because it is too large
Load Diff
+2575
-2271
File diff suppressed because it is too large
Load Diff
+2723
-2261
File diff suppressed because it is too large
Load Diff
+3235
-2932
File diff suppressed because it is too large
Load Diff
+2700
-2285
File diff suppressed because it is too large
Load Diff
+2704
-2261
File diff suppressed because it is too large
Load Diff
+2688
-2256
File diff suppressed because it is too large
Load Diff
+2561
-2267
File diff suppressed because it is too large
Load Diff
@@ -187,6 +187,7 @@ 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 }}</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>
|
||||
<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>
|
||||
{% endif %}
|
||||
</dl>
|
||||
{% if details %}
|
||||
@@ -38,4 +38,4 @@
|
||||
{% if payment_qr_codes %}
|
||||
{% include "pretixpresale/event/payment_qr_codes.html" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -786,7 +786,12 @@ class PaypalMethod(BasePaymentProvider):
|
||||
else:
|
||||
pp_captured_order = response.result
|
||||
|
||||
for purchaseunit in pp_captured_order.purchase_units:
|
||||
payment.refresh_from_db()
|
||||
|
||||
any_captures = False
|
||||
all_captures_completed = True
|
||||
for purchaseunit in pp_captured_order.purchase_units:
|
||||
if hasattr(purchaseunit, 'payments'):
|
||||
for capture in purchaseunit.payments.captures:
|
||||
try:
|
||||
ReferencedPayPalObject.objects.get_or_create(order=payment.order, payment=payment, reference=capture.id)
|
||||
@@ -794,32 +799,40 @@ class PaypalMethod(BasePaymentProvider):
|
||||
pass
|
||||
|
||||
if capture.status != '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
|
||||
all_captures_completed = False
|
||||
else:
|
||||
any_captures = True
|
||||
|
||||
payment.refresh_from_db()
|
||||
|
||||
if pp_captured_order.status != 'COMPLETED':
|
||||
payment.fail(info=pp_captured_order.dict())
|
||||
logger.error('Invalid state: %s' % repr(pp_captured_order.dict()))
|
||||
raise PaymentException(
|
||||
_('We were unable to process your payment. See below for details on how to proceed.')
|
||||
)
|
||||
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||
logger.warning('PayPal success event even though order is already marked as paid')
|
||||
return
|
||||
|
||||
try:
|
||||
# Payment has at least one capture, but it is not yet completed
|
||||
if any_captures and not 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.save(update_fields=['info'])
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise PaymentException(str(e))
|
||||
payment.state = OrderPayment.PAYMENT_STATE_PENDING
|
||||
payment.save()
|
||||
return
|
||||
# Payment has at least one capture and all captures are completed
|
||||
elif any_captures and all_captures_completed:
|
||||
if pp_captured_order.status != 'COMPLETED':
|
||||
payment.fail(info=pp_captured_order.dict())
|
||||
logger.error('Invalid state: %s' % repr(pp_captured_order.dict()))
|
||||
raise PaymentException(
|
||||
_('We were unable to process your payment. See below for details on how to proceed.')
|
||||
)
|
||||
|
||||
if payment.state == OrderPayment.PAYMENT_STATE_CONFIRMED:
|
||||
logger.warning('PayPal success event even though order is already marked as paid')
|
||||
return
|
||||
|
||||
try:
|
||||
payment.info = json.dumps(pp_captured_order.dict())
|
||||
payment.save(update_fields=['info'])
|
||||
payment.confirm()
|
||||
except Quota.QuotaExceededException as e:
|
||||
raise PaymentException(str(e))
|
||||
# Payment has not any captures yet - so it's probably in created status
|
||||
else:
|
||||
return
|
||||
finally:
|
||||
if 'payment_paypal_oid' in request.session:
|
||||
del request.session['payment_paypal_oid']
|
||||
@@ -829,7 +842,7 @@ class PaypalMethod(BasePaymentProvider):
|
||||
try:
|
||||
if (
|
||||
payment.info
|
||||
and payment.info_data['purchase_units'][0]['payments']['captures'][0]['status'] == 'pending'
|
||||
and payment.info_data['purchase_units'][0]['payments']['captures'][0]['status'] == 'PENDING'
|
||||
):
|
||||
retry = False
|
||||
except (KeyError, IndexError):
|
||||
|
||||
@@ -38,10 +38,13 @@ from i18nfield.strings import LazyI18nString
|
||||
|
||||
from pretix.base.email import get_email_context
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import Checkin, Event, InvoiceAddress, Order, User
|
||||
from pretix.base.models import (
|
||||
CachedFile, Checkin, Event, InvoiceAddress, Order, User,
|
||||
)
|
||||
from pretix.base.services.mail import mail
|
||||
from pretix.base.services.tasks import ProfiledEventTask
|
||||
from pretix.celery_app import app
|
||||
from pretix.helpers.format import format_map
|
||||
|
||||
|
||||
def _chunks(lst, n):
|
||||
@@ -61,6 +64,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
user = User.objects.get(pk=user) if user else None
|
||||
subject = LazyI18nString(subject)
|
||||
message = LazyI18nString(message)
|
||||
attachments_for_log = [cf.filename for cf in CachedFile.objects.filter(pk__in=attachments)] if attachments else []
|
||||
|
||||
def _send_to_order(o):
|
||||
send_to_order = recipients in ('both', 'orders')
|
||||
@@ -118,7 +122,7 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
|
||||
with language(o.locale, event.settings.region):
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia, position=p)
|
||||
outgoing_mail = mail(
|
||||
mail(
|
||||
p.attendee_email,
|
||||
subject,
|
||||
message,
|
||||
@@ -131,17 +135,25 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
attach_ical=attach_ical,
|
||||
attach_cached_files=attachments
|
||||
)
|
||||
if outgoing_mail:
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent.attendee',
|
||||
user=user,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent.attendee',
|
||||
user=user,
|
||||
data={
|
||||
'position': p.positionid,
|
||||
'subject': format_map(subject.localize(o.locale), email_context),
|
||||
'message': format_map(message.localize(o.locale), email_context),
|
||||
'recipient': p.attendee_email,
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': attachments_for_log,
|
||||
}
|
||||
)
|
||||
|
||||
if send_to_order and o.email:
|
||||
with language(o.locale, event.settings.region):
|
||||
email_context = get_email_context(event=event, order=o, invoice_address=ia)
|
||||
outgoing_mail = mail(
|
||||
mail(
|
||||
o.email,
|
||||
subject,
|
||||
message,
|
||||
@@ -153,12 +165,19 @@ def send_mails_to_orders(event: Event, user: int, subject: dict, message: dict,
|
||||
attach_ical=attach_ical,
|
||||
attach_cached_files=attachments,
|
||||
)
|
||||
if outgoing_mail:
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent',
|
||||
user=user,
|
||||
data=outgoing_mail.log_data(),
|
||||
)
|
||||
o.log_action(
|
||||
'pretix.plugins.sendmail.order.email.sent',
|
||||
user=user,
|
||||
data={
|
||||
'subject': format_map(subject.localize(o.locale), email_context),
|
||||
'message': format_map(message.localize(o.locale), email_context),
|
||||
'recipient': o.email,
|
||||
'attach_tickets': attach_tickets,
|
||||
'attach_ical': attach_ical,
|
||||
'attach_other_files': [],
|
||||
'attach_cached_files': attachments_for_log,
|
||||
}
|
||||
)
|
||||
|
||||
for chunk in _chunks(objects, 1000):
|
||||
orders = Order.objects.filter(pk__in=chunk, event=event)
|
||||
|
||||
@@ -118,6 +118,7 @@ logger = logging.getLogger('pretix.plugins.stripe')
|
||||
# - UPI: ✗
|
||||
# - Netbanking: ✗
|
||||
# - TWINT: ✓
|
||||
# - Wero: ✓ (No settings UI yet)
|
||||
#
|
||||
# Bank transfers
|
||||
# - ACH Bank Transfer: ✗
|
||||
@@ -509,6 +510,15 @@ 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:
|
||||
@@ -1946,3 +1956,15 @@ 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,
|
||||
StripeWeChatPay, StripeWero,
|
||||
)
|
||||
|
||||
return [
|
||||
StripeSettingsHolder, StripeCC, StripeGiropay, StripeIdeal, StripeAlipay, StripeBancontact,
|
||||
StripeSofort, StripeEPS, StripeMultibanco, StripePayByBank, StripePrzelewy24, StripePromptPay, StripeRevolutPay,
|
||||
StripeWeChatPay, StripeSEPADirectDebit, StripeAffirm, StripeKlarna, StripePayPal, StripeSwish,
|
||||
StripeTwint, StripeMobilePay
|
||||
StripeTwint, StripeMobilePay, StripeWero
|
||||
]
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user