mirror of
https://github.com/pretix/pretix.git
synced 2026-06-10 01:15:05 +00:00
Compare commits
1 Commits
dependabot
...
pajowu/fon
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69ea5b4069 |
@@ -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)
|
||||
|
||||
@@ -1070,7 +1070,6 @@ 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``
|
||||
|
||||
|
||||
@@ -21,16 +21,12 @@ 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_orderpositions list of integers 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_orderposition integer Internal ID of a ticket this medium is linked to.
|
||||
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.
|
||||
@@ -43,14 +39,6 @@ Existing media types are:
|
||||
- ``nfc_uid``
|
||||
- ``nfc_mf0aes``
|
||||
|
||||
|
||||
.. versionchanged:: 2026.5
|
||||
|
||||
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
|
||||
---------
|
||||
|
||||
@@ -89,7 +77,6 @@ Endpoints
|
||||
"active": True,
|
||||
"expires": None,
|
||||
"customer": None,
|
||||
"linked_orderpositions": [],
|
||||
"linked_orderposition": None,
|
||||
"linked_giftcard": None,
|
||||
"notes": None,
|
||||
@@ -105,13 +92,10 @@ 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_orderpositions"``,
|
||||
``"linked_orderposition"`` (**DEPRECATED**), 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_orderposition"``,
|
||||
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.
|
||||
@@ -150,7 +134,6 @@ Endpoints
|
||||
"active": True,
|
||||
"expires": None,
|
||||
"customer": None,
|
||||
"linked_orderpositions": [],
|
||||
"linked_orderposition": None,
|
||||
"linked_giftcard": None,
|
||||
"notes": None,
|
||||
@@ -208,7 +191,6 @@ Endpoints
|
||||
"active": True,
|
||||
"expires": None,
|
||||
"customer": None,
|
||||
"linked_orderpositions": [],
|
||||
"linked_orderposition": None,
|
||||
"linked_giftcard": None,
|
||||
"notes": None,
|
||||
@@ -216,9 +198,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_orderpositions"``, or ``"customer"``, the respective
|
||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"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_orderpositions`` each will have an attribute of the
|
||||
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
|
||||
can be given multiple times.
|
||||
:statuscode 201: no error
|
||||
@@ -245,7 +227,6 @@ Endpoints
|
||||
"active": True,
|
||||
"expires": None,
|
||||
"customer": None,
|
||||
"linked_orderpositions": [],
|
||||
"linked_orderposition": None,
|
||||
"linked_giftcard": None,
|
||||
"notes": None,
|
||||
@@ -270,7 +251,6 @@ Endpoints
|
||||
"active": True,
|
||||
"expires": None,
|
||||
"customer": None,
|
||||
"linked_orderpositions": [],
|
||||
"linked_orderposition": None,
|
||||
"linked_giftcard": None,
|
||||
"notes": None,
|
||||
@@ -278,7 +258,7 @@ Endpoints
|
||||
}
|
||||
|
||||
:param organizer: The ``slug`` field of the organizer to create a medium for
|
||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderpositions"``, or ``"customer"``, the respective
|
||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"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
|
||||
@@ -307,7 +287,7 @@ Endpoints
|
||||
Content-Length: 94
|
||||
|
||||
{
|
||||
"linked_orderpositions": [13, 29]
|
||||
"linked_orderposition": 13
|
||||
}
|
||||
|
||||
**Example response**:
|
||||
@@ -328,8 +308,7 @@ Endpoints
|
||||
"active": True,
|
||||
"expires": None,
|
||||
"customer": None,
|
||||
"linked_orderpositions": [13, 29],
|
||||
"linked_orderposition": None,
|
||||
"linked_orderposition": 13,
|
||||
"linked_giftcard": None,
|
||||
"notes": None,
|
||||
"info": {}
|
||||
@@ -337,7 +316,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_orderpositions"``, or ``"customer"``, the respective
|
||||
:query string expand: If you pass ``"linked_giftcard"``, ``"linked_orderposition"``, oder ``"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
|
||||
|
||||
@@ -65,7 +65,7 @@ Backend
|
||||
.. automodule:: pretix.control.signals
|
||||
:members: nav_event, html_head, html_page_start, quota_detail_html, nav_topbar, nav_global, nav_organizer, nav_event_settings,
|
||||
order_info, order_approve_info, event_settings_widget, oauth_application_registered,
|
||||
order_position_buttons, subevent_forms, item_formsets, order_search_filter_q, order_search_forms, subevent_detail_html
|
||||
order_position_buttons, subevent_forms, item_formsets, order_search_filter_q, order_search_forms
|
||||
|
||||
.. automodule:: pretix.base.signals
|
||||
:no-index:
|
||||
|
||||
@@ -29,8 +29,8 @@ classifiers = [
|
||||
dependencies = [
|
||||
"arabic-reshaper==3.0.1", # Support for Arabic in reportlab
|
||||
"babel",
|
||||
"BeautifulSoup4==4.15.*",
|
||||
"bleach==6.4.*",
|
||||
"BeautifulSoup4==4.14.*",
|
||||
"bleach==6.3.*",
|
||||
"celery==5.6.*",
|
||||
"chardet==5.2.*",
|
||||
"cryptography>=48.0.0",
|
||||
@@ -108,7 +108,7 @@ dependencies = [
|
||||
[project.optional-dependencies]
|
||||
memcached = ["pylibmc"]
|
||||
dev = [
|
||||
"aiohttp==3.14.*",
|
||||
"aiohttp==3.13.*",
|
||||
"coverage",
|
||||
"coveralls",
|
||||
"fakeredis==2.36.*",
|
||||
|
||||
@@ -66,14 +66,13 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
expand_nested = self.context['request'].query_params.getlist('expand')
|
||||
|
||||
if 'linked_giftcard' in expand_nested:
|
||||
if 'linked_giftcard' in self.context['request'].query_params.getlist('expand'):
|
||||
if not self.context["can_read_giftcards"]:
|
||||
raise PermissionDenied("No permission to access gift card details.")
|
||||
|
||||
self.fields['linked_giftcard'] = NestedGiftCardSerializer(read_only=True, context=self.context)
|
||||
if 'linked_giftcard.owner_ticket' in expand_nested:
|
||||
if 'linked_giftcard.owner_ticket' in self.context['request'].query_params.getlist('expand'):
|
||||
self.fields['linked_giftcard'].fields['owner_ticket'] = NestedOrderPositionSerializer(read_only=True, context=self.context)
|
||||
else:
|
||||
self.fields['linked_giftcard'] = serializers.PrimaryKeyRelatedField(
|
||||
@@ -82,27 +81,17 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
queryset=self.context['organizer'].issued_gift_cards.all()
|
||||
)
|
||||
|
||||
# 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
|
||||
)
|
||||
if 'linked_orderposition' in self.context['request'].query_params.getlist('expand'):
|
||||
# Permission Check performed in to_representation
|
||||
self.fields['linked_orderposition'] = NestedOrderPositionSerializer(read_only=True)
|
||||
else:
|
||||
self.fields['linked_orderpositions'] = serializers.PrimaryKeyRelatedField(
|
||||
many=True,
|
||||
self.fields['linked_orderposition'] = serializers.PrimaryKeyRelatedField(
|
||||
required=False,
|
||||
allow_null=True,
|
||||
queryset=OrderPosition.all.filter(order__event__organizer=self.context['organizer']),
|
||||
)
|
||||
|
||||
if 'customer' in expand_nested:
|
||||
if 'customer' in self.context['request'].query_params.getlist('expand'):
|
||||
if not self.context["can_read_customers"]:
|
||||
raise PermissionDenied("No permission to access customer details.")
|
||||
|
||||
@@ -117,21 +106,6 @@ 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 and 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 []
|
||||
del data['linked_orderposition']
|
||||
|
||||
if 'type' in data and 'identifier' in data:
|
||||
qs = self.context['organizer'].reusable_media.filter(
|
||||
identifier=data['identifier'], type=data['type']
|
||||
@@ -147,28 +121,14 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
def to_representation(self, instance):
|
||||
r = super().to_representation(instance)
|
||||
request = self.context.get('request')
|
||||
|
||||
ops = r.get('linked_orderpositions', [])
|
||||
# late permission evaluations for checks that depend on the actual linked events
|
||||
expand_nested = self.context['request'].query_params.getlist('expand')
|
||||
perm_holder = request.auth if isinstance(request.auth, (Device, TeamAPIToken)) else request.user
|
||||
if ops and 'linked_orderposition' in expand_nested or 'linked_orderpositions' in expand_nested:
|
||||
ops_noperm = []
|
||||
for lop in instance.linked_orderpositions.all():
|
||||
event = lop.order.event
|
||||
if 'linked_orderposition' in expand_nested:
|
||||
if instance.linked_orderposition is not None:
|
||||
event = instance.linked_orderposition.order.event
|
||||
if not perm_holder.has_event_permission(event.organizer, event, 'event.orders:read', request):
|
||||
ops_noperm.append(lop.id)
|
||||
if ops_noperm:
|
||||
ops = [
|
||||
{'id': op['id']} if op['id'] in ops_noperm
|
||||
else op
|
||||
for op in ops
|
||||
]
|
||||
r['linked_orderpositions'] = ops
|
||||
|
||||
# add linked_orderposition (singular) for backwards compatibility
|
||||
if len(ops) < 2:
|
||||
r['linked_orderposition'] = ops[0] if ops else None
|
||||
r['linked_orderposition'] = {'id': instance.linked_orderposition.id}
|
||||
|
||||
if 'linked_giftcard.owner_ticket' in expand_nested:
|
||||
gc = instance.linked_giftcard
|
||||
@@ -188,12 +148,10 @@ class ReusableMediaSerializer(I18nAwareModelSerializer):
|
||||
'updated',
|
||||
'type',
|
||||
'identifier',
|
||||
'claim_token',
|
||||
'label',
|
||||
'active',
|
||||
'expires',
|
||||
'customer',
|
||||
'linked_orderpositions',
|
||||
'linked_orderposition',
|
||||
'linked_giftcard',
|
||||
'info',
|
||||
'notes',
|
||||
|
||||
@@ -1043,15 +1043,13 @@ 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', 'add_to_reusable_medium', 'discount')
|
||||
'requested_valid_from', 'use_reusable_medium', 'discount')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
@@ -1063,8 +1061,6 @@ 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():
|
||||
@@ -1080,9 +1076,6 @@ 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(
|
||||
@@ -1156,13 +1149,6 @@ 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
|
||||
|
||||
|
||||
@@ -1602,7 +1588,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 not in ('answers', '_quotas', 'use_reusable_medium', 'add_to_reusable_medium')})
|
||||
pos = OrderPosition(**{k: v for k, v in pos_data.items() if k != 'answers' and k != '_quotas' and k != 'use_reusable_medium'})
|
||||
if simulate:
|
||||
pos.order = order._wrapped
|
||||
else:
|
||||
@@ -1676,7 +1662,6 @@ 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)
|
||||
|
||||
@@ -1718,25 +1703,10 @@ class OrderCreateSerializer(I18nAwareModelSerializer):
|
||||
answ.options.add(*options)
|
||||
|
||||
if use_reusable_medium:
|
||||
for op_pk in use_reusable_medium.linked_orderpositions.values_list('pk', flat=True):
|
||||
use_reusable_medium.log_action(
|
||||
'pretix.reusable_medium.linked_orderposition.removed',
|
||||
data={
|
||||
'linked_orderposition': op_pk,
|
||||
}
|
||||
)
|
||||
use_reusable_medium.linked_orderpositions.set([pos])
|
||||
use_reusable_medium.linked_orderposition = pos
|
||||
use_reusable_medium.save(update_fields=['linked_orderposition'])
|
||||
use_reusable_medium.log_action(
|
||||
'pretix.reusable_medium.linked_orderposition.added',
|
||||
data={
|
||||
'by_order': order.code,
|
||||
'linked_orderposition': pos.pk,
|
||||
}
|
||||
)
|
||||
elif add_to_reusable_medium:
|
||||
add_to_reusable_medium.linked_orderpositions.add(pos)
|
||||
add_to_reusable_medium.log_action(
|
||||
'pretix.reusable_medium.linked_orderposition.added',
|
||||
'pretix.reusable_medium.linked_orderposition.changed',
|
||||
data={
|
||||
'by_order': order.code,
|
||||
'linked_orderposition': pos.pk,
|
||||
|
||||
@@ -491,7 +491,6 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
)
|
||||
raw_barcode_for_checkin = None
|
||||
from_revoked_secret = False
|
||||
reusable_medium_used = None
|
||||
if simulate:
|
||||
common_checkin_args['__fake_arg_to_prevent_this_from_being_saved'] = True
|
||||
|
||||
@@ -522,12 +521,11 @@ 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.active().filter(
|
||||
Exists(ReusableMedium.linked_orderpositions.through.objects.filter(reusablemedium_id=OuterRef('pk')))
|
||||
).get(
|
||||
media = ReusableMedium.objects.select_related('linked_orderposition').active().get(
|
||||
organizer_id=checkinlists[0].event.organizer_id,
|
||||
type=source_type,
|
||||
identifier=raw_barcode,
|
||||
linked_orderposition__isnull=False,
|
||||
)
|
||||
raw_barcode_for_checkin = raw_barcode
|
||||
except ReusableMedium.DoesNotExist:
|
||||
@@ -630,9 +628,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'list': MiniCheckinListSerializer(list_by_event[revoked_matches[0].event_id]).data,
|
||||
}, status=400)
|
||||
else:
|
||||
linked_ops = media.linked_orderpositions.all().select_related("order").prefetch_related("addons")
|
||||
linked_event_ids = {op.order.event_id for op in linked_ops}
|
||||
if not any(event_id in list_by_event for event_id in linked_event_ids):
|
||||
if media.linked_orderposition.order.event_id not in list_by_event:
|
||||
# Medium exists but connected ticket is for the wrong event
|
||||
if not simulate:
|
||||
checkinlists[0].event.log_action('pretix.event.checkin.unknown', data={
|
||||
@@ -658,91 +654,28 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'checkin_texts': [],
|
||||
'list': MiniCheckinListSerializer(checkinlists[0]).data,
|
||||
}, status=404)
|
||||
op_candidates = []
|
||||
for op in linked_ops:
|
||||
if op.order.event_id in list_by_event:
|
||||
reusable_medium_used = media
|
||||
op_candidates.append(op)
|
||||
if list_by_event[op.order.event_id].addon_match:
|
||||
op_candidates += list(op.addons.all())
|
||||
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())
|
||||
|
||||
# 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 multiple linked_orderpositions or the ``addon_match`` case
|
||||
# here and need to figure out which op has the right product. This basically is a valid-for-checkin-test on every op.
|
||||
# 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()})
|
||||
)
|
||||
]
|
||||
|
||||
if not reusable_medium_used:
|
||||
# 3a. First, we clean up that we made an imprecise query above. If a scan is made for multiple check-in lists,
|
||||
# we have queried ``addon_to__secret=raw_barcode``, even if some of the lists in question do not allow addon
|
||||
# matching. So we accept all candidates that match one of these cases:
|
||||
# - Exactly the ticket secret we scanned (because that's always a possible result)
|
||||
# - Exactly the ticket pk we scanned (on legacy endpoints)
|
||||
# - An add-on on a list that allows add-on matching
|
||||
# This is not necessary when a reusable media was used, since in that case we already obeyed list.addon_match
|
||||
# correctly above.
|
||||
op_candidates_filtered = [
|
||||
op for op in op_candidates
|
||||
if (
|
||||
op.secret == raw_barcode or
|
||||
list_by_event[op.order.event_id].addon_match or
|
||||
(str(op.pk) == raw_barcode and legacy_url_support and not untrusted_input)
|
||||
)
|
||||
]
|
||||
else:
|
||||
op_candidates_filtered = op_candidates
|
||||
|
||||
if len(op_candidates_filtered) > 1:
|
||||
# 3b. If we still have multiple candidates, we filter by product based on the check-in list configuration.
|
||||
# This is relevant for the addon_match scenario where the scanned ticket has multiple add-ons, but only
|
||||
# one is contained in the check-in list used to scan. It makes sense to filter this first, since it is a
|
||||
# "static" check, i.e. scanning the same QR code on the same check-in list will always do the same, no matter
|
||||
# when I scan it, and it is "intentional" filtering in the sense that the admin configured this behaviour
|
||||
# into the check-in list.
|
||||
op_candidates_filtered = [
|
||||
op for op in op_candidates_filtered
|
||||
if 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()}
|
||||
]
|
||||
|
||||
if len(op_candidates_filtered) > 1:
|
||||
# 3c. If we still have multiple candidates, we filter by validity date. This was introduced for the case where
|
||||
# a reusable media refers to two tickets, one currently valid and one expired or in the future. Howeer,
|
||||
# it could in theory also happen with two add-ons being on the same check-in list but without overlapping
|
||||
# validity. It makes sense to filter this "after" the previous checks since it is not "intentional" filtering
|
||||
# configured by the admin but "accidental" filtering that depends on the time of execution.
|
||||
op_candidates_filtered = [
|
||||
op for op in op_candidates_filtered
|
||||
if (
|
||||
(not op.valid_from or op.valid_from <= datetime) and
|
||||
(not op.valid_until or op.valid_until > datetime)
|
||||
)
|
||||
]
|
||||
|
||||
if len(op_candidates_filtered) == 0:
|
||||
# None of the ops is valid today or has the correct product, too bad! We could just error out here, but
|
||||
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
|
||||
# instead we just continue with *any* product and have it rejected by the check in perform_checkin.
|
||||
# To improve the error message, we select the op that will "work next" or - if none matches - "worked last".
|
||||
op_candidate = None
|
||||
for op in op_candidates:
|
||||
if (
|
||||
op.valid_from and op.valid_from > datetime and
|
||||
(not op_candidate or op.valid_from < op_candidate.valid_from)
|
||||
):
|
||||
op_candidate = op
|
||||
|
||||
if not op_candidate:
|
||||
# no candidate in the future, get closest in the past
|
||||
for op in op_candidates:
|
||||
if (
|
||||
op.valid_until and op.valid_until < datetime and
|
||||
(not op_candidate or op.valid_until > op_candidate.valid_until)
|
||||
):
|
||||
op_candidate = op
|
||||
|
||||
if not op_candidate:
|
||||
op_candidate = op_candidates[0]
|
||||
|
||||
op_candidates = [op_candidate]
|
||||
elif len(op_candidates_filtered) > 1:
|
||||
# This has the advantage of a better error message.
|
||||
op_candidates = [op_candidates[0]]
|
||||
elif len(op_candidates_matching_product) > 1:
|
||||
# It's still ambiguous, we'll error out.
|
||||
# We choose the first match (regardless of product) for the logging since it's most likely to be the
|
||||
# base product according to our order_by above.
|
||||
@@ -776,7 +709,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
'list': MiniCheckinListSerializer(list_by_event[op.order.event_id]).data,
|
||||
}, status=400)
|
||||
else:
|
||||
op_candidates = op_candidates_filtered
|
||||
op_candidates = op_candidates_matching_product
|
||||
|
||||
op = op_candidates[0]
|
||||
common_checkin_args['list'] = list_by_event[op.order.event_id]
|
||||
@@ -788,10 +721,7 @@ def _redeem_process(*, checkinlists, raw_barcode, answers_data, datetime, force,
|
||||
if str(q.pk) in answers_data:
|
||||
try:
|
||||
if q.type == Question.TYPE_FILE:
|
||||
if answers_data[str(q.pk)]:
|
||||
given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
|
||||
else:
|
||||
given_answers[q] = None
|
||||
given_answers[q] = _handle_file_upload(answers_data[str(q.pk)], user, auth)
|
||||
else:
|
||||
given_answers[q] = q.clean_answer(answers_data[str(q.pk)])
|
||||
except (ValidationError, BaseValidationError):
|
||||
|
||||
@@ -53,12 +53,10 @@ 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_orderpositions', 'linked_giftcard']
|
||||
fields = ['identifier', 'type', 'active', 'customer', 'linked_orderposition', 'linked_giftcard']
|
||||
|
||||
|
||||
class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||
@@ -77,7 +75,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_orderpositions',
|
||||
'linked_orderposition',
|
||||
queryset=OrderPosition.objects.select_related(
|
||||
'order', 'order__event', 'order__event__organizer', 'seat',
|
||||
).prefetch_related(
|
||||
@@ -119,38 +117,14 @@ class ReusableMediaViewSet(viewsets.ModelViewSet):
|
||||
|
||||
@transaction.atomic()
|
||||
def perform_update(self, serializer):
|
||||
rm = ReusableMedium.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
||||
prev_linked_ops_pks = list(rm.linked_orderpositions.values_list("pk", flat=True))
|
||||
ReusableMedium.objects.select_for_update(of=OF_SELF).get(pk=self.get_object().pk)
|
||||
inst = serializer.save(identifier=serializer.instance.identifier, type=serializer.instance.type)
|
||||
linked_ops_pks = inst.linked_orderpositions.values_list("pk", flat=True)
|
||||
for op_pk in prev_linked_ops_pks:
|
||||
if op_pk not in linked_ops_pks:
|
||||
inst.log_action(
|
||||
'pretix.reusable_medium.linked_orderposition.removed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'linked_orderposition': op_pk,
|
||||
}
|
||||
)
|
||||
for op_pk in linked_ops_pks:
|
||||
if op_pk not in prev_linked_ops_pks:
|
||||
inst.log_action(
|
||||
'pretix.reusable_medium.linked_orderposition.added',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data={
|
||||
'linked_orderposition': op_pk,
|
||||
}
|
||||
)
|
||||
data = {k: v for k, v in self.request.data.items() if k not in ('linked_orderposition', 'linked_orderpositions')}
|
||||
if data:
|
||||
inst.log_action(
|
||||
'pretix.reusable_medium.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=data,
|
||||
)
|
||||
inst.log_action(
|
||||
'pretix.reusable_medium.changed',
|
||||
user=self.request.user,
|
||||
auth=self.request.auth,
|
||||
data=self.request.data,
|
||||
)
|
||||
return inst
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
@@ -183,6 +157,7 @@ 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})
|
||||
|
||||
@@ -194,7 +194,7 @@ with scopes_disabled():
|
||||
)
|
||||
).values('id')
|
||||
|
||||
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderpositions__order_id', flat=True)
|
||||
matching_media = ReusableMedium.objects.filter(identifier=u).values_list('linked_orderposition__order_id', flat=True)
|
||||
|
||||
mainq = (
|
||||
code
|
||||
@@ -1034,7 +1034,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_orderpositions', flat=True)
|
||||
matching_media = ReusableMedium.objects.filter(identifier=value).values_list('linked_orderposition', flat=True)
|
||||
return queryset.filter(
|
||||
Q(secret__istartswith=value)
|
||||
| Q(attendee_name_cached__icontains=value)
|
||||
|
||||
@@ -20,13 +20,12 @@
|
||||
# <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 OrderPosition, ReusableMedium
|
||||
from ..models import ReusableMedium
|
||||
from ..signals import register_multievent_data_exporters
|
||||
|
||||
|
||||
@@ -45,9 +44,7 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
media = ReusableMedium.objects.filter(
|
||||
organizer=self.organizer,
|
||||
).select_related(
|
||||
'customer', 'linked_giftcard',
|
||||
).prefetch_related(
|
||||
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order"))
|
||||
'customer', 'linked_orderposition', 'linked_giftcard',
|
||||
).order_by('created')
|
||||
|
||||
headers = [
|
||||
@@ -65,16 +62,17 @@ class ReusableMediaExporter(OrganizerLevelExportMixin, ListExporter):
|
||||
yield self.ProgressSetTotal(total=media.count())
|
||||
|
||||
for medium in media.iterator(chunk_size=1000):
|
||||
yield [
|
||||
row = [
|
||||
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 '',
|
||||
', '.join([f"{op.order.code}-{op.positionid}" for op in medium.linked_orderpositions.all()]),
|
||||
f"{medium.linked_orderposition.order.code}-{medium.linked_orderposition.positionid}" if medium.linked_orderposition_id else '',
|
||||
medium.linked_giftcard.secret if medium.linked_giftcard_id else '',
|
||||
medium.notes,
|
||||
]
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
return f'{self.organizer.slug}_media'
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Generated by Django 4.2.26 on 2025-11-24 11:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("pretixbase", "0299_itemprogramtime_location"),
|
||||
]
|
||||
|
||||
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;",
|
||||
),
|
||||
]
|
||||
@@ -1,44 +0,0 @@
|
||||
# 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", "0300_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"
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -72,16 +72,6 @@ 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'),
|
||||
@@ -99,14 +89,12 @@ class ReusableMedium(LoggedModel):
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_('Customer account'),
|
||||
)
|
||||
linked_orderpositions = models.ManyToManyField(
|
||||
linked_orderposition = models.ForeignKey(
|
||||
OrderPosition,
|
||||
null=True, blank=True,
|
||||
related_name='linked_media',
|
||||
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.'
|
||||
)
|
||||
on_delete=models.SET_NULL,
|
||||
verbose_name=_('Linked ticket'),
|
||||
)
|
||||
linked_giftcard = models.ForeignKey(
|
||||
GiftCard,
|
||||
|
||||
@@ -83,7 +83,8 @@ from pretix.base.templatetags.money import money_filter
|
||||
from pretix.base.templatetags.phone_format import phone_format
|
||||
from pretix.helpers.daterange import datetimerange
|
||||
from pretix.helpers.reportlab import (
|
||||
ThumbnailingImageReader, register_ttf_font_if_new, reshaper,
|
||||
ThumbnailingImageReader, find_font_supporting_text,
|
||||
register_ttf_font_if_new, reshaper,
|
||||
)
|
||||
from pretix.presale.style import get_fonts
|
||||
|
||||
@@ -805,7 +806,10 @@ class Renderer:
|
||||
else:
|
||||
self.bg_bytes = None
|
||||
self.bg_pdf = None
|
||||
self.event_fonts = list(get_fonts(event, pdf_support_required=True).keys()) + ['Open Sans']
|
||||
|
||||
event_fonts = get_fonts(event, pdf_support_required=True) | {'Open Sans': {"bold", "italic", "bolditalic"}}
|
||||
# sorted by font name to match ordering of libpretixprint
|
||||
self.event_fonts = dict(sorted(event_fonts.items(), key=lambda x: x[0]))
|
||||
|
||||
@classmethod
|
||||
def _register_fonts(cls, event: Event = None):
|
||||
@@ -1004,7 +1008,25 @@ class Renderer:
|
||||
)
|
||||
canvas.restoreState()
|
||||
|
||||
def _text_paragraph(self, op: OrderPosition, order: Order, o: dict, legacy_lineheight=False, override_fontsize=None):
|
||||
def _prepare_text_paragraph_text(self, op: OrderPosition, order: Order, o: dict):
|
||||
# add an almost-invisible space   after hyphens as word-wrap in ReportLab only works on space chars
|
||||
text = conditional_escape(
|
||||
self._get_text_content(op, order, o) or "",
|
||||
).replace("\n", "<br/>\n").replace("-", "- ")
|
||||
|
||||
# reportlab does not support unicode combination characters
|
||||
# It's important we do this before we use ArabicReshaper
|
||||
text = unicodedata.normalize("NFC", text)
|
||||
|
||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||
try:
|
||||
text = "<br/>".join(get_display(reshaper.reshape(l)) for l in text.split("<br/>"))
|
||||
except:
|
||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||
return text
|
||||
|
||||
def _get_text_paragraph_font(self, o: dict, text: str):
|
||||
font = o['fontfamily']
|
||||
|
||||
# Since pdfmetrics.registerFont is global, we want to make sure that no one tries to sneak in a font, they
|
||||
@@ -1018,6 +1040,14 @@ class Renderer:
|
||||
if o['italic']:
|
||||
font += ' I'
|
||||
|
||||
font = find_font_supporting_text(self.event_fonts, text, font)
|
||||
|
||||
return font
|
||||
|
||||
def _text_paragraph(self, op: OrderPosition, order: Order, o: dict, legacy_lineheight=False, override_fontsize=None):
|
||||
text = self._prepare_text_paragraph_text(op, order, o)
|
||||
font = self._get_text_paragraph_font(o, text)
|
||||
|
||||
fontsize = override_fontsize if override_fontsize is not None else float(o['fontsize'])
|
||||
try:
|
||||
ad = getAscentDescent(font, fontsize)
|
||||
@@ -1046,21 +1076,6 @@ class Renderer:
|
||||
alignment=align_map[o['align']],
|
||||
splitLongWords=o.get('splitlongwords', True),
|
||||
)
|
||||
# add an almost-invisible space   after hyphens as word-wrap in ReportLab only works on space chars
|
||||
text = conditional_escape(
|
||||
self._get_text_content(op, order, o) or "",
|
||||
).replace("\n", "<br/>\n").replace("-", "- ")
|
||||
|
||||
# reportlab does not support unicode combination characters
|
||||
# It's important we do this before we use ArabicReshaper
|
||||
text = unicodedata.normalize("NFC", text)
|
||||
|
||||
# reportlab does not support RTL, ligature-heavy scripts like Arabic. Therefore, we use ArabicReshaper
|
||||
# to resolve all ligatures and python-bidi to switch RTL texts.
|
||||
try:
|
||||
text = "<br/>".join(get_display(reshaper.reshape(l)) for l in text.split("<br/>"))
|
||||
except:
|
||||
logger.exception('Reshaping/Bidi fixes failed on string {}'.format(repr(text)))
|
||||
|
||||
p = Paragraph(text, style=style)
|
||||
return p, ad, lineheight
|
||||
|
||||
@@ -3515,8 +3515,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={
|
||||
|
||||
@@ -1871,7 +1871,7 @@ class ReusableMediaFilterForm(FilterForm):
|
||||
Q(identifier__icontains=query)
|
||||
| Q(customer__identifier__icontains=query)
|
||||
| Q(customer__external_identifier__istartswith=query)
|
||||
| Q(linked_orderpositions__order__code__icontains=query)
|
||||
| Q(linked_orderposition__order__code__icontains=query)
|
||||
| Q(linked_giftcard__secret__icontains=query)
|
||||
)
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ from pretix.control.forms import ExtFileField, SplitDateTimeField
|
||||
from pretix.control.forms.event import (
|
||||
SafeEventMultipleChoiceField, multimail_validate,
|
||||
)
|
||||
from pretix.control.forms.widgets import Select2, Select2Multiple
|
||||
from pretix.control.forms.widgets import Select2
|
||||
from pretix.multidomain.models import KnownDomain
|
||||
from pretix.multidomain.urlreverse import build_absolute_uri
|
||||
|
||||
@@ -249,15 +249,6 @@ 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)
|
||||
|
||||
def label_from_instance(self, op):
|
||||
return f'{op.order.code}-{op.positionid} ({str(op.item) + ((" - " + str(op.variation)) if op.variation else "")})'
|
||||
|
||||
|
||||
class EventMetaPropertyForm(I18nModelForm):
|
||||
class Meta:
|
||||
model = EventMetaProperty
|
||||
@@ -972,12 +963,12 @@ class ReusableMediumUpdateForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = ReusableMedium
|
||||
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderpositions', 'notes']
|
||||
fields = ['active', 'expires', 'customer', 'linked_giftcard', 'linked_orderposition', 'notes']
|
||||
field_classes = {
|
||||
'expires': SplitDateTimeField,
|
||||
'customer': SafeModelChoiceField,
|
||||
'linked_giftcard': SafeModelChoiceField,
|
||||
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
|
||||
'linked_orderposition': SafeOrderPositionChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'expires': SplitDateTimePickerWidget,
|
||||
@@ -987,8 +978,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
organizer = self.instance.organizer
|
||||
|
||||
self.fields['linked_orderpositions'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
|
||||
self.fields['linked_orderpositions'].widget = Select2Multiple(
|
||||
self.fields['linked_orderposition'].queryset = OrderPosition.all.filter(order__event__organizer=organizer).all()
|
||||
self.fields['linked_orderposition'].widget = Select2(
|
||||
attrs={
|
||||
'data-model-select2': 'generic',
|
||||
'data-select2-url': reverse('control:organizer.ticket_select2', kwargs={
|
||||
@@ -996,8 +987,8 @@ class ReusableMediumUpdateForm(forms.ModelForm):
|
||||
}),
|
||||
}
|
||||
)
|
||||
self.fields['linked_orderpositions'].widget.choices = self.fields['linked_orderpositions'].choices
|
||||
self.fields['linked_orderpositions'].required = False
|
||||
self.fields['linked_orderposition'].widget.choices = self.fields['linked_orderposition'].choices
|
||||
self.fields['linked_orderposition'].required = False
|
||||
|
||||
self.fields['linked_giftcard'].queryset = organizer.issued_gift_cards.all()
|
||||
self.fields['linked_giftcard'].widget = Select2(
|
||||
@@ -1051,12 +1042,12 @@ class ReusableMediumCreateForm(ReusableMediumUpdateForm):
|
||||
|
||||
class Meta:
|
||||
model = ReusableMedium
|
||||
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderpositions', 'linked_giftcard', 'customer', 'notes']
|
||||
fields = ['active', 'type', 'identifier', 'expires', 'linked_orderposition', 'linked_giftcard', 'customer', 'notes']
|
||||
field_classes = {
|
||||
'expires': SplitDateTimeField,
|
||||
'customer': SafeModelChoiceField,
|
||||
'linked_giftcard': SafeModelChoiceField,
|
||||
'linked_orderpositions': SafeOrderPositionMultipleChoiceField,
|
||||
'linked_orderposition': SafeOrderPositionChoiceField,
|
||||
}
|
||||
widgets = {
|
||||
'expires': SplitDateTimePickerWidget,
|
||||
|
||||
@@ -743,8 +743,6 @@ 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.removed': _('A ticket has been removed from 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.'),
|
||||
|
||||
@@ -213,16 +213,6 @@ quota as argument in the ``quota`` keyword argument.
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
subevent_detail_html = EventPluginSignal()
|
||||
"""
|
||||
Arguments: 'subevent'
|
||||
|
||||
This signal allows you to append HTML to a SubEvent's detail view. You receive the
|
||||
subevent as argument in the ``subevent`` keyword argument.
|
||||
|
||||
As with all event plugin signals, the ``sender`` keyword argument will contain the event.
|
||||
"""
|
||||
|
||||
organizer_edit_tabs = DeprecatedSignal()
|
||||
"""
|
||||
Arguments: 'organizer', 'request'
|
||||
|
||||
@@ -9,24 +9,23 @@
|
||||
<h3 class="panel-title">{% trans "Go offline" %}</h3>
|
||||
</div>
|
||||
<div class="row panel-body">
|
||||
<div class="col-sm-12 col-lg-6">
|
||||
<p>
|
||||
<div class="col-sm-12 col-md-9 nomargin-bottom">
|
||||
{% blocktrans trimmed %}
|
||||
You can take your event offline. Nobody except your team will be able to see or access it any more.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
<form class="col-sm-12 col-lg-6 text-right"
|
||||
action="{% url "control:event.live" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="live" value="false">
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-3">
|
||||
<form action="{% url "control:event.live" event=request.event.slug organizer=request.organizer.slug %}"
|
||||
method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="live" value="false">
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
<span class="fa fa-power-off"></span>
|
||||
{% trans "Go offline" %}
|
||||
</button>
|
||||
</form>
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||
<span class="fa fa-power-off"></span>
|
||||
{% trans "Go offline" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,24 +34,22 @@
|
||||
<h3 class="panel-title">{% trans "Cancel event" %}</h3>
|
||||
</div>
|
||||
<div class="row panel-body">
|
||||
<div class="col-sm-12 col-lg-6">
|
||||
<p>
|
||||
<div class="col-sm-12 col-md-9 nomargin-bottom">
|
||||
{% blocktrans trimmed %}
|
||||
If you need to call off your event you want to cancel and refund all tickets, you can do so through
|
||||
this option.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12 col-lg-6 text-right">
|
||||
<a href="{% url "control:event.cancel" organizer=request.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-danger btn-lg pull-right {% if "event:cancel" not in request.eventpermset %}disabled{% endif %}">
|
||||
<span class="fa fa-ban"></span>
|
||||
{% if "event:cancel" in request.eventpermset %}
|
||||
<div class="col-sm-12 col-md-3 text-center">
|
||||
{% if "event:cancel" in request.eventpermset %}
|
||||
<a href="{% url "control:event.cancel" organizer=request.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-danger btn-block btn-lg">
|
||||
<span class="fa fa-ban"></span>
|
||||
{% trans "Cancel event" %}
|
||||
{% else %}
|
||||
{% trans "No permission" %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</a>
|
||||
{% else %}
|
||||
{% trans "No permission" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -62,16 +59,15 @@
|
||||
<h3 class="panel-title">{% trans "Delete personal data" %}</h3>
|
||||
</div>
|
||||
<div class="row panel-body">
|
||||
<div class="col-sm-12 col-lg-6">
|
||||
<p>
|
||||
<div class="col-sm-12 col-md-9 nomargin-bottom">
|
||||
{% blocktrans trimmed %}
|
||||
You can remove personal data such as names and email addresses from your event and only retain the
|
||||
financial information such as the number and type of tickets sold.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12 col-lg-6 text-right">
|
||||
<a href="{% url "control:event.shredder.start" event=request.event.slug organizer=request.organizer.slug %}" class="btn btn-danger btn-lg">
|
||||
<div class="col-sm-12 col-md-3">
|
||||
<a href="
|
||||
{% url "control:event.shredder.start" event=request.event.slug organizer=request.organizer.slug %}" class="btn btn-danger btn-lg btn-block">
|
||||
<span class="fa fa-eraser"></span>
|
||||
{% trans "Delete personal data" %}
|
||||
</a>
|
||||
@@ -84,17 +80,15 @@
|
||||
<h3 class="panel-title">{% trans "Delete event" %}</h3>
|
||||
</div>
|
||||
<div class="row panel-body">
|
||||
<div class="col-sm-12 col-lg-6">
|
||||
<p>
|
||||
<div class="col-sm-12 col-md-9 nomargin-bottom">
|
||||
{% blocktrans trimmed %}
|
||||
You can delete your event completely only as long as it does not contain any undeletable data, such as
|
||||
orders not performed in test mode.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-12 col-lg-6 text-right">
|
||||
<div class="col-sm-12 col-md-3">
|
||||
<a href="{% url "control:event.delete" organizer=request.organizer.slug event=request.event.slug %}"
|
||||
class="btn btn-danger btn-lg {% if not request.event.allow_delete %}disabled{% endif %}">
|
||||
class="btn btn-danger btn-block btn-lg {% if not request.event.allow_delete %}disabled{% endif %}">
|
||||
<span class="fa fa-trash"></span>
|
||||
{% trans "Delete event" %}
|
||||
</a>
|
||||
|
||||
@@ -58,8 +58,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' '-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>
|
||||
<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>
|
||||
<th>{% trans "Connections" context "reusable_media" %}</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
@@ -90,13 +90,13 @@
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% for op in m.linked_orderpositions.all %}
|
||||
{% if m.linked_orderposition %}
|
||||
<span class="helper-display-block">
|
||||
<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 }}
|
||||
<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 }}
|
||||
</span>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if m.linked_giftcard %}
|
||||
<span class="helper-display-block">
|
||||
<span class="fa fa-credit-card fa-fw"></span>
|
||||
|
||||
@@ -26,19 +26,7 @@
|
||||
<dt>{% trans "Media type" context "reusable_media" %}</dt>
|
||||
<dd>{{ medium.get_type_display }}</dd>
|
||||
<dt>{% trans "Identifier" context "reusable_media" %}</dt>
|
||||
<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>
|
||||
<dd><code>{{ medium.identifier }}</code></dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>
|
||||
{% if not medium.active %}
|
||||
@@ -53,34 +41,34 @@
|
||||
<dd>
|
||||
{% if medium.customer %}
|
||||
<span class="helper-display-block">
|
||||
<span class="fa fa-user fa-fw"></span>
|
||||
{% if "organizer.customers:read" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=medium.customer.identifier %}">
|
||||
{{ medium.customer }}
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="fa fa-user fa-fw"></span>
|
||||
{% if "organizer.customers:read" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.customer" organizer=request.organizer.slug customer=medium.customer.identifier %}">
|
||||
{{ medium.customer }}
|
||||
{% endif %}
|
||||
</span>
|
||||
</a>
|
||||
{% else %}
|
||||
{{ medium.customer }}
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% for op in medium.linked_orderpositions.all %}
|
||||
{% if medium.linked_orderposition %}
|
||||
<span class="helper-display-block">
|
||||
<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 %}
|
||||
<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 %}
|
||||
{% if medium.linked_giftcard %}
|
||||
<span class="helper-display-block">
|
||||
<span class="fa fa-credit-card fa-fw"></span>
|
||||
{% if "organizer.giftcards:read" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=medium.linked_giftcard.id %}">
|
||||
{{ medium.linked_giftcard.secret }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ medium.linked_giftcard.secret|slice:":3" }}…
|
||||
{% endif %}
|
||||
</span>
|
||||
<span class="fa fa-credit-card fa-fw"></span>
|
||||
{% if "organizer.giftcards:read" in request.orgapermset %}
|
||||
<a href="{% url "control:organizer.giftcard" organizer=request.organizer.slug giftcard=medium.linked_giftcard.id %}">
|
||||
{{ medium.linked_giftcard.secret }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ medium.linked_giftcard.secret|slice:":3" }}…
|
||||
{% endif %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
{% if medium.notes %}
|
||||
|
||||
@@ -4,306 +4,290 @@
|
||||
{% load formset_tags %}
|
||||
{% load eventsignal %}
|
||||
{% load static %}
|
||||
{% load money %}
|
||||
{% load icon %}
|
||||
{% block title %}{% blocktrans trimmed with name=subevent.name context "subevent" %}Date: {{ name }}
|
||||
{% endblocktrans %}{% endblock %}
|
||||
{% block title %}{% trans "Date" context "subevent" %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1>
|
||||
{% blocktrans trimmed with name=subevent.name context "subevent" %}Date: {{ name }}{% endblocktrans %}
|
||||
{% if 'event.subevents:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.subevent.edit" event=request.event.slug organizer=request.event.organizer.slug subevent=subevent.pk %}"
|
||||
class="btn btn-default">
|
||||
<span class="fa fa-edit"></span>
|
||||
{% trans "Edit" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<div class="row">
|
||||
<div class="{% if "event.orders:read" in request.eventpermset %}col-md-5{% else %}col-md-10{% endif %} col-xs-12">
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
<dl class="dl-horizontal">
|
||||
<dt>{% trans "Name" %}</dt>
|
||||
<dd>{{ subevent.name }}</dd>
|
||||
<dt>{% trans "ID" %}</dt>
|
||||
<dd>#{{ subevent.pk }}</dd>
|
||||
<dt>{% trans "Status" %}</dt>
|
||||
<dd>
|
||||
{% if not subevent.active %}
|
||||
<span class="label label-danger">{% trans "Disabled" %}</span>
|
||||
{% elif subevent.presale_has_ended %}
|
||||
<span class="label label-warning">{% trans "Presale over" %}</span>
|
||||
{% elif not subevent.presale_is_running %}
|
||||
<span class="label label-warning">{% trans "Presale not started" %}</span>
|
||||
{% else %}
|
||||
<span class="label label-success">{% trans "On sale" %}</span>
|
||||
{% endif %}
|
||||
</dd>
|
||||
<dt>{% trans "Event start time" %}</dt>
|
||||
<dd>{{ subevent.date_from|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
<dt>{% trans "Event end time" %}</dt>
|
||||
<dd>{{ subevent.date_to|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
{% if subevent.date_admission %}
|
||||
<dt>{% trans "Admission time" %}</dt>
|
||||
<dd>{{ subevent.date_admission|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
{% endif %}
|
||||
{% if subevent.presale_start %}
|
||||
<dt>{% trans "Start of presale" %}</dt>
|
||||
<dd>{{ subevent.presale_start|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
{% endif %}
|
||||
{% if subevent.presale_end %}
|
||||
<dt>{% trans "End of presale" %}</dt>
|
||||
<dd>{{ subevent.presale_end|date:"SHORT_DATETIME_FORMAT" }}</dd>
|
||||
{% endif %}
|
||||
{% if subevent.location %}
|
||||
<dt>{% trans "Location" %}</dt>
|
||||
<dd>{{ subevent.location|linebreaksbr }}</dd>
|
||||
{% endif %}
|
||||
<dt>{% trans "Show in lists" %}</dt>
|
||||
<dd>{{ subevent.is_public|yesno }}</dd>
|
||||
{% for k, v in subevent.meta_data.items %}
|
||||
<dt>{{ k }}</dt>
|
||||
<dd>{{ v }}</dd>
|
||||
{% endfor %}
|
||||
{% if subevent.comment %}
|
||||
<dt>{% trans "Internal comment" %}</dt>
|
||||
<dd>{{ subevent.comment|linebreaksbr }}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Quotas" %}</legend>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Quota name" %}</th>
|
||||
<th>{% trans "Products" %}</th>
|
||||
<th>{% trans "Total capacity" %}</th>
|
||||
<th>{% trans "Capacity left" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for q in quotas %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a
|
||||
href="{% url "control:event.items.quotas.show" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}">{{ q.name }}</a></strong>
|
||||
{% if q.ignore_for_event_availability %}
|
||||
<span class="fa fa-eye-slash text-muted" data-toggle="tooltip"
|
||||
title="{% trans "Ignore this quota when determining event availability" %}"></span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<ul>
|
||||
{% for item in q.cached_items %}
|
||||
{% if not item.has_variations %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% for v in q.variations.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=v.item.id %}#tab-0-3-open">
|
||||
{{ v.item }} – {{ v }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
<td>{% if q.size == None %}Unlimited{% else %}{{ q.size }}{% endif %}</td>
|
||||
<td>{% include "pretixcontrol/items/fragment_quota_availability.html" with availability=q.cached_avail closed=q.closed %}</td>
|
||||
<td class="text-right flip">
|
||||
{% if 'event.items:write' in request.eventpermset %}
|
||||
<a href="{% url "control:event.items.quotas.edit" organizer=request.event.organizer.slug event=request.event.slug quota=q.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</fieldset>
|
||||
{% if checkinlists %}
|
||||
{% if not subevent.pk %}
|
||||
<h1>{% trans "Create date" context "subevent" %}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Date" context "subevent" %}</h1>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% for f in itemvar_forms %}
|
||||
{% bootstrap_form_errors f %}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 {% if subevent.pk %}col-lg-10{% endif %}">
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in lists" %}</legend>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-quotas">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Name" %}</th>
|
||||
{% if "event.orders:read" in request.eventpermset %}
|
||||
<th>{% trans "Checked in" %}</th>
|
||||
{% endif %}
|
||||
<th>{% trans "Products" %}</th>
|
||||
<th class="action-col-2"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for cl in checkinlists %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong><a
|
||||
href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}">{{ cl.name }}</a></strong>
|
||||
</td>
|
||||
{% if "event.orders:read" in request.eventpermset %}
|
||||
<td>
|
||||
<div class="quotabox availability">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success progress-bar-{{ cl.percent }}">
|
||||
</div>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.active layout="control" %}
|
||||
{% bootstrap_field form.date_from layout="control" %}
|
||||
{% bootstrap_field form.date_to layout="control" %}
|
||||
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
||||
{% bootstrap_field form.date_admission layout="control" %}
|
||||
{% bootstrap_field form.frontpage_text layout="control" %}
|
||||
{% bootstrap_field form.is_public layout="control" %}
|
||||
{% bootstrap_field form.comment layout="control" %}
|
||||
{% if meta_forms %}
|
||||
<div class="form-group metadata-group">
|
||||
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
|
||||
<div class="col-md-9">
|
||||
{% for form in meta_forms %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.value.id_for_label }}">
|
||||
{{ form.property.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Timeline" %}</legend>
|
||||
{% bootstrap_field form.presale_start layout="control" %}
|
||||
{% bootstrap_field form.presale_end layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Quotas" %}</legend>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="numbers">
|
||||
{{ cl.checkin_count|default_if_none:"0" }} /
|
||||
{{ cl.position_count|default_if_none:"0" }}
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
{% if cl.all_products %}
|
||||
<em>{% trans "All" %}</em>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for item in cl.limit_products.all %}
|
||||
<li>
|
||||
<a href="{% url "control:event.item" organizer=request.event.organizer.slug event=request.event.slug item=item.id %}">{{ item }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-right flip">
|
||||
{% if "event.orders:read" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.show" organizer=request.event.organizer.slug event=request.event.slug list=cl.id %}"
|
||||
class="btn btn-default btn-sm"><i class="fa fa-eye"></i></a>
|
||||
{% endif %}
|
||||
{% if "event.settings.general:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.orders.checkinlists.add" organizer=request.event.organizer.slug event=request.event.slug %}?copy_from={{ cl.id }}"
|
||||
class="btn btn-sm btn-default" title="{% trans "Clone" %}"
|
||||
data-toggle="tooltip">
|
||||
<span class="fa fa-copy"></span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.size layout="control" %}
|
||||
{% bootstrap_field form.itemvars layout="control" %}
|
||||
{% bootstrap_field form.release_after_exit layout="control" %}
|
||||
{% bootstrap_field form.ignore_for_event_availability layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field formset.empty_form.size layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.itemvars layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.release_after_exit layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.ignore_for_event_availability layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new quota" %}</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% eventsignal request.event "pretix.control.signals.subevent_detail_html" subevent=subevent %}
|
||||
</div>
|
||||
{% if "event.orders:read" in request.eventpermset %}
|
||||
<div class="col-md-5 col-xs-12">
|
||||
<fieldset>
|
||||
<legend>
|
||||
{% trans "Orders" %}
|
||||
<span class="badge">
|
||||
{{ order_count }}
|
||||
</span>
|
||||
</legend>
|
||||
{% if order_count %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-hover table-orders">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Order code" %}</th>
|
||||
<th>{% trans "Details" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for o in orders %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="{% url "control:event.order" event=request.event.slug organizer=request.event.organizer.slug code=o.code %}">
|
||||
{{ o.code }}
|
||||
</a>
|
||||
</strong>
|
||||
<br>
|
||||
{% if o.testmode %}
|
||||
<span class="label label-warning">{% trans "TEST MODE" %}</span>
|
||||
{% endif %}
|
||||
{% if o.status == "p" and o.pcnt == 0 %}
|
||||
{# Everything related to this subevent is canceled #}
|
||||
<span class="label label-danger">
|
||||
<span class="fa fa-times"></span>
|
||||
{% trans "partially canceled" %}
|
||||
</span>
|
||||
{% else %}
|
||||
{% include "pretixcontrol/orders/fragment_order_status.html" with order=o %}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if "." in o.sales_channel.icon %}
|
||||
<img src="{% static o.sales_channel.icon %}" class="fa-like-image"
|
||||
data-toggle="tooltip" title="{{ o.sales_channel.label }}">
|
||||
{% else %}
|
||||
<span class="fa fa-fw fa-{{ o.sales_channel.icon }} text-muted"
|
||||
data-toggle="tooltip" title="{{ o.sales_channel.label }}"></span>
|
||||
{% endif %}
|
||||
{{ o.datetime|date:"SHORT_DATETIME_FORMAT" }}
|
||||
{% if o.email %}
|
||||
<br>{% icon "envelope-o fa-fw text-muted" %}
|
||||
{{ o.email|default_if_none:"" }}
|
||||
{% endif %}
|
||||
{% if o.invoice_address.name %}
|
||||
<br>{% icon "user fa-fw text-muted" %} {{ o.invoice_address.name }}
|
||||
{% endif %}
|
||||
<br>{% icon "ticket text-muted fa-fw" %} {{ o.pcnt }}
|
||||
{% if o.comment %}
|
||||
<br>
|
||||
<span class="text-muted">
|
||||
{{ o.comment|linebreaksbr }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if o.custom_followup_due %}
|
||||
<br>
|
||||
<span class="label label-danger">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}
|
||||
TODO {{ date }}{% endblocktrans %}</span>
|
||||
{% elif o.custom_followup_at %}
|
||||
<br>
|
||||
<span class="label label-default">{% blocktrans trimmed with date=o.custom_followup_at|date:"SHORT_DATE_FORMAT" context "followup" %}
|
||||
TODO {{ date }}{% endblocktrans %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<legend>{% trans "Product settings" %}</legend>
|
||||
<p class="text-muted">
|
||||
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
|
||||
</p>
|
||||
{% for f in itemvar_forms %}
|
||||
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}">
|
||||
{% bootstrap_form_errors f %}
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<br>
|
||||
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if order_count > 10 %}
|
||||
<p class="text-center">
|
||||
<a href="{% url "control:event.orders" organizer=request.organizer.slug event=request.event.slug %}?subevent={{ subevent.pk }}"
|
||||
class="btn btn-default">
|
||||
{% trans "View all" %}
|
||||
</a>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="empty-collection">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
No orders found.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in lists" %}</legend>
|
||||
<p class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
You can choose to either add one or more check-in lists for every date in your series individually,
|
||||
or use just one check-in list for all your dates and limit admission through check-in rules. Which
|
||||
approach is better depends on multiple factors, such as the number of dates in your series. For a
|
||||
series with one or less event date per day, individual lists are usually more helpful. If you
|
||||
use dates to represent many time slots on the same day, or even overlapping time slots, working with
|
||||
just one large check-in list will be easier.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
|
||||
{{ cl_formset.management_form }}
|
||||
{% bootstrap_formset_errors cl_formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in cl_formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.include_pending layout="control" %}
|
||||
{% bootstrap_field form.all_products layout="control" %}
|
||||
{% bootstrap_field form.limit_products layout="control" %}
|
||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||
{% if form.gates %}
|
||||
{% bootstrap_field form.gates layout="control" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ cl_formset.empty_form.id }}
|
||||
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
||||
{% if cl_formset.empty_form.gates %}
|
||||
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
|
||||
</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
{% for f in plugin_forms %}
|
||||
{% if f.title %}
|
||||
<fieldset>
|
||||
<legend>{{ f.title }}</legend>
|
||||
{% if f.template %}
|
||||
{% include f.template with form=f %}
|
||||
{% else %}
|
||||
{% bootstrap_form f layout="control" %}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Additional settings" %}</legend>
|
||||
{% for f in plugin_forms %}
|
||||
{% if not f.title %}
|
||||
{% if f.template %}
|
||||
{% include f.template with form=f %}
|
||||
{% else %}
|
||||
{% bootstrap_form f layout="control" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-md-2 col-xs-12">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Date history" context "subevent" %}
|
||||
</h3>
|
||||
{% if subevent.pk %}
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Date history" context "subevent" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=subevent %}
|
||||
</div>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=subevent %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group submit-group submit-group-sticky">
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,296 +0,0 @@
|
||||
{% extends "pretixcontrol/event/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% load formset_tags %}
|
||||
{% load eventsignal %}
|
||||
{% load static %}
|
||||
{% block title %}{% trans "Date" context "subevent" %}{% endblock %}
|
||||
{% block content %}
|
||||
{% if not subevent.pk %}
|
||||
<h1>{% trans "Create date" context "subevent" %}</h1>
|
||||
{% else %}
|
||||
<h1>{% trans "Date" context "subevent" %}</h1>
|
||||
{% endif %}
|
||||
<form action="" method="post" class="form-horizontal" enctype="multipart/form-data">
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form_errors form %}
|
||||
{% for f in itemvar_forms %}
|
||||
{% bootstrap_form_errors f %}
|
||||
{% endfor %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 {% if subevent.pk %}col-lg-10{% endif %}">
|
||||
<fieldset>
|
||||
<legend>{% trans "General information" %}</legend>
|
||||
{% bootstrap_field form.name layout="control" %}
|
||||
{% bootstrap_field form.active layout="control" %}
|
||||
{% bootstrap_field form.date_from layout="control" %}
|
||||
{% bootstrap_field form.date_to layout="control" %}
|
||||
{% include "pretixcontrol/event/fragment_geodata.html" %}
|
||||
{% bootstrap_field form.date_admission layout="control" %}
|
||||
{% bootstrap_field form.frontpage_text layout="control" %}
|
||||
{% bootstrap_field form.is_public layout="control" %}
|
||||
{% bootstrap_field form.comment layout="control" %}
|
||||
{% if meta_forms %}
|
||||
<div class="form-group metadata-group">
|
||||
<label class="col-md-3 control-label">{% trans "Meta data" %}</label>
|
||||
<div class="col-md-9">
|
||||
{% for form in meta_forms %}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<label for="{{ form.value.id_for_label }}">
|
||||
{{ form.property.name }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% bootstrap_form form layout="inline" error_types="all" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Timeline" %}</legend>
|
||||
{% bootstrap_field form.presale_start layout="control" %}
|
||||
{% bootstrap_field form.presale_end layout="control" %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Quotas" %}</legend>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ formset.prefix }}">
|
||||
{{ formset.management_form }}
|
||||
{% bootstrap_formset_errors formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.size layout="control" %}
|
||||
{% bootstrap_field form.itemvars layout="control" %}
|
||||
{% bootstrap_field form.release_after_exit layout="control" %}
|
||||
{% bootstrap_field form.ignore_for_event_availability layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ formset.empty_form.id }}
|
||||
{% bootstrap_field formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field formset.empty_form.size layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.itemvars layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.release_after_exit layout="control" %}
|
||||
{% bootstrap_field formset.empty_form.ignore_for_event_availability layout="control" %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new quota" %}</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Product settings" %}</legend>
|
||||
<p class="text-muted">
|
||||
{% trans "These settings are optional, if you leave them empty, the default values from the product settings will be used." %}
|
||||
</p>
|
||||
{% for f in itemvar_forms %}
|
||||
<div data-itemvar="{{ f.item.id }}{% if f.variation %}-{{ f.variation.id }}{% endif %}">
|
||||
{% bootstrap_form_errors f %}
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<label class="col-md-3 control-label" for="id_{{ f.prefix }}-price">
|
||||
{% if f.variation %}{{ f.item }} – {{ f.variation }}{% else %}{{ f.item }}{% endif %}
|
||||
</label>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.price.id_for_label }}" class="text-muted">{% trans "Price" %}</label><br>
|
||||
{% bootstrap_field f.price addon_after=request.event.currency form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<br>
|
||||
{% bootstrap_field f.disabled layout="inline" form_group_class="" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group subevent-itemvar-group">
|
||||
<div class="col-md-4 col-md-offset-3">
|
||||
<label for="{{ f.available_from.id_for_label }}" class="text-muted">{% trans "Available from" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_from_mode %}<br>
|
||||
{% bootstrap_field f.available_from form_group_class="foo" layout="inline" %}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="{{ f.available_until.id_for_label }}" class="text-muted">{% trans "Available until" %}</label>
|
||||
{% include "pretixcontrol/subevents/fragment_unavail_mode_indicator.html" with mode=f.available_until_mode %}<br>
|
||||
{% bootstrap_field f.available_until form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Check-in lists" %}</legend>
|
||||
<p class="help-block">
|
||||
{% blocktrans trimmed %}
|
||||
You can choose to either add one or more check-in lists for every date in your series individually,
|
||||
or use just one check-in list for all your dates and limit admission through check-in rules. Which
|
||||
approach is better depends on multiple factors, such as the number of dates in your series. For a
|
||||
series with one or less event date per day, individual lists are usually more helpful. If you
|
||||
use dates to represent many time slots on the same day, or even overlapping time slots, working with
|
||||
just one large check-in list will be easier.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<div class="formset" data-formset data-formset-prefix="{{ cl_formset.prefix }}">
|
||||
{{ cl_formset.management_form }}
|
||||
{% bootstrap_formset_errors cl_formset %}
|
||||
<div data-formset-body>
|
||||
{% for form in cl_formset %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ form.id }}
|
||||
{% bootstrap_field form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_form_errors form %}
|
||||
{% bootstrap_field form.include_pending layout="control" %}
|
||||
{% bootstrap_field form.all_products layout="control" %}
|
||||
{% bootstrap_field form.limit_products layout="control" %}
|
||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||
{% if form.gates %}
|
||||
{% bootstrap_field form.gates layout="control" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="form-template" data-formset-empty-form>
|
||||
{% escapescript %}
|
||||
<div class="panel panel-default" data-formset-form>
|
||||
<div class="sr-only">
|
||||
{{ cl_formset.empty_form.id }}
|
||||
{% bootstrap_field cl_formset.empty_form.DELETE form_group_class="" layout="inline" %}
|
||||
</div>
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<div class="row">
|
||||
<div class="col-md-10">
|
||||
{% bootstrap_field cl_formset.empty_form.name layout='inline' form_group_class="" %}
|
||||
</div>
|
||||
<div class="col-md-2 text-right flip">
|
||||
<button type="button" class="btn btn-danger" data-formset-delete-button>
|
||||
<i class="fa fa-trash"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="panel-body form-horizontal">
|
||||
{% bootstrap_field cl_formset.empty_form.include_pending layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.all_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.limit_products layout="control" %}
|
||||
{% bootstrap_field cl_formset.empty_form.allow_entry_after_exit layout="control" %}
|
||||
{% if cl_formset.empty_form.gates %}
|
||||
{% bootstrap_field cl_formset.empty_form.gates layout="control" %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endescapescript %}
|
||||
</script>
|
||||
<p>
|
||||
<button type="button" class="btn btn-default" data-formset-add>
|
||||
<i class="fa fa-plus"></i> {% trans "Add a new check-in list" %}
|
||||
</button>
|
||||
</p>
|
||||
</fieldset>
|
||||
{% for f in plugin_forms %}
|
||||
{% if f.title %}
|
||||
<fieldset>
|
||||
<legend>{{ f.title }}</legend>
|
||||
{% if f.template %}
|
||||
{% include f.template with form=f %}
|
||||
{% else %}
|
||||
{% bootstrap_form f layout="control" %}
|
||||
{% endif %}
|
||||
</fieldset>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<fieldset>
|
||||
<legend>{% trans "Additional settings" %}</legend>
|
||||
{% for f in plugin_forms %}
|
||||
{% if not f.title %}
|
||||
{% if f.template %}
|
||||
{% include f.template with form=f %}
|
||||
{% else %}
|
||||
{% bootstrap_form f layout="control" %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</fieldset>
|
||||
</div>
|
||||
{% if subevent.pk %}
|
||||
<div class="col-xs-12 col-lg-2">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
{% trans "Date history" context "subevent" %}
|
||||
</h3>
|
||||
</div>
|
||||
{% include "pretixcontrol/includes/logs.html" with obj=subevent %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group submit-group submit-group-sticky">
|
||||
<a href="{{ next_url }}" class="btn btn-default btn-cancel">
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<button type="submit" class="btn btn-primary btn-save">
|
||||
{% trans "Save" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -133,7 +133,7 @@
|
||||
</td>
|
||||
{% endif %}
|
||||
<td>
|
||||
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}">
|
||||
<strong><a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}">
|
||||
{{ s.name }}</a></strong><br>
|
||||
<small class="text-muted">
|
||||
#{{ s.pk }}
|
||||
@@ -182,7 +182,7 @@
|
||||
{% endif %}
|
||||
|
||||
{% if "event.subevents:write" in request.eventpermset %}
|
||||
<a href="{% url "control:event.subevent.edit" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<a href="{% url "control:event.subevent" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-default btn-sm"><i class="fa fa-edit"></i></a>
|
||||
<div class="btn-group {% if forloop.revcounter0 < 2 %}dropup{% endif %}">
|
||||
<button type="button" class="btn btn-default btn-sm dropdown-toggle"
|
||||
data-toggle="dropdown">
|
||||
@@ -201,7 +201,7 @@
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="{% url "control:event.subevent.delete" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?next={{ request.get_full_path|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
<a href="{% url "control:event.subevent.delete" organizer=request.event.organizer.slug event=request.event.slug subevent=s.id %}?returnto={{ request.GET.urlencode|urlencode }}" class="btn btn-danger btn-sm"><i class="fa fa-trash"></i></a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -308,8 +308,7 @@ urlpatterns = [
|
||||
re_path(r'^pdf/editor/(?P<filename>[^/]+).pdf$', pdf.PdfView.as_view(), name='pdf.background'),
|
||||
re_path(r'^subevents/$', subevents.SubEventList.as_view(), name='event.subevents'),
|
||||
re_path(r'^subevents/select2$', typeahead.subevent_select2, name='event.subevents.select2'),
|
||||
re_path(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventDetail.as_view(), name='event.subevent'),
|
||||
re_path(r'^subevents/(?P<subevent>\d+)/edit$', subevents.SubEventUpdate.as_view(), name='event.subevent.edit'),
|
||||
re_path(r'^subevents/(?P<subevent>\d+)/$', subevents.SubEventUpdate.as_view(), name='event.subevent'),
|
||||
re_path(r'^subevents/(?P<subevent>\d+)/delete$', subevents.SubEventDelete.as_view(),
|
||||
name='event.subevent.delete'),
|
||||
re_path(r'^subevents/add$', subevents.SubEventCreate.as_view(), name='event.subevents.add'),
|
||||
|
||||
@@ -44,7 +44,7 @@ from pretix.control.permissions import (
|
||||
from pretix.helpers.models import modelcopy
|
||||
|
||||
from ...helpers.compat import CompatDeleteView
|
||||
from . import CreateView, UpdateView
|
||||
from . import CreateView, PaginationMixin, UpdateView
|
||||
|
||||
|
||||
class DiscountDelete(EventPermissionRequiredMixin, CompatDeleteView):
|
||||
@@ -183,7 +183,7 @@ class DiscountCreate(EventPermissionRequiredMixin, CreateView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class DiscountList(ListView):
|
||||
class DiscountList(PaginationMixin, ListView):
|
||||
model = Discount
|
||||
context_object_name = 'discounts'
|
||||
template_name = 'pretixcontrol/items/discounts.html'
|
||||
|
||||
@@ -335,7 +335,7 @@ class CategoryCreate(EventPermissionRequiredMixin, CreateView):
|
||||
return super().form_invalid(form)
|
||||
|
||||
|
||||
class CategoryList(ListView):
|
||||
class CategoryList(PaginationMixin, ListView):
|
||||
model = ItemCategory
|
||||
context_object_name = 'categories'
|
||||
template_name = 'pretixcontrol/items/categories.html'
|
||||
|
||||
@@ -3384,10 +3384,8 @@ class ReusableMediaListView(OrganizerDetailViewMixin, OrganizerPermissionRequire
|
||||
|
||||
def get_queryset(self):
|
||||
qs = self.request.organizer.reusable_media.select_related(
|
||||
'customer',
|
||||
'linked_giftcard',
|
||||
).prefetch_related(
|
||||
Prefetch('linked_orderpositions', queryset=OrderPosition.objects.select_related("order", "order__event"))
|
||||
'customer', 'linked_orderposition', 'linked_orderposition__order', 'linked_orderposition__order__event',
|
||||
'linked_giftcard'
|
||||
)
|
||||
if self.filter_form.is_valid():
|
||||
qs = self.filter_form.filter_qs(qs)
|
||||
@@ -3435,14 +3433,10 @@ class ReusableMediumCreateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
r = super().form_valid(form)
|
||||
|
||||
data = {
|
||||
form.instance.log_action('pretix.reusable_medium.created', user=self.request.user, 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
|
||||
|
||||
@@ -3467,40 +3461,13 @@ class ReusableMediumUpdateView(OrganizerDetailViewMixin, OrganizerPermissionRequ
|
||||
|
||||
@transaction.atomic
|
||||
def form_valid(self, form):
|
||||
prev_linked_ops_pks = list(getattr(self.object, "linked_orderpositions").values_list("pk", flat=True))
|
||||
result = super().form_valid(form)
|
||||
if form.has_changed():
|
||||
data = {
|
||||
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data={
|
||||
k: getattr(self.object, k)
|
||||
for k in form.changed_data
|
||||
}
|
||||
if "linked_orderpositions" in data:
|
||||
# handle changes to linked_orderpositions separately
|
||||
linked_ops_pks = data["linked_orderpositions"].values_list("pk", flat=True)
|
||||
del data["linked_orderpositions"]
|
||||
for op_pk in prev_linked_ops_pks:
|
||||
if op_pk not in linked_ops_pks:
|
||||
self.object.log_action(
|
||||
'pretix.reusable_medium.linked_orderposition.removed',
|
||||
user=self.request.user,
|
||||
data={
|
||||
'linked_orderposition': op_pk,
|
||||
}
|
||||
)
|
||||
for op_pk in linked_ops_pks:
|
||||
if op_pk not in prev_linked_ops_pks:
|
||||
self.object.log_action(
|
||||
'pretix.reusable_medium.linked_orderposition.added',
|
||||
user=self.request.user,
|
||||
data={
|
||||
'linked_orderposition': op_pk,
|
||||
}
|
||||
)
|
||||
if data:
|
||||
# log change-action only for changes other than linked_orderpositions
|
||||
self.object.log_action('pretix.reusable_medium.changed', user=self.request.user, data=data)
|
||||
})
|
||||
messages.success(self.request, _('Your changes have been saved.'))
|
||||
return result
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse('control:organizer.reusable_medium', kwargs={
|
||||
|
||||
@@ -41,9 +41,7 @@ from django.contrib import messages
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.files import File
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, OuterRef, Prefetch, ProtectedError, Subquery,
|
||||
)
|
||||
from django.db.models import Count, F, Prefetch, ProtectedError
|
||||
from django.db.models.functions import Coalesce, TruncDate, TruncTime
|
||||
from django.forms import inlineformset_factory
|
||||
from django.http import Http404, HttpResponse, HttpResponseRedirect
|
||||
@@ -51,21 +49,17 @@ from django.shortcuts import redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.formats import get_format
|
||||
from django.utils.functional import cached_property
|
||||
from django.utils.http import url_has_allowed_host_and_scheme
|
||||
from django.utils.timezone import make_aware, now
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django.views import View
|
||||
from django.views.generic import (
|
||||
CreateView, DetailView, FormView, ListView, UpdateView,
|
||||
)
|
||||
from django.views.generic import CreateView, FormView, ListView, UpdateView
|
||||
|
||||
from pretix.base.models import CartPosition, LogEntry, OrderPosition
|
||||
from pretix.base.models import CartPosition, LogEntry
|
||||
from pretix.base.models.checkin import CheckinList
|
||||
from pretix.base.models.event import SubEvent, SubEventMetaValue
|
||||
from pretix.base.models.items import (
|
||||
Item, ItemVariation, Quota, SubEventItem, SubEventItemVariation,
|
||||
ItemVariation, Quota, SubEventItem, SubEventItemVariation,
|
||||
)
|
||||
from pretix.base.models.orders import CancellationRequest
|
||||
from pretix.base.reldate import RelativeDate, RelativeDateWrapper
|
||||
from pretix.base.services import tickets
|
||||
from pretix.base.services.quotas import QuotaAvailability
|
||||
@@ -511,68 +505,9 @@ class SubEventEditorMixin(MetaDataEditorMixin):
|
||||
) and self.cl_formset.is_valid() and all(f.is_valid() for f in self.plugin_forms)
|
||||
|
||||
|
||||
class SubEventDetail(EventPermissionRequiredMixin, DetailView):
|
||||
model = SubEvent
|
||||
template_name = 'pretixcontrol/subevents/detail.html'
|
||||
permission = None
|
||||
context_object_name = 'subevent'
|
||||
|
||||
def get_object(self, queryset=None) -> SubEvent:
|
||||
try:
|
||||
return self.request.event.subevents.get(
|
||||
id=self.kwargs['subevent']
|
||||
)
|
||||
except SubEvent.DoesNotExist:
|
||||
raise Http404(pgettext_lazy("subevent", "The requested date does not exist."))
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
oqs = self.request.event.orders.filter(
|
||||
Exists(
|
||||
OrderPosition.objects.filter(
|
||||
subevent=self.object,
|
||||
order_id=OuterRef("id"),
|
||||
)
|
||||
)
|
||||
).annotate(
|
||||
pcnt=Subquery(
|
||||
OrderPosition.objects.filter(
|
||||
subevent=self.object,
|
||||
).values("subevent").annotate(c=Count("*")).values("c")
|
||||
),
|
||||
has_cancellation_request=Exists(CancellationRequest.objects.filter(order=OuterRef("pk"))),
|
||||
).select_related("invoice_address").prefetch_related("sales_channel")
|
||||
ctx = {
|
||||
"quotas": self.object.quotas.prefetch_related(
|
||||
Prefetch(
|
||||
"items",
|
||||
queryset=Item.objects.annotate(
|
||||
has_variations=Exists(ItemVariation.objects.filter(item=OuterRef("pk")))
|
||||
),
|
||||
to_attr="cached_items"
|
||||
),
|
||||
"variations",
|
||||
"variations__item",
|
||||
).order_by("name", "pk"),
|
||||
"checkinlists": self.object.checkinlist_set.prefetch_related("limit_products"),
|
||||
"orders": oqs[:11],
|
||||
"order_count": oqs.count(),
|
||||
}
|
||||
|
||||
qa = QuotaAvailability()
|
||||
qa.queue(*ctx["quotas"])
|
||||
qa.compute()
|
||||
for quota in ctx["quotas"]:
|
||||
quota.cached_avail = qa.results[quota]
|
||||
|
||||
return super().get_context_data(
|
||||
**kwargs,
|
||||
**ctx,
|
||||
)
|
||||
|
||||
|
||||
class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateView):
|
||||
model = SubEvent
|
||||
template_name = 'pretixcontrol/subevents/edit.html'
|
||||
template_name = 'pretixcontrol/subevents/detail.html'
|
||||
permission = 'event.subevents:write'
|
||||
context_object_name = 'subevent'
|
||||
form_class = SubEventForm
|
||||
@@ -638,28 +573,20 @@ class SubEventUpdate(EventPermissionRequiredMixin, SubEventEditorMixin, UpdateVi
|
||||
return HttpResponseRedirect(self.get_success_url())
|
||||
|
||||
def get_success_url(self) -> str:
|
||||
if "next" in self.request.GET and url_has_allowed_host_and_scheme(self.request.GET.get("next"), allowed_hosts=None):
|
||||
return self.request.GET.get("next")
|
||||
return reverse('control:event.subevent', kwargs={
|
||||
return reverse('control:event.subevents', kwargs={
|
||||
'organizer': self.request.event.organizer.slug,
|
||||
'event': self.request.event.slug,
|
||||
'subevent': self.object.pk,
|
||||
})
|
||||
}) + ('?' + self.request.GET.get('returnto') if 'returnto' in self.request.GET else '')
|
||||
|
||||
def get_form_kwargs(self):
|
||||
kwargs = super().get_form_kwargs()
|
||||
kwargs['event'] = self.request.event
|
||||
return kwargs
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
return super().get_context_data(
|
||||
next_url=self.get_success_url()
|
||||
)
|
||||
|
||||
|
||||
class SubEventCreate(SubEventEditorMixin, EventPermissionRequiredMixin, CreateView):
|
||||
model = SubEvent
|
||||
template_name = 'pretixcontrol/subevents/edit.html'
|
||||
template_name = 'pretixcontrol/subevents/detail.html'
|
||||
permission = 'event.subevents:write'
|
||||
context_object_name = 'subevent'
|
||||
form_class = SubEventForm
|
||||
|
||||
@@ -70,37 +70,40 @@ reshaper = SimpleLazyObject(lambda: ArabicReshaper(configuration={
|
||||
}))
|
||||
|
||||
|
||||
def font_supports_text(text, font_name):
|
||||
if not text:
|
||||
return True
|
||||
font = pdfmetrics.getFont(font_name)
|
||||
return all(
|
||||
ord(c) in font.face.charToGlyph or not c.isprintable()
|
||||
for c in text
|
||||
)
|
||||
|
||||
|
||||
def find_font_supporting_text(fonts, text, preferred_font):
|
||||
if font_supports_text(text, preferred_font):
|
||||
return preferred_font
|
||||
for family, styles in fonts.items():
|
||||
if font_supports_text(text, family):
|
||||
if (preferred_font.endswith("It") or preferred_font.endswith(" I")) and "italic" in styles:
|
||||
return family + " I"
|
||||
if (preferred_font.endswith("Bd") or preferred_font.endswith(" B")) and "bold" in styles:
|
||||
return family + " B"
|
||||
return family
|
||||
return preferred_font
|
||||
|
||||
|
||||
class FontFallbackParagraph(Paragraph):
|
||||
def __init__(self, text, style=None, *args, **kwargs):
|
||||
if style is None:
|
||||
style = ParagraphStyle(name='paragraphImplicitDefaultStyle')
|
||||
|
||||
if not self._font_supports_text(text, style.fontName):
|
||||
newFont = self._find_font(text, style.fontName)
|
||||
if newFont:
|
||||
logger.debug(f"replacing {style.fontName} with {newFont} for {text!r}")
|
||||
style = style.clone(name=style.name + '_' + newFont, fontName=newFont)
|
||||
|
||||
supporting_font = find_font_supporting_text(get_fonts(pdf_support_required=True), text, style.fontName)
|
||||
if supporting_font != style.fontName:
|
||||
logger.debug(f"replacing {style.fontName} with {supporting_font} for {text!r}")
|
||||
style = style.clone(name=style.name + '_' + supporting_font, fontName=supporting_font)
|
||||
super().__init__(text, style, *args, **kwargs)
|
||||
|
||||
def _font_supports_text(self, text, font_name):
|
||||
if not text:
|
||||
return True
|
||||
font = pdfmetrics.getFont(font_name)
|
||||
return all(
|
||||
ord(c) in font.face.charToGlyph or not c.isprintable()
|
||||
for c in text
|
||||
)
|
||||
|
||||
def _find_font(self, text, original_font):
|
||||
for family, styles in get_fonts(pdf_support_required=True).items():
|
||||
if self._font_supports_text(text, family):
|
||||
if (original_font.endswith("It") or original_font.endswith(" I")) and "italic" in styles:
|
||||
return family + " I"
|
||||
if (original_font.endswith("Bd") or original_font.endswith(" B")) and "bold" in styles:
|
||||
return family + " B"
|
||||
return family
|
||||
|
||||
|
||||
def register_ttf_font_if_new(name, path):
|
||||
from reportlab.pdfbase import pdfmetrics
|
||||
|
||||
@@ -1098,27 +1098,6 @@ def test_question_upload(token_client, organizer, clist, event, order, question)
|
||||
assert order.positions.first().answers.get(question=question[0]).file
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_question_upload_optional(token_client, organizer, clist, event, order, question):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
question[0].type = 'F'
|
||||
question[0].required = False
|
||||
question[0].save()
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'incomplete'
|
||||
with scopes_disabled():
|
||||
assert resp.data['questions'] == [QuestionSerializer(question[0]).data]
|
||||
|
||||
resp = _redeem(token_client, organizer, clist, p.pk, {'answers': {question[0].pk: ""}})
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
with scopes_disabled():
|
||||
assert not order.positions.first().answers.filter(question=question[0]).exists()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_store_failed(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
|
||||
@@ -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():
|
||||
rm = ReusableMedium.objects.create(
|
||||
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,71 +301,6 @@ 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_all, event, order):
|
||||
with scopes_disabled():
|
||||
rm = ReusableMedium.objects.create(
|
||||
type="barcode",
|
||||
identifier="abcdef",
|
||||
organizer=organizer,
|
||||
)
|
||||
op_item_first = order.positions.first()
|
||||
rm.linked_orderpositions.add(op_item_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_all, "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_all, "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_all, "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_all, "abcdef", {"source_type": "barcode"})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'already_redeemed'
|
||||
|
||||
with scopes_disabled():
|
||||
op_item_first.valid_from = datetime.datetime(2020, 1, 1, 10, 0, 0, tzinfo=event.timezone)
|
||||
op_item_first.valid_until = datetime.datetime(2020, 1, 1, 12, 0, 0, tzinfo=event.timezone)
|
||||
op_item_first.save()
|
||||
|
||||
with freeze_time("2020-01-01 15:45:00"):
|
||||
resp = _redeem(token_client, organizer, clist_all, "abcdef", {"source_type": "barcode"})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'invalid_time'
|
||||
|
||||
with scopes_disabled():
|
||||
op_item_first.canceled = True
|
||||
op_item_first.save()
|
||||
op_item_other.canceled = True
|
||||
op_item_other.save()
|
||||
|
||||
resp = _redeem(token_client, organizer, clist_all, "abcdef", {"source_type": "barcode"})
|
||||
assert resp.status_code == 400
|
||||
assert resp.data['status'] == 'error'
|
||||
assert resp.data['reason'] == 'canceled'
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_by_medium_not_connected(token_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
@@ -383,12 +318,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():
|
||||
rm = ReusableMedium.objects.create(
|
||||
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'
|
||||
@@ -402,12 +337,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():
|
||||
rm = ReusableMedium.objects.create(
|
||||
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'
|
||||
@@ -420,13 +355,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():
|
||||
rm = ReusableMedium.objects.create(
|
||||
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'
|
||||
|
||||
@@ -3121,78 +3121,9 @@ 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_orderpositions.first()
|
||||
assert o.positions.first() == medium.linked_orderposition
|
||||
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):
|
||||
@@ -3237,7 +3168,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_orderpositions.first() == o.positions.first()
|
||||
assert m.linked_orderposition == o.positions.first()
|
||||
assert m.type == "barcode"
|
||||
|
||||
|
||||
|
||||
@@ -89,13 +89,10 @@ 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": {},
|
||||
@@ -173,7 +170,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_orderpositions.add(op)
|
||||
medium.linked_orderposition = op
|
||||
medium.linked_giftcard = giftcard
|
||||
medium.customer = customer
|
||||
medium.save()
|
||||
@@ -276,7 +273,7 @@ def test_medium_detail_event_permission_missing(token_client, organizer, event,
|
||||
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_orderpositions.add(op)
|
||||
medium.linked_orderposition = op
|
||||
medium.linked_giftcard = giftcard
|
||||
medium.customer = customer
|
||||
medium.save()
|
||||
@@ -355,110 +352,6 @@ def test_medium_create(token_client, organizer, giftcard):
|
||||
assert m.updated > now() - timedelta(minutes=10)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_medium_create_linked_orderposition(token_client, organizer, event, org2_event, medium):
|
||||
with scopes_disabled():
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
|
||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||
total=14, locale='en'
|
||||
)
|
||||
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"))
|
||||
op2 = o.positions.create(item=ticket, price=Decimal("14"))
|
||||
|
||||
org2_o = Order.objects.create(
|
||||
code='FOO', event=org2_event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
|
||||
sales_channel=org2_event.organizer.sales_channels.get(identifier="web"),
|
||||
total=14, locale='en'
|
||||
)
|
||||
org2_ticket = org2_event.items.create(name='Early-bird ticket', category=None, default_price=23, admission=True,
|
||||
personalized=True)
|
||||
org2_op = org2_o.positions.create(item=org2_ticket, price=Decimal("14"))
|
||||
|
||||
payload = dict(TEST_MEDIUM_CREATE_PAYLOAD)
|
||||
|
||||
# wrong orderposition for organizer
|
||||
payload['linked_orderposition'] = org2_op.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
|
||||
payload,
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
# unkown orderposition
|
||||
payload['linked_orderposition'] = "unknown"
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
|
||||
payload,
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
# create with linked_orderposition
|
||||
payload['linked_orderposition'] = op.pk
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
|
||||
payload,
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
m = ReusableMedium.objects.get(pk=resp.data['id'])
|
||||
assert list(m.linked_orderpositions.values_list('pk', flat=True)) == [op.pk]
|
||||
|
||||
# double-check API-response for fallback-values
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, resp.data['id'])
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['linked_orderposition'] == op.pk
|
||||
assert resp.data['linked_orderpositions'] == [op.pk]
|
||||
|
||||
# create with linked_orderposition and linked_orderpositions (not allowed)
|
||||
payload['identifier'] = "FOOBAZ"
|
||||
payload['linked_orderpositions'] = [op.pk, org2_op.pk]
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
|
||||
payload,
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
# multiple linked_orderpositions, but from different organizers
|
||||
del payload['linked_orderposition']
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
|
||||
payload,
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
# multiple linked_orderpositions from same organizer
|
||||
payload['linked_orderpositions'] = [op.pk, op2.pk]
|
||||
resp = token_client.post(
|
||||
'/api/v1/organizers/{}/reusablemedia/'.format(organizer.slug),
|
||||
payload,
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 201
|
||||
with scopes_disabled():
|
||||
m = ReusableMedium.objects.get(pk=resp.data['id'])
|
||||
assert list(m.linked_orderpositions.values_list('pk', flat=True)) == [op.pk, op2.pk]
|
||||
|
||||
# double-check API-response for fallback-values
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, resp.data['id'])
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['linked_orderposition'] is None
|
||||
assert resp.data['linked_orderpositions'] == [op.pk, op2.pk]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_medium_foreignkeyval(token_client, organizer, giftcard2):
|
||||
payload = dict(TEST_MEDIUM_CREATE_PAYLOAD)
|
||||
@@ -505,68 +398,6 @@ def test_medium_patch(token_client, organizer, event, medium, giftcard, customer
|
||||
assert medium.info == {'test': 2}
|
||||
assert medium.identifier == "ABCDEFGH"
|
||||
|
||||
# test patch with linked_orderpositions
|
||||
with scopes_disabled():
|
||||
o = Order.objects.create(
|
||||
code='FOO', event=event, email='dummy@dummy.test',
|
||||
status=Order.STATUS_PENDING, datetime=now(), expires=now() + timedelta(days=10),
|
||||
sales_channel=event.organizer.sales_channels.get(identifier="web"),
|
||||
total=14, locale='en'
|
||||
)
|
||||
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"))
|
||||
op2 = o.positions.create(item=ticket, price=Decimal("14"))
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, medium.pk),
|
||||
{
|
||||
'linked_orderposition': op.pk,
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
medium.refresh_from_db()
|
||||
with scopes_disabled():
|
||||
assert list(medium.linked_orderpositions.values_list('pk', flat=True)) == [op.pk]
|
||||
assert medium.all_logentries().count() == 2
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, medium.pk),
|
||||
{
|
||||
'linked_orderpositions': [op.pk, op2.pk],
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
medium.refresh_from_db()
|
||||
with scopes_disabled():
|
||||
assert list(medium.linked_orderpositions.values_list('pk', flat=True)) == [op.pk, op2.pk]
|
||||
assert medium.all_logentries().count() == 3
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, medium.pk),
|
||||
{
|
||||
'linked_orderpositions': [op2.pk],
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
medium.refresh_from_db()
|
||||
with scopes_disabled():
|
||||
assert list(medium.linked_orderpositions.values_list('pk', flat=True)) == [op2.pk]
|
||||
assert medium.all_logentries().count() == 4
|
||||
|
||||
resp = token_client.patch(
|
||||
'/api/v1/organizers/{}/reusablemedia/{}/'.format(organizer.slug, medium.pk),
|
||||
{
|
||||
'linked_orderposition': op.pk,
|
||||
'linked_orderpositions': [op.pk, op2.pk],
|
||||
},
|
||||
format='json'
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_medium_no_deletion(token_client, organizer, event, medium):
|
||||
@@ -707,7 +538,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_orderpositions.add(op)
|
||||
medium2.linked_orderposition = op
|
||||
medium2.linked_giftcard = giftcard2
|
||||
medium2.save()
|
||||
|
||||
|
||||
@@ -137,7 +137,6 @@ event_urls = [
|
||||
"subevents/select2",
|
||||
"subevents/add",
|
||||
"subevents/2/delete",
|
||||
"subevents/2/edit",
|
||||
"subevents/2/",
|
||||
"quotas/",
|
||||
"quotas/2/delete",
|
||||
@@ -361,9 +360,8 @@ event_permission_urls = [
|
||||
("event.items:write", "discounts/reorder", 400, HTTP_POST),
|
||||
("event.items:write", "discounts/add", 200, HTTP_GET),
|
||||
(None, "subevents/", 200, HTTP_GET),
|
||||
(None, "subevents/2/", 404, HTTP_GET),
|
||||
("event.subevents:write", "subevents/2/edit", 404, HTTP_GET),
|
||||
("event.subevents:write", "subevents/2/edit", 404, HTTP_POST),
|
||||
("event.subevents:write", "subevents/2/", 404, HTTP_GET),
|
||||
("event.subevents:write", "subevents/2/", 404, HTTP_POST),
|
||||
("event.subevents:write", "subevents/2/delete", 404, HTTP_GET),
|
||||
("event.subevents:write", "subevents/add", 200, HTTP_GET),
|
||||
("event.subevents:write", "subevents/bulk_add", 200, HTTP_GET),
|
||||
|
||||
@@ -110,9 +110,9 @@ class SubEventsTest(SoupTest):
|
||||
assert se.checkinlist_set.count() == 1
|
||||
|
||||
def test_modify(self):
|
||||
doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/edit' % self.subevent1.pk)
|
||||
doc = self.get_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk)
|
||||
assert doc.select("input[name=quotas-TOTAL_FORMS]")
|
||||
doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/edit' % self.subevent1.pk, {
|
||||
doc = self.post_doc('/control/event/ccc/30c3/subevents/%d/' % self.subevent1.pk, {
|
||||
'name_0': 'SE2',
|
||||
'active': 'on',
|
||||
'date_from_0': '2017-07-01',
|
||||
|
||||
@@ -731,13 +731,11 @@ def event_series(organizer):
|
||||
"""Create an event series with multiple subevents, items, and quotas."""
|
||||
from pretix.base.models import ItemCategory
|
||||
|
||||
base_date = _future_dt(days=30, hour=19)
|
||||
|
||||
event = Event.objects.create(
|
||||
organizer=organizer,
|
||||
name='Concert Series',
|
||||
slug='concert-series',
|
||||
date_from=base_date,
|
||||
date_from=_future_dt(days=30, hour=19),
|
||||
has_subevents=True,
|
||||
currency='EUR',
|
||||
live=True,
|
||||
@@ -762,8 +760,9 @@ def event_series(organizer):
|
||||
)
|
||||
|
||||
subevents = []
|
||||
base_date = _future_dt(days=30, hour=19)
|
||||
|
||||
for i in range(20):
|
||||
for i in range(15):
|
||||
se = SubEvent.objects.create(
|
||||
event=event,
|
||||
name=f'Concert Night {i + 1}',
|
||||
|
||||
Reference in New Issue
Block a user