Compare commits

..

53 Commits

Author SHA1 Message Date
Martin Gross
0fa2da5d30 Rebase against origin/master 2026-02-25 13:24:31 +01:00
Maximilian Richt
710cd2c97c Add copy and qr button to reusable medium detail view 2026-02-25 13:01:22 +01:00
Maximilian Richt
a979381ab0 Fix sorting of reusable media type in overview 2026-02-25 13:01:22 +01:00
Richard Schreiber
b9290a358e improve check 2026-02-25 13:01:22 +01:00
Richard Schreiber
71c01a3302 fix flake8 2026-02-25 13:01:22 +01:00
Richard Schreiber
b34c30cf6b fix multi-op media filter 2026-02-25 13:01:22 +01:00
Richard Schreiber
d1d249ae06 fix indentation 2026-02-25 13:01:22 +01:00
Richard Schreiber
6f01be0baf Add help-text 2026-02-25 13:01:22 +01:00
Richard Schreiber
690b357b17 add test 2026-02-25 13:01:22 +01:00
Richard Schreiber
1d48959a8b fix linked_orderpositions filter in checkinrpc 2026-02-25 13:01:22 +01:00
Richard Schreiber
1815dacfed Add test for order-API add_to_reusable_medium 2026-02-25 13:01:22 +01:00
Richard Schreiber
d7c1a8af6c list ops comma-separated in export 2026-02-25 13:01:22 +01:00
Richard Schreiber
0b9a5008c1 Clarify docs 2026-02-25 13:01:22 +01:00
Richard Schreiber
25f71422d5 fix flake8 2026-02-25 13:01:22 +01:00
Richard Schreiber
7a3d1f2b14 fix tests regarding claim_token 2026-02-25 13:01:22 +01:00
Richard Schreiber
fe9942eb4c add add_to_reusable_medium to order-serializer 2026-02-25 13:01:22 +01:00
Richard Schreiber
7d3183f67b fix flake8 2026-02-25 13:01:22 +01:00
Richard Schreiber
d13fe331f3 fix missing claim_token in serializer 2026-02-25 13:01:22 +01:00
Richard Schreiber
dcc050449d fix disallow of use of singular and plural linked_op in API 2026-02-25 13:01:22 +01:00
Richard Schreiber
a9183e846c Update reusablemedia.rst 2026-02-25 13:01:22 +01:00
Richard Schreiber
2a408ac3a6 Update reusablemedia.rst 2026-02-25 13:01:22 +01:00
Richard Schreiber
27a5d31098 Update reusablemedia.rst 2026-02-25 13:01:22 +01:00
Richard Schreiber
cac855e2b5 unifiy deprecated style 2026-02-25 13:01:22 +01:00
Richard Schreiber
f157a9bb10 Update docs for claim_token 2026-02-25 13:01:22 +01:00
Richard Schreiber
5f1c5db7fe rename secret to claim_token 2026-02-25 13:01:22 +01:00
Richard Schreiber
510fc14216 add filter based on op.valid_from/until 2026-02-25 13:01:22 +01:00
Richard Schreiber
0552441e4a select_related order instead prefetch 2026-02-25 13:01:22 +01:00
Richard Schreiber
76ab775876 improve readability 2026-02-25 13:01:22 +01:00
Richard Schreiber
2b13fae184 no need to prefetch linked_orderpositions 2026-02-25 13:01:22 +01:00
Richard Schreiber
f758a1710d clarify docs 2026-02-25 13:01:22 +01:00
Richard Schreiber
e6ef7f847d clarify docs updating multiple linked_orderpositions 2026-02-25 13:01:22 +01:00
Richard Schreiber
af48890152 update docs 2026-02-25 13:01:22 +01:00
Richard Schreiber
91c996c19a fix tests 2026-02-25 13:01:22 +01:00
Richard Schreiber
6872ca99da fix migration NOT NULL 2026-02-25 13:01:22 +01:00
Richard Schreiber
ab49d09c18 add label to reusablemedium 2026-02-25 13:01:22 +01:00
Richard Schreiber
dd247c47f8 fix code style 2026-02-25 13:01:22 +01:00
Richard Schreiber
e4a529c22f fix more tests 2026-02-25 13:01:22 +01:00
Richard Schreiber
b76f1dfd69 fix tests 2026-02-25 13:01:22 +01:00
Richard Schreiber
040d9815a5 fix create/update logging 2026-02-25 13:01:22 +01:00
Richard Schreiber
0bc61f673f remove unneeded comment 2026-02-25 13:01:22 +01:00
Richard Schreiber
23d630791e adapt checkin API for multiple orderpositions 2026-02-25 13:01:22 +01:00
Richard Schreiber
3a2f42f34d fix media-issue signal 2026-02-25 13:01:22 +01:00
Richard Schreiber
d23855a1c4 remove cached_property linked_orderposition - keep only in API 2026-02-25 13:01:22 +01:00
Richard Schreiber
d6ed21c776 fix API orders matching media 2026-02-25 13:01:22 +01:00
Richard Schreiber
3e5d368d6e fix API orders 2026-02-25 13:01:22 +01:00
Richard Schreiber
312b243add update control media forms 2026-02-25 13:01:22 +01:00
Richard Schreiber
cf880272f1 fix media-view filter 2026-02-25 13:01:22 +01:00
Richard Schreiber
9e01914b0f fix last media-API commit 2026-02-25 13:01:22 +01:00
Richard Schreiber
245fb48989 update media-API 2026-02-25 13:01:22 +01:00
Richard Schreiber
da48ec0f45 add multi-op to export 2026-02-25 13:01:22 +01:00
Richard Schreiber
5c0505c9c3 return last op as fallback for linked_orderposition 2026-02-25 13:01:22 +01:00
Richard Schreiber
f5da1e00fa Update media views to list ops 2026-02-25 13:01:22 +01:00
Richard Schreiber
8aa75cb256 change linked orderpositions to many-to-many 2026-02-25 13:01:21 +01:00
49 changed files with 3541 additions and 5326 deletions

View File

@@ -192,7 +192,7 @@ Cart position endpoints
* ``attendee_email`` (optional)
* ``subevent`` (optional)
* ``expires`` (optional)
* ``includes_tax`` (optional, **deprecated**, do not use, will be removed)
* ``includes_tax`` (optional, **DEPRECATED**, do not use, will be removed)
* ``sales_channel`` (optional)
* ``voucher`` (optional, expect a voucher code)
* ``addons`` (optional, expect a list of nested objects of cart positions)

View File

@@ -1066,6 +1066,7 @@ Creating orders
* ``valid_until`` (optional, if both ``valid_from`` and ``valid_until`` are **missing** (not ``null``) the availability will be computed from the given product)
* ``requested_valid_from`` (optional, can be set **instead** of ``valid_from`` and ``valid_until`` to signal a user choice for the start time that may or may not be respected)
* ``use_reusable_medium`` (optional, causes the new ticket to take over the given reusable medium, identified by its ID)
* ``add_to_reusable_medium`` (optional, causes the new ticket to be added to the given reusable medium, identified by its ID)
* ``discount`` (optional, only possible if ``price`` is set; attention: if this is set to not-``null`` on any position, automatic calculation of discounts will not run)
* ``answers``

View File

@@ -21,12 +21,16 @@ id integer Internal ID of
type string Type of medium, e.g. ``"barcode"``, ``"nfc_uid"`` or ``"nfc_mf0aes"``.
organizer string Organizer slug of the organizer who "owns" this medium.
identifier string Unique identifier of the medium. The format depends on the ``type``.
claim_token string Secret token to claim ownership of the medium (or ``null``)
label string Label to identify the medium, usually something human readable (or ``null``)
active boolean Whether this medium may be used.
created datetime Date of creation
updated datetime Date of last modification
expires datetime Expiry date (or ``null``)
customer string Identifier of a customer account this medium belongs to.
linked_orderposition integer Internal ID of a ticket this medium is linked to.
linked_orderpositions list of integer Internal IDs of tickets this medium is linked to.
linked_orderposition integer **DEPRECATED.** ID of the ticket the medium is linked to, if it is linked to
only one ticket. ``null``, if the medium is linked to none or multiple tickets.
linked_giftcard integer Internal ID of a gift card this medium is linked to.
info object Additional data, content depends on the ``type``. Consider
this internal to the system and don't use it for your own data.
@@ -39,6 +43,14 @@ Existing media types are:
- ``nfc_uid``
- ``nfc_mf0aes``
.. versionchanged:: 2025.11
The ``claim_token``, ``label``, ``linked_orderpositions`` attributes have been added, the ``linked_orderposition`` attribute has been
deprecated. Note: To maintain backwards compatibility ``linked_orderposition`` contains the internal ID of the linked order position
if the medium has exactly one order position in ``linked_orderpositions``.
Endpoints
---------
@@ -77,6 +89,7 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -92,10 +105,13 @@ Endpoints
:query string customer: Only show media linked to the given customer.
:query string created_since: Only show media created since a given date.
:query string updated_since: Only show media updated since a given date.
:query integer linked_orderpositions: Only show media linked to the given tickets. Note: you can pass multiple ticket IDs by passing
``linked_orderpositions`` multiple times. Any medium matching any linked orderposition will be returned.
:query integer linked_orderposition: Only show media linked to the given ticket.
:query integer linked_giftcard: Only show media linked to the given gift card.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderposition"``,
or ``"customer"``, the respective field will be shown as a nested value instead of just an ID.
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_giftcard.owner_ticket"``, ``"linked_orderpositions"``,
``"linked_orderposition"`` (**DEPRECATED**), or ``"customer"``, the respective field will be shown
as a nested value instead of just an ID.
The nested objects are identical to the respective resources, except that order positions
will have an attribute of the format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make
matching easier. The parameter can be given multiple times.
@@ -134,6 +150,7 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -191,6 +208,7 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -198,9 +216,9 @@ Endpoints
}
:param organizer: The ``slug`` field of the organizer to look up a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
the respective resources, except that the ``linked_orderpositions`` each will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
can be given multiple times.
:statuscode 201: no error
@@ -227,6 +245,7 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -251,6 +270,7 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderpositions": [],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
@@ -258,7 +278,7 @@ Endpoints
}
:param organizer: The ``slug`` field of the organizer to create a medium for
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter
@@ -287,7 +307,7 @@ Endpoints
Content-Length: 94
{
"linked_orderposition": 13
"linked_orderpositions": [13, 29]
}
**Example response**:
@@ -308,7 +328,8 @@ Endpoints
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": 13,
"linked_orderpositions": [13, 29],
"linked_orderposition": None,
"linked_giftcard": None,
"notes": None,
"info": {}
@@ -316,7 +337,7 @@ Endpoints
:param organizer: The ``slug`` field of the organizer to modify
:param id: The ``id`` field of the medium to modify
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"customer"``, the respective
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
field will be shown as a nested value instead of just an ID. The nested objects are identical to
the respective resources, except that the ``linked_orderposition`` will have an attribute of the
format ``"order": {"code": "ABCDE", "event": "eventslug"}`` to make matching easier. The parameter

View File

@@ -65,9 +65,11 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
expand_nested = self.context['request'].query_params.getlist('expand')
if 'linked_giftcard' in expand_nested:
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
if 'linked_giftcard.owner_ticket' in expand_nested:
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
else:
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
@@ -76,16 +78,27 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
queryset=self.context['organizer'].issued_gift_cards.all()
)
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
# keep linked_orderposition (singular) for backwards compatibility, will be overwritten in self.validate
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
required=False,
allow_null=True,
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
)
if 'linked_orderposition' in expand_nested or 'linked_orderpositions' in expand_nested:
self.fields['linked_orderpositions'] = NestedOrderPositionSerializer(
many=True,
read_only=True
)
else:
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
self.fields['linked_orderpositions'] = serializers.PrimaryKeyRelatedField(
many=True,
required=False,
allow_null=True,
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
)
if 'customer' in self.context['request'].query_params.getlist('expand'):
if 'customer' in expand_nested:
self.fields['customer'] = CustomerSerializer(read_only=True)
else:
self.fields['customer'] = serializers.SlugRelatedField(
@@ -97,6 +110,20 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
def validate(self, data):
data = super().validate(data)
if 'linked_orderposition' in data:
linked_orderposition = data['linked_orderposition']
# backwards-compatibility
if 'linked_orderpositions' in data:
raise ValidationError({
'linked_orderposition': _('You cannot use linked_orderposition and linked_orderpositions at the same time.')
})
if self.instance.linked_orderpositions.count() > 1:
raise ValidationError({
'linked_orderposition': _('There are more than one linked_orderposition. You need to use linked_orderpositions.')
})
data['linked_orderpositions'] = [linked_orderposition] if linked_orderposition else []
if 'type' in data and 'identifier' in data:
qs = self.context['organizer'].reusable_media.filter(
identifier=data['identifier'], type=data['type']
@@ -109,6 +136,14 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
)
return data
def to_representation(self, obj):
r = super(ReusableMediaSerializer, self).to_representation(obj)
ops = r.get('linked_orderpositions')
if len(ops) < 2:
# add linked_orderposition (singular) for backwards compatibility
r['linked_orderposition'] = ops[0] if ops else None
return r
class Meta:
model = ReusableMedium
fields = (
@@ -118,10 +153,12 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
'updated',
'type',
'identifier',
'claim_token',
'label',
'active',
'expires',
'customer',
'linked_orderposition',
'linked_orderpositions',
'linked_giftcard',
'info',
'notes',

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
@@ -1030,13 +1029,15 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
requested_valid_from = serializers.DateTimeField(required=False, allow_null=True)
use_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
add_to_reusable_medium = serializers.PrimaryKeyRelatedField(queryset=ReusableMedium.objects.none(),
required=False, allow_null=True)
class Meta:
model = OrderPosition
fields = ('positionid', 'item', 'variation', 'price', 'attendee_name', 'attendee_name_parts', 'attendee_email',
'company', 'street', 'zipcode', 'city', 'country', 'state', 'is_bundled',
'secret', 'addon_to', 'subevent', 'answers', 'seat', 'voucher', 'valid_from', 'valid_until',
'requested_valid_from', 'use_reusable_medium', 'discount')
'requested_valid_from', 'use_reusable_medium', 'add_to_reusable_medium', 'discount')
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -1048,6 +1049,8 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
with scopes_disabled():
if 'use_reusable_medium' in self.fields:
self.fields['use_reusable_medium'].queryset = ReusableMedium.objects.all()
if 'add_to_reusable_medium' in self.fields:
self.fields['add_to_reusable_medium'].queryset = ReusableMedium.objects.all()
def validate_secret(self, secret):
if secret and OrderPosition.all.filter(order__event=self.context['event'], secret=secret).exists():
@@ -1063,6 +1066,9 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
)
return m
def validate_add_to_reusable_medium(self, m):
return self.validate_use_reusable_medium(m)
def validate_item(self, item):
if item.event != self.context['event']:
raise ValidationError(
@@ -1136,6 +1142,13 @@ class OrderPositionCreateSerializer(I18nAwareModelSerializer):
raise ValidationError(
{'discount': ['You can only specify a discount if you do the price computation, but price is not set.']}
)
if 'use_reusable_medium' in data and 'add_to_reusable_medium' in data:
raise ValidationError({
'use_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
'add_to_reusable_medium': ['You can only specify either use_reusable_medium or add_to_reusable_medium.'],
})
return data
@@ -1216,18 +1229,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.')
@@ -1563,7 +1564,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
pos_data['attendee_name_parts'] = {
'_legacy': attendee_name
}
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k not in ('answers', '_quotas', 'use_reusable_medium', 'add_to_reusable_medium')})
if simulate:
pos.order = order._wrapped
else:
@@ -1637,6 +1638,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
for pos_data in positions_data:
answers_data = pos_data.pop('answers', [])
use_reusable_medium = pos_data.pop('use_reusable_medium', None)
add_to_reusable_medium = pos_data.pop('add_to_reusable_medium', None)
pos = pos_data['__instance']
pos._calculate_tax(invoice_address=ia)
@@ -1678,8 +1680,7 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
answ.options.add(*options)
if use_reusable_medium:
use_reusable_medium.linked_orderposition = pos
use_reusable_medium.save(update_fields=['linked_orderposition'])
use_reusable_medium.linked_orderpositions.set([pos])
use_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.changed',
data={
@@ -1687,6 +1688,15 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
'linked_orderposition': pos.pk,
}
)
if add_to_reusable_medium:
add_to_reusable_medium.linked_orderpositions.add(pos)
add_to_reusable_medium.log_action(
'pretix.reusable_medium.linked_orderposition.added',
data={
'by_order': order.code,
'linked_orderposition': pos.pk,
}
)
if not simulate:
for cp in delete_cps:

View File

@@ -365,10 +365,9 @@ class TeamInviteSerializer(serializers.ModelSerializer):
def _send_invite(self, instance):
mail(
instance.email,
_('Account invitation'),
_('pretix account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.context['organizer'].name,
'team': instance.team.name,

View File

@@ -520,11 +520,13 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
# with respecting the force option), or it's a reusable medium (-> proceed with that)
if not op_candidates:
try:
media = ReusableMedium.objects.select_related('linked_orderposition').active().get(
media = ReusableMedium.objects.active().annotate(
has_linked_orderpositions=Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk')))
).get(
organizer_id=checkinlists[0].event.organizer_id,
type=source_type,
identifier=raw_barcode,
linked_orderposition__isnull=False,
has_linked_orderpositions=True,
)
raw_barcode_for_checkin = raw_barcode
except ReusableMedium.DoesNotExist:
@@ -627,7 +629,8 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
}, status=400)
else:
if media.linked_orderposition.order.event_id not in list_by_event:
linked_event_ids = media.linked_orderpositions.values_list("order__event_id", flat=True).order_by().distinct()
if not any(event_id in list_by_event for event_id in linked_event_ids):
# Medium exists but connected ticket is for the wrong event
if not simulate:
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
@@ -653,21 +656,34 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
'checkin_texts': [],
'list': MiniCheckinListSerializer(checkinlists[0]).data,
}, status=404)
op_candidates = [media.linked_orderposition]
if list_by_event[media.linked_orderposition.order.event_id].addon_match:
op_candidates += list(media.linked_orderposition.addons.all())
op_candidates = []
for op in media.linked_orderpositions.all().select_related("order"):
op_candidates.append(op)
if list_by_event[op.order.event_id].addon_match:
op_candidates += list(op.addons.all())
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
# which add-on has the right product.
if len(op_candidates) > 1:
op_candidates_matching_product = [
op for op in op_candidates
if (
(list_by_event[op.order.event_id].addon_match or op.secret == raw_barcode or legacy_url_support) and
(list_by_event[op.order.event_id].all_products or op.item_id in {i.pk for i in list_by_event[op.order.event_id].limit_products.all()})
)
]
# only check addons if at most one non-addon-op is in op_candidates
# otherwise it is likely a medium linked to multiple orderpositions, which we need to filter based on validity
if len([op for op in op_candidates if not op.addon_to]) <= 1:
op_candidates_matching_product = [
op for op in op_candidates
if (
(list_by_event[op.order.event_id].addon_match or op.secret == raw_barcode or legacy_url_support) and
(list_by_event[op.order.event_id].all_products or op.item_id in {i.pk for i in list_by_event[op.order.event_id].limit_products.all()})
)
]
else:
op_candidates_matching_product = [
op for op in op_candidates
if (
(not op.valid_from or op.valid_from < now()) and
(not op.valid_until or op.valid_until > now())
)
]
if len(op_candidates_matching_product) == 0:
# None of the found add-ons has the correct product, too bad! We could just error out here, but

View File

@@ -53,10 +53,12 @@ with scopes_disabled():
customer = django_filters.CharFilter(field_name='customer__identifier')
updated_since = django_filters.IsoDateTimeFilter(field_name='updated', lookup_expr='gte')
created_since = django_filters.IsoDateTimeFilter(field_name='created', lookup_expr='gte')
# backwards-compatible
linked_orderposition = django_filters.NumberFilter(field_name='linked_orderpositions__id')
class Meta:
model = ReusableMedium
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderposition', 'linked_giftcard']
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderpositions', 'linked_giftcard']
class ReusableMediaViewSet(viewsets.ModelViewSet):
@@ -75,7 +77,7 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
).order_by().values('card').annotate(s=Sum('value')).values('s')
return self.request.organizer.reusable_media.prefetch_related(
Prefetch(
'linked_orderposition',
'linked_orderpositions',
queryset=OrderPosition.objects.select_related(
'order', 'order__event', 'order__event__organizer', 'seat',
).prefetch_related(
@@ -155,7 +157,6 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
type=s.validated_data["type"],
identifier=s.validated_data["identifier"],
)
m.linked_orderposition = None # not relevant for cross-organizer
m.customer = None # not relevant for cross-organizer
s = self.get_serializer(m)
return Response({"result": s.data})

View File

@@ -193,7 +193,7 @@ with scopes_disabled():
)
).values('id')
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderposition__order_id', flat=True)
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderpositions__order_id', flat=True)
mainq = (
code
@@ -1030,7 +1030,7 @@ with scopes_disabled():
search = django_filters.CharFilter(method='search_qs')
def search_qs(self, queryset, name, value):
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderposition', flat=True)
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderpositions', flat=True)
return queryset.filter(
Q(secret__istartswith=value)
| Q(attendee_name_cached__icontains=value)

View File

@@ -20,12 +20,13 @@
# <https://www.gnu.org/licenses/>.
#
from django.db.models import Prefetch
from django.dispatch import receiver
from django.utils.formats import date_format
from django.utils.translation import gettext_lazy as _, pgettext, pgettext_lazy
from ..exporter import ListExporter, OrganizerLevelExportMixin
from ..models import ReusableMedium
from ..models import OrderPosition, ReusableMedium
from ..signals import register_multievent_data_exporters
@@ -40,7 +41,9 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
media = ReusableMedium.objects.filter(
organizer=self.organizer,
).select_related(
'customer', 'linked_orderposition', 'linked_giftcard',
'customer', 'linked_giftcard',
).prefetch_related(
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order"))
).order_by('created')
headers = [
@@ -58,17 +61,16 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
yield self.ProgressSetTotal(total=media.count())
for medium in media.iterator(chunk_size=1000):
row = [
yield [
medium.type,
medium.identifier,
_('Yes') if medium.active else _('No'),
date_format(medium.expires, 'SHORT_DATETIME_FORMAT') if medium.expires else '',
medium.customer.identifier if medium.customer_id else '',
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
', '.join([f"{op.order.code}-{op.positionid}" for op in medium.linked_orderpositions.all()]),
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
medium.notes,
]
yield row
def get_filename(self):
return f'{self.organizer.slug}_media'

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(

View File

@@ -0,0 +1,35 @@
# Generated by Django 4.2.26 on 2025-11-24 11:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0297_outgoingmail"),
]
operations = [
migrations.AddField(
model_name="reusablemedium",
name="claim_token",
field=models.CharField(max_length=200, null=True),
),
migrations.AddField(
model_name="reusablemedium",
name="label",
field=models.CharField(max_length=200, null=True),
),
# use temporary related_name "linked_mediums" for ManyToManyField, so we can migrate existing data
migrations.AddField(
model_name="reusablemedium",
name="linked_orderpositions",
field=models.ManyToManyField(
related_name="linked_mediums", to="pretixbase.orderposition"
),
),
migrations.RunSQL(
sql="INSERT INTO pretixbase_reusablemedium_linked_orderpositions (reusablemedium_id, orderposition_id) SELECT id, linked_orderposition_id FROM pretixbase_reusablemedium WHERE linked_orderposition_id IS NOT NULL;",
reverse_sql="DELETE FROM pretixbase_reusablemedium_linked_orderpositions;",
),
]

View File

@@ -0,0 +1,44 @@
# Generated by Django 4.2.26 on 2025-11-24 11:32
from django.db import migrations, models
def reverse(apps, schema_editor):
ReusableMedium = apps.get_model('pretixbase', 'ReusableMedium')
qs = ReusableMedium.linked_orderpositions.through.objects
objs = []
# get last added orderposition from linked_orderpositions
for rm_id, op_id in qs.filter(id__in=qs.values("reusablemedium_id").annotate(max_id=models.Max('id')).values('max_id')).values_list("reusablemedium_id", "orderposition_id"):
obj = ReusableMedium(
id=rm_id,
linked_orderposition_id=op_id,
)
objs.append(obj)
ReusableMedium.objects.bulk_update(objs, ['linked_orderposition_id'])
class Migration(migrations.Migration):
dependencies = [
("pretixbase", "0298_add_reusablemedium_label"),
]
operations = [
# according to the docs, UPDATE FROM should run similarly on sqlite and postgres, but I could not get it to work
# so roll back the data migration with code before deleting data from through-table in 0297
migrations.RunPython(migrations.RunPython.noop, reverse),
migrations.RemoveField(
model_name="reusablemedium",
name="linked_orderposition",
),
# change related_name for new ManyToManyField to previously used linked_media
migrations.AlterField(
model_name="reusablemedium",
name="linked_orderpositions",
field=models.ManyToManyField(
related_name="linked_media", to="pretixbase.orderposition"
),
),
]

View File

@@ -346,8 +346,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
{
'user': self,
'messages': msg,
'url': build_absolute_uri('control:user.settings'),
'instance': settings.PRETIX_INSTANCE_NAME,
'url': build_absolute_uri('control:user.settings')
},
event=None,
user=self,
@@ -392,7 +391,6 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
'user': self,
'reason': msg,
'code': code,
'instance': settings.PRETIX_INSTANCE_NAME,
},
event=None,
user=self,
@@ -432,7 +430,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)))

View File

@@ -72,6 +72,16 @@ class ReusableMedium(LoggedModel):
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Identifier'),
)
claim_token = models.CharField(
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Claim token'),
null=True, blank=True
)
label = models.CharField(
max_length=200,
verbose_name=pgettext_lazy('reusable_medium', 'Label'),
null=True, blank=True
)
active = models.BooleanField(
verbose_name=_('Active'),
@@ -89,12 +99,14 @@ class ReusableMedium(LoggedModel):
on_delete=models.SET_NULL,
verbose_name=_('Customer account'),
)
linked_orderposition = models.ForeignKey(
linked_orderpositions = models.ManyToManyField(
OrderPosition,
null=True, blank=True,
related_name='linked_media',
on_delete=models.SET_NULL,
verbose_name=_('Linked ticket'),
verbose_name=_('Linked tickets'),
help_text=_(
'If you link to more than one ticket, make sure there is no overlap in validity. '
'If multiple tickets are valid at once, this will lead to failed check-ins.'
)
)
linked_giftcard = models.ForeignKey(
GiftCard,

View File

@@ -334,8 +334,7 @@ def _check_position_constraints(
raise CartPositionError(error_messages['voucher_invalid_subevent'])
# Voucher expired
# (checked using real_now_dt as vouchers influence quota calculations)
if voucher and voucher.valid_until and voucher.valid_until < real_now_dt:
if voucher and voucher.valid_until and voucher.valid_until < time_machine_now_dt:
raise CartPositionError(error_messages['voucher_expired'])
# Subevent has been disabled

View File

@@ -3495,8 +3495,8 @@ def signal_listener_issue_media(sender: Event, order: Order, **kwargs):
identifier=mt.generate_identifier(sender.organizer),
active=True,
customer=order.customer,
linked_orderposition=p,
)
rm.linked_orderpositions.add(p)
rm.log_action(
'pretix.reusable_medium.created',
data={

View File

@@ -176,7 +176,6 @@ def shred(self, event: Event, fileid: str, confirm_code: str, user: int=None, lo
_('Data shredding completed'),
'pretixbase/email/shred_completed.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': user,
'organizer': event.organizer.name,
'event': str(event.name),

View File

@@ -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 %}

View File

@@ -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]))

View File

@@ -1744,7 +1744,7 @@ class ReusableMediaFilterForm(FilterForm):
Q(identifier__icontains=query)
| Q(customer__identifier__icontains=query)
| Q(customer__external_identifier__istartswith=query)
| Q(linked_orderposition__order__code__icontains=query)
| Q(linked_orderpositions__order__code__icontains=query)
| Q(linked_giftcard__secret__icontains=query)
)

View File

@@ -83,7 +83,7 @@ from pretix.control.forms import ExtFileField, SplitDateTimeField
from pretix.control.forms.event import (
SafeEventMultipleChoiceField, multimail_validate,
)
from pretix.control.forms.widgets import Select2
from pretix.control.forms.widgets import Select2, Select2Multiple
from pretix.multidomain.models import KnownDomain
from pretix.multidomain.urlreverse import build_absolute_uri
@@ -246,6 +246,12 @@ class SafeOrderPositionChoiceField(forms.ModelChoiceField):
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
class SafeOrderPositionMultipleChoiceField(forms.ModelMultipleChoiceField):
def __init__(self, queryset, **kwargs):
queryset = queryset.model.all.none()
super().__init__(queryset, **kwargs)
class EventMetaPropertyForm(I18nModelForm):
class Meta:
model = EventMetaProperty
@@ -845,12 +851,12 @@ class ReusableMediumUpdateForm(forms.ModelForm):
class Meta:
model = ReusableMedium
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderposition', 'notes']
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderpositions', 'notes']
field_classes = {
'expires': SplitDateTimeField,
'customer': SafeModelChoiceField,
'linked_giftcard': SafeModelChoiceField,
'linked_orderposition': SafeOrderPositionChoiceField,
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,
@@ -860,8 +866,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
super().__init__(*args, **kwargs)
organizer = self.instance.organizer
self.fields['linked_orderposition'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
self.fields['linked_orderposition'].widget = Select2(
self.fields['linked_orderpositions'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
self.fields['linked_orderpositions'].widget = Select2Multiple(
attrs={
'data-model-select2': 'generic',
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
@@ -869,8 +875,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
}),
}
)
self.fields['linked_orderposition'].widget.choices = self.fields['linked_orderposition'].choices
self.fields['linked_orderposition'].required = False
self.fields['linked_orderpositions'].widget.choices = self.fields['linked_orderpositions'].choices
self.fields['linked_orderpositions'].required = False
self.fields['linked_giftcard'].queryset = organizer.issued_gift_cards.all()
self.fields['linked_giftcard'].widget = Select2(
@@ -924,12 +930,12 @@ class ReusableMediumCreateForm(ReusableMediumUpdateForm):
class Meta:
model = ReusableMedium
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderposition', 'linked_giftcard', 'customer', 'notes']
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderpositions', 'linked_giftcard', 'customer', 'notes']
field_classes = {
'expires': SplitDateTimeField,
'customer': SafeModelChoiceField,
'linked_giftcard': SafeModelChoiceField,
'linked_orderposition': SafeOrderPositionChoiceField,
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
}
widgets = {
'expires': SplitDateTimePickerWidget,

View File

@@ -518,7 +518,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': _(
@@ -742,6 +741,7 @@ class CoreUserImpersonatedLogEntryType(UserImpersonatedLogEntryType):
'pretix.reusable_medium.created': _('The reusable medium has been created.'),
'pretix.reusable_medium.created.auto': _('The reusable medium has been created automatically.'),
'pretix.reusable_medium.changed': _('The reusable medium has been changed.'),
'pretix.reusable_medium.linked_orderposition.added': _('A new ticket has been added to the medium.'),
'pretix.reusable_medium.linked_orderposition.changed': _('The medium has been connected to a new ticket.'),
'pretix.reusable_medium.linked_giftcard.changed': _('The medium has been connected to a new gift card.'),
'pretix.email.error': _('Sending of an email has failed.'),

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -54,8 +54,8 @@
<a href="?{% url_replace request 'ordering' 'identifier' %}"><i class="fa fa-caret-up"></i></a>
</th>
<th>{% trans "Media type" context "reusable_media" %}
<a href="?{% url_replace request 'ordering' '-email' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'email' %}"><i class="fa fa-caret-up"></i></a></th>
<a href="?{% url_replace request 'ordering' '-type' %}"><i class="fa fa-caret-down"></i></a>
<a href="?{% url_replace request 'ordering' 'type' %}"><i class="fa fa-caret-up"></i></a></th>
<th>{% trans "Connections" context "reusable_media" %}</th>
<th></th>
</tr>
@@ -82,13 +82,13 @@
</a>
</span>
{% endif %}
{% if m.linked_orderposition %}
{% for op in m.linked_orderpositions.all %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=m.linked_orderposition.order.event.slug organizer=request.organizer.slug code=m.linked_orderposition.order.code %}">
{{ m.linked_orderposition.order.code }}</a>-{{ m.linked_orderposition.positionid }}
<a href="{% url "control:event.order" event=op.order.event.slug organizer=request.organizer.slug code=op.order.code %}">
{{ op.order.code }}</a>-{{ op.positionid }}
</span>
{% endif %}
{% endfor %}
{% if m.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>

View File

@@ -28,7 +28,19 @@
<dt>{% trans "Media type" context "reusable_media" %}</dt>
<dd>{{ medium.get_type_display }}</dd>
<dt>{% trans "Identifier" context "reusable_media" %}</dt>
<dd><code>{{ medium.identifier }}</code></dd>
<dd>
<code id="medium_identifier">{{ medium.identifier }}</code>
<button type="button" class="btn btn-default btn-xs btn-clipboard js-only" data-clipboard-target="#medium_identifier">
<i class="fa fa-clipboard" aria-hidden="true"></i>
<span class="sr-only">{% trans "Copy to clipboard" %}</span>
</button>
{% if medium.type == "barcode" %}
<button type="button" class="btn btn-default btn-xs js-only" data-toggle="qrcode" data-qrcode="{{ medium.identifier }}">
<i class="fa fa-qrcode" aria-hidden="true"></i>
<span class="sr-only">{% trans "Create QR code" %}</span>
</button>
{% endif %}
</dd>
<dt>{% trans "Status" %}</dt>
<dd>
{% if not medium.active %}
@@ -49,13 +61,13 @@
</a>
</span>
{% endif %}
{% if medium.linked_orderposition %}
{% for op in medium.linked_orderpositions.all %}
<span class="helper-display-block">
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=medium.linked_orderposition.order.event.slug organizer=request.organizer.slug code=medium.linked_orderposition.order.code %}">
{{ medium.linked_orderposition.order.code }}</a>-{{ medium.linked_orderposition.positionid }}
</span>
{% endif %}
<span class="fa fa-ticket fa-fw"></span>
<a href="{% url "control:event.order" event=op.order.event.slug organizer=request.organizer.slug code=op.order.code %}">
{{ op.order.code }}</a>-{{ op.positionid }}
</span>
{% endfor %}
{% if medium.linked_giftcard %}
<span class="helper-display-block">
<span class="fa fa-credit-card fa-fw"></span>

View File

@@ -1641,17 +1641,9 @@ class OrderCheckVATID(OrderView):
try:
normalized_id = validate_vat_id(ia.vat_id, str(ia.country))
with transaction.atomic():
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
self.order.log_action(
'pretix.event.order.vatid.validated',
data={
'vat_id': normalized_id,
},
user=self.request.user,
)
ia.vat_id_validated = True
ia.vat_id = normalized_id
ia.save()
except VATIDFinalError as e:
messages.error(self.request, e.message)
except VATIDTemporaryError:

View File

@@ -1039,10 +1039,9 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
def _send_invite(self, instance):
mail(
instance.email,
_('Account invitation'),
_('pretix account invitation'),
'pretixcontrol/email/invitation.txt',
{
'instance': settings.PRETIX_INSTANCE_NAME,
'user': self,
'organizer': self.request.organizer.name,
'team': instance.team.name,
@@ -3339,8 +3338,10 @@ class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequire
def get_queryset(self):
qs = self.request.organizer.reusable_media.select_related(
'customer', 'linked_orderposition', 'linked_orderposition__order', 'linked_orderposition__order__event',
'linked_giftcard'
'customer',
'linked_giftcard',
).prefetch_related(
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order", "order__event"))
)
if self.filter_form.is_valid():
qs = self.filter_form.filter_qs(qs)
@@ -3388,10 +3389,14 @@ class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
@transaction.atomic
def form_valid(self, form):
r = super().form_valid(form)
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, data={
data = {
k: getattr(form.instance, k)
for k in form.changed_data
})
}
if "linked_orderpositions" in data:
data["linked_orderpositions"] = data["linked_orderpositions"].values_list("pk", flat=True)
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, data=data)
messages.success(self.request, _('Your changes have been saved.'))
return r
@@ -3417,10 +3422,13 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
@transaction.atomic
def form_valid(self, form):
if form.has_changed():
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data={
data = {
k: getattr(self.object, k)
for k in form.changed_data
})
}
if "linked_orderpositions" in data:
data["linked_orderpositions"] = data["linked_orderpositions"].values_list("pk", flat=True)
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data=data)
messages.success(self.request, _('Your changes have been saved.'))
return super().form_valid(form)

View File

@@ -8,16 +8,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-02-25 23:00+0000\n"
"Last-Translator: David Ibáñez Cerdeira <dibanez@gmail.com>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix/el/>"
"\n"
"PO-Revision-Date: 2025-02-14 21:00+0000\n"
"Last-Translator: deborahfoell <deborah.foell@om.org>\n"
"Language-Team: Greek <https://translate.pretix.eu/projects/pretix/pretix/el/"
">\n"
"Language: el\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16\n"
"X-Generator: Weblate 5.9.2\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -20467,7 +20467,7 @@ msgstr "Ορίστε νέο κωδικό πρόσβασης"
#: pretix/presale/templates/pretixpresale/organizers/customer_password.html:25
#: pretix/presale/templates/pretixpresale/organizers/customer_setpassword.html:25
msgid "Save"
msgstr "gardar"
msgstr "Αποθηκεύση"
#: pretix/control/templates/pretixcontrol/auth/register.html:7
msgid "Create a new account"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-03 20:00+0000\n"
"PO-Revision-Date: 2026-02-21 18:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: Spanish <https://translate.pretix.eu/projects/pretix/pretix/"
"es/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.1\n"
"X-Generator: Weblate 5.16\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -17058,20 +17058,28 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "Debe especificar tantas butacas como vales de compra."
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "Seleccione una opción válida."
msgstr "Por favor seleccione una butaca válida."
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "Solo incluir productos activos."
msgstr "Productos activos"
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "Ya se ha enviado un vale para esta entrada en la lista de espera."
msgstr "Ya existe un vale de compra con este código."
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "El producto seleccionado no esactivo."
msgstr "El producto seleccionado ha sido desactivado."
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -17721,7 +17729,7 @@ msgstr ""
#: pretix/control/logdisplay.py:589
msgid "The voucher has been changed."
msgstr "El vale de compra ha sido cambiado."
msgstr "EL vale de compra ha sido cambiado."
#: pretix/control/logdisplay.py:590
msgid "The voucher has been deleted."
@@ -18635,7 +18643,7 @@ msgstr "Entradas"
#: pretix/control/templates/pretixcontrol/order/index.html:764
#: pretix/presale/templates/pretixpresale/event/fragment_cart.html:457
msgid "Taxes"
msgstr "Impuestos"
msgstr "gravámenes"
#: pretix/control/navigation.py:97
msgid "Invoicing"
@@ -18875,10 +18883,6 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"Parece que el navegador no acepta nuestras cookies y es necesario iniciar "
"sesión repetidamente. Por favor, compruebe si el navegador está configurado "
"para bloquear cookies o elimine todas las cookies existentes y vuelva a "
"intentarlo."
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -18990,9 +18994,8 @@ msgid ""
"This application has <strong>not</strong> been reviewed by the pretix team. "
"Granting access to your pretix account happens at your own risk."
msgstr ""
"Esta aplicación <strong>no</strong> ha sido revisada por el equipo de "
"pretix. La concesión del acceso a su cuenta pretix se realiza bajo su propio "
"riesgo."
"Esta aplicación<strong>no</strong> ha sido revisada por el equipo de pretix. "
"La concesión del acceso a su cuenta pretix se realiza bajo su propio riesgo."
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html:54
msgid "Error:"
@@ -19126,8 +19129,8 @@ msgid ""
"We've detected that you are using <strong>Microsoft Internet Explorer</"
"strong>."
msgstr ""
"Hemos detectado que estás usando <strong>Microsoft Internet Explorer</strong>"
"."
"Hemos detectado que estás usando <strong> Microsoft Internet Explorer </"
"strong>."
#: pretix/control/templates/pretixcontrol/base.html:332
#: pretix/presale/templates/pretixpresale/base.html:54
@@ -19166,7 +19169,7 @@ msgid ""
"people from actually buying tickets."
msgstr ""
"Tu evento contiene <strong>pedidos de modo de prueba</strong> a pesar de que "
"<strong>el modo de prueba se ha deshabilitado</strong>. Deberías eliminar "
"<strong> el modo de prueba se ha deshabilitado</strong>. Deberías eliminar "
"estes pedidos para asegurarte que no se muestren en tus reportes "
"estadísticos y bloquear la compra de entradas a las personas."
@@ -20761,7 +20764,7 @@ msgid ""
"duplicate payment attempts. You should review the cases and consider "
"refunding the overpaid amount to the user."
msgstr ""
"Este evento contiene <strong>pedidos pagados en exceso</strong>, por "
"Este evento contiene <strong> pedidos pagados en exceso</strong>, por "
"ejemplo, debido a que hay intentos de pago duplicados. Debe revisar los "
"casos y considerar la devolución de la cantidad pagada en exceso al usuario."
@@ -20774,7 +20777,7 @@ msgid ""
"This event contains <strong>pending refunds</strong> that you should take "
"care of."
msgstr ""
"Este evento contiene <strong>devoluciones pendientes</strong> sobre las que "
"Este evento contiene <strong>devoluciones pendientes </strong> sobre las que "
"debe prestar atención."
#: pretix/control/templates/pretixcontrol/event/index.html:50
@@ -20812,7 +20815,7 @@ msgid ""
"arrived. You should review the cases and consider either refunding the "
"customer or creating more space."
msgstr ""
"Este evento contiene <strong>pedidos completamente pagados</strong> que no "
"Este evento contiene <strong> pedidos completamente pagados</strong> que no "
"están marcadas como pagados, probablemente porque no se dejo ningún cupo al "
"momento que llegó el pago. Debería revisar estos casos y considerar, "
"devolver el dinero o crear más espacio."
@@ -21476,7 +21479,7 @@ msgid ""
"as examples, you can add more in the \"Settings\" part of your event."
msgstr ""
"pretix soporta un <a href=\"https://pretix.eu/about/en/features/payment\" "
"target=\"_blank\">amplio rango de proveedores de pago</a> permitiéndole "
"target=\"_blank\">amplio rango de proveedores de pago </a> permitiéndole "
"elegir los métodos de pago que mejor se adapten a su flujo de trabajo. Aquí "
"hay sólo dos de ellos a modo de ejemplo, puede añadir más en la parte "
"\"Configuración\" de su evento."
@@ -23888,8 +23891,8 @@ msgid ""
"Do you really want to delete this order? <strong>You really cannot revert "
"this action and we can't either.</strong>"
msgstr ""
"¿Realmente quieres eliminar este pedido? <strong>No puedes revertir esta "
"acción y tampoco nosotros.</strong>"
"¿Realmente quieres eliminar este pedido? <strong> No puedes revertir esta "
"acción y tampoco nosotros. </strong>"
#: pretix/control/templates/pretixcontrol/order/delete.html:25
msgid "Yes, delete order"
@@ -24513,8 +24516,9 @@ msgid ""
msgstr ""
"Hemos recibido la notificación de que <strong>%(amount)s</strong> ha sido "
"devuelto a través de <strong>%(method)s</strong>. Si este reembolso está "
"procesado, el pedido habrá sido pagado con un importe inferior en <strong>%"
"(pending)s</strong>. El total del pedido es <strong>%(total)s</strong>."
"procesado, el pedido habrá sido pagado con un importe inferior "
"en<strong>%(pending)s</strong>. El total del pedido es <strong>%(total)s</"
"strong>."
#: pretix/control/templates/pretixcontrol/order/refund_process.html:30
msgid "Since the order is already canceled, this will not affect its state."
@@ -27258,7 +27262,7 @@ msgid ""
"Using this option will <strong>delete all current quotas</strong> from "
"<strong>all selected dates</strong>."
msgstr ""
"Al utilizar esta opción se <strong>eliminarán todas las cuotas actuales</"
"Al utilizar esta opción se <strong> eliminarán todas las cuotas actuales </"
"strong> de <strong>todas las fechas seleccionadas</strong>."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:277
@@ -27994,7 +27998,7 @@ msgstr "Borrar vale de compra"
msgid ""
"Are you sure you want to delete the voucher <strong>%(voucher)s</strong>?"
msgstr ""
"¿Está seguro de que desea borrar el vale de compra <strong>%(voucher)s</"
"¿Está seguro de que desea borrar el vale de compra<strong>%(voucher)s</"
"strong>?"
#: pretix/control/templates/pretixcontrol/vouchers/delete_bulk.html:4
@@ -28187,8 +28191,10 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "Editar entrada"
msgstr "Ingreso"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -28250,7 +28256,7 @@ msgid ""
"quota is available) or you can press the big button below this text to send "
"out as many vouchers as currently possible to the persons who waited longest."
msgstr ""
"Ha configurado que los vales de compra se enviarán <strong>no</strong> "
"Ha configurado que los vales de compra se enviarán <strong>no </strong> "
"automáticamente. Puede enviarlos uno por uno en un orden de su elección "
"haciendo clic en los botones junto a una línea de esta tabla (si hay "
"suficiente cuota disponible) o puede presionar el botón grande debajo de "
@@ -30127,7 +30133,15 @@ msgstr ""
"La autenticación de dos factores ahora está desactivada para su cuenta."
#: pretix/control/views/user.py:635
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -30141,7 +30155,8 @@ msgstr ""
"verlos aquí.\n"
"\n"
"Sus códigos de emergencias:\n"
"{tokens}"
"- \n"
"- "
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -30311,8 +30326,10 @@ msgid "The selected entry has been deleted."
msgstr "Se ha borrado la entrada seleccionada."
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "Se ha modificado la entrada de la lista de espera."
msgstr "Las entradas de la lista de espera han sido transferidas."
#: pretix/helpers/countries.py:134
msgid "Belarus"
@@ -36994,8 +37011,8 @@ msgid ""
"If you're looking to buy a ticket, you need to follow a direct link to an "
"event or organizer profile."
msgstr ""
"Si busca comprar una entrada, necesita seguir un enlace directo a un evento "
"o a un perfil de organizador."
"SI buscas comprar una entrada, necesitar seguir un enlace directo a un "
"evento o a un perfil de organizador."
#: pretix/presale/templates/pretixpresale/index.html:20
#, python-format

View File

@@ -4,16 +4,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-03 20:00+0000\n"
"PO-Revision-Date: 2026-02-21 18:00+0000\n"
"Last-Translator: CVZ-es <damien.bremont@casadevelazquez.org>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/"
"fr/>\n"
"Language-Team: French <https://translate.pretix.eu/projects/pretix/pretix/fr/"
">\n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n > 1;\n"
"X-Generator: Weblate 5.16.1\n"
"X-Generator: Weblate 5.16\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -13011,9 +13011,7 @@ msgstr "Texte daide du champ email"
#: pretix/base/settings.py:3398
msgid "Allow creating a new team during event creation"
msgstr ""
"Autoriser la création d'une nouvelle équipe lors de la création d'un "
"événement"
msgstr "Ancienne API de lappareil denregistrement"
#: pretix/base/settings.py:3399
msgid ""
@@ -17206,20 +17204,28 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "Vous devez spécifier autant de sièges que de codes promotionnels."
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "Sélectionnez une option valide."
msgstr "Veuillez sélectionner un siège valide."
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "Comprend uniquement les produits actifs."
msgstr "Produits actifs"
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "Un bon pour cette inscription sur la liste d'attente a déjà été envoyé."
msgstr "Un bon de réduction avec ce code existe déjà."
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "Le produit sélectionné n'est pas actif."
msgstr "Le produit sélectionné a été désacti."
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -19015,10 +19021,6 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"Il semble que votre navigateur n'accepte pas nos cookies et que vous deviez "
"vous connecter à plusieurs reprises. Veuillez vérifier si votre navigateur "
"est configuré pour bloquer les cookies, ou supprimez tous les cookies "
"existants et réessayez."
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -19106,8 +19108,8 @@ msgid ""
"Do you really want to grant the application <strong>%(application)s</strong> "
"access to your pretix account?"
msgstr ""
"Voulez-vous vraiment accorder à l'application <strong>%(application)s</"
"strong> un accès à votre compte pretix ?"
"Voulez-vous vraiment accorder à l'application <strong>1%(application)s2</"
"strong>3 un accès à votre compte pretix ?"
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html:24
#, python-format
@@ -19130,8 +19132,8 @@ msgid ""
"This application has <strong>not</strong> been reviewed by the pretix team. "
"Granting access to your pretix account happens at your own risk."
msgstr ""
"Cette application n'a <strong>pas</strong> été véifiée par l'équipe pretix. "
"L'accès à votre compte Pretix se fait à vos propres risques."
"Cette application n'a <strong>1pas</strong>2 été véifiée par l'équipe "
"pretix. L'accès à votre compte Pretix se fait à vos propres risques."
#: pretix/control/templates/pretixcontrol/auth/oauth_authorization.html:54
msgid "Error:"
@@ -19534,7 +19536,7 @@ msgstr[0] ""
"Êtes-vous sûr de vouloir supprimer l'enregistrement <strong>d'un billet</"
"strong>?"
msgstr[1] ""
"Êtes-vous sûr de vouloir supprimer l'enregistrement <strong>%(count)s "
"Êtes-vous sûr de vouloir supprimer l'enregistrement <strong>%(count)s "
"billets</strong>?"
#: pretix/control/templates/pretixcontrol/checkin/bulk_revert_confirm.html:24
@@ -21632,7 +21634,7 @@ msgid ""
"as examples, you can add more in the \"Settings\" part of your event."
msgstr ""
"pretix supporte une <a href=\"https://pretix.eu/about/en/features/payment\" "
"target=\"_blank\">large gamme de fournisseurs de paiement</a> vous "
"target=\"_blank\">large gamme de fournisseurs de paiement </a> vous "
"permettant de choisir les méthodes de paiement qui conviennent le mieux à "
"votre flux de travail. En voici deux à titre d'exemple, vous pouvez en "
"ajouter dans la partie \"Paramètres\" de votre événement."
@@ -23576,8 +23578,8 @@ msgid ""
msgstr ""
"Veuillez sélectionner les produits ou les variantes de produits auxquels ce "
"quota doit s'appliquer. Si vous appliquez deux quotas à un même produit, il "
"ne sera seulement disponible si les quotas <strong>et</strong> ont encore de "
"la place."
"ne sera seulement disponible si les quotas <strong>1 et </strong>2 ont "
"encore de la place."
#: pretix/control/templates/pretixcontrol/items/quota_edit.html:41
msgid "Advanced options"
@@ -23636,8 +23638,8 @@ msgid ""
"Are you sure you want to disable the application <strong>%(application)s</"
"strong> permanently?"
msgstr ""
"Êtes-vous sûr de vouloir désactiver l'application <strong>%(application)s</"
"strong> de manière permanente ?"
"Êtes-vous sûr de vouloir désactiver l'application <strong>1%(application)s2</"
"strong>3 en permanence ?"
#: pretix/control/templates/pretixcontrol/oauth/app_list.html:4
#: pretix/control/templates/pretixcontrol/oauth/app_list.html:6
@@ -27463,8 +27465,8 @@ msgid ""
"Using this option will <strong>delete all current quotas</strong> from "
"<strong>all selected dates</strong>."
msgstr ""
"Cette option permet de <strong>supprimer tous les quotas actuels</strong> de "
"<strong>toutes les dates sélectionnées</strong>."
"Cette option permet de <strong> supprimer tous les quotas actuels</strong> "
"de <strong>toutes les dates sélectionnées</strong>."
#: pretix/control/templates/pretixcontrol/subevents/bulk_edit.html:277
msgid ""
@@ -28397,8 +28399,10 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "Modifier l'entrée"
msgstr "Entrée"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -28460,7 +28464,7 @@ msgid ""
"quota is available) or you can press the big button below this text to send "
"out as many vouchers as currently possible to the persons who waited longest."
msgstr ""
"Vous avez configuré que les bons <strong>ne</strong> seront envoyés "
"Vous avez configuré que les bons <strong>1not</strong>2 seront envoyés "
"automatiquement. Vous pouvez soit les envoyer un par un dans l'ordre de "
"votre choix en cliquant sur les boutons à côté d'une ligne dans ce tableau "
"(si un quota suffisant est disponible), soit vous pouvez appuyer sur le gros "
@@ -30364,7 +30368,15 @@ msgstr ""
"compte."
#: pretix/control/views/user.py:635
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -30378,7 +30390,8 @@ msgstr ""
"appareils. Vous ne pourrez plus les consulter ici.\n"
"\n"
"Vos codes d'urgence :\n"
"{tokens}"
"-\n"
"- "
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -30545,8 +30558,10 @@ msgid "The selected entry has been deleted."
msgstr "L'entrée sélectionnée a été supprimée."
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "L'entrée dans la liste d'attente a été modifiée."
msgstr "Lentrée de la liste dattente a été transférée."
#: pretix/helpers/countries.py:134
msgid "Belarus"

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-01-26 09:10+0000\n"
"PO-Revision-Date: 2026-03-02 21:00+0000\n"
"Last-Translator: Sandra Rial Pérez <sandrarial@gestiontickets.online>\n"
"PO-Revision-Date: 2025-12-03 23:00+0000\n"
"Last-Translator: sandra r <sandrarial@gestiontickets.online>\n"
"Language-Team: Galician <https://translate.pretix.eu/projects/pretix/pretix-"
"js/gl/>\n"
"Language: gl\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.1\n"
"X-Generator: Weblate 5.14.3\n"
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:56
#: pretix/plugins/banktransfer/static/pretixplugins/banktransfer/ui.js:62
@@ -162,12 +162,12 @@ msgstr "Pedidos pagados"
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (ordered)"
msgstr "Asistentes (ordenados)"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:27
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:39
msgid "Attendees (paid)"
msgstr "Asistentes (de pago)"
msgstr ""
#: pretix/plugins/statistics/static/pretixplugins/statistics/statistics.js:51
msgid "Total revenue"
@@ -732,8 +732,8 @@ msgid ""
"The items in your cart are no longer reserved for you. You can still "
"complete your order as long as theyre available."
msgstr ""
"Os artigos do teu carro xa non están reservados para ti. Podes completar o "
"teu pedido sempre que estean dispoñibles."
"Os artigos da túa cesta xa non están reservados para ti. Aínda podes "
"completar o teu pedido mentres estean dispoñibles."
#: pretix/static/pretixpresale/js/ui/cart.js:49
msgid "Cart expired"

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-02 10:00+0000\n"
"PO-Revision-Date: 2026-02-24 11:49+0000\n"
"Last-Translator: Hijiri Umemoto <hijiri@umemoto.org>\n"
"Language-Team: Japanese <https://translate.pretix.eu/projects/pretix/pretix/"
"ja/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 5.16.1\n"
"X-Generator: Weblate 5.16\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -6997,22 +6997,22 @@ msgstr "2006/112/EC号指令の第309条に基づいて免除"
#: pretix/base/models/tax.py:253
msgctxt "tax_code"
msgid "Intra-Community acquisition from second hand means of transport"
msgstr "中古輸送手段の域内取得"
msgstr "中古輸送手段のEU域内取得"
#: pretix/base/models/tax.py:255
msgctxt "tax_code"
msgid "Intra-Community acquisition of second hand goods"
msgstr "中古品の域内取得"
msgstr "中古品のEU域内取得"
#: pretix/base/models/tax.py:257
msgctxt "tax_code"
msgid "Intra-Community acquisition of works of art"
msgstr "芸術作品の域内取得"
msgstr "芸術作品のEU域内取得"
#: pretix/base/models/tax.py:259
msgctxt "tax_code"
msgid "Intra-Community acquisition of collectors items and antiques"
msgstr "コレクターアイテムおよび骨董品の域内取得"
msgstr "コレクターアイテムおよび骨董品のEU域内取得"
#: pretix/base/models/tax.py:261
msgctxt "tax_code"
@@ -16579,20 +16579,28 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "あなたはバウチャーコードの数と同じだけの席を指定する必要があります。"
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "有効な選択肢を選んでください。"
msgstr "有効な座席を選択してください。"
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "有効な製品のみを含みます。"
msgstr "有効な製品"
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "この空席待ちリストの内容を持つバウチャーはすでに発送済みです。"
msgstr "このコードを持つバウチャーはすでに存在しています。"
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "選択された製品無効です。"
msgstr "選択された製品無効化されました。"
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -18340,9 +18348,6 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"お使いのブラウザが私どものクッキーを受け入れていないようですので、繰り返し"
"ログインする必要があります。ブラウザがクッキーをブロックする設定になっている"
"か、既存のクッキーをすべて削除して再試行してください。"
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -27395,8 +27400,10 @@ msgstr "次のエントリはすでにバウチャーが添付されているた
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "エントリーを編集する"
msgstr "入場"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -29276,7 +29283,15 @@ msgid "Two-factor authentication is now disabled for your account."
msgstr "アカウントの二要素認証が無効になりました。"
#: pretix/control/views/user.py:635
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -29290,7 +29305,8 @@ msgstr ""
"ことはできません。\n"
"\n"
"あなたの緊急コード:\n"
"{tokens}"
"-\n"
"- "
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -29455,8 +29471,10 @@ msgid "The selected entry has been deleted."
msgstr "選択されたエントリは削除されました。"
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "空席待ちリスト登録が変更されました。"
msgstr "空席待ちリスト登録が転送されました。"
#: pretix/helpers/countries.py:134
msgid "Belarus"

View File

@@ -7,16 +7,16 @@ msgstr ""
"Project-Id-Version: 1\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-03 20:00+0000\n"
"PO-Revision-Date: 2026-02-21 03:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/>"
"\n"
"Language-Team: Dutch <https://translate.pretix.eu/projects/pretix/pretix/nl/"
">\n"
"Language: nl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.1\n"
"X-Generator: Weblate 5.16\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -7996,7 +7996,7 @@ msgstr "Deze cadeaubon is in de tussentijd gebruikt. Probeer het opnieuw."
#: pretix/base/pdf.py:98
msgid "Ticket code (barcode content)"
msgstr "Ticketcode (inhoud van QR-code)"
msgstr "Ticketcode (waarde van QR-code)"
#: pretix/base/pdf.py:110
msgid "Order position number"
@@ -9664,7 +9664,7 @@ msgstr "Het bedrag is van uw kaart afgeschreven."
#: pretix/base/services/placeholders.py:699
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "Maak het geld over naar deze bankrekening: 9999-9999-9999-9999"
msgstr "Maak het geld over naar deze bankrekening: NL13 TEST 0123 4567 89"
#: pretix/base/services/placeholders.py:799
#: pretix/control/views/organizer.py:349
@@ -11081,8 +11081,8 @@ msgid ""
"If turned off, ticket downloads are only possible after an order has been "
"marked as paid."
msgstr ""
"Indien uitgeschakeld, kunnen tickets alleen gedownload worden als de "
"bestelling als betaald gemarkeerd is."
"Als deze optie is uitgeschakeld, kunnen tickets alleen worden gedownload "
"nadat een bestelling als betaald is gemarkeerd."
#: pretix/base/settings.py:1763
msgid "Do not issue ticket before email address is validated"
@@ -13801,12 +13801,12 @@ msgstr "Altijd"
#: pretix/base/timeline.py:60
msgctxt "timeline"
msgid "Your event starts"
msgstr "Start van uw evenement"
msgstr "Uw evenement start"
#: pretix/base/timeline.py:68
msgctxt "timeline"
msgid "Your event ends"
msgstr "Einde van uw evenement"
msgstr "Uw evenement eindigt"
#: pretix/base/timeline.py:76
msgctxt "timeline"
@@ -17002,20 +17002,28 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "U moet evenveel stoelnummers als vouchercodes opgeven."
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "Maak een geldige keuze."
msgstr "Kies een geldige beschikbare stoel."
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "Bevat alleen actieve producten."
msgstr "Actieve producten"
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "Er is al een voucher verzonden naar deze inschrijver op de wachtlijst."
msgstr "Er bestaat al een voucher met deze code."
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "Het gekozen product is niet actief."
msgstr "Het gekozen product is uitgeschakeld."
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -18803,9 +18811,6 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"Uw browser lijkt ons cookie niet te accepteren, waardoor u telkens opnieuw "
"moet inloggen. Controleer of uw browser cookies blokkeert of verwijder alle "
"opgeslagen cookies en probeer het opnieuw."
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -22354,7 +22359,7 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/item/base.html:24
#: pretix/control/templates/pretixcontrol/item/include_variations.html:79
msgid "Manage quotas"
msgstr "Quota beheren"
msgstr "Vragen quotums"
#: pretix/control/templates/pretixcontrol/item/base.html:27
#: pretix/control/templates/pretixcontrol/item/include_variations.html:82
@@ -28072,8 +28077,10 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "Inschrijving bewerken"
msgstr "Binnenkomst"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -30013,7 +30020,15 @@ msgid "Two-factor authentication is now disabled for your account."
msgstr "Twee-factor-authenticatie is nu uitgeschakeld voor uw account."
#: pretix/control/views/user.py:635
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -30027,7 +30042,8 @@ msgstr ""
"U kunt ze hier niet meer opnieuw laten tonen.\n"
"\n"
"Uw noodcodes: \n"
"{tokens}"
"-\n"
"- "
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -30194,8 +30210,10 @@ msgid "The selected entry has been deleted."
msgstr "De gekozen inschrijving is verwijderd."
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "De inschrijving op de wachtlijst is veranderd."
msgstr "De wachtlijstinschrijving is overgedragen."
#: pretix/helpers/countries.py:134
msgid "Belarus"

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-02-24 11:50+0000\n"
"PO-Revision-Date: 2026-03-02 10:00+0000\n"
"PO-Revision-Date: 2026-02-21 03:00+0000\n"
"Last-Translator: Ruud Hendrickx <ruud@leckxicon.eu>\n"
"Language-Team: Dutch (informal) <https://translate.pretix.eu/projects/pretix/"
"pretix/nl_Informal/>\n"
@@ -17,7 +17,7 @@ msgstr ""
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.16.1\n"
"X-Generator: Weblate 5.16\n"
#: pretix/_base_settings.py:87
msgid "English"
@@ -9673,7 +9673,7 @@ msgstr "Het bedrag is van je kaart afgeschreven."
#: pretix/base/services/placeholders.py:699
msgid "Please transfer money to this bank account: 9999-9999-9999-9999"
msgstr "Maak het geld over naar deze bankrekening: 9999-9999-9999-9999"
msgstr "Maak het geld over naar deze bankrekening: NL13 TEST 0123 4567 89"
#: pretix/base/services/placeholders.py:799
#: pretix/control/views/organizer.py:349
@@ -17032,20 +17032,28 @@ msgid "You need to specify as many seats as voucher codes."
msgstr "Je moet evenveel stoelnummers als vouchercodes opgeven."
#: pretix/control/forms/waitinglist.py:39
#, fuzzy
#| msgid "Please select a valid seat."
msgid "Select a valid choice."
msgstr "Maak een geldige keuze."
msgstr "Kies een geldige beschikbare stoel."
#: pretix/control/forms/waitinglist.py:107
#, fuzzy
#| msgid "Active products"
msgid "Only includes active products."
msgstr "Bevat alleen actieve producten."
msgstr "Actieve producten"
#: pretix/control/forms/waitinglist.py:115
#, fuzzy
#| msgid "A voucher with this code already exists."
msgid "A voucher for this waiting list entry was already sent out."
msgstr "Er is al een voucher verzonden naar deze inschrijver op de wachtlijst."
msgstr "Er bestaat al een voucher met deze code."
#: pretix/control/forms/waitinglist.py:125
#, fuzzy
#| msgid "The selected product has been deactivated."
msgid "The selected product is not active."
msgstr "Het gekozen product is niet actief."
msgstr "Het gekozen product is uitgeschakeld."
#: pretix/control/logdisplay.py:73 pretix/control/logdisplay.py:83
msgid "The order has been changed:"
@@ -18836,9 +18844,6 @@ msgid ""
"in repeatedly. Please check if your browser is set to block cookies, or "
"delete all existing cookies and retry."
msgstr ""
"Je browser lijkt ons cookie niet te accepteren, waardoor je telkens opnieuw "
"moet inloggen. Controleer of je browser cookies blokkeert of verwijder alle "
"opgeslagen cookies en probeer het opnieuw."
#: pretix/control/templates/pretixcontrol/auth/login.html:35
#: pretix/presale/templates/pretixpresale/fragment_login_status.html:19
@@ -28136,8 +28141,10 @@ msgstr ""
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:4
#: pretix/control/templates/pretixcontrol/waitinglist/edit.html:6
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:273
#, fuzzy
#| msgid "Entry"
msgid "Edit entry"
msgstr "Inschrijving bewerken"
msgstr "Binnenkomst"
#: pretix/control/templates/pretixcontrol/waitinglist/index.html:17
msgid ""
@@ -30073,7 +30080,15 @@ msgid "Two-factor authentication is now disabled for your account."
msgstr "Twee-factor-authenticatie is nu uitgeschakeld voor je account."
#: pretix/control/views/user.py:635
#, python-brace-format
#, fuzzy, python-brace-format
#| msgid ""
#| "Your emergency codes have been newly generated. Remember to store them in "
#| "a safe place in case you lose access to your devices. You will not be "
#| "able to view them again here.\n"
#| "\n"
#| "Your emergency codes:\n"
#| "- \n"
#| "- "
msgid ""
"Your emergency codes have been newly generated. Remember to store them in a "
"safe place in case you lose access to your devices. You will not be able to "
@@ -30087,7 +30102,8 @@ msgstr ""
"Je kunt ze hier niet meer opnieuw laten tonen.\n"
"\n"
"Je noodcodes:\n"
"{tokens}"
"-\n"
"- "
#: pretix/control/views/user.py:655
msgid "Your notifications have been disabled."
@@ -30254,8 +30270,10 @@ msgid "The selected entry has been deleted."
msgstr "De gekozen inschrijving is verwijderd."
#: pretix/control/views/waitinglist.py:417
#, fuzzy
#| msgid "The waitinglist entry has been transferred."
msgid "The waitinglist entry has been changed."
msgstr "De inschrijving op de wachtlijst is veranderd."
msgstr "De wachtlijstvermelding is overgedragen."
#: pretix/helpers/countries.py:134
msgid "Belarus"
@@ -34057,7 +34075,7 @@ msgid ""
"completed your payment, you can refresh this page."
msgstr ""
"Scan de QR-code hieronder om je WeChat-betaling uit te voeren. Als je de "
"betaling hebt afgerond, kan je deze pagina verversen."
"betaling hebt afgerond kan je deze pagina verversen."
#: pretix/plugins/stripe/templates/pretixplugins/stripe/pending.html:62
msgid ""
@@ -35231,7 +35249,7 @@ msgstr "incl. belasting"
#: pretix/presale/templates/pretixpresale/event/voucher.html:359
#, python-format
msgid "<strong>plus</strong> %(rate)s%% %(name)s"
msgstr "<strong>plus</strong> %(rate)s%% %(name)s"
msgstr "<strong>excl.</strong> %(rate)s%% %(name)s"
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:180
#: pretix/presale/templates/pretixpresale/event/fragment_addon_choice.html:320

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,6 @@
{% load eventurl %}
{% load safelink %}
{% load rich_text %}
{% load anonymize_email %}
{% block thetitle %}
{% if messages %}
{{ messages|join:" " }} ::
@@ -220,7 +219,7 @@
{% endblock %}
{% block footernav %}
{% if request.event.settings.contact_mail %}
<li><a href="{{ 'mailto:'|add:request.event.settings.contact_mail|anon_email }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
<li><a href="mailto:{{ request.event.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
{% endif %}
{% if request.event.settings.privacy_url %}
<li><a href="{% safelink request.event.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>

View File

@@ -20,7 +20,6 @@
{% bootstrap_form_errors timemachine_form "all" %}
<p>{% trans "Test your shop as if it were a different date and time." %}</p>
<p>{% trans "Please note that the changed time is not taken into account for aspects of the shop that affect quotas, such as the validity period of carts and vouchers." %}</p>
<div class="row">
<div class="col-md-6">
@@ -45,4 +44,4 @@
<div class="clear"></div>
</div>
</div>
{% endblock %}
{% endblock %}

View File

@@ -21,5 +21,4 @@
<script type="text/javascript" src="{% static "pretixpresale/js/ui/cart.js" %}"></script>
<script type="text/javascript" src="{% static "pretixpresale/js/ui/iframe.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/addressform.js" %}"></script>
<script type="text/javascript" src="{% static "pretixbase/js/deanonymize_email.js" %}"></script>
{% endcompress %}

View File

@@ -5,7 +5,6 @@
{% load thumb %}
{% load eventurl %}
{% load safelink %}
{% load anonymize_email %}
{% block thetitle %}
{% block title %}{% endblock %}{% if url_name != "organizer.index" %} :: {% endif %}{{ organizer.name }}
{% endblock %}
@@ -98,7 +97,7 @@
{% endblock %}
{% block footernav %}
{% if not request.event and request.organizer.settings.contact_mail %}
<li><a href="{{ 'mailto:'|add:request.organizer.settings.contact_mail|anon_email }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
<li><a href="mailto:{{ request.organizer.settings.contact_mail }}" target="_blank" rel="noopener">{% trans "Contact" %}</a></li>
{% endif %}
{% if not request.event and request.organizer.settings.privacy_url %}
<li><a href="{% safelink request.organizer.settings.privacy_url %}" target="_blank" rel="noopener">{% trans "Privacy policy" %}</a></li>

View File

@@ -1,7 +0,0 @@
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('a[href^="mailto:"]').forEach(function(link) {
// Replace [at] with @ and the [dot] with . in both the href and the displayed text (if needed)
link.href = link.href.replace('[at]', '@').replace('[dot]', '.');
link.textContent = link.textContent.replace('[at]', '@').replace('[dot]', '.');
});
});

View File

@@ -286,12 +286,12 @@ def test_by_secret_special_chars(token_client, organizer, clist, event, order):
@pytest.mark.django_db
def test_by_medium(token_client, organizer, clist, event, order):
with scopes_disabled():
ReusableMedium.objects.create(
rm = ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
linked_orderposition=order.positions.first(),
)
rm.linked_orderpositions.add(order.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
@@ -301,6 +301,48 @@ def test_by_medium(token_client, organizer, clist, event, order):
assert ci.raw_source_type == "barcode"
@pytest.mark.django_db
def test_by_medium_multiple_orderpositions(token_client, organizer, clist, event, order):
with scopes_disabled():
rm = ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
)
rm.linked_orderpositions.add(order.positions.first())
op_item_other = order.positions.all()[1]
rm.linked_orderpositions.add(op_item_other)
# multiple tickets are valid => no check-in
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'ambiguous'
with scopes_disabled():
op_item_other.valid_from = datetime.datetime(2020, 1, 1, 12, 0, 0, tzinfo=event.timezone)
op_item_other.valid_until = datetime.datetime(2020, 1, 1, 15, 0, 0, tzinfo=event.timezone)
op_item_other.save()
with freeze_time("2020-01-01 13:45:00"):
# multiple tickets are valid => no check-in
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'ambiguous'
with freeze_time("2020-01-01 10:45:00"):
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 201
assert resp.data['status'] == 'ok'
with freeze_time("2020-01-01 15:45:00"):
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 400
assert resp.data['status'] == 'error'
assert resp.data['reason'] == 'already_redeemed'
@pytest.mark.django_db
def test_by_medium_not_connected(token_client, organizer, clist, event, order):
with scopes_disabled():
@@ -318,12 +360,12 @@ def test_by_medium_not_connected(token_client, organizer, clist, event, order):
@pytest.mark.django_db
def test_by_medium_wrong_event(token_client, organizer, clist, event, order2):
with scopes_disabled():
ReusableMedium.objects.create(
rm = ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
linked_orderposition=order2.positions.first(),
)
rm.linked_orderpositions.add(order2.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 404
assert resp.data['status'] == 'error'
@@ -337,12 +379,12 @@ def test_by_medium_wrong_event(token_client, organizer, clist, event, order2):
@pytest.mark.django_db
def test_by_medium_wrong_type(token_client, organizer, clist, event, order):
with scopes_disabled():
ReusableMedium.objects.create(
rm = ReusableMedium.objects.create(
type="nfc_uid",
identifier="abcdef",
organizer=organizer,
linked_orderposition=order.positions.first(),
)
rm.linked_orderpositions.add(order.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 404
assert resp.data['status'] == 'error'
@@ -355,13 +397,13 @@ def test_by_medium_wrong_type(token_client, organizer, clist, event, order):
@pytest.mark.django_db
def test_by_medium_inactive(token_client, organizer, clist, event, order):
with scopes_disabled():
ReusableMedium.objects.create(
rm = ReusableMedium.objects.create(
type="barcode",
identifier="abcdef",
organizer=organizer,
active=False,
linked_orderposition=order.positions.first(),
)
rm.linked_orderpositions.add(order.positions.first())
resp = _redeem(token_client, organizer, clist, "abcdef", {"source_type": "barcode"})
assert resp.status_code == 404
assert resp.data['status'] == 'error'

View File

@@ -895,41 +895,6 @@ def test_order_create_payment_info_optional(token_client, organizer, event, item
assert json.loads(p.info) == res['payment_info']
@pytest.mark.django_db
def test_order_create_payment_info_valid_object(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['answers'][0]['question'] = question.pk
res["payment_info"] = [{"should": "fail"}]
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
res['payment_info'] = {
'foo': {
'bar': [1, 2],
'test': False
}
}
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
p = o.payments.first()
assert p.provider == "banktransfer"
assert p.amount == o.total
assert json.loads(p.info) == res['payment_info']
@pytest.mark.django_db
def test_order_create_position_secret_optional(token_client, organizer, event, item, quota, question):
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
@@ -3121,9 +3086,78 @@ def test_order_create_use_medium(token_client, organizer, event, item, quota, qu
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
medium.refresh_from_db()
assert o.positions.first() == medium.linked_orderposition
assert o.positions.first() == medium.linked_orderpositions.first()
assert resp.data['positions'][0]['pdf_data']['medium_identifier'] == medium.identifier
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 1
assert o.positions.first() == medium.linked_orderpositions.first()
assert resp.data['positions'][0]['pdf_data']['medium_identifier'] == medium.identifier
@pytest.mark.django_db
def test_order_create_add_to_medium(token_client, organizer, event, item, quota, question, medium):
item.media_type = medium.type
item.media_policy = Item.MEDIA_POLICY_REUSE_OR_NEW
item.save()
res = copy.deepcopy(ORDER_CREATE_PAYLOAD)
res['positions'][0]['item'] = item.pk
res['positions'][0]['use_reusable_medium'] = medium.pk
res['positions'][0]['add_to_reusable_medium'] = medium.pk
res['positions'][0]['answers'][0]['question'] = question.pk
# do not use use_reusable_medium and add_to_reusable_medium
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 400
del res['positions'][0]['use_reusable_medium']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 1
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 2
res['positions'][0]['use_reusable_medium'] = medium.pk
del res['positions'][0]['add_to_reusable_medium']
resp = token_client.post(
'/api/v1/organizers/{}/events/{}/orders/?pdf_data=true'.format(
organizer.slug, event.slug
), format='json', data=res
)
assert resp.status_code == 201
with scopes_disabled():
o = Order.objects.get(code=resp.data['code'])
medium.refresh_from_db()
assert medium.linked_orderpositions.count() == 1
assert o.positions.first() == medium.linked_orderpositions.first()
@pytest.mark.django_db
def test_order_create_use_medium_other_organizer(token_client, organizer, event, item, quota, question, medium2):
@@ -3168,7 +3202,7 @@ def test_order_create_create_medium(token_client, organizer, event, item, quota,
i = resp.data['positions'][0]['pdf_data']['medium_identifier']
assert i
m = organizer.reusable_media.get(identifier=i)
assert m.linked_orderposition == o.positions.first()
assert m.linked_orderpositions.first() == o.positions.first()
assert m.type == "barcode"

View File

@@ -89,10 +89,13 @@ TEST_MEDIUM_RES = {
"organizer": "dummy",
"identifier": "ABCDEFGH",
"type": "barcode",
"claim_token": None,
"label": None,
"active": True,
"expires": None,
"customer": None,
"linked_orderposition": None,
"linked_orderpositions": [],
"linked_giftcard": None,
"notes": None,
"info": {},
@@ -138,7 +141,7 @@ def test_medium_detail(token_client, organizer, event, medium, giftcard, custome
ticket = event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
medium.linked_orderposition = op
medium.linked_orderpositions.add(op)
medium.linked_giftcard = giftcard
medium.customer = customer
medium.save()
@@ -419,7 +422,7 @@ def test_medium_lookup_cross_organizer(token_client, organizer, organizer2, org2
ticket = org2_event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
personalized=True)
op = o.positions.create(item=ticket, price=Decimal("14"))
medium2.linked_orderposition = op
medium2.linked_orderpositions.add(op)
medium2.linked_giftcard = giftcard2
medium2.save()