diff --git a/doc/api/resources/orders.rst b/doc/api/resources/orders.rst index f863a6df4..e09b8e09d 100644 --- a/doc/api/resources/orders.rst +++ b/doc/api/resources/orders.rst @@ -1719,6 +1719,56 @@ List of all order positions :statuscode 401: Authentication failure :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. +.. http:get:: /api/v1/organizers/(organizer)/orderpositions/ + + Returns a list of all order positions within all events of a given organizer (with sufficient access permissions). + + The supported query parameters and output format of this endpoint are almost identical to those of the list endpoint + within an event. + The only changes are that responses also contain the ``event`` attribute in each result and that the 'pdf_data' + parameter is not supported. + + **Example request**: + + .. sourcecode:: http + + GET /api/v1/organizers/bigevents/orderpositions/ HTTP/1.1 + Host: pretix.eu + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: application/json + X-Page-Generated: 2017-12-01T10:00:00Z + + { + "count": 1, + "next": null, + "previous": null, + "results": [ + { + "id:": 23442 + "event": "sampleconf", + "order": "ABC12", + "positionid": 1, + "canceled": false, + "item": 1345, + ... + } + ] + } + + :param organizer: The ``slug`` field of the organizer to fetch + :statuscode 200: no error + :statuscode 401: Authentication failure + :statuscode 403: The requested organizer/event does not exist **or** you have no permission to view this resource. + + + Fetching individual positions ----------------------------- diff --git a/pyproject.toml b/pyproject.toml index ce2aaa7a9..42970855e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,7 @@ dependencies = [ "phonenumberslite==9.0.*", "Pillow==12.1.*", "pretix-plugin-build", - "protobuf==6.33.*", + "protobuf==7.34.*", "psycopg2-binary", "pycountry", "pycparser==3.0", @@ -92,7 +92,7 @@ dependencies = [ "redis==7.1.*", "reportlab==4.4.*", "requests==2.32.*", - "sentry-sdk==2.53.*", + "sentry-sdk==2.54.*", "sepaxml==2.7.*", "stripe==7.9.*", "text-unidecode==1.*", @@ -113,7 +113,7 @@ dev = [ "fakeredis==2.34.*", "flake8==7.3.*", "freezegun", - "isort==7.0.*", + "isort==8.0.*", "pep8-naming==0.15.*", "potypo", "pytest-asyncio>=0.24", diff --git a/src/pretix/__init__.py b/src/pretix/__init__.py index 73d4f9a26..e8dfc803d 100644 --- a/src/pretix/__init__.py +++ b/src/pretix/__init__.py @@ -19,4 +19,4 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # -__version__ = "2026.2.0.dev0" +__version__ = "2026.3.0.dev0" diff --git a/src/pretix/api/serializers/order.py b/src/pretix/api/serializers/order.py index 4fb4dce05..3f6b0405a 100644 --- a/src/pretix/api/serializers/order.py +++ b/src/pretix/api/serializers/order.py @@ -19,6 +19,7 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import json import logging import os from collections import Counter, defaultdict @@ -636,6 +637,14 @@ class OrderPositionSerializer(I18nAwareModelSerializer): return entry +class OrganizerOrderPositionSerializer(OrderPositionSerializer): + event = SlugRelatedField(slug_field='slug', read_only=True) + + class Meta(OrderPositionSerializer.Meta): + fields = OrderPositionSerializer.Meta.fields + ('event',) + read_only_fields = OrderPositionSerializer.Meta.read_only_fields + ('event',) + + class RequireAttentionField(serializers.Field): def to_representation(self, instance: OrderPosition): return instance.require_checkin_attention @@ -1215,6 +1224,18 @@ class OrderCreateSerializer(I18nAwareModelSerializer): raise ValidationError('The given payment provider is not known.') return pp + def validate_payment_info(self, info): + if info: + try: + obj = json.loads(info) + except ValueError: + raise ValidationError('payment_info must be valid JSON.') + + if not isinstance(obj, dict): + # only objects are allowed + raise ValidationError('payment_info must be a JSON object.') + return info + def validate_expires(self, expires): if expires < now(): raise ValidationError('Expiration date must be in the future.') diff --git a/src/pretix/api/serializers/organizer.py b/src/pretix/api/serializers/organizer.py index 6d8f6743b..ab12e7b94 100644 --- a/src/pretix/api/serializers/organizer.py +++ b/src/pretix/api/serializers/organizer.py @@ -365,9 +365,10 @@ class TeamInviteSerializer(serializers.ModelSerializer): def _send_invite(self, instance): mail( instance.email, - _('pretix account invitation'), + _('Account invitation'), 'pretixcontrol/email/invitation.txt', { + 'instance': settings.PRETIX_INSTANCE_NAME, 'user': self, 'organizer': self.context['organizer'].name, 'team': instance.team.name, diff --git a/src/pretix/api/urls.py b/src/pretix/api/urls.py index a4db75dfd..911ce8c9e 100644 --- a/src/pretix/api/urls.py +++ b/src/pretix/api/urls.py @@ -67,6 +67,7 @@ orga_router.register(r'invoices', order.InvoiceViewSet) orga_router.register(r'scheduled_exports', exporters.ScheduledOrganizerExportViewSet) orga_router.register(r'exporters', exporters.OrganizerExportersViewSet, basename='exporters') orga_router.register(r'transactions', order.OrganizerTransactionViewSet) +orga_router.register(r'orderpositions', order.OrganizerOrderPositionViewSet, basename='orderpositions') team_router = routers.DefaultRouter() team_router.register(r'members', organizer.TeamMemberViewSet) @@ -83,7 +84,7 @@ event_router.register(r'discounts', discount.DiscountViewSet) event_router.register(r'quotas', item.QuotaViewSet) event_router.register(r'vouchers', voucher.VoucherViewSet) event_router.register(r'orders', order.EventOrderViewSet) -event_router.register(r'orderpositions', order.OrderPositionViewSet) +event_router.register(r'orderpositions', order.EventOrderPositionViewSet) event_router.register(r'transactions', order.TransactionViewSet) event_router.register(r'invoices', order.InvoiceViewSet) event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets') diff --git a/src/pretix/api/views/order.py b/src/pretix/api/views/order.py index 8ec782876..10c911994 100644 --- a/src/pretix/api/views/order.py +++ b/src/pretix/api/views/order.py @@ -57,9 +57,10 @@ from pretix.api.serializers.order import ( BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer, OrderPaymentCreateSerializer, OrderPaymentSerializer, OrderPositionSerializer, OrderRefundCreateSerializer, - OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer, - PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer, - SimulatedOrderSerializer, TransactionSerializer, + OrderRefundSerializer, OrderSerializer, OrganizerOrderPositionSerializer, + OrganizerTransactionSerializer, PriceCalcSerializer, PrintLogSerializer, + RevokedTicketSecretSerializer, SimulatedOrderSerializer, + TransactionSerializer, ) from pretix.api.serializers.orderchange import ( BlockNameSerializer, OrderChangeOperationSerializer, @@ -1065,8 +1066,7 @@ with scopes_disabled(): } -class OrderPositionViewSet(viewsets.ModelViewSet): - serializer_class = OrderPositionSerializer +class OrderPositionViewSetMixin: queryset = OrderPosition.all.none() filter_backends = (DjangoFilterBackend, RichOrderingFilter) ordering = ('order__datetime', 'positionid') @@ -1087,8 +1087,7 @@ class OrderPositionViewSet(viewsets.ModelViewSet): def get_serializer_context(self): ctx = super().get_serializer_context() - ctx['event'] = self.request.event - ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true' + ctx['pdf_data'] = False ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true' return ctx @@ -1097,9 +1096,8 @@ class OrderPositionViewSet(viewsets.ModelViewSet): qs = OrderPosition.all else: qs = OrderPosition.objects - - qs = qs.filter(order__event=self.request.event) - if self.request.query_params.get('pdf_data', 'false').lower() == 'true': + qs = qs.filter(order__event__organizer=self.request.organizer) + if self.request.query_params.get('pdf_data', 'false').lower() == 'true' and getattr(self.request, 'event', None): prefetch_related_objects([self.request.organizer], 'meta_properties') prefetch_related_objects( [self.request.event], @@ -1154,9 +1152,9 @@ class OrderPositionViewSet(viewsets.ModelViewSet): qs = qs.prefetch_related( Prefetch('checkins', queryset=Checkin.objects.select_related("device")), Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')), - 'answers', 'answers__options', 'answers__question', + 'answers', 'answers__options', 'answers__question', 'order__event', 'order__event__organizer' ).select_related( - 'item', 'order', 'order__event', 'order__event__organizer', 'seat' + 'item', 'order', 'seat' ) return qs @@ -1168,6 +1166,45 @@ class OrderPositionViewSet(viewsets.ModelViewSet): return prov raise NotFound('Unknown output provider.') + +class OrganizerOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ReadOnlyModelViewSet): + serializer_class = OrganizerOrderPositionSerializer + + def get_queryset(self): + qs = super().get_queryset() + + perm = self.permission if self.request.method in SAFE_METHODS else self.write_permission + + if isinstance(self.request.auth, (TeamAPIToken, Device)): + auth_obj = self.request.auth + elif self.request.user.is_authenticated: + auth_obj = self.request.user + else: + raise PermissionDenied("Unknown authentication scheme") + + qs = qs.filter( + order__event__in=auth_obj.get_events_with_permission(perm, request=self.request).filter( + organizer=self.request.organizer + ) + ) + + return qs + + +class EventOrderPositionViewSet(OrderPositionViewSetMixin, viewsets.ModelViewSet): + serializer_class = OrderPositionSerializer + + def get_serializer_context(self): + ctx = super().get_serializer_context() + ctx['event'] = self.request.event + ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true' + return ctx + + def get_queryset(self): + qs = super().get_queryset() + qs = qs.filter(order__event=self.request.event) + return qs + @action(detail=True, methods=['POST'], url_name='price_calc') def price_calc(self, request, *args, **kwargs): """ diff --git a/src/pretix/api/webhooks.py b/src/pretix/api/webhooks.py index aed66d461..39a5a613b 100644 --- a/src/pretix/api/webhooks.py +++ b/src/pretix/api/webhooks.py @@ -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,6 +198,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, @@ -473,7 +475,7 @@ def register_default_webhook_events(sender, **kwargs): ), ParametrizedGiftcardTransactionWebhookEvent( 'pretix.giftcards.transaction.*', - _('Gift card used in transcation'), + _('Gift card used in transaction'), ) ) diff --git a/src/pretix/base/datasync/datasync.py b/src/pretix/base/datasync/datasync.py index bc3c5bfae..3177aaf26 100644 --- a/src/pretix/base/datasync/datasync.py +++ b/src/pretix/base/datasync/datasync.py @@ -216,7 +216,10 @@ class OutboundSyncProvider: try: mapped_objects = self.sync_order(sq.order) - if not all(all(not res or res.sync_info.get("action", "") == "nothing_to_do" for res in res_list) for res_list in mapped_objects.values()): + actions_taken = [res and res.sync_info.get("action", "") for res_list in mapped_objects.values() for res in res_list] + should_write_logentry = any(action not in (None, "nothing_to_do") for action in actions_taken) + logger.info('Synced order %s to %s, actions: %r, log: %r', sq.order.code, sq.sync_provider, actions_taken, should_write_logentry) + if should_write_logentry: sq.order.log_action("pretix.event.order.data_sync.success", { "provider": self.identifier, "objects": { @@ -237,7 +240,7 @@ class OutboundSyncProvider: sq.set_sync_error("exceeded", e.messages, e.full_message) else: logger.info( - f"Could not sync order {sq.order.code} to {type(self).__name__} " + f"Could not sync order {sq.order.code} to {sq.sync_provider} " f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})", exc_info=True, ) diff --git a/src/pretix/base/exporters/orderlist.py b/src/pretix/base/exporters/orderlist.py index 848d98300..b3471eac2 100644 --- a/src/pretix/base/exporters/orderlist.py +++ b/src/pretix/base/exporters/orderlist.py @@ -315,8 +315,9 @@ class OrderListExporter(MultiSheetListExporter): for id, vn in payment_methods: headers.append(_('Paid by {method}').format(method=vn)) - # get meta_data labels from first cached event - headers += next(iter(self.event_object_cache.values())).meta_data.keys() + if self.event_object_cache: + # get meta_data labels from first cached event if any + headers += next(iter(self.event_object_cache.values())).meta_data.keys() yield headers full_fee_sum_cache = { @@ -503,8 +504,9 @@ class OrderListExporter(MultiSheetListExporter): headers.append(_('External customer ID')) headers.append(_('Payment providers')) - # get meta_data labels from first cached event - headers += next(iter(self.event_object_cache.values())).meta_data.keys() + if self.event_object_cache: + # get meta_data labels from first cached event if any + headers += next(iter(self.event_object_cache.values())).meta_data.keys() yield headers yield self.ProgressSetTotal(total=qs.count()) @@ -707,9 +709,9 @@ class OrderListExporter(MultiSheetListExporter): _('Position order link') ] - # get meta_data labels from first cached event - meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys() if has_subevents: + # get meta_data labels from first cached event + meta_data_labels = next(iter(self.event_object_cache.values())).meta_data.keys() headers += meta_data_labels yield headers diff --git a/src/pretix/base/forms/questions.py b/src/pretix/base/forms/questions.py index 1fd4b8759..ca29fd10a 100644 --- a/src/pretix/base/forms/questions.py +++ b/src/pretix/base/forms/questions.py @@ -1415,6 +1415,7 @@ class BaseInvoiceAddressForm(forms.ModelForm): if not data.get(r): raise ValidationError({r: _("This field is required for the selected type of invoice transmission.")}) + transmission_type.validate_invoice_address_data(data) self.instance.transmission_type = transmission_type.identifier self.instance.transmission_info = transmission_type.form_data_to_transmission_info(data) elif transmission_type.is_exclusive(self.event, data.get("country"), data.get("is_business")): diff --git a/src/pretix/base/forms/widgets.py b/src/pretix/base/forms/widgets.py index 2acf7dd53..45eb48d87 100644 --- a/src/pretix/base/forms/widgets.py +++ b/src/pretix/base/forms/widgets.py @@ -42,6 +42,8 @@ from django.utils.html import escape from django.utils.timezone import get_current_timezone, now from django.utils.translation import gettext_lazy as _ +from pretix.helpers.format import PlainHtmlAlternativeString + def replace_arabic_numbers(inp): if not isinstance(inp, str): @@ -61,11 +63,18 @@ def replace_arabic_numbers(inp): return inp.translate(table) +def format_placeholder_help_text(placeholder_name, sample_value): + if isinstance(sample_value, PlainHtmlAlternativeString): + sample_value = sample_value.plain + title = (_("Sample: %s") % sample_value) if sample_value else "" + return ('' % (escape(title), escape(placeholder_name))) + + def format_placeholders_help_text(placeholders, event=None): placeholders = [(k, v.render_sample(event) if event else v) for k, v in placeholders.items()] placeholders.sort(key=lambda x: x[0]) phs = [ - '' % (escape(_("Sample: %s") % v) if v else "", escape(k)) + format_placeholder_help_text(k, v) for k, v in placeholders ] return _('Available placeholders: {list}').format( diff --git a/src/pretix/base/invoicing/email.py b/src/pretix/base/invoicing/email.py index 155e7906a..8bc6a3e54 100644 --- a/src/pretix/base/invoicing/email.py +++ b/src/pretix/base/invoicing/email.py @@ -33,8 +33,7 @@ from pretix.base.invoicing.transmission import ( transmission_types, ) from pretix.base.models import Invoice, InvoiceAddress -from pretix.base.services.mail import mail, render_mail -from pretix.helpers.format import format_map +from pretix.base.services.mail import mail @transmission_types.new() @@ -134,9 +133,7 @@ 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 - subject = format_map(subject, context) - email_content = render_mail(template, context) - mail( + outgoing_mail = mail( [recipient], subject, template, @@ -151,19 +148,10 @@ class EmailTransmissionProvider(TransmissionProvider): plain_text_only=True, no_order_links=True, ) - 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': [], - } - ) + if outgoing_mail: + invoice.order.log_action( + 'pretix.event.order.email.invoice', + user=None, + auth=None, + data=outgoing_mail.log_data() + ) diff --git a/src/pretix/base/invoicing/pdf.py b/src/pretix/base/invoicing/pdf.py index 2eed2f8e3..21765a629 100644 --- a/src/pretix/base/invoicing/pdf.py +++ b/src/pretix/base/invoicing/pdf.py @@ -148,6 +148,10 @@ class NumberedCanvas(Canvas): self.restoreState() +class InvoiceNotReadyException(Exception): + pass + + class BaseInvoiceRenderer: """ This is the base class for all invoice renderers. diff --git a/src/pretix/base/invoicing/peppol.py b/src/pretix/base/invoicing/peppol.py index 194bf195e..0a26b6c10 100644 --- a/src/pretix/base/invoicing/peppol.py +++ b/src/pretix/base/invoicing/peppol.py @@ -204,6 +204,12 @@ class PeppolTransmissionType(TransmissionType): } return base | {"transmission_peppol_participant_id"} + def validate_invoice_address_data(self, address_data: dict): + # Special case Belgium: If a Belgian business ID is used as Peppol ID, it should match the VAT ID + if address_data.get("transmission_peppol_participant_id").startswith("0208:") and address_data.get("vat_id"): + if address_data["vat_id"].removeprefix("BE") != address_data["transmission_peppol_participant_id"].removeprefix("0208:"): + raise ValidationError({"transmission_peppol_participant_id": _("The Peppol participant ID does not match your VAT ID.")}) + def pdf_watermark(self) -> str: return pgettext("peppol_invoice", "Visual copy") diff --git a/src/pretix/base/invoicing/transmission.py b/src/pretix/base/invoicing/transmission.py index 9c6899381..a82bef73b 100644 --- a/src/pretix/base/invoicing/transmission.py +++ b/src/pretix/base/invoicing/transmission.py @@ -24,7 +24,7 @@ from typing import Optional from django.utils.translation import gettext_lazy as _ from django_countries.fields import Country -from pretix.base.models import Invoice, InvoiceAddress +from pretix.base.models import Invoice from pretix.base.signals import EventPluginRegistry, Registry @@ -89,7 +89,7 @@ class TransmissionType: def invoice_address_form_fields_visible(self, country: Country, is_business: bool) -> set: return set(self.invoice_address_form_fields.keys()) - def validate_address(self, ia: InvoiceAddress): + def validate_invoice_address_data(self, address_data: dict): pass @property diff --git a/src/pretix/base/models/auth.py b/src/pretix/base/models/auth.py index 0d2ba97a2..d3987d566 100644 --- a/src/pretix/base/models/auth.py +++ b/src/pretix/base/models/auth.py @@ -346,7 +346,8 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): { 'user': self, 'messages': msg, - 'url': build_absolute_uri('control:user.settings') + 'url': build_absolute_uri('control:user.settings'), + 'instance': settings.PRETIX_INSTANCE_NAME, }, event=None, user=self, @@ -391,6 +392,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): 'user': self, 'reason': msg, 'code': code, + 'instance': settings.PRETIX_INSTANCE_NAME, }, event=None, user=self, @@ -430,6 +432,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin): mail( self.email, _('Password recovery'), 'pretixcontrol/email/forgot.txt', { + 'instance': settings.PRETIX_INSTANCE_NAME, 'user': self, 'url': (build_absolute_uri('control:auth.forgot.recover') + '?id=%d&token=%s' % (self.id, default_token_generator.make_token(self))) diff --git a/src/pretix/base/models/datasync.py b/src/pretix/base/models/datasync.py index e6b2bbd22..bdcc2dec4 100644 --- a/src/pretix/base/models/datasync.py +++ b/src/pretix/base/models/datasync.py @@ -86,7 +86,7 @@ class OrderSyncQueue(models.Model): def set_sync_error(self, failure_mode, messages, full_message): logger.exception( - f"Could not sync order {self.order.code} to {type(self).__name__} ({failure_mode})" + f"Could not sync order {self.order.code} to {self.sync_provider} ({failure_mode})" ) self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", { "provider": self.sync_provider, diff --git a/src/pretix/base/models/mail.py b/src/pretix/base/models/mail.py index 2128c2859..b228b44d0 100644 --- a/src/pretix/base/models/mail.py +++ b/src/pretix/base/models/mail.py @@ -220,3 +220,20 @@ 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, + } diff --git a/src/pretix/base/models/orders.py b/src/pretix/base/models/orders.py index a2b48ed98..466af8b0d 100644 --- a/src/pretix/base/models/orders.py +++ b/src/pretix/base/models/orders.py @@ -87,7 +87,6 @@ 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 ( @@ -1167,7 +1166,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, render_mail + from pretix.base.services.mail import mail if not self.email and not (position and position.attendee_email): return @@ -1177,32 +1176,20 @@ class Order(LockModel, LoggedModel): if position and position.attendee_email: recipient = position.attendee_email - email_content = render_mail(template, context) - if not isinstance(subject, FormattedString): - subject = format_map(subject, context) - mail( + outgoing_mail = 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, ) - 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 [], - } - ) + if outgoing_mail: + self.log_action( + log_entry_type, + user=user, + auth=auth, + data=outgoing_mail.log_data(), + ) def resend_link(self, user=None, auth=None): with language(self.locale, self.event.settings.region): @@ -2900,17 +2887,14 @@ 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, render_mail + from pretix.base.services.mail import mail if not self.attendee_email: return 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) - mail( + outgoing_mail = mail( recipient, subject, template, context, self.event, self.order.locale, order=self.order, headers=headers, sender=sender, position=self, @@ -2919,21 +2903,13 @@ class OrderPosition(AbstractPosition): attach_ical=attach_ical, attach_other_files=attach_other_files, ) - 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': [], - } - ) + if outgoing_mail: + self.order.log_action( + log_entry_type, + user=user, + auth=auth, + data=outgoing_mail.log_data(), + ) def resend_link(self, user=None, auth=None): diff --git a/src/pretix/base/models/waitinglist.py b/src/pretix/base/models/waitinglist.py index 6ae14591c..2e90ec069 100644 --- a/src/pretix/base/models/waitinglist.py +++ b/src/pretix/base/models/waitinglist.py @@ -34,10 +34,9 @@ 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, render_mail +from pretix.base.services.mail import 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 @@ -181,10 +180,11 @@ class WaitingListEntry(LoggedModel): block_quota=True, item_id=self.item_id, subevent_id=self.subevent_id, - waitinglistentries__isnull=False + waitinglistentries__isnull=False, + seat__isnull=True ).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0 free_seats = num_free_seats_for_product - num_valid_vouchers_for_product - if not free_seats: + if free_seats < 1: raise WaitingListException(_('No seat with this product is currently available.')) if '@' not in self.email: @@ -272,9 +272,7 @@ class WaitingListEntry(LoggedModel): with language(self.locale, self.event.settings.region): recipient = self.email - email_content = render_mail(template, context) - subject = format_map(subject, context) - mail( + outgoing_mail = mail( recipient, subject, template, context, self.event, self.locale, @@ -284,18 +282,13 @@ class WaitingListEntry(LoggedModel): attach_other_files=attach_other_files, attach_cached_files=attach_cached_files, ) - 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 [], - } - ) + if outgoing_mail: + self.log_action( + log_entry_type, + user=user, + auth=auth, + data=outgoing_mail.log_data(), + ) @staticmethod def clean_itemvar(event, item, variation): diff --git a/src/pretix/base/payment.py b/src/pretix/base/payment.py index 31b97526d..e7ad88cbc 100644 --- a/src/pretix/base/payment.py +++ b/src/pretix/base/payment.py @@ -1295,6 +1295,7 @@ 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, diff --git a/src/pretix/base/services/cancelevent.py b/src/pretix/base/services/cancelevent.py index 012f01b85..575a13f14 100644 --- a/src/pretix/base/services/cancelevent.py +++ b/src/pretix/base/services/cancelevent.py @@ -45,7 +45,6 @@ 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__) @@ -55,7 +54,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, - format_map(subject, email_context), + str(subject), message, email_context, wle.event, @@ -73,9 +72,8 @@ 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( - real_subject, message, email_context, + subject, message, email_context, 'pretix.event.order.email.event_canceled', user, ) @@ -85,14 +83,13 @@ 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( - real_subject, message, email_context, + subject, message, email_context, 'pretix.event.order.email.event_canceled', position=p, user=user diff --git a/src/pretix/base/services/cart.py b/src/pretix/base/services/cart.py index 6080378f0..59abf6c87 100644 --- a/src/pretix/base/services/cart.py +++ b/src/pretix/base/services/cart.py @@ -334,7 +334,8 @@ def _check_position_constraints( raise CartPositionError(error_messages['voucher_invalid_subevent']) # Voucher expired - if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt: + # (checked using real_now_dt as vouchers influence quota calculations) + if voucher and voucher.valid_until and voucher.valid_until < real_now_dt: raise CartPositionError(error_messages['voucher_expired']) # Subevent has been disabled diff --git a/src/pretix/base/services/invoices.py b/src/pretix/base/services/invoices.py index d76c48c99..deadc4d2f 100644 --- a/src/pretix/base/services/invoices.py +++ b/src/pretix/base/services/invoices.py @@ -51,6 +51,7 @@ from django_scopes import scope, scopes_disabled from i18nfield.strings import LazyI18nString from pretix.base.i18n import language +from pretix.base.invoicing.pdf import InvoiceNotReadyException from pretix.base.invoicing.transmission import ( get_transmission_types, transmission_providers, ) @@ -504,7 +505,7 @@ def generate_invoice(order: Order, trigger_pdf=True): return invoice -@app.task(base=TransactionAwareTask) +@app.task(base=TransactionAwareTask, throws=(InvoiceNotReadyException,)) def invoice_pdf_task(invoice: int): with scopes_disabled(): i = Invoice.objects.get(pk=invoice) diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index cbc5ddd6e..0d8849b24 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -149,13 +149,13 @@ def prefix_subject(settings_holder, subject, highlight=False): return subject -def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, LazyI18nString], +def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString], 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): + sensitive: bool=False) -> Optional[OutgoingMail]: """ Sends out an email to a user. The mail will be sent synchronously or asynchronously depending on the installation. @@ -335,14 +335,26 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La 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): - m.should_attach_cached_files.add(CachedFile.objects.get(pk=cf)) - else: - m.should_attach_cached_files.add(cf) + 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() send_task = mail_send_task.si( outgoing_mail=m.id @@ -364,6 +376,8 @@ def mail(email: Union[str, Sequence[str]], subject: str, template: Union[str, La lambda: chain(*task_chain).apply_async() ) + return m + class CustomEmail(EmailMultiAlternatives): def _create_mime_attachment(self, content, mimetype): @@ -389,7 +403,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") @@ -409,6 +423,18 @@ def mail_send_task(self, **kwargs) -> bool: outgoing_mail.inflight_since = now() outgoing_mail.save(update_fields=["status", "inflight_since"]) + # Performance optimization, saves database queries later on if we resolve the known relationships + if outgoing_mail.event_id: + assert outgoing_mail.event.organizer_id == outgoing_mail.organizer.pk + outgoing_mail.event.organizer = outgoing_mail.organizer + if outgoing_mail.order_id: + assert outgoing_mail.order.event_id == outgoing_mail.event_id + outgoing_mail.order.event = outgoing_mail.event + outgoing_mail.order.organizer = outgoing_mail.organizer + if outgoing_mail.orderposition_id: + assert outgoing_mail.orderposition.order_id == outgoing_mail.order_id + outgoing_mail.orderposition.order = outgoing_mail.order + headers = dict(outgoing_mail.headers) headers.setdefault('X-PX-Correlation', str(outgoing_mail.guid)) email = CustomEmail( @@ -443,15 +469,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: diff --git a/src/pretix/base/services/orders.py b/src/pretix/base/services/orders.py index 904113e15..c80d735f2 100644 --- a/src/pretix/base/services/orders.py +++ b/src/pretix/base/services/orders.py @@ -1799,8 +1799,6 @@ 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) @@ -1817,7 +1815,9 @@ 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)) - self._invoice_dirty = True + if pos.price: + # We do not consider the invoice dirty if only 0€-valued taxes are changed + self._invoice_dirty = True def cancel_fee(self, fee: OrderFee): self._totaldiff_guesstimate -= fee.value diff --git a/src/pretix/base/services/placeholders.py b/src/pretix/base/services/placeholders.py index 8ce503dab..f9b81554d 100644 --- a/src/pretix/base/services/placeholders.py +++ b/src/pretix/base/services/placeholders.py @@ -24,6 +24,7 @@ import logging from datetime import timedelta from decimal import Decimal +from django.db.models import Prefetch, prefetch_related_objects from django.dispatch import receiver from django.utils.formats import date_format from django.utils.html import escape, mark_safe @@ -35,6 +36,7 @@ from pretix.base.forms.widgets import format_placeholders_help_text from pretix.base.i18n import ( LazyCurrencyNumber, LazyDate, LazyExpiresDate, LazyNumber, ) +from pretix.base.models import EventMetaValue from pretix.base.reldate import RelativeDateWrapper from pretix.base.settings import PERSON_NAME_SCHEMES, get_name_parts_localized from pretix.base.signals import ( @@ -752,6 +754,11 @@ def base_placeholders(sender, **kwargs): name_scheme['sample'][f] )) + prefetch_related_objects( + [sender], + Prefetch('meta_values', queryset=EventMetaValue.objects.select_related("property"), to_attr="meta_values_cached") + ) + prefetch_related_objects([sender.organizer], Prefetch('meta_properties')) for k, v in sender.meta_data.items(): ph.append(MarkdownTextPlaceholder( 'meta_%s' % k, ['event'], lambda event, k=k: event.meta_data[k], diff --git a/src/pretix/base/services/shredder.py b/src/pretix/base/services/shredder.py index 6211ad4a8..43d88b362 100644 --- a/src/pretix/base/services/shredder.py +++ b/src/pretix/base/services/shredder.py @@ -176,6 +176,7 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo _('Data shredding completed'), 'pretixbase/email/shred_completed.txt', { + 'instance': settings.PRETIX_INSTANCE_NAME, 'user': user, 'organizer': event.organizer.name, 'event': str(event.name), diff --git a/src/pretix/base/services/vouchers.py b/src/pretix/base/services/vouchers.py index 452dfbaa8..8682c1dbd 100644 --- a/src/pretix/base/services/vouchers.py +++ b/src/pretix/base/services/vouchers.py @@ -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]) - mail( + outgoing_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': subject, - 'message': message, + 'subject': outgoing_mail.subject, + 'message': outgoing_mail.body_plain, }, save=False )) diff --git a/src/pretix/base/shredder.py b/src/pretix/base/shredder.py index 21b280305..bc38e72ab 100644 --- a/src/pretix/base/shredder.py +++ b/src/pretix/base/shredder.py @@ -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' + 'recipient', 'message', 'subject', 'full_mail', 'old_email', 'new_email', 'bcc', 'cc', ]) diff --git a/src/pretix/base/templates/error.html b/src/pretix/base/templates/error.html index 790f80d15..2dcc3e9fa 100644 --- a/src/pretix/base/templates/error.html +++ b/src/pretix/base/templates/error.html @@ -12,6 +12,9 @@ {% block custom_header %}{% endblock %} + {% if css_theme %} + + {% endif %}
diff --git a/src/pretix/base/templates/pretixbase/email/shred_completed.txt b/src/pretix/base/templates/pretixbase/email/shred_completed.txt index 726612c14..4bde98e16 100644 --- a/src/pretix/base/templates/pretixbase/email/shred_completed.txt +++ b/src/pretix/base/templates/pretixbase/email/shred_completed.txt @@ -13,5 +13,5 @@ Start time: {{ start_time }} (new data added after this time might not have been Best regards, -Your pretix team +Your {{ instance }} team {% endblocktrans %} diff --git a/src/pretix/base/templatetags/anonymize_email.py b/src/pretix/base/templatetags/anonymize_email.py new file mode 100644 index 000000000..ed29a4c46 --- /dev/null +++ b/src/pretix/base/templatetags/anonymize_email.py @@ -0,0 +1,34 @@ +# +# This file is part of pretix (Community Edition). +# +# Copyright (C) 2014-2020 Raphael Michel and contributors +# Copyright (C) 2020-today pretix GmbH and contributors +# +# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General +# Public License as published by the Free Software Foundation in version 3 of the License. +# +# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are +# applicable granting you additional permissions and placing additional restrictions on your usage of this software. +# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive +# this file, see . +# +# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied +# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along with this program. If not, see +# . +# +from django import template +from django.utils.html import mark_safe + +register = template.Library() + + +@register.filter("anon_email") +def anon_email(value): + """Replaces @ with [at] and . with [dot] for anonymization.""" + if not isinstance(value, str): + return value + value = value.replace("@", "[at]").replace(".", "[dot]") + return mark_safe(''.join(['&#{0};'.format(ord(char)) for char in value])) diff --git a/src/pretix/base/timeframes.py b/src/pretix/base/timeframes.py index f3f2b3a4d..d43261803 100644 --- a/src/pretix/base/timeframes.py +++ b/src/pretix/base/timeframes.py @@ -423,7 +423,7 @@ def resolve_timeframe_to_dates_inclusive(ref_dt, frame, timezone) -> Tuple[Optio raise ValueError(f"Invalid timeframe '{frame}'") -def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]: +def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[datetime], Optional[datetime]]: """ Given a serialized timeframe, evaluate it relative to `ref_dt` and return a tuple of datetimes where the first element ist the first possible datetime within the timeframe and the second diff --git a/src/pretix/control/forms/waitinglist.py b/src/pretix/control/forms/waitinglist.py index 7dc33c4a9..cf77380f6 100644 --- a/src/pretix/control/forms/waitinglist.py +++ b/src/pretix/control/forms/waitinglist.py @@ -19,17 +19,44 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +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, } diff --git a/src/pretix/control/logdisplay.py b/src/pretix/control/logdisplay.py index c12b866d0..2e22dd9fc 100644 --- a/src/pretix/control/logdisplay.py +++ b/src/pretix/control/logdisplay.py @@ -518,6 +518,7 @@ def pretixcontrol_orderposition_blocked_display(sender: Event, orderposition, bl 'The order requires approval before it can continue to be processed.'), 'pretix.event.order.approved': _('The order has been approved.'), 'pretix.event.order.denied': _('The order has been denied (comment: "{comment}").'), + 'pretix.event.order.vatid.validated': _('The customer VAT ID has been verified.'), 'pretix.event.order.contact.changed': _('The email address has been changed from "{old_email}" ' 'to "{new_email}".'), 'pretix.event.order.contact.confirmed': _( diff --git a/src/pretix/control/templates/pretixcontrol/auth/login.html b/src/pretix/control/templates/pretixcontrol/auth/login.html index ff9b9b3bb..fc23c24c1 100644 --- a/src/pretix/control/templates/pretixcontrol/auth/login.html +++ b/src/pretix/control/templates/pretixcontrol/auth/login.html @@ -19,6 +19,14 @@
{% endif %} + {% if possible_cookie_problem %} +
+ {% 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 %} +
+ {% endif %} {% csrf_token %} {% bootstrap_form form %}
diff --git a/src/pretix/control/templates/pretixcontrol/email/confirmation_code.txt b/src/pretix/control/templates/pretixcontrol/email/confirmation_code.txt index 5ac75436d..27966e6d9 100644 --- a/src/pretix/control/templates/pretixcontrol/email/confirmation_code.txt +++ b/src/pretix/control/templates/pretixcontrol/email/confirmation_code.txt @@ -9,5 +9,5 @@ Please do never give this code to another person. Our support team will never as If this code was not requested by you, please contact us immediately. Best regards, -Your pretix team +Your {{ instance }} team {% endblocktrans %} diff --git a/src/pretix/control/templates/pretixcontrol/email/forgot.txt b/src/pretix/control/templates/pretixcontrol/email/forgot.txt index c777e33e8..a9292eb52 100644 --- a/src/pretix/control/templates/pretixcontrol/email/forgot.txt +++ b/src/pretix/control/templates/pretixcontrol/email/forgot.txt @@ -5,5 +5,5 @@ you requested a new password. Please go to the following page to reset your pass {{ url }} Best regards, -Your pretix team -{% endblocktrans %} \ No newline at end of file +Your {{ instance }} team +{% endblocktrans %} diff --git a/src/pretix/control/templates/pretixcontrol/email/invitation.txt b/src/pretix/control/templates/pretixcontrol/email/invitation.txt index 9742db4fa..7b3b6c160 100644 --- a/src/pretix/control/templates/pretixcontrol/email/invitation.txt +++ b/src/pretix/control/templates/pretixcontrol/email/invitation.txt @@ -1,6 +1,6 @@ {% load i18n %}{% blocktrans with url=url|safe %}Hello, -you have been invited to a team on pretix, a platform to perform event +you have been invited to a team on {{ instance }}, a platform to perform event ticket sales. Organizer: {{ organizer }} @@ -13,5 +13,5 @@ If you do not want to join, you can safely ignore or delete this email. Best regards, -Your pretix team +Your {{ instance }} team {% endblocktrans %} diff --git a/src/pretix/control/templates/pretixcontrol/email/security_notice.txt b/src/pretix/control/templates/pretixcontrol/email/security_notice.txt index 3eceb3608..a8f6531f3 100644 --- a/src/pretix/control/templates/pretixcontrol/email/security_notice.txt +++ b/src/pretix/control/templates/pretixcontrol/email/security_notice.txt @@ -1,6 +1,6 @@ {% load i18n %}{% blocktrans with url=url|safe messages=messages|safe %}Hello, -this is to inform you that the account information of your pretix account has been +this is to inform you that the account information of your {{ instance }} account has been changed. In particular, the following changes have been performed: {{ messages }} @@ -12,5 +12,5 @@ You can review and change your account settings here: {{ url }} Best regards, -Your pretix team +Your {{ instance }} team {% endblocktrans %} diff --git a/src/pretix/control/templates/pretixcontrol/events/index.html b/src/pretix/control/templates/pretixcontrol/events/index.html index ae09794ab..4ef033f7f 100644 --- a/src/pretix/control/templates/pretixcontrol/events/index.html +++ b/src/pretix/control/templates/pretixcontrol/events/index.html @@ -2,6 +2,7 @@ {% load i18n %} {% load urlreplace %} {% load bootstrap3 %} +{% load static %} {% block title %}{% trans "Events" %}{% endblock %} {% block content %}

{% trans "Events" %}

@@ -74,6 +75,7 @@ {% endif %} + {% trans "Sales channels" %} {% trans "Start date" %} @@ -108,6 +110,21 @@ {% endfor %} {% if not hide_orga %}{{ e.organizer }}{% endif %} + + {% for c in e.organizer.sales_channels.all %} + {% if e.all_sales_channels or c in e.limit_sales_channels.all %} + {% if "." in c.icon %} + + {% else %} + + {% endif %} + {% else %} + + {% endif %} + {% endfor %} + {% if e.has_subevents %} diff --git a/src/pretix/control/templates/pretixcontrol/order/mail_history.html b/src/pretix/control/templates/pretixcontrol/order/mail_history.html index 85d85693f..1e814f054 100644 --- a/src/pretix/control/templates/pretixcontrol/order/mail_history.html +++ b/src/pretix/control/templates/pretixcontrol/order/mail_history.html @@ -24,7 +24,9 @@ {% if log.display %}
{{ log.display }} {% endif %} - {% if log.parsed_data.recipient %} + {% if log.parsed_data.to %} +
{{ log.parsed_data.to|join:", " }} + {% elif log.parsed_data.recipient %} {# legacy #}
{{ log.parsed_data.recipient }} {% endif %}

diff --git a/src/pretix/control/templates/pretixcontrol/organizers/detail.html b/src/pretix/control/templates/pretixcontrol/organizers/detail.html index 7c2373e72..a1315c8de 100644 --- a/src/pretix/control/templates/pretixcontrol/organizers/detail.html +++ b/src/pretix/control/templates/pretixcontrol/organizers/detail.html @@ -1,6 +1,7 @@ {% extends "pretixcontrol/organizers/base.html" %} {% load i18n %} {% load bootstrap3 %} +{% load static %} {% block inner %}

{% blocktrans with name=request.organizer.name %}Organizer: {{ name }}{% endblocktrans %} @@ -62,6 +63,7 @@ {% trans "Event name" %} + {% trans "Sales channels" %} {% trans "Start date" %} / @@ -77,10 +79,30 @@ {{ e.name }} -
{{ e.slug }} - {% for k, v in e.meta_data.items %} - {% if v %} - · {{ k }}: {{ v }} +
+ + {{ e.slug }} + + + {% for k, v in e.meta_data.items %} + {% if v %} + · {{ k }}: {{ v }} + {% endif %} + {% endfor %} + + + + {% for c in sales_channels %} + {% if e.all_sales_channels or c in e.limit_sales_channels.all %} + {% if "." in c.icon %} + + {% else %} + + {% endif %} + {% else %} + {% endif %} {% endfor %} diff --git a/src/pretix/control/templates/pretixcontrol/pdf/index.html b/src/pretix/control/templates/pretixcontrol/pdf/index.html index ffe126435..0935a6c12 100644 --- a/src/pretix/control/templates/pretixcontrol/pdf/index.html +++ b/src/pretix/control/templates/pretixcontrol/pdf/index.html @@ -264,12 +264,17 @@ The paper size will match the PDF. {% endblocktrans %}

-

+

{% trans "Upload PDF as background" %} + + {% blocktrans trimmed with size=maxfilesize|filesizeformat %} + max. {{ size }}, smaller is better + {% endblocktrans %} +

diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/edit.html b/src/pretix/control/templates/pretixcontrol/waitinglist/edit.html new file mode 100644 index 000000000..d4362f9c0 --- /dev/null +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/edit.html @@ -0,0 +1,33 @@ +{% extends "pretixcontrol/event/base.html" %} +{% load i18n %} +{% load bootstrap3 %} +{% block title %}{% trans "Edit entry" %}{% endblock %} +{% block content %} +

{% trans "Edit entry" %}

+
+ {% 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" %} +
+ + {% trans "Cancel" %} + + +
+
+{% endblock %} diff --git a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html index 1539b2c9d..42c4a9033 100644 --- a/src/pretix/control/templates/pretixcontrol/waitinglist/index.html +++ b/src/pretix/control/templates/pretixcontrol/waitinglist/index.html @@ -124,6 +124,7 @@ {% endfor %} + {% if request.event.has_subevents %}