Compare commits

..

1 Commits

Author SHA1 Message Date
Raphael Michel ba7d4ec13c API: Fix crash in check-in API (PRETIXEU-CT1) 2026-01-13 13:16:13 +01:00
308 changed files with 169233 additions and 235342 deletions
+14
View File
@@ -208,6 +208,20 @@ Additionally, when creating a device through the user interface or API, a user c
the device. These include an allow list of specific API calls that may be made by the device. pretix ships with security
policies for official pretix apps like pretixSCAN and pretixPOS.
Removing a device
-----------------
If you want implement a way to to deprovision a device in your software, you can call the ``revoke`` endpoint to
invalidate your API key. There is no way to reverse this operation.
.. sourcecode:: http
POST /api/v1/device/revoke HTTP/1.1
Host: pretix.eu
Authorization: Device 1kcsh572fonm3hawalrncam4l1gktr2rzx25a22l8g9hx108o9oi0rztpcvwnfnd
This can also be done by the user through the web interface.
Event selection
---------------
-50
View File
@@ -1719,56 +1719,6 @@ 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
-----------------------------
-3
View File
@@ -60,9 +60,6 @@ The following values for ``action_types`` are valid with pretix core:
* ``pretix.event.added``
* ``pretix.event.changed``
* ``pretix.event.deleted``
* ``pretix.giftcards.created``
* ``pretix.giftcards.modified``
* ``pretix.giftcards.transaction.*``
* ``pretix.voucher.added``
* ``pretix.voucher.changed``
* ``pretix.voucher.deleted``
+2 -2
View File
@@ -1,6 +1,6 @@
sphinx==9.1.*
sphinx-rtd-theme~=3.1.0
sphinxcontrib-httpdomain~=2.0.0
sphinx-rtd-theme~=3.1.0rc2
sphinxcontrib-httpdomain~=1.8.1
sphinxcontrib-images~=1.0.1
sphinxcontrib-jquery~=4.1
sphinxcontrib-spelling~=8.0.2
+2 -2
View File
@@ -1,7 +1,7 @@
-e ../
sphinx==9.1.*
sphinx-rtd-theme~=3.1.0
sphinxcontrib-httpdomain~=2.0.0
sphinx-rtd-theme~=3.1.0rc2
sphinxcontrib-httpdomain~=1.8.1
sphinxcontrib-images~=1.0.1
sphinxcontrib-jquery~=4.1
sphinxcontrib-spelling~=8.0.2
+9 -9
View File
@@ -33,7 +33,7 @@ dependencies = [
"celery==5.6.*",
"chardet==5.2.*",
"cryptography>=44.0.0",
"css-inline==0.20.*",
"css-inline==0.19.*",
"defusedcsv>=1.1.0",
"dnspython==2.*",
"Django[argon2]==4.2.*,>=4.2.26",
@@ -41,7 +41,7 @@ dependencies = [
"django-compressor==4.6.0",
"django-countries==8.2.*",
"django-filter==25.1",
"django-formset-js-improved==0.5.0.5",
"django-formset-js-improved==0.5.0.4",
"django-formtools==2.5.1",
"django-hierarkey==2.0.*,>=2.0.1",
"django-hijack==3.7.*",
@@ -65,7 +65,7 @@ dependencies = [
"kombu==5.6.*",
"libsass==0.23.*",
"lxml",
"markdown==3.10.2", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
"markdown==3.10", # 3.3.5 requires importlib-metadata>=4.4, but django-bootstrap3 requires importlib-metadata<3.
# We can upgrade markdown again once django-bootstrap3 upgrades or once we drop Python 3.6 and 3.7
"mt-940==4.30.*",
"oauthlib==3.3.*",
@@ -73,14 +73,14 @@ dependencies = [
"packaging",
"paypalrestsdk==1.13.*",
"paypal-checkout-serversdk==1.0.*",
"PyJWT==2.11.*",
"PyJWT==2.10.*",
"phonenumberslite==9.0.*",
"Pillow==12.1.*",
"pretix-plugin-build",
"protobuf==7.34.*",
"protobuf==6.33.*",
"psycopg2-binary",
"pycountry",
"pycparser==3.0",
"pycparser==2.23",
"pycryptodome==3.23.*",
"pypdf==6.5.*",
"python-bidi==0.6.*", # Support for Arabic in reportlab
@@ -92,7 +92,7 @@ dependencies = [
"redis==7.1.*",
"reportlab==4.4.*",
"requests==2.32.*",
"sentry-sdk==2.54.*",
"sentry-sdk==2.49.*",
"sepaxml==2.7.*",
"stripe==7.9.*",
"text-unidecode==1.*",
@@ -110,10 +110,10 @@ dev = [
"aiohttp==3.13.*",
"coverage",
"coveralls",
"fakeredis==2.34.*",
"fakeredis==2.33.*",
"flake8==7.3.*",
"freezegun",
"isort==8.0.*",
"isort==7.0.*",
"pep8-naming==0.15.*",
"potypo",
"pytest-asyncio>=0.24",
+1 -1
View File
@@ -19,4 +19,4 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
__version__ = "2026.3.0.dev0"
__version__ = "2025.11.0.dev0"
-1
View File
@@ -806,7 +806,6 @@ class EventSettingsSerializer(SettingsSerializer):
'invoice_reissue_after_modify',
'invoice_include_free',
'invoice_generate',
'invoice_generate_only_business',
'invoice_period',
'invoice_numbers_consecutive',
'invoice_numbers_prefix',
+2 -34
View File
@@ -19,7 +19,6 @@
# 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/>.
#
import json
import logging
import os
from collections import Counter, defaultdict
@@ -192,7 +191,7 @@ class InvoiceAddressSerializer(I18nAwareModelSerializer):
{"transmission_info": {r: "This field is required for the selected type of invoice transmission."}}
)
break # do not call else branch of for loop
elif t.is_exclusive(self.context["request"].event, data.get("country"), data.get("is_business")):
elif t.exclusive:
if t.is_available(self.context["request"].event, data.get("country"), data.get("is_business")):
raise ValidationError({
"transmission_type": "The transmission type '%s' must be used for this country or address type." % (
@@ -637,14 +636,6 @@ 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
@@ -713,16 +704,6 @@ class CheckinListOrderPositionSerializer(OrderPositionSerializer):
if 'answers.question' in self.context['expand']:
self.fields['answers'].child.fields['question'] = QuestionSerializer(read_only=True)
if 'addons' in self.context['expand']:
# Experimental feature, undocumented on purpose for now in case we need to remove it again
# for performance reasons
subl = CheckinListOrderPositionSerializer(read_only=True, many=True, context={
**self.context,
'expand': [v for v in self.context['expand'] if v != 'addons'],
'pdf_data': False,
})
self.fields['addons'] = subl
class OrderPaymentTypeField(serializers.Field):
# TODO: Remove after pretix 2.2
@@ -1224,18 +1205,6 @@ 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.')
@@ -1632,7 +1601,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
order.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.price,
cp.addon_to, cp.is_bundled, pos._voucher_discount)
bool(cp.addon_to), cp.is_bundled, pos._voucher_discount)
for cp in order_positions
]
)
@@ -1764,7 +1733,6 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
rounding_mode = self.context["event"].settings.tax_rounding
changed = apply_rounding(
rounding_mode,
ia,
self.context["event"].currency,
[*pos_map.values(), *fees]
)
+19 -18
View File
@@ -49,7 +49,7 @@ from pretix.base.plugins import (
PLUGIN_LEVEL_EVENT, PLUGIN_LEVEL_EVENT_ORGANIZER_HYBRID,
PLUGIN_LEVEL_ORGANIZER,
)
from pretix.base.services.mail import mail
from pretix.base.services.mail import SendMailException, mail
from pretix.base.settings import validate_organizer_settings
from pretix.helpers.urls import build_absolute_uri as build_global_uri
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -363,22 +363,24 @@ class TeamInviteSerializer(serializers.ModelSerializer):
)
def _send_invite(self, instance):
mail(
instance.email,
_('Account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,
'url': build_global_uri('control:auth.invite', kwargs={
'token': instance.token
})
},
event=None,
locale=get_language_without_region() # TODO: expose?
)
try:
mail(
instance.email,
_('pretix account invitation'),
'pretixcontrol/email/invitation.txt',
{
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,
'url': build_global_uri('control:auth.invite', kwargs={
'token': instance.token
})
},
event=None,
locale=get_language_without_region() # TODO: expose?
)
except SendMailException:
pass # Already logged
def create(self, validated_data):
if 'email' in validated_data:
@@ -441,7 +443,6 @@ class OrganizerSettingsSerializer(SettingsSerializer):
'customer_accounts',
'customer_accounts_native',
'customer_accounts_link_by_email',
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'imprint_url',
+1 -2
View File
@@ -67,7 +67,6 @@ 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)
@@ -84,7 +83,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.EventOrderPositionViewSet)
event_router.register(r'orderpositions', order.OrderPositionViewSet)
event_router.register(r'transactions', order.TransactionViewSet)
event_router.register(r'invoices', order.InvoiceViewSet)
event_router.register(r'revokedsecrets', order.RevokedSecretViewSet, basename='revokedsecrets')
+25 -42
View File
@@ -188,15 +188,11 @@ class CheckinListViewSet(viewsets.ModelViewSet):
clist = self.get_object()
if serializer.validated_data.get('nonce'):
if kwargs.get('position'):
prev = kwargs['position'].all_checkins.filter(
nonce=serializer.validated_data['nonce'],
successful=False
).first()
prev = kwargs['position'].all_checkins.filter(nonce=serializer.validated_data['nonce']).first()
else:
prev = clist.checkins.filter(
nonce=serializer.validated_data['nonce'],
raw_barcode=serializer.validated_data['raw_barcode'],
successful=False
).first()
if prev:
# Ignore because nonce is already handled
@@ -385,21 +381,15 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
qs = qs.filter(reduce(operator.or_, lists_qs))
prefetch_related = [
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
]
select_related = [
'item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat'
]
if pdf_data:
qs = qs.prefetch_related(
# Don't add to list, we don't want to propagate to addons
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation')),
Prefetch('order', Order.objects.select_related('invoice_address').prefetch_related(
Prefetch(
'event',
@@ -414,39 +404,32 @@ def _checkin_list_position_queryset(checkinlists, ignore_status=False, ignore_pr
)
)
))
).select_related(
'item', 'variation', 'item__category', 'addon_to', 'order', 'order__invoice_address', 'seat'
)
else:
qs = qs.prefetch_related(
Prefetch(
lookup='checkins',
queryset=Checkin.objects.filter(list_id__in=[cl.pk for cl in checkinlists]).select_related('device')
),
Prefetch('print_logs', queryset=PrintLog.objects.select_related('device')),
'answers', 'answers__options', 'answers__question',
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
).select_related('item', 'variation', 'order', 'addon_to', 'order__invoice_address', 'order', 'seat')
if expand and 'subevent' in expand:
prefetch_related += [
qs = qs.prefetch_related(
'subevent', 'subevent__event', 'subevent__subeventitem_set', 'subevent__subeventitemvariation_set',
'subevent__seat_category_mappings', 'subevent__meta_values'
]
)
if expand and 'item' in expand:
prefetch_related += [
'item', 'item__addons', 'item__bundles', 'item__meta_values',
'item__variations',
]
select_related.append('item__tax_rule')
qs = qs.prefetch_related('item', 'item__addons', 'item__bundles', 'item__meta_values',
'item__variations').select_related('item__tax_rule')
if expand and 'variation' in expand:
prefetch_related += [
'variation', 'variation__meta_values',
]
if expand and 'addons' in expand:
prefetch_related += [
Prefetch('addons', OrderPosition.objects.prefetch_related(*prefetch_related).select_related(*select_related)),
]
else:
prefetch_related += [
Prefetch('addons', OrderPosition.objects.select_related('item', 'variation'))
]
if pdf_data:
select_related.remove("order") # Don't need it twice on this queryset
qs = qs.prefetch_related(*prefetch_related).select_related(*select_related)
qs = qs.prefetch_related('variation', 'variation__meta_values')
return qs
+1 -1
View File
@@ -106,7 +106,7 @@ class ItemViewSet(ConditionalListView, viewsets.ModelViewSet):
'variations', 'addons', 'bundles', 'meta_values', 'meta_values__property',
'variations__meta_values', 'variations__meta_values__property',
'require_membership_types', 'variations__require_membership_types',
'limit_sales_channels', 'variations__limit_sales_channels', 'program_times'
'limit_sales_channels', 'variations__limit_sales_channels',
).all()
def perform_create(self, serializer):
+24 -51
View File
@@ -57,10 +57,9 @@ from pretix.api.serializers.order import (
BlockedTicketSecretSerializer, InvoiceSerializer, OrderCreateSerializer,
OrderPaymentCreateSerializer, OrderPaymentSerializer,
OrderPositionSerializer, OrderRefundCreateSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerOrderPositionSerializer,
OrganizerTransactionSerializer, PriceCalcSerializer, PrintLogSerializer,
RevokedTicketSecretSerializer, SimulatedOrderSerializer,
TransactionSerializer,
OrderRefundSerializer, OrderSerializer, OrganizerTransactionSerializer,
PriceCalcSerializer, PrintLogSerializer, RevokedTicketSecretSerializer,
SimulatedOrderSerializer, TransactionSerializer,
)
from pretix.api.serializers.orderchange import (
BlockNameSerializer, OrderChangeOperationSerializer,
@@ -91,6 +90,7 @@ from pretix.base.services.invoices import (
generate_cancellation, generate_invoice, invoice_pdf, invoice_qualified,
regenerate_invoice, transmit_invoice,
)
from pretix.base.services.mail import SendMailException
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _order_placed_email,
_order_placed_email_attendee, approve_order, cancel_order, deny_order,
@@ -439,6 +439,8 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except SendMailException:
pass
return self.retrieve(request, [], **kwargs)
return Response(
@@ -632,7 +634,10 @@ class EventOrderViewSet(OrderViewSetMixin, viewsets.ModelViewSet):
order = self.get_object()
if not order.email:
return Response({'detail': 'There is no email address associated with this order.'}, status=status.HTTP_400_BAD_REQUEST)
order.resend_link(user=self.request.user, auth=self.request.auth)
try:
order.resend_link(user=self.request.user, auth=self.request.auth)
except SendMailException:
return Response({'detail': _('There was an error sending the mail. Please try again later.')}, status=status.HTTP_503_SERVICE_UNAVAILABLE)
return Response(
status=status.HTTP_204_NO_CONTENT
@@ -1066,7 +1071,8 @@ with scopes_disabled():
}
class OrderPositionViewSetMixin:
class OrderPositionViewSet(viewsets.ModelViewSet):
serializer_class = OrderPositionSerializer
queryset = OrderPosition.all.none()
filter_backends = (DjangoFilterBackend, RichOrderingFilter)
ordering = ('order__datetime', 'positionid')
@@ -1087,7 +1093,8 @@ class OrderPositionViewSetMixin:
def get_serializer_context(self):
ctx = super().get_serializer_context()
ctx['pdf_data'] = False
ctx['event'] = self.request.event
ctx['pdf_data'] = self.request.query_params.get('pdf_data', 'false').lower() == 'true'
ctx['check_quotas'] = self.request.query_params.get('check_quotas', 'true').lower() == 'true'
return ctx
@@ -1096,8 +1103,9 @@ class OrderPositionViewSetMixin:
qs = OrderPosition.all
else:
qs = OrderPosition.objects
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):
qs = qs.filter(order__event=self.request.event)
if self.request.query_params.get('pdf_data', 'false').lower() == 'true':
prefetch_related_objects([self.request.organizer], 'meta_properties')
prefetch_related_objects(
[self.request.event],
@@ -1152,9 +1160,9 @@ class OrderPositionViewSetMixin:
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', 'order__event', 'order__event__organizer'
'answers', 'answers__options', 'answers__question',
).select_related(
'item', 'order', 'seat'
'item', 'order', 'order__event', 'order__event__organizer', 'seat'
)
return qs
@@ -1166,45 +1174,6 @@ class OrderPositionViewSetMixin:
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):
"""
@@ -1647,6 +1616,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
)
except Quota.QuotaExceededException:
pass
except SendMailException:
pass
serializer = OrderPaymentSerializer(r, context=serializer.context)
@@ -1684,6 +1655,8 @@ class PaymentViewSet(CreateModelMixin, viewsets.ReadOnlyModelViewSet):
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except PaymentException as e:
return Response({'detail': str(e)}, status=status.HTTP_400_BAD_REQUEST)
except SendMailException:
pass
return self.retrieve(request, [], **kwargs)
@action(detail=True, methods=['POST'])
@@ -2058,7 +2031,7 @@ class InvoiceViewSet(viewsets.ReadOnlyModelViewSet):
else:
order = Order.objects.select_for_update(of=OF_SELF).get(pk=inv.order_id)
c = generate_cancellation(inv)
if invoice_qualified(order):
if inv.order.status != Order.STATUS_CANCELED:
inv = generate_invoice(order)
else:
inv = c
+7 -28
View File
@@ -249,24 +249,12 @@ class GiftCardViewSet(viewsets.ModelViewSet):
def perform_create(self, serializer):
value = serializer.validated_data.pop('value')
inst = serializer.save(issuer=self.request.organizer)
inst.log_action(
action='pretix.giftcards.created',
user=self.request.user,
auth=self.request.auth,
)
inst.transactions.create(value=value, acceptor=self.request.organizer)
inst.log_action(
action='pretix.giftcards.transaction.manual',
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data=merge_dicts(
self.request.data,
{
'id': inst.pk,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
)
data=merge_dicts(self.request.data, {'id': inst.pk})
)
@transaction.atomic()
@@ -281,7 +269,7 @@ class GiftCardViewSet(viewsets.ModelViewSet):
inst = serializer.save(secret=serializer.instance.secret, currency=serializer.instance.currency,
testmode=serializer.instance.testmode)
inst.log_action(
action='pretix.giftcards.modified',
'pretix.giftcards.modified',
user=self.request.user,
auth=self.request.auth,
data=self.request.data,
@@ -294,14 +282,10 @@ class GiftCardViewSet(viewsets.ModelViewSet):
diff = value - old_value
inst.transactions.create(value=diff, acceptor=self.request.organizer)
inst.log_action(
action='pretix.giftcards.transaction.manual',
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={
'value': diff,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
data={'value': diff}
)
return inst
@@ -325,15 +309,10 @@ class GiftCardViewSet(viewsets.ModelViewSet):
}, status=status.HTTP_409_CONFLICT)
gc.transactions.create(value=value, text=text, info=info, acceptor=self.request.organizer)
gc.log_action(
action='pretix.giftcards.transaction.manual',
'pretix.giftcards.transaction.manual',
user=self.request.user,
auth=self.request.auth,
data={
'value': value,
'text': text,
'acceptor_id': self.request.organizer.id,
'acceptor_slug': self.request.organizer.slug
}
data={'value': value, 'text': text}
)
return Response(GiftCardSerializer(gc, context=self.get_serializer_context()).data, status=status.HTTP_200_OK)
-44
View File
@@ -174,38 +174,6 @@ class ParametrizedEventWebhookEvent(ParametrizedWebhookEvent):
}
class ParametrizedGiftcardWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
giftcard = logentry.content_object
if not giftcard:
return None
return {
'notification_id': logentry.pk,
'issuer_id': logentry.organizer_id,
'issuer_slug': logentry.organizer.slug,
'giftcard': giftcard.pk,
'action': logentry.action_type,
}
class ParametrizedGiftcardTransactionWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
giftcard = logentry.content_object
if not giftcard:
return None
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,
}
class ParametrizedVoucherWebhookEvent(ParametrizedWebhookEvent):
def build_payload(self, logentry: LogEntry):
@@ -465,18 +433,6 @@ def register_default_webhook_events(sender, **kwargs):
'pretix.customer.anonymized',
_('Customer account anonymized'),
),
ParametrizedGiftcardWebhookEvent(
'pretix.giftcards.created',
_('Gift card added'),
),
ParametrizedGiftcardWebhookEvent(
'pretix.giftcards.modified',
_('Gift card modified'),
),
ParametrizedGiftcardTransactionWebhookEvent(
'pretix.giftcards.transaction.*',
_('Gift card used in transaction'),
)
)
+2 -5
View File
@@ -216,10 +216,7 @@ class OutboundSyncProvider:
try:
mapped_objects = self.sync_order(sq.order)
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:
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()):
sq.order.log_action("pretix.event.order.data_sync.success", {
"provider": self.identifier,
"objects": {
@@ -240,7 +237,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 {sq.sync_provider} "
f"Could not sync order {sq.order.code} to {type(self).__name__} "
f"(transient error, attempt #{sq.failed_attempts}, next {sq.not_before})",
exc_info=True,
)
+7 -9
View File
@@ -39,7 +39,7 @@ from pretix.base.templatetags.rich_text import (
DEFAULT_CALLBACKS, EMAIL_RE, URL_RE, abslink_callback,
markdown_compile_email, truelink_callback,
)
from pretix.helpers.format import FormattedString, SafeFormatter, format_map
from pretix.helpers.format import SafeFormatter, format_map
from pretix.base.services.placeholders import ( # noqa
get_available_placeholders, PlaceholderContext
@@ -141,7 +141,6 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
return markdown_compile_email(plaintext, context=context)
def render(self, plain_body: str, plain_signature: str, subject: str, order, position, context) -> str:
apply_format_map = not isinstance(plain_body, FormattedString)
body_md = self.compile_markdown(plain_body, context)
if context:
linker = bleach.Linker(
@@ -150,13 +149,12 @@ class TemplateBasedMailRenderer(BaseHTMLMailRenderer):
callbacks=DEFAULT_CALLBACKS + [truelink_callback, abslink_callback],
parse_email=True
)
if apply_format_map:
body_md = format_map(
body_md,
context=context,
mode=SafeFormatter.MODE_RICH_TO_HTML,
linkifier=linker
)
body_md = format_map(
body_md,
context=context,
mode=SafeFormatter.MODE_RICH_TO_HTML,
linkifier=linker
)
htmlctx = {
'site': settings.PRETIX_INSTANCE_NAME,
'site_url': settings.SITE_URL,
+6 -43
View File
@@ -39,8 +39,8 @@ from zoneinfo import ZoneInfo
from django import forms
from django.conf import settings
from django.db.models import (
Case, CharField, Count, DateTimeField, Exists, F, IntegerField, Max, Min,
OuterRef, Q, Subquery, Sum, When,
Case, CharField, Count, DateTimeField, F, IntegerField, Max, Min, OuterRef,
Q, Subquery, Sum, When,
)
from django.db.models.functions import Coalesce
from django.dispatch import receiver
@@ -144,18 +144,6 @@ class OrderListExporter(MultiSheetListExporter):
d = OrderedDict(d)
if not self.is_multievent and not self.event.has_subevents:
del d['event_date_range']
if not self.is_multievent:
d["items"] = forms.ModelMultipleChoiceField(
label=_("Products"),
queryset=self.event.items.all(),
widget=forms.CheckboxSelectMultiple(
attrs={"class": "scrolling-multiple-choice"}
),
help_text=_("If none are selected, all products are included. Orders are included if they contain "
"at least one position of this product. The order totals etc. still include all products "
"contained in the order."),
required=False,
)
return d
def _get_all_payment_methods(self, qs):
@@ -261,14 +249,6 @@ class OrderListExporter(MultiSheetListExporter):
pcnt=Subquery(s, output_field=IntegerField())
).select_related('invoice_address', 'customer')
if form_data.get('items'):
qs = qs.filter(
Exists(OrderPosition.all.filter(
order=OuterRef('pk'),
item__in=form_data["items"]
))
)
qs = self._date_filter(qs, form_data, rel='')
if form_data['paid_only']:
@@ -384,7 +364,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state_for_address,
order.invoice_address.state,
order.invoice_address.custom_field,
order.invoice_address.vat_id,
]
@@ -460,14 +440,6 @@ class OrderListExporter(MultiSheetListExporter):
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
if form_data.get('items'):
qs = qs.filter(
Exists(OrderPosition.all.filter(
order=OuterRef('order'),
item__in=form_data["items"]
))
)
qs = self._date_filter(qs, form_data, rel='order__')
return qs
@@ -543,7 +515,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state_for_address,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
@@ -563,11 +535,6 @@ class OrderListExporter(MultiSheetListExporter):
if form_data['paid_only']:
qs = qs.filter(order__status=Order.STATUS_PAID, canceled=False)
if form_data.get('items'):
qs = qs.filter(
item__in=form_data["items"]
)
qs = self._date_filter(qs, form_data, rel='order__')
return qs
@@ -650,8 +617,6 @@ class OrderListExporter(MultiSheetListExporter):
_('Country'),
pgettext('address', 'State'),
_('Voucher'),
_('Voucher budget usage'),
_('Voucher tag'),
_('Pseudonymization ID'),
_('Ticket secret'),
_('Seat ID'),
@@ -767,10 +732,8 @@ class OrderListExporter(MultiSheetListExporter):
op.zipcode or '',
op.city or '',
op.country if op.country else '',
op.state_for_address or '',
op.state 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,
]
@@ -834,7 +797,7 @@ class OrderListExporter(MultiSheetListExporter):
order.invoice_address.city,
order.invoice_address.country if order.invoice_address.country else
order.invoice_address.country_old,
order.invoice_address.state_for_address,
order.invoice_address.state,
order.invoice_address.vat_id,
]
except InvoiceAddress.DoesNotExist:
+7 -8
View File
@@ -890,18 +890,18 @@ class BaseQuestionsForm(forms.Form):
if not help_text:
if q.valid_date_min and q.valid_date_max:
help_text = format_lazy(
_('Please enter a date between {min} and {max}.'),
'Please enter a date between {min} and {max}.',
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
)
elif q.valid_date_min:
help_text = format_lazy(
_('Please enter a date no earlier than {min}.'),
'Please enter a date no earlier than {min}.',
min=date_format(q.valid_date_min, "SHORT_DATE_FORMAT"),
)
elif q.valid_date_max:
help_text = format_lazy(
_('Please enter a date no later than {max}.'),
'Please enter a date no later than {max}.',
max=date_format(q.valid_date_max, "SHORT_DATE_FORMAT"),
)
if initial and initial.answer:
@@ -939,18 +939,18 @@ class BaseQuestionsForm(forms.Form):
if not help_text:
if q.valid_datetime_min and q.valid_datetime_max:
help_text = format_lazy(
_('Please enter a date and time between {min} and {max}.'),
'Please enter a date and time between {min} and {max}.',
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
)
elif q.valid_datetime_min:
help_text = format_lazy(
_('Please enter a date and time no earlier than {min}.'),
'Please enter a date and time no earlier than {min}.',
min=date_format(q.valid_datetime_min, "SHORT_DATETIME_FORMAT"),
)
elif q.valid_datetime_max:
help_text = format_lazy(
_('Please enter a date and time no later than {max}.'),
'Please enter a date and time no later than {max}.',
max=date_format(q.valid_datetime_max, "SHORT_DATETIME_FORMAT"),
)
@@ -1415,10 +1415,9 @@ 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")):
elif transmission_type.exclusive:
if transmission_type.is_available(self.event, data.get("country"), data.get("is_business")):
raise ValidationError({
"transmission_type": "The transmission type '%s' must be used for this country or address type." % (
+1 -10
View File
@@ -42,8 +42,6 @@ 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):
@@ -63,18 +61,11 @@ 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 ('<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (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 = [
format_placeholder_help_text(k, v)
'<button type="button" class="content-placeholder" title="%s">{%s}</button>' % (escape(_("Sample: %s") % v) if v else "", escape(k))
for k, v in placeholders
]
return _('Available placeholders: {list}').format(
+12 -50
View File
@@ -34,13 +34,14 @@
from contextlib import contextmanager
from asgiref.local import Local
from babel import localedata
from django.conf import settings
from django.utils import translation
from django.utils.formats import date_format, number_format
from django.utils.translation import gettext
from pretix.base.templatetags.money import money_filter
from i18nfield.fields import ( # noqa
I18nCharField, I18nTextarea, I18nTextField, I18nTextInput,
)
@@ -50,9 +51,6 @@ from i18nfield.strings import LazyI18nString # noqa
from i18nfield.utils import I18nJSONEncoder # noqa
_active_region = Local()
class LazyDate:
def __init__(self, value):
self.value = value
@@ -88,8 +86,6 @@ class LazyCurrencyNumber:
return self.__str__()
def __str__(self):
from pretix.base.templatetags.money import money_filter
return money_filter(self.value, self.currency)
@@ -109,41 +105,14 @@ ALLOWED_LANGUAGES = dict(settings.LANGUAGES)
def get_babel_locale():
# Babel, and therefore also django-phonenumberfield, do not support our custom locales such das de_Informal
# Also, this returns best-effort region information for number formatting etc
current_language = translation.get_language()
current_region = getattr(_active_region, "value", None)
# Babel only accepts locales that exist on the system. We try combinations in the following order:
# language-languageversion-region
# language-region
# language-languageversion
# language
# fallback to system default
# fallback to english
try_locales = []
if current_language:
if "-" in current_language:
lng_parts = current_language.split("-")
if current_region:
try_locales.append(f"{lng_parts[0]}_{lng_parts[1].title()}_{current_region.upper()}")
try_locales.append(f"{lng_parts[0]}_{current_region.upper()}")
try_locales.append(f"{lng_parts[0]}_{lng_parts[1].upper()}")
try_locales.append(f"{lng_parts[0]}_{lng_parts[1].title()}")
try_locales.append(f"{lng_parts[0]}")
else:
if current_region:
try_locales.append(f"{current_language}_{current_region.upper()}")
try_locales.append(f"{current_language}")
try_locales.append(settings.LANGUAGE_CODE)
for locale in try_locales:
if localedata.exists(locale):
return localedata.normalize_locale(locale)
return "en"
babel_locale = 'en'
# Babel, and therefore django-phonenumberfield, do not support our custom locales such das de_Informal
if translation.get_language():
if localedata.exists(translation.get_language()):
babel_locale = translation.get_language()
elif localedata.exists(translation.get_language()[:2]):
babel_locale = translation.get_language()[:2]
return babel_locale
def get_language_without_region(lng=None):
@@ -163,10 +132,6 @@ def get_language_without_region(lng=None):
return lng
def set_region(region):
_active_region.value = region
@contextmanager
def language(lng, region=None):
"""
@@ -178,18 +143,15 @@ def language(lng, region=None):
formatting. If you pass a ``lng`` that already contains a region, e.g. ``pt-br``, the ``region``
attribute will be ignored.
"""
lng_before = translation.get_language()
region_before = getattr(_active_region, "value", None)
_lng = translation.get_language()
lng = lng or settings.LANGUAGE_CODE
if '-' not in lng and region:
lng += '-' + region.lower()
translation.activate(lng)
_active_region.value = region
try:
yield
finally:
translation.activate(lng_before)
_active_region.value = region_before
translation.activate(_lng)
class LazyLocaleException(Exception):
+35 -19
View File
@@ -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 SendMailException, mail, render_mail
from pretix.helpers.format import format_map
@transmission_types.new()
@@ -132,26 +133,41 @@ class EmailTransmissionProvider(TransmissionProvider):
template = invoice.order.event.settings.get('mail_text_order_invoice', as_type=LazyI18nString)
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(
[recipient],
subject,
template,
context=context,
event=invoice.order.event,
locale=invoice.order.locale,
order=invoice.order,
invoices=[invoice],
attach_tickets=False,
auto_email=True,
attach_ical=False,
plain_text_only=True,
no_order_links=True,
)
if outgoing_mail:
try:
# 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(
[recipient],
subject,
template,
context=context,
event=invoice.order.event,
locale=invoice.order.locale,
order=invoice.order,
invoices=[invoice],
attach_tickets=False,
auto_email=True,
attach_ical=False,
plain_text_only=True,
no_order_links=True,
)
except SendMailException:
raise
else:
invoice.order.log_action(
'pretix.event.order.email.invoice',
user=None,
auth=None,
data=outgoing_mail.log_data()
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': [],
}
)
+1 -3
View File
@@ -36,11 +36,9 @@ class ItalianSdITransmissionType(TransmissionType):
identifier = "it_sdi"
verbose_name = pgettext_lazy("italian_invoice", "Italian Exchange System (SdI)")
public_name = pgettext_lazy("italian_invoice", "Exchange System (SdI)")
exclusive = True
enforce_transmission = True
def is_exclusive(self, event, country: Country, is_business: bool) -> bool:
return str(country) == "IT"
def is_available(self, event, country: Country, is_business: bool):
return str(country) == "IT" and super().is_available(event, country, is_business)
-4
View File
@@ -148,10 +148,6 @@ class NumberedCanvas(Canvas):
self.restoreState()
class InvoiceNotReadyException(Exception):
pass
class BaseInvoiceRenderer:
"""
This is the base class for all invoice renderers.
+1 -13
View File
@@ -64,7 +64,7 @@ class PeppolIdValidator:
"0020": "[0-9]{9}",
"0201": "[0-9a-zA-Z]{6}",
"0204": "[0-9]{2,12}(-[0-9A-Z]{0,30})?-[0-9]{2}",
"0208": "[01][0-9]{9}",
"0208": "0[0-9]{9}",
"0209": ".*",
"0210": "[A-Z0-9]+",
"0211": "IT[0-9]{11}",
@@ -179,12 +179,6 @@ class PeppolTransmissionType(TransmissionType):
def is_available(self, event, country: Country, is_business: bool):
return is_business and super().is_available(event, country, is_business)
def is_exclusive(self, event, country: Country, is_business: bool) -> bool:
if is_business and str(country) == "BE" and event and event.settings.invoice_address_from_country == "BE":
# Peppol is required to be used for intra-Belgian B2B invoices
return True
return False
@property
def invoice_address_form_fields(self) -> dict:
return {
@@ -204,12 +198,6 @@ 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")
+11 -28
View File
@@ -21,10 +21,9 @@
#
from typing import Optional
from django.utils.translation import gettext_lazy as _
from django_countries.fields import Country
from pretix.base.models import Invoice
from pretix.base.models import Invoice, InvoiceAddress
from pretix.base.signals import EventPluginRegistry, Registry
@@ -59,6 +58,15 @@ class TransmissionType:
"""
return 100
@property
def exclusive(self) -> bool:
"""
If a transmission type is exclusive, no other type can be chosen if this type is
available. Use e.g. if a certain transmission type is legally required in a certain
jurisdiction.
"""
return False
@property
def enforce_transmission(self) -> bool:
"""
@@ -74,22 +82,13 @@ class TransmissionType:
for provider, _ in providers
)
def is_exclusive(self, event, country: Country, is_business: bool) -> bool:
"""
If a transmission type is exclusive, no other type can be chosen if this type is
available. Use e.g. if a certain transmission type is legally required in a certain
jurisdiction. Event can be None in organizer-level contexts. Exclusiveness has no effect if
the type is not available.
"""
return False
def invoice_address_form_fields_required(self, country: Country, is_business: bool):
return set()
def invoice_address_form_fields_visible(self, country: Country, is_business: bool) -> set:
return set(self.invoice_address_form_fields.keys())
def validate_invoice_address_data(self, address_data: dict):
def validate_address(self, ia: InvoiceAddress):
pass
@property
@@ -107,22 +106,6 @@ class TransmissionType:
def transmission_info_to_form_data(self, transmission_info: dict) -> dict:
return transmission_info
def describe_info(self, transmission_info: dict, country: Country, is_business: bool):
form_data = self.transmission_info_to_form_data(transmission_info)
data = []
visible_field_keys = self.invoice_address_form_fields_visible(country, is_business)
for k, f in self.invoice_address_form_fields.items():
if k not in visible_field_keys:
continue
v = form_data.get(k)
if v is True:
v = _("Yes")
elif v is False:
v = _("No")
if v:
data.append((f.label, v))
return data
def pdf_watermark(self) -> Optional[str]:
"""
Return a watermark that should be rendered across the PDF file.
+7 -21
View File
@@ -294,28 +294,14 @@ def metric_values():
channel = app.broker_connection().channel()
if hasattr(channel, 'client') and channel.client is not None:
client = channel.client
priority_steps = settings.CELERY_BROKER_TRANSPORT_OPTIONS.get("priority_steps", [0])
sep = settings.CELERY_BROKER_TRANSPORT_OPTIONS.get("sep", ":")
for q in settings.CELERY_TASK_QUEUES:
queue_lengths = []
queue_delays = []
for prio in priority_steps:
if prio:
qname = f"{q.name}{sep}{prio}"
else:
qname = q.name
queue_length = client.llen(qname)
queue_lengths.append(queue_length)
oldest_queue_item = client.lindex(qname, -1)
if oldest_queue_item:
ldata = json.loads(oldest_queue_item)
oldest_item_age = time.time() - ldata.get('created', 0)
queue_delays.append(oldest_item_age)
metrics['pretix_celery_tasks_queued_count']['{queue="%s"}' % q.name] = sum(queue_lengths)
if queue_delays:
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = max(queue_delays)
llen = client.llen(q.name)
lfirst = client.lindex(q.name, -1)
metrics['pretix_celery_tasks_queued_count']['{queue="%s"}' % q.name] = llen
if lfirst:
ldata = json.loads(lfirst)
dt = time.time() - ldata.get('created', 0)
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = dt
else:
metrics['pretix_celery_tasks_queued_age_seconds']['{queue="%s"}' % q.name] = 0
+1 -5
View File
@@ -35,7 +35,7 @@ from django.utils.translation.trans_real import (
parse_accept_lang_header,
)
from pretix.base.i18n import get_language_without_region, set_region
from pretix.base.i18n import get_language_without_region
from pretix.base.settings import global_settings_object
from pretix.multidomain.urlreverse import (
get_event_domain, get_organizer_domain,
@@ -92,14 +92,10 @@ class LocaleMiddleware(MiddlewareMixin):
)
if '-' not in language and settings_holder.settings.region:
language += '-' + settings_holder.settings.region
if settings_holder.settings.region:
set_region(settings_holder.settings.region)
else:
gs = global_settings_object(request)
if '-' not in language and gs.settings.region:
language += '-' + gs.settings.region
if gs.settings.region:
set_region(gs.settings.region)
translation.activate(language)
request.LANGUAGE_CODE = get_language_without_region()
@@ -1,120 +0,0 @@
# Generated by Django 4.2.26 on 2026-01-22 13:44
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import pretix.base.models.mail
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0296_invoice_invoice_from_state"),
]
operations = [
migrations.CreateModel(
name="OutgoingMail",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False
),
),
("guid", models.UUIDField(db_index=True, default=uuid.uuid4)),
("status", models.CharField(default="queued", max_length=200)),
("created", models.DateTimeField(auto_now_add=True)),
("sent", models.DateTimeField(blank=True, null=True)),
("inflight_since", models.DateTimeField(blank=True, null=True)),
("retry_after", models.DateTimeField(blank=True, null=True)),
("error", models.TextField(null=True)),
("error_detail", models.TextField(null=True)),
("sensitive", models.BooleanField(default=False)),
("subject", models.TextField()),
("body_plain", models.TextField()),
("body_html", models.TextField(null=True)),
("sender", models.CharField(max_length=500)),
("headers", models.JSONField(default=dict)),
("to", models.JSONField(default=list)),
("cc", models.JSONField(default=list)),
("bcc", models.JSONField(default=list)),
("recipient_count", models.IntegerField()),
("should_attach_tickets", models.BooleanField(default=False)),
("should_attach_ical", models.BooleanField(default=False)),
("should_attach_other_files", models.JSONField(default=list)),
("actual_attachments", models.JSONField(default=list)),
(
"customer",
models.ForeignKey(
null=True,
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
related_name="outgoing_mails",
to="pretixbase.customer",
),
),
(
"event",
models.ForeignKey(
null=True,
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
related_name="outgoing_mails",
to="pretixbase.event",
),
),
(
"order",
models.ForeignKey(
null=True,
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
related_name="outgoing_mails",
to="pretixbase.order",
),
),
(
"orderposition",
models.ForeignKey(
null=True,
on_delete=pretix.base.models.mail.CASCADE_IF_QUEUED,
related_name="outgoing_mails",
to="pretixbase.orderposition",
),
),
(
"organizer",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="outgoing_mails",
to="pretixbase.organizer",
),
),
(
"should_attach_cached_files",
models.ManyToManyField(
related_name="outgoing_mails", to="pretixbase.cachedfile"
),
),
(
"should_attach_invoices",
models.ManyToManyField(
related_name="outgoing_mails", to="pretixbase.invoice"
),
),
(
"user",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="outgoing_mails",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"ordering": ("-created",),
},
),
]
+3 -3
View File
@@ -132,7 +132,7 @@ class AllowIgnoreQuotaColumn(BooleanColumnMixin, ImportColumn):
class PriceModeColumn(ImportColumn):
identifier = 'price_mode'
verbose_name = gettext_lazy('Price effect')
verbose_name = gettext_lazy('Price mode')
default_value = None
initial = 'static:none'
@@ -147,7 +147,7 @@ class PriceModeColumn(ImportColumn):
elif value in reverse:
return reverse[value]
else:
raise ValidationError(_("Could not parse {value} as a price effect, use one of {options}.").format(
raise ValidationError(_("Could not parse {value} as a price mode, use one of {options}.").format(
value=value, options=', '.join(d.keys())
))
@@ -162,7 +162,7 @@ class ValueColumn(DecimalColumnMixin, ImportColumn):
def clean(self, value, previous_values):
value = super().clean(value, previous_values)
if value and previous_values.get("price_mode") == "none":
raise ValidationError(_("It is pointless to set a value without a price effect."))
raise ValidationError(_("It is pointless to set a value without a price mode."))
return value
def assign(self, value, obj: Voucher, **kwargs):
-1
View File
@@ -41,7 +41,6 @@ from .items import (
itempicture_upload_to,
)
from .log import LogEntry
from .mail import OutgoingMail
from .media import ReusableMedium
from .memberships import Membership, MembershipType
from .notifications import NotificationSetting
+19 -19
View File
@@ -334,25 +334,27 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
return self.email
def send_security_notice(self, messages, email=None):
from pretix.base.services.mail import mail
from pretix.base.services.mail import SendMailException, mail
with language(self.locale):
msg = '- ' + '\n- '.join(str(m) for m in messages)
try:
with language(self.locale):
msg = '- ' + '\n- '.join(str(m) for m in messages)
mail(
email or self.email,
_('Account information changed'),
'pretixcontrol/email/security_notice.txt',
{
'user': self,
'messages': msg,
'url': build_absolute_uri('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
locale=self.locale
)
mail(
email or self.email,
_('Account information changed'),
'pretixcontrol/email/security_notice.txt',
{
'user': self,
'messages': msg,
'url': build_absolute_uri('control:user.settings')
},
event=None,
user=self,
locale=self.locale
)
except SendMailException:
pass # Already logged
def send_confirmation_code(self, session, reason, email=None, state=None):
"""
@@ -392,7 +394,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'user': self,
'reason': msg,
'code': code,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -432,7 +433,6 @@ 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)))
-2
View File
@@ -130,8 +130,6 @@ class LoggingMixin:
organizer_id = self.event.organizer_id
elif hasattr(self, 'organizer_id'):
organizer_id = self.organizer_id
elif hasattr(self, 'issuer_id'):
organizer_id = self.issuer_id
if user and not user.is_authenticated:
user = None
+1 -25
View File
@@ -40,7 +40,6 @@ from i18nfield.fields import I18nCharField
from phonenumber_field.modelfields import PhoneNumberField
from pretix.base.banlist import banned
from pretix.base.i18n import language
from pretix.base.models.base import LoggedModel
from pretix.base.models.fields import MultiStringField
from pretix.base.models.giftcards import GiftCardTransaction
@@ -165,28 +164,6 @@ class Customer(LoggedModel):
self.attendee_profiles.all().delete()
self.invoice_addresses.all().delete()
def send_security_notice(self, message, email=None):
from pretix.base.services.mail import SendMailException, mail
from pretix.multidomain.urlreverse import build_absolute_uri
try:
with language(self.locale):
mail(
email or self.email,
self.organizer.settings.mail_subject_customer_security_notice,
self.organizer.settings.mail_text_customer_security_notice,
{
**self.get_email_context(),
'message': str(message),
'url': build_absolute_uri(self.organizer, 'presale:organizer.customer.index')
},
customer=self,
organizer=self.organizer,
locale=self.locale
)
except SendMailException:
pass # Already logged
@scopes_disabled()
def assign_identifier(self):
charset = list('ABCDEFGHJKLMNPQRSTUVWXYZ23456789')
@@ -316,7 +293,6 @@ class Customer(LoggedModel):
locale=self.locale,
customer=self,
organizer=self.organizer,
sensitive=True,
)
def usable_gift_cards(self, used_cards=[]):
@@ -373,7 +349,7 @@ class AttendeeProfile(models.Model):
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return _(sd.name)
return sd.name
return self.state
@property
+1 -1
View File
@@ -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 {self.sync_provider} ({failure_mode})"
f"Could not sync order {self.order.code} to {type(self).__name__} ({failure_mode})"
)
self.order.log_action(f"pretix.event.order.data_sync.failed.{failure_mode}", {
"provider": self.sync_provider,
+5 -41
View File
@@ -37,7 +37,7 @@ from pretix.base.decimal import round_decimal
from pretix.base.models.base import LoggedModel
PositionInfo = namedtuple('PositionInfo',
['item_id', 'subevent_id', 'subevent_date_from', 'line_price_gross', 'addon_to',
['item_id', 'subevent_id', 'subevent_date_from', 'line_price_gross', 'is_addon_to',
'voucher_discount'])
@@ -279,42 +279,6 @@ class Discount(LoggedModel):
for idx in condition_idx_group:
collect_potential_discounts[idx] = [(self, inf, -1, subevent_id)]
def _addon_idx(self, positions, idx):
"""
If we have the following cart:
- Main product
- 10x Addon product 5
- Main product
- 10x Addon product 5
And we have a discount rule that grants "every 10th product is free", people tend to expect
- Main product
- 9x Addon product 5
- 1x Addon product free
- Main product
- 9x Addon product 5
- 1x Addon product free
And get confused if they get
- Main product
- 8x Addon product 5
- 2x Addon product free
- Main product
- 10x Addon product 5
Even if the result is the same. Therefore, we sort positions in the cart not only by price, but also by their
relative index within their addon group. This is only a heuristic and there are *still* scenarios where the more
unexpected version happens, e.g. if prices are different. We need to accept this as long as discounts work on
cart level and not on addon-group level, but this simple sorting reduces the number of support issues by making
the weird case less likely.
"""
if not positions[idx].addon_to:
return 0
return len([1 for i, p in positions.items() if i < idx and p.addon_to == positions[idx].addon_to])
def _apply_min_count(self, positions, condition_idx_group, benefit_idx_group, result, collect_potential_discounts, subevent_id):
if len(condition_idx_group) < self.condition_min_count:
return
@@ -324,8 +288,8 @@ class Discount(LoggedModel):
if self.benefit_only_apply_to_cheapest_n_matches:
# sort by line_price
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx), -idx))
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx), -idx))
condition_idx_group = sorted(condition_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
benefit_idx_group = sorted(benefit_idx_group, key=lambda idx: (positions[idx].line_price_gross, -idx))
# Prevent over-consuming of items, i.e. if our discount is "buy 2, get 1 free", we only
# want to match multiples of 3
@@ -470,7 +434,7 @@ class Discount(LoggedModel):
for idx, p in positions.items():
subevent_to_idx[p.subevent_id].append(idx)
for v in subevent_to_idx.values():
v.sort(key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx)))
v.sort(key=lambda idx: positions[idx].line_price_gross)
subevent_order = sorted(list(subevent_to_idx.keys()), key=lambda s: len(subevent_to_idx[s]), reverse=True)
# Build groups of exactly condition_min_count distinct subevents
@@ -494,7 +458,7 @@ class Discount(LoggedModel):
# Sort the list by prices, then pick one. For "buy 2 get 1 free" we apply a "pick 1 from the start
# and 2 from the end" scheme to optimize price distribution among groups
candidates = sorted(candidates, key=lambda idx: (positions[idx].line_price_gross, self._addon_idx(positions, idx)))
candidates = sorted(candidates, key=lambda idx: positions[idx].line_price_gross)
if len(current_group) < (self.benefit_only_apply_to_cheapest_n_matches or 0):
candidate = candidates[0]
else:
+4 -5
View File
@@ -594,11 +594,10 @@ class Item(LoggedModel):
on_delete=models.SET_NULL,
verbose_name=_("Only show after sellout of"),
help_text=_("If you select a product here, this product will only be shown when that product is "
"no longer available. This will happen either because the other product has sold out or because "
"the time is outside of the sales window for the other product. If combined with the option "
"to hide sold-out products, this allows you to swap out products for more expensive ones once "
"the cheaper option is sold out. There might be a short period in which both products are visible "
"while all tickets of the referenced product are reserved, but not yet sold.")
"sold out. If combined with the option to hide sold-out products, this allows you to "
"swap out products for more expensive ones once the cheaper option is sold out. There might "
"be a short period in which both products are visible while all tickets of the referenced "
"product are reserved, but not yet sold.")
)
hidden_if_item_available_mode = models.CharField(
choices=UNAVAIL_MODES,
+1 -2
View File
@@ -141,9 +141,8 @@ class LogEntry(models.Model):
log_entry_type, meta = log_entry_types.get(action_type=self.action_type)
if log_entry_type:
sender = self.event if self.event else self.organizer
link_info = log_entry_type.get_object_link_info(self)
if is_app_active(sender, meta['plugin']):
if is_app_active(self.event, meta['plugin']):
return make_link(link_info, log_entry_type.object_link_wrapper)
else:
return make_link(link_info, log_entry_type.object_link_wrapper, is_active=False,
-239
View File
@@ -1,239 +0,0 @@
#
# 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 <https://pretix.eu/about/en/license>.
#
# 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
# <https://www.gnu.org/licenses/>.
#
import uuid
from django.core.mail import get_connection
from django.db import models
from django.utils.translation import gettext_lazy as _
from django_scopes import scope, scopes_disabled
def CASCADE_IF_QUEUED(collector, field, sub_objs, using):
# If the email is still queued and the thing it is related to vanishes, the email can vanish as well
cascade_objs = [
o for o in sub_objs if o.status == OutgoingMail.STATUS_QUEUED
]
if cascade_objs:
models.CASCADE(collector, field, cascade_objs, using)
# In all other cases, set to NULL to keep the email on record
models.SET_NULL(collector, field, [o for o in sub_objs if o not in cascade_objs], using)
class OutgoingMail(models.Model):
STATUS_QUEUED = "queued"
STATUS_WITHHELD = "withheld"
STATUS_INFLIGHT = "inflight"
STATUS_AWAITING_RETRY = "awaiting_retry"
STATUS_FAILED = "failed"
STATUS_SENT = "sent"
STATUS_BOUNCED = "bounced"
STATUS_ABORTED = "aborted"
STATUS_CHOICES = (
(STATUS_QUEUED, _("queued")),
(STATUS_INFLIGHT, _("being sent")),
(STATUS_AWAITING_RETRY, _("awaiting retry")),
(STATUS_WITHHELD, _("withheld")), # for plugin use
(STATUS_FAILED, _("failed")),
(STATUS_ABORTED, _("aborted")),
(STATUS_SENT, _("sent")),
(STATUS_BOUNCED, _("bounced")), # for plugin use
)
STATUS_LIST_ABORTABLE = {
STATUS_QUEUED,
STATUS_WITHHELD,
STATUS_AWAITING_RETRY,
}
STATUS_LIST_RETRYABLE = {
STATUS_FAILED,
STATUS_WITHHELD,
}
# The GUID is a globally unique ID for the email added to a header of the email for later tracing
# in bug reports etc. We could theoretically also use this as a basis for the Message-ID header, but
# we currently don't since we are unsure if some intermediary SMTP servers have opinions on setting
# their own Message-ID headers.
guid = models.UUIDField(db_index=True, default=uuid.uuid4)
status = models.CharField(max_length=200, choices=STATUS_CHOICES, default=STATUS_QUEUED)
created = models.DateTimeField(auto_now_add=True)
# sent will be the time the email was sent or the email failed
sent = models.DateTimeField(null=True, blank=True)
inflight_since = models.DateTimeField(null=True, blank=True)
retry_after = models.DateTimeField(null=True, blank=True)
error = models.TextField(null=True, blank=True)
error_detail = models.TextField(null=True, blank=True)
# There is a conflict here between the different purposes of the model. As a system administrator,
# one wants *all* emails to be persisted as long as possible to debug issues. This means that if
# e.g. the event or order is deleted, we want SET_NULL behavior. However, in that case, the email
# would be an "orphan" forever and there's no way to remove the personal information.
# We try to find a middle-ground with the following behaviour:
# - The email is always deleted if the entire organizer or user is deleted
# - The email is always deleted if it has not yet been sent
# - The email is kept in all other cases
# This is only an acceptable trade-off since emails are stored for a short period only, and because
# orders and customers are never deleted during normal operation. If we ever make this a long-term
# storage / email archive, we'd need to find another way to make sure personal information is removed
# if personal information of orders etc is removed.
organizer = models.ForeignKey(
'pretixbase.Organizer',
on_delete=models.CASCADE,
related_name='outgoing_mails',
null=True, blank=True,
)
event = models.ForeignKey(
'pretixbase.Event',
on_delete=CASCADE_IF_QUEUED,
related_name='outgoing_mails',
null=True, blank=True,
)
order = models.ForeignKey(
'pretixbase.Order',
on_delete=CASCADE_IF_QUEUED,
related_name='outgoing_mails',
null=True, blank=True,
)
orderposition = models.ForeignKey(
'pretixbase.OrderPosition',
on_delete=CASCADE_IF_QUEUED,
related_name='outgoing_mails',
null=True, blank=True,
)
customer = models.ForeignKey(
'pretixbase.Customer',
on_delete=CASCADE_IF_QUEUED,
related_name='outgoing_mails',
null=True, blank=True,
)
user = models.ForeignKey(
'pretixbase.User',
on_delete=models.CASCADE,
related_name='outgoing_mails',
null=True, blank=True,
)
sensitive = models.BooleanField(default=False)
subject = models.TextField()
body_plain = models.TextField()
body_html = models.TextField(null=True)
sender = models.CharField(max_length=500)
headers = models.JSONField(default=dict)
to = models.JSONField(default=list)
cc = models.JSONField(default=list)
bcc = models.JSONField(default=list)
recipient_count = models.IntegerField()
# We don't store the actual invoices, tickets or calendar invites, so if the email is re-sent at a later time, a
# newer version of the files might be used. We accept that risk to save on storage and also because the new
# version might actually be more useful.
should_attach_invoices = models.ManyToManyField(
'pretixbase.Invoice',
related_name='outgoing_mails'
)
should_attach_tickets = models.BooleanField(default=False)
should_attach_ical = models.BooleanField(default=False)
# clean_cached_files makes sure not to delete these as long as the email is in a retryable state
should_attach_cached_files = models.ManyToManyField(
'pretixbase.CachedFile',
related_name='outgoing_mails',
)
# This is used to send files stored in settings. In most cases, these aren't short-lived and should still be there
# if the email is sent. Otherwise, they will be skipped. We accept that risk.
should_attach_other_files = models.JSONField(default=list)
# [{name, type size}] of the attachments we actually setn
actual_attachments = models.JSONField(default=list)
class Meta:
ordering = ('-created',)
def get_mail_backend(self):
if self.event:
return self.event.get_mail_backend()
elif self.organizer:
return self.organizer.get_mail_backend()
else:
return get_connection(fail_silently=False)
def scope_manager(self):
if self.organizer:
return scope(organizer=self.organizer) # noqa
else:
return scopes_disabled() # noqa
@property
def is_failed(self):
return self.status in (
OutgoingMail.STATUS_FAILED,
OutgoingMail.STATUS_AWAITING_RETRY,
OutgoingMail.STATUS_BOUNCED,
)
def save(self, *args, **kwargs):
if self.orderposition_id and not self.order_id:
self.order = self.orderposition.order
if self.order_id and not self.event_id:
self.event = self.order.event
if self.event_id and not self.organizer_id:
self.organizer = self.event.organizer
if self.customer_id and not self.organizer_id:
self.organizer = self.customer.organizer
self.recipient_count = len(self.to) + len(self.cc) + len(self.bcc)
super().save(*args, **kwargs)
def log_parameters(self):
if self.order:
error_log_action_type = 'pretix.event.order.email.error'
log_target = self.order
elif self.customer:
error_log_action_type = 'pretix.customer.email.error'
log_target = self.customer
elif self.user:
error_log_action_type = 'pretix.user.email.error'
log_target = self.user
else:
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,
}
+92 -40
View File
@@ -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 format_map
from ...helpers.names import build_name
from ...testutils.middleware import debugflags_var
from ._transactions import (
@@ -1166,7 +1167,9 @@ 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 (
SendMailException, mail, render_mail,
)
if not self.email and not (position and position.attendee_email):
return
@@ -1176,19 +1179,34 @@ class Order(LockModel, LoggedModel):
if position and position.attendee_email:
recipient = position.attendee_email
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,
)
if outgoing_mail:
try:
email_content = render_mail(template, context)
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,
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
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):
@@ -1657,7 +1675,7 @@ class AbstractPosition(RoundingCorrectionMixin, models.Model):
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return _(sd.name)
return sd.name
return self.state
@property
@@ -2006,30 +2024,40 @@ class OrderPayment(models.Model):
transmit_invoice.apply_async(args=(self.order.event_id, invoice.pk, False))
def _send_paid_mail_attendee(self, position, user):
from pretix.base.services.mail import SendMailException
with language(self.order.locale, self.order.event.settings.region):
email_template = self.order.event.settings.mail_text_order_paid_attendee
email_subject = self.order.event.settings.mail_subject_order_paid_attendee
email_context = get_email_context(event=self.order.event, order=self.order, position=position)
position.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[],
attach_tickets=True,
attach_ical=self.order.event.settings.mail_attach_ical
)
try:
position.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[],
attach_tickets=True,
attach_ical=self.order.event.settings.mail_attach_ical
)
except SendMailException:
logger.exception('Order paid email could not be sent')
def _send_paid_mail(self, invoice, user, mail_text):
from pretix.base.services.mail import SendMailException
with language(self.order.locale, self.order.event.settings.region):
email_template = self.order.event.settings.mail_text_order_paid
email_subject = self.order.event.settings.mail_subject_order_paid
email_context = get_email_context(event=self.order.event, order=self.order, payment_info=mail_text)
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice else [],
attach_tickets=True,
attach_ical=self.order.event.settings.mail_attach_ical
)
try:
self.order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_paid', user,
invoices=[invoice] if invoice else [],
attach_tickets=True,
attach_ical=self.order.event.settings.mail_attach_ical
)
except SendMailException:
logger.exception('Order paid email could not be sent')
@property
def refunded_amount(self):
@@ -2887,28 +2915,44 @@ 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 (
SendMailException, 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(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
invoices=invoices,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
attach_other_files=attach_other_files,
)
if outgoing_mail:
try:
email_content = render_mail(template, context)
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event, self.order.locale, order=self.order, headers=headers, sender=sender,
position=self,
invoices=invoices,
attach_tickets=attach_tickets,
attach_ical=attach_ical,
attach_other_files=attach_other_files,
)
except SendMailException:
raise
else:
self.order.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
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):
@@ -3436,7 +3480,7 @@ class InvoiceAddress(models.Model):
def state_name(self):
sd = pycountry.subdivisions.get(code='{}-{}'.format(self.country, self.state))
if sd:
return _(sd.name)
return sd.name
return self.state
@property
@@ -3485,10 +3529,18 @@ class InvoiceAddress(models.Model):
def describe_transmission(self):
from pretix.base.invoicing.transmission import transmission_types
data = []
t, __ = transmission_types.get(identifier=self.transmission_type)
data.append((_("Transmission type"), t.public_name))
if self.transmission_info:
data += t.describe_info(self.transmission_info, self.country, self.is_business)
form_data = t.transmission_info_to_form_data(self.transmission_info or {})
for k, f in t.invoice_address_form_fields.items():
v = form_data.get(k)
if v is True:
v = _("Yes")
elif v is False:
v = _("No")
if v:
data.append((f.label, v))
return data
+1 -1
View File
@@ -239,7 +239,7 @@ class Voucher(LoggedModel):
)
)
price_mode = models.CharField(
verbose_name=_("Price effect"),
verbose_name=_("Price mode"),
max_length=100,
choices=PRICE_MODES,
default='none'
+44 -40
View File
@@ -34,9 +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
from pretix.helpers import OF_SELF
from pretix.base.services.mail import SendMailException, mail, render_mail
from ...helpers.format import format_map
from ...helpers.names import build_name
from .base import LoggedModel
from .event import Event, SubEvent
@@ -158,7 +158,6 @@ class WaitingListEntry(LoggedModel):
if availability[1] is None or availability[1] < 1:
raise WaitingListException(_('This product is currently not available.'))
event = self.event
ev = self.subevent or self.event
if ev.seat_category_mappings.filter(product=self.item).exists():
# Generally, we advertise the waiting list to be based on quotas only. This makes it dangerous
@@ -180,56 +179,50 @@ class WaitingListEntry(LoggedModel):
block_quota=True,
item_id=self.item_id,
subevent_id=self.subevent_id,
waitinglistentries__isnull=False,
seat__isnull=True
waitinglistentries__isnull=False
).aggregate(free=Sum(F('max_usages') - F('redeemed')))['free'] or 0
free_seats = num_free_seats_for_product - num_valid_vouchers_for_product
if free_seats < 1:
if not free_seats:
raise WaitingListException(_('No seat with this product is currently available.'))
if self.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.'))
if '@' not in self.email:
raise WaitingListException(_('This entry is anonymized and can no longer be used.'))
with transaction.atomic():
locked_wle = WaitingListEntry.objects.select_for_update(of=OF_SELF).get(pk=self.pk)
locked_wle.event = event
if locked_wle.voucher:
raise WaitingListException(_('A voucher has already been sent to this person.'))
e = locked_wle.email
if locked_wle.name:
e += ' / ' + locked_wle.name
e = self.email
if self.name:
e += ' / ' + self.name
v = Voucher.objects.create(
event=locked_wle.event,
event=self.event,
max_usages=1,
valid_until=now() + timedelta(hours=locked_wle.event.settings.waiting_list_hours),
item=locked_wle.item,
variation=locked_wle.variation,
valid_until=now() + timedelta(hours=self.event.settings.waiting_list_hours),
item=self.item,
variation=self.variation,
tag='waiting-list',
comment=_('Automatically created from waiting list entry for {email}').format(
email=e
),
block_quota=True,
subevent=locked_wle.subevent,
subevent=self.subevent,
)
v.log_action('pretix.voucher.added', {
'item': locked_wle.item.pk,
'variation': locked_wle.variation.pk if locked_wle.variation else None,
'item': self.item.pk,
'variation': self.variation.pk if self.variation else None,
'tag': 'waiting-list',
'block_quota': True,
'valid_until': v.valid_until.isoformat(),
'max_usages': 1,
'subevent': locked_wle.subevent.pk if locked_wle.subevent else None,
'subevent': self.subevent.pk if self.subevent else None,
'source': 'waitinglist',
}, user=user, auth=auth)
v.log_action('pretix.voucher.added.waitinglist', {
'email': locked_wle.email,
'waitinglistentry': locked_wle.pk,
'email': self.email,
'waitinglistentry': self.pk,
}, user=user, auth=auth)
locked_wle.voucher = v
locked_wle.save()
self.refresh_from_db()
self.event = event
self.voucher = v
self.save()
with language(self.locale, self.event.settings.region):
self.send_mail(
@@ -272,22 +265,33 @@ class WaitingListEntry(LoggedModel):
with language(self.locale, self.event.settings.region):
recipient = self.email
outgoing_mail = mail(
recipient, subject, template, context,
self.event,
self.locale,
headers=headers,
sender=sender,
auto_email=auto_email,
attach_other_files=attach_other_files,
attach_cached_files=attach_cached_files,
)
if outgoing_mail:
try:
email_content = render_mail(template, context)
subject = format_map(subject, context)
mail(
recipient, subject, template, context,
self.event,
self.locale,
headers=headers,
sender=sender,
auto_email=auto_email,
attach_other_files=attach_other_files,
attach_cached_files=attach_cached_files,
)
except SendMailException:
raise
else:
self.log_action(
log_entry_type,
user=user,
auth=auth,
data=outgoing_mail.log_data(),
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
+2 -20
View File
@@ -1231,8 +1231,8 @@ class ManualPayment(BasePaymentProvider):
def is_allowed(self, request: HttpRequest, total: Decimal=None):
return 'pretix.plugins.manualpayment' in self.event.plugins and super().is_allowed(request, total)
def order_change_allowed(self, order: Order, request=None):
return 'pretix.plugins.manualpayment' in self.event.plugins and super().order_change_allowed(order, request)
def order_change_allowed(self, order: Order):
return 'pretix.plugins.manualpayment' in self.event.plugins and super().order_change_allowed(order)
@property
def public_name(self):
@@ -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,
@@ -1647,14 +1646,6 @@ class GiftCardPayment(BasePaymentProvider):
'transaction_id': trans.pk,
}
payment.confirm(send_mail=not is_early_special_case, generate_invoice=not is_early_special_case)
gc.log_action(
action='pretix.giftcards.transaction.payment',
data={
'value': trans.value,
'acceptor_id': self.event.organizer.id,
'acceptor_slug': self.event.organizer.slug
}
)
except PaymentException as e:
payment.fail(info={'error': str(e)})
raise e
@@ -1679,15 +1670,6 @@ class GiftCardPayment(BasePaymentProvider):
'transaction_id': trans.pk,
}
refund.done()
gc.log_action(
action='pretix.giftcards.transaction.refund',
data={
'value': refund.amount,
'acceptor_id': self.event.organizer.id,
'acceptor_slug': self.event.organizer.slug,
'text': refund.comment,
}
)
@receiver(register_payment_providers, dispatch_uid="payment_free")
+1 -1
View File
@@ -65,7 +65,7 @@ def get_all_plugins(*, event=None, organizer=None) -> List[type]:
if app.name in settings.PRETIX_PLUGINS_EXCLUDE:
continue
level = getattr(meta, "level", PLUGIN_LEVEL_EVENT)
level = getattr(app, "level", PLUGIN_LEVEL_EVENT)
if level == PLUGIN_LEVEL_EVENT:
if event and hasattr(app, 'is_available'):
if not app.is_available(event):
+32 -20
View File
@@ -36,7 +36,7 @@ from pretix.base.models import (
SubEvent, TaxRule, User, WaitingListEntry,
)
from pretix.base.services.locking import LockTimeoutException
from pretix.base.services.mail import mail
from pretix.base.services.mail import SendMailException, mail
from pretix.base.services.orders import (
OrderChangeManager, OrderError, _cancel_order, _try_auto_refund,
)
@@ -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__)
@@ -52,14 +53,17 @@ logger = logging.getLogger(__name__)
def _send_wle_mail(wle: WaitingListEntry, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent):
with language(wle.locale, wle.event.settings.region):
email_context = get_email_context(event_or_subevent=subevent or wle.event, event=wle.event)
mail(
wle.email,
str(subject),
message,
email_context,
wle.event,
locale=wle.locale
)
try:
mail(
wle.email,
format_map(subject, email_context),
message,
email_context,
wle.event,
locale=wle.locale
)
except SendMailException:
logger.exception('Waiting list canceled email could not be sent')
def _send_mail(order: Order, subject: LazyI18nString, message: LazyI18nString, subevent: SubEvent,
@@ -72,28 +76,36 @@ 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)
order.send_mail(
subject, message, email_context,
'pretix.event.order.email.event_canceled',
user,
)
real_subject = format_map(subject, email_context)
try:
order.send_mail(
real_subject, message, email_context,
'pretix.event.order.email.event_canceled',
user,
)
except SendMailException:
logger.exception('Order canceled email could not be sent')
for p in positions:
if subevent and p.subevent_id != subevent.id:
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,
'pretix.event.order.email.event_canceled',
position=p,
user=user
)
try:
order.send_mail(
real_subject, message, email_context,
'pretix.event.order.email.event_canceled',
position=p,
user=user
)
except SendMailException:
logger.exception('Order canceled email could not be sent to attendee')
@app.task(base=ProfiledEventTask, bind=True, max_retries=5, default_retry_delay=1, throws=(OrderError,))
+3 -3
View File
@@ -1528,7 +1528,7 @@ class CartManager:
self._sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
cp.addon_to, cp.is_bundled, cp.listed_price - cp.price_after_voucher)
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in positions
]
)
@@ -1639,7 +1639,7 @@ def get_fees(event, request, _total_ignored_=None, invoice_address=None, payment
if fee.tax_rule and not fee.tax_rule.pk:
fee.tax_rule = None # TODO: deprecate
apply_rounding(event.settings.tax_rounding, invoice_address, event.currency, [*positions, *fees])
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
if total != 0 and payments:
@@ -1679,7 +1679,7 @@ def get_fees(event, request, _total_ignored_=None, invoice_address=None, payment
fees.append(pf)
# Re-apply rounding as grand total has changed
apply_rounding(event.settings.tax_rounding, invoice_address, event.currency, [*positions, *fees])
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
# Re-calculate to_pay as grand total has changed
+2 -14
View File
@@ -23,12 +23,11 @@ from datetime import timedelta
from django.conf import settings
from django.core.management import call_command
from django.db.models import Exists, OuterRef
from django.dispatch import receiver
from django.utils.timezone import now
from django_scopes import scopes_disabled
from pretix.base.models import CachedCombinedTicket, CachedTicket, OutgoingMail
from pretix.base.models import CachedCombinedTicket, CachedTicket
from pretix.base.models.customers import CustomerSSOGrant
from ..models import CachedFile, CartPosition, InvoiceAddress
@@ -50,18 +49,7 @@ def clean_cart_positions(sender, **kwargs):
@receiver(signal=periodic_task)
@scopes_disabled()
def clean_cached_files(sender, **kwargs):
has_queued_email = Exists(
OutgoingMail.objects.filter(
should_attach_cached_files__pk=OuterRef("pk"),
status__in=(
OutgoingMail.STATUS_QUEUED,
OutgoingMail.STATUS_INFLIGHT,
OutgoingMail.STATUS_AWAITING_RETRY,
OutgoingMail.STATUS_FAILED,
),
)
)
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()).exclude(has_queued_email):
for cf in CachedFile.objects.filter(expires__isnull=False, expires__lt=now()):
cf.delete()
+1 -1
View File
@@ -121,7 +121,7 @@ class CrossSellingService:
self.sales_channel,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
cp.addon_to, cp.is_bundled,
bool(cp.addon_to), cp.is_bundled,
cp.listed_price - cp.price_after_voucher)
for cp in self.cartpositions
],
+3 -15
View File
@@ -51,7 +51,6 @@ 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,
)
@@ -505,7 +504,7 @@ def generate_invoice(order: Order, trigger_pdf=True):
return invoice
@app.task(base=TransactionAwareTask, throws=(InvoiceNotReadyException,))
@app.task(base=TransactionAwareTask)
def invoice_pdf_task(invoice: int):
with scopes_disabled():
i = Invoice.objects.get(pk=invoice)
@@ -522,20 +521,9 @@ def invoice_pdf_task(invoice: int):
def invoice_qualified(order: Order):
if order.total == Decimal('0.00'):
if order.total == Decimal('0.00') or order.require_approval or \
order.sales_channel.identifier not in order.event.settings.get('invoice_generate_sales_channels'):
return False
if order.require_approval:
return False
if order.sales_channel.identifier not in order.event.settings.invoice_generate_sales_channels:
return False
if order.status in (Order.STATUS_CANCELED, Order.STATUS_EXPIRED):
return False
if order.event.settings.invoice_generate_only_business:
try:
ia = order.invoice_address
return ia.is_business
except InvoiceAddress.DoesNotExist:
return False
return True
File diff suppressed because it is too large Load Diff
+9 -23
View File
@@ -19,8 +19,6 @@
# 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/>.
#
import uuid
import css_inline
from django.conf import settings
from django.template.loader import get_template
@@ -28,9 +26,7 @@ from django.utils.timezone import override
from django_scopes import scope, scopes_disabled
from pretix.base.i18n import language
from pretix.base.models import (
LogEntry, NotificationSetting, OutgoingMail, User,
)
from pretix.base.models import LogEntry, NotificationSetting, User
from pretix.base.notifications import Notification, get_all_notification_types
from pretix.base.services.mail import mail_send_task
from pretix.base.services.tasks import ProfiledTask, TransactionAwareTask
@@ -157,26 +153,16 @@ def send_notification_mail(notification: Notification, user: User):
tpl_plain = get_template('pretixbase/email/notification.txt')
body_plain = tpl_plain.render(ctx)
guid = uuid.uuid4()
m = OutgoingMail.objects.create(
guid=guid,
user=user,
to=[user.email],
subject='[{}] {}: {}'.format(
mail_send_task.apply_async(kwargs={
'to': [user.email],
'subject': '[{}] {}: {}'.format(
settings.PRETIX_INSTANCE_NAME,
notification.event.settings.mail_prefix or notification.event.slug.upper(),
notification.title
),
body_plain=body_plain,
body_html=body_html,
sender=settings.MAIL_FROM_NOTIFICATIONS,
headers={
'X-Auto-Response-Suppress': 'OOF, NRN, AutoReply, RN',
'Auto-Submitted': 'auto-generated',
'X-Mailer': 'pretix',
'X-PX-Correlation': str(guid),
},
)
mail_send_task.apply_async(kwargs={
'outgoing_mail': m.pk,
'body': body_plain,
'html': body_html,
'sender': settings.MAIL_FROM_NOTIFICATIONS,
'headers': {},
'user': user.pk
})
+150 -195
View File
@@ -90,6 +90,7 @@ from pretix.base.services.invoices import (
from pretix.base.services.locking import (
LOCK_TRUST_WINDOW, LockTimeoutException, lock_objects,
)
from pretix.base.services.mail import SendMailException
from pretix.base.services.memberships import (
create_membership, validate_memberships_in_order,
)
@@ -247,16 +248,6 @@ def reactivate_order(order: Order, force: bool=False, user: User=None, auth=None
for gc in position.issued_gift_cards.all():
gc = GiftCard.objects.select_for_update(of=OF_SELF).get(pk=gc.pk)
gc.transactions.create(value=position.price, order=order, acceptor=order.event.organizer)
gc.log_action(
action='pretix.giftcards.transaction.manual',
user=user,
auth=auth,
data={
'value': position.price,
'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
}
)
break
for m in position.granted_memberships.all():
@@ -447,27 +438,33 @@ def approve_order(order, user=None, send_mail: bool=True, auth=None, force=False
email_attendee_subject = order.event.settings.mail_subject_order_approved_attendee
email_context = get_email_context(event=order.event, order=order)
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_approved', user,
attach_tickets=True,
attach_ical=order.event.settings.mail_attach_ical and (
not order.event.settings.mail_attach_ical_paid_only or
order.total == Decimal('0.00') or
order.valid_if_pending
),
invoices=[invoice] if invoice and transmit_invoice_mail else []
)
try:
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_approved', user,
attach_tickets=True,
attach_ical=order.event.settings.mail_attach_ical and (
not order.event.settings.mail_attach_ical_paid_only or
order.total == Decimal('0.00') or
order.valid_if_pending
),
invoices=[invoice] if invoice and transmit_invoice_mail else []
)
except SendMailException:
logger.exception('Order approved email could not be sent')
if email_attendees:
for p in order.positions.all():
if p.addon_to_id is None and p.attendee_email and p.attendee_email != order.email:
email_attendee_context = get_email_context(event=order.event, order=order, position=p)
p.send_mail(
email_attendee_subject, email_attendee_template, email_attendee_context,
'pretix.event.order.email.order_approved', user,
attach_tickets=True,
)
try:
p.send_mail(
email_attendee_subject, email_attendee_template, email_attendee_context,
'pretix.event.order.email.order_approved', user,
attach_tickets=True,
)
except SendMailException:
logger.exception('Order approved email could not be sent to attendee')
return order.pk
@@ -504,10 +501,13 @@ def deny_order(order, comment='', user=None, send_mail: bool=True, auth=None):
email_template = order.event.settings.mail_text_order_denied
email_subject = order.event.settings.mail_subject_order_denied
email_context = get_email_context(event=order.event, order=order, comment=comment)
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_denied', user
)
try:
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_denied', user
)
except SendMailException:
logger.exception('Order denied email could not be sent')
return order.pk
@@ -558,15 +558,6 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
)
else:
gc.transactions.create(value=-position.price, order=order, acceptor=order.event.organizer)
gc.log_action(
action='pretix.giftcards.transaction.manual',
user=user,
data={
'value': -position.price,
'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
}
)
for m in position.granted_memberships.all():
m.canceled = True
@@ -669,11 +660,14 @@ def _cancel_order(order, user=None, send_mail: bool=True, api_token=None, device
email_template = order.event.settings.mail_text_order_canceled
email_subject = order.event.settings.mail_subject_order_canceled
email_context = get_email_context(event=order.event, order=order, comment=comment or "")
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_canceled', user,
invoices=transmit_invoices_mail,
)
try:
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_canceled', user,
invoices=transmit_invoices_mail,
)
except SendMailException:
logger.exception('Order canceled email could not be sent')
for p in order.payments.filter(state__in=(OrderPayment.PAYMENT_STATE_CREATED, OrderPayment.PAYMENT_STATE_PENDING)):
try:
@@ -920,7 +914,7 @@ def _check_positions(event: Event, now_dt: datetime, time_machine_now_dt: dateti
sales_channel.identifier,
[
(cp.item_id, cp.subevent_id, cp.subevent.date_from if cp.subevent_id else None, cp.line_price_gross,
cp.addon_to, cp.is_bundled, cp.listed_price - cp.price_after_voucher)
bool(cp.addon_to), cp.is_bundled, cp.listed_price - cp.price_after_voucher)
for cp in sorted_positions
]
)
@@ -974,7 +968,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
fee.tax_rule = None # TODO: deprecate
# Apply rounding to get final total in case no payment fees will be added
apply_rounding(event.settings.tax_rounding, address, event.currency, [*positions, *fees])
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
payments_assigned = Decimal("0.00")
@@ -1001,7 +995,7 @@ def _apply_rounding_and_fees(positions: List[CartPosition], payment_requests: Li
p['fee'] = pf
# Re-apply rounding as grand total has changed
apply_rounding(event.settings.tax_rounding, address, event.currency, [*positions, *fees])
apply_rounding(event.settings.tax_rounding, event.currency, [*positions, *fees])
total = sum([c.price for c in positions]) + sum([f.value for f in fees])
# Re-calculate to_pay as grand total has changed
@@ -1114,40 +1108,46 @@ def _order_placed_email(event: Event, order: Order, email_template, subject_temp
log_entry: str, invoice, payments: List[OrderPayment], is_free=False):
email_context = get_email_context(event=event, order=order, payments=payments)
order.send_mail(
subject_template, email_template, email_context,
log_entry,
invoices=[invoice] if invoice else [],
attach_tickets=True,
attach_ical=event.settings.mail_attach_ical and (
not event.settings.mail_attach_ical_paid_only or
is_free or
order.valid_if_pending
),
attach_other_files=[a for a in [
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
] if a],
)
try:
order.send_mail(
subject_template, email_template, email_context,
log_entry,
invoices=[invoice] if invoice else [],
attach_tickets=True,
attach_ical=event.settings.mail_attach_ical and (
not event.settings.mail_attach_ical_paid_only or
is_free or
order.valid_if_pending
),
attach_other_files=[a for a in [
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
] if a],
)
except SendMailException:
logger.exception('Order received email could not be sent')
def _order_placed_email_attendee(event: Event, order: Order, position: OrderPosition, email_template, subject_template,
log_entry: str, is_free=False):
email_context = get_email_context(event=event, order=order, position=position)
position.send_mail(
subject_template, email_template, email_context,
log_entry,
invoices=[],
attach_tickets=True,
attach_ical=event.settings.mail_attach_ical and (
not event.settings.mail_attach_ical_paid_only or
is_free or
order.valid_if_pending
),
attach_other_files=[a for a in [
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
] if a],
)
try:
position.send_mail(
subject_template, email_template, email_context,
log_entry,
invoices=[],
attach_tickets=True,
attach_ical=event.settings.mail_attach_ical and (
not event.settings.mail_attach_ical_paid_only or
is_free or
order.valid_if_pending
),
attach_other_files=[a for a in [
event.settings.get('mail_attachment_new_order', as_type=str, default='')[len('file://'):]
] if a],
)
except SendMailException:
logger.exception('Order received email could not be sent to attendee')
def _perform_order(event: Event, payment_requests: List[dict], position_ids: List[str],
@@ -1476,10 +1476,13 @@ def send_expiry_warnings(sender, **kwargs):
email_template = settings.mail_text_order_pending_warning
email_subject = settings.mail_subject_order_pending_warning
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.expire_warning_sent'
)
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.expire_warning_sent'
)
except SendMailException:
logger.exception('Reminder email could not be sent')
@receiver(signal=periodic_task)
@@ -1540,11 +1543,14 @@ def send_download_reminders(sender, **kwargs):
email_template = event.settings.mail_text_download_reminder
email_subject = event.settings.mail_subject_download_reminder
email_context = get_email_context(event=event, order=o)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True
)
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True
)
except SendMailException:
logger.exception('Reminder email could not be sent')
if event.settings.mail_send_download_reminder_attendee:
for p in positions:
@@ -1558,11 +1564,14 @@ def send_download_reminders(sender, **kwargs):
email_template = event.settings.mail_text_download_reminder_attendee
email_subject = event.settings.mail_subject_download_reminder_attendee
email_context = get_email_context(event=event, order=o, position=p)
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True, position=p
)
try:
o.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.download_reminder_sent',
attach_tickets=True, position=p
)
except SendMailException:
logger.exception('Reminder email could not be sent to attendee')
def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
@@ -1570,10 +1579,13 @@ def notify_user_changed_order(order, user=None, auth=None, invoices=[]):
email_template = order.event.settings.mail_text_order_changed
email_context = get_email_context(event=order.event, order=order)
email_subject = order.event.settings.mail_subject_order_changed
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices, attach_tickets=True,
)
try:
order.send_mail(
email_subject, email_template, email_context,
'pretix.event.order.email.order_changed', user, auth=auth, invoices=invoices, attach_tickets=True,
)
except SendMailException:
logger.exception('Order changed email could not be sent')
class OrderChangeManager:
@@ -1629,7 +1641,6 @@ class OrderChangeManager:
ChangeValidUntilOperation = namedtuple('ChangeValidUntilOperation', ('position', 'valid_until'))
AddBlockOperation = namedtuple('AddBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
RemoveBlockOperation = namedtuple('RemoveBlockOperation', ('position', 'block_name', 'ignore_from_quota_while_blocked'))
ForceRecomputeOperation = namedtuple('ForceRecomputeOperation', tuple())
class AddPositionResult:
_position: Optional[OrderPosition]
@@ -1793,12 +1804,13 @@ class OrderChangeManager:
positions = self.order.positions.select_related('item', 'item__tax_rule')
ia = self._invoice_address
tax_rules = self._current_tax_rules()
self._operations.append(self.ForceRecomputeOperation())
for pos in positions:
tax_rule = tax_rules.get(pos.pk, pos.tax_rule)
if not tax_rule:
continue
if not pos.price:
continue
try:
new_rate = tax_rule.tax_rate_for(ia)
@@ -1815,9 +1827,7 @@ class OrderChangeManager:
override_tax_rate=new_rate, override_tax_code=new_code)
self._totaldiff_guesstimate += new_tax.gross - pos.price
self._operations.append(self.PriceOperation(pos, new_tax, new_tax.gross - pos.price))
if pos.price:
# We do not consider the invoice dirty if only 0€-valued taxes are changed
self._invoice_dirty = True
self._invoice_dirty = True
def cancel_fee(self, fee: OrderFee):
self._totaldiff_guesstimate -= fee.value
@@ -2084,43 +2094,6 @@ class OrderChangeManager:
)
item_counts[item] += 1
# Detect removed add-ons and create RemoveOperations
for cp, al in list(current_addons.items()):
for k, v in al.items():
input_num = input_addons[cp.id].get(k, 0)
current_num = len(current_addons[cp].get(k, []))
if input_num < current_num:
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
is_unavailable = (
# If an item is no longer available due to time, it should usually also be no longer
# user-removable, because e.g. the stock has already been ordered.
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
# not mean it should be unremovable for others.
# This also prevents accidental removal through the UI because a hidden product will no longer
# be part of the input.
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
or (
not a.item.all_sales_channels and
not a.item.limit_sales_channels.contains(self.order.sales_channel)
)
)
if is_unavailable:
# "Re-select" add-on
selected_addons[cp.id, a.item.category_id][a.item_id, a.variation_id] += 1
continue
if a.checkins.filter(list__consider_tickets_used=True).exists():
raise OrderError(
error_messages['addon_already_checked_in'] % {
'addon': str(a.item.name),
}
)
self.cancel(a)
item_counts[a.item] -= 1
# Check constraints on the add-on combinations
for op in toplevel_op:
item = op.item
@@ -2153,6 +2126,41 @@ class OrderChangeManager:
}
)
# Detect removed add-ons and create RemoveOperations
for cp, al in list(current_addons.items()):
for k, v in al.items():
input_num = input_addons[cp.id].get(k, 0)
current_num = len(current_addons[cp].get(k, []))
if input_num < current_num:
for a in current_addons[cp][k][:current_num - input_num]:
if a.canceled:
continue
is_unavailable = (
# If an item is no longer available due to time, it should usually also be no longer
# user-removable, because e.g. the stock has already been ordered.
# We always pass has_voucher=True because if a product now requires a voucher, it usually does
# not mean it should be unremovable for others.
# This also prevents accidental removal through the UI because a hidden product will no longer
# be part of the input.
(a.variation and a.variation.unavailability_reason(has_voucher=True, subevent=a.subevent))
or (a.variation and not a.variation.all_sales_channels and not a.variation.limit_sales_channels.contains(self.order.sales_channel))
or a.item.unavailability_reason(has_voucher=True, subevent=a.subevent)
or (
not item.all_sales_channels and
not item.limit_sales_channels.contains(self.order.sales_channel)
)
)
if is_unavailable:
continue
if a.checkins.filter(list__consider_tickets_used=True).exists():
raise OrderError(
error_messages['addon_already_checked_in'] % {
'addon': str(a.item.name),
}
)
self.cancel(a)
item_counts[a.item] -= 1
for item, count in item_counts.items():
if count == 0:
continue
@@ -2453,16 +2461,6 @@ class OrderChangeManager:
))
else:
gc.transactions.create(value=-position.price, order=self.order, acceptor=self.order.event.organizer)
gc.log_action(
action='pretix.giftcards.transaction.manual',
user=self.user,
auth=self.auth,
data={
'value': -position.price,
'acceptor_id': self.order.event.organizer.id,
'acceptor_slug': self.order.event.organizer.slug
}
)
for m in position.granted_memberships.with_usages().all():
m.canceled = True
@@ -2480,16 +2478,6 @@ class OrderChangeManager:
))
else:
gc.transactions.create(value=-opa.position.price, order=self.order, acceptor=self.order.event.organizer)
gc.log_action(
action='pretix.giftcards.transaction.manual',
user=self.user,
auth=self.auth,
data={
'value': -opa.position.price,
'acceptor_id': self.order.event.organizer.id,
'acceptor_slug': self.order.event.organizer.slug
}
)
for m in opa.granted_memberships.with_usages().all():
m.canceled = True
@@ -2652,10 +2640,6 @@ class OrderChangeManager:
except BlockedTicketSecret.DoesNotExist:
pass
# todo: revoke list handling
elif isinstance(op, self.ForceRecomputeOperation):
self.order.log_action('pretix.event.order.changed.recomputed', user=self.user, auth=self.auth, data={})
else:
raise TypeError(f"Unknown operation {type(op)}")
for p in secret_dirty:
assign_ticket_secret(
@@ -2710,10 +2694,7 @@ class OrderChangeManager:
fees.append(new_fee)
changed_by_rounding = set(apply_rounding(
self.order.tax_rounding_mode,
self._invoice_address,
self.event.currency,
[p for p in split_positions if not p.canceled] + fees
self.order.tax_rounding_mode, self.event.currency, [p for p in split_positions if not p.canceled] + fees
))
split_order.total = sum([p.price for p in split_positions if not p.canceled])
@@ -2735,10 +2716,7 @@ class OrderChangeManager:
fee.delete()
changed_by_rounding |= set(apply_rounding(
self.order.tax_rounding_mode,
self._invoice_address,
self.event.currency,
[p for p in split_positions if not p.canceled] + fees
self.order.tax_rounding_mode, self.event.currency, [p for p in split_positions if not p.canceled] + fees
))
split_order.total = sum([p.price for p in split_positions if not p.canceled]) + sum([f.value for f in fees])
@@ -2855,12 +2833,7 @@ class OrderChangeManager:
if fee_changed:
fees = list(self.order.fees.all())
changed = apply_rounding(
self.order.tax_rounding_mode,
self._invoice_address,
self.order.event.currency,
[*positions, *fees]
)
changed = apply_rounding(self.order.tax_rounding_mode, self.order.event.currency, [*positions, *fees])
for l in changed:
if isinstance(l, OrderPosition):
l.save(update_fields=[
@@ -3158,10 +3131,7 @@ def _try_auto_refund(order, auto_refund=True, manual_refund=False, allow_partial
customer=order.customer,
testmode=order.testmode
)
giftcard.log_action(
action='pretix.giftcards.created',
data={}
)
giftcard.log_action('pretix.giftcards.created', data={})
r = order.refunds.create(
order=order,
payment=None,
@@ -3299,12 +3269,8 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
positions = list(order.positions.all())
fees = list(order.fees.all())
try:
ia = order.invoice_address
except InvoiceAddress.DoesNotExist:
ia = None
rounding_changed = set(apply_rounding(
order.tax_rounding_mode, ia, order.event.currency, [*positions, *[f for f in fees if f.pk != fee.pk]]
order.tax_rounding_mode, order.event.currency, [*positions, *[f for f in fees if f.pk != fee.pk]]
))
total_without_fee = sum(c.price for c in positions) + sum(f.value for f in fees if f.pk != fee.pk)
pending_sum_without_fee = max(Decimal("0.00"), total_without_fee - already_paid)
@@ -3329,7 +3295,7 @@ def change_payment_provider(order: Order, payment_provider, amount=None, new_pay
fee = None
rounding_changed |= set(apply_rounding(
order.tax_rounding_mode, ia, order.event.currency, [*positions, *fees]
order.tax_rounding_mode, order.event.currency, [*positions, *fees]
))
for l in rounding_changed:
if isinstance(l, OrderPosition):
@@ -3448,18 +3414,7 @@ def signal_listener_issue_giftcards(sender: Event, order: Order, **kwargs):
currency=sender.currency, issued_in=p, testmode=order.testmode,
expires=sender.organizer.default_gift_card_expiry,
)
gc.log_action(
action='pretix.giftcards.created',
)
trans = gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer)
gc.log_action(
action='pretix.giftcards.transaction.manual',
data={
'value': trans.value,
'acceptor_id': order.event.organizer.id,
'acceptor_slug': order.event.organizer.slug
}
)
gc.transactions.create(value=p.price - issued, order=order, acceptor=sender.organizer)
any_giftcards = True
p.secret = gc.secret
p.save(update_fields=['secret'])
-7
View File
@@ -24,7 +24,6 @@ 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
@@ -36,7 +35,6 @@ 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 (
@@ -754,11 +752,6 @@ 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],
+5 -14
View File
@@ -174,9 +174,7 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
:param event: Event the cart belongs to
:param sales_channel: Sales channel the cart was created with
:param positions: Tuple of the form ``(item_id, subevent_id, subevent_date_from, line_price_gross, addon_to_id, is_bundled, voucher_discount)``
``addon_to_id`` does not have to be the proper ID, any identifier is okay, even ``True``/``False`` are accepted, but
a better result may be given if addons to the same main product have the same distinct value.
:param positions: Tuple of the form ``(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)``
:param collect_potential_discounts: If a `defaultdict(list)` is supplied, all discounts that could be applied to the cart
based on the "consumed" items, but lack matching "benefitting" items will be collected therein.
The dict will contain a mapping from index in the `positions` list of the item that could be consumed, to a list
@@ -198,9 +196,9 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
).prefetch_related('condition_limit_products', 'benefit_limit_products').order_by('position', 'pk')
for discount in discount_qs:
result = discount.apply({
idx: PositionInfo(item_id, subevent_id, subevent_date_from, line_price_gross, addon_to, voucher_discount)
idx: PositionInfo(item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, voucher_discount)
for
idx, (item_id, subevent_id, subevent_date_from, line_price_gross, addon_to, is_bundled, voucher_discount)
idx, (item_id, subevent_id, subevent_date_from, line_price_gross, is_addon_to, is_bundled, voucher_discount)
in enumerate(positions)
if not is_bundled and idx not in new_prices
}, collect_potential_discounts)
@@ -211,8 +209,7 @@ def apply_discounts(event: Event, sales_channel: Union[str, SalesChannel],
return [new_prices.get(idx, (p[3], None)) for idx, p in enumerate(positions)]
def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only_business", "sum_by_net_keep_gross"],
invoice_address: Optional[InvoiceAddress], currency: str,
def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_keep_gross"], currency: str,
lines: List[Union[OrderPosition, CartPosition, OrderFee]]) -> list:
"""
Given a list of order positions / cart positions / order fees (may be mixed), applies the given rounding mode
@@ -227,17 +224,11 @@ def apply_rounding(rounding_mode: Literal["line", "sum_by_net", "sum_by_net_only
When rounding mode is set to ``"sum_by_net"``, the gross prices and tax values of the individual lines will be
adjusted such that the per-taxrate/taxcode subtotal is rounded correctly. The net prices will stay constant.
:param rounding_mode: One of ``"line"``, ``"sum_by_net"``, ``"sum_by_net_only_business"``, or ``"sum_by_net_keep_gross"``.
:param invoice_address: The invoice address, or ``None``
:param rounding_mode: One of ``"line"``, ``"sum_by_net"``, or ``"sum_by_net_keep_gross"``.
:param currency: Currency that will be used to determine rounding precision
:param lines: List of order/cart contents
:return: Collection of ``lines`` members that have been changed and may need to be persisted to the database.
"""
if rounding_mode == "sum_by_net_only_business":
if invoice_address and invoice_address.is_business:
rounding_mode = "sum_by_net"
else:
rounding_mode = "line"
def _key(line):
return (line.tax_rate, line.tax_code or "")
+19 -17
View File
@@ -48,7 +48,7 @@ from django.utils.translation import gettext_lazy as _
from pretix.base.i18n import language
from pretix.base.models import CachedFile, Event, User, cachedfile_name
from pretix.base.services.mail import mail
from pretix.base.services.mail import SendMailException, mail
from pretix.base.services.tasks import ProfiledEventTask
from pretix.base.shredder import ShredError
from pretix.celery_app import app
@@ -171,19 +171,21 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
if user:
with language(user.locale):
mail(
user.email,
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
},
event=None,
user=user,
locale=user.locale,
)
try:
mail(
user.email,
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),
'start_time': date_format(parse(indexdata['time']).astimezone(event.timezone), 'SHORT_DATETIME_FORMAT'),
'shredders': ', '.join([str(s.verbose_name) for s in shredders])
},
event=None,
user=user,
locale=user.locale,
)
except SendMailException:
pass # Already logged
+1 -10
View File
@@ -112,8 +112,7 @@ def dictsum(*dicts) -> dict:
def order_overview(
event: Event, subevent: SubEvent=None, date_filter='', date_from=None, date_until=None, fees=False,
admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_date_until=None,
skip_empty_lines=False,
admission_only=False, base_qs=None, base_fees_qs=None, subevent_date_from=None, subevent_date_until=None
) -> Tuple[List[Tuple[ItemCategory, List[Item]]], Dict[str, Tuple[Decimal, Decimal]]]:
items = event.items.all().select_related(
'category', # for re-grouping
@@ -206,21 +205,13 @@ def order_overview(
for l in states.keys():
var.num[l] = num[l].get((item.id, variid), (0, 0, 0))
var.num['total'] = num['total'].get((item.id, variid), (0, 0, 0))
var._skip = all(v[0] == 0 for v in var.num.values())
for l in states.keys():
item.num[l] = tuplesum(var.num[l] for var in item.all_variations)
item.num['total'] = tuplesum(var.num['total'] for var in item.all_variations)
if skip_empty_lines:
item.all_variations = [v for v in item.all_variations if not v._skip]
item._skip = not item.all_variations
else:
for l in states.keys():
item.num[l] = num[l].get((item.id, None), (0, 0, 0))
item.num['total'] = num['total'].get((item.id, None), (0, 0, 0))
item._skip = all(v[0] == 0 for v in item.num.values())
if skip_empty_lines:
items = [i for i in items if not i._skip]
nonecat = ItemCategory(name=_('Uncategorized'))
# Regroup those by category
+3 -3
View File
@@ -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
))
+2 -49
View File
@@ -76,12 +76,11 @@ from pretix.base.validators import multimail_validate
from pretix.control.forms import (
ExtFileField, FontSelect, MultipleLanguagesWidget, SingleLanguageWidget,
)
from pretix.helpers.countries import CachedCountries, pycountry_add
from pretix.helpers.countries import CachedCountries
ROUNDING_MODES = (
('line', _('Compute taxes for every line individually')),
('sum_by_net', _('Compute taxes based on net total')),
('sum_by_net_only_business', _('For business customers, compute taxes based on net total. For individuals, use line-based rounding')),
('sum_by_net_keep_gross', _('Compute taxes based on net total with stable gross prices')),
# We could also have sum_by_gross, but we're not aware of any use-cases for it
)
@@ -181,19 +180,6 @@ DEFAULTS = {
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-customer_accounts'}),
)
},
'customer_accounts_require_login_for_order_access': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Require login to access order confirmation pages"),
help_text=_("If enabled, users who were logged in at the time of purchase must also log in to access their order information. "
"If a customer account is created while placing an order, the restriction only becomes active after the customer "
"account is activated."),
widget=forms.CheckboxInput(attrs={'data-display-dependency': '#id_settings-customer_accounts'}),
)
},
'customer_accounts_link_by_email': {
'default': 'False',
'type': bool,
@@ -1226,15 +1212,6 @@ DEFAULTS = {
'default': json.dumps(['web']),
'type': list
},
'invoice_generate_only_business': {
'default': 'False',
'type': bool,
'form_class': forms.BooleanField,
'serializer_class': serializers.BooleanField,
'form_kwargs': dict(
label=_("Only issue invoices to business customers"),
)
},
'invoice_address_from': {
'default': '',
'type': str,
@@ -2948,28 +2925,6 @@ If you did not request a new password, please ignore this email.
Best regards,
Your {organizer} team""")) # noqa: W291
},
'mail_subject_customer_security_notice': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("Changes to your account at {organizer}")),
},
'mail_text_customer_security_notice': {
'type': LazyI18nString,
'default': LazyI18nString.from_gettext(gettext_noop("""Hello {name},
the following change has been made to your account at {organizer}:
{message}
You can review and change your account settings here:
{url}
If this change was not performed by you, please contact us immediately.
Best regards,
Your {organizer} team""")) # noqa: W291
},
'smtp_use_custom': {
@@ -3959,7 +3914,7 @@ COUNTRIES_WITH_STATE_IN_ADDRESS = {
'MX': (['State', 'Federal district', 'Federal entity'], 'short'),
'US': (['State', 'Outlying area', 'District'], 'short'),
'IT': (['Province', 'Free municipal consortium', 'Metropolitan city', 'Autonomous province',
'Decentralized regional entity'], 'short'),
'Free municipal consortium', 'Decentralized regional entity'], 'short'),
}
COUNTRY_STATE_LABEL = {
# Countries in which the "State" field should not be called "State"
@@ -3967,8 +3922,6 @@ COUNTRY_STATE_LABEL = {
'JP': pgettext_lazy('address', 'Prefecture'),
'IT': pgettext_lazy('address', 'Province'),
}
# Workaround for https://github.com/pretix/pretix/issues/5796
pycountry_add(pycountry.subdivisions, code="IT-AO", country_code="IT", name="Valle d'Aosta", parent="23", parent_code="IT-23", type="Province")
settings_hierarkey = Hierarkey(attribute_name='settings')
+2 -6
View File
@@ -51,7 +51,7 @@ from pretix.api.serializers.waitinglist import WaitingListSerializer
from pretix.base.i18n import LazyLocaleException
from pretix.base.models import (
CachedCombinedTicket, CachedTicket, Event, InvoiceAddress, OrderPayment,
OrderPosition, OrderRefund, OutgoingMail, QuestionAnswer,
OrderPosition, OrderRefund, QuestionAnswer,
)
from pretix.base.services.invoices import invoice_pdf_task
from pretix.base.signals import register_data_shredders
@@ -329,10 +329,6 @@ class EmailAddressShredder(BaseDataShredder):
sleep_time=2,
)
slow_delete(
OutgoingMail.objects.filter(event=self.event)
)
for o in _progress_helper(qs_orders, progress_callback, qs_op_cnt, total):
changed = bool(o.email) or bool(o.customer)
o.email = None
@@ -363,7 +359,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'
])
+2 -10
View File
@@ -944,40 +944,32 @@ As with all event-plugin signals, the ``sender`` keyword argument will contain t
email_filter = EventPluginSignal()
"""
Arguments: ``message``, ``order``, ``user``, ``outgoing_mail``
Arguments: ``message``, ``order``, ``user``
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
return a (possibly modified) copy of the message object passed to you.
As with all event-plugin signals, the ``sender`` keyword argument will contain the event.
The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
The ``outgoing_mail`` argument will contain the ``OutgoingMail`` model instance. Note that the ``message`` object
might have newer information if a previous plugin already modified the email.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``.
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
well, otherwise it will be ``None``.
You can raise ``WithholdMailException`` to prevent the email from being sent, e.g. when implementing rate limiting.
"""
global_email_filter = GlobalSignal()
"""
Arguments: ``message``, ``order``, ``user``, ``customer``, ``organizer``, ``outgoing_mail``
Arguments: ``message``, ``order``, ``user``, ``customer``, ``organizer``
This signal allows you to implement a middleware-style filter on all outgoing emails. You are expected to
return a (possibly modified) copy of the message object passed to you.
This signal is called on all events and even if there is no known event. ``sender`` is an event or None.
The ``message`` argument will contain an ``EmailMultiAlternatives`` object.
The ``outgoing_mail`` argument will contain the ``OutgoingMail`` model instance. Note that the ``message`` object
might have newer information if a previous plugin already modified the email.
If the email is associated with a specific order, the ``order`` argument will be passed as well, otherwise
it will be ``None``.
If the email is associated with a specific user, e.g. a notification email, the ``user`` argument will be passed as
well, otherwise it will be ``None``.
You can raise ``WithholdMailException`` to prevent the email from being sent, e.g. when implementing rate limiting.
"""
+3
View File
@@ -8,6 +8,9 @@
<h1>{% trans "Not found" %}</h1>
<p>{% trans "I'm afraid we could not find the the resource you requested." %}</p>
<p>{{ exception }}</p>
<p class="links">
<a id='goback' href='#'>{% trans "Take a step back" %}</a>
</p>
{% if request.user.is_staff and not staff_session %}
<form action="{% url 'control:user.sudo' %}?next={{ request.path|add:"?"|add:request.GET.urlencode|urlencode }}" method="post">
<p>
-3
View File
@@ -12,9 +12,6 @@
<meta charset="utf-8">
<link rel="icon" href="{% static "pretixbase/img/favicon.ico" %}">
{% block custom_header %}{% endblock %}
{% if css_theme %}
<link rel="stylesheet" type="text/css" href="{{ css_theme }}" />
{% endif %}
</head>
<body>
<div class="container">
@@ -13,5 +13,5 @@ Start time: {{ start_time }} (new data added after this time might not have been
Best regards,
Your {{ instance }} team
Your pretix team
{% endblocktrans %}
@@ -1,34 +0,0 @@
#
# 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 <https://pretix.eu/about/en/license>.
#
# 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
# <https://www.gnu.org/licenses/>.
#
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]))
+8 -6
View File
@@ -26,8 +26,7 @@ from babel.numbers import format_currency
from django import template
from django.conf import settings
from django.template.defaultfilters import floatformat
from pretix.base.i18n import get_babel_locale
from django.utils import translation
register = template.Library()
@@ -60,10 +59,13 @@ def money_filter(value: Decimal, arg='', hide_currency=False):
if hide_currency:
return floatformat(value, f"{places}g")
try:
locale = Locale(get_babel_locale())
except UnknownLocaleError:
locale = "en"
locale_parts = translation.get_language().split("-", 1)
locale = locale_parts[0]
if len(locale_parts) > 1 and len(locale_parts[1]) == 2:
try:
locale = Locale(locale_parts[0], locale_parts[1].upper())
except UnknownLocaleError:
pass
try:
return format_currency(value, arg, locale=locale)
+1 -1
View File
@@ -156,7 +156,7 @@ def safelink_callback(attrs, new=False):
Makes sure that all links to a different domain are passed through a redirection handler
to ensure there's no passing of referers with secrets inside them.
"""
url = html.unescape(attrs.get((None, 'href'), '/'))
url = attrs.get((None, 'href'), '/')
if not url_has_allowed_host_and_scheme(url, allowed_hosts=None) and not url.startswith('mailto:') and not url.startswith('tel:'):
signer = signing.Signer(salt='safe-redirect')
attrs[None, 'href'] = reverse('redirect') + '?url=' + urllib.parse.quote(signer.sign(url))
+1 -1
View File
@@ -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[datetime], Optional[datetime]]:
def resolve_timeframe_to_datetime_start_inclusive_end_exclusive(ref_dt, frame, timezone) -> Tuple[Optional[date], Optional[date]]:
"""
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
-1
View File
@@ -95,7 +95,6 @@ class OrganizerSlugBanlistValidator(BanlistValidator):
'csp_report',
'widget',
'lead',
'scheduling',
]
+3 -16
View File
@@ -20,7 +20,6 @@
# <https://www.gnu.org/licenses/>.
#
import pycountry
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext, pgettext, pgettext_lazy
@@ -30,7 +29,6 @@ from django_scopes import scope
from pretix.base.addressvalidation import (
COUNTRIES_WITH_STREET_ZIPCODE_AND_CITY_REQUIRED,
)
from pretix.base.i18n import language
from pretix.base.invoicing.transmission import get_transmission_types
from pretix.base.models import Organizer
from pretix.base.models.tax import VAT_ID_COUNTRIES
@@ -84,14 +82,14 @@ def _info(cc):
statelist = [s for s in pycountry.subdivisions.get(country_code=cc) if s.type in types]
return {
'data': [
{'name': gettext(s.name), 'code': s.code[3:]}
{'name': s.name, 'code': s.code[3:]}
for s in sorted(statelist, key=lambda s: s.name)
],
**info,
}
def _address_form(request):
def address_form(request):
cc = request.GET.get("country", "DE")
info = _info(cc)
@@ -111,7 +109,7 @@ def _address_form(request):
for t in get_transmission_types():
if t.is_available(event=event, country=country, is_business=is_business):
result = {"name": str(t.public_name), "code": t.identifier}
if t.is_exclusive(event=event, country=country, is_business=is_business):
if t.exclusive:
info["transmission_types"] = [result]
break
else:
@@ -159,15 +157,4 @@ def _address_form(request):
# The help text explains that it is optional, so we want to hide that if it is required
info["vat_id"]["helptext_visible"] = False
return info
def address_form(request):
locale = request.GET.get('locale')
if locale in dict(settings.LANGUAGES):
with language(locale):
info = _address_form(request)
else:
info = _address_form(request)
return JsonResponse(info)
-6
View File
@@ -867,11 +867,6 @@ class TaxSettingsForm(EventSettingsValidationMixin, SettingsForm):
"The gross price of some products may be changed to ensure correct rounding, while the net "
"prices will be kept as configured. This may cause the actual payment amount to differ."
),
"sum_by_net_only_business": _(
"Same as above, but only applied to business customers. Line-based rounding will be used for consumers. "
"Recommended when e-invoicing is only used for business customers and consumers do not receive "
"invoices. This can cause the payment amount to change when the invoice address is changed."
),
"sum_by_net_keep_gross": _(
"Recommended for e-invoicing when you primarily sell to consumers. "
"The gross or net price of some products may be changed automatically to ensure correct "
@@ -944,7 +939,6 @@ class InvoiceSettingsForm(EventSettingsValidationMixin, SettingsForm):
'invoice_show_payments',
'invoice_reissue_after_modify',
'invoice_generate',
'invoice_generate_only_business',
'invoice_period',
'invoice_attendee_name',
'invoice_event_location',
+6 -65
View File
@@ -57,9 +57,8 @@ from pretix.base.forms.widgets import (
from pretix.base.models import (
Checkin, CheckinList, Device, Event, EventMetaProperty, EventMetaValue,
Gate, Invoice, InvoiceAddress, Item, Order, OrderPayment, OrderPosition,
OrderRefund, Organizer, OutgoingMail, Question, QuestionAnswer, Quota,
SalesChannel, SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite,
Voucher,
OrderRefund, Organizer, Question, QuestionAnswer, Quota, SalesChannel,
SubEvent, SubEventMetaValue, Team, TeamAPIToken, TeamInvite, Voucher,
)
from pretix.base.signals import register_payment_providers
from pretix.base.timeframes import (
@@ -1316,10 +1315,10 @@ class QuestionAnswerFilterForm(forms.Form):
if date_range is not None:
d_start, d_end = resolve_timeframe_to_datetime_start_inclusive_end_exclusive(now(), date_range, self.event.timezone)
if d_start:
opqs = opqs.filter(subevent__date_from__gte=d_start)
if d_end:
opqs = opqs.filter(subevent__date_from__lt=d_end)
opqs = opqs.filter(
subevent__date_from__gte=d_start,
subevent__date_from__lt=d_end
)
s = fdata.get("status", Order.STATUS_PENDING + Order.STATUS_PAID)
if s != "":
@@ -2816,61 +2815,3 @@ class DeviceFilterForm(FilterForm):
qs = qs.order_by('-device_id')
return qs
class OutgoingMailFilterForm(FilterForm):
orders = {
'date': 'created',
'-date': '-created',
}
query = forms.CharField(
label=_('Search email address or subject'),
widget=forms.TextInput(attrs={
'placeholder': _('Search email address or subject'),
}),
required=False
)
event = forms.ModelChoiceField(
queryset=Event.objects.none(),
label=_('Event'),
empty_label=_('All events'),
required=False,
)
status = forms.ChoiceField(
label=_('Status'),
choices=[
('', _('All')),
*OutgoingMail.STATUS_CHOICES,
],
required=False
)
def __init__(self, *args, **kwargs):
request = kwargs.pop('request')
super().__init__(*args, **kwargs)
self.fields['event'].queryset = request.organizer.events.all()
def filter_qs(self, qs):
fdata = self.cleaned_data
if fdata.get('query'):
query = fdata.get('query')
qs = qs.filter(
Q(to__containsstring=query.lower())
| Q(cc__containsstring=query.lower())
| Q(bcc__containsstring=query.lower())
| Q(subject__icontains=query)
)
if fdata.get('event'):
qs = qs.filter(event=fdata['event'])
if fdata.get('status'):
qs = qs.filter(status=fdata['status'])
if fdata.get('ordering'):
qs = qs.order_by(self.get_order_by())
else:
qs = qs.order_by("-created", "-pk")
return qs
-17
View File
@@ -474,7 +474,6 @@ class OrganizerSettingsForm(SettingsForm):
'customer_accounts',
'customer_accounts_native',
'customer_accounts_link_by_email',
'customer_accounts_require_login_for_order_access',
'invoice_regenerate_allowed',
'contact_mail',
'imprint_url',
@@ -585,7 +584,6 @@ class MailSettingsForm(SettingsForm):
help_text=''.join([
str(_("All emails will be sent to this address as a Bcc copy.")),
str(_("You can specify multiple recipients separated by commas.")),
str(_("Sensitive emails like password resets will not be sent in Bcc.")),
]),
validators=[multimail_validate],
required=False,
@@ -635,16 +633,6 @@ class MailSettingsForm(SettingsForm):
required=False,
widget=I18nMarkdownTextarea,
)
mail_subject_customer_security_notice = I18nFormField(
label=_("Subject"),
required=False,
widget=I18nTextInput,
)
mail_text_customer_security_notice = I18nFormField(
label=_("Text"),
required=False,
widget=I18nMarkdownTextarea,
)
base_context = {
'mail_text_customer_registration': ['customer', 'url'],
@@ -653,8 +641,6 @@ class MailSettingsForm(SettingsForm):
'mail_subject_customer_email_change': ['customer', 'url'],
'mail_text_customer_reset': ['customer', 'url'],
'mail_subject_customer_reset': ['customer', 'url'],
'mail_text_customer_security_notice': ['customer', 'url', 'message'],
'mail_subject_customer_security_notice': ['customer', 'url', 'message'],
}
def _get_sample_context(self, base_parameters):
@@ -668,9 +654,6 @@ class MailSettingsForm(SettingsForm):
'presale:organizer.customer.activate'
) + '?token=' + get_random_string(30)
if 'message' in base_parameters:
placeholders['message'] = _('Your password has been changed.')
if 'customer' in base_parameters:
placeholders['name'] = pgettext_lazy('person_name_sample', 'John Doe')
name_scheme = PERSON_NAME_SCHEMES[self.organizer.settings.name_scheme]
+1 -89
View File
@@ -19,44 +19,17 @@
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
# <https://www.gnu.org/licenses/>.
#
from django import forms
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django_scopes.forms import SafeModelChoiceField
from phonenumber_field.formfields import PhoneNumberField
from pretix.base.forms import I18nModelForm
from pretix.base.forms.questions import (
NamePartsFormField, WrappedPhoneNumberPrefixWidget,
)
from pretix.base.models import WaitingListEntry
from pretix.control.forms.widgets import Select2
class WaitingListEntryEditForm(I18nModelForm):
itemvar = forms.ChoiceField(
error_messages={
'invalid_choice': _("Select a valid choice.")
}
)
class WaitingListEntryTransferForm(I18nModelForm):
def __init__(self, *args, **kwargs):
self.instance = kwargs.get('instance', None)
initial = kwargs.get('initial', {})
choices = []
if self.instance and self.instance.pk and 'itemvar' not in initial:
if self.instance.variation is not None:
initial['itemvar'] = f'{self.instance.item.pk}-{self.instance.variation.pk}'
if self.instance.variation.active is False:
choices.append((initial['itemvar'], str(self.instance.variation)))
else:
initial['itemvar'] = self.instance.item.pk
if self.instance.item.active is False:
choices.append((initial['itemvar'], str(self.instance)))
kwargs['initial'] = initial
super().__init__(*args, **kwargs)
if self.event.has_subevents:
@@ -72,73 +45,12 @@ class WaitingListEntryEditForm(I18nModelForm):
}
)
self.fields['subevent'].widget.choices = self.fields['subevent'].choices
else:
del self.fields['subevent']
if self.event.settings.waiting_list_names_asked:
self.fields['name_parts'] = NamePartsFormField(
max_length=255,
required=self.event.settings.waiting_list_names_required,
scheme=self.event.organizer.settings.name_scheme,
titles=self.event.organizer.settings.name_scheme_titles,
label=_('Name'),
)
else:
del self.fields['name_parts']
if not self.event.settings.waiting_list_phones_asked:
del self.fields['phone']
items = self.event.items.filter(active=True).prefetch_related(
'variations'
)
for item in items:
if len(item.variations.all()) > 0:
for variation in item.variations.all():
if variation.active:
choices.append(
('{}-{}'.format(item.pk, variation.pk), '{} - {}'.format(str(item), str(variation)))
)
else:
choices.append(('{}'.format(item.pk), str(item)))
self.fields['itemvar'].label = _("Product")
self.fields['itemvar'].help_text = _("Only includes active products.")
self.fields['itemvar'].required = True
self.fields['itemvar'].choices = choices
def clean(self):
cleaned_data = super().clean()
if self.instance.voucher is not None:
raise forms.ValidationError(_('A voucher for this waiting list entry was already sent out.'))
itemvar = cleaned_data.get('itemvar')
if itemvar:
self.instance.item = self.event.items.get(pk=itemvar.split('-')[0])
if '-' in itemvar:
self.instance.variation = self.instance.item.variations.get(pk=itemvar.split('-')[1])
if ((self.instance.item and not self.instance.item.active) or
(self.instance.variation and not self.instance.variation.active)):
self.add_error('itemvar', _('The selected product is not active.'))
return cleaned_data
class Meta:
model = WaitingListEntry
fields = [
'email',
'name_parts',
'phone',
'subevent',
]
field_classes = {
'subevent': SafeModelChoiceField,
'email': forms.EmailField,
'phone': PhoneNumberField,
}
widgets = {
'phone': WrappedPhoneNumberPrefixWidget,
}
-11
View File
@@ -170,12 +170,6 @@ class OrderFeeAdded(OrderChangeLogEntryType):
plain = _('A fee has been added')
@log_entry_types.new()
class OrderRecomputed(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.recomputed'
plain = _('Taxes and rounding have been recomputed')
@log_entry_types.new()
class OrderFeeChanged(OrderChangeLogEntryType):
action_type = 'pretix.event.order.changed.feevalue'
@@ -518,7 +512,6 @@ 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': _(
@@ -706,8 +699,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.organizer.export.schedule.deleted': _('A scheduled export has been deleted.'),
'pretix.organizer.export.schedule.executed': _('A scheduled export has been executed.'),
'pretix.organizer.export.schedule.failed': _('A scheduled export has failed: {reason}.'),
'pretix.organizer.outgoingmails.retried': _('Failed emails have been scheduled to be retried.'),
'pretix.organizer.outgoingmails.aborted': _('Queued emails have been aborted.'),
'pretix.giftcards.acceptance.added': _('Gift card acceptance for another organizer has been added.'),
'pretix.giftcards.acceptance.removed': _('Gift card acceptance for another organizer has been removed.'),
'pretix.giftcards.acceptance.acceptor.invited': _('A new gift card acceptor has been invited.'),
@@ -802,8 +793,6 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.giftcards.created': _('The gift card has been created.'),
'pretix.giftcards.modified': _('The gift card has been changed.'),
'pretix.giftcards.transaction.manual': _('A manual transaction has been performed.'),
'pretix.giftcards.transaction.payment': _('A payment has been performed.'),
'pretix.giftcards.transaction.refund': _('A refund has been performed. '),
'pretix.team.token.created': _('The token "{name}" has been created.'),
'pretix.team.token.deleted': _('The token "{name}" has been revoked.'),
'pretix.event.checkin.reset': _('The check-in and print log state has been reset.')
-9
View File
@@ -679,15 +679,6 @@ def get_organizer_navigation(request):
'active': (url.url_name == 'organizer.datasync.failedjobs'),
}])
nav.append({
'label': _('Outgoing emails'),
'url': reverse('control:organizer.outgoingmails', kwargs={
'organizer': request.organizer.slug,
}),
'active': 'organizer.outgoingmail' in url.url_name,
'icon': 'send',
})
merge_in(nav, sorted(
sum((list(a[1]) for a in nav_organizer.send(request.organizer, request=request, organizer=request.organizer)),
[]),
@@ -19,14 +19,6 @@
</ul>
<br>
{% endif %}
{% if possible_cookie_problem %}
<div class="alert alert-warning">
{% blocktrans trimmed %}
It looks like your browser is not accepting our cookie and you need to log in repeatedly. Please
check if your browser is set to block cookies, or delete all existing cookies and retry.
{% endblocktrans %}
</div>
{% endif %}
{% csrf_token %}
{% bootstrap_form form %}
<div class="form-group buttons">
@@ -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 {{ instance }} team
Your pretix team
{% endblocktrans %}
@@ -5,5 +5,5 @@ you requested a new password. Please go to the following page to reset your pass
{{ url }}
Best regards,
Your {{ instance }} team
{% endblocktrans %}
Your pretix team
{% endblocktrans %}
@@ -1,6 +1,6 @@
{% load i18n %}{% blocktrans with url=url|safe %}Hello,
you have been invited to a team on {{ instance }}, a platform to perform event
you have been invited to a team on pretix, 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 {{ instance }} team
Your pretix team
{% endblocktrans %}
@@ -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 {{ instance }} account has been
this is to inform you that the account information of your pretix 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 {{ instance }} team
Your pretix team
{% endblocktrans %}
@@ -12,7 +12,6 @@
<legend>{% trans "Invoice generation" %}</legend>
{% bootstrap_field form.invoice_generate layout="control" %}
{% bootstrap_field form.invoice_generate_sales_channels layout="control" %}
{% bootstrap_field form.invoice_generate_only_business layout="control" %}
{% bootstrap_field form.invoice_email_attachment layout="control" %}
{% bootstrap_field form.invoice_email_organizer layout="control" %}
{% bootstrap_field form.invoice_language layout="control" %}
@@ -112,6 +111,11 @@
<span class="text-success">
<span class="fa fa-check fa-fw"></span>
{% trans "Available" %}
{% if t.exclusive %}
<span data-toggle="tooltip" title="{% trans "When this type is available for an invoice address, no other type can be selected." %}">
{% trans "(exclusive)" %}
</span>
{% endif %}
</span>
{% else %}
<span class="text-muted">
@@ -2,7 +2,6 @@
{% load i18n %}
{% load urlreplace %}
{% load bootstrap3 %}
{% load static %}
{% block title %}{% trans "Events" %}{% endblock %}
{% block content %}
<h1>{% trans "Events" %}</h1>
@@ -75,7 +74,6 @@
<a href="?{% url_replace request 'ordering' 'organizer' %}"><i class="fa fa-caret-up"></i></a>
</th>
{% endif %}
<th>{% trans "Sales channels" %}</th>
<th>
{% trans "Start date" %}
<a href="?{% url_replace request 'ordering' '-date_from' %}"><i class="fa fa-caret-down"></i></a>
@@ -110,21 +108,6 @@
{% endfor %}
</td>
{% if not hide_orga %}<td>{{ e.organizer }}</td>{% endif %}
<td>
{% 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 %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% else %}
<span class="fa fa-fw"></span>
{% endif %}
{% endfor %}
</td>
<td class="event-date-col">
{% if e.has_subevents %}
<span class="fa fa-fw- fa-calendar"></span>
@@ -353,7 +353,7 @@
data-toggle="tooltip"
title="{% trans 'Generate a cancellation document for this invoice and create a new invoice with a new invoice number.' %}"
{% endif %}>
{% if order.status == "c" or not invoice_qualified %}
{% if order.status == "c" %}
{% trans "Generate cancellation" %}
{% else %}
{% trans "Cancel and reissue" %}
@@ -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>
@@ -22,7 +22,7 @@
{{ s.owner.fullname|default:s.owner.email }}
</span>
</div>
<div class="col-lg-4 col-md-5 col-xs-12">
<div class="col-lg-5 col-md-6 col-xs-12">
{% if s.schedule_next_run %}
<span class="fa fa-clock-o fa-fw"></span>
{% trans "Next run:" %}
@@ -53,7 +53,7 @@
{{ s.mail_subject }}
</span>
</div>
<div class="col-lg-3 col-md-3 col-xs-12 text-right">
<div class="col-lg-2 col-md-2 col-xs-12 text-right">
<form action="{% url "control:event.orders.export.do" event=request.event.slug organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
@@ -73,9 +73,6 @@
<a href="?identifier={{ s.export_identifier }}&scheduled={{ s.pk }}" class="btn btn-default" title="{% trans "Edit" %}" data-toggle="tooltip">
<span class="fa fa-edit"></span>
</a>
<a href="?identifier={{ s.export_identifier }}&scheduled_copy_from={{ s.pk }}" class="btn btn-default" title="{% trans "Copy" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
{% endif %}
<a href="{% url "control:event.orders.export.scheduled.delete" event=request.event.slug organizer=request.event.organizer.slug pk=s.pk %}" class="btn btn-danger" title="{% trans "Delete" %}" data-toggle="tooltip">
<span class="fa fa-trash"></span>
@@ -42,11 +42,7 @@
<div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask>
{% if scheduled_copy_from %}
{% trans "Save copy" %}
{% else %}
{% trans "Save" %}
{% endif %}
{% trans "Save" %}
</button>
</div>
{% else %}
@@ -1,7 +1,6 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load static %}
{% block inner %}
<h1>
{% blocktrans with name=request.organizer.name %}Organizer: {{ name }}{% endblocktrans %}
@@ -63,7 +62,6 @@
<thead>
<tr>
<th>{% trans "Event name" %}</th>
<th>{% trans "Sales channels" %}</th>
<th>
{% trans "Start date" %}
/
@@ -79,30 +77,10 @@
<td>
<strong><a
href="{% url "control:event.index" organizer=e.organizer.slug event=e.slug %}">{{ e.name }}</a></strong>
<br>
<small>
{{ e.slug }}
</small>
<small class="text-muted">
{% for k, v in e.meta_data.items %}
{% if v %}
&middot; {{ k }}: {{ v }}
{% endif %}
{% endfor %}
</small>
</td>
<td>
{% for c in sales_channels %}
{% if e.all_sales_channels or c in e.limit_sales_channels.all %}
{% if "." in c.icon %}
<img src="{% static c.icon %}" class="fa-like-image"
data-toggle="tooltip" title="{{ c.label }}">
{% else %}
<span class="fa fa-fw fa-{{ c.icon }} text-muted"
data-toggle="tooltip" title="{{ c.label }}"></span>
{% endif %}
{% else %}
<span class="fa fa-fw"></span>
<br><small>{{ e.slug }}</small>
{% for k, v in e.meta_data.items %}
{% if v %}
<small class="text-muted">&middot; {{ k }}: {{ v }}</small>
{% endif %}
{% endfor %}
</td>
@@ -132,7 +132,6 @@
<legend>{% trans "Customer accounts" %}</legend>
{% bootstrap_field sform.customer_accounts layout="control" %}
{% bootstrap_field sform.customer_accounts_native layout="control" %}
{% bootstrap_field sform.customer_accounts_require_login_for_order_access layout="control" %}
{% bootstrap_field sform.customer_accounts_link_by_email layout="control" %}
{% bootstrap_field sform.name_scheme layout="control" %}
{% bootstrap_field sform.name_scheme_titles layout="control" %}
@@ -22,7 +22,7 @@
{{ s.owner.fullname|default:s.owner.email }}
</span>
</div>
<div class="col-lg-4 col-md-5 col-xs-12">
<div class="col-lg-5 col-md-6 col-xs-12">
{% if s.schedule_next_run %}
<span class="fa fa-clock-o fa-fw"></span>
{% trans "Next run:" %}
@@ -53,7 +53,7 @@
{{ s.mail_subject }}
</span>
</div>
<div class="col-lg-3 col-md-3 col-xs-12 text-right">
<div class="col-lg-2 col-md-2 col-xs-12 text-right">
<form action="{% url "control:organizer.export.do" organizer=request.organizer.slug %}"
method="post" class="form-horizontal" data-asynctask data-asynctask-download
data-asynctask-long>
@@ -73,9 +73,6 @@
<a href="?identifier={{ s.export_identifier }}&scheduled={{ s.pk }}" class="btn btn-default" title="{% trans "Edit" %}" data-toggle="tooltip">
<span class="fa fa-edit"></span>
</a>
<a href="?identifier={{ s.export_identifier }}&scheduled_copy_from={{ s.pk }}" class="btn btn-default" title="{% trans "Copy" %}" data-toggle="tooltip">
<span class="fa fa-copy"></span>
</a>
{% endif %}
<a href="{% url "control:organizer.export.scheduled.delete" organizer=request.organizer.slug pk=s.pk %}" class="btn btn-danger" title="{% trans "Delete" %}" data-toggle="tooltip">
<span class="fa fa-trash"></span>
@@ -43,11 +43,7 @@
<div class="form-group submit-group">
<button formaction="{{ request.get_full_path }}" name="schedule" value="save" type="submit"
class="btn btn-primary btn-save" data-no-asynctask>
{% if scheduled_copy_from %}
{% trans "Save copy" %}
{% else %}
{% trans "Save" %}
{% endif %}
{% trans "Save" %}
</button>
</div>
{% else %}
@@ -65,9 +65,6 @@
{% blocktrans asvar title_reset %}Customer account password reset{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="reset" title=title_reset items="mail_subject_customer_reset,mail_text_customer_reset" %}
{% blocktrans asvar title_security_notice %}Customer account security notification{% endblocktrans %}
{% include "pretixcontrol/event/mail_settings_fragment.html" with pid="security_notice" title=title_security_notice items="mail_subject_customer_security_notice,mail_text_customer_security_notice" %}
</div>
</fieldset>
</div>
@@ -1,222 +0,0 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load urlreplace %}
{% load icon %}
{% load compress %}
{% load static %}
{% block inner %}
<h1>
{% trans "Outgoing email" %}
</h1>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Email details" %}</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-lg-7 col-md-12">
<dl class="dl-horizontal">
<dt>{% trans "From" context "email" %}</dt>
<dd>{{ sender }}</dd>
<dt>{% trans "To" context "email" %}</dt>
<dd>{{ mail.to|join:", " }}</dd>
{% if mail.cc %}
<dt>{% trans "Cc" context "email" %}</dt>
<dd>{{ mail.cc|join:", " }}</dd>
{% endif %}
{% if mail.bcc %}
<dt>{% trans "Bcc" context "email" %}</dt>
<dd>{{ mail.bcc|join:", " }}</dd>
{% endif %}
<dt>{% trans "Subject" %}</dt>
<dd>{{ mail.subject }}</dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% if mail.status == "queued" %}
<span class="label label-info">{% icon "clock-o" %} {% trans "queued" %}</span>
{% elif mail.status == "inflight" %}
<span class="label label-info">{% icon "send" %} {% trans "being sent" %}</span>
{% elif mail.status == "awaiting_retry" %}
<span class="label label-warning">{% icon "repeat" %} {% trans "will be retried" %}</span>
{% elif mail.status == "failed" %}
<span class="label label-danger">{% icon "warning" %} {% trans "failed" %}</span>
{% elif mail.status == "bounced" %}
<span class="label label-danger">{% icon "exclamation-circle" %} {% trans "bounced" %}</span>
{% elif mail.status == "withheld" %}
<span class="label label-warning">{% icon "ban" %} {% trans "withheld" %}</span>
{% elif mail.status == "aborted" %}
<span class="label label-danger">{% icon "ban" %} {% trans "aborted" %}</span>
{% elif mail.status == "sent" %}
<span class="label label-success">{% icon "check" %} {% trans "sent" %}</span>
{% endif %}
</dd>
<dt>{% trans "Creation" %}</dt>
<dd>{{ mail.created|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% if mail.sent %}
<dt>{% trans "Sent" %}</dt>
<dd>{{ mail.sent|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% endif %}
{% if mail.retry_after and mail.status == "awaiting_retry" %}
<dt>{% trans "Next attempt (estimate)" %}</dt>
<dd>{{ mail.retry_after|date:"SHORT_DATETIME_FORMAT" }}</dd>
{% endif %}
{% if mail.event %}
<dt>{% trans "Event" %}</dt>
<dd>
<a href="{% url "control:event.index" organizer=request.organizer.slug event=mail.event.slug %}">
{{ mail.event }}
</a>
</dd>
{% endif %}
{% if mail.order %}
<dt>{% trans "Order" %}</dt>
<dd>
<a href="{% url "control:event.order" organizer=request.organizer.slug event=mail.event.slug code=mail.order.code %}">
{{ mail.order.code }}</a>{% if mail.orderposition %}-
{{ mail.orderposition.positionid }}{% endif %}
</dd>
{% endif %}
{% if mail.customer %}
<dt>{% trans "Customer" %}</dt>
<dd>
{% icon "user fa-fw" %}
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=mail.customer.identifier %}">
{{ mail.customer }}
</a>
</dd>
{% endif %}
</dl>
</div>
{% if mail.actual_attachments %}
<div class="col-lg-5 col-md-12">
<strong>{% trans "Attachments" %}</strong><br>
<ul class="list-unstyled">
{% for a in mail.actual_attachments %}
<li>
{% if a.type == "text/calendar" %}
{% icon "calendar-plus-o fa-fw" %}
{% elif a.type == "application/pdf" %}
{% icon "file-pdf-o fa-fw" %}
{% elif "image/" in a.type %}
{% icon "file-image-o fa-fw" %}
{% elif "msword" in a.type or "document" in a.type %}
{% icon "file-word-o fa-fw" %}
{% elif "excel" in a.type or "spreadsheet" in a.type %}
{% icon "file-excel-o fa-fw" %}
{% elif "powerpoint" in a.type or "presentation" in a.type %}
{% icon "file-powerpoint-o fa-fw" %}
{% elif "pkpass" in a.type %}
{% icon "qrcode fa-fw" %}
{% else %}
{% icon "file-o fa-fw" %}
{% endif %}
{{ a.name }}
<span class="text-muted">
({{ a.size|filesizeformat }})
</span>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</div>
<div>
<ul class="nav nav-tabs" role="tablist">
{% if mail.is_failed %}
<li role="presentation" class="active">
<a href="#tab-error" role="tab" data-toggle="tab">
<span class="fa fa-warning"></span>
{% trans "Error" %}
</a>
</li>
{% endif %}
{% if mail.body_html %}
<li role="presentation"
{% if not mail.is_failed %}class="active"{% endif %}>
<a href="#tab-html" role="tab" data-toggle="tab">
<span class="fa fa-eye"></span>
{% trans "HTML content" %}
</a>
</li>
{% endif %}
<li role="presentation"
{% if not mail.is_failed and not mail.body_html %}class="active"{% endif %}>
<a href="#tab-text" role="tab" data-toggle="tab">
<span class="fa fa-file-text-o"></span>
{% trans "Text content" %}
</a>
</li>
<li role="presentation">
<a href="#tab-headers" role="tab" data-toggle="tab">
<span class="fa fa-code"></span>
{% trans "Headers" %}
</a>
</li>
</ul>
<div class="tab-content">
{% if mail.is_failed %}
<div role="tabpanel" class="tab-pane active" id="tab-error">
<strong>
{{ mail.error }}
</strong>
<pre>{{ mail.error_detail }}</pre>
</div>
{% endif %}
{% if mail.body_html %}
<div role="tabpanel"
class="tab-pane {% if not mail.is_failed %}active{% endif %}"
id="tab-html">
{% if mail.sensitive %}
<div class="empty-collection">
<p>
{% icon "eye-slash fa-4x" %}
</p>
<p>
{% blocktrans trimmed %}
Sensitive content not shown for security reasons
{% endblocktrans %}
</p>
</div>
{% else %}
{{ data_url|json_script:"mail_body_html" }}
{% endif %}
</div>
{% endif %}
<div role="tabpanel"
class="tab-pane {% if not mail.is_failed and not mail.body_html %}active{% endif %}"
id="tab-text">
{% if mail.sensitive %}
<div class="empty-collection">
<p>
{% icon "eye-slash fa-4x" %}
</p>
<p>
{% blocktrans trimmed %}
Sensitive content not shown for security reasons
{% endblocktrans %}
</p>
</div>
{% else %}
<pre><code>{{ mail.body_plain }}</code></pre>
{% endif %}
</div>
<div role="tabpanel"
class="tab-pane"
id="tab-headers">
<pre><code>{% for k, v in mail.headers.items %}{{ k }}: {{ v }}<br>{% endfor %}</code></pre>
<p class="text-muted">
{% trans "Additional headers will be added by the mail server and are not visible here." %}
</p>
</div>
</div>
</div>
{% compress js %}
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/outgoingmail.js" %}"></script>
{% endcompress %}
{% endblock %}
@@ -1,185 +0,0 @@
{% extends "pretixcontrol/organizers/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% load urlreplace %}
{% load icon %}
{% block inner %}
<h1>
{% trans "Outgoing emails" %}
</h1>
<p>
{% blocktrans trimmed with days=days %}
This is an overview of all emails sent by your organizer account in the last {{ days }} days.
{% endblocktrans %}
</p>
{% if mails|length == 0 and not filter_form.filtered %}
<div class="empty-collection">
<p>
{% blocktrans trimmed %}
You haven't sent any emails recently.
{% endblocktrans %}
</p>
</div>
{% else %}
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{% trans "Filter" %}</h3>
</div>
<form class="panel-body filter-form" action="" method="get">
<div class="row">
<div class="col-md-4 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.query %}
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.status %}
</div>
<div class="col-md-5 col-sm-6 col-xs-12">
{% bootstrap_field filter_form.event %}
</div>
</div>
<div class="text-right">
<button class="btn btn-primary btn-lg" type="submit">
<span class="fa fa-filter"></span>
{% trans "Filter" %}
</button>
</div>
</form>
</div>
<form action="{% url "control:organizer.outgoingmails.bulk_action" organizer=request.organizer.slug %}" method="post">
{% csrf_token %}
{% for field in filter_form %}
{{ field.as_hidden }}
{% endfor %}
<div class="table-responsive">
<table class="table table-condensed table-hover table-quotas">
<thead>
<tr>
<th>
<label aria-label="{% trans "select all rows for batch-operation" %}"
class="batch-select-label"><input type="checkbox" data-toggle-table/></label>
</th>
<th>{% trans "Subject" %}</th>
<th>{% trans "Recipients" %}</th>
<th>{% trans "Context" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Date" %}
<a href="?{% url_replace request 'ordering' '-date' %}"><i
class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'date' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th></th>
</tr>
{% if page_obj.paginator.num_pages > 1 %}
<tr class="table-select-all warning hidden">
<td>
<input type="checkbox" name="__ALL" id="__all"
data-results-total="{{ page_obj.paginator.count }}">
</td>
<td colspan="7">
<label for="__all">
{% trans "Select all results on other pages as well" %}
</label>
</td>
</tr>
{% endif %}
</thead>
<tbody>
{% for m in mails %}
<tr>
<td>
<label aria-label="{% trans "select row for batch-operation" %}"
class="batch-select-label"><input type="checkbox" name="outgoingmail"
class="batch-select-checkbox"
value="{{ m.pk }}"/></label>
</td>
<td>
<a href="{% url "control:organizer.outgoingmail" organizer=request.organizer.slug mail=m.id %}">
{{ m.subject }}
</a>
{% if m.sensitive %}
<span class="text-muted">{% icon "eye-slash" %}</span>
{% endif %}
</td>
<td>
{{ m.to|join:", " }}
{% if m.cc %}
<br><small class="text-muted">{% trans "Cc" context "email" %}: {{ m.cc|join:", " }}</small>
{% endif %}
{% if m.bcc %}
<br><small class="text-muted">{% trans "Bcc" context "email" %}: {{ m.bcc|join:", " }}</small>
{% endif %}
</td>
<td>
{% if m.event %}
<div>
{% icon "calendar fa-fw" %}
<a href="{% url "control:event.index" organizer=request.organizer.slug event=m.event.slug %}">
{{ m.event }}
</a>
</div>
{% endif %}
{% if m.order %}
<div>
{% icon "shopping-cart fa-fw" %}
<a href="{% url "control:event.order" organizer=request.organizer.slug event=m.event.slug code=m.order.code %}">
{{ m.order.code }}</a>{% if m.orderposition %}-{{ m.orderposition.positionid }}{% endif %}
</div>
{% endif %}
{% if m.customer %}
<div>
{% icon "user fa-fw" %}
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=m.customer.identifier %}">
{{ m.customer }}
</a>
</div>
{% endif %}
</td>
<td>
{% if m.status == "queued" %}
<span class="label label-info">{% icon "clock-o" %} {% trans "queued" %}</span>
{% elif m.status == "inflight" %}
<span class="label label-info">{% icon "send" %} {% trans "being sent" %}</span>
{% elif m.status == "awaiting_retry" %}
<span class="label label-warning">{% icon "repeat" %} {% trans "will be retried" %}</span>
{% elif m.status == "failed" %}
<span class="label label-danger">{% icon "warning" %} {% trans "failed" %}</span>
{% elif m.status == "bounced" %}
<span class="label label-danger">{% icon "exclamation-circle" %} {% trans "bounced" %}</span>
{% elif m.status == "withheld" %}
<span class="label label-warning">{% icon "ban" %} {% trans "withheld" %}</span>
{% elif m.status == "aborted" %}
<span class="label label-danger">{% icon "ban" %} {% trans "aborted" %}</span>
{% elif m.status == "sent" %}
<span class="label label-success">{% icon "check" %} {% trans "sent" %}</span>
{% endif %}
</td>
<td>
{{ m.created|date:"SHORT_DATETIME_FORMAT" }}
{% if m.sent %}
<br>
<small class="text-muted">{% trans "Sent:" %} {{ m.sent|date:"SHORT_DATETIME_FORMAT" }}</small>
{% endif %}
</td>
<td class="text-right flip">
<a href="{% url "control:organizer.outgoingmail" organizer=request.organizer.slug mail=m.id %}"
class="btn btn-default btn-sm">{% icon "eye" %}</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="batch-select-actions">
<button type="submit" class="btn btn-primary btn-save" name="action" value="retry">
{% icon "repeat" %}
{% trans "Retry (if failed or withheld)" %}
</button>
<button type="submit" class="btn btn-danger btn-save" name="action" value="abort">
{% icon "ban" %}
{% trans "Abort (if queued, awaiting retry or withheld)" %}
</button>
</div>
</form>
{% include "pretixcontrol/pagination.html" %}
{% endif %}
{% endblock %}
@@ -264,17 +264,12 @@
The paper size will match the PDF.
{% endblocktrans %}
</p>
<p class="text-center">
<p>
<span class="btn btn-default fileinput-button background-button btn-block">
<i class="fa fa-upload"></i>
<span>{% trans "Upload PDF as background" %}</span>
<input id="fileupload" type="file" name="background" accept="application/pdf">
</span>
<small class="text-muted">
{% blocktrans trimmed with size=maxfilesize|filesizeformat %}
max. {{ size }}, smaller is better
{% endblocktrans %}
</small>
</p>
<p class="text-center">
<a class="btn btn-link background-download-button" href="{{ pdf }}" target="_blank">
@@ -144,23 +144,14 @@
</div>
<div class="panel-body">
<p>
{% blocktrans trimmed %}
If you lose access to your devices, you can use one of your emergency tokens to log in.
We recommend to store them in a safe place, e.g. printed out or in a password manager.
Every token can be used at most once.
{% endblocktrans %}
{% trans "If you lose access to your devices, you can use one of the following keys to log in. We recommend to store them in a safe place, e.g. printed out or in a password manager. Every token can be used at most once." %}
</p>
{% if static_tokens_device %}
<p>
{% blocktrans trimmed with generation_date_time=static_tokens_device.created_at %}
You generated your emergency tokens on {{ generation_date_time }}.
{% endblocktrans %}
</p>
{% else %}
<p>
{% trans "You don't have any emergency tokens yet." %}
</p>
{% endif %}
<p>{% trans "Unused tokens:" %}</p>
<ul>
{% for t in static_tokens %}
<li><code>{{ t.token }}</code></li>
{% endfor %}
</ul>
<a href="{% url "control:user.settings.2fa.regenemergency" %}" class="btn btn-default">
<span class="fa fa-refresh"></span>
{% trans "Generate new emergency tokens" %}
@@ -1,33 +0,0 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Edit entry" %}{% endblock %}
{% block content %}
<h1>{% trans "Edit entry" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
{% if form.subevent %}
{% bootstrap_field form.subevent layout="control" %}
{% endif %}
{% bootstrap_field form.email layout="control" %}
{% if form.name_parts %}
{% bootstrap_field form.name_parts layout="control" %}
{% endif %}
{% if form.phone %}
{% bootstrap_field form.phone layout="control" %}
{% endif %}
{% bootstrap_field form.itemvar layout="control" %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Save" %}
</button>
</div>
</form>
{% endblock %}
@@ -124,7 +124,6 @@
</option>
{% endfor %}
</select>
<input name="search" type="text" placeholder="{% trans "Search" %}" class="form-control" value="{{ request.GET.search }}">
{% if request.event.has_subevents %}
<select name="subevent" class="form-control">
<option value="">{% trans "All dates" context "subevent" %}</option>
@@ -268,13 +267,13 @@
data-toggle="tooltip" title="{% trans "Move to the end of the list" %}">
<span class="fa fa-thumbs-down"></span>
</button>
<a href="{% url "control:event.orders.waitinglist.edit" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
class="btn btn-default btn-sm" title="{% trans "Edit entry" %}"
data-toggle="tooltip">
<i class="fa fa-edit" aria-hidden="true"></i>
</a>
{% if request.event.has_subevents %}
<a href="{% url "control:event.orders.waitinglist.transfer" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}"
class="btn btn-default btn-sm" title="{% trans "Transfer to other date" context "subevent" %}"
data-toggle="tooltip">
<i class="fa fa-calendar" aria-hidden="true"></i>
</a>
{% endif %}
<a href="{% url "control:event.orders.waitinglist.delete" organizer=request.event.organizer.slug event=request.event.slug entry=e.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
{% else %}
<button class="btn btn-default btn-sm disabled">
@@ -0,0 +1,23 @@
{% extends "pretixcontrol/event/base.html" %}
{% load i18n %}
{% load bootstrap3 %}
{% block title %}{% trans "Transfer entry" %}{% endblock %}
{% block content %}
<h1>{% trans "Transfer entry" %}</h1>
<form action="" method="post" class="form-horizontal">
{% csrf_token %}
<p>{% blocktrans trimmed context "subevent" %}
Please select the date to which the following waiting list entry should be
transferred: <strong>{{ entry }}</strong>?
{% endblocktrans %}</p>
{% bootstrap_field form.subevent layout="control" %}
<div class="form-group submit-group">
<a href="{% url "control:event.orders.waitinglist" organizer=request.event.organizer.slug event=request.event.slug %}" class="btn btn-default btn-cancel">
{% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-primary btn-save">
{% trans "Transfer" %}
</button>
</div>
</form>
{% endblock %}
+4 -8
View File
@@ -38,9 +38,8 @@ from django.views.generic.base import RedirectView
from pretix.control.views import (
auth, checkin, dashboards, datasync, discounts, event, geo,
global_settings, item, mail, main, modelimport, oauth, orders, organizer,
pdf, search, shredder, subevents, typeahead, user, users, vouchers,
waitinglist,
global_settings, item, main, modelimport, oauth, orders, organizer, pdf,
search, shredder, subevents, typeahead, user, users, vouchers, waitinglist,
)
urlpatterns = [
@@ -241,9 +240,6 @@ urlpatterns = [
name='organizer.gate.edit'),
re_path(r'^organizer/(?P<organizer>[^/]+)/gate/(?P<gate>[^/]+)/delete$', organizer.GateDeleteView.as_view(),
name='organizer.gate.delete'),
re_path(r'^organizer/(?P<organizer>[^/]+)/outgoingmails$', mail.OutgoingMailListView.as_view(), name='organizer.outgoingmails'),
re_path(r'^organizer/(?P<organizer>[^/]+)/outgoingmail/bulk_action$', mail.OutgoingMailBulkAction.as_view(), name='organizer.outgoingmails.bulk_action'),
re_path(r'^organizer/(?P<organizer>[^/]+)/outgoingmail/(?P<mail>[0-9]+)/$', mail.OutgoingMailDetailView.as_view(), name='organizer.outgoingmail'),
re_path(r'^organizer/(?P<organizer>[^/]+)/teams$', organizer.TeamListView.as_view(), name='organizer.teams'),
re_path(r'^organizer/(?P<organizer>[^/]+)/team/add$', organizer.TeamCreateView.as_view(), name='organizer.team.add'),
re_path(r'^organizer/(?P<organizer>[^/]+)/team/(?P<team>[^/]+)/$', organizer.TeamMemberView.as_view(),
@@ -480,8 +476,8 @@ urlpatterns = [
re_path(r'^waitinglist/auto_assign$', waitinglist.AutoAssign.as_view(), name='event.orders.waitinglist.auto'),
re_path(r'^waitinglist/(?P<entry>\d+)/delete$', waitinglist.EntryDelete.as_view(),
name='event.orders.waitinglist.delete'),
re_path(r'^waitinglist/(?P<entry>\d+)/edit$', waitinglist.EntryEdit.as_view(),
name='event.orders.waitinglist.edit'),
re_path(r'^waitinglist/(?P<entry>\d+)/transfer$', waitinglist.EntryTransfer.as_view(),
name='event.orders.waitinglist.transfer'),
re_path(r'^checkins/$', checkin.CheckinListView.as_view(), name='event.orders.checkins'),
re_path(r'^checkinlists/$', checkin.CheckinListList.as_view(), name='event.orders.checkinlists'),
re_path(r'^checkinlists/add$', checkin.CheckinListCreate.as_view(), name='event.orders.checkinlists.add'),

Some files were not shown because too many files have changed in this diff Show More