forked from CGM_Public/pretix_original
Compare commits
26 Commits
v4.11.1
...
addon-matc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec48c2a373 | ||
|
|
b1b4177947 | ||
|
|
fe28a8f539 | ||
|
|
86be7b7934 | ||
|
|
5ded99c74a | ||
|
|
a52cee2c45 | ||
|
|
c2cb968b82 | ||
|
|
52fafa115c | ||
|
|
39f7bfe16f | ||
|
|
3c0ba3c8e8 | ||
|
|
db1c480905 | ||
|
|
96b57f9a50 | ||
|
|
0faf245290 | ||
|
|
cee72b5a6d | ||
|
|
76e8cc42c2 | ||
|
|
d22feada57 | ||
|
|
c792621bcb | ||
|
|
d9a58cf27f | ||
|
|
79ba2185fd | ||
|
|
fcf4750d5f | ||
|
|
5c56139b56 | ||
|
|
1f8da968ba | ||
|
|
6ee034784d | ||
|
|
1ab701c100 | ||
|
|
f0661fb11c | ||
|
|
443283de66 |
@@ -34,6 +34,7 @@ allow_multiple_entries boolean If ``true``, su
|
||||
allow_entry_after_exit boolean If ``true``, subsequent scans of a ticket on this list are valid if the last scan of the ticket was an exit scan.
|
||||
rules object Custom check-in logic. The contents of this field are currently not considered a stable API and modifications through the API are highly discouraged.
|
||||
exit_all_at datetime Automatically check out (i.e. perform an exit scan) at this point in time. After this happened, this property will automatically be set exactly one day into the future. Note that this field is considered "internal configuration" and if you pull the list with ``If-Modified-Since``, the daily change in this field will not trigger a response.
|
||||
addon_match boolean If ``true``, tickets on this list can be redeemed by scanning their parent ticket if this still leads to an unambiguous match.
|
||||
===================================== ========================== =======================================================
|
||||
|
||||
.. versionchanged:: 3.9
|
||||
@@ -53,6 +54,10 @@ exit_all_at datetime Automatically c
|
||||
|
||||
The ``ends_after`` and ``expand`` query parameters have been added.
|
||||
|
||||
.. versionchanged:: 4.12
|
||||
|
||||
The ``addon_match`` attribute has been added.
|
||||
|
||||
Endpoints
|
||||
---------
|
||||
|
||||
@@ -94,6 +99,7 @@ Endpoints
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -146,6 +152,7 @@ Endpoints
|
||||
"allow_entry_after_exit": true,
|
||||
"exit_all_at": null,
|
||||
"rules": {},
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -245,6 +252,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -269,6 +277,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -323,6 +332,7 @@ Endpoints
|
||||
"subevent": null,
|
||||
"allow_multiple_entries": false,
|
||||
"allow_entry_after_exit": true,
|
||||
"addon_match": false,
|
||||
"auto_checkin_sales_channels": [
|
||||
"pretixpos"
|
||||
]
|
||||
@@ -611,8 +621,12 @@ Order position endpoints
|
||||
Tries to redeem an order position, identified by its internal ID, i.e. checks the attendee in. This endpoint
|
||||
accepts a number of optional requests in the body.
|
||||
|
||||
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter.
|
||||
**Tip:** Instead of an ID, you can also use the ``secret`` field as the lookup parameter. In this case, you should
|
||||
always set ``untrusted_input=true`` as a query parameter to avoid security issues.
|
||||
|
||||
:query boolean untrusted_input: If set to true, the lookup parameter is **always** interpreted as a ``secret``, never
|
||||
as an ``id``. This should be always set if you are passing through untrusted, scanned
|
||||
data to avoid guessing of ticket IDs.
|
||||
:<json boolean questions_supported: When this parameter is set to ``true``, handling of questions is supported. If
|
||||
you do not implement question handling in your user interface, you **must**
|
||||
set this to ``false``. In that case, questions will just be ignored. Defaults
|
||||
@@ -739,6 +753,7 @@ Order position endpoints
|
||||
* ``already_redeemed`` - Ticket already has been redeemed
|
||||
* ``product`` - Tickets with this product may not be scanned at this device
|
||||
* ``rules`` - Check-in prevented by a user-defined rule
|
||||
* ``ambiguous`` - Multiple tickets match scan, rejected
|
||||
|
||||
In case of reason ``rules``, there might be an additional response field ``reason_explanation`` with a human-readable
|
||||
description of the violated rules. However, that field can also be missing or be ``null``.
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 236 KiB After Width: | Height: | Size: 274 KiB |
@@ -2,8 +2,25 @@
|
||||
|
||||
|
||||
partition "data-based check" {
|
||||
"Check based on local database" --> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
"Check based on local database" -down-> "Is addon_match set to true?"
|
||||
--> if "" then
|
||||
-down->[no] "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
else
|
||||
-right->[yes] "Build a list that includes the position\nas well as all its add-ons"
|
||||
-down-> "Filter list for products that are part of the check-in list"
|
||||
--> if "" then
|
||||
-down->[one found] Proceed with the matching position
|
||||
--> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
else
|
||||
--> if "" then
|
||||
-right->[none found] "Return error PRODUCT "
|
||||
else
|
||||
-down->[multiple found] Return error AMBIGUOUS
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
"Is the order in status PAID or PENDING\nand is the position not canceled?" --> if "" then
|
||||
-right->[no] "Return error CANCELED"
|
||||
else
|
||||
-down->[yes] "Is the product part of the check-in list?"
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 147 KiB After Width: | Height: | Size: 175 KiB |
@@ -19,8 +19,25 @@ else
|
||||
endif
|
||||
|
||||
|
||||
===CHECK=== -down-> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
===CHECK=== -down-> "Is addon_match set to true?"
|
||||
--> if "" then
|
||||
-down->[no] "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
else
|
||||
-right->[yes] "Build a list that includes the position\nas well as all its add-ons"
|
||||
-down-> "Filter list for products that are part of the check-in list"
|
||||
--> if "" then
|
||||
-down->[one found] Proceed with the matching position
|
||||
--> "Is the order in status PAID or PENDING\nand is the position not canceled?"
|
||||
else
|
||||
--> if "" then
|
||||
-right->[none found] "Return error PRODUCT "
|
||||
else
|
||||
-down->[multiple found] Return error AMBIGUOUS
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
|
||||
"Is the order in status PAID or PENDING\nand is the position not canceled?" --> if "" then
|
||||
-right->[no] "Return error CANCELED"
|
||||
else
|
||||
-down->[yes] "Is the product part of the check-in list?"
|
||||
|
||||
@@ -78,7 +78,7 @@ Synchronization setting any
|
||||
----------------------------------------------- ----------------------------------- ----------------------------------------------------------------------- -----------------------------------------------------------------------
|
||||
Ticket secrets any Random Signed Random Signed
|
||||
=============================================== =================================== =================================== =================================== ================================= =====================================
|
||||
Scenario supported on platforms Android, Desktop, iOS Android, Desktop, iOS Android, Desktop Android, Desktop Android, Desktop
|
||||
Scenario supported on platforms Android, Desktop, iOS Android, Desktop, iOS Android, Desktop Android, Desktop, iOS Android, Desktop, iOS
|
||||
Synchronization speed for large data sets slow slow fast fast
|
||||
Tickets can be scanned yes yes yes no yes
|
||||
Ticket is valid after sale immediately next sync (~5 minutes) immediately never immediately
|
||||
@@ -90,6 +90,7 @@ Name and seat visible on scanner yes
|
||||
Order-specific check-in attention flag yes yes yes (except directly after sale) n/a no
|
||||
Ticket search by order code or name yes yes yes (except directly after sale) no no
|
||||
Check-in statistics on scanner yes yes mostly accurate no no
|
||||
Support for add-on check-in with main ticket yes yes yes (except directly after sale) no no
|
||||
=============================================== =================================== =================================== =================================== ================================= =====================================
|
||||
|
||||
.. _EdDSA: https://en.wikipedia.org/wiki/EdDSA#Ed25519
|
||||
|
||||
@@ -135,6 +135,10 @@ Alternatively, you can select one or more categories to be shown::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" categories="12,25"></pretix-widget>
|
||||
|
||||
Or variation IDs::
|
||||
|
||||
<pretix-widget event="https://pretix.eu/demo/democon/" variations="15,2,68"></pretix-widget>
|
||||
|
||||
Multi-event selection
|
||||
---------------------
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ recursive-include pretix/plugins/manualpayment/templates *
|
||||
recursive-include pretix/plugins/manualpayment/static *
|
||||
recursive-include pretix/plugins/paypal/templates *
|
||||
recursive-include pretix/plugins/paypal/static *
|
||||
recursive-include pretix/plugins/paypal2/templates *
|
||||
recursive-include pretix/plugins/paypal2/static *
|
||||
recursive-include pretix/plugins/pretixdroid/templates *
|
||||
recursive-include pretix/plugins/pretixdroid/static *
|
||||
recursive-include pretix/plugins/sendmail/templates *
|
||||
|
||||
@@ -19,4 +19,4 @@
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
__version__ = "4.11.0"
|
||||
__version__ = "4.12.0.dev0"
|
||||
|
||||
@@ -37,7 +37,7 @@ class CheckinListSerializer(I18nAwareModelSerializer):
|
||||
model = CheckinList
|
||||
fields = ('id', 'name', 'all_products', 'limit_products', 'subevent', 'checkin_count', 'position_count',
|
||||
'include_pending', 'auto_checkin_sales_channels', 'allow_multiple_entries', 'allow_entry_after_exit',
|
||||
'rules', 'exit_all_at')
|
||||
'rules', 'exit_all_at', 'addon_match')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@@ -25,6 +25,7 @@ from django.core.exceptions import ValidationError
|
||||
from django.db import transaction
|
||||
from django.db.models import (
|
||||
Count, Exists, F, Max, OrderBy, OuterRef, Prefetch, Q, Subquery,
|
||||
prefetch_related_objects,
|
||||
)
|
||||
from django.db.models.functions import Coalesce
|
||||
from django.http import Http404
|
||||
@@ -280,7 +281,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
serializer_class = CheckinListOrderPositionSerializer
|
||||
queryset = OrderPosition.all.none()
|
||||
filter_backends = (ExtendedBackend, RichOrderingFilter)
|
||||
ordering = ('attendee_name_cached', 'positionid')
|
||||
ordering = (F('attendee_name_cached').asc(nulls_last=True), 'positionid')
|
||||
ordering_fields = (
|
||||
'order__code', 'order__datetime', 'positionid', 'attendee_name',
|
||||
'last_checked_in', 'order__email',
|
||||
@@ -408,6 +409,13 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
raise ValidationError("Invalid check-in type.")
|
||||
ignore_unpaid = bool(self.request.data.get('ignore_unpaid', False))
|
||||
nonce = self.request.data.get('nonce')
|
||||
untrusted_input = (
|
||||
self.request.GET.get('untrusted_input', '') not in ('0', 'false', 'False', '')
|
||||
or (isinstance(self.request.auth, Device) and 'pretixscan' in (self.request.auth.software_brand or '').lower())
|
||||
)
|
||||
|
||||
if not self.checkinlist.all_products:
|
||||
prefetch_related_objects([self.checkinlist], 'limit_products')
|
||||
|
||||
if 'datetime' in self.request.data:
|
||||
dt = DateTimeField().to_internal_value(self.request.data.get('datetime'))
|
||||
@@ -427,19 +435,32 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
raw_barcode_for_checkin = None
|
||||
from_revoked_secret = False
|
||||
|
||||
try:
|
||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True)
|
||||
if self.kwargs['pk'].isnumeric():
|
||||
op = queryset.get(Q(pk=self.kwargs['pk']) | Q(secret=self.kwargs['pk']))
|
||||
else:
|
||||
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
|
||||
# `id`, however, is part of a path where this technically is not allowed. Old versions of our
|
||||
# scan apps still do it, so we try work around it!
|
||||
try:
|
||||
op = queryset.get(secret=self.kwargs['pk'])
|
||||
except OrderPosition.DoesNotExist:
|
||||
op = queryset.get(secret=self.kwargs['pk'].replace('+', ' '))
|
||||
except OrderPosition.DoesNotExist:
|
||||
# 1. Gather a list of positions that could be the one we looking fore, either from their ID, secret or
|
||||
# parent secret
|
||||
queryset = self.get_queryset(ignore_status=True, ignore_products=True).order_by(
|
||||
F('addon_to').asc(nulls_first=True)
|
||||
)
|
||||
|
||||
q = Q(secret=self.kwargs['pk'])
|
||||
if self.checkinlist.addon_match:
|
||||
q |= Q(addon_to__secret=self.kwargs['pk'])
|
||||
if self.kwargs['pk'].isnumeric() and not untrusted_input:
|
||||
q |= Q(pk=self.kwargs['pk'])
|
||||
|
||||
op_candidates = list(queryset.filter(q))
|
||||
if not op_candidates and '+' in self.kwargs['pk']:
|
||||
# In application/x-www-form-urlencoded, you can encodes space ' ' with '+' instead of '%20'.
|
||||
# `id`, however, is part of a path where this technically is not allowed. Old versions of our
|
||||
# scan apps still do it, so we try work around it!
|
||||
q = Q(secret=self.kwargs['pk'].replace('+', ' '))
|
||||
if self.checkinlist.addon_match:
|
||||
q |= Q(addon_to__secret=self.kwargs['pk'].replace('+', ' '))
|
||||
op_candidates = list(queryset.filter(q))
|
||||
|
||||
# 2. Handle the "nothing found" case: Either it's really a bogus secret that we don't know (-> error), or it
|
||||
# might be a revoked one that we actually know (-> error, but with better error message and logging and
|
||||
# with respecting the force option).
|
||||
if not op_candidates:
|
||||
revoked_matches = list(self.request.event.revoked_secrets.filter(secret=self.kwargs['pk']))
|
||||
if len(revoked_matches) == 0:
|
||||
self.request.event.log_action('pretix.event.checkin.unknown', data={
|
||||
@@ -499,7 +520,9 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'require_attention': False,
|
||||
}, status=404)
|
||||
elif revoked_matches and force:
|
||||
op = revoked_matches[0].position
|
||||
op_candidates = [revoked_matches[0].position]
|
||||
if self.checkinlist.addon_match:
|
||||
op_candidates += list(revoked_matches[0].position.addons.all())
|
||||
raw_barcode_for_checkin = self.kwargs['pk']
|
||||
from_revoked_secret = True
|
||||
else:
|
||||
@@ -524,6 +547,56 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||
}, status=400)
|
||||
|
||||
# 3. Handle the "multiple options found" case: Except for the unlikely case of a secret being also a valid primary
|
||||
# key on the same list, we're probably dealing with the ``addon_match`` case here and need to figure out
|
||||
# which add-on has the right product.
|
||||
if len(op_candidates) > 1:
|
||||
if self.checkinlist.addon_match and not self.checkinlist.all_products:
|
||||
op_candidates_matching_product = [
|
||||
op for op in op_candidates if op.item_id in {i.pk for i in self.checkinlist.limit_products.all()}
|
||||
]
|
||||
else:
|
||||
op_candidates_matching_product = op_candidates
|
||||
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.
|
||||
# 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.
|
||||
op = op_candidates[0]
|
||||
op.order.log_action('pretix.event.checkin.denied', data={
|
||||
'position': op.id,
|
||||
'positionid': op.positionid,
|
||||
'errorcode': Checkin.REASON_AMBIGUOUS,
|
||||
'reason_explanation': None,
|
||||
'force': force,
|
||||
'datetime': dt,
|
||||
'type': type,
|
||||
'list': self.checkinlist.pk
|
||||
}, user=self.request.user, auth=self.request.auth)
|
||||
Checkin.objects.create(
|
||||
position=op,
|
||||
successful=False,
|
||||
error_reason=Checkin.REASON_AMBIGUOUS,
|
||||
error_explanation=None,
|
||||
**common_checkin_args,
|
||||
)
|
||||
return Response({
|
||||
'status': 'error',
|
||||
'reason': Checkin.REASON_AMBIGUOUS,
|
||||
'reason_explanation': None,
|
||||
'require_attention': op.item.checkin_attention or op.order.checkin_attention,
|
||||
'position': CheckinListOrderPositionSerializer(op, context=self.get_serializer_context()).data
|
||||
}, status=400)
|
||||
else:
|
||||
op_candidates = op_candidates_matching_product
|
||||
|
||||
op = op_candidates[0]
|
||||
|
||||
# 5. Pre-validate all incoming answers, handle file upload
|
||||
given_answers = {}
|
||||
if 'answers' in self.request.data:
|
||||
aws = self.request.data.get('answers')
|
||||
@@ -537,6 +610,7 @@ class CheckinListPositionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||
except ValidationError:
|
||||
pass
|
||||
|
||||
# 6. Pass to our actual check-in logic
|
||||
with language(self.request.event.settings.locale):
|
||||
try:
|
||||
perform_checkin(
|
||||
|
||||
@@ -475,8 +475,11 @@ def base_placeholders(sender, **kwargs):
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'event_admission_time', ['event_or_subevent'],
|
||||
lambda event_or_subevent: date_format(event_or_subevent.date_admission, 'TIME_FORMAT') if event_or_subevent.date_admission else '',
|
||||
lambda event: date_format(event.date_admission, 'TIME_FORMAT') if event.date_admission else '',
|
||||
lambda event_or_subevent:
|
||||
date_format(event_or_subevent.date_admission.astimezone(event_or_subevent.timezone), 'TIME_FORMAT')
|
||||
if event_or_subevent.date_admission
|
||||
else '',
|
||||
lambda event: date_format(event.date_admission.astimezone(event.timezone), 'TIME_FORMAT') if event.date_admission else '',
|
||||
),
|
||||
SimpleFunctionalMailTextPlaceholder(
|
||||
'subevent', ['waiting_list_entry', 'event'],
|
||||
|
||||
@@ -23,6 +23,7 @@ from .answers import * # noqa
|
||||
from .dekodi import * # noqa
|
||||
from .events import * # noqa
|
||||
from .invoices import * # noqa
|
||||
from .items import * # noqa
|
||||
from .json import * # noqa
|
||||
from .mail import * # noqa
|
||||
from .orderlist import * # noqa
|
||||
|
||||
222
src/pretix/base/exporters/items.py
Normal file
222
src/pretix/base/exporters/items.py
Normal file
@@ -0,0 +1,222 @@
|
||||
#
|
||||
# This file is part of pretix (Community Edition).
|
||||
#
|
||||
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
||||
# Copyright (C) 2020-2021 rami.io GmbH and contributors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
||||
# Public License as published by the Free Software Foundation in version 3 of the License.
|
||||
#
|
||||
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
||||
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
||||
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
||||
# this file, see <https://pretix.eu/about/en/license>.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
||||
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||
# details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
||||
# <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
from django.db.models import Prefetch
|
||||
from django.dispatch import receiver
|
||||
from django.utils.formats import date_format
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from openpyxl.styles import Alignment
|
||||
from openpyxl.utils import get_column_letter
|
||||
|
||||
from ...helpers.safe_openpyxl import SafeCell
|
||||
from ..channels import get_all_sales_channels
|
||||
from ..exporter import ListExporter
|
||||
from ..models import ItemMetaValue
|
||||
from ..signals import register_data_exporters
|
||||
|
||||
|
||||
def _max(a1, a2):
|
||||
if a1 and a2:
|
||||
return max(a1, a2)
|
||||
return a1 or a2
|
||||
|
||||
|
||||
def _min(a1, a2):
|
||||
if a1 and a2:
|
||||
return min(a1, a2)
|
||||
return a1 or a2
|
||||
|
||||
|
||||
class ItemDataExporter(ListExporter):
|
||||
identifier = 'itemdata'
|
||||
verbose_name = _('Product data')
|
||||
|
||||
def iterate_list(self, form_data):
|
||||
locales = self.event.settings.locales
|
||||
scs = get_all_sales_channels()
|
||||
header = [
|
||||
_("Product ID"),
|
||||
_("Variation ID"),
|
||||
_("Product category"),
|
||||
_("Internal name"),
|
||||
]
|
||||
for l in locales:
|
||||
header.append(
|
||||
_("Item name") + f" ({l})"
|
||||
)
|
||||
for l in locales:
|
||||
header.append(
|
||||
_("Variation") + f" ({l})"
|
||||
)
|
||||
header += [
|
||||
_("Active"),
|
||||
_("Sales channels"),
|
||||
_("Default price"),
|
||||
_("Free price input"),
|
||||
_("Sales tax"),
|
||||
_("Is an admission ticket"),
|
||||
_("Generate tickets"),
|
||||
_("Waiting list"),
|
||||
_("Available from"),
|
||||
_("Available until"),
|
||||
_("This product can only be bought using a voucher."),
|
||||
_("This product will only be shown if a voucher matching the product is redeemed."),
|
||||
_("Buying this product requires approval"),
|
||||
_("Only sell this product as part of a bundle"),
|
||||
_("Allow product to be canceled or changed"),
|
||||
_("Minimum amount per order"),
|
||||
_("Maximum amount per order"),
|
||||
_("Requires special attention"),
|
||||
_("Original price"),
|
||||
_("This product is a gift card"),
|
||||
_("Require a valid membership"),
|
||||
_("Hide without a valid membership"),
|
||||
]
|
||||
props = list(self.event.item_meta_properties.all())
|
||||
for p in props:
|
||||
header.append(p.name)
|
||||
|
||||
if form_data["_format"] == "xlsx":
|
||||
row = []
|
||||
for h in header:
|
||||
c = SafeCell(self.__ws, value=h)
|
||||
c.alignment = Alignment(wrap_text=True, vertical='top')
|
||||
row.append(c)
|
||||
else:
|
||||
row = header
|
||||
|
||||
yield row
|
||||
|
||||
for i in self.event.items.prefetch_related(
|
||||
'variations',
|
||||
Prefetch(
|
||||
'meta_values',
|
||||
ItemMetaValue.objects.select_related('property'),
|
||||
to_attr='meta_values_cached'
|
||||
)
|
||||
).select_related('category', 'tax_rule'):
|
||||
m = i.meta_data
|
||||
vars = list(i.variations.all())
|
||||
|
||||
if vars:
|
||||
for v in vars:
|
||||
row = [
|
||||
i.pk,
|
||||
v.pk,
|
||||
str(i.category) if i.category else "",
|
||||
i.internal_name or "",
|
||||
]
|
||||
for l in locales:
|
||||
row.append(i.name.localize(l))
|
||||
for l in locales:
|
||||
row.append(v.value.localize(l))
|
||||
row += [
|
||||
_("Yes") if i.active and v.active else "",
|
||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels and s in v.sales_channels]),
|
||||
v.default_price or i.default_price,
|
||||
_("Yes") if i.free_price else "",
|
||||
str(i.tax_rule) if i.tax_rule else "",
|
||||
_("Yes") if i.admission else "",
|
||||
_("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""),
|
||||
_("Yes") if i.allow_waitinglist else "",
|
||||
date_format(_max(i.available_from, v.available_from).astimezone(self.timezone),
|
||||
"SHORT_DATETIME_FORMAT") if i.available_from or v.available_from else "",
|
||||
date_format(_min(i.available_until, v.available_until).astimezone(self.timezone),
|
||||
"SHORT_DATETIME_FORMAT") if i.available_until or v.available_until else "",
|
||||
_("Yes") if i.require_voucher else "",
|
||||
_("Yes") if i.hide_without_voucher or v.hide_without_voucher else "",
|
||||
_("Yes") if i.require_approval or v.require_approval else "",
|
||||
_("Yes") if i.require_bundling else "",
|
||||
_("Yes") if i.allow_cancel else "",
|
||||
i.min_per_order if i.min_per_order is not None else "",
|
||||
i.max_per_order if i.max_per_order is not None else "",
|
||||
_("Yes") if i.checkin_attention else "",
|
||||
v.original_price or i.original_price or "",
|
||||
_("Yes") if i.issue_giftcard else "",
|
||||
_("Yes") if i.require_membership or v.require_membership else "",
|
||||
_("Yes") if i.require_membership_hidden or v.require_membership_hidden else "",
|
||||
]
|
||||
row += [
|
||||
m.get(p.name, '') for p in props
|
||||
]
|
||||
yield row
|
||||
|
||||
else:
|
||||
row = [
|
||||
i.pk,
|
||||
"",
|
||||
str(i.category) if i.category else "",
|
||||
i.internal_name or "",
|
||||
]
|
||||
for l in locales:
|
||||
row.append(i.name.localize(l))
|
||||
for l in locales:
|
||||
row.append("")
|
||||
row += [
|
||||
_("Yes") if i.active else "",
|
||||
", ".join([str(sn.verbose_name) for s, sn in scs.items() if s in i.sales_channels]),
|
||||
i.default_price,
|
||||
_("Yes") if i.free_price else "",
|
||||
str(i.tax_rule) if i.tax_rule else "",
|
||||
_("Yes") if i.admission else "",
|
||||
_("Yes") if i.generate_tickets else (_("Default") if i.generate_tickets is None else ""),
|
||||
_("Yes") if i.allow_waitinglist else "",
|
||||
date_format(i.available_from.astimezone(self.timezone),
|
||||
"SHORT_DATETIME_FORMAT") if i.available_from else "",
|
||||
date_format(i.available_until.astimezone(self.timezone),
|
||||
"SHORT_DATETIME_FORMAT") if i.available_until else "",
|
||||
_("Yes") if i.require_voucher else "",
|
||||
_("Yes") if i.hide_without_voucher else "",
|
||||
_("Yes") if i.require_approval else "",
|
||||
_("Yes") if i.require_bundling else "",
|
||||
_("Yes") if i.allow_cancel else "",
|
||||
i.min_per_order if i.min_per_order is not None else "",
|
||||
i.max_per_order if i.max_per_order is not None else "",
|
||||
_("Yes") if i.checkin_attention else "",
|
||||
i.original_price or "",
|
||||
_("Yes") if i.issue_giftcard else "",
|
||||
_("Yes") if i.require_membership else "",
|
||||
_("Yes") if i.require_membership_hidden else "",
|
||||
]
|
||||
|
||||
row += [
|
||||
m.get(p.name, '') for p in props
|
||||
]
|
||||
yield row
|
||||
|
||||
def get_filename(self):
|
||||
return '{}_products'.format(self.events.first().organizer.slug)
|
||||
|
||||
def prepare_xlsx_sheet(self, ws):
|
||||
self.__ws = ws
|
||||
ws.freeze_panes = 'A1'
|
||||
ws.column_dimensions['C'].width = 25
|
||||
ws.column_dimensions['D'].width = 25
|
||||
for i in range(len(self.event.settings.locales)):
|
||||
ws.column_dimensions[get_column_letter(5 + 2 * i + 0)].width = 25
|
||||
ws.column_dimensions[get_column_letter(5 + 2 * i + 1)].width = 25
|
||||
ws.column_dimensions[get_column_letter(5 + 2 * len(self.event.settings.locales) + 1)].width = 25
|
||||
ws.row_dimensions[1].height = 40
|
||||
|
||||
|
||||
@receiver(register_data_exporters, dispatch_uid="exporter_itemdata")
|
||||
def register_itemdata_exporter(sender, **kwargs):
|
||||
return ItemDataExporter
|
||||
18
src/pretix/base/migrations/0218_checkinlist_addon_match.py
Normal file
18
src/pretix/base/migrations/0218_checkinlist_addon_match.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.2.2 on 2022-06-29 17:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pretixbase', '0217_eventfooterlink_organizerfooterlink'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='checkinlist',
|
||||
name='addon_match',
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -56,6 +56,12 @@ class CheckinList(LoggedModel):
|
||||
default=False,
|
||||
help_text=_('With this option, people will be able to check in even if the '
|
||||
'order has not been paid.'))
|
||||
addon_match = models.BooleanField(
|
||||
verbose_name=_('Allow checking in add-on tickets by scanning the main ticket'),
|
||||
default=False,
|
||||
help_text=_('A scan will only be possible if the check-in list is configured such that there is always exactly '
|
||||
'one matching add-on ticket. Ambiguous scans will be rejected..')
|
||||
)
|
||||
gates = models.ManyToManyField(
|
||||
'Gate', verbose_name=_("Gates"), blank=True,
|
||||
help_text=_("Does not have any effect for the validation of tickets, only for the automatic configuration of "
|
||||
@@ -258,6 +264,7 @@ class Checkin(models.Model):
|
||||
REASON_REVOKED = 'revoked'
|
||||
REASON_INCOMPLETE = 'incomplete'
|
||||
REASON_ALREADY_REDEEMED = 'already_redeemed'
|
||||
REASON_AMBIGUOUS = 'ambiguous'
|
||||
REASON_ERROR = 'error'
|
||||
REASONS = (
|
||||
(REASON_CANCELED, _('Order canceled')),
|
||||
@@ -268,6 +275,7 @@ class Checkin(models.Model):
|
||||
(REASON_INCOMPLETE, _('Information required')),
|
||||
(REASON_ALREADY_REDEEMED, _('Ticket already used')),
|
||||
(REASON_PRODUCT, _('Ticket type not allowed here')),
|
||||
(REASON_AMBIGUOUS, _('Ticket code is ambiguous on list')),
|
||||
(REASON_ERROR, _('Server error')),
|
||||
)
|
||||
|
||||
|
||||
@@ -843,7 +843,7 @@ class Order(LockModel, LoggedModel):
|
||||
if terms:
|
||||
term_last = min(terms)
|
||||
else:
|
||||
term_last = None
|
||||
return None
|
||||
else:
|
||||
term_last = term_last.datetime(self.event).date()
|
||||
term_last = make_aware(datetime.combine(
|
||||
@@ -1588,7 +1588,7 @@ class OrderPayment(models.Model):
|
||||
if status_change:
|
||||
self.order.create_transactions()
|
||||
|
||||
def fail(self, info=None, user=None, auth=None):
|
||||
def fail(self, info=None, user=None, auth=None, log_data=None):
|
||||
"""
|
||||
Marks the order as failed and sets info to ``info``, but only if the order is in ``created`` or ``pending``
|
||||
state. This is equivalent to setting ``state`` to ``OrderPayment.PAYMENT_STATE_FAILED`` and logging a failure,
|
||||
@@ -1616,6 +1616,7 @@ class OrderPayment(models.Model):
|
||||
'local_id': self.local_id,
|
||||
'provider': self.provider,
|
||||
'info': info,
|
||||
'data': log_data,
|
||||
}, user=user, auth=auth)
|
||||
|
||||
def confirm(self, count_waitinglist=True, send_mail=True, force=False, user=None, auth=None, mail_text='',
|
||||
|
||||
@@ -143,8 +143,8 @@ class Sig1TicketSecretGenerator(BaseTicketSecretGenerator):
|
||||
The resulting string is REVERSED, to avoid all secrets of same length beginning with the same 10
|
||||
characters, which would make it impossible to search for secrets manually.
|
||||
"""
|
||||
verbose_name = _('pretix signature scheme 1 (for very large events, does not work with pretixSCAN on iOS and '
|
||||
'changes semantics of offline scanning – please refer to documentation or support for details)')
|
||||
verbose_name = _('pretix signature scheme 1 (for very large events, changes semantics of offline scanning – '
|
||||
'please refer to documentation or support for details)')
|
||||
identifier = 'pretix_sig1'
|
||||
use_revocation_list = True
|
||||
|
||||
|
||||
@@ -96,6 +96,7 @@ ALLOWED_ATTRIBUTES = {
|
||||
'div': ['class'],
|
||||
'p': ['class'],
|
||||
'span': ['class', 'title'],
|
||||
'ol': ['start'],
|
||||
# Update doc/user/markdown.rst if you change this!
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,10 @@
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.urls import reverse
|
||||
from django.utils.timezone import get_current_timezone, make_aware, now
|
||||
from django.utils.translation import pgettext_lazy
|
||||
from django.utils.translation import gettext_lazy as _, pgettext_lazy
|
||||
from django_scopes.forms import (
|
||||
SafeModelChoiceField, SafeModelMultipleChoiceField,
|
||||
)
|
||||
@@ -109,6 +110,7 @@ class CheckinListForm(forms.ModelForm):
|
||||
'rules',
|
||||
'gates',
|
||||
'exit_all_at',
|
||||
'addon_match',
|
||||
]
|
||||
widgets = {
|
||||
'limit_products': forms.CheckboxSelectMultiple(attrs={
|
||||
@@ -130,6 +132,12 @@ class CheckinListForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
d = super().clean()
|
||||
d['rules'] = CheckinList.validate_rules(d.get('rules'))
|
||||
|
||||
if d.get('addon_match') and d.get('all_products'):
|
||||
raise ValidationError(_('If you allow checking in add-on tickets by scanning the main ticket, you must '
|
||||
'select a specific set of products for this check-in list, only including the '
|
||||
'possible add-on products.'))
|
||||
|
||||
return d
|
||||
|
||||
|
||||
|
||||
@@ -366,7 +366,7 @@ def pretixcontrol_logentry_display(sender: Event, logentry: LogEntry, **kwargs):
|
||||
'pretix.event.order.payment.changed': _('A new payment {local_id} has been started instead of the previous one.'),
|
||||
'pretix.event.order.email.sent': _('An unidentified type email has been sent.'),
|
||||
'pretix.event.order.email.error': _('Sending of an email has failed.'),
|
||||
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attachments since they '
|
||||
'pretix.event.order.email.attachments.skipped': _('The email has been sent without attached tickets since they '
|
||||
'would have been too large to be likely to arrive.'),
|
||||
'pretix.event.order.email.custom_sent': _('A custom email has been sent.'),
|
||||
'pretix.event.order.position.email.custom_sent': _('A custom email has been sent to an attendee.'),
|
||||
|
||||
@@ -48,16 +48,14 @@
|
||||
Make sure to always use the latest version of our scanning apps for these options to work.
|
||||
{% endblocktrans %}
|
||||
<br>
|
||||
<strong>
|
||||
{% blocktrans trimmed %}
|
||||
If you make use of these advanced options, we recommend using our Android and Desktop apps.
|
||||
Custom check-in rules do not work offline with our iOS scanning app.
|
||||
{% endblocktrans %}
|
||||
</strong>
|
||||
{% blocktrans trimmed %}
|
||||
If you make use of these advanced options, we recommend using our Android and Desktop apps.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
{% bootstrap_field form.allow_multiple_entries layout="control" %}
|
||||
{% bootstrap_field form.allow_entry_after_exit layout="control" %}
|
||||
{% bootstrap_field form.addon_match layout="control" %}
|
||||
{% bootstrap_field form.exit_all_at layout="control" %}
|
||||
{% bootstrap_field form.auto_checkin_sales_channels layout="control" %}
|
||||
{% if form.gates %}
|
||||
|
||||
@@ -32,7 +32,7 @@ class PayPalHttpClient(VendorPayPalHttpClient):
|
||||
# Cached access tokens are not updated by PayPal to include new Merchants that granted access rights since
|
||||
# the access token was generated. Therefor we increment the cycle count and by that invalidate the cached
|
||||
# token and pull a new one.
|
||||
incr = cache.get('pretix_paypal_token_hash_cycle', default=0)
|
||||
incr = cache.get('pretix_paypal_token_hash_cycle', default=1)
|
||||
|
||||
# Then we get all the items that make up the current credentials and create a hash to detect changes
|
||||
checksum = hashlib.sha256(''.join([
|
||||
|
||||
@@ -354,6 +354,9 @@ class PaypalMethod(BasePaymentProvider):
|
||||
|
||||
@property
|
||||
def is_enabled(self) -> bool:
|
||||
if self.settings.connect_client_id and self.settings.connect_secret_key and not self.settings.secret:
|
||||
if not self.settings.isu_merchant_id:
|
||||
return False
|
||||
return self.settings.get('_enabled', as_type=bool) and self.settings.get('method_{}'.format(self.method),
|
||||
as_type=bool)
|
||||
|
||||
@@ -588,6 +591,9 @@ class PaypalMethod(BasePaymentProvider):
|
||||
raise PaymentException(_('We were unable to process your payment. See below for details on how to '
|
||||
'proceed.'))
|
||||
|
||||
if self.settings.connect_client_id and self.settings.connect_secret_key and not self.settings.secret:
|
||||
if not self.settings.isu_merchant_id:
|
||||
raise PaymentException('Payment method misconfigured')
|
||||
self.init_api()
|
||||
try:
|
||||
req = OrdersGetRequest(request.session.get('payment_paypal_oid'))
|
||||
|
||||
@@ -199,7 +199,7 @@ def isu_return(request, *args, **kwargs):
|
||||
if not any(k in request.GET for k in getparams) or not any(k in request.session for k in sessionparams):
|
||||
messages.error(request, _('An error occurred returning from PayPal: request parameters missing. Please try again.'))
|
||||
missing_getparams = set(getparams) - set(request.GET)
|
||||
missing_sessionparams = set(sessionparams) - set(request.session)
|
||||
missing_sessionparams = {p for p in sessionparams if p not in request.session}
|
||||
logger.exception('PayPal2 - Missing params in GET {} and/or Session {}'.format(missing_getparams, missing_sessionparams))
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
@@ -211,7 +211,7 @@ def isu_return(request, *args, **kwargs):
|
||||
try:
|
||||
cache.incr('pretix_paypal_token_hash_cycle')
|
||||
except ValueError:
|
||||
cache.set('pretix_paypal_token_hash_cycle', 0)
|
||||
cache.set('pretix_paypal_token_hash_cycle', 1, None)
|
||||
|
||||
gs = GlobalSettingsObject()
|
||||
prov = Paypal(event)
|
||||
@@ -376,7 +376,7 @@ def webhook(request, *args, **kwargs):
|
||||
prov.init_api()
|
||||
|
||||
try:
|
||||
if rso:
|
||||
if rso and 'id' in rso.payment.info_data:
|
||||
payloadid = rso.payment.info_data['id']
|
||||
sale = prov.client.execute(pp_orders.OrdersGetRequest(payloadid)).result
|
||||
except IOError:
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, false, false)">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, false, false, true)">
|
||||
{{ $root.strings['modal.continue'] }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" @click="showUnpaidModal = false">
|
||||
@@ -188,7 +188,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, true)">
|
||||
<button type="button" class="btn btn-primary pull-right" @click="check(checkResult.position.secret, true, true, true)">
|
||||
{{ $root.strings['modal.continue'] }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-default" @click="showQuestionsModal = false">
|
||||
@@ -296,7 +296,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
selectResult(res) {
|
||||
this.check(res.id, false, false, false)
|
||||
this.check(res.id, false, false, false, false)
|
||||
},
|
||||
answerSetM(qid, opid, checked) {
|
||||
let arr = this.answers[qid] ? this.answers[qid].split(',') : [];
|
||||
@@ -320,7 +320,7 @@ export default {
|
||||
this.showQuestionsModal = false
|
||||
this.answers = {}
|
||||
},
|
||||
check(id, ignoreUnpaid, keepAnswers, fallbackToSearch) {
|
||||
check(id, ignoreUnpaid, keepAnswers, fallbackToSearch, untrusted) {
|
||||
if (!keepAnswers) {
|
||||
this.answers = {}
|
||||
} else if (this.showQuestionsModal) {
|
||||
@@ -339,7 +339,11 @@ export default {
|
||||
this.$refs.input.blur()
|
||||
})
|
||||
|
||||
fetch(this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation', {
|
||||
let url = this.$root.api.lists + this.checkinlist.id + '/positions/' + encodeURIComponent(id) + '/redeem/?expand=item&expand=subevent&expand=variation'
|
||||
if (untrusted) {
|
||||
url += '&untrusted_input=true'
|
||||
}
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector("input[name=csrfmiddlewaretoken]").value,
|
||||
@@ -439,7 +443,7 @@ export default {
|
||||
startSearch(fallbackToScan) {
|
||||
if (this.query.length >= 32 && fallbackToScan) {
|
||||
// likely a secret, not a search result
|
||||
this.check(this.query, false, false, true)
|
||||
this.check(this.query, false, false, true, true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@ window.vapp = new Vue({
|
||||
'result.rules': gettext('Entry not allowed'),
|
||||
'result.revoked': gettext('Ticket code revoked/changed'),
|
||||
'result.canceled': gettext('Order canceled'),
|
||||
'result.ambiguous': gettext('Ticket code is ambiguous on list'),
|
||||
'status.checkin': gettext('Checked-in Tickets'),
|
||||
'status.position': gettext('Valid Tickets'),
|
||||
'status.inside': gettext('Currently inside'),
|
||||
|
||||
@@ -54,7 +54,9 @@ from lxml import html
|
||||
|
||||
from pretix.base.context import get_powered_by
|
||||
from pretix.base.i18n import language
|
||||
from pretix.base.models import CartPosition, Event, Quota, SubEvent, Voucher
|
||||
from pretix.base.models import (
|
||||
CartPosition, Event, ItemVariation, Quota, SubEvent, Voucher,
|
||||
)
|
||||
from pretix.base.services.cart import error_messages
|
||||
from pretix.base.settings import GlobalSettingsObject
|
||||
from pretix.base.templatetags.rich_text import rich_text
|
||||
@@ -222,9 +224,18 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
def _get_items(self):
|
||||
qs = self.request.event.items
|
||||
if 'items' in self.request.GET:
|
||||
qs = qs.filter(pk__in=self.request.GET.get('items').split(","))
|
||||
qs = qs.filter(pk__in=[pk for pk in self.request.GET.get('items').split(",") if pk.isdigit()])
|
||||
if 'categories' in self.request.GET:
|
||||
qs = qs.filter(category__pk__in=self.request.GET.get('categories').split(","))
|
||||
qs = qs.filter(category__pk__in=[pk for pk in self.request.GET.get('categories').split(",") if pk.isdigit()])
|
||||
variation_filter = None
|
||||
if 'variations' in self.request.GET:
|
||||
variation_filter = [int(pk) for pk in self.request.GET.get('variations').split(",") if pk.isdigit()]
|
||||
qs = qs.filter(
|
||||
pk__in=ItemVariation.objects.filter(
|
||||
item__event=self.request.event,
|
||||
pk__in=variation_filter,
|
||||
).values_list('item_id', flat=True)
|
||||
)
|
||||
|
||||
items, display_add_to_cart = get_grouped_items(
|
||||
self.request.event,
|
||||
@@ -295,7 +306,7 @@ class WidgetAPIProductList(EventListMixin, View):
|
||||
var.cached_availability[0],
|
||||
var.cached_availability[1] if item.do_show_quota_left else None
|
||||
],
|
||||
} for var in item.available_variations
|
||||
} for var in item.available_variations if (not variation_filter or var.id in variation_filter)
|
||||
]
|
||||
|
||||
} for item in g
|
||||
|
||||
2411
src/pretix/static/npm_dir/package-lock.json
generated
2411
src/pretix/static/npm_dir/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,13 @@
|
||||
"private": true,
|
||||
"scripts": {},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.18.2",
|
||||
"@babel/preset-env": "^7.18.2",
|
||||
"@babel/core": "^7.18.6",
|
||||
"@babel/preset-env": "^7.18.6",
|
||||
"@rollup/plugin-babel": "^5.3.1",
|
||||
"@rollup/plugin-node-resolve": "^13.3.0",
|
||||
"vue": "^2.6.14",
|
||||
"rollup": "^2.75.5",
|
||||
"vue": "^2.7.0",
|
||||
"rollup": "^2.75.7",
|
||||
"rollup-plugin-vue": "^5.0.1",
|
||||
"vue-template-compiler": "^2.6.14"
|
||||
"vue-template-compiler": "^2.7.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,6 +116,7 @@ ignore =
|
||||
tests/plugins/badges/*
|
||||
tests/plugins/banktransfer/*
|
||||
tests/plugins/paypal/*
|
||||
tests/plugins/paypal2/*
|
||||
tests/plugins/pretixdroid/*
|
||||
tests/plugins/stripe/*
|
||||
tests/plugins/sendmail/*
|
||||
|
||||
@@ -204,7 +204,7 @@ setup(
|
||||
'packaging',
|
||||
'paypalrestsdk==1.13.*',
|
||||
'paypal-checkout-serversdk==1.0.*',
|
||||
'PyJWT==2.0.*',
|
||||
'PyJWT==2.4.*',
|
||||
'phonenumberslite==8.12.*',
|
||||
'Pillow==9.1.*',
|
||||
'protobuf==3.19.*',
|
||||
@@ -237,7 +237,7 @@ setup(
|
||||
'dev': [
|
||||
'coverage',
|
||||
'coveralls',
|
||||
'django-debug-toolbar==3.2.*',
|
||||
'django-debug-toolbar==3.5.*',
|
||||
'flake8==4.0.*',
|
||||
'freezegun',
|
||||
'isort==5.10.*',
|
||||
|
||||
@@ -70,7 +70,7 @@ def order(event, item, other_item, taxrule):
|
||||
total=46, locale='en'
|
||||
)
|
||||
InvoiceAddress.objects.create(order=o, company="Sample company", country=Country('NZ'))
|
||||
OrderPosition.objects.create(
|
||||
op1 = OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=1,
|
||||
item=item,
|
||||
@@ -90,6 +90,16 @@ def order(event, item, other_item, taxrule):
|
||||
secret="sf4HZG73fU6kwddgjg2QOusFbYZwVKpK",
|
||||
pseudonymization_id="BACDEFGHKL",
|
||||
)
|
||||
OrderPosition.objects.create(
|
||||
order=o,
|
||||
positionid=3,
|
||||
item=other_item,
|
||||
addon_to=op1,
|
||||
variation=None,
|
||||
price=Decimal("0"),
|
||||
secret="3u4ez6vrrbgb3wvezxhq446p548dt2wn",
|
||||
pseudonymization_id="FOOBAR12345",
|
||||
)
|
||||
return o
|
||||
|
||||
|
||||
@@ -157,6 +167,38 @@ TEST_ORDERPOSITION2_RES = {
|
||||
"pseudonymization_id": "BACDEFGHKL",
|
||||
}
|
||||
|
||||
TEST_ORDERPOSITION3_RES = {
|
||||
"id": 3,
|
||||
"require_attention": False,
|
||||
"order__status": "p",
|
||||
"order": "FOO",
|
||||
"positionid": 3,
|
||||
"item": 1,
|
||||
"variation": None,
|
||||
"price": "0.00",
|
||||
"attendee_name": "Peter",
|
||||
"attendee_name_parts": {'full_name': "Peter"},
|
||||
"attendee_email": None,
|
||||
"voucher": None,
|
||||
"tax_rate": "0.00",
|
||||
"tax_value": "0.00",
|
||||
"tax_rule": None,
|
||||
"secret": "3u4ez6vrrbgb3wvezxhq446p548dt2wn",
|
||||
"addon_to": None,
|
||||
"checkins": [],
|
||||
"downloads": [],
|
||||
"answers": [],
|
||||
"seat": None,
|
||||
"company": None,
|
||||
"street": None,
|
||||
"zipcode": None,
|
||||
"city": None,
|
||||
"country": None,
|
||||
"state": None,
|
||||
"subevent": None,
|
||||
"pseudonymization_id": "FOOBAR12345",
|
||||
}
|
||||
|
||||
TEST_LIST_RES = {
|
||||
"name": "Default",
|
||||
"all_products": False,
|
||||
@@ -168,6 +210,7 @@ TEST_LIST_RES = {
|
||||
"allow_entry_after_exit": True,
|
||||
"subevent": None,
|
||||
"exit_all_at": None,
|
||||
"addon_match": False,
|
||||
"rules": {}
|
||||
}
|
||||
|
||||
@@ -396,27 +439,31 @@ def test_list_update(token_client, organizer, event, clist):
|
||||
def test_list_all_items_positions(token_client, organizer, event, clist, clist_all, item, other_item, order):
|
||||
with scopes_disabled():
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
p1["id"] = order.positions.first().pk
|
||||
p1["id"] = order.positions.get(positionid=1).pk
|
||||
p1["item"] = item.pk
|
||||
p2 = dict(TEST_ORDERPOSITION2_RES)
|
||||
p2["id"] = order.positions.last().pk
|
||||
p2["id"] = order.positions.get(positionid=2).pk
|
||||
p2["item"] = other_item.pk
|
||||
p3 = dict(TEST_ORDERPOSITION3_RES)
|
||||
p3["id"] = order.positions.get(positionid=3).pk
|
||||
p3["item"] = other_item.pk
|
||||
p3["addon_to"] = p1["id"]
|
||||
|
||||
# All items
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert [p1, p2, p3] == resp.data['results']
|
||||
|
||||
# Check-ins on other list ignored
|
||||
with scopes_disabled():
|
||||
order.positions.first().checkins.create(list=clist)
|
||||
c = order.positions.get(positionid=1).checkins.create(list=clist)
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert [p1, p2, p3] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?has_checkin=1'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
@@ -425,7 +472,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
|
||||
# Only checked in
|
||||
with scopes_disabled():
|
||||
c = order.positions.first().checkins.create(list=clist_all)
|
||||
c = order.positions.get(positionid=1).checkins.create(list=clist_all)
|
||||
p1['checkins'] = [
|
||||
{
|
||||
'id': c.pk,
|
||||
@@ -448,7 +495,7 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2] == resp.data['results']
|
||||
assert [p2, p3] == resp.data['results']
|
||||
|
||||
# Order by checkin
|
||||
resp = token_client.get(
|
||||
@@ -456,18 +503,18 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert resp.data['results'][0] == p1
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=last_checked_in'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2, p1] == resp.data['results']
|
||||
assert resp.data['results'][-1] == p1
|
||||
|
||||
# Order by checkin date
|
||||
time.sleep(1)
|
||||
with scopes_disabled():
|
||||
c = order.positions.last().checkins.create(list=clist_all)
|
||||
c = order.positions.get(positionid=2).checkins.create(list=clist_all)
|
||||
p2['checkins'] = [
|
||||
{
|
||||
'id': c.pk,
|
||||
@@ -480,23 +527,23 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
}
|
||||
]
|
||||
resp = token_client.get(
|
||||
'/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-last_checked_in'.format(
|
||||
'/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-last_checked_in,positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2, p1] == resp.data['results']
|
||||
assert [p2, p1, p3] == resp.data['results']
|
||||
|
||||
# Order by attendee_name
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-attendee_name'.format(
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=-attendee_name,positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=attendee_name'.format(
|
||||
assert [p1, p3, p2] == resp.data['results']
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=attendee_name,positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p2, p1] == resp.data['results']
|
||||
assert [p2, p1, p3] == resp.data['results']
|
||||
|
||||
# Paid only
|
||||
order.status = Order.STATUS_PENDING
|
||||
@@ -513,32 +560,41 @@ def test_list_all_items_positions(token_client, organizer, event, clist, clist_a
|
||||
assert resp.status_code == 200
|
||||
p1['order__status'] = 'n'
|
||||
p2['order__status'] = 'n'
|
||||
assert [p2, p1] == resp.data['results']
|
||||
p3['order__status'] = 'n'
|
||||
assert [p2, p1, p3] == resp.data['results']
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_all_items_positions_by_subevent(token_client, organizer, event, clist, clist_all, item, other_item, order, subevent):
|
||||
with scopes_disabled():
|
||||
se2 = event.subevents.create(name="Foobar", date_from=datetime.datetime(2017, 12, 27, 10, 0, 0, tzinfo=UTC))
|
||||
pfirst = order.positions.first()
|
||||
pfirst = order.positions.get(positionid=1)
|
||||
pfirst.subevent = se2
|
||||
pfirst.save()
|
||||
p1 = dict(TEST_ORDERPOSITION1_RES)
|
||||
p1["id"] = pfirst.pk
|
||||
p1["subevent"] = se2.pk
|
||||
p1["item"] = item.pk
|
||||
plast = order.positions.last()
|
||||
plast.subevent = subevent
|
||||
plast.save()
|
||||
psecond = order.positions.get(positionid=2)
|
||||
psecond.subevent = subevent
|
||||
psecond.save()
|
||||
p2 = dict(TEST_ORDERPOSITION2_RES)
|
||||
p2["id"] = plast.pk
|
||||
p2["id"] = psecond.pk
|
||||
p2["item"] = other_item.pk
|
||||
p2["subevent"] = subevent.pk
|
||||
pthird = order.positions.get(positionid=3)
|
||||
pthird.subevent = se2
|
||||
pthird.save()
|
||||
p3 = dict(TEST_ORDERPOSITION3_RES)
|
||||
p3["id"] = pthird.pk
|
||||
p3["addon_to"] = pfirst.pk
|
||||
p3["item"] = other_item.pk
|
||||
p3["subevent"] = se2.pk
|
||||
resp = token_client.get('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/?ordering=positionid'.format(
|
||||
organizer.slug, event.slug, clist_all.pk
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert [p1, p2] == resp.data['results']
|
||||
assert [p1, p2, p3] == resp.data['results']
|
||||
|
||||
clist_all.subevent = subevent
|
||||
clist_all.save()
|
||||
@@ -593,7 +649,7 @@ def test_status(token_client, organizer, event, clist_all, item, other_item, ord
|
||||
))
|
||||
assert resp.status_code == 200
|
||||
assert resp.data['checkin_count'] == 1
|
||||
assert resp.data['position_count'] == 2
|
||||
assert resp.data['position_count'] == 3
|
||||
assert resp.data['inside_count'] == 1
|
||||
assert resp.data['items'] == [
|
||||
{
|
||||
@@ -622,7 +678,7 @@ def test_status(token_client, organizer, event, clist_all, item, other_item, ord
|
||||
'id': other_item.pk,
|
||||
'checkin_count': 0,
|
||||
'admission': False,
|
||||
'position_count': 1,
|
||||
'position_count': 2,
|
||||
'variations': []
|
||||
}
|
||||
]
|
||||
@@ -1185,12 +1241,14 @@ def test_redeem_unknown_revoked_force(token_client, organizer, clist, event, ord
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["status"] == "ok"
|
||||
with scopes_disabled():
|
||||
assert Checkin.objects.last().forced
|
||||
assert Checkin.objects.last().force_sent
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.forced
|
||||
assert ci.force_sent
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clist, event, order):
|
||||
def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clist, event):
|
||||
device.software_brand = "pretixSCAN"
|
||||
device.software_version = "1.11.1"
|
||||
device.save()
|
||||
@@ -1199,7 +1257,6 @@ def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clis
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
print(resp.data)
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "already_redeemed"
|
||||
@@ -1219,3 +1276,121 @@ def test_redeem_unknown_legacy_device_bug(device, device_client, organizer, clis
|
||||
assert resp.data["reason"] == "invalid"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_by_id_not_allowed_if_pretixscan(device, device_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
device.software_brand = "pretixSCAN"
|
||||
device.software_version = "1.14.2"
|
||||
device.save()
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 404
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.secret
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_by_id_not_allowed_if_untrusted(device, device_client, organizer, clist, event, order):
|
||||
with scopes_disabled():
|
||||
p = order.positions.first()
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/?untrusted_input=true'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.pk
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 404
|
||||
resp = device_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/?untrusted_input=true'.format(
|
||||
organizer.slug, event.slug, clist.pk, p.secret
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_disabled(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w'
|
||||
), {
|
||||
}, format='json')
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "product"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_enabled(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
p = order.positions.first().addons.all().first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w'
|
||||
), {
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
assert resp.data['status'] == 'ok'
|
||||
assert resp.data['position']['attendee_name'] == 'Peter' # test propagation of names
|
||||
assert resp.data['position']['item'] == other_item.pk
|
||||
with scopes_disabled():
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.position == p
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_ambiguous(token_client, organizer, clist, item, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([item, other_item])
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'z3fsn8jyufm5kpk768q69gkbyr5f4h6w'
|
||||
), {
|
||||
}, format='json')
|
||||
assert resp.status_code == 400
|
||||
assert resp.data["status"] == "error"
|
||||
assert resp.data["reason"] == "ambiguous"
|
||||
with scopes_disabled():
|
||||
assert not Checkin.objects.last()
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_redeem_addon_if_match_and_revoked_force(token_client, organizer, clist, other_item, event, order):
|
||||
with scopes_disabled():
|
||||
event.revoked_secrets.create(position=order.positions.get(positionid=1), secret='revoked_secret')
|
||||
clist.all_products = False
|
||||
clist.addon_match = True
|
||||
clist.save()
|
||||
clist.limit_products.set([other_item])
|
||||
p = order.positions.first().addons.all().first()
|
||||
resp = token_client.post('/api/v1/organizers/{}/events/{}/checkinlists/{}/positions/{}/redeem/'.format(
|
||||
organizer.slug, event.slug, clist.pk, 'revoked_secret'
|
||||
), {
|
||||
'force': True
|
||||
}, format='json')
|
||||
assert resp.status_code == 201
|
||||
assert resp.data["status"] == "ok"
|
||||
with scopes_disabled():
|
||||
ci = Checkin.objects.last()
|
||||
assert ci.forced
|
||||
assert ci.force_sent
|
||||
assert ci.position == p
|
||||
|
||||
@@ -290,6 +290,50 @@ class WidgetCartTest(CartTestMixin, TestCase):
|
||||
data = json.loads(response.content.decode())
|
||||
assert len(data['items_by_category']) == 0
|
||||
|
||||
def test_product_list_view_variation_filter(self):
|
||||
response = self.client.get('/%s/%s/widget/product_list?variations=%s' % (self.orga.slug, self.event.slug,
|
||||
self.shirt_red.pk))
|
||||
assert response['Access-Control-Allow-Origin'] == '*'
|
||||
data = json.loads(response.content.decode())
|
||||
assert data['items_by_category'] == [
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"require_voucher": False,
|
||||
"order_min": None,
|
||||
"max_price": "14.00",
|
||||
"price": None,
|
||||
"picture": None,
|
||||
"has_variations": 4,
|
||||
"allow_waitinglist": True,
|
||||
"description": None,
|
||||
"min_price": "12.00",
|
||||
"avail": None,
|
||||
"variations": [
|
||||
{
|
||||
"value": "Red",
|
||||
"id": self.shirt_red.pk,
|
||||
'original_price': None,
|
||||
"price": {"gross": "14.00", "net": "11.76", "tax": "2.24", "name": "",
|
||||
"rate": "19.00", "includes_mixed_tax_rate": False},
|
||||
"description": None,
|
||||
"avail": [100, None],
|
||||
"order_max": 2
|
||||
}
|
||||
],
|
||||
"id": self.shirt.pk,
|
||||
"free_price": False,
|
||||
"original_price": None,
|
||||
"name": "T-Shirt",
|
||||
"order_max": None
|
||||
}
|
||||
],
|
||||
"description": None,
|
||||
"id": self.category.pk,
|
||||
"name": "Everything"
|
||||
}
|
||||
]
|
||||
|
||||
def test_product_list_view_with_voucher(self):
|
||||
with scopes_disabled():
|
||||
self.event.vouchers.create(item=self.ticket, code="ABCDE")
|
||||
|
||||
Reference in New Issue
Block a user